Add cascading location picker: select parent first, then child
Deploy to LXC / deploy (push) Successful in 19s

LocationPicker component shows parent locations first. Once a parent
is selected, a second dropdown appears with its children. If the
parent has no children, it's selected directly. Used in device
create/edit, component create/edit, and installation log forms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 16:59:49 +07:00
parent 63b57e8ac3
commit 948617a285
11 changed files with 94 additions and 46 deletions
@@ -35,7 +35,7 @@ export const load: PageServerLoad = async ({ params }) => {
compDetails = cd ?? null;
}
const locationList = await db.select({ id: locations.id, name: locations.name }).from(locations);
const locationList = await db.select({ id: locations.id, name: locations.name, parentId: locations.parentId }).from(locations).orderBy(locations.name);
return { device, computerDetails: compDetails, locations: locationList };
};
@@ -2,6 +2,7 @@
import { enhance } from '$app/forms';
import { DEVICE_CATEGORIES, DEVICE_CONDITIONS, VOLTAGE_OPTIONS, FREQUENCY_OPTIONS } from '$lib/constants.js';
import AutocompleteInput from '$lib/components/ui/AutocompleteInput.svelte';
import LocationPicker from '$lib/components/ui/LocationPicker.svelte';
let { data, form } = $props();
@@ -157,14 +158,8 @@
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Location & Notes</h2>
<div class="mb-4">
<label for="locationId" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Location</label>
<select id="locationId" name="locationId"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="">No location</option>
{#each data.locations as loc}
<option value={loc.id} selected={loc.id === (form?.values?.locationId ?? d.locationId)}>{loc.name}</option>
{/each}
</select>
<span class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Location</span>
<LocationPicker locations={data.locations} value={d.locationId} />
</div>
<div>
<label for="generalNotes" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">General Notes</label>
+1 -1
View File
@@ -27,7 +27,7 @@ const deviceSchema = z.object({
});
export const load: PageServerLoad = async () => {
const locationList = await db.select({ id: locations.id, name: locations.name }).from(locations);
const locationList = await db.select({ id: locations.id, name: locations.name, parentId: locations.parentId }).from(locations).orderBy(locations.name);
return { locations: locationList };
};
+3 -8
View File
@@ -2,6 +2,7 @@
import { enhance } from '$app/forms';
import { DEVICE_CATEGORIES, DEVICE_CONDITIONS, VOLTAGE_OPTIONS, FREQUENCY_OPTIONS } from '$lib/constants.js';
import AutocompleteInput from '$lib/components/ui/AutocompleteInput.svelte';
import LocationPicker from '$lib/components/ui/LocationPicker.svelte';
let { data, form } = $props();
@@ -179,14 +180,8 @@
<h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Location & Notes</h2>
<div class="mb-4">
<label for="locationId" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Location</label>
<select id="locationId" name="locationId"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="">No location</option>
{#each data.locations as loc}
<option value={loc.id} selected={form?.values?.locationId === loc.id}>{loc.name}</option>
{/each}
</select>
<span class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Location</span>
<LocationPicker locations={data.locations} />
</div>
<div>