mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-21 21:33:02 +01:00
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:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -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",
|
||||
|
||||
15
Taskfile.yml
15
Taskfile.yml
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)");
|
||||
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const props = defineProps({
|
||||
text: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<template>
|
||||
<div class="max-w-full"></div>
|
||||
<div class="max-w-full" />
|
||||
</template>
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}"
|
||||
/>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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> & {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
72
frontend/eslint.config.mjs
Normal file
72
frontend/eslint.config.mjs
Normal 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -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 => ({
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { Toaster } from "~/components/ui/sonner";
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<Toaster />
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { BaseAPI, route } from "../base";
|
||||
import type {
|
||||
MaintenanceEntry,
|
||||
MaintenanceEntryWithDetails,
|
||||
MaintenanceEntryUpdate,
|
||||
MaintenanceEntryWithDetails,
|
||||
MaintenanceFilterStatus,
|
||||
} from "../types/data-contracts";
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { scorePassword } from ".";
|
||||
|
||||
describe("scorePassword tests", () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
10387
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
export default {
|
||||
darkMode: ["class"],
|
||||
safelist: [
|
||||
"dark",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user