|
23 | 23 | const labelStore = useLabelStore();
|
24 | 24 | const labels = computed(() => labelStore.labels);
|
25 | 25 |
|
26 |
| - const { data: item, refresh } = useAsyncData(async () => { |
| 26 | + const { data: nullableItem, refresh } = useAsyncData(async () => { |
27 | 27 | const { data, error } = await api.items.get(itemId.value);
|
28 | 28 | if (error) {
|
29 | 29 | toast.error("Failed to load item");
|
30 | 30 | navigateTo("/home");
|
31 | 31 | return;
|
32 | 32 | }
|
33 | 33 |
|
34 |
| - if (locations) { |
| 34 | + if (locations && data.location?.id) { |
| 35 | + // @ts-expect-error - we know the locations is valid |
35 | 36 | const location = locations.value.find(l => l.id === data.location.id);
|
36 | 37 | if (location) {
|
37 | 38 | data.location = location;
|
|
45 | 46 | return data;
|
46 | 47 | });
|
47 | 48 |
|
| 49 | + const item = computed<ItemOut>(() => nullableItem.value as ItemOut); |
| 50 | +
|
48 | 51 | onMounted(() => {
|
49 | 52 | refresh();
|
50 | 53 | });
|
51 | 54 |
|
52 | 55 | async function saveItem() {
|
| 56 | + if (!item.value.location?.id) { |
| 57 | + toast.error("Failed to save item: no location selected"); |
| 58 | + return; |
| 59 | + } |
| 60 | +
|
53 | 61 | const payload: ItemUpdate = {
|
54 | 62 | ...item.value,
|
55 | 63 | locationId: item.value.location?.id,
|
|
68 | 76 | navigateTo("/item/" + itemId.value);
|
69 | 77 | }
|
70 | 78 |
|
71 |
| - type FormField = { |
72 |
| - type: "text" | "textarea" | "select" | "date" | "label" | "location" | "number" | "checkbox"; |
| 79 | + type StringKeys<T> = { [k in keyof T]: T[k] extends string ? k : never }[keyof T]; |
| 80 | + type OnlyString<T> = { [k in StringKeys<T>]: string }; |
| 81 | +
|
| 82 | + type NumberKeys<T> = { [k in keyof T]: T[k] extends number ? k : never }[keyof T]; |
| 83 | + type OnlyNumber<T> = { [k in NumberKeys<T>]: number }; |
| 84 | +
|
| 85 | + type TextFormField = { |
| 86 | + type: "text" | "textarea"; |
73 | 87 | label: string;
|
74 |
| - ref: keyof ItemOut; |
| 88 | + // key of ItemOut where the value is a string |
| 89 | + ref: keyof OnlyString<ItemOut>; |
75 | 90 | };
|
76 | 91 |
|
| 92 | + type NumberFormField = { |
| 93 | + type: "number"; |
| 94 | + label: string; |
| 95 | + ref: keyof OnlyNumber<ItemOut> | keyof OnlyString<ItemOut>; |
| 96 | + }; |
| 97 | +
|
| 98 | + // https://stackoverflow.com/questions/50851263/how-do-i-require-a-keyof-to-be-for-a-property-of-a-specific-type |
| 99 | + // I don't know why typescript can't just be normal |
| 100 | + type BooleanKeys<T> = { [k in keyof T]: T[k] extends boolean ? k : never }[keyof T]; |
| 101 | + type OnlyBoolean<T> = { [k in BooleanKeys<T>]: boolean }; |
| 102 | +
|
| 103 | + interface BoolFormField { |
| 104 | + type: "checkbox"; |
| 105 | + label: string; |
| 106 | + ref: keyof OnlyBoolean<ItemOut>; |
| 107 | + } |
| 108 | +
|
| 109 | + type DateKeys<T> = { [k in keyof T]: T[k] extends Date | string ? k : never }[keyof T]; |
| 110 | + type OnlyDate<T> = { [k in DateKeys<T>]: Date | string }; |
| 111 | +
|
| 112 | + type DateFormField = { |
| 113 | + type: "date"; |
| 114 | + label: string; |
| 115 | + ref: keyof OnlyDate<ItemOut>; |
| 116 | + }; |
| 117 | +
|
| 118 | + type FormField = TextFormField | BoolFormField | DateFormField | NumberFormField; |
| 119 | +
|
77 | 120 | const mainFields: FormField[] = [
|
78 | 121 | {
|
79 | 122 | type: "text",
|
|
163 | 206 | },
|
164 | 207 | ];
|
165 | 208 |
|
166 |
| - const soldFields = [ |
| 209 | + const soldFields: FormField[] = [ |
167 | 210 | {
|
168 | 211 | type: "text",
|
169 | 212 | label: "Sold To",
|
|
194 | 237 | refAttachmentInput.value.click();
|
195 | 238 | }
|
196 | 239 |
|
197 |
| - function uploadImage(e: InputEvent) { |
| 240 | + function uploadImage(e: Event) { |
198 | 241 | const files = (e.target as HTMLInputElement).files;
|
199 | 242 | if (!files || !files.item(0)) {
|
200 | 243 | return;
|
|
273 | 316 | editState.type = attachment.type;
|
274 | 317 | editState.modal = true;
|
275 | 318 |
|
276 |
| - editState.obj = attachmentOpts.find(o => o.value === attachment.type); |
| 319 | + editState.obj = attachmentOpts.find(o => o.value === attachment.type) || attachmentOpts[0]; |
277 | 320 | }
|
278 | 321 |
|
279 | 322 | async function updateAttachment() {
|
|
337 | 380 |
|
338 | 381 | <section>
|
339 | 382 | <div class="space-y-6">
|
340 |
| - <div class="card bg-base-100 shadow-xl sm:rounded-lg overflow-visible"> |
341 |
| - <BaseSectionHeader v-if="item" class="p-5"> |
342 |
| - <span class="text-base-content"> Edit </span> |
343 |
| - <template #after> |
344 |
| - <div class="modal-action mt-3"> |
345 |
| - <div class="mr-auto tooltip" data-tip="Show Advanced Options"> |
346 |
| - <label class="label cursor-pointer mr-auto"> |
347 |
| - <input v-model="preferences.editorAdvancedView" type="checkbox" class="toggle toggle-primary" /> |
348 |
| - <span class="label-text ml-4"> Advanced </span> |
349 |
| - </label> |
350 |
| - </div> |
351 |
| - <BaseButton size="sm" @click="saveItem"> |
352 |
| - <template #icon> |
353 |
| - <Icon name="mdi-content-save-outline" /> |
354 |
| - </template> |
355 |
| - Save |
356 |
| - </BaseButton> |
| 383 | + <BaseCard class="overflow-visible"> |
| 384 | + <template #title> Edit Details </template> |
| 385 | + <template #title-actions> |
| 386 | + <div class="flex flex-wrap justify-between items-center mt-2 gap-4"> |
| 387 | + <div class="mr-auto tooltip" data-tip="Show Advanced Options"> |
| 388 | + <label class="label cursor-pointer mr-auto"> |
| 389 | + <input v-model="preferences.editorAdvancedView" type="checkbox" class="toggle toggle-primary" /> |
| 390 | + <span class="label-text ml-4"> Advanced </span> |
| 391 | + </label> |
357 | 392 | </div>
|
358 |
| - </template> |
359 |
| - </BaseSectionHeader> |
360 |
| - <div class="px-5 mb-6 grid md:grid-cols-2 gap-4"> |
| 393 | + <BaseButton size="sm" @click="saveItem"> |
| 394 | + <template #icon> |
| 395 | + <Icon name="mdi-content-save-outline" /> |
| 396 | + </template> |
| 397 | + Save |
| 398 | + </BaseButton> |
| 399 | + </div> |
| 400 | + </template> |
| 401 | + <div class="px-5 pt-2 border-t mb-6 grid md:grid-cols-2 gap-4"> |
361 | 402 | <LocationSelector v-model="item.location" />
|
362 | 403 | <FormMultiselect v-model="item.labels" label="Labels" :items="labels ?? []" />
|
363 |
| - |
364 | 404 | <Autocomplete
|
365 | 405 | v-if="preferences.editorAdvancedView"
|
366 | 406 | v-model="parent"
|
|
404 | 444 | </div>
|
405 | 445 | </div>
|
406 | 446 | </div>
|
407 |
| - </div> |
| 447 | + </BaseCard> |
408 | 448 |
|
409 | 449 | <BaseCard>
|
410 | 450 | <template #title> Custom Fields </template>
|
411 |
| - <div class="px-5 divide-y divide-gray-300 space-y-4"> |
| 451 | + <div class="px-5 border-t divide-y divide-gray-300 space-y-4"> |
412 | 452 | <div
|
413 | 453 | v-for="(field, idx) in item.fields"
|
414 | 454 | :key="`field-${idx}`"
|
|
0 commit comments