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

@@ -4,7 +4,7 @@
},
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"package.json": "package-lock.json, yarn.lock, .eslintrc.js, tsconfig.json, .prettierrc, .editorconfig, pnpm-lock.yaml, postcss.config.js, tailwind.config.js",
"package.json": "package-lock.json, yarn.lock, eslint.config.mjs, tsconfig.json, .prettierrc, .editorconfig, pnpm-lock.yaml, postcss.config.js, tailwind.config.js",
"docker-compose.yml": "Dockerfile, .dockerignore, docker-compose.dev.yml, docker-compose.yml",
"README.md": "LICENSE, SECURITY.md"
},
@@ -22,6 +22,7 @@
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"eslint.format.enable": true,
"eslint.useFlatConfig": true,
"css.validate": false,
"tailwindCSS.includeLanguages": {
"vue": "html",

View File

@@ -93,6 +93,16 @@ tasks:
- go run ./app/api/ {{ .CLI_ARGS }} &
silent: true
go:ci:with-frontend:
desc: Run backend with frontend in CI mode
dir: frontend
cmds:
- pnpm install
- pnpm run build
- cp -r ./.output/public ../backend/app/api/static/
- task: go:ci
silent: true
go:test:
desc: Runs all go tests using gotestsum - supports passing gotestsum args
dir: backend
@@ -201,12 +211,11 @@ tasks:
desc: Runs end-to-end test on a live server
dir: frontend
cmds:
- task: go:ci
- task: ui:ci
- task: go:ci:with-frontend
- pnpm exec playwright install-deps
- pnpm exec playwright install
- sleep 30
- TEST_SHUTDOWN_API_SERVER=true pnpm exec playwright test -c ./test/playwright.config.ts {{ .CLI_ARGS }}
- TEST_SHUTDOWN_API_SERVER=true E2E_BASE_URL=http://localhost:7745 pnpm exec playwright test -c ./test/playwright.config.ts {{ .CLI_ARGS }}
pr:
desc: Runs all tasks required for a PR

View File

@@ -1,55 +0,0 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:vue/essential",
"plugin:@typescript-eslint/recommended",
"@nuxtjs/eslint-config-typescript",
"plugin:vue/vue3-recommended",
"plugin:prettier/recommended",
"plugin:tailwindcss/recommended",
],
parserOptions: {
ecmaVersion: "latest",
parser: "@typescript-eslint/parser",
sourceType: "module",
},
plugins: ["vue", "@typescript-eslint"],
rules: {
"no-console": 0,
"no-unused-vars": "off",
"vue/multi-word-component-names": "off",
"vue/no-setup-props-destructure": 0,
"vue/no-multiple-template-root": 0,
"vue/no-v-model-argument": 0,
"vue/no-v-html": 0,
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/ban-ts-comment": 0,
"tailwindcss/no-custom-classname": "warn",
"@typescript-eslint/no-unused-vars": [
"error",
{
ignoreRestSiblings: true,
destructuredArrayIgnorePattern: "_",
caughtErrors: "none",
},
],
"prettier/prettier": [
"warn",
{
arrowParens: "avoid",
semi: true,
tabWidth: 2,
useTabs: false,
vueIndentScriptAndStyle: true,
singleQuote: false,
trailingComma: "es5",
printWidth: 120,
},
],
},
};

View File

@@ -6,7 +6,7 @@
<NuxtLayout>
<Html :lang="locale" :data-theme="theme || 'homebox'" />
<Link rel="icon" type="image/svg" href="/favicon.svg"></Link>
<Link rel="icon" type="image/svg" href="/favicon.svg" />
<Link rel="apple-touch-icon" href="/apple-touch-icon.png" size="180x180" />
<Link rel="mask-icon" href="/mask-icon.svg" color="#5b7f67" />
<Meta name="theme-color" content="#5b7f67" />

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,18 +32,19 @@ 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;
};
@@ -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)

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";

View File

@@ -14,6 +14,7 @@ export function defineObserver(key: string, observer: Observer): RemoveObserver
observers[key] = observer;
return () => {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete observers[key];
};
}

View File

