diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts
index 36697fc7..15005f70 100644
--- a/frontend/lib/api/types/data-contracts.ts
+++ b/frontend/lib/api/types/data-contracts.ts
@@ -54,10 +54,10 @@ export enum AttachmentType {
export interface CurrenciesCurrency {
code: string;
+ decimals: number;
local: string;
name: string;
symbol: string;
- decimals: number;
}
export interface EntAttachment {
@@ -465,6 +465,13 @@ export interface BarcodeProduct {
search_engine_name: string;
}
+export interface DuplicateOptions {
+ copyAttachments: boolean;
+ copyCustomFields: boolean;
+ copyMaintenance: boolean;
+ copyPrefix: string;
+}
+
export interface Group {
createdAt: Date | string;
currency: string;
@@ -571,6 +578,8 @@ export interface ItemOut {
export interface ItemPatch {
id: string;
+ labelIds?: string[] | null;
+ locationId?: string | null;
quantity?: number | null;
}
diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts
index a3a1a7cf..83ca1af0 100644
--- a/frontend/lib/utils.ts
+++ b/frontend/lib/utils.ts
@@ -1,3 +1,4 @@
+import type { Updater } from "@tanstack/vue-table";
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
@@ -5,6 +6,11 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function valueUpdater
>(updaterOrValue: T, ref: Ref) {
+ ref.value = typeof updaterOrValue === "function" ? updaterOrValue(ref.value) : updaterOrValue;
+}
+
/**
* Returns either '#000' or '#fff' depending on which has better contrast with the given background color.
* Accepts hex (#RRGGBB or #RGB) or rgb(a) strings.
@@ -36,3 +42,5 @@ export function getContrastTextColor(bgColor: string): string {
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.5 ? "#000" : "#fff";
}
+
+export const camelToSnakeCase = (str: string) => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
diff --git a/frontend/locales/en.json b/frontend/locales/en.json
index 7c628b24..64a1f4d6 100644
--- a/frontend/locales/en.json
+++ b/frontend/locales/en.json
@@ -138,18 +138,53 @@
"searching": "Searching…"
},
"view": {
+ "change_details": {
+ "title": "Change Item Details",
+ "failed_to_update_item": "Failed to update item",
+ "add_labels": "Add Labels",
+ "remove_labels": "Remove Labels"
+ },
"selectable": {
"card": "Card",
"items": "Items",
"no_items": "No Items to Display",
- "table": "Table"
+ "table": "Table",
+ "select_all": "Select All",
+ "select_row": "Select Row",
+ "select_card": "Select Card"
},
"table": {
"headers": "Headers",
"page": "Page",
"rows_per_page": "Rows per page",
+ "quick_actions": "Enable Quick Actions & Selection",
"table_settings": "Table Settings",
- "view_item": "View Item"
+ "view_item": "View Item",
+ "selected_rows": "{selected} of {total} row(s) selected.",
+ "dropdown": {
+ "open_menu": "Open menu",
+ "actions": "Actions",
+ "view_item": "View item",
+ "view_items": "View items",
+ "toggle_expand": "Toggle Expand",
+ "download_csv": "Download Table as CSV",
+ "download_json": "Download Table as JSON",
+ "delete_selected": "Delete Selected Items",
+ "delete_item": "Delete Item",
+ "error_deleting": "Error Deleting Item",
+ "delete_confirmation": "Are you sure you want to delete the selected item(s)? This action cannot be undone.",
+ "duplicate_selected": "Duplicate Selected Items",
+ "duplicate_item": "Duplicate Item",
+ "error_duplicating": "Error Duplicating Item",
+ "open_multi_tab_warning": "For security reasons browsers do not allow multiple tabs to be opened at once by default, to change this please follow the documentation:",
+ "create_maintenance_selected": "Create Maintenance Entry for Selected Items",
+ "create_maintenance_item": "Create Maintenance Entry for Item",
+ "create_maintenance_success": "Maintenance Entry(s) Created",
+ "change_location": "Change Location",
+ "change_location_success": "Location Changed",
+ "change_labels": "Change Labels",
+ "change_labels_success": "Labels Changed"
+ }
}
}
},
diff --git a/frontend/package.json b/frontend/package.json
index b9cf4f5b..873fa7c5 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -50,6 +50,7 @@
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
+ "@tanstack/vue-table": "^8.21.3",
"@vuepic/vue-datepicker": "^11.0.2",
"@vueuse/core": "^13.8.0",
"@vueuse/nuxt": "^13.8.0",
diff --git a/frontend/pages/home/index.vue b/frontend/pages/home/index.vue
index 077f61ec..898220ad 100644
--- a/frontend/pages/home/index.vue
+++ b/frontend/pages/home/index.vue
@@ -8,10 +8,10 @@
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";
+ import Table from "~/components/Item/View/Table.vue";
const { t } = useI18n();
@@ -50,7 +50,7 @@
{{ $t("items.no_results") }}
-
+
diff --git a/frontend/pages/item/[id]/index.vue b/frontend/pages/item/[id]/index.vue
index db9ba48e..33e5b096 100644
--- a/frontend/pages/item/[id]/index.vue
+++ b/frontend/pages/item/[id]/index.vue
@@ -478,22 +478,28 @@
return resp.data;
});
- const items = computedAsync(async () => {
- if (!item.value) {
- return [];
+ const { data: items, refresh: refreshItemList } = useAsyncData(
+ () => itemId.value + "_item_list",
+ async () => {
+ if (!itemId.value) {
+ return [];
+ }
+
+ const resp = await api.items.getAll({
+ parentIds: [itemId.value],
+ });
+
+ if (resp.error) {
+ toast.error(t("items.toast.failed_load_items"));
+ return [];
+ }
+
+ return resp.data.items;
+ },
+ {
+ watch: [itemId],
}
-
- const resp = await api.items.getAll({
- parentIds: [item.value.id],
- });
-
- if (resp.error) {
- toast.error(t("items.toast.failed_load_items"));
- return [];
- }
-
- return resp.data.items;
- });
+ );
async function duplicateItem(settings?: DuplicateSettings) {
if (!item.value) {
@@ -787,7 +793,7 @@
diff --git a/frontend/pages/items.vue b/frontend/pages/items.vue
index c9114e24..cbdb5f71 100644
--- a/frontend/pages/items.vue
+++ b/frontend/pages/items.vue
@@ -6,7 +6,6 @@
import { useLabelStore } from "~~/stores/labels";
import { useLocationStore } from "~~/stores/locations";
import MdiLoading from "~icons/mdi/loading";
- import MdiSelectSearch from "~icons/mdi/select-search";
import MdiMagnify from "~icons/mdi/magnify";
import MdiDelete from "~icons/mdi/delete";
import { Button } from "@/components/ui/button";
@@ -15,18 +14,9 @@
import { Switch } from "@/components/ui/switch";
import { Separator } from "@/components/ui/separator";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
- import {
- Pagination,
- PaginationEllipsis,
- PaginationFirst,
- PaginationLast,
- PaginationList,
- PaginationListItem,
- } from "@/components/ui/pagination";
import BaseContainer from "@/components/Base/Container.vue";
- import BaseSectionHeader from "@/components/Base/SectionHeader.vue";
import SearchFilter from "~/components/Search/Filter.vue";
- import ItemCard from "~/components/Item/Card.vue";
+ import ItemViewSelectable from "~/components/Item/View/Selectable.vue";
const { t } = useI18n();
@@ -56,7 +46,6 @@
},
});
- const pageSize = useRouteQuery("pageSize", 24);
const query = useRouteQuery("q", "");
const advanced = useRouteQuery("advanced", false);
const includeArchived = useRouteQuery("archived", false);
@@ -66,7 +55,8 @@
const onlyWithPhoto = useRouteQuery("onlyWithPhoto", false);
const orderBy = useRouteQuery("orderBy", "name");
- const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
+ const preferences = useViewPreferences();
+ const pageSize = computed(() => preferences.value.itemsPerTablePage);
const route = useRoute();
const router = useRouter();
@@ -243,8 +233,7 @@
advanced: route.query.advanced,
q: query.value,
page: page.value,
- pageSize: pageSize.value,
- includeArchived: includeArchived.value ? "true" : "false",
+ archived: includeArchived.value ? "true" : "false",
negateLabels: negateLabels.value ? "true" : "false",
onlyWithoutPhoto: onlyWithoutPhoto.value ? "true" : "false",
onlyWithPhoto: onlyWithPhoto.value ? "true" : "false",
@@ -330,7 +319,6 @@
onlyWithoutPhoto: onlyWithoutPhoto.value ? "true" : "false",
onlyWithPhoto: onlyWithPhoto.value ? "true" : "false",
orderBy: orderBy.value,
- pageSize: pageSize.value,
page: page.value,
q: query.value,
@@ -361,7 +349,6 @@
query: {
archived: "false",
fieldSelector: "false",
- pageSize: pageSize.value,
page: 1,
orderBy: "name",
q: "",
@@ -373,6 +360,15 @@
await search();
}
+
+ const pagination = proxyRefs({
+ page,
+ pageSize,
+ totalSize: total,
+ setPage: (newPage: number) => {
+ page.value = newPage;
+ },
+ });
@@ -503,41 +499,12 @@
- {{ $t("global.items") }}
-
- {{ $t("items.results", { total: total }) }}
- {{ $t("items.pages", { page: page, totalPages: totalPages }) }}
-
-
-
-
-
{{ $t("items.no_results") }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ search()"
+ />
diff --git a/frontend/pages/label/[id].vue b/frontend/pages/label/[id].vue
index d04ae634..83620867 100644
--- a/frontend/pages/label/[id].vue
+++ b/frontend/pages/label/[id].vue
@@ -94,28 +94,34 @@
updating.value = false;
}
- const items = computedAsync(async () => {
- if (!label.value) {
- return {
- items: [],
- totalPrice: null,
- };
+ const { data: items, refresh: refreshItemList } = useAsyncData(
+ () => labelId.value + "_item_list",
+ async () => {
+ if (!labelId.value) {
+ return {
+ items: [],
+ totalPrice: null,
+ };
+ }
+
+ const resp = await api.items.getAll({
+ labels: [labelId.value],
+ });
+
+ if (resp.error) {
+ toast.error(t("items.toast.failed_load_items"));
+ return {
+ items: [],
+ totalPrice: null,
+ };
+ }
+
+ return resp.data;
+ },
+ {
+ watch: [labelId],
}
-
- const resp = await api.items.getAll({
- labels: [label.value.id],
- });
-
- if (resp.error) {
- toast.error(t("items.toast.failed_load_items"));
- return {
- items: [],
- totalPrice: null,
- };
- }
-
- return resp.data;
- });
+ );
@@ -200,7 +206,7 @@
diff --git a/frontend/pages/location/[id].vue b/frontend/pages/location/[id].vue
index 9db2e785..67395e4b 100644
--- a/frontend/pages/location/[id].vue
+++ b/frontend/pages/location/[id].vue
@@ -120,22 +120,28 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parent = ref({});
- const items = computedAsync(async () => {
- if (!location.value) {
- return [];
+ const { data: items, refresh: refreshItemList } = useAsyncData(
+ () => locationId.value + "_item_list",
+ async () => {
+ if (!locationId.value) {
+ return [];
+ }
+
+ const resp = await api.items.getAll({
+ locations: [locationId.value],
+ });
+
+ if (resp.error) {
+ toast.error(t("items.toast.failed_load_items"));
+ return [];
+ }
+
+ return resp.data.items;
+ },
+ {
+ watch: [locationId],
}
-
- const resp = await api.items.getAll({
- locations: [location.value.id],
- });
-
- if (resp.error) {
- toast.error(t("items.toast.failed_load_items"));
- return [];
- }
-
- return resp.data.items;
- });
+ );
@@ -228,7 +234,7 @@
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index e156ef87..7a8bcfac 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -29,6 +29,9 @@ importers:
'@tailwindcss/typography':
specifier: ^0.5.16
version: 0.5.16(tailwindcss@3.4.17)
+ '@tanstack/vue-table':
+ specifier: ^8.21.3
+ version: 8.21.3(vue@3.5.20(typescript@5.9.2))
'@vuepic/vue-datepicker':
specifier: ^11.0.2
version: 11.0.2(vue@3.5.20(typescript@5.9.2))
@@ -2187,9 +2190,19 @@ packages:
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
+ '@tanstack/table-core@8.21.3':
+ resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
+ engines: {node: '>=12'}
+
'@tanstack/virtual-core@3.13.12':
resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
+ '@tanstack/vue-table@8.21.3':
+ resolution: {integrity: sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ vue: '>=3.2'
+
'@tanstack/vue-virtual@3.13.12':
resolution: {integrity: sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==}
peerDependencies:
@@ -8897,8 +8910,15 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 3.4.17
+ '@tanstack/table-core@8.21.3': {}
+
'@tanstack/virtual-core@3.13.12': {}
+ '@tanstack/vue-table@8.21.3(vue@3.5.20(typescript@5.9.2))':
+ dependencies:
+ '@tanstack/table-core': 8.21.3
+ vue: 3.5.20(typescript@5.9.2)
+
'@tanstack/vue-virtual@3.13.12(vue@3.5.20(typescript@5.9.2))':
dependencies:
'@tanstack/virtual-core': 3.13.12