mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-21 13:23:14 +01:00
* 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
183 lines
5.9 KiB
TypeScript
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();
|
|
}
|
|
},
|
|
});
|
|
}
|