Merge branch 'main' into parent-location-sync

This commit is contained in:
Matt Kilgore
2024-10-28 15:51:31 -04:00
committed by GitHub
62 changed files with 2234 additions and 586 deletions

View File

@@ -4,7 +4,6 @@
class="flex items-center text-3xl font-bold tracking-tight"
:class="{
'text-neutral-content': dark,
'text-content': !dark,
}"
>
<slot />

View File

@@ -85,7 +85,10 @@
type Props = {
label: string;
modelValue: SupportValues | null | undefined;
items: string[] | object[];
items: {
id: string;
treeString: string;
}[];
display?: string;
multiple?: boolean;
};
@@ -156,11 +159,28 @@
const matches = index.value.search("*" + search.value + "*");
const resultIDs = [];
for (let i = 0; i < matches.length; i++) {
const match = matches[i];
const item = props.items[parseInt(match.ref)];
const display = extractDisplay(item);
list.push({ id: i, display, value: item });
resultIDs.push(item.id);
}
/**
* Supplementary search,
* Resolve the issue of language not being supported
*/
for (let i = 0; i < props.items.length; i++) {
const item = props.items[i];
if (resultIDs.find(item_ => item_ === item.id) !== undefined) {
continue;
}
if (item.treeString.includes(search.value)) {
const display = extractDisplay(item);
list.push({ id: i, display, value: item });
}
}
return list;

View File

@@ -6,10 +6,10 @@
:class="{
'text-red-600':
typeof value === 'string' &&
((maxLength && value.length > maxLength) || (minLength && value.length < minLength)),
((maxLength !== -1 && value.length > maxLength) || (minLength !== -1 && value.length < minLength)),
}"
>
{{ typeof value === "string" && (maxLength || minLength) ? `${value.length}/${maxLength}` : "" }}
{{ typeof value === "string" && (maxLength !== -1 || minLength !== -1) ? `${value.length}/${maxLength}` : "" }}
</span>
</label>
<textarea ref="el" v-model="value" class="textarea textarea-bordered h-28 w-full" :placeholder="placeholder" />
@@ -21,10 +21,10 @@
:class="{
'text-red-600':
typeof value === 'string' &&
((maxLength && value.length > maxLength) || (minLength && value.length < minLength)),
((maxLength !== -1 && value.length > maxLength) || (minLength !== -1 && value.length < minLength)),
}"
>
{{ typeof value === "string" && (maxLength || minLength) ? `${value.length}/${maxLength}` : "" }}
{{ typeof value === "string" && (maxLength !== -1 || minLength !== -1) ? `${value.length}/${maxLength}` : "" }}
</span>
</label>
<textarea
@@ -63,10 +63,12 @@
},
maxLength: {
type: Number,
default: -1,
required: false,
},
minLength: {
type: Number,
default: -1,
required: false,
},
});
@@ -84,7 +86,4 @@
});
const value = useVModel(props, "modelValue", emit);
const valueLen = computed(() => {
return value.value ? value.value.length : 0;
});
</script>

View File

@@ -6,10 +6,10 @@
:class="{
'text-red-600':
typeof value === 'string' &&
((maxLength && value.length > maxLength) || (minLength && value.length < minLength)),
((maxLength !== -1 && value.length > maxLength) || (minLength !== -1 && value.length < minLength)),
}"
>
{{ typeof value === "string" && (maxLength || minLength) ? `${value.length}/${maxLength}` : "" }}
{{ typeof value === "string" && (maxLength !== -1 || minLength !== -1) ? `${value.length}/${maxLength}` : "" }}
</span>
</label>
<input
@@ -28,10 +28,10 @@
:class="{
'text-red-600':
typeof value === 'string' &&
((maxLength && value.length > maxLength) || (minLength && value.length < minLength)),
((maxLength !== -1 && value.length > maxLength) || (minLength !== -1 && value.length < minLength)),
}"
>
{{ typeof value === "string" && (maxLength || minLength) ? `${value.length}/${maxLength}` : "" }}
{{ typeof value === "string" && (maxLength !== -1 || minLength !== -1) ? `${value.length}/${maxLength}` : "" }}
</span>
</label>
<input
@@ -76,10 +76,12 @@
},
maxLength: {
type: Number,
default: -1,
required: false,
},
minLength: {
type: Number,
default: -1,
required: false,
},
});

