Show parent > child location on detail pages, add location move
Deploy to LXC / deploy (push) Successful in 18s
Deploy to LXC / deploy (push) Successful in 18s
- Device/component detail pages show "Parent › Child" for locations - Device list cards show full location path - Location edit form now includes a Parent selector to move locations between parents or make them top-level - Prevents setting a location as its own parent Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ export const load: PageServerLoad = async ({ params }) => {
|
||||
deviceTitle: devices.title,
|
||||
locationId: components.locationId,
|
||||
locationName: locations.name,
|
||||
locationParentId: locations.parentId,
|
||||
createdAt: components.createdAt,
|
||||
updatedAt: components.updatedAt
|
||||
})
|
||||
@@ -32,6 +33,15 @@ export const load: PageServerLoad = async ({ params }) => {
|
||||
|
||||
if (!component) error(404, 'Component not found');
|
||||
|
||||
let parentLocationName: string | null = null;
|
||||
if (component.locationParentId) {
|
||||
const [parent] = await db
|
||||
.select({ name: locations.name })
|
||||
.from(locations)
|
||||
.where(eq(locations.id, component.locationParentId));
|
||||
parentLocationName = parent?.name ?? null;
|
||||
}
|
||||
|
||||
const images = await db
|
||||
.select()
|
||||
.from(componentImages)
|
||||
@@ -57,7 +67,7 @@ export const load: PageServerLoad = async ({ params }) => {
|
||||
.where(eq(installationLog.componentId, params.id))
|
||||
.orderBy(desc(installationLog.performedAt));
|
||||
|
||||
return { component, images, documents, history };
|
||||
return { component, parentLocationName, images, documents, history };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<a href="/devices/{c.currentDeviceId}" class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400">{c.deviceTitle}</a>
|
||||
</p>
|
||||
{:else if c.locationName}
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">In storage at <span class="font-medium">{c.locationName}</span></p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">In storage at <span class="font-medium">{#if data.parentLocationName}{data.parentLocationName} › {/if}{c.locationName}</span></p>
|
||||
{:else}
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">In storage (no location set)</p>
|
||||
{/if}
|
||||
|
||||
@@ -47,7 +47,8 @@ export const load: PageServerLoad = async ({ url }) => {
|
||||
model: devices.model,
|
||||
condition: devices.condition,
|
||||
year: devices.year,
|
||||
locationName: locations.name
|
||||
locationName: locations.name,
|
||||
locationParentId: locations.parentId
|
||||
})
|
||||
.from(devices)
|
||||
.leftJoin(locations, eq(devices.locationId, locations.id))
|
||||
@@ -56,6 +57,17 @@ export const load: PageServerLoad = async ({ url }) => {
|
||||
.limit(pageSize)
|
||||
.offset((page - 1) * pageSize);
|
||||
|
||||
// Resolve parent location names
|
||||
const parentIds = [...new Set(deviceList.filter(d => d.locationParentId).map(d => d.locationParentId!))];
|
||||
let parentNameMap: Record<string, string> = {};
|
||||
if (parentIds.length > 0) {
|
||||
const parents = await db
|
||||
.select({ id: locations.id, name: locations.name })
|
||||
.from(locations)
|
||||
.where(sql`${locations.id} IN ${parentIds}`);
|
||||
for (const p of parents) parentNameMap[p.id] = p.name;
|
||||
}
|
||||
|
||||
// Fetch first image for each device
|
||||
const deviceIds = deviceList.map((d) => d.id);
|
||||
let imageMap: Record<string, string> = {};
|
||||
@@ -80,7 +92,12 @@ export const load: PageServerLoad = async ({ url }) => {
|
||||
return {
|
||||
devices: deviceList.map((d) => ({
|
||||
...d,
|
||||
thumbnail: imageMap[d.id] ?? null
|
||||
thumbnail: imageMap[d.id] ?? null,
|
||||
fullLocation: d.locationName
|
||||
? (d.locationParentId && parentNameMap[d.locationParentId]
|
||||
? `${parentNameMap[d.locationParentId]} › ${d.locationName}`
|
||||
: d.locationName)
|
||||
: null
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
|
||||
@@ -117,8 +117,8 @@
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">{device.year}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if device.locationName}
|
||||
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">{device.locationName}</p>
|
||||
{#if device.fullLocation}
|
||||
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">{device.fullLocation}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -38,6 +38,7 @@ export const load: PageServerLoad = async ({ params }) => {
|
||||
generalNotes: devices.generalNotes,
|
||||
locationId: devices.locationId,
|
||||
locationName: locations.name,
|
||||
locationParentId: locations.parentId,
|
||||
disabled: devices.disabled,
|
||||
createdAt: devices.createdAt,
|
||||
updatedAt: devices.updatedAt
|
||||
@@ -49,6 +50,16 @@ export const load: PageServerLoad = async ({ params }) => {
|
||||
if (!device) error(404, 'Device not found');
|
||||
if (device.disabled) error(404, 'Device not found');
|
||||
|
||||
// Resolve parent location name
|
||||
let parentLocationName: string | null = null;
|
||||
if (device.locationParentId) {
|
||||
const [parent] = await db
|
||||
.select({ name: locations.name })
|
||||
.from(locations)
|
||||
.where(eq(locations.id, device.locationParentId));
|
||||
parentLocationName = parent?.name ?? null;
|
||||
}
|
||||
|
||||
// Computer details
|
||||
let compDetails = null;
|
||||
if (device.category === 'Computer') {
|
||||
@@ -138,6 +149,7 @@ export const load: PageServerLoad = async ({ params }) => {
|
||||
|
||||
return {
|
||||
device,
|
||||
parentLocationName,
|
||||
computerDetails: compDetails,
|
||||
images,
|
||||
documents,
|
||||
|
||||
@@ -544,7 +544,9 @@
|
||||
{#if data.device.locationName}
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Location</dt>
|
||||
<dd class="text-gray-900 dark:text-white">{data.device.locationName}</dd>
|
||||
<dd class="text-gray-900 dark:text-white">
|
||||
{#if data.parentLocationName}{data.parentLocationName} › {/if}{data.device.locationName}
|
||||
</dd>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
|
||||
@@ -73,11 +73,15 @@ export const actions: Actions = {
|
||||
const id = formData.get('id') as string;
|
||||
const name = (formData.get('name') as string)?.trim();
|
||||
const description = (formData.get('description') as string)?.trim();
|
||||
const parentId = (formData.get('parentId') as string) || null;
|
||||
if (!name) return fail(400, { error: 'Name is required' });
|
||||
|
||||
// Prevent setting self as parent
|
||||
if (parentId === id) return fail(400, { error: 'Cannot set location as its own parent' });
|
||||
|
||||
await db
|
||||
.update(locations)
|
||||
.set({ name, description: description || null, updatedAt: new Date() })
|
||||
.set({ name, description: description || null, parentId, updatedAt: new Date() })
|
||||
.where(eq(locations.id, id));
|
||||
return { renamed: true };
|
||||
},
|
||||
|
||||
@@ -102,6 +102,18 @@
|
||||
<input type="text" id="desc-{loc.id}" name="description" value={loc.description ?? ''}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="parent-{loc.id}" class="mb-1 block text-xs text-gray-500 dark:text-gray-400">Parent</label>
|
||||
<select id="parent-{loc.id}" name="parentId"
|
||||
class="rounded-md border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white">
|
||||
<option value="">None (top level)</option>
|
||||
{#each sortedLocations as other}
|
||||
{#if other.id !== loc.id}
|
||||
<option value={other.id} selected={other.id === loc.parentId}>{'—'.repeat(other.depth)} {other.name}</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700">Save</button>
|
||||
<button type="button" onclick={() => (editingId = null)} class="rounded-md px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700">Cancel</button>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user