mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-23 16:03:33 +00:00
* feat: configurable workspace roles with per-workspace overrides Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: close role review gaps in pickers, declarative sync and removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: keep resolve_role_name doc on its function Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: validate overrides on instance role changes and bill admins fully Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: allow pipeline drafts and keep role checks on one connection Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: list data pipelines in operator_can_draft Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: expect a 403 role refusal when an operator resolves jobs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: require a valid license for custom workspace roles Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: freeze workspace roles when the license lapses Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: pin the ee companion for workspace roles Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: broadcast role override changes from the write path, not a trigger Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: update ee-repo-ref to b5294d38db082bdd104d5358a47695390694c05d This commit updates the EE repository reference after PR #891 was merged in windmill-ee-private. Previous ee-repo-ref: 688e00c5982865b14098f530d54280e2311994c0 New ee-repo-ref: b5294d38db082bdd104d5358a47695390694c05d Automated by sync-ee-ref workflow. * feat: add workspace roles settings UI and role pickers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: show the server's reason when a settings save fails Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: default role pickers to a defined role and confirm resets of unknown capabilities Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: treat null role capabilities as unset and key role drafts by map Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: gate the frontend on workspace role capabilities Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: gate deploys, bulk actions and chat composition per role kind Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: treat a superadmin as full in deploy permission checks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: leave page visibility out of isRestricted, as the server does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: let a member discard their own draft without the authoring right Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: show custom roles as inactive without a valid license Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep the user store import on the agent page Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: show custom roles as frozen without a valid license Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: reorder instance roles with a drag handle Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: let an admin row that kept the operator flag hold full rights everywhere Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: treat superadmin members as admins and return role names from get user Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: move the instance role label into the expanded panel Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
593 lines
18 KiB
Svelte
593 lines
18 KiB
Svelte
<script module lang="ts">
|
|
let cachedOwners: Record<string, string[]> = {}
|
|
let cachedIntegrations: string[] = []
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import { sendUserToast } from '$lib/toast'
|
|
import FlowScriptPickerQuick from '../pickers/FlowScriptPickerQuick.svelte'
|
|
import { defaultScriptLanguages, processInlineLangs } from '$lib/scripts'
|
|
import { defaultScripts, enterpriseLicense, hubBaseUrlStore } from '$lib/stores'
|
|
import { useRoleCapabilities } from '$lib/operatorWriteRights'
|
|
import type { SupportedLanguage } from '$lib/common'
|
|
import { createEventDispatcher, getContext, untrack } from 'svelte'
|
|
import type { FlowBuilderWhitelabelCustomUi } from '$lib/components/custom_ui'
|
|
import { type Script, type ScriptLang, type HubScriptKind } from '$lib/gen'
|
|
import ListFiltersQuick from '$lib/components/home/ListFiltersQuick.svelte'
|
|
import { ExternalLink, Folder, User, X } from 'lucide-svelte'
|
|
import { fade } from 'svelte/transition'
|
|
import { flip } from 'svelte/animate'
|
|
import { Button } from '$lib/components/common'
|
|
import { SettingsIcon } from 'lucide-svelte'
|
|
import DefaultScriptsInner from '$lib/components/DefaultScriptsInner.svelte'
|
|
import GenAiQuick from './GenAiQuick.svelte'
|
|
import FlowToplevelNode from '../pickers/FlowToplevelNode.svelte'
|
|
import { copilotInfo } from '$lib/aiStore'
|
|
import {
|
|
canHavePreprocessor,
|
|
canHaveTrigger,
|
|
canHaveApproval,
|
|
canHaveFailure
|
|
} from '$lib/script_helpers'
|
|
import {
|
|
useOperatingUser,
|
|
useOperatingWorkspace
|
|
} from '$lib/components/operatingWorkspace.svelte'
|
|
|
|
const operatingWorkspace = useOperatingWorkspace()
|
|
const roleCaps = useRoleCapabilities()
|
|
const composeOnlyFlows = $derived($roleCaps.flows === 'compose')
|
|
const operatingUser = useOperatingUser()
|
|
const actingUser = $derived(operatingUser.current)
|
|
|
|
const dispatch = createEventDispatcher()
|
|
|
|
interface Props {
|
|
summary?: string | undefined
|
|
filter?: string
|
|
disableAi?: boolean
|
|
preFilter?: 'all' | 'workspace' | 'hub'
|
|
funcDesc: string
|
|
owners?: string[]
|
|
loading?: boolean
|
|
kind: 'trigger' | 'script' | 'preprocessor' | 'failure' | 'approval'
|
|
selectedKind?: 'script' | 'flow' | 'approval' | 'trigger' | 'preprocessor' | 'failure'
|
|
displayPath?: boolean
|
|
refreshCount?: number
|
|
}
|
|
|
|
let {
|
|
summary = undefined,
|
|
filter = $bindable(''),
|
|
disableAi = false,
|
|
preFilter = 'hub',
|
|
funcDesc,
|
|
owners = $bindable([]),
|
|
loading = $bindable(false),
|
|
kind,
|
|
selectedKind = kind,
|
|
displayPath = false,
|
|
refreshCount = 0
|
|
}: Props = $props()
|
|
|
|
if ($operatingWorkspace && cachedOwners?.[$operatingWorkspace]) {
|
|
owners = cachedOwners[$operatingWorkspace]
|
|
}
|
|
type HubCompletion = {
|
|
path: string
|
|
summary: string
|
|
/** The hub's own wording, before `summary` is rewritten as the display label. */
|
|
hubSummary: string
|
|
id: number
|
|
version_id: number
|
|
ask_id: number
|
|
app: string
|
|
kind: HubScriptKind
|
|
}
|
|
|
|
let lang: ScriptLang | undefined = $state(undefined)
|
|
|
|
let filteredWorkspaceItems: (Script & { marked?: string })[] = $state([])
|
|
|
|
let hubCompletions: HubCompletion[] = $state([])
|
|
|
|
let selected: { kind: 'owner' | 'integrations'; name: string | undefined } | undefined =
|
|
$state(undefined)
|
|
|
|
let integrations: string[] = $state(cachedIntegrations)
|
|
|
|
let customUi: undefined | FlowBuilderWhitelabelCustomUi = getContext('customUi')
|
|
|
|
function displayLang(
|
|
lang: SupportedLanguage | 'docker',
|
|
kind: 'script' | 'flow' | 'approval' | 'trigger' | 'preprocessor' | 'failure'
|
|
) {
|
|
if (kind == 'trigger') {
|
|
return canHaveTrigger(lang as SupportedLanguage)
|
|
} else if (kind == 'script') {
|
|
return true
|
|
} else if (kind == 'approval') {
|
|
return canHaveApproval(lang as SupportedLanguage)
|
|
} else if (kind == 'flow') {
|
|
return false
|
|
} else if (kind == 'preprocessor') {
|
|
return canHavePreprocessor(lang as SupportedLanguage)
|
|
} else if (kind == 'failure') {
|
|
return canHaveFailure(lang as SupportedLanguage)
|
|
}
|
|
}
|
|
|
|
async function onGenerate() {
|
|
if (!$copilotInfo.enabled) {
|
|
sendUserToast(
|
|
'Windmill AI is not enabled, you can activate it in the workspace settings',
|
|
true
|
|
)
|
|
return
|
|
}
|
|
console.log('ongenerate', selectedKind, lang, funcDesc)
|
|
dispatch('new', {
|
|
kind: selectedKind,
|
|
inlineScript: {
|
|
language: lang,
|
|
kind: selectedKind,
|
|
subkind: 'flow',
|
|
summary,
|
|
instructions: funcDesc
|
|
}
|
|
})
|
|
}
|
|
|
|
let openScriptSettings = $state(false)
|
|
|
|
let selectedByKeyboard = $state(0)
|
|
|
|
function onSelectedKindChange(
|
|
_selectedKind: 'script' | 'flow' | 'approval' | 'trigger' | 'preprocessor' | 'failure'
|
|
) {
|
|
selectedByKeyboard = 0
|
|
}
|
|
|
|
let inlineScripts: [string, SupportedLanguage | 'docker'][] = $state([])
|
|
|
|
const enterpriseLangs = ['mssql', 'oracledb']
|
|
|
|
function computeInlineScriptChoices(
|
|
funcDesc: string,
|
|
selected: { kind: 'owner' | 'integrations'; name: string | undefined } | undefined,
|
|
preFilter: 'all' | 'workspace' | 'hub',
|
|
selectedKind: 'script' | 'flow' | 'approval' | 'trigger' | 'preprocessor' | 'failure'
|
|
) {
|
|
if (
|
|
!composeOnlyFlows &&
|
|
['script', 'trigger', 'failure', 'approval', 'preprocessor'].includes(selectedKind)
|
|
) {
|
|
if (!selected && preFilter == 'all') {
|
|
inlineScripts = langs.filter((lang) => {
|
|
return (
|
|
(customUi?.languages == undefined || customUi?.languages?.includes(lang?.[1])) &&
|
|
(funcDesc?.length == 0 ||
|
|
lang?.[0]?.toLowerCase()?.includes(funcDesc?.toLowerCase())) &&
|
|
displayLang(lang?.[1], selectedKind)
|
|
)
|
|
})
|
|
return
|
|
}
|
|
}
|
|
inlineScripts = []
|
|
}
|
|
|
|
const allToplevelNodes: [string, string][] = [
|
|
['For loop', 'forloop'],
|
|
['While loop', 'whileloop'],
|
|
['Branch to one', 'branchone'],
|
|
['Branch to all', 'branchall'],
|
|
...(customUi?.aiAgent != false
|
|
? ([
|
|
['AI Agent', 'aiagent'],
|
|
['AI Decision', 'aidecision']
|
|
] as [string, string][])
|
|
: [])
|
|
]
|
|
|
|
let topLevelNodes: [string, string][] = $state([])
|
|
function computeToplevelNodeChoices(funcDesc: string, preFilter: 'all' | 'workspace' | 'hub') {
|
|
if (funcDesc.length > 0 && preFilter == 'all' && kind == 'script') {
|
|
topLevelNodes = allToplevelNodes.filter((node) =>
|
|
node[0].toLowerCase().startsWith(funcDesc.toLowerCase())
|
|
)
|
|
} else {
|
|
topLevelNodes = []
|
|
}
|
|
}
|
|
|
|
function onPrefilterChange(preFilter: 'all' | 'workspace' | 'hub') {
|
|
if (preFilter == 'workspace') {
|
|
hubCompletions = []
|
|
} else if (preFilter == 'hub') {
|
|
filteredWorkspaceItems = []
|
|
}
|
|
selectedByKeyboard = 0
|
|
}
|
|
|
|
let scrollable: HTMLElement | undefined = $state()
|
|
function onKeyDown(e: KeyboardEvent) {
|
|
let length = hubOffset + (hubCompletions?.length ?? 0)
|
|
if (e.key === 'ArrowDown') {
|
|
selectedByKeyboard = (selectedByKeyboard + 1) % length
|
|
scrollable?.scrollTo({ top: selectedByKeyboard * 32, behavior: 'smooth' })
|
|
e.preventDefault()
|
|
} else if (e.key === 'ArrowUp') {
|
|
selectedByKeyboard = (selectedByKeyboard - 1 + length) % length
|
|
scrollable?.scrollTo({ top: selectedByKeyboard * 32, behavior: 'smooth' })
|
|
e.preventDefault()
|
|
}
|
|
}
|
|
|
|
// Mouse and keyboard share this one index, so rows report hover on mousemove rather than
|
|
// mouseenter: arrow keys scroll the list, which slides a row under a stationary cursor and
|
|
// fires mouseenter, which would otherwise hijack the selection mid-navigation.
|
|
function hover(index: number) {
|
|
selectedByKeyboard = index
|
|
}
|
|
|
|
let langs = $derived(
|
|
processInlineLangs(undefined, $defaultScripts?.order ?? Object.keys(defaultScriptLanguages))
|
|
.map((l) => [defaultScriptLanguages[l], l])
|
|
.filter(
|
|
(x) => $defaultScripts?.hidden == undefined || !$defaultScripts.hidden.includes(x[1])
|
|
) as [string, SupportedLanguage | 'docker'][]
|
|
)
|
|
$effect(() => {
|
|
selectedKind
|
|
untrack(() => onSelectedKindChange(selectedKind))
|
|
})
|
|
$effect(() => {
|
|
;[funcDesc, preFilter]
|
|
untrack(() => computeToplevelNodeChoices(funcDesc, preFilter))
|
|
})
|
|
$effect(() => {
|
|
;[funcDesc, selected, preFilter, selectedKind]
|
|
untrack(() => computeInlineScriptChoices(funcDesc, selected, preFilter, selectedKind))
|
|
})
|
|
$effect(() => {
|
|
preFilter
|
|
untrack(() => onPrefilterChange(preFilter))
|
|
})
|
|
// Gates the two AI rows and their slots in the index space; both must agree or arrow keys land
|
|
// on indices that render nothing.
|
|
let showAiRows = $derived(
|
|
!disableAi &&
|
|
!$copilotInfo.workspaceDisabled &&
|
|
!composeOnlyFlows &&
|
|
funcDesc?.length > 0 &&
|
|
kind != 'failure' &&
|
|
kind != 'preprocessor' &&
|
|
(selectedKind == 'script' || selectedKind == 'trigger') &&
|
|
preFilter == 'all'
|
|
)
|
|
let aiLength = $derived(showAiRows ? 2 : 0)
|
|
|
|
const aiSandboxHeading = 'AI Sandbox'
|
|
const aiSandboxes = [
|
|
{ label: 'Claude Code', subkind: 'claudesandbox' },
|
|
{ label: 'Pi', subkind: 'pisandbox' }
|
|
] as const
|
|
// A query matching the heading keeps every sandbox, otherwise only the ones it names.
|
|
let matchingAiSandboxes = $derived.by(() => {
|
|
const query = funcDesc?.trim().toLowerCase() ?? ''
|
|
const matches = (term: string) => {
|
|
const lowered = term.toLowerCase()
|
|
return lowered.startsWith(query) || lowered.split(' ').some((w) => w.startsWith(query))
|
|
}
|
|
if (query.length == 0 || matches(aiSandboxHeading)) {
|
|
return aiSandboxes
|
|
}
|
|
return aiSandboxes.filter((s) => matches(s.label))
|
|
})
|
|
// Gates the AI Sandbox rows and their slots in the index space; both must agree or arrow keys
|
|
// land on an index that renders nothing.
|
|
let aiSandboxRows = $derived(
|
|
selectedKind === 'script' &&
|
|
preFilter === 'all' &&
|
|
!selected &&
|
|
customUi?.aiSandbox != false &&
|
|
!composeOnlyFlows
|
|
? matchingAiSandboxes
|
|
: []
|
|
)
|
|
|
|
// Hub runnables carry code the workspace never reviewed, and the backend refuses them in a
|
|
// flow a builder authors, so the hub browser and its integration filters are not offered.
|
|
let showHub = $derived(!composeOnlyFlows)
|
|
|
|
// Every result row lives in one keyboard index space, and hovering a row moves that index, so
|
|
// mouse and keyboard can never highlight two different rows. Offsets follow the render order.
|
|
let inlineOffset = $derived(topLevelNodes.length)
|
|
let aiOffset = $derived(inlineOffset + (inlineScripts?.length ?? 0))
|
|
let workspaceOffset = $derived(aiOffset + aiLength)
|
|
let aiSandboxOffset = $derived(workspaceOffset + (filteredWorkspaceItems?.length ?? 0))
|
|
let hubOffset = $derived(aiSandboxOffset + aiSandboxRows.length)
|
|
</script>
|
|
|
|
<svelte:window onkeydown={onKeyDown} />
|
|
<div class="flex flex-row grow min-w-0 divide-x relative bg-surface-tertiary rounded-md">
|
|
{#if selectedKind != 'preprocessor'}
|
|
<div class="h-full overflow-auto p-2 w-36 shrink-0 gap-1 flex flex-col">
|
|
{#if ['script', 'trigger', 'approval', 'preprocessor', 'failure'].includes(selectedKind)}
|
|
{#if (preFilter === 'all' && owners.length > 0) || preFilter === 'workspace'}
|
|
{#if preFilter !== 'workspace'}
|
|
<div class="pb-0 text-2xs font-normal text-secondary ml-2">Folders</div>
|
|
{/if}
|
|
|
|
{#if owners.length > 0}
|
|
{#each owners as owner (owner)}
|
|
<div
|
|
in:fade={{ duration: 50 }}
|
|
animate:flip={{ duration: 100 }}
|
|
class="w-full px-0.5"
|
|
>
|
|
<Button
|
|
selected={owner === selected?.name}
|
|
onClick={() => {
|
|
selected = selected?.name == owner ? undefined : { kind: 'owner', name: owner }
|
|
}}
|
|
variant="subtle"
|
|
unifiedSize="sm"
|
|
btnClasses="justify-start"
|
|
startIcon={{
|
|
icon: owner.startsWith('f/') ? Folder : User,
|
|
props: { width: 14, height: 14 }
|
|
}}
|
|
title={owner.slice(2)}
|
|
>
|
|
<span class="truncate">{owner.slice(2)}</span>
|
|
</Button>
|
|
</div>
|
|
{/each}
|
|
<div class="pb-1"></div>
|
|
{:else}
|
|
<div class="text-2xs text-primary font-normal text-center py-3 px-3 items-center">
|
|
No items found.
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
|
|
{#if showHub && (preFilter === 'hub' || preFilter === 'all')}
|
|
{#if preFilter == 'all'}
|
|
<div class="pb-0 text-2xs font-normal text-secondary ml-2 pt-1">Integrations</div>
|
|
{/if}
|
|
<ListFiltersQuick
|
|
on:selected={() => {
|
|
filteredWorkspaceItems = []
|
|
selectedByKeyboard = 0
|
|
}}
|
|
filters={integrations}
|
|
bind:selectedFilter={selected}
|
|
resourceType
|
|
/>
|
|
{#if !selected && customUi?.suggestIntegration != false}
|
|
<div class="pl-2 py-1">
|
|
<a
|
|
href={`${$hubBaseUrlStore}?suggest_integration=true`}
|
|
target="_blank"
|
|
class="text-2xs flex flex-row items-center gap-1"
|
|
>Suggest integration <ExternalLink class="size-3" />
|
|
</a>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
{:else if selectedKind === 'flow'}
|
|
{#if owners.length > 0}
|
|
{#each owners as owner (owner)}
|
|
<div in:fade={{ duration: 50 }} animate:flip={{ duration: 100 }}>
|
|
<Button
|
|
selected={owner === selected?.name}
|
|
variant="subtle"
|
|
unifiedSize="sm"
|
|
btnClasses="justify-start"
|
|
startIcon={{
|
|
icon: owner.startsWith('f/') ? Folder : User
|
|
}}
|
|
onClick={() => {
|
|
selected = selected?.name == owner ? undefined : { kind: 'owner', name: owner }
|
|
}}
|
|
>
|
|
{owner.slice(2)}
|
|
</Button>
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
<div
|
|
bind:this={scrollable}
|
|
id="flow-editor-flow-atoms"
|
|
class="h-full overflow-auto grow min-w-0 p-2 gap-1 flex flex-col"
|
|
>
|
|
{#if kind == 'script'}
|
|
{#each topLevelNodes as [label, kind], i (label)}
|
|
<FlowToplevelNode
|
|
on:click={() => {
|
|
dispatch('new', { kind })
|
|
}}
|
|
{label}
|
|
selected={selectedByKeyboard === i}
|
|
onHover={() => hover(i)}
|
|
/>
|
|
{/each}
|
|
{/if}
|
|
|
|
{#if inlineScripts?.length > 0}
|
|
<div class="pb-0 flex flex-row items-center gap-2">
|
|
<div class="text-2xs font-normal text-secondary ml-2"
|
|
>New {selectedKind != 'script' ? selectedKind + ' ' : ''}script</div
|
|
>
|
|
{#if actingUser?.is_admin || actingUser?.is_super_admin}
|
|
{#if !openScriptSettings}
|
|
<Button
|
|
onClick={() => (openScriptSettings = true)}
|
|
startIcon={{ icon: SettingsIcon }}
|
|
unifiedSize="sm"
|
|
variant="subtle"
|
|
title="Edit global default scripts"
|
|
btnClasses="-my-3"
|
|
/>
|
|
{:else}
|
|
<Button
|
|
onClick={() => (openScriptSettings = false)}
|
|
startIcon={{ icon: X }}
|
|
variant="accent"
|
|
unifiedSize="sm"
|
|
btnClasses="-my-3"
|
|
>
|
|
Close
|
|
</Button>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
{#if openScriptSettings}
|
|
<div class="p-2">
|
|
<DefaultScriptsInner small />
|
|
</div>
|
|
{/if}
|
|
{#each inlineScripts as [label, lang], i (lang)}
|
|
<FlowScriptPickerQuick
|
|
eeRestricted={!$enterpriseLicense && enterpriseLangs.includes(lang)}
|
|
selected={selectedByKeyboard === i + inlineOffset}
|
|
onHover={() => hover(i + inlineOffset)}
|
|
{enterpriseLangs}
|
|
{label}
|
|
lang={lang == 'docker' ? 'bash' : lang}
|
|
on:click={() => {
|
|
dispatch('new', {
|
|
kind: selectedKind,
|
|
inlineScript: {
|
|
language: lang == 'docker' ? 'bash' : lang,
|
|
kind: selectedKind,
|
|
subkind:
|
|
lang == 'docker'
|
|
? 'docker'
|
|
: selectedKind == 'preprocessor'
|
|
? 'preprocessor'
|
|
: 'flow',
|
|
summary
|
|
}
|
|
})
|
|
}}
|
|
/>
|
|
{/each}
|
|
{/if}
|
|
|
|
{#if showAiRows}
|
|
<ul class="transition-all">
|
|
<li
|
|
><GenAiQuick
|
|
{funcDesc}
|
|
lang="TypeScript"
|
|
selected={selectedByKeyboard === aiOffset}
|
|
onHover={() => hover(aiOffset)}
|
|
on:click={() => {
|
|
lang = 'bun'
|
|
onGenerate()
|
|
}}
|
|
/>
|
|
</li>
|
|
<li>
|
|
<GenAiQuick
|
|
{funcDesc}
|
|
lang="Python"
|
|
selected={selectedByKeyboard === aiOffset + 1}
|
|
onHover={() => hover(aiOffset + 1)}
|
|
on:click={() => {
|
|
lang = 'python3'
|
|
onGenerate()
|
|
}}
|
|
/>
|
|
</li>
|
|
</ul>
|
|
{/if}
|
|
|
|
{#if (!selected || selected?.kind === 'owner') && (preFilter === 'workspace' || preFilter === 'all')}
|
|
{#if !selected && (preFilter !== 'workspace' || funcDesc?.length > 0)}
|
|
<div class="pt-2 pb-0 text-2xs font-normal text-secondary ml-2">Workspace</div>
|
|
{/if}
|
|
{#await import('../pickers/WorkspaceScriptPickerQuick.svelte') then Module}
|
|
<Module.default
|
|
bind:owners={
|
|
() => owners,
|
|
(v) => {
|
|
$operatingWorkspace && (cachedOwners[$operatingWorkspace] = v)
|
|
owners = v
|
|
}
|
|
}
|
|
bind:ownerFilter={selected}
|
|
bind:filteredWithOwner={filteredWorkspaceItems}
|
|
{filter}
|
|
kind={selectedKind}
|
|
selected={selectedByKeyboard - workspaceOffset}
|
|
on:pickScript
|
|
on:pickFlow
|
|
{displayPath}
|
|
{refreshCount}
|
|
onHover={(i) => hover(workspaceOffset + i)}
|
|
/>
|
|
{/await}
|
|
<div class="pb-1"></div>
|
|
{/if}
|
|
{#if aiSandboxRows.length > 0}
|
|
<div class="pb-0 text-2xs font-normal text-secondary ml-2">{aiSandboxHeading}</div>
|
|
{#each aiSandboxRows as sandbox, i (sandbox.subkind)}
|
|
<FlowScriptPickerQuick
|
|
eeRestricted={false}
|
|
selected={selectedByKeyboard === aiSandboxOffset + i}
|
|
onHover={() => hover(aiSandboxOffset + i)}
|
|
enterpriseLangs={[]}
|
|
label={sandbox.label}
|
|
lang={sandbox.subkind}
|
|
on:click={() => {
|
|
dispatch('new', {
|
|
kind: selectedKind,
|
|
inlineScript: {
|
|
language: 'bun',
|
|
kind: selectedKind,
|
|
subkind: sandbox.subkind,
|
|
summary
|
|
}
|
|
})
|
|
}}
|
|
/>
|
|
{/each}
|
|
{/if}
|
|
{#if showHub && selectedKind != 'preprocessor' && selectedKind != 'flow'}
|
|
{#if (!selected || selected?.kind === 'integrations') && (preFilter === 'hub' || preFilter === 'all')}
|
|
{#if !selected && preFilter !== 'hub'}
|
|
<div class=" pb-0 text-2xs font-normal text-secondary ml-2">Hub</div>
|
|
{/if}
|
|
{#await import('../pickers/PickHubScriptQuick.svelte') then Module}
|
|
<Module.default
|
|
bind:items={hubCompletions}
|
|
bind:filter
|
|
bind:apps={
|
|
() => integrations,
|
|
(v) => {
|
|
cachedIntegrations = v
|
|
integrations = v
|
|
}
|
|
}
|
|
appFilter={selected?.name}
|
|
kind={selectedKind}
|
|
selected={selectedByKeyboard - hubOffset}
|
|
on:pickScript
|
|
bind:loading
|
|
{displayPath}
|
|
{refreshCount}
|
|
onHover={(i) => hover(hubOffset + i)}
|
|
/>
|
|
{/await}
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
</div>
|