View File

@@ -1,19 +1,25 @@
<template>
<NuxtLink class="group card rounded-md border border-gray-300" :to="`/item/${item.id}`">
<div class="relative h-[200px]">
<img v-if="imageUrl" class="h-[200px] w-full rounded-t border-gray-300 object-cover shadow-sm" :src="imageUrl" />
<div class="absolute bottom-1 left-1">
<img
v-if="imageUrl"
class="h-[200px] w-full rounded-t border-gray-300 object-cover shadow-sm"
:src="imageUrl"
alt=""
/>
<div class="absolute bottom-1 left-1 text-wrap">
<NuxtLink
v-if="item.location"
class="badge rounded-md text-sm shadow-md hover:link"
:to="`/location/${item.location.id}`"
loading="lazy"
>
{{ item.location.name }}
</NuxtLink>
</div>
</div>
<div class="col-span-4 flex grow flex-col gap-y-1 rounded-b bg-base-100 p-4 pt-2">
<h2 class="line-clamp-2 text-ellipsis text-lg font-bold">{{ item.name }}</h2>
<h2 class="line-clamp-2 text-ellipsis text-wrap text-lg font-bold">{{ item.name }}</h2>
<div class="divider my-0"></div>
<div class="flex gap-2">
<div v-if="item.insured" class="tooltip z-10" data-tip="Insured">

View File

@@ -8,17 +8,27 @@
v-model="form.name"
:trigger-focus="focused"
:autofocus="true"
label="Item Name"
:label="$t('components.item.create_modal.item_name')"
:max-length="255"
:min-length="1"
/>
<FormTextArea v-model="form.description" label="Item Description" :max-length="1000" />
<FormMultiselect v-model="form.labels" label="Labels" :items="labels ?? []" />
<FormTextArea
v-model="form.description"
:label="$t('components.item.create_modal.item_description')"
:max-length="1000"
/>
<FormMultiselect v-model="form.labels" :label="$t('global.labels')" :items="labels ?? []" />
<div class="modal-action mb-6">
<div>
<label for="photo" class="btn">{{ $t("components.item.create_modal.photo_button") }}</label>
<input id="photo" class="hidden" type="file" accept="image/png,image/jpeg,image/gif" @change="previewImage" />
<input
id="photo"
class="hidden"
type="file"
accept="image/png,image/jpeg,image/gif,image/avif,image/webp"
@change="previewImage"
/>
</div>
<div class="grow"></div>
<div>

View File

