libraries/library menus

This commit is contained in:
Gauthier Roebroeck 2025-12-15 15:05:40 +08:00
parent 7c205ebc2f
commit 6a73408c3c
18 changed files with 698 additions and 26 deletions

View file

@ -90,3 +90,75 @@ export const useUpdateLibrary = defineMutation(() => {
},
})
})
export const useDeleteLibrary = defineMutation(() => {
const queryCache = useQueryCache()
return useMutation({
mutation: (libraryId: string) =>
komgaClient.DELETE('/api/v1/libraries/{libraryId}', {
params: {
path: {
libraryId: libraryId,
},
},
}),
onSuccess: () => {
void queryCache.invalidateQueries({ key: QUERY_KEYS_LIBRARIES.root })
},
})
})
export const useRefreshMetadataLibrary = defineMutation(() =>
useMutation({
mutation: (libraryId: string) =>
komgaClient.POST('/api/v1/libraries/{libraryId}/metadata/refresh', {
params: {
path: {
libraryId: libraryId,
},
},
}),
}),
)
export const useEmptyTrashLibrary = defineMutation(() =>
useMutation({
mutation: (libraryId: string) =>
komgaClient.POST('/api/v1/libraries/{libraryId}/empty-trash', {
params: {
path: {
libraryId: libraryId,
},
},
}),
}),
)
export const useScanLibrary = defineMutation(() =>
useMutation({
mutation: ({ libraryId, deep = false }: { libraryId: string; deep?: boolean }) =>
komgaClient.POST('/api/v1/libraries/{libraryId}/scan', {
params: {
path: {
libraryId: libraryId,
},
query: {
deep: deep,
},
},
}),
}),
)
export const useAnalyzeLibrary = defineMutation(() =>
useMutation({
mutation: (libraryId: string) =>
komgaClient.POST('/api/v1/libraries/{libraryId}/analyze', {
params: {
path: {
libraryId: libraryId,
},
},
}),
}),
)

View file

@ -54,6 +54,7 @@ declare module 'vue' {
LayoutAppDrawerMenuMedia: typeof import('./components/layout/app/drawer/menu/Media.vue')['default']
LayoutAppDrawerMenuServer: typeof import('./components/layout/app/drawer/menu/Server.vue')['default']
LayoutAppDrawerReorderLibraries: typeof import('./components/layout/app/drawer/ReorderLibraries.vue')['default']
LibraryDeletionWarning: typeof import('./components/library/DeletionWarning.vue')['default']
LibraryFormCreateEdit: typeof import('./components/library/form/CreateEdit.vue')['default']
LibraryFormStepGeneral: typeof import('./components/library/form/StepGeneral.vue')['default']
LibraryFormStepMetadata: typeof import('./components/library/form/StepMetadata.vue')['default']

View file

@ -0,0 +1,14 @@
import { Meta } from '@storybook/addon-docs/blocks';
import * as Stories from './Confirm.stories';
<Meta of={Stories} />
# DialogConfirm
A configurable confirmation dialog.
Confirmation can be one of:
- click the confirm button
- check a checkbox, then click the confirm button
- type a validation text, then click the confirm button

View file

@ -0,0 +1,57 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Confirm from './Confirm.vue'
import { fn } from 'storybook/test'
const meta = {
component: Confirm,
render: (args: object) => ({
components: { Confirm },
setup() {
return { args }
},
template: '<Confirm v-bind="args"/>',
}),
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
},
args: {
dialog: true,
onConfirm: fn(),
},
} satisfies Meta<typeof Confirm>
export default meta
type Story = StoryObj<typeof meta>
export const Click: Story = {
args: {
...meta.args,
mode: 'click',
},
}
export const Checkbox: Story = {
args: {
...meta.args,
mode: 'checkbox',
color: 'warning',
},
}
export const TextInput: Story = {
args: {
...meta.args,
mode: 'textinput',
},
}
export const TextInputCustomText: Story = {
args: {
...meta.args,
title: 'dialog title',
subtitle: 'dialog subtitle',
validateText: "let's go",
mode: 'textinput',
},
}

