import { computed, type ComputedRef } from "vue"; import { createContext } from "reka-ui"; import { useMagicKeys, useActiveElement } from "@vueuse/core"; import type { BarcodeProduct, ItemSummary, MaintenanceEntry, MaintenanceEntryWithDetails } from "~~/lib/api/types/data-contracts"; export enum DialogID { AttachmentEdit = "attachment-edit", ChangePassword = "changePassword", CreateItem = "create-item", CreateLocation = "create-location", CreateLabel = "create-label", CreateNotifier = "create-notifier", DuplicateSettings = "duplicate-settings", DuplicateTemporarySettings = "duplicate-temporary-settings", EditMaintenance = "edit-maintenance", Import = "import", ItemImage = "item-image", ItemTableSettings = "item-table-settings", PrintLabel = "print-label", ProductImport = "product-import", QuickMenu = "quick-menu", Scanner = "scanner", PageQRCode = "page-qr-code", UpdateLabel = "update-label", UpdateLocation = "update-location", ItemChangeDetails = "item-table-updater", } /** * - Keys present without ? => params required * - Keys present with ? => params optional * - Keys not present => no params allowed */ export type DialogParamsMap = { [DialogID.ItemImage]: ( | { type: "preloaded"; originalSrc: string; originalType?: string; thumbnailSrc?: string; } | { type: "attachment"; mimeType: string; thumbnailId?: string; } ) & { itemId: string; attachmentId: string; }; [DialogID.CreateItem]?: { product?: BarcodeProduct }; [DialogID.ProductImport]?: { barcode?: string }; [DialogID.EditMaintenance]: | { type: "create"; itemId: string | string[] } | { type: "update"; maintenanceEntry: MaintenanceEntry | MaintenanceEntryWithDetails } | { type: "duplicate"; maintenanceEntry: MaintenanceEntry | MaintenanceEntryWithDetails; itemId: string }; [DialogID.ItemChangeDetails]: { items: ItemSummary[]; changeLocation?: boolean; addLabels?: boolean; removeLabels?: boolean; }; }; /** * Defines the payload type for a dialog's onClose callback. */ export type DialogResultMap = { [DialogID.ItemImage]?: { action: "delete"; id: string }; [DialogID.EditMaintenance]?: boolean; [DialogID.ItemChangeDetails]?: boolean; }; /** Helpers to split IDs by requirement */ type OptionalKeys = { // eslint-disable-next-line @typescript-eslint/no-empty-object-type [K in keyof T]-?: {} extends Pick ? K : never; }[keyof T]; type RequiredKeys = Exclude>; type SpecifiedDialogIDs = keyof DialogParamsMap; export type NoParamDialogIDs = Exclude; export type RequiredDialogIDs = RequiredKeys; export type OptionalDialogIDs = OptionalKeys; type ParamsOf = T extends SpecifiedDialogIDs ? DialogParamsMap[T] : never; type ResultOf = T extends keyof DialogResultMap ? DialogResultMap[T] : void; type OpenDialog = { // Dialogs with no parameters ( dialogId: T, options?: { onClose?: (result?: ResultOf) => void; params?: never } ): void; // Dialogs with required parameters ( dialogId: T, options: { params: ParamsOf; onClose?: (result?: ResultOf) => void } ): void; // Dialogs with optional parameters ( dialogId: T, options?: { params?: ParamsOf; onClose?: (result?: ResultOf) => void } ): void; }; type CloseDialog = { // Close the currently active dialog, no ID specified. No result payload. (): void; // Close a specific dialog that has a defined result type. (dialogId: T, result?: ResultOf): void; // Close a specific dialog that has NO defined result type. >(dialogId: T, result?: never): void; (dialogId: T): void; }; type OpenCallback = { (dialogId: T, cb: () => void): () => void; (dialogId: T, cb: (params: ParamsOf) => void): () => void; (dialogId: T, cb: (params?: ParamsOf) => void): () => void; }; export const [useDialog, provideDialogContext] = createContext<{ activeDialog: ComputedRef; activeAlerts: ComputedRef; registerOpenDialogCallback: OpenCallback; openDialog: OpenDialog; closeDialog: CloseDialog; addAlert: (alertId: string) => void; removeAlert: (alertId: string) => void; }>("DialogProvider"); /** * Hotkey helper: * - No/optional params: pass dialogId + key * - Required params: pass dialogId + key + getParams() */ type HotkeyKey = { shift?: boolean; ctrl?: boolean; code: string; }; export function useDialogHotkey(dialogId: T, key: HotkeyKey): void; export function useDialogHotkey( dialogId: T, key: HotkeyKey, getParams: () => ParamsOf ): void; export function useDialogHotkey(dialogId: DialogID, key: HotkeyKey, getParams?: () => unknown) { const { openDialog } = useDialog(); const activeElement = useActiveElement(); const notUsingInput = computed( () => activeElement.value?.tagName !== "INPUT" && activeElement.value?.tagName !== "TEXTAREA" ); useMagicKeys({ passive: false, onEventFired: event => { if ( notUsingInput.value && event.type === "keydown" && event.code === key.code && (key.shift === undefined || event.shiftKey === key.shift) && (key.ctrl === undefined || event.ctrlKey === key.ctrl) ) { if (getParams) { openDialog(dialogId as RequiredDialogIDs, { params: getParams() as never, }); } else { openDialog(dialogId as NoParamDialogIDs); } event.preventDefault(); } }, }); }