@@ -18,7 +18,7 @@
}"
>
<template v-if="typeof h === 'string'">{{ h }}</template>
<template v-else>{{ h.text }}</template>
<template v-else>{{ $t(h.text) }}</template>
<div
v-if="sortByProperty === h.value"
:class="`inline-flex ${sortByProperty === h.value ? '' : 'opacity-0'}`"
@@ -45,7 +45,7 @@
}"
>
<template v-if="h.type === 'name'">
<NuxtLink class="hover" :to="`/item/${d.id}`">
<NuxtLink class="hover text-wrap" :to="`/item/${d.id}`">
{{ d.name }}
</NuxtLink>
</template>
@@ -83,7 +83,7 @@
</label>
<ul tabindex="0" class="dropdown-content rounded-box flex w-64 flex-col gap-2 bg-base-100 p-2 pl-3 shadow">
<li>Headers:</li>
<li v-for="(h, i) in headers" class="flex flex-row items-center gap-1">
<li v-for="(h, i) in headers" :key="h.value" class="flex flex-row items-center gap-1">
<button
class="btn btn-square btn-ghost btn-xs"
:class="{
@@ -109,11 +109,11 @@
:checked="h.enabled"
@change="toggleHeader(h.value)"
/>
<label class="label-text" :for="h.value"> {{ h.text }} </label>
<label class="label-text" :for="h.value"> {{ $t(h.text) }} </label>
</li>
</ul>
</div>
<div class="hidden md:block">Rows per page</div>
<div class="hidden md:block">{{ $t("components.item.view.table.rows_per_page") }}</div>
<select v-model.number="pagination.rowsPerPage" class="select select-primary select-sm">
<option :value="10">10</option>
<option :value="25">25</option>
@@ -122,7 +122,7 @@
</select>
<div class="btn-group">
<button :disabled="!hasPrev" class="btn btn-sm" @click="prev()">«</button>
<button class="btn btn-sm">Page {{ pagination.page }}</button>
<button class="btn btn-sm">{{ $t("components.item.view.table.page") }} {{ pagination.page }}</button>
<button :disabled="!hasNext" class="btn btn-sm" @click="next()">»</button>
</div>
</div>
@@ -149,20 +149,29 @@
const preferences = useViewPreferences();
const defaultHeaders = [
{ text: "Name", value: "name", enabled: true, type: "name" },
{ text: "Quantity", value: "quantity", align: "center", enabled: true },
{ text: "Insured", value: "insured", align: "center", enabled: true, type: "boolean" },
{ text: "Price", value: "purchasePrice", align: "center", enabled: true, type: "price" },
{ text: "Location", value: "location", align: "center", enabled: false, type: "location" },
{ text: "Archived", value: "archived", align: "center", enabled: false, type: "boolean" },
{ text: "Created At", value: "createdAt", align: "center", enabled: false, type: "date" },
{ text: "Updated At", value: "updatedAt", align: "center", enabled: false, type: "date" },
{
text: "items.name",
value: "name",
enabled: true,
type: "name",
},
{ text: "items.quantity", value: "quantity", align: "center", enabled: true },
{ text: "items.insured", value: "insured", align: "center", enabled: true, type: "boolean" },
{ text: "items.purchase_price", value: "purchasePrice", align: "center", enabled: true, type: "price" },
{ text: "items.location", value: "location", align: "center", enabled: false, type: "location" },
{ text: "items.archived", value: "archived", align: "center", enabled: false, type: "boolean" },
{ text: "items.created_at", value: "createdAt", align: "center", enabled: false, type: "date" },
{ text: "items.updated_at", value: "updatedAt", align: "center", enabled: false, type: "date" },
] satisfies TableHeader[];
const headers = ref<TableHeader[]>(
(preferences.value.tableHeaders ?? []).concat(
defaultHeaders.filter(h => !preferences.value.tableHeaders?.find(h2 => h2.value === h.value))
)
(preferences.value.tableHeaders ?? [])
.concat(defaultHeaders.filter(h => !preferences.value.tableHeaders?.find(h2 => h2.value === h.value)))
// this is a hack to make sure that any changes to the defaultHeaders are reflected in the preferences
.map(h => ({
...(defaultHeaders.find(h2 => h2.value === h.value) as TableHeader),
enabled: h.enabled,
}))
);
console.log(headers.value);

View File

@@ -7,11 +7,15 @@
v-model="form.name"
:trigger-focus="focused"
:autofocus="true"
label="Label Name"
:label="$t('components.label.create_modal.label_name')"
:max-length="255"
:min-length="1"
/>
<FormTextArea v-model="form.description" label="Label Description" :max-length="255" />
<FormTextArea
v-model="form.description"
:label="$t('components.label.create_modal.label_description')"
:max-length="255"
/>
<div class="modal-action">
<div class="flex justify-center">
<BaseButton class="rounded-r-none" :loading="loading" type="submit"> {{ $t("global.create") }} </BaseButton>

View File

@@ -8,11 +8,15 @@
:trigger-focus="focused"
:autofocus="true"
:required="true"
label="Location Name"
:label="$t('components.location.create_modal.location_name')"
:max-length="255"
:min-length="1"
/>
<FormTextArea v-model="form.description" label="Location Description" :max-length="1000" />
<FormTextArea
v-model="form.description"
:label="$t('components.location.create_modal.location_description')"
:max-length="1000"
/>
<LocationSelector v-model="form.parent" />
<div class="modal-action">
<div class="flex justify-center">
@@ -21,7 +25,7 @@
<label tabindex="0" class="btn rounded-l-none rounded-r-xl">
<MdiChevronDown class="size-5" />
</label>
<ul tabindex="0" class="dropdown-content menu rounded-box bg-base-100 right-0 w-64 p-2 shadow">
<ul tabindex="0" class="dropdown-content menu rounded-box right-0 w-64 bg-base-100 p-2 shadow">
<li>
<button type="button" @click="create(false)">{{ $t("global.create_and_add") }}</button>
</li>

