diff --git a/frontend/src/lib/components/triggers/native/NativeConnectionPicker.svelte b/frontend/src/lib/components/triggers/native/NativeConnectionPicker.svelte index 705e434869..356e27e074 100644 --- a/frontend/src/lib/components/triggers/native/NativeConnectionPicker.svelte +++ b/frontend/src/lib/components/triggers/native/NativeConnectionPicker.svelte @@ -4,8 +4,11 @@ import { Button } from '$lib/components/common' import Alert from '$lib/components/common/alert/Alert.svelte' import Select from '$lib/components/select/Select.svelte' + import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte' + import { createAsyncConfirmationModal } from '$lib/components/common/confirmationModal/asyncConfirmationModal.svelte' import { sendUserToast } from '$lib/toast' - import { Plug } from 'lucide-svelte' + import { Plus, Unplug } from 'lucide-svelte' + import { userStore } from '$lib/stores' import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' import { connectNativeAccount, getServiceConfig } from './utils' @@ -15,12 +18,34 @@ /** An existing trigger keeps the connection its webhook was registered under. */ locked?: boolean disabled?: boolean + /** Only offer the caller's own connections. */ + ownOnly?: boolean + /** Select the first connection when none is chosen yet. */ + autoSelect?: boolean + clearable?: boolean + placeholder?: string + /** Called instead of setting `value` directly when the user picks or clears a connection. */ + onSelect?: (path: string | undefined) => void + /** Called after an account is connected or disconnected from the picker. */ + onConnectionsChange?: () => void } - let { service, value = $bindable(), locked = false, disabled = false }: Props = $props() + let { + service, + value = $bindable(), + locked = false, + disabled = false, + ownOnly = false, + autoSelect = true, + clearable = false, + placeholder, + onSelect, + onConnectionsChange + }: Props = $props() const operatingWorkspace = useOperatingWorkspace() const displayName = $derived(getServiceConfig(service)?.serviceDisplayName ?? service) + const confirmationModal = createAsyncConfirmationModal() let connections = $state(undefined) let connecting = $state(false) @@ -35,8 +60,8 @@ connections = [] sendUserToast(`Failed to load ${displayName} connections: ${err.body ?? err.message}`, true) } - if (!locked && value === undefined && connections.length > 0) { - value = connections[0].path + if (autoSelect && !locked && value === undefined && offered.length > 0) { + select(offered[0].path) } } @@ -46,13 +71,19 @@ load() }) + function select(path: string | undefined) { + if (onSelect) onSelect(path) + else value = path + } + async function connect() { connecting = true try { const path = await connectNativeAccount($operatingWorkspace!, service) await load() - value = path sendUserToast(`${displayName} account connected`) + onConnectionsChange?.() + select(path) } catch (err: any) { sendUserToast(`Could not connect ${displayName}: ${err.body ?? err.message}`, true) } finally { @@ -60,9 +91,40 @@ } } + async function disconnect(path: string) { + const confirmed = await confirmationModal.ask({ + title: `Disconnect ${path}?`, + confirmationText: 'Disconnect', + children: `This deletes the ${displayName} triggers created with this account. This action cannot be undone.` + }) + if (!confirmed) return + try { + await WorkspaceIntegrationService.deleteNativeTriggerConnection({ + workspace: $operatingWorkspace!, + serviceName: service, + path + }) + sendUserToast(`${path} disconnected`) + if (value === path) select(undefined) + await load() + onConnectionsChange?.() + } catch (err: any) { + sendUserToast(`Failed to disconnect ${path}: ${err.body ?? err.message}`, true) + } + } + + // Members cannot write the workspace connection; the server refuses anyone else it does not own. + function canDisconnect(c: NativeTriggerConnection): boolean { + if ($userStore?.is_admin) return true + return !c.workspace && !!c.owner && c.owner === $userStore?.username + } + + const offered = $derived( + (connections ?? []).filter((c) => !ownOnly || c.owner === $userStore?.username) + ) const items = $derived.by(() => { - const list = (connections ?? []).map((c) => ({ - label: c.workspace ? `${c.path} (workspace connection)` : c.path, + const list = offered.map((c) => ({ + label: c.workspace && !ownOnly ? `${c.path} (workspace connection)` : c.path, value: c.path })) if (value && !list.some((i) => i.value === value)) { @@ -71,34 +133,50 @@ return list }) const unusable = $derived( - connections !== undefined && !!value && !connections.some((c) => c.path === value) + !ownOnly && connections !== undefined && !!value && !connections.some((c) => c.path === value) )
-
- value, (v) => select(v)} + onClear={() => select(undefined)} + {clearable} + disabled={disabled || locked} + loading={connections === undefined || connecting} + placeholder={placeholder ?? `Connect a ${displayName} account to create this trigger`} + noItemsMsg="No {displayName} account connected yet" + > + {#snippet endSnippet({ item, close })} + {@const connection = connections?.find((c) => c.path === item.value)} + {#if connection && canDisconnect(connection)} + +
+ {/snippet} + {#if locked}

The webhook is registered under this account. To use another one, create a new trigger. @@ -111,3 +189,5 @@ {/if}

+ + diff --git a/frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte b/frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte index d14c5a4c2d..c593876245 100644 --- a/frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte +++ b/frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte @@ -1,10 +1,10 @@