@@ -34,7 +34,6 @@ export interface IAuthContext {
}
class AuthContext implements IAuthContext {
// eslint-disable-next-line no-use-before-define
private static _instance?: AuthContext;
private static readonly cookieTokenKey = "hb.auth.session";
@@ -45,7 +44,7 @@ class AuthContext implements IAuthContext {
private _attachmentToken: CookieRef<string | null>;
get token() {
// @ts-ignore sometimes it's a boolean I guess?
// @ts-expect-error sometimes it's a boolean I guess?
return this._token.value === "true" || this._token.value === true;
}

View File

@@ -1,4 +1,5 @@
import type { UseConfirmDialogRevealResult, UseConfirmDialogReturn } from "@vueuse/core";
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { UseConfirmDialogReturn, UseConfirmDialogRevealResult } from "@vueuse/core";
import type { Ref } from "vue";
type Store = UseConfirmDialogReturn<any, boolean, boolean> & {

View File

@@ -44,7 +44,6 @@ export function useBreakpoints(): Breakpoints {
}
class ThemeObserver {
// eslint-disable-next-line no-use-before-define
private static instance?: ThemeObserver;
private readonly observer: MutationObserver;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
type DeferFunction<TArgs extends any[], TReturn> = (...args: TArgs) => TReturn;
// useDefer is a function that takes a function and returns a function that

View File

@@ -1,5 +1,5 @@
import type { Ref } from "vue";
import type { TreeItem, LocationSummary } from "~~/lib/api/types/data-contracts";
import type { LocationSummary, TreeItem } from "~~/lib/api/types/data-contracts";
export interface FlatTreeItem {
id: string;

View File

@@ -8,6 +8,7 @@ export function useRouteQuery(q: string, def: string): WritableComputedRef<strin
export function useRouteQuery(q: string, def: boolean): WritableComputedRef<boolean>;
export function useRouteQuery(q: string, def: number): WritableComputedRef<number>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useRouteQuery(q: string, def: any): WritableComputedRef<any> {
const route = useRoute();
const router = useRouter();

View File

@@ -39,7 +39,7 @@ function connect(onmessage: (m: EventMessage) => void) {
console.error("websocket error", err);
};
const thorttled = new Map<ServerEvent, any>();
const thorttled = new Map<ServerEvent, (m: EventMessage) => void>();
thorttled.set(ServerEvent.LocationMutation, useThrottleFn(onmessage, 1000));
thorttled.set(ServerEvent.ItemMutation, useThrottleFn(onmessage, 1000));

View File

@@ -1,5 +1,5 @@
import type { ComputedRef } from "vue";
import { type DaisyTheme } from "~~/lib/data/themes";
import type { DaisyTheme } from "~~/lib/data/themes";
export interface UseTheme {
theme: ComputedRef<DaisyTheme>;

View File

@@ -59,8 +59,8 @@ export function maybeUrl(str: string): MaybeUrlResult {
const match = str.match(/\[(.*)\]\((.*)\)/);
if (match && match.length === 3) {
result.isUrl = true;
result.text = match[1];
result.url = match[2];
result.text = match[1]!;
result.url = match[2]!;
}
} else {
result.url = str;

View File

@@ -0,0 +1,72 @@
import withNuxt from "./.nuxt/eslint.config.mjs";
import tailwind from "eslint-plugin-tailwindcss";
import prettier from "eslint-plugin-prettier";
import { includeIgnoreFile } from "@eslint/compat";
import { fileURLToPath } from "node:url";
const gitignorePath = fileURLToPath(new URL("../.gitignore", import.meta.url));
export default withNuxt([
includeIgnoreFile(gitignorePath, "Imported ../.gitignore patterns"),
...tailwind.configs["flat/recommended"],
{
plugins: {
prettier,
},
rules: {
"vue/no-undef-components": [
"error",
{
// ignore anything that start with a lowercase letter or #composables
ignorePatterns: [
"^i18n",
"ClientOnly",
"Html",
"Link",
"Meta",
"NuxtLayout",
"NuxtPage",
"NuxtLink",
"Title",
],
},
],
"no-console": 0,
"no-unused-vars": "off",
"vue/multi-word-component-names": "off",
"vue/no-setup-props-destructure": 0,
"vue/no-multiple-template-root": 0,
"vue/no-v-model-argument": 0,
"vue/no-v-html": 0,
"vue/html-self-closing": 0,
"tailwindcss/no-custom-classname": "warn",
"@typescript-eslint/no-unused-vars": [
"error",
{
ignoreRestSiblings: true,
destructuredArrayIgnorePattern: "_",
caughtErrors: "none",
},
],
"@typescript-eslint/no-invalid-void-type": "off",
"prettier/prettier": [
"warn",
{
arrowParens: "avoid",
semi: true,
tabWidth: 2,
useTabs: false,
vueIndentScriptAndStyle: true,
singleQuote: false,
trailingComma: "es5",
printWidth: 120,
},
],
},
},
]);

View File

@@ -6,7 +6,7 @@
up the tree
-->
<ModalConfirm />
<AppOutdatedModal v-if="status" :status="status" />
<OutdatedModal v-if="status" :status="status" />
<ItemCreateModal />
<LabelCreateModal />
<LocationCreateModal />
@@ -124,7 +124,7 @@
<AppHeaderText class="h-6" />
</NuxtLink>
</div>
<div class="sm:grow"></div>
<div class="sm:grow" />
<div class="flex h-1/2 grow items-center justify-end gap-2 sm:h-auto">
<Input
v-model:model-value="search"
@@ -146,8 +146,8 @@
</div>
</div>
<slot></slot>
<div class="grow"></div>
<slot />
<div class="grow" />
<footer v-if="status" class="bottom-0 w-full pb-4 text-center">
<p class="text-center text-sm">
@@ -157,9 +157,9 @@
$t('global.footer.version_link', { version: status.build.version, build: status.build.commit })
)
"
></span>
/>
~
<span v-html="DOMPurify.sanitize($t('global.footer.api_link'))"></span>
<span v-html="DOMPurify.sanitize($t('global.footer.api_link'))" />
</p>
</footer>
</div>
@@ -188,16 +188,17 @@
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarInset,
SidebarRail,
SidebarTrigger,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuLink,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from "@/components/ui/sidebar";
import {
DropdownMenu,
@@ -211,6 +212,18 @@
import { Button } from "~/components/ui/button";
import { toast } from "@/components/ui/sonner";
import { DialogID, type NoParamDialogIDs, type OptionalDialogIDs } from "~/components/ui/dialog-provider/utils";
import ModalConfirm from "~/components/ModalConfirm.vue";
import OutdatedModal from "~/components/App/OutdatedModal.vue";
import ItemCreateModal from "~/components/Item/CreateModal.vue";
import LabelCreateModal from "~/components/Label/CreateModal.vue";
import LocationCreateModal from "~/components/Location/CreateModal.vue";
import ItemBarcodeModal from "~/components/Item/BarcodeModal.vue";
import AppQuickMenuModal from "~/components/App/QuickMenuModal.vue";
import AppScannerModal from "~/components/App/ScannerModal.vue";
import AppLogo from "~/components/App/Logo.vue";
import AppHeaderDecor from "~/components/App/HeaderDecor.vue";
import AppHeaderText from "~/components/App/HeaderText.vue";
const { t, locale } = useI18n();
const username = computed(() => authCtx.user?.name || "User");
@@ -344,7 +357,7 @@
...dropdown.map(v => ({
text: computed(() => v.name.value),
dialogId: v.dialogId as NoParamDialogIDs,
shortcut: v.shortcut.split("+")[1],
shortcut: v.shortcut.split("+")[1] as string,
type: "create" as const,
})),
...nav.map(v => ({

View File

@@ -1,4 +1,6 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { Toaster } from "~/components/ui/sonner";
</script>
<template>
<div>
<Toaster />

View File

@@ -9,7 +9,7 @@ import { Requests } from "../../../requests";
function itemField(id = null): ItemField {
return {
// @ts-expect-error
// @ts-expect-error - not actually an issue
id,
name: faker.lorem.word(),
type: "text",
@@ -45,7 +45,7 @@ function label(): LabelCreate {
return {
name: faker.lorem.word(),
description: faker.lorem.sentence(),
color: faker.internet.color(),
color: faker.color.rgb(),
};
}

View File

@@ -1,4 +1,4 @@
import { describe, test, expect } from "vitest";
import { describe, expect, test } from "vitest";
import { factories } from "./factories";
describe("[GET] /api/v1/status", () => {

View File

@@ -1,5 +1,5 @@
import { faker } from "@faker-js/faker";
import { describe, test, expect } from "vitest";
import { describe, expect, test } from "vitest";
import { factories } from "../factories";
import { sharedUserClient } from "../test-utils";

View File

@@ -1,5 +1,5 @@
import { faker } from "@faker-js/faker";
import { describe, test, expect } from "vitest";
import { describe, expect, test } from "vitest";
import type { ItemField, ItemUpdate, LocationOut } from "../../types/data-contracts";
import { AttachmentTypes } from "../../types/non-generated";
import type { UserClient } from "../../user";
@@ -55,9 +55,9 @@ describe("user should be able to create an item and add an attachment", () => {
expect(itmResp.status).toBe(200);
expect(data.attachments).toHaveLength(1);
expect(data.attachments[0].title).toBe("test.txt");
expect(data.attachments[0]?.title).toBe("test.txt");
const resp = await api.items.attachments.delete(data.id, data.attachments[0].id);
const resp = await api.items.attachments.delete(data.id, data.attachments[0]!.id);
expect(resp.response.status).toBe(204);
api.items.delete(item.id);
@@ -100,21 +100,21 @@ describe("user should be able to create an item and add an attachment", () => {
expect(item2.fields).toHaveLength(fields.length);
for (let i = 0; i < fields.length; i++) {
expect(item2.fields[i].name).toBe(fields[i].name);
expect(item2.fields[i].textValue).toBe(fields[i].textValue);
expect(item2.fields[i].numberValue).toBe(fields[i].numberValue);
expect(item2.fields[i]?.name).toBe(fields[i]!.name);
expect(item2.fields[i]?.textValue).toBe(fields[i]!.textValue);
expect(item2.fields[i]?.numberValue).toBe(fields[i]!.numberValue);
}
itemUpdate.fields = [fields[0], fields[1]];
itemUpdate.fields = [fields[0]!, fields[1]!];
const { response: updateResponse2, data: item3 } = await api.items.update(item.id, itemUpdate as ItemUpdate);
expect(updateResponse2.status).toBe(200);
expect(item3.fields).toHaveLength(2);
for (let i = 0; i < item3.fields.length; i++) {
expect(item3.fields[i].name).toBe(itemUpdate.fields[i].name);
expect(item3.fields[i].textValue).toBe(itemUpdate.fields[i].textValue);
expect(item3.fields[i].numberValue).toBe(itemUpdate.fields[i].numberValue);
expect(item3.fields[i]?.name).toBe(itemUpdate.fields[i]!.name);
expect(item3.fields[i]?.textValue).toBe(itemUpdate.fields[i]!.textValue);
expect(item3.fields[i]?.numberValue).toBe(itemUpdate.fields[i]!.numberValue);
}
cleanup();
@@ -168,7 +168,7 @@ describe("user should be able to create an item and add an attachment", () => {
// Skip first one
const { response, data: loc } = await api.locations.create({
parentId: lastLocationId,
name: locations[i],
name: locations[i]!,
description: "",
});
expect(response.status).toBe(201);

View File

@@ -28,7 +28,7 @@ type ImportObj = {
};
function toCsv(data: ImportObj[]): string {
const headers = Object.keys(data[0]).join("\t");
const headers = Object.keys(data[0]!).join("\t");
const rows = data.map(row => {
return Object.values(row).join("\t");
});

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Requests } from "../../requests";
import { route } from ".";
@@ -47,7 +48,7 @@ export function parseDate<T>(obj: T, keys: Array<keyof T> = []): T {
throw new Error(`Invalid date format: ${value}`);
}
const [year, month, day] = split;
const [year, month, day] = split as [string, string, string];
const dt = new Date();
@@ -90,9 +91,9 @@ export class BaseAPI {
protected dropFields<T>(obj: T, keys: Array<keyof T> = []): T {
const result = { ...obj };
[...keys, "createdAt", "updatedAt"].forEach(key => {
// @ts-ignore - we are checking for the key above
// @ts-expect-error - TS doesn't know that we're checking for the key above
if (hasKey(result, key)) {
// @ts-ignore - we are guarding against this above
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete result[key];
}
});

View File

@@ -1,5 +1,5 @@
import { BaseAPI, route } from "../base";
import type { LocationOutCount, LocationCreate, LocationOut, LocationUpdate, TreeItem } from "../types/data-contracts";
import type { LocationCreate, LocationOut, LocationOutCount, LocationUpdate, TreeItem } from "../types/data-contracts";
export type LocationsQuery = {
filterChildren: boolean;

View File

@@ -1,8 +1,8 @@
import { BaseAPI, route } from "../base";
import type {
MaintenanceEntry,
MaintenanceEntryWithDetails,
MaintenanceEntryUpdate,
MaintenanceEntryWithDetails,
MaintenanceFilterStatus,
} from "../types/data-contracts";

View File

@@ -1,5 +1,5 @@
import { describe, test, expect } from "vitest";
import { format, zeroTime, factorRange, parse } from "./datelib";
import { describe, expect, test } from "vitest";
import { factorRange, format, parse, zeroTime } from "./datelib";
describe("format", () => {
test("should format a date as a string", () => {

View File

@@ -7,7 +7,7 @@ export function format(date: Date | string): string {
if (typeof date === "string") {
return date;
}
return date.toISOString().split("T")[0];
return date.toISOString().split("T")[0]!;
}
export function zeroTime(date: Date): Date {
@@ -31,6 +31,6 @@ export function factory(offset = 0): Date {
}
export function parse(yyyyMMdd: string): Date {
const parts = yyyyMMdd.split("-");
const parts = yyyyMMdd.split("-") as [string, string, string];
return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
}

View File

@@ -1,4 +1,4 @@
import { describe, test, expect } from "vitest";
import { describe, expect, test } from "vitest";
import { scorePassword } from ".";
describe("scorePassword tests", () => {

View File

@@ -23,8 +23,8 @@ export function scorePassword(pass: string): number {
const letters: { [key: string]: number } = {};
for (let i = 0; i < pass.length; i++) {
letters[pass[i]] = (letters[pass[i]] || 0) + 1;
score += 5.0 / letters[pass[i]];
letters[pass[i]!] = (letters[pass[i]!] || 0) + 1;
score += 5.0 / letters[pass[i]!]!;
}
// bonus points for mixing it up

View File

@@ -82,7 +82,7 @@ export class Requests {
const token = this.token();
if (token !== "" && payload.headers !== undefined) {
// @ts-expect-error - we know that the header is there
payload.headers["Authorization"] = token; // eslint-disable-line dot-notation
payload.headers["Authorization"] = token;
}
if (this.methodSupportsBody(method)) {

View File

@@ -25,7 +25,7 @@ export function getContrastTextColor(bgColor: string): string {
g = parseInt(hex.slice(2, 4), 16);
b = parseInt(hex.slice(4, 6), 16);
} else if (bgColor.startsWith("rgb")) {
const match = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
const match = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/) as [string, string, string, string];
if (match) {
r = parseInt(match[1]);
g = parseInt(match[2]);

View File

@@ -4,6 +4,10 @@ import { defineNuxtConfig } from "nuxt/config";
export default defineNuxtConfig({
ssr: false,
components: {
dirs: [],
},
build: {
transpile: ["vue-i18n"],
},
@@ -15,8 +19,13 @@ export default defineNuxtConfig({
"@vite-pwa/nuxt",
"unplugin-icons/nuxt",
"shadcn-nuxt",
"@nuxt/eslint",
],
eslint: {
config: {},
},
nitro: {
devProxy: {
"/api": {

View File

@@ -5,78 +5,77 @@
"dev": "nuxt dev",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore --ignore-pattern \"components/ui\" .",
"lint:fix": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore --ignore-pattern \"components/ui\" . --fix",
"lint:ci": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore --ignore-pattern \"components/ui\" . --max-warnings 1",
"typecheck": "pnpm vue-tsc --noEmit",
"lint": "eslint --ext \".ts,.js,.vue\" --ignore-pattern \"components/ui\" .",
"lint:fix": "eslint --ext \".ts,.js,.vue\" --ignore-pattern \"components/ui\" . --fix",
"lint:ci": "eslint --ext \".ts,.js,.vue\" --ignore-pattern \"components/ui\" . --max-warnings 1",
"typecheck": "pnpm nuxi typecheck --noEmit",
"test:ci": "TEST_SHUTDOWN_API_SERVER=true vitest --run --config ./test/vitest.config.ts",
"test:local": "TEST_SHUTDOWN_API_SERVER=false && vitest --run --config ./test/vitest.config.ts",
"test:watch": " TEST_SHUTDOWN_API_SERVER=false vitest --config ./test/vitest.config.ts"
},
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@eslint/compat": "^1.3.2",
"@faker-js/faker": "^10.0.0",
"@iconify-json/mdi": "^1.2.3",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@nuxtjs/eslint-config-typescript": "^12.1.0",
"@playwright/test": "^1.52.0",
"@types/markdown-it": "^14.1.0",
"@intlify/unplugin-vue-i18n": "^6.0.8",
"@nuxt/eslint": "^1.9.0",
"@playwright/test": "^1.55.0",
"@types/markdown-it": "^14.1.2",
"@types/semver": "^7.7.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vite-pwa/nuxt": "^0.5.0",
"@vue/runtime-core": "^3.5.13",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-tailwindcss": "^3.18.0",
"eslint-plugin-vue": "^9.33.0",
"@vite-pwa/nuxt": "^1.0.4",
"@vue/runtime-core": "^3.5.20",
"eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-tailwindcss": "^3.18.2",
"globals": "^16.3.0",
"h3": "^1.7.1",
"intl-messageformat": "^10.7.16",
"isomorphic-fetch": "^3.0.0",
"nuxt": "3.12.4",
"prettier": "^3.5.3",
"shadcn-nuxt": "0.11.3",
"typescript": "5.6.2",
"unplugin-icons": "^0.18.5",
"nuxt": "4.0.3",
"prettier": "^3.6.2",
"shadcn-nuxt": "2.2.0",
"typescript": "5.9.2",
"unplugin-icons": "^22.2.0",
"vite-plugin-eslint": "^1.8.1",
"vitest": "^1.6.1",
"vue-i18n": "^9.14.4",
"vue-tsc": "2.1.6"
"vitest": "^3.2.4",
"vue-i18n": "^11.1.11",
"vue-tsc": "3.0.6"
},
"dependencies": {
"@mdit/plugin-img-size": "^0.22.2",
"@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/tailwindcss": "^6.13.2",
"@pinia/nuxt": "^0.5.5",
"@nuxtjs/tailwindcss": "^6.14.0",
"@pinia/nuxt": "^0.11.2",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@vuepic/vue-datepicker": "^8.8.1",
"@vueuse/core": "^12.8.2",
"@vueuse/nuxt": "^10.11.1",
"@vueuse/router": "^10.11.1",
"@vuepic/vue-datepicker": "^11.0.2",
"@vueuse/core": "^13.8.0",
"@vueuse/nuxt": "^13.8.0",
"@vueuse/router": "^13.8.0",
"@zxing/library": "^0.21.3",
"autoprefixer": "^10.4.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"dompurify": "^3.2.5",
"date-fns": "^4.1.0",
"dompurify": "^3.2.6",
"fuzzysort": "^3.1.0",
"h3": "^1.15.1",
"h3": "^1.15.4",
"http-proxy": "^1.18.1",
"lucide-vue-next": "^0.474.0",
"lucide-vue-next": "^0.542.0",
"markdown-it": "^14.1.0",
"pinia": "^2.3.1",
"postcss": "^8.5.3",
"reka-ui": "^2.2.0",
"semver": "^7.7.1",
"tailwind-merge": "^2.6.0",
"pinia": "^3.0.3",
"postcss": "^8.5.6",
"reka-ui": "^2.5.0",
"semver": "^7.7.2",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"vaul-vue": "^0.4.1",
"vite": "^6.3.5",
"vue": "3.4.8",
"vue-router": "^4.5.0",
"vue-sonner": "^1.3.2"
"vite": "^7.1.3",
"vue": "3.5.20",
"vue-router": "^4.5.1",
"vue-sonner": "^2.0.8"
}
}

View File

@@ -1,6 +1,9 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { toast } from "@/components/ui/sonner";
import BaseContainer from "@/components/Base/Container.vue";
import BaseSectionHeader from "@/components/Base/SectionHeader.vue";
import ItemCard from "~/components/Item/Card.vue";
const { t } = useI18n();
@@ -26,7 +29,7 @@
navigateTo("/home");
break;
case 1:
navigateTo(`/item/${data.items[0].id}`, { replace: true, redirectCode: 302 });
navigateTo(`/item/${data.items[0]!.id}`, { replace: true, redirectCode: 302 });
break;
default:
return data.items;

View File

@@ -4,6 +4,14 @@
import { itemsTable } from "./table";
import { useLabelStore } from "~~/stores/labels";
import { useLocationStore } from "~~/stores/locations";
import BaseContainer from "@/components/Base/Container.vue";
import BaseCard from "@/components/Base/Card.vue";
import Subtitle from "~/components/global/Subtitle.vue";
import StatCard from "~/components/global/StatCard/StatCard.vue";
import ItemViewTable from "~/components/Item/View/Table.vue";
import ItemCard from "~/components/Item/Card.vue";
import LocationCard from "~/components/Location/Card.vue";
import LabelChip from "~/components/Label/Chip.vue";
const { t } = useI18n();

View File

@@ -10,10 +10,16 @@ type StatCard = {
export function statCardData(api: UserClient) {
const { t } = useI18n();
const { data: statistics } = useAsyncData(async () => {
const { data: statistics } = useAsyncData(
"statistics",
async () => {
const { data } = await api.stats.group();
return data;
});
},
{
deep: true,
}
);
return computed(() => {
return [

View File

@@ -1,14 +1,20 @@
import type { UserClient } from "~~/lib/api/user";
export function itemsTable(api: UserClient) {
const { data: items, refresh } = useAsyncData(async () => {
const { data: items, refresh } = useAsyncData(
"items",
async () => {
const { data } = await api.items.getAll({
page: 1,
pageSize: 5,
orderBy: "createdAt",
});
return data.items;
});
},
{
deep: true,
}
);
onServerEvent(ServerEvent.ItemMutation, () => {
console.log("item mutation");

View File

@@ -13,7 +13,12 @@
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import LanguageSelector from "~/components/App/LanguageSelector.vue";
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import AppLogo from "~/components/App/Logo.vue";
import FormTextField from "~/components/Form/TextField.vue";
import FormPassword from "~/components/Form/Password.vue";
import FormCheckbox from "~/components/Form/Checkbox.vue";
import PasswordScore from "~/components/global/PasswordScore.vue";
const { t } = useI18n();
@@ -176,7 +181,7 @@
<path
fill-opacity="1"
d="M0,32L80,69.3C160,107,320,181,480,181.3C640,181,800,107,960,117.3C1120,128,1280,224,1360,272L1440,320L1440,0L1360,0C1280,0,1120,0,960,0C800,0,640,0,480,0C320,0,160,0,80,0L0,0Z"
></path>
/>
</svg>
</div>
<div>

View File

@@ -23,6 +23,19 @@
import { Switch } from "@/components/ui/switch";
import { Card } from "@/components/ui/card";
import { DialogID } from "~/components/ui/dialog-provider/utils";
import BaseContainer from "@/components/Base/Container.vue";
import ItemImageDialog from "~/components/Item/ImageDialog.vue";
import ItemDuplicateSettings from "~/components/Item/DuplicateSettings.vue";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import LabelChip from "~/components/Label/Chip.vue";
import DateTime from "~/components/global/DateTime.vue";
import LabelMaker from "~/components/global/LabelMaker.vue";
import Markdown from "~/components/global/Markdown.vue";
import BaseCard from "@/components/Base/Card.vue";
import CopyText from "@/components/global/CopyText.vue";
import DetailsSection from "~/components/global/DetailsSection/DetailsSection.vue";
import ItemAttachmentsList from "~/components/Item/AttachmentsList.vue";
import ItemViewSelectable from "~/components/Item/View/Selectable.vue";
const { t } = useI18n();
@@ -645,7 +658,7 @@
</header>
<Separator v-if="item.description" />
<div v-if="item.description" class="prose max-w-full p-1">
<Markdown class="text-base" :source="item.description"> </Markdown>
<Markdown class="text-base" :source="item.description" />
</div>
</Card>

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable @typescript-eslint/no-explicit-any -->
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { toast } from "@/components/ui/sonner";
@@ -15,10 +16,20 @@
import { useDialog } from "@/components/ui/dialog-provider";
import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { DialogID } from "~/components/ui/dialog-provider/utils";
import FormTextField from "~/components/Form/TextField.vue";
import FormTextArea from "~/components/Form/TextArea.vue";
import FormDatePicker from "~/components/Form/DatePicker.vue";
import FormCheckbox from "~/components/Form/Checkbox.vue";
import LocationSelector from "~/components/Location/Selector.vue";
import ItemSelector from "~/components/Item/Selector.vue";
import LabelSelector from "~/components/Label/Selector.vue";
import BaseCard from "@/components/Base/Card.vue";
import { Card } from "~/components/ui/card";
import DropZone from "~/components/global/DropZone.vue";
const { t } = useI18n();
@@ -52,7 +63,7 @@
return;
}
if (locations && data.location?.id) {
if (locations.value && data.location?.id) {
// @ts-expect-error - we know the locations is valid
const location = locations.value.find(l => l.id === data.location.id);
if (location) {
@@ -67,7 +78,7 @@
return data;
});
const item = ref<ItemOut & { labelIds: string[] }>(null as any);
const item = ref<ItemOut & { labelIds: string[] }>(null as never);
watchEffect(() => {
if (nullableItem.value) {
@@ -314,7 +325,7 @@
const dropReceipt = (files: File[] | null) => uploadAttachment(files, AttachmentTypes.Receipt);
async function uploadAttachment(files: File[] | null, type: AttachmentTypes | null) {
if (!files || files.length === 0) {
if (!files || files.length === 0 || !files[0]) {
return;
}
@@ -362,7 +373,7 @@
});
const attachmentOpts = Object.entries(AttachmentTypes).map(([key, value]) => ({
text: key[0].toUpperCase() + key.slice(1),
text: key[0]!.toUpperCase() + key.slice(1),
value,
}));
@@ -373,7 +384,7 @@
editState.primary = attachment.primary;
openDialog(DialogID.AttachmentEdit);
editState.obj = attachmentOpts.find(o => o.value === attachment.type) || attachmentOpts[0];
editState.obj = attachmentOpts.find(o => o.value === attachment.type) || attachmentOpts[0]!;
}
async function updateAttachment() {

View File

@@ -1,5 +1,7 @@
<script setup lang="ts">
import type { ItemOut } from "~~/lib/api/types/data-contracts";
import BaseContainer from "@/components/Base/Container.vue";
import MaintenanceListView from "~/components/Maintenance/ListView.vue";
const props = defineProps<{
item: ItemOut;
@@ -8,6 +10,6 @@
<template>
<BaseContainer class="flex flex-col gap-8">
<MaintenanceListView :current-item-id="props.item.id"></MaintenanceListView>
<MaintenanceListView :current-item-id="props.item.id" />
</BaseContainer>
</template>

View File

@@ -23,6 +23,10 @@
PaginationList,
PaginationListItem,
} from "@/components/ui/pagination";
import BaseContainer from "@/components/Base/Container.vue";
import BaseSectionHeader from "@/components/Base/SectionHeader.vue";
import SearchFilter from "~/components/Search/Filter.vue";
import ItemCard from "~/components/Item/Card.vue";
const { t } = useI18n();
@@ -398,27 +402,27 @@
<PopoverContent class="z-40 flex flex-col gap-2">
<Label class="flex cursor-pointer items-center">
<Switch v-model="includeArchived" class="ml-auto" />
<div class="grow"></div>
<div class="grow" />
{{ $t("items.include_archive") }}
</Label>
<Label class="flex cursor-pointer items-center">
<Switch v-model="fieldSelector" class="ml-auto" />
<div class="grow"></div>
<div class="grow" />
{{ $t("items.field_selector") }}
</Label>
<Label class="flex cursor-pointer items-center">
<Switch v-model="negateLabels" class="ml-auto" />
<div class="grow"></div>
<div class="grow" />
{{ $t("items.negate_labels") }}
</Label>
<Label class="flex cursor-pointer items-center">
<Switch v-model="onlyWithoutPhoto" class="ml-auto" />
<div class="grow"></div>
<div class="grow" />
{{ $t("items.only_without_photo") }}
</Label>
<Label class="flex cursor-pointer items-center">
<Switch v-model="onlyWithPhoto" class="ml-auto" />
<div class="grow"></div>
<div class="grow" />
{{ $t("items.only_with_photo") }}
</Label>
<Label class="flex cursor-pointer flex-col gap-2">
@@ -440,7 +444,7 @@
<Button @click="reset"> {{ $t("items.reset_search") }} </Button>
</PopoverContent>
</Popover>
<div class="grow"></div>
<div class="grow" />
<Popover>
<PopoverTrigger as-child>
<Button size="sm" variant="outline"> {{ $t("items.tips") }}</Button>
@@ -466,7 +470,7 @@
<div v-for="(f, idx) in fieldTuples" :key="idx" class="flex flex-wrap gap-2">
<div class="flex w-full flex-col gap-1 md:w-auto md:grow">
<Label> Field </Label>
<Select v-model="fieldTuples[idx][0]" @update:model-value="fetchValues(f[0])">
<Select v-model="fieldTuples[idx]![0]" @update:model-value="fetchValues(f[0])">
<SelectTrigger>
<SelectValue :placeholder="$t('items.select_field')" />
</SelectTrigger>
@@ -477,7 +481,7 @@
</div>
<div class="flex w-full flex-col gap-1 md:w-auto md:grow">
<Label> {{ $t("items.field_value") }} </Label>
<Select v-model="fieldTuples[idx][1]">
<Select v-model="fieldTuples[idx]![1]">
<SelectTrigger>
<SelectValue placeholder="Select a value" />
</SelectTrigger>

View File

@@ -13,6 +13,14 @@
import ColorSelector from "@/components/Form/ColorSelector.vue";
import { getContrastTextColor } from "~/lib/utils";
import { DialogID } from "~/components/ui/dialog-provider/utils";
import FormTextField from "~/components/Form/TextField.vue";
import FormTextArea from "~/components/Form/TextArea.vue";
import BaseContainer from "@/components/Base/Container.vue";
import Currency from "~/components/global/Currency.vue";
import DateTime from "~/components/global/DateTime.vue";
import PageQRCode from "~/components/global/PageQRCode.vue";
import Markdown from "~/components/global/Markdown.vue";
import ItemViewSelectable from "~/components/Item/View/Selectable.vue";
definePageMeta({
middleware: ["auth"],

View File

@@ -21,6 +21,17 @@
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { DialogID } from "~/components/ui/dialog-provider/utils";
import FormTextField from "~/components/Form/TextField.vue";
import FormTextArea from "~/components/Form/TextArea.vue";
import LocationSelector from "~/components/Location/Selector.vue";
import BaseContainer from "@/components/Base/Container.vue";
import Currency from "~/components/global/Currency.vue";
import DateTime from "~/components/global/DateTime.vue";
import LabelMaker from "~/components/global/LabelMaker.vue";
import Markdown from "~/components/global/Markdown.vue";
import BaseSectionHeader from "@/components/Base/SectionHeader.vue";
import ItemViewSelectable from "~/components/Item/View/Selectable.vue";
import LocationCard from "~/components/Location/Card.vue";
definePageMeta({
middleware: ["auth"],
@@ -106,6 +117,7 @@
const locationStore = useLocationStore();
const locations = computed(() => locationStore.allLocations);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parent = ref<LocationSummary | any>({});
const items = computedAsync(async () => {
@@ -213,8 +225,7 @@
</div>
</header>
<Separator v-if="location && location.description" />
<Markdown v-if="location && location.description" class="mt-3 text-base" :source="location.description">
</Markdown>
<Markdown v-if="location && location.description" class="mt-3 text-base" :source="location.description" />
</Card>
<section v-if="location && items">
<ItemViewSelectable :items="items" />

View File

@@ -4,9 +4,13 @@
import MdiCollapseAllOutline from "~icons/mdi/collapse-all-outline";
import MdiExpandAllOutline from "~icons/mdi/expand-all-outline";
import { ButtonGroup, Button } from "@/components/ui/button";
import { Button, ButtonGroup } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import type { TreeItem } from "~/lib/api/types/data-contracts";
import BaseContainer from "@/components/Base/Container.vue";
import BaseSectionHeader from "@/components/Base/SectionHeader.vue";
import LocationTreeRoot from "~/components/Location/Tree/Root.vue";
import BaseCard from "@/components/Base/Card.vue";
const { t } = useI18n();

View File

@@ -1,5 +1,8 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import BaseContainer from "@/components/Base/Container.vue";
import BaseSectionHeader from "@/components/Base/SectionHeader.vue";
import MaintenanceListView from "~/components/Maintenance/ListView.vue";
const { t } = useI18n();
@@ -15,7 +18,7 @@
<div>
<BaseContainer class="flex flex-col gap-4">
<BaseSectionHeader> {{ $t("menu.maintenance") }} </BaseSectionHeader>
<MaintenanceListView></MaintenanceListView>
<MaintenanceListView />
</BaseContainer>
</div>
</template>

View File

@@ -18,9 +18,19 @@
import { Label } from "@/components/ui/label";
import { badgeVariants } from "@/components/ui/badge";
import LanguageSelector from "~/components/App/LanguageSelector.vue";
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { DialogID } from "~/components/ui/dialog-provider/utils";
import ThemePicker from "~/components/App/ThemePicker.vue";
import ItemDuplicateSettings from "~/components/Item/DuplicateSettings.vue";
import FormPassword from "~/components/Form/Password.vue";
import FormCheckbox from "~/components/Form/Checkbox.vue";
import FormTextField from "~/components/Form/TextField.vue";
import BaseContainer from "@/components/Base/Container.vue";
import BaseCard from "@/components/Base/Card.vue";
import BaseSectionHeader from "@/components/Base/SectionHeader.vue";
import DetailsSection from "@/components/global/DetailsSection/DetailsSection.vue";
import CopyText from "@/components/global/CopyText.vue";
import DateTime from "@/components/global/DateTime.vue";
const { t } = useI18n();
@@ -360,7 +370,7 @@
<Button variant="secondary" :disabled="!(notifier && notifier.url)" type="button" @click="testNotifier">
{{ $t("profile.test") }}
</Button>
<div class="grow"></div>
<div class="grow" />
<Button type="submit"> {{ $t("global.submit") }} </Button>
</DialogFooter>
</div>

View File

@@ -2,7 +2,7 @@
import { useI18n } from "vue-i18n";
import DOMPurify from "dompurify";
import { route } from "../../lib/api/base";
import { toast, Toaster } from "@/components/ui/sonner";
import { Toaster, toast } from "@/components/ui/sonner";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
@@ -319,7 +319,7 @@
});
}
page.rows[page.rows.length - 1].items.push(item);
page.rows[page.rows.length - 1]!.items.push(item);
}
calc.push(page);
@@ -344,12 +344,12 @@
<p>
{{ $t("reports.label_generator.instruction_2") }}
</p>
<p v-html="DOMPurify.sanitize($t('reports.label_generator.instruction_3'))"></p>
<p v-html="DOMPurify.sanitize($t('reports.label_generator.instruction_3'))" />
<h2>{{ $t("reports.label_generator.tips") }}</h2>
<ul>
<li v-html="DOMPurify.sanitize($t('reports.label_generator.tip_1'))"></li>
<li v-html="DOMPurify.sanitize($t('reports.label_generator.tip_2'))"></li>
<li v-html="DOMPurify.sanitize($t('reports.label_generator.tip_3'))"></li>
<li v-html="DOMPurify.sanitize($t('reports.label_generator.tip_1'))" />
<li v-html="DOMPurify.sanitize($t('reports.label_generator.tip_2'))" />
<li v-html="DOMPurify.sanitize($t('reports.label_generator.tip_3'))" />
</ul>
<div class="flex flex-wrap gap-2">
<NuxtLink href="/tools">{{ $t("menu.tools") }}</NuxtLink>

View File

@@ -40,7 +40,7 @@
<DetailAction @action="openDialog(DialogID.Import)">
<template #title> {{ $t("tools.import_export_set.import") }} </template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="DOMPurify.sanitize($t('tools.import_export_set.import_sub'))"></div>
<div v-html="DOMPurify.sanitize($t('tools.import_export_set.import_sub'))" />
<template #button> {{ $t("tools.import_export_set.import_button") }} </template>
</DetailAction>
<DetailAction @action="getExportCSV()">
@@ -57,7 +57,7 @@
<span> {{ $t("tools.actions") }} </span>
<template #description>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="DOMPurify.sanitize($t('tools.actions_sub'))"></div>
<div v-html="DOMPurify.sanitize($t('tools.actions_sub'))" />
</template>
</BaseSectionHeader>
</template>
@@ -75,19 +75,19 @@
<DetailAction @action="resetItemDateTimes">
<template #title> {{ $t("tools.actions_set.zero_datetimes") }} </template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="DOMPurify.sanitize($t('tools.actions_set.zero_datetimes_sub'))"></div>
<div v-html="DOMPurify.sanitize($t('tools.actions_set.zero_datetimes_sub'))" />
<template #button> {{ $t("tools.actions_set.zero_datetimes_button") }} </template>
</DetailAction>
<DetailAction @action="setPrimaryPhotos">
<template #title> {{ $t("tools.actions_set.set_primary_photo") }} </template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="DOMPurify.sanitize($t('tools.actions_set.set_primary_photo_sub'))"></div>
<div v-html="DOMPurify.sanitize($t('tools.actions_set.set_primary_photo_sub'))" />
<template #button> {{ $t("tools.actions_set.set_primary_photo_button") }} </template>
</DetailAction>
<DetailAction @action="createMissingThumbnails">
<template #title> {{ $t("tools.actions_set.create_missing_thumbnails") }} </template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="DOMPurify.sanitize($t('tools.actions_set.create_missing_thumbnails_sub'))"></div>
<div v-html="DOMPurify.sanitize($t('tools.actions_set.create_missing_thumbnails_sub'))" />
<template #button> {{ $t("tools.actions_set.create_missing_thumbnails_button") }} </template>
</DetailAction>
</div>
@@ -106,6 +106,11 @@
import MdiAlert from "~icons/mdi/alert";
import { useDialog } from "~/components/ui/dialog-provider";
import { DialogID } from "~/components/ui/dialog-provider/utils";
import AppImportDialog from "@/components/App/ImportDialog.vue";
import BaseContainer from "@/components/Base/Container.vue";
import BaseCard from "@/components/Base/Card.vue";
import BaseSectionHeader from "@/components/Base/SectionHeader.vue";
import DetailAction from "@/components/DetailAction.vue";
const { t } = useI18n();

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { CompileError, MessageContext } from "vue-i18n";
import { createI18n } from "vue-i18n";
import { IntlMessageFormat } from "intl-messageformat";
@@ -49,7 +50,7 @@ export const messages = () => {
};
export const messageCompiler: (
message: String | any,
message: string | any,
{
locale,
key,
@@ -74,7 +75,9 @@ export const messageCompiler: (
* If you would like to support it,
* You need to transform locale messages such as `json`, `yaml`, etc. with the bundle plugin.
*/
onError && onError(new Error("not support for AST") as CompileError);
if (onError) {
onError(new Error("not support for AST") as CompileError);
}
return () => key;
}
};

10387
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,10 @@
try {
console.log('Setting theme');
const theme = JSON.parse(
localStorage.getItem('homebox/preferences/location')
).theme;
console.log("Setting theme");
const theme = JSON.parse(localStorage.getItem("homebox/preferences/location")).theme;
if (theme) {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.classList.add('theme-' + theme);
document.documentElement.setAttribute("data-theme", theme);
document.documentElement.classList.add("theme-" + theme);
}
} catch (e) {
console.error('Failed to set theme', e);
console.error("Failed to set theme", e);
}

View File

@@ -1,5 +1,5 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
export default {
darkMode: ["class"],
safelist: [
"dark",

View File

@@ -1,4 +1,4 @@
import { test, expect } from "@playwright/test";
import { expect, test } from "@playwright/test";
test("valid login", async ({ page }) => {
await page.goto("/home");

View File

@@ -4,10 +4,10 @@ function globalTeardown() {
if (process.env.TEST_SHUTDOWN_API_SERVER) {
const pc = exec("pkill -SIGTERM api"); // Kill background API process
const fr = exec("pkill -SIGTERM task"); // Kill background Frontend process
pc.stdout?.on("data", (data: void) => {
pc.stdout?.on("data", (data: unknown) => {
console.log(`stdout: ${data}`);
});
fr.stdout?.on("data", (data: void) => {
fr.stdout?.on("data", (data: unknown) => {
console.log(`stdout: ${data}`);
});
}

View File

@@ -1,8 +1,8 @@
import path from "path";
import { defineConfig } from "vite";
export default async () => {
const { defineConfig } = await import("vitest/config");
const path = await import("path");
export default defineConfig({
// @ts-ignore
return defineConfig({
test: {
globalSetup: "./test/setup.ts",
include: ["**/*.test.ts"],
@@ -14,3 +14,4 @@ export default defineConfig({
},
},
});
};

Some files were not shown because too many files have changed in this diff Show More