Files
windmill/frontend/src/lib/components/ChannelSelector.svelte
T
6186a0645d feat: edit variables, resources and triggers in their own session tab (#11206)
* feat: edit variables, resources and triggers in their own session tab

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: take a trigger's flow kind from its config and name the written trigger

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: one operating-workspace context for editors acting on a session's workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: reload page item tabs only in sessions acting on the tool's workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: resolve open in workspace for every preview tab kind in one place

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: guard every component under a session editor against the navigation workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: show a page item tab whose item is gone instead of mounting its editor

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: follow a saved page item in the tab's own workspace and restore in-frame rows

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: close a page item tab from its editor header

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: judge trigger permissions by the user acting in the session workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: judge every session-editor permission by the user acting in that workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep session permissions reactive to the acting role and restore tabs as last seen

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: default Path's owner controls to the user acting in its workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-09-19 23:29:14 +02:00

180 lines
4.9 KiB
Svelte

<script lang="ts">
import Select from './select/Select.svelte'
import { WorkspaceService } from '$lib/gen'
import { RefreshCcw } from 'lucide-svelte'
import { Button } from './common'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface ChannelItem {
channel_id?: string
channel_name?: string
}
interface Props {
disabled?: boolean
placeholder?: string
selectedChannel?: ChannelItem | undefined
containerClass?: string
minWidth?: string
channels?: ChannelItem[]
teamId?: string
showRefreshButton?: boolean
onError?: (error: Error) => void
onSelectedChannelChange?: (channel: ChannelItem | undefined) => void
/** Workspace to list Teams channels from; defaults to the operating workspace (see
* `useOperatingWorkspace`). */
workspace?: string
}
let {
disabled = false,
placeholder = 'Select channel',
selectedChannel = $bindable(),
containerClass = 'w-64',
minWidth = '160px',
channels = undefined,
teamId,
showRefreshButton = true,
onError,
onSelectedChannelChange,
workspace = undefined
}: Props = $props()
let effectiveWorkspace = $derived(workspace ?? $operatingWorkspace)
let isFetching = $state(false)
let loadedChannels = $state<ChannelItem[]>([])
let loadedForTeamId = $state<string | undefined>(undefined)
const searchMode = $derived(!channels && !!teamId)
let displayChannels = $derived.by(() => {
const baseChannels = channels || loadedChannels
if (
selectedChannel &&
!baseChannels.find((c) => c.channel_id === selectedChannel?.channel_id)
) {
return [selectedChannel, ...baseChannels]
}
return baseChannels
})
// Single setter to bridge Select's string value -> selectedChannel object.
function setSelectedChannelById(newId: string | undefined) {
if (newId) {
const channel = displayChannels.find((c) => c.channel_id === newId)
if (channel && channel.channel_id !== selectedChannel?.channel_id) {
selectedChannel = channel
}
} else if (selectedChannel !== undefined) {
selectedChannel = undefined
}
}
let previousChannelId = $state<string | undefined>(undefined)
$effect(() => {
if (selectedChannel?.channel_id !== previousChannelId) {
previousChannelId = selectedChannel?.channel_id
onSelectedChannelChange?.(selectedChannel)
}
})
// Fetch channels when teamId is set or changes
$effect(() => {
if (searchMode && teamId && teamId !== loadedForTeamId) {
loadedForTeamId = teamId
fetchChannels()
}
})
async function fetchChannels() {
if (!teamId) return
isFetching = true
try {
const response = await WorkspaceService.listAvailableTeamsChannels({
workspace: effectiveWorkspace!,
teamId: teamId
})
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
}
}
async function refreshChannels() {
if (searchMode) {
await fetchChannels()
}
}
</script>
<div class={containerClass}>
<div class="flex flex-col gap-1">
<div class="flex items-center gap-1">
<div class="flex-grow" style="min-width: {minWidth};">
{#if searchMode}
<Select
containerStyle={'min-width: ' + minWidth}
items={displayChannels
.filter((channel) => channel.channel_id && channel.channel_name)
.map((channel) => ({
label: channel.channel_name ?? 'Unknown Channel',
value: channel.channel_id ?? ''
}))}
placeholder={isFetching ? 'Loading...' : teamId ? placeholder : 'Select a team first'}
clearable
disabled={disabled || !teamId}
loading={isFetching}
bind:value={() => selectedChannel?.channel_id, (newId) => setSelectedChannelById(newId)}
/>
{:else}
<Select
containerStyle={'min-width: ' + minWidth}
items={displayChannels
.filter((channel) => 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={() => selectedChannel?.channel_id, (newId) => setSelectedChannelById(newId)}
/>
{/if}
</div>
{#if showRefreshButton && searchMode}
<Button
onclick={refreshChannels}
disabled={isFetching || disabled || !teamId}
title="Refresh channels"
startIcon={{ icon: RefreshCcw, props: { class: isFetching ? 'animate-spin' : '' } }}
unifiedSize="sm"
variant="subtle"
iconOnly
/>
{/if}
</div>
{#if searchMode && loadedChannels.length > 0 && !isFetching}
<span class="text-2xs text-tertiary pl-1">
{loadedChannels.length} channel{loadedChannels.length === 1 ? '' : 's'}
</span>
{/if}
</div>
</div>