bug fix + add feature

fix salah url ketika sidebar collapsed
menambahkan type select pada reusable searchparam
This commit is contained in:
faiztyanirh 2026-02-13 22:05:08 +07:00
parent b03c0565f1
commit 4f61832f19
5 changed files with 117 additions and 14 deletions

View File

@ -2,6 +2,8 @@ export function useSearch(searchFields, searchApiFunction) {
let searchQuery = $state(initializeSearchQuery(searchFields)); let searchQuery = $state(initializeSearchQuery(searchFields));
let isLoading = $state(false); let isLoading = $state(false);
let searchData = $state([]); let searchData = $state([]);
let selectOptions = $state({});
let loadingOptions = $state({});
function initializeSearchQuery(fields) { function initializeSearchQuery(fields) {
const query = {}; const query = {};
@ -30,12 +32,38 @@ export function useSearch(searchFields, searchApiFunction) {
searchQuery = initializeSearchQuery(searchFields); searchQuery = initializeSearchQuery(searchFields);
} }
async function fetchOptions(field) {
if (!field.optionsEndpoint) return;
if (selectOptions[field.key]?.length) return;
loadingOptions[field.key] = true;
try {
const response = await fetch(field.optionsEndpoint);
const json = await response.json();
selectOptions[field.key] = json.data.map(item => ({
value: item[field.valueKey ?? 'value'],
label: item[field.labelKey ?? 'label'],
}));
} catch (err) {
console.error("Failed fetching options:", err);
selectOptions[field.key] = [];
} finally {
loadingOptions[field.key] = false;
}
}
return { return {
get searchQuery() { return searchQuery; }, get searchQuery() { return searchQuery; },
set searchQuery(value) { searchQuery = value; }, set searchQuery(value) { searchQuery = value; },
get searchData() { return searchData; }, get searchData() { return searchData; },
get isLoading() { return isLoading; }, get isLoading() { return isLoading; },
get loadingOptions() { return loadingOptions; },
get selectOptions() { return selectOptions; },
handleSearch, handleSearch,
handleReset handleReset,
fetchOptions,
}; };
} }

View File

@ -16,7 +16,8 @@ export const searchFields = [
{ {
key: "LocType", key: "LocType",
label: "Location Type", label: "Location Type",
type: "text" type: "select",
optionsEndpoint: `https://clqms01-api.services-summit.my.id/api/valueset/location_type`,
}, },
]; ];

View File

@ -20,7 +20,10 @@
</script> </script>
{#snippet searchParamSnippet()} {#snippet searchParamSnippet()}
<ReusableSearchParam {searchFields} bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}/> <ReusableSearchParam {searchFields}
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
/>
{/snippet} {/snippet}
<div <div

View File

