Feat subitem create button (#691)

Co-authored-by: greg1904 <github@koppenatsch.de>
This commit is contained in:
greg1904
2025-05-11 21:46:53 +02:00
committed by GitHub
parent 469a7df448
commit e4a45ddb59
5 changed files with 100 additions and 7 deletions

View File

@@ -579,6 +579,10 @@ func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data ItemCr
SetLocationID(data.LocationID). SetLocationID(data.LocationID).
SetAssetID(int(data.AssetID)) SetAssetID(int(data.AssetID))
if data.ParentID != uuid.Nil {
q.SetParentID(data.ParentID)
}
if len(data.LabelIDs) > 0 { if len(data.LabelIDs) > 0 {
q.AddLabelIDs(data.LabelIDs...) q.AddLabelIDs(data.LabelIDs...)
} }

View File

@@ -2,6 +2,15 @@
<BaseModal dialog-id="create-item" :title="$t('components.item.create_modal.title')"> <BaseModal dialog-id="create-item" :title="$t('components.item.create_modal.title')">
<form class="flex flex-col gap-2" @submit.prevent="create()"> <form class="flex flex-col gap-2" @submit.prevent="create()">
<LocationSelector v-model="form.location" /> <LocationSelector v-model="form.location" />
<ItemSelector
v-if="subItemCreate"
v-model="parent"
v-model:search="query"
:label="$t('components.item.create_modal.parent_item')"
:items="results"
item-text="name"
no-results-text="Type to search..."
/>
<FormTextField <FormTextField
ref="nameInput" ref="nameInput"
v-model="form.name" v-model="form.name"
@@ -139,6 +148,7 @@
import { AttachmentTypes } from "~~/lib/api/types/non-generated"; import { AttachmentTypes } from "~~/lib/api/types/non-generated";
import { useDialog, useDialogHotkey } from "~/components/ui/dialog-provider"; import { useDialog, useDialogHotkey } from "~/components/ui/dialog-provider";
import LabelSelector from "~/components/Label/Selector.vue"; import LabelSelector from "~/components/Label/Selector.vue";
import ItemSelector from "~/components/Item/Selector.vue";
interface PhotoPreview { interface PhotoPreview {
photoName: string; photoName: string;
@@ -160,6 +170,12 @@
const labels = computed(() => labelStore.labels); const labels = computed(() => labelStore.labels);
const route = useRoute(); const route = useRoute();
const router = useRouter();
const parent = ref();
const { query, results } = useItemSearch(api, { immediate: false });
const subItemCreateParam = useRouteQuery("subItemCreate", "n");
const subItemCreate = ref();
const labelId = computed(() => { const labelId = computed(() => {
if (route.fullPath.includes("/label/")) { if (route.fullPath.includes("/label/")) {
@@ -175,12 +191,20 @@
return null; return null;
}); });
const itemId = computed(() => {
if (route.fullPath.includes("/item/")) {
return route.params.id;
}
return null;
});
const nameInput = ref<HTMLInputElement | null>(null); const nameInput = ref<HTMLInputElement | null>(null);
const loading = ref(false); const loading = ref(false);
const focused = ref(false); const focused = ref(false);
const form = reactive({ const form = reactive({
location: locations.value && locations.value.length > 0 ? locations.value[0] : ({} as LocationOut), location: locations.value && locations.value.length > 0 ? locations.value[0] : ({} as LocationOut),
parentId: null,
name: "", name: "",
quantity: 1, quantity: 1,
description: "", description: "",
@@ -189,6 +213,18 @@
photos: [] as PhotoPreview[], photos: [] as PhotoPreview[],
}); });
watch(
parent,
newParent => {
if (newParent && newParent.id && subItemCreate.value) {
form.parentId = newParent.id;
} else {
form.parentId = null;
}
},
{ immediate: true }
);
const { shift } = useMagicKeys(); const { shift } = useMagicKeys();
function deleteImage(index: number) { function deleteImage(index: number) {
@@ -223,10 +259,43 @@
watch( watch(
() => activeDialog.value, () => activeDialog.value,
active => { async active => {
if (active === "create-item") { if (active === "create-item") {
if (locationId.value) { // needed since URL will be cleared in the next step => ParentId Selection should stay though
const found = locations.value.find(l => l.id === locationId.value); subItemCreate.value = subItemCreateParam.value === "y";
let parentItemLocationId = null;
if (subItemCreate.value && itemId.value) {
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("Failed to load parent item - please select manually");
console.error("Parent item fetch error:", error);
}
if (data) {
parent.value = data;
}
if (data.location) {
const { location } = data;
parentItemLocationId = location.id;
}
// clear URL Parameter (subItemCreate) since intention was communicated and received
const currentQuery = { ...route.query };
delete currentQuery.subItemCreate;
await router.push({ query: currentQuery });
} else {
// since Input is hidden in this case, make sure no accidental parent information is sent out
parent.value = {};
form.parentId = null;
}
const locId = locationId.value ? locationId.value : parentItemLocationId;
if (locId) {
const found = locations.value.find(l => l.id === locId);
if (found) { if (found) {
form.location = found; form.location = found;
} }
@@ -254,7 +323,7 @@
if (shift.value) close = false; if (shift.value) close = false;
const out: ItemCreate = { const out: ItemCreate = {
parentId: null, parentId: form.parentId,
name: form.name, name: form.name,
quantity: form.quantity, quantity: form.quantity,
description: form.description, description: form.description,

View File

@@ -67,6 +67,7 @@
"item_name": "Gegenstandsname", "item_name": "Gegenstandsname",
"item_photo": "Artikel Bild", "item_photo": "Artikel Bild",
"item_quantity": "Artikel Menge", "item_quantity": "Artikel Menge",
"parent_item" :"Übergeordneter Gegenstand",
"title": "Gegenstand erstellen", "title": "Gegenstand erstellen",
"upload_photos": "Upload Bilder" "upload_photos": "Upload Bilder"
}, },
@@ -125,6 +126,7 @@
"create": "Erstellen", "create": "Erstellen",
"create_and_add": "Erstellen und weiteren hinzufügen", "create_and_add": "Erstellen und weiteren hinzufügen",
"created": "Erstellt", "created": "Erstellt",
"create_subitem": "Sub-Gegenstand erstellen",
"delete": "Löschen", "delete": "Löschen",
"details": "Details", "details": "Details",
"duplicate": "Duplizieren", "duplicate": "Duplizieren",
@@ -214,7 +216,7 @@
"options": "Optionen", "options": "Optionen",
"order_by": "Sortieren nach", "order_by": "Sortieren nach",
"pages": "Seite { page } von { totalPages }", "pages": "Seite { page } von { totalPages }",
"parent_item": "Übergeordnetes Element", "parent_item": "Übergeordneter Gegenstand",
"photo": "Foto", "photo": "Foto",
"photos": "Fotos", "photos": "Fotos",
"prev_page": "Vorherige Seite", "prev_page": "Vorherige Seite",

View File

@@ -67,6 +67,7 @@
"item_name": "Item Name", "item_name": "Item Name",
"item_photo": "Item Photo 📷", "item_photo": "Item Photo 📷",
"item_quantity": "Item Quantity", "item_quantity": "Item Quantity",
"parent_item" :"Parent Item",
"title": "Create Item", "title": "Create Item",
"upload_photos": "Upload Photos" "upload_photos": "Upload Photos"
}, },
@@ -125,6 +126,7 @@
"create": "Create", "create": "Create",
"create_and_add": "Create and Add Another", "create_and_add": "Create and Add Another",
"created": "Created", "created": "Created",
"create_subitem": "Create Subitem",
"delete": "Delete", "delete": "Delete",
"details": "Details", "details": "Details",
"duplicate": "Duplicate", "duplicate": "Duplicate",

View File

@@ -32,6 +32,7 @@
}); });
const route = useRoute(); const route = useRoute();
const router = useRouter();
const api = useUserApi(); const api = useUserApi();
const itemId = computed<string>(() => route.params.id as string); const itemId = computed<string>(() => route.params.id as string);
@@ -509,6 +510,17 @@
toast.success("Item deleted"); toast.success("Item deleted");
navigateTo("/home"); navigateTo("/home");
} }
async function createSubitem() {
// setting URL Parameter that is read and immidiately removed in the Item-CreateModal
await router.push({
query: {
subItemCreate: "y",
},
});
openDialog("create-item");
}
</script> </script>
<template> <template>
@@ -582,11 +594,15 @@
type="asset" type="asset"
/> />
<LabelMaker v-else :id="item.id" type="item" /> <LabelMaker v-else :id="item.id" type="item" />
<Button class="w-9 md:w-auto" @click="duplicateItem"> <Button class="w-9 md:w-auto" :aria-label="$t('global.create_subitem')" @click="createSubitem">
<MdiPlus />
<span class="hidden md:inline">{{ $t("global.create_subitem") }}</span>
</Button>
<Button class="w-9 md:w-auto" :aria-label="$t('global.duplicate')" @click="duplicateItem">
<MdiContentCopy /> <MdiContentCopy />
<span class="hidden md:inline">{{ $t("global.duplicate") }}</span> <span class="hidden md:inline">{{ $t("global.duplicate") }}</span>
</Button> </Button>
<Button variant="destructive" class="w-9 md:w-auto" @click="deleteItem"> <Button variant="destructive" class="w-9 md:w-auto" :aria-label="$t('global.delete')" @click="deleteItem">
<MdiDelete /> <MdiDelete />
<span class="hidden md:inline">{{ $t("global.delete") }}</span> <span class="hidden md:inline">{{ $t("global.delete") }}</span>
</Button> </Button>