Compare commits

..

31 Commits

Author SHA1 Message Date
tonyaellie
b239567c81 fix: update E2E_BASE_URL and remove wait for timeout in login test for smoother execution 2025-09-03 22:09:55 +01:00
tonyaellie
642b7e8801 fix: update baseURL in Playwright config for local testing to use port 7745 2025-09-03 22:03:42 +01:00
tonyaellie
ae234993e5 feat: add go:ci:with-frontend task for CI mode and remove ui:ci:preview from e2e workflow 2025-09-03 21:56:02 +01:00
tonyaellie
a4fc3f03f9 fix: i was silly 2025-09-03 19:56:58 +01:00
tonyaellie
715e6da380 fix: add ui:ci:preview task for frontend build in CI mode 2025-09-03 18:42:29 +01:00
Tonya
f20204127b Merge branch 'main' into tonya/upgrade-frontend-deps 2025-09-02 13:40:15 +01:00
Matthieu Evrin
790352da34 fix(item): remove line break in Items label in location view (#975)
fix: prevent items word wrapped in firefox

Signed-off-by: lekaf974 <matthieu.evrin@gmail.com>
2025-09-01 22:52:14 +01:00
tonyaellie
d345dc6b71 fix: add time out to try and fix issues 2025-09-01 10:18:08 +01:00
tonyaellie
b5834818c9 fix: add missing import 2025-09-01 10:02:53 +01:00
tonyaellie
235929de77 fix: update vitest config for dynamic import of path and defineConfig 2025-08-30 14:06:45 +01:00
tonyaellie
5553ad6d55 fix: try sorting issue with workflows 2025-08-30 14:00:47 +01:00
tonyaellie
8d84f06ed1 fix: nuxt is the enemy 2025-08-29 22:44:05 +01:00
tonyaellie
4e0dd04b42 fix: import sonner styles 2025-08-29 22:35:12 +01:00
tonyaellie
4ee569dc71 fix: sort type issues 2025-08-29 22:26:23 +01:00
tonyaellie
9fc8cf4e8f feat: sort all type issues 2025-08-29 19:37:44 +01:00
tonyaellie
e2ca22e0e2 feat: progress 2025-08-29 17:14:09 +01:00
tonyaellie
73179ea8f5 feat: begin upgrading deps, still very buggy 2025-08-29 13:46:45 +01:00
tonyaellie
52a6a31098 fix: import close dialog 2025-08-27 19:28:28 +00:00
Katos
1d02285b0d Merge pull request #962 from sysadminsmedia/katos/screenshots
Migrate Screenshots from Imgur to Github
2025-08-24 17:19:22 +01:00
Katos
19563d8b38 Update readme to point to new screenshots folder 2025-08-24 17:16:37 +01:00
Katos
282977e82c Upload example screenshots
Upload screenshots to Github repository
2025-08-24 17:15:46 +01:00
Katos
769d5c5b95 Create screenshots folder and readme 2025-08-24 17:15:07 +01:00
rapidcow
b8f7ce7eb2 doc fix: match configure option names with help message (#959)
* doc fix: match configure option names with help message (1/2)

This is a first commit in an attempt to reconcile the differences
between the /en/configure/index doc page and the automatically
generated help message.  This addresses typos including, though not
limited to, Discussion #954, titled "[doc] apparent typo in the
documentation of GitHub release check option".

This commit fixes the CLI help command, preserving the original
order, while manually matching the option names with the help
message generated by the backend api executable.

Options are only checked for spelling correctness and existence.
In particular, the following are removed because i could not
find them in the help message.

   * --swagger-host/$HBOX_SWAGGER_HOST <string> (default: localhost:7745)
   * --swagger-scheme/$HBOX_SWAGGER_SCHEME <string> (default: http)

The following default values have also been updated:

   * --storage-conn-string/$HBOX_STORAGE_CONN_STRING
      (a slash is added to the URI path)
   * --database-sqlite-path/$HBOX_DATABASE_SQLITE_PATH
      (a query param '&_time_format=sqlite' is added)
   * --database-ssl-mode/$HBOX_DATABASE_SSL_MODE
      (default 'prefer' added)

* doc fix: match configure option names with help message (2/2)

This is a second commit in an attempt to reconcile the differences
between the /en/configure/index doc page and the automatically
generated help message.  See the previous commit for details.

This commit fixes the Markdown table.

Options are only checked for spelling correctness and existence.
The following rows are deleted in particular:

   * HBOX_SWAGGER_HOST
   * HBOX_SWAGGER_SCHEME

The following default values are updated:

   * HBOX_STORAGE_CONN_STRING
      (a slash is added to the URI path)
   * HBOX_DATABASE_SQLITE_PATH
      (a query param '&_time_format=sqlite' is added)
   * HBOX_DATABASE_SSL_MODE
      (default 'prefer' added)
2025-08-23 21:17:26 -04:00
Matthew Kilgore
62ed3fabc2 Fix broken test version of binary build 2025-08-23 17:29:21 -04:00
Matthew Kilgore
304fc7f11f Fix YAML maybe 2025-08-23 17:24:10 -04:00
Matthew Kilgore
1b7a7a1999 Fix YAML maybe 2025-08-23 17:22:29 -04:00
Matthew Kilgore
a63f08ad87 Fix YAML maybe 2025-08-23 17:21:21 -04:00
Matthew Kilgore
9cb1a3f83c Fix YAML maybe 2025-08-23 17:21:01 -04:00
Matthew Kilgore
f86d38412b Fix YAML maybe 2025-08-23 17:20:16 -04:00
Matthew Kilgore
cbbe056d01 Let us test binary builds without publishing new tags 2025-08-23 17:17:10 -04:00
Katos
5f6b1a0805 Update binaries-publish.yaml
Add COSIGN_PWD and COSIGN_YES to workflow to rectify issues with binaries building on Action
2025-08-23 20:07:12 +01:00
118 changed files with 13387 additions and 5926 deletions

View File

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

View File

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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
Screenshots/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
Screenshots/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
Screenshots/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
Screenshots/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
Screenshots/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
Screenshots/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
Screenshots/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
Screenshots/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
Screenshots/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

8
Screenshots/readme.md Normal file
View 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/)

View File

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

View File

@@ -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)

