mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2026-01-04 20:14:54 +01:00
Merge branch 'main' into parent-location-sync
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
class="flex items-center text-3xl font-bold tracking-tight"
|
||||
:class="{
|
||||
'text-neutral-content': dark,
|
||||
'text-content': !dark,
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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") }}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 }">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user