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
@@ -0,0 +1,71 @@
<script lang="ts">
interface Location {
id: string;
name: string;
parentId: string | null;
}
interface Props {
locations: Location[];
value?: string | null;
name?: string;
id?: string;
}
let { locations, value = $bindable(null), name = 'locationId', id = 'locationId' }: Props = $props();
const parents = $derived(locations.filter((l) => !l.parentId));
const childrenOf = $derived((parentId: string) => locations.filter((l) => l.parentId === parentId));
// Find current parent from value
const selectedLoc = $derived(locations.find((l) => l.id === value));
let selectedParent = $state<string>('');
// Init selectedParent from current value
$effect(() => {
if (selectedLoc) {
selectedParent = selectedLoc.parentId ?? selectedLoc.id;
}
});
const children = $derived(selectedParent ? childrenOf(selectedParent) : []);
const parentIsLeaf = $derived(selectedParent ? childrenOf(selectedParent).length === 0 : false);
// When parent changes, auto-select it if it has no children
$effect(() => {
if (selectedParent && parentIsLeaf) {
value = selectedParent;
} else if (selectedParent && children.length > 0 && !children.find((c) => c.id === value)) {
value = null;
}
});
</script>
<div class="space-y-2">
<select
onchange={(e) => {
selectedParent = (e.target as HTMLSelectElement).value;
value = null;
}}
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 parents as loc}
<option value={loc.id} selected={loc.id === selectedParent}>{loc.name}</option>
{/each}
</select>
{#if selectedParent && children.length > 0}
<select
onchange={(e) => { value = (e.target as HTMLSelectElement).value || selectedParent; }}
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="" selected={value === selectedParent}>{parents.find(p => p.id === selectedParent)?.name} (general)</option>
{#each children as child}
<option value={child.id} selected={child.id === value}>{child.name}</option>
{/each}
</select>
{/if}
<input type="hidden" {name} {id} value={value ?? ''} />
</div>