Compare commits
31 Commits
v0.21.0
...
tonya/upgr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b239567c81 | ||
|
|
642b7e8801 | ||
|
|
ae234993e5 | ||
|
|
a4fc3f03f9 | ||
|
|
715e6da380 | ||
|
|
f20204127b | ||
|
|
790352da34 | ||
|
|
d345dc6b71 | ||
|
|
b5834818c9 | ||
|
|
235929de77 | ||
|
|
5553ad6d55 | ||
|
|
8d84f06ed1 | ||
|
|
4e0dd04b42 | ||
|
|
4ee569dc71 | ||
|
|
9fc8cf4e8f | ||
|
|
e2ca22e0e2 | ||
|
|
73179ea8f5 | ||
|
|
52a6a31098 | ||
|
|
1d02285b0d | ||
|
|
19563d8b38 | ||
|
|
282977e82c | ||
|
|
769d5c5b95 | ||
|
|
b8f7ce7eb2 | ||
|
|
62ed3fabc2 | ||
|
|
304fc7f11f | ||
|
|
1b7a7a1999 | ||
|
|
a63f08ad87 | ||
|
|
9cb1a3f83c | ||
|
|
f86d38412b | ||
|
|
cbbe056d01 | ||
|
|
5f6b1a0805 |
21
.github/workflows/binaries-publish.yaml
vendored
@@ -1,6 +1,7 @@
|
||||
name: Publish Release Binaries
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags: [ 'v*.*.*' ]
|
||||
|
||||
@@ -8,6 +9,10 @@ jobs:
|
||||
goreleaser:
|
||||
name: goreleaser
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -37,6 +42,7 @@ jobs:
|
||||
go install github.com/sigstore/cosign/cmd/cosign@latest
|
||||
|
||||
- name: Run GoReleaser
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
workdir: "backend"
|
||||
@@ -45,3 +51,18 @@ jobs:
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
|
||||
COSIGN_YES: "true"
|
||||
|
||||
- name: Run GoReleaser No Release
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
workdir: "backend"
|
||||
distribution: goreleaser
|
||||
version: "~> v2"
|
||||
args: release --clean --snapshot --skip=publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
|
||||
COSIGN_YES: "true"
|
||||
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",
|
||||
|
||||
@@ -20,7 +20,7 @@ HomeBox is the inventory and organization system built for the Home User! With a
|
||||
- _Portable_ - Homebox is designed to be portable and run on anywhere. We use SQLite and an embedded Web UI to make it easy to deploy, use, and backup.
|
||||
|
||||
# Screenshots
|
||||
Check out screenshots of the project [here](https://imgur.com/a/5gLWt2j).
|
||||
Check out screenshots of the project [here](https://github.com/sysadminsmedia/homebox/tree/main/screenshots).
|
||||
You can also try the demo instances of Homebox:
|
||||
- [Demo](https://demo.homebox.software)
|
||||
- [Nightly](https://nightly.homebox.software)
|
||||
|
||||
BIN
Screenshots/1.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
Screenshots/10.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
Screenshots/2.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
Screenshots/3.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
Screenshots/4.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
Screenshots/5.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
Screenshots/6.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
Screenshots/7.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
Screenshots/8.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
Screenshots/9.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
8
Screenshots/readme.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Screenshots
|
||||
|
||||
These screenshots are taken from our public [Demo](https://demo.homebox.software) instance.
|
||||
Note that whilst we will make every effort to ensure that these are maintained and updated, they may be outdated or missing functionality and we would always advise reviewing our demo instances:
|
||||
|
||||
- [Demo](https://demo.homebox.software)
|
||||
- [Nightly](https://nightly.homebox.software)
|
||||
- [VNext](https://vnext.homebox.software/)
|
||||
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
|
||||
|
||||
@@ -16,11 +16,11 @@ aside: false
|
||||
| HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID | true | auto-increments the asset_id field for new items |
|
||||
| HBOX_OPTIONS_CURRENCY_CONFIG | | json configuration file containing additional currencie |
|
||||
| HBOX_OPTIONS_ALLOW_ANALYTICS | false | Allows the homebox team to view extremely basic information about the system that your running on. This helps make decisions regarding builds and other general decisions. |
|
||||
| HBOX_WEB_MAX_UPLOAD | 10 | maximum file upload size supported in MB |
|
||||
| HBOX_WEB_MAX_UPLOAD_SIZE | 10 | maximum file upload size supported in MB |
|
||||
| HBOX_WEB_READ_TIMEOUT | 10s | Read timeout of HTTP sever |
|
||||
| HBOX_WEB_WRITE_TIMEOUT | 10s | Write timeout of HTTP server |
|
||||
| HBOX_WEB_IDLE_TIMEOUT | 30s | Idle timeout of HTTP server |
|
||||
| HBOX_STORAGE_CONN_STRING | file://./ | path to the data directory, do not change this if you're using docker |
|
||||
| HBOX_STORAGE_CONN_STRING | file:///./ | path to the data directory, do not change this if you're using docker |
|
||||
| HBOX_STORAGE_PREFIX_PATH | .data | prefix path for the storage, if not set the storage will be used as is |
|
||||
| HBOX_LOG_LEVEL | `info` | log level to use, can be one of `trace`, `debug`, `info`, `warn`, `error`, `critical` |
|
||||
| HBOX_LOG_FORMAT | `text` | log format to use, can be one of: `text`, `json` |
|
||||
@@ -29,20 +29,18 @@ aside: false
|
||||
| HBOX_MAILER_USERNAME | | email user to use |
|
||||
| HBOX_MAILER_PASSWORD | | email password to use |
|
||||
| HBOX_MAILER_FROM | | email from address to use |
|
||||
| HBOX_SWAGGER_HOST | 7745 | swagger host to use, if not set swagger will be disabled |
|
||||
| HBOX_SWAGGER_SCHEMA | `http` | swagger schema to use, can be one of: `http`, `https` |
|
||||
| HBOX_DATABASE_DRIVER | sqlite3 | sets the correct database type (`sqlite3` or `postgres`) |
|
||||
| HBOX_DATABASE_SQLITE_PATH | ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1 | sets the directory path for Sqlite |
|
||||
| HBOX_DATABASE_SQLITE_PATH | ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite | sets the directory path for Sqlite |
|
||||
| HBOX_DATABASE_HOST | | sets the hostname for a postgres database |
|
||||
| HBOX_DATABASE_PORT | | sets the port for a postgres database |
|
||||
| HBOX_DATABASE_USERNAME | | sets the username for a postgres connection (optional if using cert auth) |
|
||||
| HBOX_DATABASE_PASSWORD | | sets the password for a postgres connection (optional if using cert auth) |
|
||||
| HBOX_DATABASE_DATABASE | | sets the database for a postgres connection |
|
||||
| HBOX_DATABASE_SSL_MODE | | sets the sslmode for a postgres connection |
|
||||
| HBOX_DATABASE_SSL_MODE | prefer | sets the sslmode for a postgres connection |
|
||||
| HBOX_DATABASE_SSL_CERT | | sets the sslcert for a postgres connection (should be a path) |
|
||||
| HBOX_DATABASE_SSL_KEY | | sets the sslkey for a postgres connection (should be a path) |
|
||||
| HBOX_DATABASE_SSL_ROOTCERT | | sets the sslrootcert for a postgres connection (should be a path) |
|
||||
| HBOX_OPTIONS_CHECK_GITHUB_RELEASE | true | check for new github releases |
|
||||
| HBOX_DATABASE_SSL_ROOT_CERT | | sets the sslrootcert for a postgres connection (should be a path) |
|
||||
| HBOX_OPTIONS_GITHUB_RELEASE_CHECK | true | check for new github releases |
|
||||
| HBOX_LABEL_MAKER_WIDTH | 526 | width for generated labels in pixels |
|
||||
| HBOX_LABEL_MAKER_HEIGHT | 200 | height for generated labels in pixels |
|
||||
| HBOX_LABEL_MAKER_PADDING | 32 | space between elements on label |
|
||||
@@ -160,8 +158,8 @@ OPTIONS
|
||||
--mode/$HBOX_MODE <string> (default: development)
|
||||
--web-port/$HBOX_WEB_PORT <string> (default: 7745)
|
||||
--web-host/$HBOX_WEB_HOST <string>
|
||||
--web-max-file-upload/$HBOX_WEB_MAX_FILE_UPLOAD <int> (default: 10)
|
||||
--storage-conn-string/$HBOX_STORAGE_CONN_STRING <string> (default: file://./)
|
||||
--web-max-upload-size/$HBOX_WEB_MAX_UPLOAD_SIZE <int> (default: 10)
|
||||
--storage-conn-string/$HBOX_STORAGE_CONN_STRING <string> (default: file:///./)
|
||||
--storage-prefix-path/$HBOX_STORAGE_PREFIX_PATH <string> (default: .data)
|
||||
--log-level/$HBOX_LOG_LEVEL <string> (default: info)
|
||||
--log-format/$HBOX_LOG_FORMAT <string> (default: text)
|
||||
@@ -170,31 +168,29 @@ OPTIONS
|
||||
--mailer-username/$HBOX_MAILER_USERNAME <string>
|
||||
--mailer-password/$HBOX_MAILER_PASSWORD <string>
|
||||
--mailer-from/$HBOX_MAILER_FROM <string>
|
||||
--swagger-host/$HBOX_SWAGGER_HOST <string> (default: localhost:7745)
|
||||
--swagger-scheme/$HBOX_SWAGGER_SCHEME <string> (default: http)
|
||||
--demo/$HBOX_DEMO <bool>
|
||||
--debug-enabled/$HBOX_DEBUG_ENABLED <bool> (default: false)
|
||||
--debug-port/$HBOX_DEBUG_PORT <string> (default: 4000)
|
||||
--database-driver/$HBOX_DATABASE_DRIVER <string> (default: sqlite3)
|
||||
--database-sqlite-path/$HBOX_DATABASE_SQLITE_PATH <string> (default: ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1)
|
||||
--database-sqlite-path/$HBOX_DATABASE_SQLITE_PATH <string> (default: ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite)
|
||||
--database-host/$HBOX_DATABASE_HOST <string>
|
||||
--database-port/$HBOX_DATABASE_PORT <string>
|
||||
--database-username/$HBOX_DATABASE_USERNAME <string>
|
||||
--database-password/$HBOX_DATABASE_PASSWORD <string>
|
||||
--database-database/$HBOX_DATABASE_DATABASE <string>
|
||||
--database-ssl-mode/$HBOX_DATABASE_SSL_MODE <string>
|
||||
--database-ssl-mode/$HBOX_DATABASE_SSL_MODE <string> (default: prefer)
|
||||
--options-allow-registration/$HBOX_OPTIONS_ALLOW_REGISTRATION <bool> (default: true)
|
||||
--options-auto-increment-asset-id/$HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID <bool> (default: true)
|
||||
--options-currency-config/$HBOX_OPTIONS_CURRENCY_CONFIG <string>
|
||||
--options-check-github-release/$HBOX_OPTIONS_CHECK_GITHUB_RELEASE <bool> (default: true)
|
||||
--options-github-release-check/$HBOX_OPTIONS_GITHUB_RELEASE_CHECK <bool> (default: true)
|
||||
--options-allow-analytics/$HBOX_OPTIONS_ALLOW_ANALYTICS <bool> (default: false)
|
||||
--label-maker-width/$HBOX_LABEL_MAKER_WIDTH <int> (default: 526)
|
||||
--label-maker-height/$HBOX_LABEL_MAKER_HEIGHT <int> (default: 200)
|
||||
--label-maker-padding/$HBOX_LABEL_MAKER_PADDING <int> (default: 32)
|
||||
--label-maker-margin/$HBOX_LABEL_MAKER_MARGIN <int> (default: 32)
|
||||
--label-maker-margin/$HBOX_LABEL_MAKER_MARGIN <int> (default: 32)
|
||||
--label-maker-font-size/$HBOX_LABEL_MAKER_FONT_SIZE <float> (default: 32.0)
|
||||
--label-maker-print-command/$HBOX_LABEL_MAKER_PRINT_COMMAND <string>
|
||||
--label-maker-additional-information/$HBOX_LABEL_MAKER_DYNAMIC_LENGTH <string> (default: true)
|
||||
--label-maker-dynamic-length/$HBOX_LABEL_MAKER_DYNAMIC_LENGTH <bool> (default: true)
|
||||
--label-maker-additional-information/$HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION <string>
|
||||
--thumbnail-enabled/$HBOX_THUMBNAIL_ENABLED <bool> (default: true)
|
||||
--thumbnail-width/$HBOX_THUMBNAIL_WIDTH <int> (default: 500)
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -1046,4 +1046,32 @@
|
||||
:root {
|
||||
--header-height: 4rem;
|
||||
--header-height-mobile: 7rem;
|
||||
}
|
||||
|
||||
/* Non-scoped styles for regular text */
|
||||
.break-all {
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Handle very long words */
|
||||
pre,
|
||||
code,
|
||||
a,
|
||||
p,
|
||||
span,
|
||||
div,
|
||||
td,
|
||||
th,
|
||||
li,
|
||||
blockquote,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: normal;
|
||||
hyphens: auto;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -30,7 +33,7 @@
|
||||
<template>
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-2 mt-4 flex items-center justify-between">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex gap-2 text-nowrap">
|
||||
{{ $t("components.item.view.selectable.items") }}
|
||||
<Badge>
|
||||
{{ items.length }}
|
||||
|
||||
@@ -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: {
|
||||
@@ -144,37 +148,5 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Non-scoped styles for regular text */
|
||||
.break-all {
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Handle very long words */
|
||||
pre,
|
||||
code,
|
||||
a,
|
||||
p,
|
||||
span,
|
||||
div,
|
||||
td,
|
||||
th,
|
||||
li,
|
||||
blockquote,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
-ms-word-break: break-all;
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
-ms-hyphens: auto;
|
||||
-moz-hyphens: auto;
|
||||
-webkit-hyphens: auto;
|
||||
hyphens: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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,21 +32,22 @@ export enum DialogID {
|
||||
* - Keys not present => no params allowed
|
||||
*/
|
||||
export type DialogParamsMap = {
|
||||
[DialogID.ItemImage]:
|
||||
| ({
|
||||
type: 'preloaded';
|
||||
[DialogID.ItemImage]: (
|
||||
| {
|
||||
type: "preloaded";
|
||||
originalSrc: string;
|
||||
originalType?: string;
|
||||
thumbnailSrc?: string;
|
||||
}
|
||||
| {
|
||||
type: 'attachment';
|
||||
type: "attachment";
|
||||
mimeType: string;
|
||||
thumbnailId?: string;
|
||||
}) & {
|
||||
itemId: string;
|
||||
attachmentId: string;
|
||||
};
|
||||
}
|
||||
) & {
|
||||
itemId: string;
|
||||
attachmentId: string;
|
||||
};
|
||||
[DialogID.CreateItem]?: { product?: BarcodeProduct };
|
||||
[DialogID.ProductImport]?: { barcode?: string };
|
||||
};
|
||||
@@ -54,11 +56,12 @@ export type DialogParamsMap = {
|
||||
* Defines the payload type for a dialog's onClose callback.
|
||||
*/
|
||||
export type DialogResultMap = {
|
||||
[DialogID.ItemImage]?: { action: 'delete', id: string };
|
||||
[DialogID.ItemImage]?: { action: "delete"; id: string };
|
||||
};
|
||||
|
||||
/** Helpers to split IDs by requirement */
|
||||
type OptionalKeys<T> = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
@@ -69,13 +72,9 @@ export type NoParamDialogIDs = Exclude<DialogID, SpecifiedDialogIDs>;
|
||||
export type RequiredDialogIDs = RequiredKeys<DialogParamsMap>;
|
||||
export type OptionalDialogIDs = OptionalKeys<DialogParamsMap>;
|
||||
|
||||
type ParamsOf<T extends DialogID> = T extends SpecifiedDialogIDs
|
||||
? DialogParamsMap[T]
|
||||
: never;
|
||||
type ParamsOf<T extends DialogID> = T extends SpecifiedDialogIDs ? DialogParamsMap[T] : never;
|
||||
|
||||
type ResultOf<T extends DialogID> = T extends keyof DialogResultMap
|
||||
? DialogResultMap[T]
|
||||
: void;
|
||||
type ResultOf<T extends DialogID> = T extends keyof DialogResultMap ? DialogResultMap[T] : void;
|
||||
|
||||
type OpenDialog = {
|
||||
// Dialogs with no parameters
|
||||
@@ -101,22 +100,14 @@ type CloseDialog = {
|
||||
// Close a specific dialog that has a defined result type.
|
||||
<T extends keyof DialogResultMap>(dialogId: T, result?: ResultOf<T>): void;
|
||||
// Close a specific dialog that has NO defined result type.
|
||||
<T extends Exclude<DialogID, keyof DialogResultMap>>(
|
||||
dialogId: T,
|
||||
result?: never
|
||||
): void;
|
||||
<T extends Exclude<DialogID, keyof DialogResultMap>>(dialogId: T, result?: never): void;
|
||||
<T extends DialogID>(dialogId: T): void;
|
||||
};
|
||||
|
||||
type OpenCallback = {
|
||||
<T extends NoParamDialogIDs>(dialogId: T, cb: () => void): () => void;
|
||||
<T extends RequiredDialogIDs>(
|
||||
dialogId: T,
|
||||
cb: (params: ParamsOf<T>) => void
|
||||
): () => void;
|
||||
<T extends OptionalDialogIDs>(
|
||||
dialogId: T,
|
||||
cb: (params?: ParamsOf<T>) => void
|
||||
): () => void;
|
||||
<T extends RequiredDialogIDs>(dialogId: T, cb: (params: ParamsOf<T>) => void): () => void;
|
||||
<T extends OptionalDialogIDs>(dialogId: T, cb: (params?: ParamsOf<T>) => void): () => void;
|
||||
};
|
||||
|
||||
export const [useDialog, provideDialogContext] = createContext<{
|
||||
@@ -127,7 +118,7 @@ export const [useDialog, provideDialogContext] = createContext<{
|
||||
closeDialog: CloseDialog;
|
||||
addAlert: (alertId: string) => void;
|
||||
removeAlert: (alertId: string) => void;
|
||||
}>('DialogProvider');
|
||||
}>("DialogProvider");
|
||||
|
||||
/**
|
||||
* Hotkey helper:
|
||||
@@ -140,36 +131,27 @@ type HotkeyKey = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
export function useDialogHotkey<T extends NoParamDialogIDs | OptionalDialogIDs>(
|
||||
dialogId: T,
|
||||
key: HotkeyKey
|
||||
): void;
|
||||
export function useDialogHotkey<T extends NoParamDialogIDs | OptionalDialogIDs>(dialogId: T, key: HotkeyKey): void;
|
||||
export function useDialogHotkey<T extends RequiredDialogIDs>(
|
||||
dialogId: T,
|
||||
key: HotkeyKey,
|
||||
getParams: () => ParamsOf<T>
|
||||
): void;
|
||||
export function useDialogHotkey(
|
||||
dialogId: DialogID,
|
||||
key: HotkeyKey,
|
||||
getParams?: () => unknown
|
||||
) {
|
||||
export function useDialogHotkey(dialogId: DialogID, key: HotkeyKey, getParams?: () => unknown) {
|
||||
const { openDialog } = useDialog();
|
||||
|
||||
const activeElement = useActiveElement();
|
||||
|
||||
const notUsingInput = computed(
|
||||
() =>
|
||||
activeElement.value?.tagName !== 'INPUT' &&
|
||||
activeElement.value?.tagName !== 'TEXTAREA'
|
||||
() => activeElement.value?.tagName !== "INPUT" && activeElement.value?.tagName !== "TEXTAREA"
|
||||
);
|
||||
|
||||
useMagicKeys({
|
||||
passive: false,
|
||||
onEventFired: (event) => {
|
||||
onEventFired: event => {
|
||||
if (
|
||||
notUsingInput.value &&
|
||||
event.type === 'keydown' &&
|
||||
event.type === "keydown" &&
|
||||
event.code === key.code &&
|
||||
(key.shift === undefined || event.shiftKey === key.shift) &&
|
||||
(key.ctrl === undefined || event.ctrlKey === key.ctrl)
|
||||
@@ -185,4 +167,4 @@ export function useDialogHotkey(
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
2
frontend/global.d.ts
vendored
@@ -1 +1 @@
|
||||
/// <reference types="unplugin-icons/types/vue" />
|
||||
/// <reference types="unplugin-icons/types/vue" />
|
||||
|
||||
@@ -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 } = await api.stats.group();
|
||||
return data;
|
||||
});
|
||||
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 } = await api.items.getAll({
|
||||
page: 1,
|
||||
pageSize: 5,
|
||||
orderBy: "createdAt",
|
||||
});
|
||||
return data.items;
|
||||
});
|
||||
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,10 +23,23 @@
|
||||
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();
|
||||
|
||||
const { openDialog } = useDialog();
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
|
||||
definePageMeta({
|
||||
middleware: ["auth"],
|
||||
@@ -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() {
|
||||
|
||||