View File

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

View File

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

View File

@@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,28 +1,29 @@
import { computed, type ComputedRef } from 'vue';
import { createContext } from 'reka-ui';
import { useMagicKeys, useActiveElement } from '@vueuse/core';
import type { BarcodeProduct } from '~~/lib/api/types/data-contracts';
/* eslint-disable @typescript-eslint/unified-signatures */
import { computed, type ComputedRef } from "vue";
import { createContext } from "reka-ui";
import { useMagicKeys, useActiveElement } from "@vueuse/core";
import type { BarcodeProduct } from "~~/lib/api/types/data-contracts";
export enum DialogID {
AttachmentEdit = 'attachment-edit',
ChangePassword = 'changePassword',
CreateItem = 'create-item',
CreateLocation = 'create-location',
CreateLabel = 'create-label',
CreateNotifier = 'create-notifier',
DuplicateSettings = 'duplicate-settings',
DuplicateTemporarySettings = 'duplicate-temporary-settings',
EditMaintenance = 'edit-maintenance',
Import = 'import',
ItemImage = 'item-image',
ItemTableSettings = 'item-table-settings',
PrintLabel = 'print-label',
ProductImport = 'product-import',
QuickMenu = 'quick-menu',
Scanner = 'scanner',
PageQRCode = 'page-qr-code',
UpdateLabel = 'update-label',
UpdateLocation = 'update-location',
AttachmentEdit = "attachment-edit",
ChangePassword = "changePassword",
CreateItem = "create-item",
CreateLocation = "create-location",
CreateLabel = "create-label",
CreateNotifier = "create-notifier",
DuplicateSettings = "duplicate-settings",
DuplicateTemporarySettings = "duplicate-temporary-settings",
EditMaintenance = "edit-maintenance",
Import = "import",
ItemImage = "item-image",
ItemTableSettings = "item-table-settings",
PrintLabel = "print-label",
ProductImport = "product-import",
QuickMenu = "quick-menu",
Scanner = "scanner",
PageQRCode = "page-qr-code",
UpdateLabel = "update-label",
UpdateLocation = "update-location",
}
/**
@@ -31,21 +32,22 @@ export enum DialogID {
* - Keys not present => no params allowed
*/
export type DialogParamsMap = {
[DialogID.ItemImage]:
| ({
type: 'preloaded';
[DialogID.ItemImage]: (
| {
type: "preloaded";
originalSrc: string;
originalType?: string;
thumbnailSrc?: string;
}
| {
type: 'attachment';
type: "attachment";
mimeType: string;
thumbnailId?: string;
}) & {
itemId: string;
attachmentId: string;
};
}
) & {
itemId: string;
attachmentId: string;
};
[DialogID.CreateItem]?: { product?: BarcodeProduct };
[DialogID.ProductImport]?: { barcode?: string };
};
@@ -54,11 +56,12 @@ export type DialogParamsMap = {
* Defines the payload type for a dialog's onClose callback.
*/
export type DialogResultMap = {
[DialogID.ItemImage]?: { action: 'delete', id: string };
[DialogID.ItemImage]?: { action: "delete"; id: string };
};
/** Helpers to split IDs by requirement */
type OptionalKeys<T> = {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];
@@ -69,13 +72,9 @@ export type NoParamDialogIDs = Exclude<DialogID, SpecifiedDialogIDs>;
export type RequiredDialogIDs = RequiredKeys<DialogParamsMap>;
export type OptionalDialogIDs = OptionalKeys<DialogParamsMap>;
type ParamsOf<T extends DialogID> = T extends SpecifiedDialogIDs
? DialogParamsMap[T]
: never;
type ParamsOf<T extends DialogID> = T extends SpecifiedDialogIDs ? DialogParamsMap[T] : never;
type ResultOf<T extends DialogID> = T extends keyof DialogResultMap
? DialogResultMap[T]
: void;
type ResultOf<T extends DialogID> = T extends keyof DialogResultMap ? DialogResultMap[T] : void;
type OpenDialog = {
// Dialogs with no parameters
@@ -101,22 +100,14 @@ type CloseDialog = {
// Close a specific dialog that has a defined result type.
<T extends keyof DialogResultMap>(dialogId: T, result?: ResultOf<T>): void;
// Close a specific dialog that has NO defined result type.
<T extends Exclude<DialogID, keyof DialogResultMap>>(
dialogId: T,
result?: never
): void;
<T extends Exclude<DialogID, keyof DialogResultMap>>(dialogId: T, result?: never): void;
<T extends DialogID>(dialogId: T): void;
};
type OpenCallback = {
<T extends NoParamDialogIDs>(dialogId: T, cb: () => void): () => void;
<T extends RequiredDialogIDs>(
dialogId: T,
cb: (params: ParamsOf<T>) => void
): () => void;
<T extends OptionalDialogIDs>(
dialogId: T,
cb: (params?: ParamsOf<T>) => void
): () => void;
<T extends RequiredDialogIDs>(dialogId: T, cb: (params: ParamsOf<T>) => void): () => void;
<T extends OptionalDialogIDs>(dialogId: T, cb: (params?: ParamsOf<T>) => void): () => void;
};
export const [useDialog, provideDialogContext] = createContext<{
@@ -127,7 +118,7 @@ export const [useDialog, provideDialogContext] = createContext<{
closeDialog: CloseDialog;
addAlert: (alertId: string) => void;
removeAlert: (alertId: string) => void;
}>('DialogProvider');
}>("DialogProvider");
/**
* Hotkey helper:
@@ -140,36 +131,27 @@ type HotkeyKey = {
code: string;
};
export function useDialogHotkey<T extends NoParamDialogIDs | OptionalDialogIDs>(
dialogId: T,
key: HotkeyKey
): void;
export function useDialogHotkey<T extends NoParamDialogIDs | OptionalDialogIDs>(dialogId: T, key: HotkeyKey): void;
export function useDialogHotkey<T extends RequiredDialogIDs>(
dialogId: T,
key: HotkeyKey,
getParams: () => ParamsOf<T>
): void;
export function useDialogHotkey(
dialogId: DialogID,
key: HotkeyKey,
getParams?: () => unknown
) {
export function useDialogHotkey(dialogId: DialogID, key: HotkeyKey, getParams?: () => unknown) {
const { openDialog } = useDialog();
const activeElement = useActiveElement();
const notUsingInput = computed(
() =>
activeElement.value?.tagName !== 'INPUT' &&
activeElement.value?.tagName !== 'TEXTAREA'
() => activeElement.value?.tagName !== "INPUT" && activeElement.value?.tagName !== "TEXTAREA"
);
useMagicKeys({
passive: false,
onEventFired: (event) => {
onEventFired: event => {
if (
notUsingInput.value &&
event.type === 'keydown' &&
event.type === "keydown" &&
event.code === key.code &&
(key.shift === undefined || event.shiftKey === key.shift) &&
(key.ctrl === undefined || event.ctrlKey === key.ctrl)
@@ -185,4 +167,4 @@ export function useDialogHotkey(
}
},
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
/// <reference types="unplugin-icons/types/vue" />
/// <reference types="unplugin-icons/types/vue" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,10 +10,16 @@ type StatCard = {
export function statCardData(api: UserClient) {
const { t } = useI18n();
const { data: statistics } = useAsyncData(async () => {
const { data } = 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 [

View File

@@ -1,14 +1,20 @@
import type { UserClient } from "~~/lib/api/user";
export function itemsTable(api: UserClient) {
const { data: items, refresh } = useAsyncData(async () => {
const { data } = 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");

View File

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

View File

@@ -23,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>

View File

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

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