Compare commits
10 Commits
v1.682.0
...
home-filte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7581a95f94 | ||
|
|
d259a54e78 | ||
|
|
9ccb38eecd | ||
|
|
9fcbe0df01 | ||
|
|
4f6da9a6a5 | ||
|
|
182ee485d4 | ||
|
|
07770a6219 | ||
|
|
93ec1ada9c | ||
|
|
5abcc09617 | ||
|
|
f33e796025 |
@@ -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)
|
||||
@@ -48,66 +74,69 @@
|
||||
{#if $disableHubStore}
|
||||
<!-- Hub disabled, show nothing -->
|
||||
{:else}
|
||||
<SearchItems
|
||||
{filter}
|
||||
items={prefilteredItems}
|
||||
bind:filteredItems
|
||||
f={(x) => x.summary + ' (' + x.apps.join(', ') + ')'}
|
||||
/>
|
||||
<div class="w-full flex items-center gap-2">
|
||||
{@render children?.()}
|
||||
<TextInput
|
||||
inputProps={{
|
||||
placeholder: 'Search Hub Apps'
|
||||
}}
|
||||
bind:value={filter}
|
||||
class="grow !pr-9"
|
||||
{size}
|
||||
<SearchItems
|
||||
{filter}
|
||||
items={prefilteredItems}
|
||||
bind:filteredItems
|
||||
f={(x) => x.summary + ' (' + x.apps.join(', ') + ')'}
|
||||
/>
|
||||
</div>
|
||||
<ListFilters {syncQuery} filters={apps} bind:selectedFilter={appFilter} resourceType />
|
||||
{#if !hideSearchbar}
|
||||
<div class="w-full flex items-center gap-2">
|
||||
{@render children?.()}
|
||||
<TextInput
|
||||
inputProps={{
|
||||
placeholder: 'Search Hub Apps'
|
||||
}}
|
||||
bind:value={filter}
|
||||
class="grow !pr-9"
|
||||
{size}
|
||||
/>
|
||||
</div>
|
||||
<ListFilters {syncQuery} filters={apps} bind:selectedFilter={appFilter} resourceType />
|
||||
{/if}
|
||||
|
||||
{#if hubNotAvailable}
|
||||
<Alert type="warning" title="Hub not available">
|
||||
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}
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<ul class="divide-y border rounded-md bg-surface-tertiary">
|
||||
{#each filteredItems 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"
|
||||
onclick={() => dispatch('pick', item)}
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<RowIcon kind="app" />
|
||||
{#if hubNotAvailable}
|
||||
<Alert type="warning" title="Hub not available">
|
||||
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 displayItems.length == 0}
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<ul class="divide-y border rounded-md bg-surface-tertiary">
|
||||
{#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"
|
||||
onclick={() => dispatch('pick', item)}
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<RowIcon kind="app" />
|
||||
|
||||
<div class="w-full text-left">
|
||||
<div class="text-emphasis flex-wrap text-xs font-semibold">
|
||||
{#if item.marked}
|
||||
{@html item.marked ?? ''}
|
||||
{:else}
|
||||
{item.summary ?? ''}
|
||||
{/if}
|
||||
<div class="w-full text-left">
|
||||
<div class="text-emphasis flex-wrap text-xs font-semibold">
|
||||
{#if item.marked}
|
||||
{@html item.marked ?? ''}
|
||||
{:else}
|
||||
{item.summary ?? ''}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-1/3 gap-2 flex flex-wrap justify-end">
|
||||
{#each item.apps as app}
|
||||
<Badge color="gray" baseClass="border">{app}</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="min-w-1/3 gap-2 flex flex-wrap justify-end">
|
||||
{#each item.apps as app}
|
||||
<Badge color="gray" baseClass="border">{app}</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each Array(10).fill(0) as _}
|
||||
<Skeleton layout={[[4], 0.5]} />
|
||||
{/each}
|
||||
{/if}
|
||||
{:else}
|
||||
{#each Array(10).fill(0) as _}
|
||||
<Skeleton layout={[[4], 0.5]} />
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -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)
|
||||
@@ -48,68 +74,71 @@
|
||||
{#if $disableHubStore}
|
||||
<!-- Hub disabled, show nothing -->
|
||||
{:else}
|
||||
<SearchItems
|
||||
{filter}
|
||||
items={prefilteredItems}
|
||||
bind:filteredItems
|
||||
f={(x) => x.summary + ' (' + x.apps.join(', ') + ')'}
|
||||
/>
|
||||
<div class="w-full flex items-center gap-2">
|
||||
{@render children?.()}
|
||||
<TextInput
|
||||
inputProps={{
|
||||
placeholder: 'Search Hub Flows'
|
||||
}}
|
||||
bind:value={filter}
|
||||
{size}
|
||||
class="grow !pr-9"
|
||||
<SearchItems
|
||||
{filter}
|
||||
items={prefilteredItems}
|
||||
bind:filteredItems
|
||||
f={(x) => x.summary + ' (' + x.apps.join(', ') + ')'}
|
||||
/>
|
||||
</div>
|
||||
<ListFilters {syncQuery} filters={apps} bind:selectedFilter={appFilter} resourceType />
|
||||
{#if !hideSearchbar}
|
||||
<div class="w-full flex items-center gap-2">
|
||||
{@render children?.()}
|
||||
<TextInput
|
||||
inputProps={{
|
||||
placeholder: 'Search Hub Flows'
|
||||
}}
|
||||
bind:value={filter}
|
||||
{size}
|
||||
class="grow !pr-9"
|
||||
/>
|
||||
</div>
|
||||
<ListFilters {syncQuery} filters={apps} bind:selectedFilter={appFilter} resourceType />
|
||||
{/if}
|
||||
|
||||
{#if hubNotAvailable}
|
||||
<Alert type="warning" title="Hub not available">
|
||||
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}
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<ul class="divide-y border rounded-md bg-surface-tertiary overflow-hidden">
|
||||
{#each filteredItems 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"
|
||||
onclick={() => dispatch('pick', item)}
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<RowIcon kind="flow" />
|
||||
{#if hubNotAvailable}
|
||||
<Alert type="warning" title="Hub not available">
|
||||
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 displayItems.length == 0}
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<ul class="divide-y border rounded-md bg-surface-tertiary overflow-hidden">
|
||||
{#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"
|
||||
onclick={() => dispatch('pick', item)}
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<RowIcon kind="flow" />
|
||||
|
||||
<div class="w-full text-left">
|
||||
<div class="text-emphasis flex-wrap text-xs font-semibold">
|
||||
{#if item.marked}
|
||||
{@html item.marked ?? ''}
|
||||
{:else}
|
||||
{item.summary ?? ''}
|
||||
{/if}
|
||||
<div class="w-full text-left">
|
||||
<div class="text-emphasis flex-wrap text-xs font-semibold">
|
||||
{#if item.marked}
|
||||
{@html item.marked ?? ''}
|
||||
{:else}
|
||||
{item.summary ?? ''}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-1/3 gap-2 flex flex-wrap justify-end">
|
||||
{#each item.apps as app}
|
||||
<Badge color="gray" baseClass="border">{app}</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="my-2"></div>
|
||||
<div class="min-w-1/3 gap-2 flex flex-wrap justify-end">
|
||||
{#each item.apps as app}
|
||||
<Badge color="gray" baseClass="border">{app}</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="my-2"></div>
|
||||
|
||||
{#each Array(10).fill(0) as _}
|
||||
<Skeleton layout={[[4], 0.5]} />
|
||||
{/each}
|
||||
{/if}
|
||||
{#each Array(10).fill(0) as _}
|
||||
<Skeleton layout={[[4], 0.5]} />
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -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
|
||||
@@ -67,7 +74,9 @@
|
||||
async function applyFilter(
|
||||
filter: string,
|
||||
filterKind: typeof kind,
|
||||
appFilter: string | undefined
|
||||
appFilter: string | undefined,
|
||||
summaryFilter: string | undefined,
|
||||
pathFilter: string | undefined
|
||||
) {
|
||||
if ($disableHubStore) return
|
||||
try {
|
||||
@@ -77,10 +86,11 @@
|
||||
startTs = ts
|
||||
await new Promise((r) => setTimeout(r, 100))
|
||||
if (ts < startTs) return
|
||||
const queryText = [filter, summaryFilter, pathFilter].filter(Boolean).join(' ')
|
||||
const scripts =
|
||||
filter.length > 0
|
||||
queryText.length > 0
|
||||
? await ScriptService.queryHubScripts({
|
||||
text: `${filter}`,
|
||||
text: queryText,
|
||||
limit: 20,
|
||||
kind: filterKind,
|
||||
app: appFilter
|
||||
@@ -132,8 +142,8 @@
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
;[filter, kind, appFilter]
|
||||
untrack(() => applyFilter(filter, kind, appFilter))
|
||||
;[filter, kind, appFilter, summaryFilter, pathFilter]
|
||||
untrack(() => applyFilter(filter, kind, appFilter, summaryFilter, pathFilter))
|
||||
})
|
||||
$effect(() => {
|
||||
kind
|
||||
@@ -144,72 +154,77 @@
|
||||
{#if $disableHubStore}
|
||||
<!-- Hub disabled, show nothing -->
|
||||
{:else}
|
||||
<div class="w-full flex items-center gap-2">
|
||||
{@render children?.()}
|
||||
<div class="relative w-full">
|
||||
<TextInput
|
||||
inputProps={{
|
||||
placeholder: 'Search Hub Scripts'
|
||||
}}
|
||||
bind:value={filter}
|
||||
class="grow !pr-9"
|
||||
{size}
|
||||
/>
|
||||
{#if loading}
|
||||
<Loader2 class="animate-spin text-gray-400 absolute right-2 top-1" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if hubNotAvailable}
|
||||
<Alert type="warning" title="Hub not available">
|
||||
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 (items.length > 0 && apps.length > 0) || !loading}
|
||||
<ListFilters {syncQuery} filters={apps} bind:selectedFilter={appFilter} resourceType />
|
||||
{#if items.length == 0}
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<ul class="divide-y border rounded-md bg-surface-tertiary">
|
||||
{#each items 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"
|
||||
onclick={() => handlePick(item)}
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex justify-center items-center">
|
||||
{#if item['app'] in APP_TO_ICON_COMPONENT}
|
||||
{@const SvelteComponent = APP_TO_ICON_COMPONENT[item['app']]}
|
||||
<SvelteComponent height={18} width={18} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="w-full text-left">
|
||||
<div class="text-emphasis flex-wrap text-xs font-semibold mb-1">
|
||||
{item.summary ?? ''}
|
||||
</div>
|
||||
<div class="text-secondary text-2xs font-normal">
|
||||
{item.path}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if kind !== 'script'}
|
||||
<Badge color="gray" baseClass="border">{capitalize(kind)}</Badge>
|
||||
{/if}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if items.length == 20}
|
||||
<div class="text-primary text-xs font-normal py-4">
|
||||
There are more items than being displayed. Refine your search.
|
||||
{#if !hideSearchbar}
|
||||
<div class="w-full flex items-center gap-2">
|
||||
{@render children?.()}
|
||||
<div class="relative w-full">
|
||||
<TextInput
|
||||
inputProps={{
|
||||
placeholder: 'Search Hub Scripts'
|
||||
}}
|
||||
bind:value={filter}
|
||||
class="grow !pr-9"
|
||||
{size}
|
||||
/>
|
||||
{#if loading}
|
||||
<Loader2 class="animate-spin text-gray-400 absolute right-2 top-1" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each Array(10).fill(0) as _}
|
||||
<Skeleton layout={[0.5, [4]]} />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if hubNotAvailable}
|
||||
<Alert type="warning" title="Hub not available">
|
||||
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 (items.length > 0 && apps.length > 0) || !loading}
|
||||
{#if !hideSearchbar}
|
||||
<ListFilters {syncQuery} filters={apps} bind:selectedFilter={appFilter} resourceType />
|
||||
{/if}
|
||||
{#if items.length == 0}
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<ul class="divide-y border rounded-md bg-surface-tertiary">
|
||||
{#each items 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"
|
||||
onclick={() => handlePick(item)}
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex justify-center items-center">
|
||||
{#if item['app'] in APP_TO_ICON_COMPONENT}
|
||||
{@const SvelteComponent = APP_TO_ICON_COMPONENT[item['app']]}
|
||||
<SvelteComponent height={18} width={18} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="w-full text-left">
|
||||
<div class="text-emphasis flex-wrap text-xs font-semibold mb-1">
|
||||
{item.summary ?? ''}
|
||||
</div>
|
||||
<div class="text-secondary text-2xs font-normal">
|
||||
{item.path}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if kind !== 'script'}
|
||||
<Badge color="gray" baseClass="border">{capitalize(kind)}</Badge>
|
||||
{/if}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if items.length == 20}
|
||||
<div class="text-primary text-xs font-normal py-4">
|
||||
There are more items than being displayed. Refine your search.
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each Array(10).fill(0) as _}
|
||||
<Skeleton layout={[0.5, [4]]} />
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { AppService, FlowService, type OpenFlow, type Script } from '$lib/gen'
|
||||
import { AppService, FlowService, IntegrationService, type OpenFlow, type Script } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { Alert, Button, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
@@ -20,18 +20,21 @@
|
||||
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 +55,56 @@
|
||||
|
||||
let subtab: 'flow' | 'script' | 'app' = $state('script')
|
||||
|
||||
let filter: string = $state('')
|
||||
// Hub filter: load integration names for tag suggestions
|
||||
let hubIntegrations: string[] = $state([])
|
||||
|
||||
// Hub filter schema (tag options update when integrations load)
|
||||
let hubFilterSchema: FilterSchemaRec = $derived({
|
||||
_default_: { type: 'string', hidden: true },
|
||||
tag: {
|
||||
type: 'oneof',
|
||||
label: 'Tag',
|
||||
description: 'Filter by integration/app tag',
|
||||
icon: Tag,
|
||||
allowCustomValue: true,
|
||||
options: hubIntegrations.map((name) => ({ value: name, label: name }))
|
||||
},
|
||||
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 (for PickHub* API calls)
|
||||
$effect(() => {
|
||||
const tag = hubFilterValue.tag as string | undefined
|
||||
hubAppFilter = tag ?? undefined
|
||||
})
|
||||
|
||||
let flowViewer: Drawer | undefined = $state(undefined)
|
||||
let flowViewerFlow: { flow?: OpenFlow & { id?: number } } | undefined = $state(undefined)
|
||||
@@ -109,7 +161,16 @@
|
||||
|
||||
let showCreateButtons = $state(false)
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
// Load hub integrations for tag suggestions
|
||||
try {
|
||||
hubIntegrations = (await IntegrationService.listHubIntegrations({ kind: 'script' })).map(
|
||||
(x) => x.name
|
||||
)
|
||||
} catch {
|
||||
hubIntegrations = []
|
||||
}
|
||||
|
||||
// Check if there's a tutorial parameter in the URL
|
||||
const tutorialParam = page.url.searchParams.get('tutorial')
|
||||
if (tutorialParam === 'workspace-onboarding') {
|
||||
@@ -117,7 +178,8 @@
|
||||
setTimeout(() => {
|
||||
workspaceTutorials?.runTutorialById('workspace-onboarding')
|
||||
}, 500)
|
||||
} else if (tutorialParam === 'workspace-onboarding-operator') { // Small delay to ensure page is fully loaded
|
||||
} else if (tutorialParam === 'workspace-onboarding-operator') {
|
||||
// Small delay to ensure page is fully loaded
|
||||
setTimeout(() => {
|
||||
workspaceTutorials?.runTutorialById('workspace-onboarding-operator')
|
||||
}, 500)
|
||||
@@ -293,7 +355,7 @@
|
||||
|
||||
<TutorialBanner />
|
||||
|
||||
<NoDirectDeployAlert onUpdateCanEditStatus={(v) => showCreateButtons = v}/>
|
||||
<NoDirectDeployAlert onUpdateCanEditStatus={(v) => (showCreateButtons = v)} />
|
||||
|
||||
{#if !$userStore?.operator}
|
||||
<div class="w-full overflow-auto scrollbar-hidden pb-2">
|
||||
@@ -308,11 +370,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 +396,25 @@
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
<FilterSearchbar
|
||||
schema={hubFilterSchema}
|
||||
bind:value={hubFilterValue}
|
||||
placeholder="Search hub..."
|
||||
class="grow"
|
||||
presets={[
|
||||
{ name: 'Slack', value: 'tag:slack' },
|
||||
{ name: 'Gmail', value: 'tag:gmail' },
|
||||
{ name: 'GitHub', value: 'tag:github' },
|
||||
{ name: 'Notion', value: 'tag:notion' },
|
||||
{ name: 'Stripe', value: 'tag:stripe' },
|
||||
{ name: 'OpenAI', value: 'tag:openai' },
|
||||
{ name: 'HubSpot', value: 'tag:hubspot' },
|
||||
{ name: 'PostgreSQL', value: 'tag:postgresql' },
|
||||
{ name: 'Google Sheets', value: 'tag:gsheets' },
|
||||
{ name: 'Shopify', value: 'tag:shopify' },
|
||||
{ name: 'Other...', value: 'tag:' }
|
||||
]}
|
||||
/>
|
||||
<Button
|
||||
startIcon={{ icon: ExternalLink }}
|
||||
target="_blank"
|
||||
@@ -342,26 +423,35 @@
|
||||
>
|
||||
Hub
|
||||
</Button>
|
||||
{/snippet}
|
||||
</div>
|
||||
|
||||
{#if subtab == 'script'}
|
||||
<PickHubScript syncQuery bind:filter on:pick={(e) => viewCode(e.detail)}>
|
||||
{#snippet children()}
|
||||
{@render toggleKinds?.()}
|
||||
{/snippet}
|
||||
</PickHubScript>
|
||||
<PickHubScript
|
||||
filter={hubFreeText}
|
||||
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
|
||||
filter={hubFreeText}
|
||||
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
|
||||
filter={hubFreeText}
|
||||
appFilter={hubAppFilter}
|
||||
summaryFilter={hubFilterValue.summary}
|
||||
pathFilter={hubFilterValue.path}
|
||||
hideSearchbar
|
||||
on:pick={(e) => viewApp(e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -369,7 +459,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