@ -66,7 +66,7 @@
{/snippet} {/snippet}
</Collapsible.Root> </Collapsible.Root>
{:else} {:else}
<Popover.Root open={openPopovers[index]} onOpenChange={(open) => openPopovers[index] = open}> <Popover.Root open={openPopovers[item.url]} onOpenChange={(open) => openPopovers[item.url] = open}>
<Sidebar.MenuItem> <Sidebar.MenuItem>
{#snippet trigger(props)} {#snippet trigger(props)}
<Popover.Trigger {...props}> <Popover.Trigger {...props}>
@ -89,8 +89,8 @@
</div> </div>
<Separator /> <Separator />
{#each item.submenus || [] as submenu} {#each item.submenus || [] as submenu}
<a href={submenu.url} <a href={`${item.url}${submenu.url}`}
onclick={() => openPopovers[index] = false} onclick={() => openPopovers[item.url] = false}
class="flex items-center rounded-md px-2 py-1.5 text-sm hover:bg-accent" class="flex items-center rounded-md px-2 py-1.5 text-sm hover:bg-accent"
class:bg-accent={$page.url.pathname === submenu.url} class:bg-accent={$page.url.pathname === submenu.url}
> >
@ -99,7 +99,7 @@
{/each} {/each}
{:else} {:else}
<a href={item.url} <a href={item.url}
onclick={() => openPopovers[index] = false} onclick={() => openPopovers[item.url] = false}
class="flex items-center rounded-md px-2 py-1.5 text-sm font-semibold hover:bg-accent" class="flex items-center rounded-md px-2 py-1.5 text-sm font-semibold hover:bg-accent"
class:bg-accent={$page.url.pathname === item.url} class:bg-accent={$page.url.pathname === item.url}
> >

View File

@ -7,10 +7,20 @@
import * as Select from "$lib/components/ui/select/index.js"; import * as Select from "$lib/components/ui/select/index.js";
let props = $props(); let props = $props();
let loadedFields = $state(new Set());
function handleOpenSelect(field) {
if (loadedFields.has(field.key)) return;
loadedFields.add(field.key);
props.fetchOptions(field);
}
$inspect(props.searchQuery)
</script> </script>
<div class="w-full"> <div class="w-full">
<div class="space-y-2"> <div class="w-full space-y-2">
{#each props.searchFields as field} {#each props.searchFields as field}
{#if field.type === "text"} {#if field.type === "text"}
<div class="space-y-2"> <div class="space-y-2">
@ -22,20 +32,81 @@
<ReusableCalendar title={field.label} bind:value={props.searchQuery[field.key]}/> <ReusableCalendar title={field.label} bind:value={props.searchQuery[field.key]}/>
</div> </div>
{:else if field.type === "select"} {:else if field.type === "select"}
<div class="space-y-2"> <div class="w-full space-y-2">
<Label for={field.key}>{field.label}</Label> <Label for={field.key}>{field.label}</Label>
<Select.Root bind:value={props.searchQuery[field.key]}> <Select.Root type="single" bind:value={props.searchQuery[field.key]}
<Select.Trigger id={field.key}> onOpenChange={() => {
<Select.Value placeholder={field.placeholder} /> props.fetchOptions(field)
}}
>
<Select.Trigger id={field.key} class="w-full flex justify-between">
{
props.selectOptions[field.key]?.find(
opt => opt.value === props.searchQuery[field.key]
)?.label ?? "Choose"
}
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content class="w-full">
{#each field.options as opt} <Select.Item value="">- None -</Select.Item>
{#each props.selectOptions[field.key] as opt}
<Select.Item value={opt.value}> <Select.Item value={opt.value}>
{opt.label} {opt.label}
</Select.Item> </Select.Item>
{/each} {/each}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
<!-- === -->
<!-- <Select.Root type="single" bind:value={props.searchQuery[field.key]}
onOpenChange={(open) => {
if (open && optionsEndpoint) {
formState.fetchOptions(
{ key, optionsEndpoint, dependsOn, endpointParamKey, valueKey, labelKey },
formState.form
);
}
}}
>
<Select.Trigger class="w-full truncate">
{selectedLabel}
</Select.Trigger>
<Select.Content>
{#if formState.loadingOptions[key]}
<Select.Item disabled value="loading">Loading...</Select.Item>
{:else}
{#if !required}
<Select.Item value="">- None -</Select.Item>
{/if}
{#each selectOptions as option}
<Select.Item value={option.value}>
{option.label}
</Select.Item>
{/each}
{/if}
</Select.Content>
</Select.Root> -->
<!-- === -->
<!-- <Select.Root bind:value={props.searchQuery[field.key]}>
<Select.Trigger id={field.key}>
<Select.Value placeholder={`Select ${field.label}`} />
</Select.Trigger>
<Select.Content>
{#if props.loadingOptions[field.key]}
<Select.Item value="" disabled>
Loading...
</Select.Item>
{:else}
<Select.Item value="">
All {field.label}
</Select.Item>
{#each props.selectOptions[field.key] || [] as option}
<Select.Item value={option.value}>
{option.label}
</Select.Item>
{/each}
{/if}
</Select.Content>
</Select.Root> -->
</div> </div>
{/if} {/if}
{/each} {/each}