From 0dd5b8e17923937bb09d591f6e17dbb70dbb1e05 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Thu, 9 Jul 2026 14:19:29 +0200 Subject: [PATCH] feat: replace home search bar with unified FilterSearchbar Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/lib/components/home/ItemsList.svelte | 196 +++++++----------- .../src/routes/(root)/(logged)/+page.svelte | 2 +- 2 files changed, 77 insertions(+), 121 deletions(-) diff --git a/frontend/src/lib/components/home/ItemsList.svelte b/frontend/src/lib/components/home/ItemsList.svelte index 0fc93d6006..c724e3f05b 100644 --- a/frontend/src/lib/components/home/ItemsList.svelte +++ b/frontend/src/lib/components/home/ItemsList.svelte @@ -21,44 +21,38 @@ ChevronsUpDown, Code2, LayoutDashboard, - ListFilterPlus, SearchCode, Tag } from 'lucide-svelte' - import { HOME_SEARCH_SHOW_FLOW, HOME_SEARCH_PLACEHOLDER } from '$lib/consts' + import { HOME_SEARCH_SHOW_FLOW } from '$lib/consts' import SearchItems from '../SearchItems.svelte' + import FilterSearchbar, { + useUrlSyncedFilterInstance, + type FilterSchemaRec + } from '$lib/components/FilterSearchbar.svelte' import ListFilters from './ListFilters.svelte' import NoItemFound from './NoItemFound.svelte' import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte' import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte' import FlowIcon from './FlowIcon.svelte' import { canWrite, getLocalSetting, storeLocalSetting } from '$lib/utils' - import { page } from '$app/state' - import { setQuery } from '$lib/navigation' import Drawer from '../common/drawer/Drawer.svelte' import HighlightCode from '../HighlightCode.svelte' import DrawerContent from '../common/drawer/DrawerContent.svelte' import Item from './Item.svelte' import TreeViewRoot from './TreeViewRoot.svelte' - import Popover from '$lib/components/meltComponents/Popover.svelte' import { getContext, untrack } from 'svelte' import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte' - import TextInput from '../text_input/TextInput.svelte' import { NetworkIcon } from 'lucide-svelte' import { base } from '$lib/base' interface Props { - filter?: string subtab?: 'flow' | 'script' | 'app' showEditButtons?: boolean } - let { - filter = $bindable(''), - subtab = $bindable('script'), - showEditButtons = true - }: Props = $props() + let { subtab = $bindable('script'), showEditButtons = true }: Props = $props() type TableItem = T & { canWrite: boolean @@ -120,10 +114,6 @@ let filteredItems: (TableScript | TableFlow | TableApp | TableRawApp)[] = $state([]) - let itemKind = $state( - (page.url.searchParams.get('kind') as 'script' | 'flow' | 'app' | 'all') ?? 'all' - ) - let loading = $state(true) let nbDisplayed = $state(15) @@ -268,11 +258,7 @@ } } - let archived = $state(false) - const TREE_VIEW_SETTING_NAME = 'treeView' - const FILTER_USER_FOLDER_SETTING_NAME = 'filterUserFolders' - const INCLUDE_WITHOUT_MAIN_SETTING_NAME = 'includeWithoutMain' let treeView = $state(getLocalSetting(TREE_VIEW_SETTING_NAME) == 'true') let filterUserFoldersType: 'only f/*' | 'u/username and f/*' | undefined = $derived( $userStore?.non_member @@ -281,7 +267,50 @@ ? 'u/username and f/*' : undefined ) - let filterUserFolders = $state(getLocalSetting(FILTER_USER_FOLDER_SETTING_NAME) == 'true') + + // FilterSearchbar schema — `_default_` is the free-text search (same fuzzy match + // as before); the rest are the relevant list filters. The library / user-folder + // filters only apply to the roles that had those toggles. + let searchFilterSchema = $derived({ + _default_: { type: 'string' as const, hidden: true }, + path: { type: 'string' as const, label: 'Path' }, + summary: { type: 'string' as const, label: 'Summary' }, + kind: { + type: 'oneof' as const, + label: 'Kind', + options: [ + { value: 'script', label: 'Script' }, + ...(HOME_SEARCH_SHOW_FLOW ? [{ value: 'flow', label: 'Flow' }] : []), + { value: 'app', label: 'App' } + ] + }, + draft_only: { type: 'boolean' as const, label: 'Draft only' }, + draft: { type: 'boolean' as const, label: 'Draft', description: 'Includes draft-only items' }, + archived: { type: 'boolean' as const, label: 'Only archived' }, + ...($userStore && !$userStore.operator + ? { include_library: { type: 'boolean' as const, label: 'Include library scripts' } } + : {}), + ...(filterUserFoldersType + ? { + only_user_folders: { + type: 'boolean' as const, + label: + filterUserFoldersType === 'only f/*' + ? 'Only f/*' + : `Only u/${$userStore?.username} and f/*` + } + } + : {}) + } satisfies FilterSchemaRec) + + // Single URL-synced source of truth for these filters (loop-safe primitive). + let filterValues = useUrlSyncedFilterInstance(untrack(() => searchFilterSchema)) + + let itemKind = $derived((filterValues.val.kind ?? 'all') as 'script' | 'flow' | 'app' | 'all') + let filter = $derived((filterValues.val._default_ ?? '') as string) + let archived = $derived(!!filterValues.val.archived) + let includeWithoutMain = $derived((filterValues.val.include_library ?? true) as boolean) + let filterUserFolders = $derived(!!filterValues.val.only_user_folders) // Pipeline entries are rendered independently of the item list, so apply the // same gates the items get — otherwise a pipeline would still show under the @@ -301,12 +330,6 @@ ) ) }) - let includeWithoutMain = $state( - getLocalSetting(INCLUDE_WITHOUT_MAIN_SETTING_NAME) - ? getLocalSetting(INCLUDE_WITHOUT_MAIN_SETTING_NAME) == 'true' - : true - ) - const openSearchWithPrefilledText: (t?: string) => void = getContext( 'openSearchWithPrefilledText' ) @@ -396,21 +419,23 @@ } prevWorkspace = ws }) + let pathFilter = $derived((filterValues.val.path ?? '').toLowerCase()) + let summaryFilter = $derived((filterValues.val.summary ?? '').toLowerCase()) + // `draft_only`/`is_draft` aren't on every item type in the union — read defensively. + const isDraftOnly = (x: any) => x?.draft_only === true + const hasDraft = (x: any) => x?.is_draft === true || x?.draft_only === true let preFilteredItems = $derived( - ownerFilter != undefined - ? combinedItems?.filter( - (x) => - x.path.startsWith(ownerFilter + '/') && - (x.type == itemKind || itemKind == 'all') && - filterItemsPathsBaseOnUserFilters(x, filterUserFolders, filterUserFoldersType) && - (labelFilter == undefined || itemLabels(x).includes(labelFilter)) - ) - : combinedItems?.filter( - (x) => - (x.type == itemKind || itemKind == 'all') && - filterItemsPathsBaseOnUserFilters(x, filterUserFolders, filterUserFoldersType) && - (labelFilter == undefined || itemLabels(x).includes(labelFilter)) - ) + combinedItems?.filter( + (x) => + (ownerFilter == undefined || x.path.startsWith(ownerFilter + '/')) && + (x.type == itemKind || itemKind == 'all') && + filterItemsPathsBaseOnUserFilters(x, filterUserFolders, filterUserFoldersType) && + (labelFilter == undefined || itemLabels(x).includes(labelFilter)) && + (!pathFilter || x.path.toLowerCase().includes(pathFilter)) && + (!summaryFilter || (x.summary ?? '').toLowerCase().includes(summaryFilter)) && + (!filterValues.val.draft_only || isDraftOnly(x)) && + (!filterValues.val.draft || hasDraft(x)) + ) ) let items = $derived(filter !== '' ? filteredItems : preFilteredItems) let displayedItems = $derived((items ?? []).slice(0, nbDisplayed)) @@ -421,12 +446,6 @@ $effect(() => { storeLocalSetting(TREE_VIEW_SETTING_NAME, treeView ? 'true' : undefined) }) - $effect(() => { - storeLocalSetting(FILTER_USER_FOLDER_SETTING_NAME, filterUserFolders ? 'true' : undefined) - }) - $effect(() => { - storeLocalSetting(INCLUDE_WITHOUT_MAIN_SETTING_NAME, includeWithoutMain ? 'true' : undefined) - })
{ - if (itemKind != 'all') { + // Shortcut into the shared filter object; `all` clears the kind filter. + filterValues.val.kind = v === 'all' ? null : v + if (v != 'all') { subtab = v } - setQuery(page.url, 'kind', v) }} > {#snippet children({ item })} @@ -498,38 +518,13 @@
-
- - + -