mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-26 08:01:38 +00:00
feat: replace home page search bars with FilterSearchbar component
Replace both Workspace and Hub tab search bars with the new FilterSearchbar component for structured filtering. Workspace tab filters: summary, path, description, kind, user, group, folder Hub tab filters: tag, summary, path, kind Existing filter shortcuts (ToggleButtonGroup, ListFilters badges, owner filter, archived/tree toggles) are preserved and bidirectionally synced with the new FilterSearchbar state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,14 +15,26 @@
|
||||
syncQuery?: boolean
|
||||
children?: import('svelte').Snippet
|
||||
size?: ButtonType.UnifiedSize
|
||||
hideSearchbar?: boolean
|
||||
appFilter?: string | undefined
|
||||
summaryFilter?: string
|
||||
pathFilter?: string
|
||||
}
|
||||
|
||||
let { filter = $bindable(''), syncQuery = false, children, size = 'md' }: Props = $props()
|
||||
let {
|
||||
filter = $bindable(''),
|
||||
syncQuery = false,
|
||||
children,
|
||||
size = 'md',
|
||||
hideSearchbar = false,
|
||||
appFilter = $bindable(undefined),
|
||||
summaryFilter,
|
||||
pathFilter
|
||||
}: Props = $props()
|
||||
|
||||
type Item = { apps: string[]; summary: string; path: string }
|
||||
let hubApps: any[] | undefined = $state(undefined)
|
||||
let filteredItems: (Item & { marked?: string })[] = $state([])
|
||||
let appFilter: string | undefined = $state(undefined)
|
||||
|
||||
const prefilteredItems = $derived(
|
||||
appFilter ? (hubApps ?? []).filter((i: Item) => i.apps.includes(appFilter!)) : (hubApps ?? [])
|
||||
@@ -30,6 +42,20 @@
|
||||
|
||||
const apps = $derived(Array.from(new Set(filteredItems?.flatMap((x) => x.apps) ?? [])).sort())
|
||||
|
||||
// Apply summary/path post-filters
|
||||
let displayItems = $derived.by(() => {
|
||||
let result = filteredItems
|
||||
if (summaryFilter) {
|
||||
const s = summaryFilter.toLowerCase()
|
||||
result = result.filter((x) => x.summary.toLowerCase().includes(s))
|
||||
}
|
||||
if (pathFilter) {
|
||||
const p = pathFilter.toLowerCase()
|
||||
result = result.filter((x) => x.path.toLowerCase().includes(p))
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let hubNotAvailable = $state(false)
|
||||
@@ -54,6 +80,7 @@
|
||||
bind:filteredItems
|
||||
f={(x) => x.summary + ' (' + x.apps.join(', ') + ')'}
|
||||
/>
|
||||
{#if !hideSearchbar}
|
||||
<div class="w-full flex items-center gap-2">
|
||||
{@render children?.()}
|
||||
<TextInput
|
||||
@@ -65,6 +92,7 @@
|
||||
{size}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<ListFilters {syncQuery} filters={apps} bind:selectedFilter={appFilter} resourceType />
|
||||
|
||||
{#if hubNotAvailable}
|
||||
@@ -72,11 +100,11 @@
|
||||
Could not connect to the Windmill Hub. If you are in a closed environment, you can disable the Hub in the <a href="/#superadmin-settings?tab=private_hub">instance settings</a>.
|
||||
</Alert>
|
||||
{:else if hubApps}
|
||||
{#if filteredItems.length == 0}
|
||||
{#if displayItems.length == 0}
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<ul class="divide-y border rounded-md bg-surface-tertiary">
|
||||
{#each filteredItems as item (item)}
|
||||
{#each displayItems as item (item)}
|
||||
<li class="flex flex-row w-full">
|
||||
<button
|
||||
class="p-4 gap-4 flex flex-row grow justify-between hover:bg-surface-hover transition-all items-center"
|
||||
|
||||
@@ -15,14 +15,26 @@
|
||||
syncQuery?: boolean
|
||||
children?: import('svelte').Snippet
|
||||
size?: ButtonType.UnifiedSize
|
||||
hideSearchbar?: boolean
|
||||
appFilter?: string | undefined
|
||||
summaryFilter?: string
|
||||
pathFilter?: string
|
||||
}
|
||||
|
||||
let { filter = $bindable(''), syncQuery = false, children, size = 'md' }: Props = $props()
|
||||
let {
|
||||
filter = $bindable(''),
|
||||
syncQuery = false,
|
||||
children,
|
||||
size = 'md',
|
||||
hideSearchbar = false,
|
||||
appFilter = $bindable(undefined),
|
||||
summaryFilter,
|
||||
pathFilter
|
||||
}: Props = $props()
|
||||
|
||||
type Item = { apps: string[]; summary: string; path: string }
|
||||
let hubFlows: any[] | undefined = $state(undefined)
|
||||
let filteredItems: (Item & { marked?: string })[] = $state([])
|
||||
let appFilter: string | undefined = $state(undefined)
|
||||
|
||||
const prefilteredItems = $derived(
|
||||
appFilter ? (hubFlows ?? []).filter((i: Item) => i.apps.includes(appFilter!)) : (hubFlows ?? [])
|
||||
@@ -30,6 +42,20 @@
|
||||
|
||||
const apps = $derived(Array.from(new Set(filteredItems?.flatMap((x) => x.apps) ?? [])).sort())
|
||||
|
||||
// Apply summary/path post-filters
|
||||
let displayItems = $derived.by(() => {
|
||||
let result = filteredItems
|
||||
if (summaryFilter) {
|
||||
const s = summaryFilter.toLowerCase()
|
||||
result = result.filter((x) => x.summary.toLowerCase().includes(s))
|
||||
}
|
||||
if (pathFilter) {
|
||||
const p = pathFilter.toLowerCase()
|
||||
result = result.filter((x) => x.path.toLowerCase().includes(p))
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let hubNotAvailable = $state(false)
|
||||
@@ -54,6 +80,7 @@
|
||||
bind:filteredItems
|
||||
f={(x) => x.summary + ' (' + x.apps.join(', ') + ')'}
|
||||
/>
|
||||
{#if !hideSearchbar}
|
||||
<div class="w-full flex items-center gap-2">
|
||||
{@render children?.()}
|
||||
<TextInput
|
||||
@@ -65,6 +92,7 @@
|
||||
class="grow !pr-9"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<ListFilters {syncQuery} filters={apps} bind:selectedFilter={appFilter} resourceType />
|
||||
|
||||
{#if hubNotAvailable}
|
||||
@@ -72,11 +100,11 @@
|
||||
Could not connect to the Windmill Hub. If you are in a closed environment, you can disable the Hub in the <a href="/#superadmin-settings?tab=private_hub">instance settings</a>.
|
||||
</Alert>
|
||||
{:else if hubFlows}
|
||||
{#if filteredItems.length == 0}
|
||||
{#if displayItems.length == 0}
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<ul class="divide-y border rounded-md bg-surface-tertiary overflow-hidden">
|
||||
{#each filteredItems as item (item)}
|
||||
{#each displayItems as item (item)}
|
||||
<li class="flex flex-row w-full">
|
||||
<button
|
||||
class="p-4 gap-4 flex flex-row grow justify-between hover:bg-surface-hover transition-all items-center"
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
syncQuery?: boolean
|
||||
children?: import('svelte').Snippet
|
||||
size?: ButtonType.UnifiedSize
|
||||
hideSearchbar?: boolean
|
||||
appFilter?: string | undefined
|
||||
summaryFilter?: string
|
||||
pathFilter?: string
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -23,7 +27,11 @@
|
||||
filter = $bindable(''),
|
||||
syncQuery = false,
|
||||
children,
|
||||
size = 'md'
|
||||
size = 'md',
|
||||
hideSearchbar = false,
|
||||
appFilter = $bindable(undefined),
|
||||
summaryFilter,
|
||||
pathFilter
|
||||
}: Props = $props()
|
||||
|
||||
let loading = $state(false)
|
||||
@@ -31,7 +39,6 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let appFilter: string | undefined = $state(undefined)
|
||||
let items: {
|
||||
path: string
|
||||
summary: string
|
||||
@@ -131,6 +138,20 @@
|
||||
dispatch('pick', item)
|
||||
}
|
||||
|
||||
// Apply summary/path post-filters
|
||||
let displayItems = $derived.by(() => {
|
||||
let result = items
|
||||
if (summaryFilter) {
|
||||
const s = summaryFilter.toLowerCase()
|
||||
result = result.filter((x) => x.summary.toLowerCase().includes(s))
|
||||
}
|
||||
if (pathFilter) {
|
||||
const p = pathFilter.toLowerCase()
|
||||
result = result.filter((x) => x.path.toLowerCase().includes(p))
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
;[filter, kind, appFilter]
|
||||
untrack(() => applyFilter(filter, kind, appFilter))
|
||||
@@ -144,6 +165,7 @@
|
||||
{#if $disableHubStore}
|
||||
<!-- Hub disabled, show nothing -->
|
||||
{:else}
|
||||
{#if !hideSearchbar}
|
||||
<div class="w-full flex items-center gap-2">
|
||||
{@render children?.()}
|
||||
<div class="relative w-full">
|
||||
@@ -160,6 +182,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if hubNotAvailable}
|
||||
<Alert type="warning" title="Hub not available">
|
||||
@@ -167,11 +190,11 @@
|
||||
</Alert>
|
||||
{:else if (items.length > 0 && apps.length > 0) || !loading}
|
||||
<ListFilters {syncQuery} filters={apps} bind:selectedFilter={appFilter} resourceType />
|
||||
{#if items.length == 0}
|
||||
{#if displayItems.length == 0}
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<ul class="divide-y border rounded-md bg-surface-tertiary">
|
||||
{#each items as item (item.path)}
|
||||
{#each displayItems as item (item.path)}
|
||||
<li class="flex flex-row w-full">
|
||||
<button
|
||||
class="p-4 gap-4 flex flex-row grow hover:bg-surface-hover transition-all items-center"
|
||||
@@ -202,7 +225,7 @@
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if items.length == 20}
|
||||
{#if displayItems.length == 20}
|
||||
<div class="text-primary text-xs font-normal py-4">
|
||||
There are more items than being displayed. Refine your search.
|
||||
</div>
|
||||
|
||||
@@ -17,22 +17,30 @@
|
||||
ChevronsDownUp,
|
||||
ChevronsUpDown,
|
||||
Code2,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
LayoutDashboard,
|
||||
ListFilterPlus,
|
||||
SearchCode
|
||||
Route,
|
||||
SearchCode,
|
||||
Type,
|
||||
User,
|
||||
Users
|
||||
} 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, {
|
||||
type FilterSchemaRec,
|
||||
useUrlSyncedFilterInstance
|
||||
} from '../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'
|
||||
@@ -41,19 +49,65 @@
|
||||
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'
|
||||
|
||||
interface Props {
|
||||
filter?: string
|
||||
subtab?: 'flow' | 'script' | 'app'
|
||||
showEditButtons?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
filter = $bindable(''),
|
||||
subtab = $bindable('script'),
|
||||
showEditButtons = true
|
||||
}: Props = $props()
|
||||
|
||||
const filterSchema: FilterSchemaRec = {
|
||||
_default_: { type: 'string', hidden: true },
|
||||
summary: {
|
||||
type: 'string',
|
||||
label: 'Summary',
|
||||
description: 'Filter by summary text',
|
||||
icon: Type
|
||||
},
|
||||
path: { type: 'string', label: 'Path', description: 'Filter by path', icon: Route },
|
||||
description: {
|
||||
type: 'string',
|
||||
label: 'Description',
|
||||
description: 'Filter by description',
|
||||
icon: FileText
|
||||
},
|
||||
kind: {
|
||||
type: 'oneof',
|
||||
label: 'Kind',
|
||||
description: 'Filter by runnable type',
|
||||
options: [
|
||||
{ value: 'script', label: 'Script' },
|
||||
{ value: 'flow', label: 'Flow' },
|
||||
{ value: 'app', label: 'App' }
|
||||
]
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
label: 'User',
|
||||
description: 'Filter by owner user (u/...)',
|
||||
icon: User
|
||||
},
|
||||
group: {
|
||||
type: 'string',
|
||||
label: 'Group',
|
||||
description: 'Filter by group access',
|
||||
icon: Users
|
||||
},
|
||||
folder: {
|
||||
type: 'string',
|
||||
label: 'Folder',
|
||||
description: 'Filter by folder (f/...)',
|
||||
icon: FolderOpen
|
||||
}
|
||||
}
|
||||
|
||||
let filterValue = useUrlSyncedFilterInstance(filterSchema)
|
||||
let freeTextFilter = $derived((filterValue.val._default_ as string) ?? '')
|
||||
|
||||
type TableItem<T, U extends 'script' | 'flow' | 'app' | 'raw_app'> = T & {
|
||||
canWrite: boolean
|
||||
marked?: string
|
||||
@@ -76,9 +130,21 @@
|
||||
|
||||
let filteredItems: (TableScript | TableFlow | TableApp | TableRawApp)[] = $state([])
|
||||
|
||||
let itemKind = $state(
|
||||
(page.url.searchParams.get('kind') as 'script' | 'flow' | 'app' | 'all') ?? 'all'
|
||||
)
|
||||
let itemKind = $state('all' as 'script' | 'flow' | 'app' | 'all')
|
||||
|
||||
// Sync FilterSearchbar kind → itemKind
|
||||
$effect(() => {
|
||||
const k = filterValue.val.kind
|
||||
itemKind = k ? (k as 'script' | 'flow' | 'app') : 'all'
|
||||
})
|
||||
|
||||
// Sync kind → subtab
|
||||
$effect(() => {
|
||||
const k = filterValue.val.kind as string | undefined
|
||||
if (k === 'script' || k === 'flow' || k === 'app') {
|
||||
subtab = k
|
||||
}
|
||||
})
|
||||
|
||||
let loading = $state(true)
|
||||
|
||||
@@ -89,8 +155,7 @@
|
||||
workspace: $workspaceStore!,
|
||||
showArchived: archived ? true : undefined,
|
||||
includeWithoutMain: includeWithoutMain ? true : undefined,
|
||||
includeDraftOnly: true,
|
||||
withoutDescription: true
|
||||
includeDraftOnly: true
|
||||
})
|
||||
|
||||
scripts = loadedScripts.map((script: Script) => {
|
||||
@@ -107,8 +172,7 @@
|
||||
await FlowService.listFlows({
|
||||
workspace: $workspaceStore!,
|
||||
showArchived: archived ? true : undefined,
|
||||
includeDraftOnly: true,
|
||||
withoutDescription: true
|
||||
includeDraftOnly: true
|
||||
})
|
||||
).map((x: Flow) => {
|
||||
return {
|
||||
@@ -301,21 +365,76 @@
|
||||
ownerFilter = undefined
|
||||
}
|
||||
})
|
||||
let preFilteredItems = $derived(
|
||||
ownerFilter != undefined
|
||||
? combinedItems?.filter(
|
||||
(x) =>
|
||||
x.path.startsWith(ownerFilter + '/') &&
|
||||
(x.type == itemKind || itemKind == 'all') &&
|
||||
filterItemsPathsBaseOnUserFilters(x, filterUserFolders, filterUserFoldersType)
|
||||
)
|
||||
: combinedItems?.filter(
|
||||
(x) =>
|
||||
(x.type == itemKind || itemKind == 'all') &&
|
||||
filterItemsPathsBaseOnUserFilters(x, filterUserFolders, filterUserFoldersType)
|
||||
)
|
||||
let preFilteredItems = $derived.by(() => {
|
||||
let result = combinedItems
|
||||
if (!result) return undefined
|
||||
|
||||
const fv = filterValue.val
|
||||
|
||||
// Kind filter (from searchbar or ToggleButtonGroup)
|
||||
if (fv.kind) {
|
||||
const k = fv.kind as string
|
||||
result = result.filter((x) =>
|
||||
k === 'app' ? x.type === 'app' || x.type === 'raw_app' : x.type === k
|
||||
)
|
||||
}
|
||||
|
||||
// Owner filter from ListFilters badges
|
||||
if (ownerFilter != undefined) {
|
||||
result = result.filter((x) => x.path.startsWith(ownerFilter + '/'))
|
||||
}
|
||||
|
||||
// User filter
|
||||
if (fv.user) {
|
||||
const u = (fv.user as string).toLowerCase()
|
||||
result = result.filter((x) => x.path.toLowerCase().startsWith(`u/${u}/`))
|
||||
}
|
||||
|
||||
// Folder filter
|
||||
if (fv.folder) {
|
||||
const f = (fv.folder as string).toLowerCase()
|
||||
result = result.filter((x) => x.path.toLowerCase().startsWith(`f/${f}/`))
|
||||
}
|
||||
|
||||
// Group filter (check extra_perms for g/<group>)
|
||||
if (fv.group) {
|
||||
const g = `g/${fv.group as string}`
|
||||
result = result.filter((x) => {
|
||||
const perms = (x as any).extra_perms as Record<string, boolean> | undefined
|
||||
return perms && g in perms
|
||||
})
|
||||
}
|
||||
|
||||
// Summary filter
|
||||
if (fv.summary) {
|
||||
const s = (fv.summary as string).toLowerCase()
|
||||
result = result.filter((x) => x.summary?.toLowerCase().includes(s))
|
||||
}
|
||||
|
||||
// Path filter
|
||||
if (fv.path) {
|
||||
const p = (fv.path as string).toLowerCase()
|
||||
result = result.filter((x) => x.path.toLowerCase().includes(p))
|
||||
}
|
||||
|
||||
// Description filter
|
||||
if (fv.description) {
|
||||
const d = (fv.description as string).toLowerCase()
|
||||
result = result.filter((x) => (x as any).description?.toLowerCase().includes(d))
|
||||
}
|
||||
|
||||
// User folders filter
|
||||
result = result.filter((x) =>
|
||||
filterItemsPathsBaseOnUserFilters(x, filterUserFolders, filterUserFoldersType)
|
||||
)
|
||||
|
||||
return result
|
||||
})
|
||||
let hasActiveFilters = $derived(
|
||||
Object.keys(filterValue.val).some((k) => k !== '_default_' && filterValue.val[k] != null) ||
|
||||
ownerFilter != undefined
|
||||
)
|
||||
let items = $derived(filter !== '' ? filteredItems : preFilteredItems)
|
||||
let items = $derived(freeTextFilter !== '' ? filteredItems : preFilteredItems)
|
||||
$effect(() => {
|
||||
items && resetScroll()
|
||||
})
|
||||
@@ -331,7 +450,7 @@
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
{filter}
|
||||
filter={freeTextFilter}
|
||||
items={preFilteredItems}
|
||||
bind:filteredItems
|
||||
f={(x) => (x.summary ? x.summary + ' (' + x.path + ')' : x.path)}
|
||||
@@ -368,10 +487,11 @@
|
||||
<ToggleButtonGroup
|
||||
bind:selected={itemKind}
|
||||
onSelected={(v) => {
|
||||
if (itemKind != 'all') {
|
||||
subtab = v
|
||||
if (v === 'all') {
|
||||
delete filterValue.val.kind
|
||||
} else {
|
||||
filterValue.val.kind = v
|
||||
}
|
||||
setQuery(page.url, 'kind', v)
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
@@ -399,39 +519,12 @@
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
<div class="relative text-primary grow min-w-[100px]">
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<TextInput
|
||||
inputProps={{
|
||||
autofocus: true,
|
||||
placeholder: HOME_SEARCH_PLACEHOLDER,
|
||||
id: 'home-search-input'
|
||||
}}
|
||||
size="md"
|
||||
bind:value={filter}
|
||||
class="!pr-10"
|
||||
/>
|
||||
<button aria-label="Search" type="submit" class="absolute right-0 top-0 mt-2 mr-4">
|
||||
<svg
|
||||
class="h-4 w-4 fill-current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 56.966 56.966"
|
||||
style="enable-background:new 0 0 56.966 56.966;"
|
||||
xml:space="preserve"
|
||||
width="512px"
|
||||
height="512px"
|
||||
>
|
||||
<path
|
||||
d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23 s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92 c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17 s-17-7.626-17-17S14.61,6,23.984,6z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<FilterSearchbar
|
||||
schema={filterSchema}
|
||||
bind:value={filterValue.val}
|
||||
placeholder="Filter scripts, flows, and apps..."
|
||||
class="grow min-w-[100px]"
|
||||
/>
|
||||
<Button
|
||||
on:click={() => openSearchWithPrefilledText('#')}
|
||||
variant="default"
|
||||
@@ -522,13 +615,15 @@
|
||||
<Skeleton layout={[[4], 0.5]} />
|
||||
{/each}
|
||||
{:else if filteredItems.length === 0}
|
||||
<NoItemFound hasFilters={filter !== '' || archived || filterUserFolders} />
|
||||
<NoItemFound
|
||||
hasFilters={freeTextFilter !== '' || hasActiveFilters || archived || filterUserFolders}
|
||||
/>
|
||||
{:else if treeView}
|
||||
<TreeViewRoot
|
||||
{items}
|
||||
{nbDisplayed}
|
||||
{collapseAll}
|
||||
isSearching={filter !== ''}
|
||||
isSearching={freeTextFilter !== '' || hasActiveFilters}
|
||||
on:scriptChanged={() => loadScripts(includeWithoutMain)}
|
||||
on:flowChanged={loadFlows}
|
||||
on:appChanged={loadApps}
|
||||
|
||||
@@ -20,18 +20,23 @@
|
||||
Globe2,
|
||||
Loader2,
|
||||
Code,
|
||||
LayoutDashboard
|
||||
LayoutDashboard,
|
||||
Route,
|
||||
Tag,
|
||||
Type
|
||||
} from 'lucide-svelte'
|
||||
import { hubBaseUrlStore } from '$lib/stores'
|
||||
import { base } from '$lib/base'
|
||||
|
||||
import ItemsList from '$lib/components/home/ItemsList.svelte'
|
||||
import FilterSearchbar, {
|
||||
type FilterSchemaRec
|
||||
} from '$lib/components/FilterSearchbar.svelte'
|
||||
import CreateActionsApp from '$lib/components/flows/CreateActionsApp.svelte'
|
||||
import PickHubApp from '$lib/components/flows/pickers/PickHubApp.svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import type { EditorBreakpoint } from '$lib/components/apps/types'
|
||||
import { HOME_SHOW_HUB, HOME_SHOW_CREATE_FLOW, HOME_SHOW_CREATE_APP } from '$lib/consts'
|
||||
import { setQuery } from '$lib/navigation'
|
||||
import { page } from '$app/state'
|
||||
import { goto, replaceState } from '$app/navigation'
|
||||
import ForkWorkspaceBanner from '$lib/components/ForkWorkspaceBanner.svelte'
|
||||
@@ -52,7 +57,65 @@
|
||||
|
||||
let subtab: 'flow' | 'script' | 'app' = $state('script')
|
||||
|
||||
let filter: string = $state('')
|
||||
// Hub filter schema and state
|
||||
const hubFilterSchema: FilterSchemaRec = {
|
||||
_default_: { type: 'string', hidden: true },
|
||||
tag: {
|
||||
type: 'string',
|
||||
label: 'Tag',
|
||||
description: 'Filter by integration/app tag',
|
||||
icon: Tag
|
||||
},
|
||||
summary: {
|
||||
type: 'string',
|
||||
label: 'Summary',
|
||||
description: 'Filter by summary text',
|
||||
icon: Type
|
||||
},
|
||||
path: { type: 'string', label: 'Path', description: 'Filter by path', icon: Route },
|
||||
kind: {
|
||||
type: 'oneof',
|
||||
label: 'Kind',
|
||||
description: 'Filter by runnable type',
|
||||
options: [
|
||||
{ value: 'script', label: 'Script' },
|
||||
{ value: 'flow', label: 'Flow' },
|
||||
{ value: 'app', label: 'App' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
let hubFilterValue: Record<string, any> = $state({})
|
||||
let hubFreeText = $derived((hubFilterValue._default_ as string) ?? '')
|
||||
let hubAppFilter: string | undefined = $state(undefined)
|
||||
|
||||
// Sync hub kind filter ↔ subtab
|
||||
$effect(() => {
|
||||
const k = hubFilterValue.kind as string | undefined
|
||||
if (k === 'script' || k === 'flow' || k === 'app') {
|
||||
subtab = k
|
||||
}
|
||||
})
|
||||
|
||||
// Sync hub tag filter → hubAppFilter
|
||||
$effect(() => {
|
||||
const tag = hubFilterValue.tag as string | undefined
|
||||
if (tag !== hubAppFilter) {
|
||||
hubAppFilter = tag ?? undefined
|
||||
}
|
||||
})
|
||||
|
||||
// Sync hubAppFilter → hub tag filter (when ListFilters badge is clicked)
|
||||
$effect(() => {
|
||||
const currentTag = hubFilterValue.tag as string | undefined
|
||||
if (hubAppFilter !== currentTag) {
|
||||
if (hubAppFilter) {
|
||||
hubFilterValue.tag = hubAppFilter
|
||||
} else {
|
||||
delete hubFilterValue.tag
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let flowViewer: Drawer | undefined = $state(undefined)
|
||||
let flowViewerFlow: { flow?: OpenFlow & { id?: number } } | undefined = $state(undefined)
|
||||
@@ -308,11 +371,11 @@
|
||||
{#if tab == 'hub'}
|
||||
<div class="flex flex-col gap-y-16">
|
||||
<div class="flex flex-col pb-8">
|
||||
{#snippet toggleKinds()}
|
||||
<div class="w-full flex items-center gap-2">
|
||||
<ToggleButtonGroup
|
||||
bind:selected={subtab}
|
||||
onSelected={(v) => {
|
||||
setQuery(page.url, 'kind', v, window.location.hash)
|
||||
hubFilterValue.kind = v
|
||||
}}
|
||||
noWFull
|
||||
>
|
||||
@@ -334,6 +397,12 @@
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
<FilterSearchbar
|
||||
schema={hubFilterSchema}
|
||||
bind:value={hubFilterValue}
|
||||
placeholder="Search hub..."
|
||||
class="grow"
|
||||
/>
|
||||
<Button
|
||||
startIcon={{ icon: ExternalLink }}
|
||||
target="_blank"
|
||||
@@ -342,26 +411,38 @@
|
||||
>
|
||||
Hub
|
||||
</Button>
|
||||
{/snippet}
|
||||
</div>
|
||||
|
||||
{#if subtab == 'script'}
|
||||
<PickHubScript syncQuery bind:filter on:pick={(e) => viewCode(e.detail)}>
|
||||
{#snippet children()}
|
||||
{@render toggleKinds?.()}
|
||||
{/snippet}
|
||||
</PickHubScript>
|
||||
<PickHubScript
|
||||
syncQuery
|
||||
filter={hubFreeText}
|
||||
bind:appFilter={hubAppFilter}
|
||||
summaryFilter={hubFilterValue.summary}
|
||||
pathFilter={hubFilterValue.path}
|
||||
hideSearchbar
|
||||
on:pick={(e) => viewCode(e.detail)}
|
||||
/>
|
||||
{:else if subtab == 'flow'}
|
||||
<PickHubFlow syncQuery bind:filter on:pick={(e) => viewFlow(e.detail)}>
|
||||
{#snippet children()}
|
||||
{@render toggleKinds?.()}
|
||||
{/snippet}
|
||||
</PickHubFlow>
|
||||
<PickHubFlow
|
||||
syncQuery
|
||||
filter={hubFreeText}
|
||||
bind:appFilter={hubAppFilter}
|
||||
summaryFilter={hubFilterValue.summary}
|
||||
pathFilter={hubFilterValue.path}
|
||||
hideSearchbar
|
||||
on:pick={(e) => viewFlow(e.detail)}
|
||||
/>
|
||||
{:else if subtab == 'app'}
|
||||
<PickHubApp syncQuery bind:filter on:pick={(e) => viewApp(e.detail)}>
|
||||
{#snippet children()}
|
||||
{@render toggleKinds?.()}
|
||||
{/snippet}
|
||||
</PickHubApp>
|
||||
<PickHubApp
|
||||
syncQuery
|
||||
filter={hubFreeText}
|
||||
bind:appFilter={hubAppFilter}
|
||||
summaryFilter={hubFilterValue.summary}
|
||||
pathFilter={hubFilterValue.path}
|
||||
hideSearchbar
|
||||
on:pick={(e) => viewApp(e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -369,7 +450,7 @@
|
||||
</div>
|
||||
|
||||
{#if tab == 'workspace'}
|
||||
<ItemsList bind:filter bind:subtab showEditButtons={showCreateButtons} />
|
||||
<ItemsList bind:subtab showEditButtons={showCreateButtons} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user