View file

@ -24,6 +24,7 @@
<slot name="warning" />
<slot name="text">
<FormattedMessage
v-if="mode === 'textinput'"
:message-descriptor="
defineMessage({
description: 'Confirmation dialog: default hint to retype validation text',
@ -31,7 +32,9 @@
id: 'XnidLu',
})
"
:values="{ validateText: validateText }"
:values="{
validateText: validateTextEffective,
}"
>
<template #b="Content">
<span class="font-weight-bold">
@ -42,11 +45,26 @@
</slot>
<v-text-field
:rules="[['sameAsIgnoreCase', validateText]]"
v-if="mode === 'textinput'"
:rules="[['sameAsIgnoreCase', validateTextEffective]]"
hide-details
class="mt-2"
autofocus
/>
<v-checkbox
v-if="mode === 'checkbox'"
:rules="['required']"
hide-details
:color="colorEffective"
:label="
checkboxLabel ||
$formatMessage({
description: 'Confirmation dialog: default checkbox label',
defaultMessage: 'Click to confirm',
id: '3rNj7/',
})
"
/>
</template>
<template #actions>
@ -63,19 +81,19 @@
/>
<v-btn
:loading="loading"
:disabled="!formValid"
:disabled="mode !== 'click' && !formValid"
:text="
okText ||
$formatMessage({
description: 'Confirmation dialog: Confirm button',
description: 'Confirmation dialog: default confirm button',
defaultMessage: 'Confirm',
id: '33t+CB',
id: 'ddthL2',
})
"
type="submit"
variant="elevated"
rounded="xs"
color="error"
:color="colorEffective"
/>
</template>
</v-card>
@ -85,9 +103,11 @@
</template>
<script setup lang="ts">
import { defineMessage } from 'vue-intl'
import { defineMessage, useIntl } from 'vue-intl'
import type { DialogConfirmProps } from '@/types/dialog'
const intl = useIntl()
const showDialog = defineModel<boolean>('dialog', { required: false })
const emit = defineEmits<{
confirm: []
@ -104,11 +124,26 @@ async function submitForm(isActive: Ref<boolean, boolean>) {
}
}
const validateTextEffective = computed(
() =>
validateText ||
intl.formatMessage({
description: 'Confirmation dialog: default validation text',
defaultMessage: 'confirm',
id: 'j7CGMQ',
}),
)
const colorEffective = computed(() => color || 'error')
const {
title = undefined,
subtitle = undefined,
okText = undefined,
validateText = 'confirm',
validateText = undefined,
checkboxLabel = undefined,
mode = 'click',
color = undefined,
maxWidth = undefined,
activator = undefined,
loading = false,

View file

@ -3,16 +3,17 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Drawer from './Drawer.vue'
import { useAppStore } from '@/stores/app'
import DialogConfirmEditInstance from '@/components/dialog/ConfirmEditInstance.vue'
import DialogConfirmInstance from '@/components/dialog/ConfirmInstance.vue'
import SnackQueue from '@/components/SnackQueue.vue'
const meta = {
component: Drawer,
render: (args: object) => ({
components: { Drawer, DialogConfirmEditInstance, SnackQueue },
components: { Drawer, DialogConfirmEditInstance, DialogConfirmInstance, SnackQueue },
setup() {
return { args }
},
template: '<Drawer /><DialogConfirmEditInstance/><SnackQueue/>',
template: '<Drawer /><DialogConfirmEditInstance/><DialogConfirmInstance/><SnackQueue/>',
}),
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout

View file

@ -8,6 +8,7 @@ import { CLIENT_SETTING_USER, type ClientSettingUserLibrary } from '@/types/Clie
import type { components } from '@/generated/openapi/komga'
import { VList } from 'vuetify/components'
import DialogConfirmEditInstance from '@/components/dialog/ConfirmEditInstance.vue'
import DialogConfirmInstance from '@/components/dialog/ConfirmInstance.vue'
import SnackQueue from '@/components/SnackQueue.vue'
import { delay, http } from 'msw'
import { response401Unauthorized } from '@/mocks/api/handlers'
@ -15,11 +16,12 @@ import { response401Unauthorized } from '@/mocks/api/handlers'
const meta = {
component: Libraries,
render: (args: object) => ({
components: { Libraries, VList, DialogConfirmEditInstance, SnackQueue },
components: { Libraries, VList, DialogConfirmEditInstance, DialogConfirmInstance, SnackQueue },
setup() {
return { args }
},
template: '<v-list nav><Libraries /></v-list><DialogConfirmEditInstance/><SnackQueue/>',
template:
'<v-list nav><Libraries /></v-list><DialogConfirmEditInstance/><DialogConfirmInstance/><SnackQueue/>',
}),
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout

View file

@ -20,7 +20,9 @@
id: '90yqRq',
})
"
@mouseenter="dialogConfirmEdit.activator = $event.currentTarget"
@mouseenter="
(event: Event) => (dialogConfirmEdit.activator = event.currentTarget as Element)
"
@click="createLibrary"
/>
<v-icon-btn

View file

@ -0,0 +1,11 @@
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
import * as Stories from './DeletionWarning.stories';
<Meta of={Stories} />
# LibraryDeletionWarning
Warning shown within a confirmation dialog before deleting a particular library.
<Canvas of={Stories.Default} />

View file

@ -0,0 +1,25 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import DeletionWarning from './DeletionWarning.vue'
const meta = {
component: DeletionWarning,
render: (args: object) => ({
components: { DeletionWarning },
setup() {
return { args }
},
template: '<DeletionWarning />',
}),
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
},
args: {},
} satisfies Meta<typeof DeletionWarning>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View file

