Files
homebox/frontend/components/ui/dialog-provider/utils.ts
Tonya 6cd9e2779f Use Tanstack table for Selectable Table, quick actions (#998)
* feat: implement example of data table

* feat: load item data into table

* chore: begin switching dialogs

* feat: implement old dialog for controlling headers and page size

* feat: get table into relatively usable state

* feat: enhance dropdown actions for multi-selection and CSV download

* feat: enhance table cell and dropdown button styles for better usability

* feat: json download for table

* feat: add expanded row component for item details in data table

* chore: add translation support

* feat: restore table on home page

* fix: oops need ids

* feat: move card view to use tanstack to allow for pagination

* feat: switch the items search to use ItemViewSelectable

* fix: update pagination handling and improve button click logic

* feat: improve selectable table

* feat: add indeterminate to checkbox

* feat: overhaul maintenance dialog to use new system and add maintenance options to table

* feat: add label ids and location id to item patch api

* feat: change location and labels in table view

* feat: add quick actions preference and enable toggle in table settings

* fix: lint

* fix: remove sized 1 pages

* fix: attempt to fix type error

* fix: various issues

* fix: remove

* fix: refactor item fetching logic to use useAsyncData for improved reactivity and improve use confirm

* fix: sort backend issues

* fix: enhance CSV export functionality by escaping fields to prevent formula injection

* fix: put aria sort on th not button

* chore: update api types
2025-09-24 02:37:38 +01:00

183 lines
5.9 KiB
TypeScript

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<T> = {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];
type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;
type SpecifiedDialogIDs = keyof DialogParamsMap;
export type NoParamDialogIDs = Exclude<DialogID, SpecifiedDialogIDs>;
export type RequiredDialogIDs = RequiredKeys<DialogParamsMap>;
export type OptionalDialogIDs = OptionalKeys<DialogParamsMap>;
type ParamsOf<T extends DialogID> = T extends SpecifiedDialogIDs ? DialogParamsMap[T] : never;
type ResultOf<T extends DialogID> = T extends keyof DialogResultMap ? DialogResultMap[T] : void;
type OpenDialog = {
// Dialogs with no parameters
<T extends NoParamDialogIDs>(
dialogId: T,
options?: { onClose?: (result?: ResultOf<T>) => void; params?: never }
): void;
// Dialogs with required parameters
<T extends RequiredDialogIDs>(
dialogId: T,
options: { params: ParamsOf<T>; onClose?: (result?: ResultOf<T>) => void }
): void;
// Dialogs with optional parameters
<T extends OptionalDialogIDs>(
dialogId: T,
options?: { params?: ParamsOf<T>; onClose?: (result?: ResultOf<T>) => 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.
<T extends keyof DialogResultMap>(dialogId: T, result?: ResultOf<T>): void;
// Close a specific dialog that has NO defined result type.
<T extends Exclude<DialogID, keyof DialogResultMap>>(dialogId: T, result?: never): void;
<T extends DialogID>(dialogId: T): void;
};
type OpenCallback = {
<T extends NoParamDialogIDs>(dialogId: T, cb: () => void): () => void;
<T extends RequiredDialogIDs>(dialogId: T, cb: (params: ParamsOf<T>) => void): () => void;
<T extends OptionalDialogIDs>(dialogId: T, cb: (params?: ParamsOf<T>) => void): () => void;
};
export const [useDialog, provideDialogContext] = createContext<{
activeDialog: ComputedRef<DialogID | null>;
activeAlerts: ComputedRef<string[]>;
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<T extends NoParamDialogIDs | OptionalDialogIDs>(dialogId: T, key: HotkeyKey): void;
export function useDialogHotkey<T extends RequiredDialogIDs>(
dialogId: T,
key: HotkeyKey,
getParams: () => ParamsOf<T>
): 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();
}
},
});
}