refactor: editor page (#276)

This commit is contained in:
Hayden 2023-02-13 10:43:09 -09:00 committed by GitHub
parent 9361997a42
commit 986d2c586e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 82 additions and 34 deletions

View file

@ -18,7 +18,7 @@
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Date, type: Date as () => Date | string,
required: false, required: false,
default: null, default: null,
}, },
@ -32,6 +32,10 @@
get() { get() {
// return modelValue as string as YYYY-MM-DD or null // return modelValue as string as YYYY-MM-DD or null
if (validDate(props.modelValue)) { if (validDate(props.modelValue)) {
if (typeof props.modelValue === "string") {
return props.modelValue;
}
return props.modelValue ? props.modelValue.toISOString().split("T")[0] : null; return props.modelValue ? props.modelValue.toISOString().split("T")[0] : null;
} }

View file

@ -5,7 +5,7 @@ const ZERO_DATE = "0001-01-01T00:00:00Z";
type BaseApiType = { type BaseApiType = {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any; [key: string]: any;
}; };
@ -26,7 +26,11 @@ export function parseDate<T>(obj: T, keys: Array<keyof T> = []): T {
return; return;
} }
result[key] = new Date(result[key]); // transform string to ensure dates are parsed as UTC dates instead of
// localized time stamps
const asStr = result[key] as string;
const cleaned = asStr.replaceAll("-", "/").split("T")[0];
result[key] = new Date(cleaned);
} }
}); });

View file

