Skip to content
This repository was archived by the owner on Jun 12, 2024. It is now read-only.

Commit 986d2c5

Browse files
authored
refactor: editor page (#276)
1 parent 9361997 commit 986d2c5

File tree

3 files changed

+82
-34
lines changed

3 files changed

+82
-34
lines changed

frontend/components/Form/DatePicker.vue

+5-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
1919
const props = defineProps({
2020
modelValue: {
21-
type: Date,
21+
type: Date as () => Date | string,
2222
required: false,
2323
default: null,
2424
},
@@ -32,6 +32,10 @@
3232
get() {
3333
// return modelValue as string as YYYY-MM-DD or null
3434
if (validDate(props.modelValue)) {
35+
if (typeof props.modelValue === "string") {
36+
return props.modelValue;
37+
}
38+
3539
return props.modelValue ? props.modelValue.toISOString().split("T")[0] : null;
3640
}
3741

frontend/lib/api/base/base-api.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const ZERO_DATE = "0001-01-01T00:00:00Z";
55
type BaseApiType = {
66
createdAt: string;
77
updatedAt: string;
8-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8+
99
[key: string]: any;
1010
};
1111

@@ -26,7 +26,11 @@ export function parseDate<T>(obj: T, keys: Array<keyof T> = []): T {
2626
return;
2727
}
2828

29-
result[key] = new Date(result[key]);
29+
// transform string to ensure dates are parsed as UTC dates instead of
30+
// localized time stamps
31+
const asStr = result[key] as string;
32+
const cleaned = asStr.replaceAll("-", "/").split("T")[0];
33+
result[key] = new Date(cleaned);
3034
}
3135
});
3236

frontend/pages/item/[id]/index/edit.vue

+71-31
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,16 @@
2323
const labelStore = useLabelStore();
2424
const labels = computed(() => labelStore.labels);
2525
26-
const { data: item, refresh } = useAsyncData(async () => {
26+
const { data: nullableItem, refresh } = useAsyncData(async () => {
2727
const { data, error } = await api.items.get(itemId.value);
2828
if (error) {
2929
toast.error("Failed to load item");
3030
navigateTo("/home");
3131
return;
3232
}
3333
34-
if (locations) {
34+
if (locations && data.location?.id) {
35+
// @ts-expect-error - we know the locations is valid
3536
const location = locations.value.find(l => l.id === data.location.id);
3637
if (location) {
3738
data.location = location;
@@ -45,11 +46,18 @@
4546
return data;
4647
});
4748
49+
const item = computed<ItemOut>(() => nullableItem.value as ItemOut);
50+
4851
onMounted(() => {
4952
refresh();
5053
});
5154
5255
async function saveItem() {
56+
if (!item.value.location?.id) {
57+
toast.error("Failed to save item: no location selected");
58+
return;
59+
}
60+
5361
const payload: ItemUpdate = {
5462
...item.value,
5563
locationId: item.value.location?.id,
@@ -68,12 +76,47 @@
6876
navigateTo("/item/" + itemId.value);
6977
}
7078
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";
7387
label: string;
74-
ref: keyof ItemOut;
88+
// key of ItemOut where the value is a string
89+
ref: keyof OnlyString<ItemOut>;
7590
};
7691
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+
77120
const mainFields: FormField[] = [
78121
{
79122
type: "text",
@@ -163,7 +206,7 @@
163206
},
164207
];
165208
166-
const soldFields = [
209+
const soldFields: FormField[] = [
167210
{
168211
type: "text",
169212
label: "Sold To",
@@ -194,7 +237,7 @@
194237
refAttachmentInput.value.click();
195238
}
196239
197-
function uploadImage(e: InputEvent) {
240+
function uploadImage(e: Event) {
198241
const files = (e.target as HTMLInputElement).files;
199242
if (!files || !files.item(0)) {
200243
return;
@@ -273,7 +316,7 @@
273316
editState.type = attachment.type;
274317
editState.modal = true;
275318
276-
editState.obj = attachmentOpts.find(o => o.value === attachment.type);
319+
editState.obj = attachmentOpts.find(o => o.value === attachment.type) || attachmentOpts[0];
277320
}
278321
279322
async function updateAttachment() {
@@ -337,30 +380,27 @@
337380

338381
<section>
339382
<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>
357392
</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">
361402
<LocationSelector v-model="item.location" />
362403
<FormMultiselect v-model="item.labels" label="Labels" :items="labels ?? []" />
363-
364404
<Autocomplete
365405
v-if="preferences.editorAdvancedView"
366406
v-model="parent"
@@ -404,11 +444,11 @@
404444
</div>
405445
</div>
406446
</div>
407-
</div>
447+
</BaseCard>
408448

409449
<BaseCard>
410450
<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">
412452
<div
413453
v-for="(field, idx) in item.fields"
414454
:key="`field-${idx}`"

0 commit comments

Comments
 (0)