mirror of
https://github.com/hay-kot/homebox.git
synced 2024-12-18 04:56:30 +00:00
refactor: editor page (#276)
This commit is contained in:
parent
9361997a42
commit
986d2c586e
3 changed files with 82 additions and 34 deletions
|
@ -18,7 +18,7 @@
|
|||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Date,
|
||||
type: Date as () => Date | string,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
|
@ -32,6 +32,10 @@
|
|||
get() {
|
||||
// return modelValue as string as YYYY-MM-DD or null
|
||||
if (validDate(props.modelValue)) {
|
||||
if (typeof props.modelValue === "string") {
|
||||
return props.modelValue;
|
||||
}
|
||||
|
||||
return props.modelValue ? props.modelValue.toISOString().split("T")[0] : null;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ const ZERO_DATE = "0001-01-01T00:00:00Z";
|
|||
type BaseApiType = {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
|
@ -26,7 +26,11 @@ export function parseDate<T>(obj: T, keys: Array<keyof T> = []): T {
|
|||
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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
const labelStore = useLabelStore();
|
||||
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);
|
||||
if (error) {
|
||||
toast.error("Failed to load item");
|
||||
|
@ -31,7 +31,8 @@
|
|||
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);
|
||||
if (location) {
|
||||
data.location = location;
|
||||
|
@ -45,11 +46,18 @@
|
|||
return data;
|
||||
});
|
||||
|
||||
const item = computed<ItemOut>(() => nullableItem.value as ItemOut);
|
||||
|
||||
onMounted(() => {
|
||||
refresh();
|
||||
});
|
||||
|
||||
async function saveItem() {
|
||||
if (!item.value.location?.id) {
|
||||
toast.error("Failed to save item: no location selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: ItemUpdate = {
|
||||
...item.value,
|
||||
locationId: item.value.location?.id,
|
||||
|
@ -68,12 +76,47 @@
|
|||
navigateTo("/item/" + itemId.value);
|
||||
}
|
||||
|
||||
type FormField = {
|
||||
type: "text" | "textarea" | "select" | "date" | "label" | "location" | "number" | "checkbox";
|
||||
type StringKeys<T> = { [k in keyof T]: T[k] extends string ? k : never }[keyof T];
|
||||
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;
|
||||
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[] = [
|
||||
{
|
||||
type: "text",
|
||||
|
@ -163,7 +206,7 @@
|
|||
},
|
||||
];
|
||||
|
||||
const soldFields = [
|
||||
const soldFields: FormField[] = [
|
||||
{
|
||||
type: "text",
|
||||
label: "Sold To",
|
||||
|
@ -194,7 +237,7 @@
|
|||
refAttachmentInput.value.click();
|
||||
}
|
||||
|
||||
function uploadImage(e: InputEvent) {
|
||||
function uploadImage(e: Event) {
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
if (!files || !files.item(0)) {
|
||||
return;
|
||||
|
@ -273,7 +316,7 @@
|
|||
editState.type = attachment.type;
|
||||
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() {
|
||||
|
@ -337,30 +380,27 @@
|
|||
|
||||
<section>
|
||||
<div class="space-y-6">
|
||||
<div class="card bg-base-100 shadow-xl sm:rounded-lg overflow-visible">
|
||||
<BaseSectionHeader v-if="item" class="p-5">
|
||||
<span class="text-base-content"> Edit </span>
|
||||
<template #after>
|
||||
<div class="modal-action mt-3">
|
||||
<div class="mr-auto tooltip" data-tip="Show Advanced Options">
|
||||
<label class="label cursor-pointer mr-auto">
|
||||
<input v-model="preferences.editorAdvancedView" type="checkbox" class="toggle toggle-primary" />
|
||||
<span class="label-text ml-4"> Advanced </span>
|
||||
</label>
|
||||
</div>
|
||||
<BaseButton size="sm" @click="saveItem">
|
||||
<template #icon>
|
||||
<Icon name="mdi-content-save-outline" />
|
||||
</template>
|
||||
Save
|
||||
</BaseButton>
|
||||
<BaseCard class="overflow-visible">
|
||||
<template #title> Edit Details </template>
|
||||
<template #title-actions>
|
||||
<div class="flex flex-wrap justify-between items-center mt-2 gap-4">
|
||||
<div class="mr-auto tooltip" data-tip="Show Advanced Options">
|
||||
<label class="label cursor-pointer mr-auto">
|
||||
<input v-model="preferences.editorAdvancedView" type="checkbox" class="toggle toggle-primary" />
|
||||
<span class="label-text ml-4"> Advanced </span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</BaseSectionHeader>
|
||||
<div class="px-5 mb-6 grid md:grid-cols-2 gap-4">
|
||||
<BaseButton size="sm" @click="saveItem">
|
||||
<template #icon>
|
||||
<Icon name="mdi-content-save-outline" />
|
||||
</template>
|
||||
Save
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
<div class="px-5 pt-2 border-t mb-6 grid md:grid-cols-2 gap-4">
|
||||
<LocationSelector v-model="item.location" />
|
||||
<FormMultiselect v-model="item.labels" label="Labels" :items="labels ?? []" />
|
||||
|
||||
<Autocomplete
|
||||
v-if="preferences.editorAdvancedView"
|
||||
v-model="parent"
|
||||
|
@ -404,11 +444,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard>
|
||||
<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
|
||||
v-for="(field, idx) in item.fields"
|
||||
:key="`field-${idx}`"
|
||||
|
|
Loading…
Reference in a new issue