diff --git a/ui/app/components/Dialog.vue b/ui/app/components/Dialog.vue new file mode 100644 index 00000000..2c4809ae --- /dev/null +++ b/ui/app/components/Dialog.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/ui/app/composables/useDialog.ts b/ui/app/composables/useDialog.ts new file mode 100644 index 00000000..8d9dd5ae --- /dev/null +++ b/ui/app/composables/useDialog.ts @@ -0,0 +1,127 @@ +import {reactive, readonly} from 'vue' + +export type DialogResult = { status: boolean; value: T } + +type BaseOptions = { + /** + * Title of the dialog + */ + title?: string + /** + * Message to display in the dialog + */ + message?: string + /** + * Text for the confirm button + */ + confirmText?: string +} + +export type PromptOptions = BaseOptions & { + /** + * Text for the input field + */ + initial?: string + /** + * Placeholder text for the input field + */ + placeholder?: string + /** + * Text for the cancel button + */ + cancelText?: string + /** + * Function to validate the input value + * @returns true if valid, or an error message string if invalid + */ + validate?: (v: string) => true | string +} + +export type ConfirmOptions = BaseOptions & { + /** + * Text for the confirm button + */ + cancelText?: string +} + +export type AlertOptions = BaseOptions & {} + +type QueueItem = { + type: 'prompt' | 'confirm' | 'alert' + opts: PromptOptions | ConfirmOptions | AlertOptions + resolve: (r: DialogResult) => void +} + +type DialogState = { + current: QueueItem | null + queue: QueueItem[] + errorMsg: string | null + input: string +} + +export const useDialog = () => { + const raw = useState('dialog:state', () => reactive({ + current: null, + queue: [], + errorMsg: null, + input: '', + } as DialogState)) + + const state = raw.value + + const _dequeue = () => { + if (!state.current && state.queue.length) { + state.current = state.queue.shift()! + state.errorMsg = null + state.input = state.current.type === 'prompt' ? (state.current.opts as PromptOptions).initial ?? '' : '' + } + } + + const promptDialog = (opts: PromptOptions) => new Promise>((resolve) => { + state.queue.push({type: 'prompt', opts, resolve}) + _dequeue() + }) + + const confirmDialog = (opts: ConfirmOptions) => new Promise>((resolve) => { + state.queue.push({type: 'confirm', opts, resolve}) + _dequeue() + }) + + const alertDialog = (opts: AlertOptions) => new Promise>((resolve) => { + state.queue.push({type: 'alert', opts, resolve}) + _dequeue() + }) + + const confirm = (value?: string) => { + if (!state.current) return + if (state.current.type === 'prompt') { + const val = value ?? state.input + const v = (state.current.opts as PromptOptions).validate?.(val) + if (v && v !== true) { + state.errorMsg = v + return + } + state.current.resolve({status: true, value: val}) + } else { + state.current.resolve({status: true, value: null}) + } + state.current = null + _dequeue() + } + + const cancel = () => { + if (!state.current) return + state.current.resolve({status: false, value: null}) + state.current = null + _dequeue() + } + + return { + promptDialog, + confirmDialog, + alertDialog, + confirm, + cancel, + state: readonly(state) as Readonly, + } +}