@ -0,0 +1,36 @@
<template>
<v-alert
type="warning"
variant="tonal"
class="mb-4"
>
<FormattedMessage :message-descriptor="message">
<template #ul="Content">
<ul class="ps-8">
<component :is="Content" />
</ul>
</template>
<template #li="Content">
<li>
<component :is="Content" />
</li>
</template>
<template #b="Content">
<div class="font-weight-bold mt-4">
<component :is="Content" />
</div>
</template>
</FormattedMessage>
</v-alert>
</template>
<script setup lang="ts">
import { defineMessage } from 'vue-intl'
const message = defineMessage({
description: 'Library deletion warning notice',
defaultMessage:
'The library will be deleted from this server.<ul><li>Series and books will be deleted, along with their metadata and read progress.</li><li>Media files will not be deleted from disk.</li></ul><b>This action cannot be undone.</b>',
id: 'bk1tX8',
})
</script>

View file

@ -16,6 +16,11 @@
<script setup lang="ts">
import { useIntl } from 'vue-intl'
import { useAppStore } from '@/stores/app'
import { useEmptyTrashLibrary, useLibraries, useScanLibrary } from '@/colada/libraries'
import { storeToRefs } from 'pinia'
import { useDialogsStore } from '@/stores/dialogs'
import { useDisplay } from 'vuetify/framework'
import { commonMessages } from '@/utils/i18n/common-messages'
const { activatorId } = defineProps<{
activatorId: string
@ -33,7 +38,61 @@ const actions = [
}),
onClick: () => (appStore.reorderLibraries = true),
},
{
title: intl.formatMessage({
description: 'Libraries menu: scan',
defaultMessage: 'Scan all libraries',
id: 'CY8sfH',
}),
onClick: () => scanAll(),
},
{
title: intl.formatMessage({
description: 'Libraries menu: empty trash',
defaultMessage: 'Empty trash for all libraries',
id: 'CwteMk',
}),
onMouseenter: (event: Event) =>
(dialogConfirm.value.activator = event.currentTarget as Element),
onClick: () => emptyTrashAll(),
},
]
const { confirm: dialogConfirm } = storeToRefs(useDialogsStore())
const display = useDisplay()
const { data: libraries } = useLibraries()
//region Scan
const { mutate: mutateScan } = useScanLibrary()
function scanAll() {
libraries.value?.forEach((it) => mutateScan({ libraryId: it.id }))
}
//endregion
//region Empty Trash
const { mutate: mutateEmptyTrash } = useEmptyTrashLibrary()
function emptyTrashAll() {
dialogConfirm.value.dialogProps = {
title: intl.formatMessage(commonMessages.dialogEmptyTrashTitle),
maxWidth: 600,
mode: 'click',
color: 'primary',
okText: intl.formatMessage(commonMessages.dialogEmptyTrashConfirm),
closeOnSave: true,
fullscreen: display.xs.value,
}
dialogConfirm.value.slotWarning = {
component: markRaw(h('div', intl.formatMessage(commonMessages.dialogEmptyTrashNotice))),
props: {},
}
dialogConfirm.value.callback = () => {
libraries.value?.forEach((it) => mutateEmptyTrash(it.id))
}
}
//endregion
</script>
<script lang="ts"></script>