View File

@@ -1,5 +1,11 @@
<template>
<FormAutocomplete2 v-if="locations" v-model="value" :items="locations" display="name" label="Parent Location">
<FormAutocomplete2
v-if="locations"
v-model="value"
:items="locations"
display="name"
:label="$t('components.location.selector.parent_location')"
>
<template #display="{ item, selected, active }">
<div>
<div class="flex w-full">

View File

@@ -10,6 +10,7 @@
</script>
<template>
<!-- eslint-disable-next-line tailwindcss/no-custom-classname -->
<div class="root border-2 p-4">
<p v-if="locs.length === 0" class="text-center text-sm">
{{ $t("location.tree.no_locations") }}

View File

@@ -25,17 +25,17 @@
},
});
const { data: maintenanceDataList, refresh: refreshList } = useAsyncData<MaintenanceEntryWithDetails[]>(
const { data: maintenanceDataList, refresh: refreshList } = useAsyncData(
async () => {
const { data } =
props.currentItemId !== undefined
? await api.items.maintenance.getLog(props.currentItemId, { status: maintenanceFilterStatus.value })
: await api.maintenance.getAll({ status: maintenanceFilterStatus.value });
console.log(data);
return data as MaintenanceEntryWithDetails[];
return data;
},
{
watch: maintenanceFilterStatus,
watch: [maintenanceFilterStatus],
}
);
@@ -80,7 +80,7 @@
<StatCard
v-for="stat in stats"
:key="stat.id"
class="stats border-l-primary block shadow-xl"
class="stats block border-l-primary shadow-xl"
:title="stat.title"
:value="stat.value"
:type="stat.type"
@@ -189,7 +189,7 @@
<div v-if="props.currentItemId" class="hidden first:block">
<button
type="button"
class="border-base-content relative block w-full rounded-lg border-2 border-dashed p-12 text-center"
class="relative block w-full rounded-lg border-2 border-dashed border-base-content p-12 text-center"
@click="maintenanceEditModal?.openCreateModal(props.currentItemId)"
>
<MdiWrenchClock class="inline size-16" />

View File

@@ -3,7 +3,7 @@
<dl class="sm:divide-y sm:divide-gray-300">
<div v-for="(detail, i) in details" :key="i" class="group py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-base-content">
{{ detail.name }}
{{ $t(detail.name) }}
</dt>
<dd class="text-start text-sm text-base-content sm:col-span-2">
<slot :name="detail.slot || detail.name" v-bind="{ detail }">

View File

@@ -24,6 +24,7 @@
</script>
<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="markdown text-wrap" v-html="raw"></div>
</template>
@@ -31,53 +32,4 @@
* {
--y-gap: 0.65rem;
}
.markdown > :first-child {
margin-top: 0px !important;
}
.markdown :where(p, ul, ol, dl, blockquote, h1, h2, h3, h4, h5, h6) {
margin-top: var(--y-gap);
margin-bottom: var(--y-gap);
}
.markdown :where(ul) {
list-style: disc;
margin-left: 2rem;
}
.markdown :where(ol) {
list-style: decimal;
margin-left: 2rem;
}
/* Heading Styles */
.markdown :where(h1) {
font-size: 2rem;
font-weight: 700;
}
.markdown :where(h2) {
font-size: 1.5rem;
font-weight: 700;
}
.markdown :where(h3) {
font-size: 1.25rem;
font-weight: 700;
}
.markdown :where(h4) {
font-size: 1rem;
font-weight: 700;
}
.markdown :where(h5) {
font-size: 0.875rem;
font-weight: 700;
}
.markdown :where(h6) {
font-size: 0.75rem;
font-weight: 700;
}
</style>