From 2a59ca2819b52478ec3fc46d7647bbbc4f93a389 Mon Sep 17 00:00:00 2001 From: Alexander Petric Date: Fri, 19 Dec 2025 08:03:15 -0500 Subject: [PATCH] fix: improve teams search ux (#7407) * feat: improve teams search ux * ee ref * claude review * chore: update ee-repo-ref to e218dfce97dcea56c6ef6032592dab812a3f5047 This commit updates the EE repository reference after PR #363 was merged in windmill-ee-private. Previous ee-repo-ref: 1b95a24ab25d96e59d2f22588901e9d3ce6c72b3 New ee-repo-ref: e218dfce97dcea56c6ef6032592dab812a3f5047 Automated by sync-ee-ref workflow. --------- Co-authored-by: windmill-internal-app[bot] --- backend/ee-repo-ref.txt | 2 +- backend/windmill-api/openapi.yaml | 64 ++-- .../src/lib/components/ChannelSelector.svelte | 136 +++++---- .../src/lib/components/InstanceSetting.svelte | 4 +- .../src/lib/components/TeamSelector.svelte | 279 +++++++++++++----- 5 files changed, 334 insertions(+), 151 deletions(-) diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index b6855b0df2..652cb55626 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -505eadbff32d102ea5245a2bef88ce6f1bb95395 +e218dfce97dcea56c6ef6032592dab812a3f5047 diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index f277a2cd6a..4bb7cacffc 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -2558,7 +2558,13 @@ paths: - $ref: "#/components/parameters/WorkspaceId" - name: search in: query - description: Search teams by name + description: Search teams by name. If omitted, returns first page of all teams. + required: false + schema: + type: string + - name: next_link + in: query + description: Pagination cursor URL from previous response. Pass this to fetch the next page of results. required: false schema: type: string @@ -2568,14 +2574,27 @@ paths: content: application/json: schema: - type: array - items: - type: object - properties: - team_name: - type: string - team_id: - type: string + type: object + properties: + teams: + type: array + items: + type: object + properties: + team_name: + type: string + team_id: + type: string + total_count: + type: integer + description: Total number of teams across all pages + per_page: + type: integer + description: Number of teams per page (configurable via TEAMS_PER_PAGE env var) + next_link: + type: string + nullable: true + description: URL to fetch next page of results. Null if no more pages. /w/{workspace}/workspaces/available_teams_channels: get: @@ -2591,26 +2610,25 @@ paths: required: true schema: type: string - - name: search - in: query - description: Search channels by name - required: false - schema: - type: string responses: "200": description: List of channels for the specified team content: application/json: schema: - type: array - items: - type: object - properties: - channel_name: - type: string - channel_id: - type: string + type: object + properties: + channels: + type: array + items: + type: object + properties: + channel_name: + type: string + channel_id: + type: string + total_count: + type: integer /w/{workspace}/workspaces/connect_teams: post: diff --git a/frontend/src/lib/components/ChannelSelector.svelte b/frontend/src/lib/components/ChannelSelector.svelte index 7bca9d26d0..dfcba309a7 100644 --- a/frontend/src/lib/components/ChannelSelector.svelte +++ b/frontend/src/lib/components/ChannelSelector.svelte @@ -2,7 +2,7 @@ import Select from './select/Select.svelte' import { WorkspaceService } from '$lib/gen' import { workspaceStore } from '$lib/stores' - import { debounce } from '$lib/utils' + import { RefreshCcw } from 'lucide-svelte' interface ChannelItem { channel_id?: string @@ -17,6 +17,7 @@ minWidth?: string channels?: ChannelItem[] teamId?: string + showRefreshButton?: boolean onError?: (error: Error) => void onSelectedChannelChange?: (channel: ChannelItem | undefined) => void } @@ -29,20 +30,22 @@ minWidth = '160px', channels = undefined, teamId, + showRefreshButton = true, onError, onSelectedChannelChange }: Props = $props() let isFetching = $state(false) - let searchResults = $state([]) + let loadedChannels = $state([]) + let loadedForTeamId = $state(undefined) let selectedChannelId = $state(selectedChannel?.channel_id) - const searchMode = !channels && !!teamId + const searchMode = $derived(!channels && !!teamId) let displayChannels = $derived.by(() => { - const baseChannels = channels || searchResults - if (selectedChannel && !baseChannels.find(c => c.channel_id === selectedChannel?.channel_id)) { + const baseChannels = channels || loadedChannels + if (selectedChannel && !baseChannels.find((c) => c.channel_id === selectedChannel?.channel_id)) { return [selectedChannel, ...baseChannels] } return baseChannels @@ -50,7 +53,7 @@ $effect(() => { const newChannel = selectedChannelId - ? displayChannels.find(c => c.channel_id === selectedChannelId) + ? displayChannels.find((c) => c.channel_id === selectedChannelId) : undefined if (newChannel?.channel_id !== selectedChannel?.channel_id) { @@ -73,77 +76,96 @@ } }) - let searchFilterText = $state('') - - const debouncedSearch = debounce(async (query: string) => { - await searchChannels(query) - }, 500) - + // Fetch channels when teamId is set or changes $effect(() => { - if (searchMode) { - if (searchFilterText.length >= 1) { - debouncedSearch.debounced(searchFilterText) - } else if (searchFilterText.length === 0) { - searchResults = [] - } + if (searchMode && teamId && teamId !== loadedForTeamId) { + loadedForTeamId = teamId + fetchChannels() } }) - async function searchChannels(query: string) { - if (!query || !teamId) return + async function fetchChannels() { + if (!teamId) return isFetching = true try { const response = await WorkspaceService.listAvailableTeamsChannels({ workspace: $workspaceStore!, - teamId: teamId, - search: query + teamId: teamId }) - searchResults = response || [] - isFetching = false - return searchResults + loadedChannels = + response.channels?.map((c) => ({ + channel_id: c.channel_id || '', + channel_name: c.channel_name || '' + })) || [] } catch (error) { + onError?.(error as Error) + console.error('Error fetching channels:', error) + loadedChannels = [] + } finally { isFetching = false - onError?.(error) - console.error('Error searching channels:', error) - searchResults = [] - return [] } } + async function refreshChannels() { + if (searchMode) { + await fetchChannels() + } + }
-
-
- {#if searchMode} - channel.channel_id && channel.channel_name).map((channel) => ({ - label: channel.channel_name ?? 'Unknown Channel', - value: channel.channel_id ?? '' - }))} - {placeholder} - clearable - disabled={disabled || displayChannels.length === 0} - bind:value={selectedChannelId} - /> +
+
+
+ {#if searchMode} + channel.channel_id && channel.channel_name) + .map((channel) => ({ + label: channel.channel_name ?? 'Unknown Channel', + value: channel.channel_id ?? '' + }))} + {placeholder} + clearable + disabled={disabled || displayChannels.length === 0} + bind:value={selectedChannelId} + /> + {/if} +
+ + {#if showRefreshButton && searchMode} + {/if}
-
+ {#if searchMode && loadedChannels.length > 0 && !isFetching} + + {loadedChannels.length} channel{loadedChannels.length === 1 ? '' : 's'} + + {/if} +
diff --git a/frontend/src/lib/components/InstanceSetting.svelte b/frontend/src/lib/components/InstanceSetting.svelte index 10ef968a43..0914654fa1 100644 --- a/frontend/src/lib/components/InstanceSetting.svelte +++ b/frontend/src/lib/components/InstanceSetting.svelte @@ -523,7 +523,7 @@
{#if $enterpriseLicense && Array.isArray($values[setting.key])} {#each $values[setting.key] ?? [] as v, i} -
+
({ - label: team.team_name, - value: team.team_id - }))} - placeholder={isFetching ? "Searching..." : "Search teams..."} - clearable - disabled={disabled || isFetching} - bind:filterText={searchFilterText} - bind:value={selectedTeamId} - /> - {:else} - ({ + label: team.team_name, + value: team.team_id + }))} + placeholder={isFetching ? 'Loading...' : 'Search teams...'} + clearable + disabled={disabled || isFetching} + bind:filterText={searchFilterText} + bind:value={selectedTeamId} + /> + {:else} +