View file

@ -29,15 +29,9 @@
>
<v-list density="compact">
<v-list-item
:title="
$formatMessage({
description: 'Library menu: manage > edit',
defaultMessage: 'Edit',
id: 'n4w2CE',
})
"
@mouseenter="dialogConfirmEdit.activator = $event.currentTarget"
@click="updateLibrary()"
v-for="(action, i) in manageActions"
:key="i"
v-bind="action"
/>
</v-list>
</v-menu>
@ -50,13 +44,21 @@
import { useIntl } from 'vue-intl'
import { storeToRefs } from 'pinia'
import { useDialogsStore } from '@/stores/dialogs'
import { useUpdateLibrary } from '@/colada/libraries'
import {
useAnalyzeLibrary,
useDeleteLibrary,
useEmptyTrashLibrary,
useRefreshMetadataLibrary,
useScanLibrary,
useUpdateLibrary,
} from '@/colada/libraries'
import { useMessagesStore } from '@/stores/messages'
import CreateEdit from '@/components/library/form/CreateEdit.vue'
import type { components } from '@/generated/openapi/komga'
import { useDisplay } from 'vuetify'
import type { ErrorCause } from '@/api/komga-client'
import { commonMessages } from '@/utils/i18n/common-messages'
import LibraryDeletionWarning from '@/components/library/DeletionWarning.vue'
const { activatorId, library } = defineProps<{
activatorId: string
@ -64,11 +66,11 @@ const { activatorId, library } = defineProps<{
}>()
const intl = useIntl()
const { confirmEdit: dialogConfirmEdit } = storeToRefs(useDialogsStore())
const { confirmEdit: dialogConfirmEdit, confirm: dialogConfirm } = storeToRefs(useDialogsStore())
const messagesStore = useMessagesStore()
const display = useDisplay()
//region actions
//region Actions
const actions = [
{
title: intl.formatMessage({
@ -76,7 +78,70 @@ const actions = [
defaultMessage: 'Scan library files',
id: 'GCwZB2',
}),
// onClick: () => (appStore.reorderLibraries = true),
onClick: () => scanLibrary(),
},
]
const manageActions = [
{
title: intl.formatMessage({
description: 'Library menu: manage > edit',
defaultMessage: 'Edit',
id: 'n4w2CE',
}),
onMouseenter: (event: Event) =>
(dialogConfirmEdit.value.activator = event.currentTarget as Element),
onClick: () => updateLibrary(),
},
{
title: intl.formatMessage({
description: 'Library menu: manage > deep scan',
defaultMessage: 'Deep scan',
id: 'foSDIW',
}),
onMouseenter: (event: Event) =>
(dialogConfirm.value.activator = event.currentTarget as Element),
onClick: () => scanDeep(),
},
{
title: intl.formatMessage({
description: 'Library menu: manage > refresh all metadata',
defaultMessage: 'Refresh all metadata',
id: 'OUyleX',
}),
onMouseenter: (event: Event) =>
(dialogConfirm.value.activator = event.currentTarget as Element),
onClick: () => refreshMetadata(),
},
{
title: intl.formatMessage({
description: 'Library menu: manage > empty trash',
defaultMessage: 'Empty trash',
id: 'sdNz1F',
}),
onMouseenter: (event: Event) =>
(dialogConfirm.value.activator = event.currentTarget as Element),
onClick: () => emptyTrash(),
},
{
title: intl.formatMessage({
description: 'Library menu: manage > analyze',
defaultMessage: 'Analyze',
id: 'E5ZMyt',
}),
onMouseenter: (event: Event) =>
(dialogConfirm.value.activator = event.currentTarget as Element),
onClick: () => analyzeLibrary(),
},
{
title: intl.formatMessage({
description: 'Library menu: manage > delete',
defaultMessage: 'Delete',
id: 'LFf8QB',
}),
onMouseenter: (event: Event) =>
(dialogConfirm.value.activator = event.currentTarget as Element),
onClick: () => deleteLibrary(),
},
]
//endregion
@ -138,6 +203,206 @@ function updateLibrary() {
}
}
//endregion
//region Refresh Metadata
const { mutate: mutateRefreshMetadata } = useRefreshMetadataLibrary()
function refreshMetadata() {
dialogConfirm.value.dialogProps = {
title: intl.formatMessage({
description: 'Library refresh metadata dialog: title',
defaultMessage: 'Refresh all metadata',
id: 'xiyw1J',
}),
subtitle: library.name,
maxWidth: 600,
mode: 'click',
color: 'primary',
okText: intl.formatMessage({
description: 'Library refresh metadata dialog: confirm button',
defaultMessage: 'Refresh',
id: 'i+kSy9',
}),
closeOnSave: true,
fullscreen: display.xs.value,
}
dialogConfirm.value.slotWarning = {
component: markRaw(
h(
'div',
intl.formatMessage({
description: 'Library refresh metadata dialog: warning text',
defaultMessage:
'Refreshes metadata for all the media files in the library. Depending on your library size, this may take a long time.',
id: 'vs88Ef',
}),
),
),
props: {},
}
dialogConfirm.value.callback = () => mutateRefreshMetadata(library.id)
}
//endregion
//region Empty Trash
const { mutate: mutateEmptyTrash } = useEmptyTrashLibrary()
function emptyTrash() {
dialogConfirm.value.dialogProps = {
title: intl.formatMessage(commonMessages.dialogEmptyTrashTitle),
subtitle: library.name,
maxWidth: 600,
mode: 'click',
color: 'primary',
okText: intl.formatMessage(commonMessages.dialogEmptyTrashConfirm),
closeOnSave: true,
fullscreen: display.xs.value,
}
dialogConfirm.value.slotWarning = {
component: markRaw(h('div', intl.formatMessage(commonMessages.dialogEmptyTrashNotice))),
props: {},
}
dialogConfirm.value.callback = () => mutateEmptyTrash(library.id)
}
//endregion
//region Analyze
const { mutate: mutateAnalyze } = useAnalyzeLibrary()
function analyzeLibrary() {
dialogConfirm.value.dialogProps = {
title: intl.formatMessage({
description: 'Library analyze dialog: title',
defaultMessage: 'Analyze library',
id: '5JGOUU',
}),
subtitle: library.name,
maxWidth: 600,
mode: 'click',
color: 'primary',
okText: intl.formatMessage({
description: 'Library analyze dialog: confirm button',
defaultMessage: 'Analyze',
id: 'jN3N1Q',
}),
closeOnSave: true,
fullscreen: display.xs.value,
}
dialogConfirm.value.slotWarning = {
component: markRaw(
h(
'div',
intl.formatMessage({
description: 'Library empty trash dialog: warning text',
defaultMessage:
'Analyzes all the media files in the library. The analysis captures information about the media. Depending on your library size, this may take a long time.',
id: '8xonXJ',
}),
),
),
props: {},
}
dialogConfirm.value.callback = () => mutateAnalyze(library.id)
}
//endregion
//region Scan
const { mutate: mutateScan } = useScanLibrary()
function scanLibrary() {
mutateScan({ libraryId: library.id })
}
function scanDeep() {
dialogConfirm.value.dialogProps = {
title: intl.formatMessage({
description: 'Library deep scan dialog: title',
defaultMessage: 'Deep scan',
id: 'hV3EW+',
}),
subtitle: library.name,
maxWidth: 600,
mode: 'click',
color: 'primary',
okText: intl.formatMessage({
description: 'Library deep scan: confirm button',
defaultMessage: 'Deep scan',
id: 'OxqfKF',
}),
closeOnSave: true,
fullscreen: display.xs.value,
}
dialogConfirm.value.slotWarning = {
component: markRaw(
h(
'div',
intl.formatMessage({
description: 'Library deep scan dialog: warning text',
defaultMessage:
'Performs a deep scan of the library files. Depending on your library size, this may take a long time.',
id: 'y3nPgO',
}),
),
),
props: {},
}
dialogConfirm.value.callback = () => mutateScan({ libraryId: library.id, deep: true })
}
//endregion
//region Delete
const { mutateAsync: mutateDelete } = useDeleteLibrary()
function deleteLibrary() {
dialogConfirm.value.dialogProps = {
title: intl.formatMessage({
description: 'Library delete dialog: title',
defaultMessage: 'Delete library',
id: '3T1ln7',
}),
subtitle: library.name,
maxWidth: 600,
mode: 'textinput',
color: 'error',
validateText: library.name,
okText: intl.formatMessage({
description: 'Library delete dialog: confirm button',
defaultMessage: 'Delete',
id: '/5Gb4y',
}),
closeOnSave: true,
fullscreen: display.xs.value,
}
dialogConfirm.value.slotWarning = {
component: markRaw(LibraryDeletionWarning),
props: {},
}
dialogConfirm.value.callback = () => {
mutateDelete(library.id)
.then(() => {
messagesStore.messages.push({
text: intl.formatMessage(
{
description: 'Snackbar notification shown upon successful library deletion',
defaultMessage: 'Library deleted: {library}',
id: 'PvKF7E',
},
{
library: library.name,
},
),
})
})
.catch((error) => {
messagesStore.messages.push({
text:
(error?.cause as ErrorCause)?.message ||
intl.formatMessage(commonMessages.networkError),
})
})
}
}
//endregion
</script>
<script lang="ts"></script>

View file

@ -96,4 +96,25 @@ export const librariesHandlers = [
return response(204).empty()
}),
httpTyped.delete('/api/v1/libraries/{libraryId}', ({ params, response }) => {
const libraryId = params['libraryId']
const existing = libraries.find((it) => it.id === libraryId)
if (!existing) {
return response.untyped(response404NotFound())
}
libraries.splice(libraries.indexOf(existing), 1)
return response(204).empty()
}),
httpTyped.post('/api/v1/libraries/{libraryId}/metadata/refresh', ({ response }) =>
response(202).empty(),
),
httpTyped.post('/api/v1/libraries/{libraryId}/analyze', ({ response }) => response(202).empty()),
httpTyped.post('/api/v1/libraries/{libraryId}/empty-trash', ({ response }) =>
response(202).empty(),
),
httpTyped.post('/api/v1/libraries/{libraryId}/scan', ({ response }) => response(202).empty()),
]

