Upgrade frontend deps, including nuxt (#982)

* feat: begin upgrading deps, still very buggy

* feat: progress

* feat: sort all type issues

* fix: sort type issues

* fix: import sonner styles

* fix: nuxt is the enemy

* fix: try sorting issue with workflows

* fix: update vitest config for dynamic import of path and defineConfig

* fix: add missing import

* fix: add time out to try and fix issues

* fix: add ui:ci:preview task for frontend build in CI mode

* fix: i was silly

* feat: add go:ci:with-frontend task for CI mode and remove ui:ci:preview from e2e workflow

* fix: update baseURL in Playwright config for local testing to use port 7745

* fix: update E2E_BASE_URL and remove wait for timeout in login test for smoother execution
This commit is contained in:
Tonya
2025-09-04 09:00:25 +01:00
committed by GitHub
parent 790352da34
commit d4e28e6f3b
103 changed files with 13314 additions and 5874 deletions

View File

@@ -47,7 +47,8 @@
import { useMediaQuery } from "@vueuse/core";
import type { DialogID } from "@/components/ui/dialog-provider/utils";
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "@/components/ui/drawer";
import { Dialog, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Dialog, DialogFooter, DialogHeader, DialogScrollContent, DialogTitle } from "@/components/ui/dialog";
import { Shortcut } from "@/components/ui/shortcut";
const isDesktop = useMediaQuery("(min-width: 768px)");

View File

@@ -51,7 +51,7 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
type Props = {
modelValue: boolean;
modelValue?: boolean;
};
const { t } = useI18n();
@@ -66,13 +66,13 @@
const api = useUserApi();
const importCsv = ref<File | null>(null);
const importCsv = ref<File | undefined>(undefined);
const importLoading = ref(false);
const importRef = ref<HTMLInputElement>();
whenever(
() => !dialog.value,
() => {
importCsv.value = null;
importCsv.value = undefined;
}
);
@@ -102,7 +102,7 @@
// Reset
dialog.value = false;
importLoading.value = false;
importCsv.value = null;
importCsv.value = undefined;
if (importRef.value) {
importRef.value.value = "";

View File

@@ -29,12 +29,12 @@
import { lt } from "semver";
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogAction,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useDialog } from "@/components/ui/dialog-provider";

View File

@@ -3,11 +3,11 @@
import { DialogID, type NoParamDialogIDs, type OptionalDialogIDs } from "@/components/ui/dialog-provider/utils";
import {
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "~/components/ui/command";
import { Shortcut } from "~/components/ui/shortcut";

View File

@@ -33,7 +33,7 @@
</ButtonGroup>
</div>
<!-- eslint-disable-next-line tailwindcss/no-custom-classname -->
<video ref="video" class="aspect-video w-full rounded-lg bg-muted shadow" poster="data:image/gif,AAAA"></video>
<video ref="video" class="aspect-video w-full rounded-lg bg-muted shadow" poster="data:image/gif,AAAA" />
<div class="mt-4">
<Select v-model="selectedSource">
<SelectTrigger class="w-full">
@@ -52,13 +52,13 @@
</template>
<script setup lang="ts">
import { ref, watch, computed } from "vue";
import { BrowserMultiFormatReader, NotFoundException, BarcodeFormat } from "@zxing/library";
import { computed, ref, watch } from "vue";
import { BarcodeFormat, BrowserMultiFormatReader, NotFoundException } from "@zxing/library";
import { useI18n } from "vue-i18n";
import { DialogID } from "@/components/ui/dialog-provider/utils";
import { Dialog, DialogHeader, DialogTitle, DialogScrollContent } from "@/components/ui/dialog";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { Dialog, DialogHeader, DialogScrollContent, DialogTitle } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Button, ButtonGroup } from "@/components/ui/button";
import MdiBarcode from "~icons/mdi/barcode";
import MdiAlertCircleOutline from "~icons/mdi/alert-circle-outline";
import { useDialog } from "@/components/ui/dialog-provider";
@@ -113,12 +113,12 @@
if (devices.length > 0) {
for (let i = 0; i < devices.length; i++) {
if (devices[i].label.toLowerCase().includes("back")) {
selectedSource.value = devices[i].deviceId;
if (devices[i]!.label.toLowerCase().includes("back")) {
selectedSource.value = devices[i]!.deviceId;
}
}
if (!selectedSource.value) {
selectedSource.value = devices[0].deviceId;
selectedSource.value = devices[0]!.deviceId;
}
} else {
errorMessage.value = t("scanner.no_sources");

View File

@@ -19,9 +19,9 @@
>
<div :data-theme="theme.value" class="w-full cursor-pointer bg-background-accent text-foreground">
<div class="grid grid-cols-5 grid-rows-3">
<div class="col-start-1 row-start-1 bg-background"></div>
<div class="col-start-1 row-start-2 bg-sidebar"></div>
<div class="col-start-1 row-start-3 bg-background-accent"></div>
<div class="col-start-1 row-start-1 bg-background" />
<div class="col-start-1 row-start-2 bg-sidebar" />
<div class="col-start-1 row-start-3 bg-background-accent" />
<div class="col-span-4 col-start-2 row-span-3 row-start-1 flex flex-col gap-1 bg-background p-2">
<div class="font-bold">{{ theme.label }}</div>
<div class="flex flex-wrap gap-1">

View File

@@ -3,7 +3,7 @@
<CardHeader v-if="$slots.title" class="px-4 py-5 sm:px-6">
<component :is="collapsable ? 'button' : 'div'" v-on="collapsable ? { click: toggle } : {}">
<h3 class="flex items-center text-lg font-medium leading-6">
<slot name="title"></slot>
<slot name="title" />
<template v-if="collapsable">
<span class="ml-2 transition-transform" :class="{ 'rotate-180': collapsed }">
<MdiChevronDown class="size-6" />
@@ -13,10 +13,10 @@
</component>
<div>
<p v-if="$slots.subtitle" class="mt-1 max-w-2xl text-sm text-gray-500">
<slot name="subtitle"></slot>
<slot name="subtitle" />
</p>
<template v-if="$slots['title-actions']">
<slot name="title-actions"></slot>
<slot name="title-actions" />
</template>
</div>
</CardHeader>

View File

@@ -2,24 +2,24 @@
<div class="flex flex-col gap-10 py-6 md:flex-row">
<div class="flex-1">
<h4 class="mb-1 text-lg font-semibold">
<slot name="title"></slot>
<slot name="title" />
</h4>
<p class="text-sm">
<slot></slot>
<slot />
</p>
</div>
<div class="flex items-center">
<template v-if="to">
<NuxtLink :to="to" :class="buttonVariants({ size: 'lg' })" class="min-w-52 grow">
<slot name="button">
<slot name="title"></slot>
<slot name="title" />
</slot>
</NuxtLink>
</template>
<template v-else>
<Button class="min-w-52 grow" size="lg" @click="$emit('action')">
<slot name="button">
<slot name="title"></slot>
<slot name="title" />
</slot>
</Button>
</template>

View File

@@ -11,7 +11,7 @@
const props = defineProps({
modelValue: {
type: String,
required: true,
required: false,
default: "",
},
label: {
@@ -81,7 +81,7 @@
:aria-label="`${t('components.color_selector.color')}: ${modelValue || t('components.color_selector.no_color_selected')}`"
role="button"
tabindex="0"
@click="$refs.colorInput.click()"
@click="($refs.colorInput as HTMLInputElement).click()"
/>
<span v-if="showHex" class="font-mono text-xs text-muted-foreground">{{
modelValue || t("components.color_selector.no_color")
@@ -122,7 +122,7 @@
:aria-label="`${t('components.color_selector.color')}: ${modelValue || t('components.color_selector.no_color_selected')}`"
role="button"
tabindex="0"
@click="$refs.colorInput.click()"
@click="($refs.colorInput as HTMLInputElement).click()"
/>
<span v-if="showHex" class="font-mono text-xs text-muted-foreground">{{
modelValue || t("components.color_selector.no_color")

View File

@@ -10,7 +10,6 @@
</template>
<script setup lang="ts">
// @ts-ignore
import VueDatePicker from "@vuepic/vue-datepicker";
import "@vuepic/vue-datepicker/dist/main.css";
import * as datelib from "~/lib/datelib/datelib";

View File

@@ -1,7 +1,6 @@
<template>
<div class="relative">
<FormTextField v-model="value" :placeholder="localizedPlaceholder" :label="localizedLabel" :type="inputType">
</FormTextField>
<FormTextField v-model="value" :placeholder="localizedPlaceholder" :label="localizedLabel" :type="inputType" />
<TooltipProvider :delay-duration="0">
<Tooltip>
<TooltipTrigger as-child>
@@ -22,7 +21,8 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import MdiEye from "~icons/mdi/eye";
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import FormTextField from "@/components/Form/TextField.vue";
const { t } = useI18n();
type Props = {

View File

@@ -2,7 +2,7 @@
<div v-if="!inline" class="flex w-full flex-col gap-1.5">
<Label :for="id" class="flex w-full px-1">
<span>{{ label }}</span>
<span class="grow"></span>
<span class="grow" />
<span :class="{ 'text-destructive': isLengthInvalid }">
{{ lengthIndicator }}
</span>
@@ -18,7 +18,7 @@
<div v-else class="sm:grid sm:grid-cols-4 sm:items-start sm:gap-4">
<Label :for="id" class="flex w-full px-1 py-2">
<span>{{ label }}</span>
<span class="grow"></span>
<span class="grow" />
<span :class="{ 'text-destructive': isLengthInvalid }">
{{ lengthIndicator }}
</span>

View File

@@ -2,7 +2,7 @@
<div v-if="!inline" class="flex w-full flex-col gap-1.5">
<Label :for="id" class="flex w-full px-1">
<span> {{ label }} </span>
<span class="grow"></span>
<span class="grow" />
<span
:class="{
'text-destructive':
@@ -26,7 +26,7 @@
<div v-else class="sm:grid sm:grid-cols-4 sm:items-start sm:gap-4">
<Label class="flex w-full px-1 py-2" :for="id">
<span> {{ label }} </span>
<span class="grow"></span>
<span class="grow" />
<span
:class="{
'text-destructive':

View File

@@ -43,7 +43,7 @@
import MdiDownload from "~icons/mdi/download";
import MdiOpenInNew from "~icons/mdi/open-in-new";
import { buttonVariants } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
const props = defineProps({
attachments: {

View File

@@ -116,6 +116,11 @@
import MdiBarcode from "~icons/mdi/barcode";
import MdiLoading from "~icons/mdi/loading";
import type { TableData } from "~/components/Item/View/Table.types";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Separator } from "@/components/ui/separator";
import BaseCard from "@/components/Base/Card.vue";
import FormTextField from "@/components/Form/TextField.vue";
const { openDialog, registerOpenDialogCallback } = useDialog();
const { t } = useI18n();

View File

@@ -32,7 +32,7 @@
{{ $t("global.archived") }}
</TooltipContent>
</Tooltip>
<div class="grow"></div>
<div class="grow" />
<Tooltip>
<TooltipTrigger>
<Badge>
@@ -62,6 +62,8 @@
import { Card } from "@/components/ui/card";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Separator } from "@/components/ui/separator";
import Markdown from "@/components/global/Markdown.vue";
import LabelChip from "@/components/Label/Chip.vue";
const api = useUserApi();

View File

@@ -190,6 +190,10 @@
import { useDialog, useDialogHotkey } from "~/components/ui/dialog-provider";
import LabelSelector from "~/components/Label/Selector.vue";
import ItemSelector from "~/components/Item/Selector.vue";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip";
import LocationSelector from "~/components/Location/Selector.vue";
import FormTextField from "~/components/Form/TextField.vue";
import FormTextArea from "~/components/Form/TextArea.vue";
interface PhotoPreview {
photoName: string;
@@ -276,8 +280,8 @@
function setPrimary(index: number) {
const primary = form.photos.findIndex(p => p.primary);
if (primary !== -1) form.photos[primary].primary = false;
if (primary !== index) form.photos[index].primary = true;
if (primary !== -1) form.photos[primary]!.primary = false;
if (primary !== index) form.photos[index]!.primary = true;
}
function previewImage(event: Event) {
@@ -306,7 +310,7 @@
let parentItemLocationId = null;
if (subItemCreate.value && itemId.value) {
const itemIdRead = typeof itemId.value === "string" ? (itemId.value as string) : itemId.value[0];
const itemIdRead = typeof itemId.value === "string" ? (itemId.value as string) : itemId.value[0]!;
const { data, error } = await api.items.get(itemIdRead);
if (error || !data) {
toast.error(t("components.item.create_modal.toast.failed_load_parent"));
@@ -376,7 +380,7 @@
loading.value = true;
if (shift.value) close = false;
if (shift?.value) close = false;
const out: ItemCreate = {
parentId: form.parentId,
@@ -440,7 +444,7 @@
function dataURLtoFile(dataURL: string, fileName: string) {
try {
const arr = dataURL.split(",");
const mimeMatch = arr[0].match(/:(.*?);/);
const mimeMatch = arr[0]!.match(/:(.*?);/);
if (!mimeMatch || !mimeMatch[1]) {
throw new Error("Invalid data URL format");
}
@@ -451,7 +455,7 @@
throw new Error("Invalid mime type, expected image");
}
const bstr = atob(arr[arr.length - 1]);
const bstr = atob(arr[arr.length - 1]!);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
@@ -500,8 +504,8 @@
// Encode image to data-uri with base64
try {
form.photos[index].fileBase64 = offScreenCanvas.toDataURL(imageType, 100);
form.photos[index].file = dataURLtoFile(form.photos[index].fileBase64, form.photos[index].photoName);
form.photos[index]!.fileBase64 = offScreenCanvas.toDataURL(imageType, 100);
form.photos[index]!.file = dataURLtoFile(form.photos[index]!.fileBase64, form.photos[index]!.photoName);
} catch (error) {
toast.error(t("components.item.create_modal.toast.rotate_process_failed"));
console.error(error);

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { buttonVariants, Button } from "@/components/ui/button";
import { Button, buttonVariants } from "@/components/ui/button";
import { useDialog } from "@/components/ui/dialog-provider";
import { DialogID } from "~/components/ui/dialog-provider/utils";
import { useConfirm } from "@/composables/use-confirm";

View File

@@ -37,7 +37,7 @@
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue";
import { computed, ref, watch } from "vue";
import { Check, ChevronsUpDown } from "lucide-vue-next";
import fuzzysort from "fuzzysort";
import { useVModel } from "@vueuse/core";
@@ -47,7 +47,6 @@
import { Label } from "~/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
import { cn } from "~/lib/utils";
import { useId } from "#imports";
const { t } = useI18n();
@@ -56,9 +55,9 @@
};
interface Props {
label: string;
modelValue: string | ItemsObject | null | undefined;
items: ItemsObject[] | string[];
label?: string;
modelValue?: string | ItemsObject | null | undefined;
items?: ItemsObject[] | string[];
itemText?: string;
itemValue?: string;
search?: string;

View File

@@ -5,6 +5,9 @@
import MdiTable from "~icons/mdi/table";
import { Badge } from "@/components/ui/badge";
import { Button, ButtonGroup } from "@/components/ui/button";
import BaseSectionHeader from "@/components/Base/SectionHeader.vue";
import ItemCard from "@/components/Item/Card.vue";
import ItemViewTable from "@/components/Item/View/Table.vue";
type Props = {
view?: ViewType;

View File

@@ -9,4 +9,5 @@ export type TableHeaderType = {
type?: "price" | "boolean" | "name" | "location" | "date";
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type TableData = Record<string, any>;

View File

@@ -133,7 +133,7 @@
:sibling-count="2"
@update:page="pagination.page = $event"
>
<PaginationList v-slot="{ pageItems }" class="flex items-center gap-1">
<PaginationList v-slot="{ items: pageItems }" class="flex items-center gap-1">
<PaginationFirst />
<template v-for="(item, index) in pageItems">
<PaginationListItem v-if="item.type === 'page'" :key="index" :value="item.value" as-child>
@@ -162,7 +162,7 @@
import MdiClose from "~icons/mdi/close";
import MdiTableCog from "~icons/mdi/table-cog";
import { Checkbox } from "@/components/ui/checkbox";
import { Table, TableBody, TableHeader, TableCell, TableHead, TableRow } from "@/components/ui/table";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import {
Pagination,
PaginationEllipsis,
@@ -175,6 +175,11 @@
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { useDialog } from "@/components/ui/dialog-provider";
import { DialogID } from "~/components/ui/dialog-provider/utils";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import BaseCard from "@/components/Base/Card.vue";
import Currency from "~/components/global/Currency.vue";
import DateTime from "~/components/global/DateTime.vue";
const { openDialog, closeDialog } = useDialog();
@@ -225,6 +230,9 @@
};
const moveHeader = (from: number, to: number) => {
const header = headers.value[from];
if (!header) {
return;
}
headers.value.splice(from, 1);
headers.value.splice(to, 0, header);

View File

@@ -34,6 +34,9 @@
import BaseModal from "@/components/App/CreateModal.vue";
import { useDialog, useDialogHotkey } from "~/components/ui/dialog-provider";
import ColorSelector from "@/components/Form/ColorSelector.vue";
import FormTextField from "~/components/Form/TextField.vue";
import FormTextArea from "~/components/Form/TextArea.vue";
import { Button, ButtonGroup } from "~/components/ui/button";
const { t } = useI18n();
@@ -72,7 +75,7 @@
loading.value = true;
if (shift.value) close = false;
if (shift?.value) close = false;
const { error, data } = await api.labels.create(form);

View File

@@ -90,6 +90,7 @@
TagsInputItemText,
} from "@/components/ui/tags-input";
import type { LabelOut } from "~/lib/api/types/data-contracts";
import { Label } from "@/components/ui/label";
const { t } = useI18n();

View File

@@ -51,8 +51,6 @@
});
const count = computed(() => {
if (hasCount.value) {
return (props.location as LocationOutCount).itemCount;
}
return hasCount.value ? (props.location as LocationOutCount).itemCount : undefined;
});
</script>

View File

@@ -37,6 +37,9 @@
import BaseModal from "@/components/App/CreateModal.vue";
import type { LocationSummary } from "~~/lib/api/types/data-contracts";
import { useDialog, useDialogHotkey } from "~/components/ui/dialog-provider";
import LocationSelector from "~/components/Location/Selector.vue";
import FormTextField from "~/components/Form/TextField.vue";
import FormTextArea from "~/components/Form/TextArea.vue";
const { t } = useI18n();
@@ -94,7 +97,7 @@
}
loading.value = true;
if (shift.value) close = false;
if (shift?.value) close = false;
const { data, error } = await api.locations.create({
name: form.name,

View File

@@ -4,6 +4,7 @@
import MdiChevronRight from "~icons/mdi/chevron-right";
import MdiMapMarker from "~icons/mdi/map-marker";
import MdiPackageVariant from "~icons/mdi/package-variant";
import LocationTreeNode from "./Node.vue";
type Props = {
treeId: string;
@@ -51,7 +52,7 @@
'hover:bg-accent hover:text-accent-foreground': hasChildren,
}"
>
<div v-if="!hasChildren" class="size-6"></div>
<div v-if="!hasChildren" class="size-6" />
<div v-else class="group/node relative size-6" :data-swap="openRef">
<div
class="absolute inset-0 flex items-center justify-center transition-transform duration-300 group-data-[swap=true]/node:rotate-90"

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { TreeItem } from "~~/lib/api/types/data-contracts";
import LocationTreeNode from "./Node.vue";
type Props = {
locs: TreeItem[];

View File

@@ -34,6 +34,9 @@
import DatePicker from "~~/components/Form/DatePicker.vue";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { useDialog } from "@/components/ui/dialog-provider";
import FormTextField from "~/components/Form/TextField.vue";
import FormTextArea from "~/components/Form/TextArea.vue";
import Button from "@/components/ui/button/Button.vue";
const { openDialog, closeDialog } = useDialog();

View File

@@ -11,9 +11,15 @@
import MdiWrenchClock from "~icons/mdi/wrench-clock";
import MdiContentDuplicate from "~icons/mdi/content-duplicate";
import MaintenanceEditModal from "~~/components/Maintenance/EditModal.vue";
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Badge } from "@/components/ui/badge";
import { ButtonGroup, Button } from "@/components/ui/button";
import { Button, ButtonGroup } from "@/components/ui/button";
import StatCard from "~/components/global/StatCard/StatCard.vue";
import BaseCard from "@/components/Base/Card.vue";
import BaseSectionHeader from "@/components/Base/SectionHeader.vue";
import DateTime from "~/components/global/DateTime.vue";
import Currency from "~/components/global/Currency.vue";
import Markdown from "~/components/global/Markdown.vue";
const maintenanceFilterStatus = ref(MaintenanceFilterStatus.MaintenanceFilterStatusScheduled);
const maintenanceEditModal = ref<InstanceType<typeof MaintenanceEditModal>>();
@@ -125,7 +131,7 @@
</section>
<section>
<!-- begin -->
<MaintenanceEditModal ref="maintenanceEditModal" @changed="refreshList"></MaintenanceEditModal>
<MaintenanceEditModal ref="maintenanceEditModal" @changed="refreshList" />
<div class="container space-y-6">
<BaseCard v-for="e in maintenanceDataList" :key="e.id">
<BaseSectionHeader class="border-b p-6">

View File

@@ -19,6 +19,16 @@
<script setup lang="ts">
import { useDialog } from "./ui/dialog-provider";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
const { text, isRevealed, confirm, cancel } = useConfirm();
const { addAlert, removeAlert } = useDialog();

View File

@@ -50,13 +50,13 @@
import { Label } from "@/components/ui/label";
type Props = {
label: string;
label?: string;
options: {
name: string;
id: string;
treeString?: string;
}[];
modelValue: {
modelValue?: {
name: string;
id: string;
treeString?: string;

View File

@@ -59,6 +59,7 @@
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
const props = defineProps({
text: {

View File

@@ -8,7 +8,7 @@
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
@@ -19,9 +19,7 @@
const props = defineProps<Props>();
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (...args: any) => Promise<infer R>
? R
: any;
type AsyncReturnType<T extends (...args: unknown[]) => unknown> = Awaited<ReturnType<T>>;
const fmt = ref<AsyncReturnType<typeof useFormatCurrency> | null>(null);

View File

@@ -73,7 +73,11 @@
import type { AnyDetail, Detail } from "./types";
import MdiOpenInNew from "~icons/mdi/open-in-new";
import { badgeVariants } from "~/components/ui/badge";
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import DateTime from "@/components/global/DateTime.vue";
import Currency from "@/components/global/Currency.vue";
import Markdown from "@/components/global/Markdown.vue";
import CopyText from "@/components/global/CopyText.vue";
defineProps({
details: {

View File

@@ -11,14 +11,14 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { useDialog } from "@/components/ui/dialog-provider";
import { Button, ButtonGroup } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
const { t } = useI18n();
const { openDialog, closeDialog } = useDialog();

View File

@@ -4,7 +4,7 @@
import DOMPurify from "dompurify";
type Props = {
source: string | null | undefined;
source?: string | null;
};
const props = withDefaults(defineProps<Props>(), {
@@ -25,7 +25,7 @@
<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="markdown text-wrap break-words" v-html="raw"></div>
<div class="markdown text-wrap break-words" v-html="raw" />
</template>
<style scoped>

View File

@@ -1,3 +1,3 @@
<template>
<div class="max-w-full"></div>
<div class="max-w-full" />
</template>

View File

@@ -12,6 +12,7 @@
</template>
<script setup lang="ts">
import Currency from "../Currency.vue";
import type { StatsFormat } from "./types";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";

View File

@@ -5,4 +5,5 @@ export type TableHeader = {
align?: "left" | "center" | "right";
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type TableData = Record<string, any>;

View File

@@ -1,28 +1,29 @@
import { computed, type ComputedRef } from 'vue';
import { createContext } from 'reka-ui';
import { useMagicKeys, useActiveElement } from '@vueuse/core';
import type { BarcodeProduct } from '~~/lib/api/types/data-contracts';
/* eslint-disable @typescript-eslint/unified-signatures */
import { computed, type ComputedRef } from "vue";
import { createContext } from "reka-ui";
import { useMagicKeys, useActiveElement } from "@vueuse/core";
import type { BarcodeProduct } 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',
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",
}
/**
@@ -31,21 +32,22 @@ export enum DialogID {
* - Keys not present => no params allowed
*/
export type DialogParamsMap = {
[DialogID.ItemImage]:
| ({
type: 'preloaded';
[DialogID.ItemImage]: (
| {
type: "preloaded";
originalSrc: string;
originalType?: string;
thumbnailSrc?: string;
}
| {
type: 'attachment';
type: "attachment";
mimeType: string;
thumbnailId?: string;
}) & {
itemId: string;
attachmentId: string;
};
}
) & {
itemId: string;
attachmentId: string;
};
[DialogID.CreateItem]?: { product?: BarcodeProduct };
[DialogID.ProductImport]?: { barcode?: string };
};
@@ -54,11 +56,12 @@ export type DialogParamsMap = {
* Defines the payload type for a dialog's onClose callback.
*/
export type DialogResultMap = {
[DialogID.ItemImage]?: { action: 'delete', id: string };
[DialogID.ItemImage]?: { action: "delete"; id: string };
};
/** 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];
@@ -69,13 +72,9 @@ 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 ParamsOf<T extends DialogID> = T extends SpecifiedDialogIDs ? DialogParamsMap[T] : never;
type ResultOf<T extends DialogID> = T extends keyof DialogResultMap
? DialogResultMap[T]
: void;
type ResultOf<T extends DialogID> = T extends keyof DialogResultMap ? DialogResultMap[T] : void;
type OpenDialog = {
// Dialogs with no parameters
@@ -101,22 +100,14 @@ type CloseDialog = {
// 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 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;
<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<{
@@ -127,7 +118,7 @@ export const [useDialog, provideDialogContext] = createContext<{
closeDialog: CloseDialog;
addAlert: (alertId: string) => void;
removeAlert: (alertId: string) => void;
}>('DialogProvider');
}>("DialogProvider");
/**
* Hotkey helper:
@@ -140,36 +131,27 @@ type HotkeyKey = {
code: string;
};
export function useDialogHotkey<T extends NoParamDialogIDs | OptionalDialogIDs>(
dialogId: T,
key: HotkeyKey
): void;
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
) {
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'
() => activeElement.value?.tagName !== "INPUT" && activeElement.value?.tagName !== "TEXTAREA"
);
useMagicKeys({
passive: false,
onEventFired: (event) => {
onEventFired: event => {
if (
notUsingInput.value &&
event.type === 'keydown' &&
event.type === "keydown" &&
event.code === key.code &&
(key.shift === undefined || event.shiftKey === key.shift) &&
(key.ctrl === undefined || event.ctrlKey === key.ctrl)
@@ -185,4 +167,4 @@ export function useDialogHotkey(
}
},
});
}
}

View File

@@ -7,9 +7,9 @@
const { closeDialog, activeDialog } = useDialog();
const isOpen = computed(() => (activeDialog.value && activeDialog.value === props.dialogId));
const isOpen = computed(() => (activeDialog.value && activeDialog.value === props.dialogId) ?? false);
const onOpenChange = (open: boolean) => {
if (!open) closeDialog(props.dialogId as any);
if (!open) closeDialog(props.dialogId);
};
const forwarded = useForwardPropsEmits(props, emits);

View File

@@ -1,7 +1,8 @@
<script lang="ts" setup>
import { Toaster as Sonner, type ToasterProps } from 'vue-sonner'
import { Toaster as Sonner, type ToasterProps } from "vue-sonner";
import "vue-sonner/style.css";
const props = defineProps<ToasterProps>()
const props = defineProps<ToasterProps>();
</script>
<template>
@@ -9,15 +10,14 @@ const props = defineProps<ToasterProps>()
class="toaster group"
v-bind="props"
rich-colors
visible-toasts="10"
:visible-toasts="10"
:toast-options="{
classes: {
toast: 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton:
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton:
'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
},
}"
/>

View File

@@ -1,2 +1,2 @@
export { default as Toaster } from './Sonner.vue'
export { toast } from './toast'
export { default as Toaster } from "./Sonner.vue";
export { toast } from "./toast";