@ -23,7 +23,7 @@
const labelStore = useLabelStore(); const labelStore = useLabelStore();
const labels = computed(() => labelStore.labels); const labels = computed(() => labelStore.labels);
const { data: item, refresh } = useAsyncData(async () => { const { data: nullableItem, refresh } = useAsyncData(async () => {
const { data, error } = await api.items.get(itemId.value); const { data, error } = await api.items.get(itemId.value);
if (error) { if (error) {
toast.error("Failed to load item"); toast.error("Failed to load item");
@ -31,7 +31,8 @@
return; return;
} }
if (locations) { if (locations && data.location?.id) {
// @ts-expect-error - we know the locations is valid
const location = locations.value.find(l => l.id === data.location.id); const location = locations.value.find(l => l.id === data.location.id);
if (location) { if (location) {
data.location = location; data.location = location;
@ -45,11 +46,18 @@
return data; return data;
}); });
const item = computed<ItemOut>(() => nullableItem.value as ItemOut);
onMounted(() => { onMounted(() => {
refresh(); refresh();
}); });
async function saveItem() { async function saveItem() {
if (!item.value.location?.id) {
toast.error("Failed to save item: no location selected");
return;
}
const payload: ItemUpdate = { const payload: ItemUpdate = {
...item.value, ...item.value,
locationId: item.value.location?.id, locationId: item.value.location?.id,
@ -68,12 +76,47 @@
navigateTo("/item/" + itemId.value); navigateTo("/item/" + itemId.value);
} }
type FormField = { type StringKeys<T> = { [k in keyof T]: T[k] extends string ? k : never }[keyof T];
type: "text" | "textarea" | "select" | "date" | "label" | "location" | "number" | "checkbox"; type OnlyString<T> = { [k in StringKeys<T>]: string };
type NumberKeys<T> = { [k in keyof T]: T[k] extends number ? k : never }[keyof T];
type OnlyNumber<T> = { [k in NumberKeys<T>]: number };
type TextFormField = {
type: "text" | "textarea";
label: string; label: string;
ref: keyof ItemOut; // key of ItemOut where the value is a string
ref: keyof OnlyString<ItemOut>;
}; };
type NumberFormField = {
type: "number";
label: string;
ref: keyof OnlyNumber<ItemOut> | keyof OnlyString<ItemOut>;
};
// https://stackoverflow.com/questions/50851263/how-do-i-require-a-keyof-to-be-for-a-property-of-a-specific-type
// I don't know why typescript can't just be normal
type BooleanKeys<T> = { [k in keyof T]: T[k] extends boolean ? k : never }[keyof T];
type OnlyBoolean<T> = { [k in BooleanKeys<T>]: boolean };
interface BoolFormField {
type: "checkbox";
label: string;
ref: keyof OnlyBoolean<ItemOut>;
}
type DateKeys<T> = { [k in keyof T]: T[k] extends Date | string ? k : never }[keyof T];
type OnlyDate<T> = { [k in DateKeys<T>]: Date | string };
type DateFormField = {
type: "date";
label: string;
ref: keyof OnlyDate<ItemOut>;
};
type FormField = TextFormField | BoolFormField | DateFormField | NumberFormField;
const mainFields: FormField[] = [ const mainFields: FormField[] = [
{ {
type: "text", type: "text",
@ -163,7 +206,7 @@
}, },
]; ];
const soldFields = [ const soldFields: FormField[] = [
{ {
type: "text", type: "text",
label: "Sold To", label: "Sold To",
@ -194,7 +237,7 @@
refAttachmentInput.value.click(); refAttachmentInput.value.click();
} }
function uploadImage(e: InputEvent) { function uploadImage(e: Event) {
const files = (e.target as HTMLInputElement).files; const files = (e.target as HTMLInputElement).files;
if (!files || !files.item(0)) { if (!files || !files.item(0)) {
return; return;
@ -273,7 +316,7 @@
editState.type = attachment.type; editState.type = attachment.type;
editState.modal = true; editState.modal = true;
editState.obj = attachmentOpts.find(o => o.value === attachment.type); editState.obj = attachmentOpts.find(o => o.value === attachment.type) || attachmentOpts[0];
} }
async function updateAttachment() { async function updateAttachment() {
@ -337,11 +380,10 @@
<section> <section>
<div class="space-y-6"> <div class="space-y-6">
<div class="card bg-base-100 shadow-xl sm:rounded-lg overflow-visible"> <BaseCard class="overflow-visible">
<BaseSectionHeader v-if="item" class="p-5"> <template #title> Edit Details </template>
<span class="text-base-content"> Edit </span> <template #title-actions>
<template #after> <div class="flex flex-wrap justify-between items-center mt-2 gap-4">
<div class="modal-action mt-3">
<div class="mr-auto tooltip" data-tip="Show Advanced Options"> <div class="mr-auto tooltip" data-tip="Show Advanced Options">
<label class="label cursor-pointer mr-auto"> <label class="label cursor-pointer mr-auto">
<input v-model="preferences.editorAdvancedView" type="checkbox" class="toggle toggle-primary" /> <input v-model="preferences.editorAdvancedView" type="checkbox" class="toggle toggle-primary" />
@ -356,11 +398,9 @@
</BaseButton> </BaseButton>
</div> </div>
</template> </template>
</BaseSectionHeader> <div class="px-5 pt-2 border-t mb-6 grid md:grid-cols-2 gap-4">
<div class="px-5 mb-6 grid md:grid-cols-2 gap-4">
<LocationSelector v-model="item.location" /> <LocationSelector v-model="item.location" />
<FormMultiselect v-model="item.labels" label="Labels" :items="labels ?? []" /> <FormMultiselect v-model="item.labels" label="Labels" :items="labels ?? []" />
<Autocomplete <Autocomplete
v-if="preferences.editorAdvancedView" v-if="preferences.editorAdvancedView"
v-model="parent" v-model="parent"
@ -404,11 +444,11 @@
</div> </div>
</div> </div>
</div> </div>
</div> </BaseCard>
<BaseCard> <BaseCard>
<template #title> Custom Fields </template> <template #title> Custom Fields </template>
<div class="px-5 divide-y divide-gray-300 space-y-4"> <div class="px-5 border-t divide-y divide-gray-300 space-y-4">
<div <div
v-for="(field, idx) in item.fields" v-for="(field, idx) in item.fields"
:key="`field-${idx}`" :key="`field-${idx}`"