View file

@ -79,6 +79,7 @@ function showDialog(action: ACTION, apiKey?: components['schemas']['ApiKeyDto'])
subtitle: apiKey?.comment,
maxWidth: 600,
validateText: apiKey?.comment,
mode: 'textinput',
okText: intl.formatMessage({
description: 'Delete API Key dialog: confirmation button text',
defaultMessage: 'Delete',
@ -103,6 +104,7 @@ function showDialog(action: ACTION, apiKey?: components['schemas']['ApiKeyDto'])
subtitle: apiKey?.comment,
maxWidth: 600,
validateText: apiKey?.comment,
mode: 'textinput',
okText: intl.formatMessage({
description: 'Force Sync API Key dialog: confirmation button text',
defaultMessage: 'I understand',

View file

@ -154,6 +154,7 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
subtitle: user?.email,
maxWidth: 600,
validateText: user?.email,
mode: 'textinput',
okText: intl.formatMessage({
description: 'Delete user dialog: confirmation button text',
defaultMessage: 'Delete',

View file

@ -1,25 +1,77 @@
type DialogBaseProps = {
/**
* Title of the dialog.
*/
title?: string
/**
* Subtitle of the dialog.
*/
subtitle?: string
/**
* Maximum width of the dialog.
*/
maxWidth?: string | number
/**
* Activator for the dialog.
*/
activator?: Element | string
/**
* Loading indicator, applies to the dialog's card.
*/
loading?: boolean
/**
* Whether the dialog should be displayed in full screen.
*/
fullscreen?: boolean
/**
* Whether the dialog is scrollable.
*/
scrollable?: boolean
/**
* Controls the dialog's visibility.
*/
shown?: boolean
}
type DialogConfirmBaseProps = DialogBaseProps & {
/**
* Text for the confirmation button.
*/
okText?: string
/**
* Whether the dialog should close itself on save.
* If disabled, the dialog should be closed inside the callback function.
*/
closeOnSave?: boolean
}
export type DialogSimpleProps = DialogBaseProps
export type DialogConfirmProps = DialogConfirmBaseProps & {
/**
* Text that needs to be typed to validate. Only shown when `mode` is set to `textinput`.
*/
validateText?: string
/**
* Label shown next to the confirmation checkbox. Only shown when `mode` is set to `checkbox`.
*/
checkboxLabel?: string
/**
* Confirmation mechanism:
* - `textinput`: requires typing the content of 'validateText' into a text field.
* - `checkbox`: requires checking a checkbox.
* - `click`: no extra validation, just click on the confirm button of the dialog.
*/
mode?: 'textinput' | 'checkbox' | 'click'
/**
* Color used for the checkbox and confirm button. Defaults to `error`.
*/
color?: string
}
export type DialogConfirmEditProps = DialogConfirmBaseProps & {
/**
* CSS classes applied to the card.
*/
cardTextClass?: string
}

View file

@ -36,4 +36,20 @@ export const commonMessages = {
defaultMessage: 'Can consume lots of resources on large libraries or slow hardware',
id: 'uoc99F',
}),
dialogEmptyTrashTitle: defineMessage({
description: 'Library empty trash dialog: title',
defaultMessage: 'Empty trash',
id: 'ELttw/',
}),
dialogEmptyTrashConfirm: defineMessage({
description: 'Library empty trash dialog: confirm button',
defaultMessage: 'Empty trash',
id: '7M1pUf',
}),
dialogEmptyTrashNotice: defineMessage({
description: 'Library empty trash dialog: warning text',
defaultMessage:
"By default the media server doesn't remove information for media right away. This helps if a drive is temporarily disconnected. When you empty the trash for a library, all information about missing media is deleted.",
id: 'kDc7YL',
}),
}