mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
c000bbca28
* fix(frontend): scope raw-app/flow/script editors to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): scope flow and script editor operations to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): scope flow preview, inline-script creation and datatable schema to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review — thread session workspace through flow resource pickers, script fetch, preview cancel/recording and path collision check Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Claude review — pass session workspace to preview FlowStatusViewer and align FlowChatManager guards Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Pi review — show acting workspace in script-not-found message and fetch picked script from it in EditorBar Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 2 — thread session workspace into flow step test, raw-app inline runnable, inline editor toolbars and MCP OAuth path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 3 — thread session workspace into dynamic-input helpers and the flow-preview argument side panel (history/saved-inputs/captures) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 4 — thread session workspace into nested flow/script drawers, flow chat inputs and the flow input side tabs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 5 — thread session workspace into script-module fork/reload and key the raw-app schema cache by workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 6 — key the DB manager schema cache by acting workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 7 — thread session workspace into resource-valued arg pickers and the editor variable/resource helper drawers Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): scope the flow asset explorer's ResourceEditorDrawer to the acting workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: thread acting workspace through flow asset explore controls Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: thread acting workspace through SQL REPL, secret args, helper forms, S3 inputs, saved inputs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
639 lines
17 KiB
Svelte
639 lines
17 KiB
Svelte
<script lang="ts" module>
|
|
const lastMetaUsed = writable<Meta | undefined>(undefined)
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import { pathToMeta, type Meta } from '$lib/common'
|
|
|
|
import { copyToClipboard, localeConcatAnd, pluralize } from '$lib/utils'
|
|
import {
|
|
AppService,
|
|
FlowService,
|
|
FolderService,
|
|
ResourceService,
|
|
ScheduleService,
|
|
ScriptService,
|
|
HttpTriggerService,
|
|
VariableService,
|
|
WebsocketTriggerService,
|
|
KafkaTriggerService,
|
|
PostgresTriggerService,
|
|
NatsTriggerService,
|
|
MqttTriggerService,
|
|
SqsTriggerService,
|
|
GcpTriggerService,
|
|
AzureTriggerService,
|
|
EmailTriggerService
|
|
} from '$lib/gen'
|
|
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
|
import { createEventDispatcher, getContext, untrack } from 'svelte'
|
|
import { writable } from 'svelte/store'
|
|
import { Alert, Button } from './common'
|
|
import { random_adj } from './random_positive_adjetive'
|
|
import { ChevronDown, Copy, SearchCode } from 'lucide-svelte'
|
|
import Tooltip from './Tooltip.svelte'
|
|
import { tick } from 'svelte'
|
|
import FolderPicker from './FolderPicker.svelte'
|
|
import PathNameAutocomplete from './PathNameAutocomplete.svelte'
|
|
import TextInput, {
|
|
inputBaseClass,
|
|
inputBorderClass,
|
|
inputSizeClasses
|
|
} from './text_input/TextInput.svelte'
|
|
import Select from './select/Select.svelte'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import InputError from './InputError.svelte'
|
|
|
|
type PathKind =
|
|
| 'resource'
|
|
| 'script'
|
|
| 'variable'
|
|
| 'flow'
|
|
| 'schedule'
|
|
| 'app'
|
|
| 'raw_app'
|
|
| 'http_trigger'
|
|
| 'websocket_trigger'
|
|
| 'kafka_trigger'
|
|
| 'postgres_trigger'
|
|
| 'nats_trigger'
|
|
| 'mqtt_trigger'
|
|
| 'sqs_trigger'
|
|
| 'gcp_trigger'
|
|
| 'azure_trigger'
|
|
| 'email_trigger'
|
|
let meta: Meta | undefined = $state(undefined)
|
|
interface Props {
|
|
fullNamePlaceholder?: string | undefined
|
|
namePlaceholder?: string
|
|
initialPath: string
|
|
path?: string
|
|
error?: string
|
|
disabled?: boolean
|
|
checkInitialPathExistence?: boolean
|
|
autofocus?: boolean
|
|
dirty?: boolean
|
|
kind: PathKind
|
|
hideUser?: boolean
|
|
disableEditing?: boolean
|
|
size?: 'sm' | 'md'
|
|
drawerOffset?: number
|
|
/** Workspace the folder list and path-existence checks run against.
|
|
* Defaults to the navigation `$workspaceStore`; pass the session's acting
|
|
* workspace when the editor operates on a workspace other than the one the
|
|
* top nav points at (see the sessions preview / dev-workspace flows). */
|
|
workspaceOverride?: string
|
|
}
|
|
|
|
let {
|
|
fullNamePlaceholder = undefined,
|
|
namePlaceholder = '',
|
|
initialPath,
|
|
path = $bindable(undefined),
|
|
error = $bindable(undefined),
|
|
disabled = $bindable(false),
|
|
checkInitialPathExistence = false,
|
|
autofocus = true,
|
|
dirty = $bindable(false),
|
|
kind,
|
|
hideUser = false,
|
|
disableEditing = false,
|
|
size = 'md',
|
|
drawerOffset = 0,
|
|
workspaceOverride = undefined
|
|
}: Props = $props()
|
|
|
|
let ws = $derived(workspaceOverride ?? $workspaceStore)
|
|
|
|
$effect.pre(() => {
|
|
if (path == undefined) {
|
|
path = ''
|
|
}
|
|
if (error == undefined) {
|
|
error = ''
|
|
}
|
|
})
|
|
|
|
let inputP: PathNameAutocomplete | undefined = $state(undefined)
|
|
|
|
const dispatch = createEventDispatcher()
|
|
|
|
let folders: { name: string; write: boolean }[] = $state([])
|
|
|
|
function onMetaChange() {
|
|
if (meta) {
|
|
path = metaToPath(meta)
|
|
validate(meta, path, kind)
|
|
$lastMetaUsed = {
|
|
...meta,
|
|
name: ''
|
|
}
|
|
}
|
|
}
|
|
|
|
function metaToPath(meta: Meta): string {
|
|
return [meta.ownerKind?.charAt(0) ?? '', meta.owner, meta.name].join('/')
|
|
}
|
|
|
|
export function focus() {
|
|
inputP?.focus()
|
|
}
|
|
|
|
function handleKeyUp(event: KeyboardEvent) {
|
|
setDirty()
|
|
const key = event.key
|
|
|
|
if (key === 'Enter') {
|
|
event.preventDefault()
|
|
dispatch('enter')
|
|
}
|
|
}
|
|
|
|
export function setName(x: string) {
|
|
if (meta) {
|
|
meta.name = x
|
|
onMetaChange()
|
|
}
|
|
}
|
|
|
|
export async function reset() {
|
|
if (path == '' || path == 'u//' || path?.startsWith('tmp/') || path?.startsWith('hub/')) {
|
|
if ($lastMetaUsed == undefined || $lastMetaUsed.owner != $userStore?.username) {
|
|
meta = {
|
|
ownerKind: hideUser ? 'folder' : 'user',
|
|
name: fullNamePlaceholder ?? random_adj() + '_' + namePlaceholder,
|
|
owner: ''
|
|
}
|
|
if (!hideUser) {
|
|
if ($userStore?.username?.includes('@')) {
|
|
meta.owner = $userStore!.username.split('@')[0].replace(/[^a-zA-Z0-9_]/g, '')
|
|
} else {
|
|
meta.owner = $userStore!.username!
|
|
}
|
|
}
|
|
} else {
|
|
if ($lastMetaUsed.ownerKind == 'user' && hideUser) {
|
|
meta = {
|
|
ownerKind: 'folder',
|
|
owner: '',
|
|
name: fullNamePlaceholder ?? random_adj() + '_' + namePlaceholder
|
|
}
|
|
} else {
|
|
meta = {
|
|
...$lastMetaUsed,
|
|
name: fullNamePlaceholder ?? random_adj() + '_' + namePlaceholder
|
|
}
|
|
}
|
|
}
|
|
let newMeta = { ...meta }
|
|
while (await pathExists(metaToPath(newMeta), kind)) {
|
|
disabled = true
|
|
error = 'finding an available name...'
|
|
newMeta.name = random_adj() + '_' + (fullNamePlaceholder ?? namePlaceholder)
|
|
}
|
|
error = ''
|
|
disabled = false
|
|
meta = newMeta
|
|
path = metaToPath(meta)
|
|
} else {
|
|
meta = pathToMeta(path ?? '', hideUser)
|
|
}
|
|
}
|
|
|
|
async function loadFolders(): Promise<void> {
|
|
let initialFolders: { name: string; write: boolean }[] = []
|
|
let initialFolder = ''
|
|
if (initialPath?.split('/')?.[0] == 'f') {
|
|
initialFolder = initialPath?.split('/')?.[1]
|
|
initialFolders.push({ name: initialFolder, write: true })
|
|
}
|
|
const excludedFolders = [initialFolder, 'app_groups', 'app_custom', 'app_themes']
|
|
folders = initialFolders.concat(
|
|
(
|
|
await FolderService.listFolderNames({
|
|
workspace: ws!
|
|
})
|
|
)
|
|
.filter((x) => !excludedFolders.includes(x))
|
|
.map((x) => ({
|
|
name: x,
|
|
write:
|
|
$userStore?.folders?.includes(x) == true ||
|
|
($userStore?.is_admin ?? false) ||
|
|
($userStore?.is_super_admin ?? false)
|
|
}))
|
|
)
|
|
}
|
|
|
|
async function validate(meta: Meta, path: string, kind: PathKind) {
|
|
error = ''
|
|
validateName(meta) && validatePath(path, kind)
|
|
}
|
|
|
|
let validateTimeout: number | undefined = undefined
|
|
|
|
async function validatePath(path: string, kind: PathKind): Promise<void> {
|
|
if (validateTimeout) {
|
|
clearTimeout(validateTimeout)
|
|
}
|
|
validateTimeout = setTimeout(async () => {
|
|
if (
|
|
(path == '' || checkInitialPathExistence || path != initialPath) &&
|
|
(await pathExists(path, kind))
|
|
) {
|
|
error = 'path already used'
|
|
} else if (meta && validateName(meta)) {
|
|
error = ''
|
|
}
|
|
validateTimeout = undefined
|
|
}, 500)
|
|
}
|
|
|
|
async function pathExists(path: string, kind: PathKind): Promise<boolean> {
|
|
if (!path.length) return false
|
|
if (kind == 'flow') {
|
|
return await FlowService.existsFlowByPath({ workspace: ws!, path: path })
|
|
} else if (kind == 'script') {
|
|
return await ScriptService.existsScriptByPath({
|
|
workspace: ws!,
|
|
path: path
|
|
})
|
|
} else if (kind == 'resource') {
|
|
return await ResourceService.existsResource({
|
|
workspace: ws!,
|
|
path: path
|
|
})
|
|
} else if (kind == 'variable') {
|
|
return await VariableService.existsVariable({
|
|
workspace: ws!,
|
|
path: path
|
|
})
|
|
} else if (kind == 'schedule') {
|
|
return await ScheduleService.existsSchedule({ workspace: ws!, path: path })
|
|
} else if (kind == 'app') {
|
|
return await AppService.existsApp({ workspace: ws!, path: path })
|
|
} else if (kind == 'http_trigger') {
|
|
return await HttpTriggerService.existsHttpTrigger({
|
|
workspace: ws!,
|
|
path: path
|
|
})
|
|
} else if (kind == 'websocket_trigger') {
|
|
return await WebsocketTriggerService.existsWebsocketTrigger({
|
|
workspace: ws!,
|
|
path: path
|
|
})
|
|
} else if (kind == 'kafka_trigger') {
|
|
return await KafkaTriggerService.existsKafkaTrigger({
|
|
workspace: ws!,
|
|
path: path
|
|
})
|
|
} else if (kind == 'postgres_trigger') {
|
|
return await PostgresTriggerService.existsPostgresTrigger({
|
|
workspace: ws!,
|
|
path: path
|
|
})
|
|
} else if (kind == 'nats_trigger') {
|
|
return await NatsTriggerService.existsNatsTrigger({
|
|
workspace: ws!,
|
|
path: path
|
|
})
|
|
} else if (kind === 'mqtt_trigger') {
|
|
return await MqttTriggerService.existsMqttTrigger({
|
|
workspace: ws!,
|
|
path: path
|
|
})
|
|
} else if (kind == 'sqs_trigger') {
|
|
return await SqsTriggerService.existsSqsTrigger({
|
|
workspace: ws!,
|
|
path: path
|
|
})
|
|
} else if (kind === 'gcp_trigger') {
|
|
return await GcpTriggerService.existsGcpTrigger({
|
|
workspace: ws!,
|
|
path: path
|
|
})
|
|
} else if (kind === 'azure_trigger') {
|
|
return await AzureTriggerService.existsAzureTrigger({
|
|
workspace: ws!,
|
|
path: path
|
|
})
|
|
} else if (kind === 'email_trigger') {
|
|
return await EmailTriggerService.existsEmailTrigger({
|
|
workspace: ws!,
|
|
path: path
|
|
})
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
function validateName(meta: Meta): boolean {
|
|
if (meta.name == undefined || meta.name == '') {
|
|
error = 'Choose a name'
|
|
return false
|
|
} else if (!/^[\w-]+(\/[\w-]+)*$/.test(meta.name)) {
|
|
error = 'This name is not valid'
|
|
return false
|
|
} else if (meta.owner == '' && meta.ownerKind == 'folder') {
|
|
error = 'Folder needs to be chosen'
|
|
return false
|
|
} else if (meta.owner == '' && meta.ownerKind == 'group') {
|
|
error = 'Group needs to be chosen'
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
async function initPath() {
|
|
await tick()
|
|
if (path != undefined && path != '' && !path?.startsWith('tmp/') && !path?.startsWith('hub/')) {
|
|
meta = pathToMeta(path, hideUser)
|
|
onMetaChange()
|
|
return
|
|
}
|
|
if (initialPath == undefined || initialPath == '' || initialPath?.startsWith('tmp/')) {
|
|
reset()
|
|
} else {
|
|
meta = pathToMeta(initialPath, hideUser)
|
|
onMetaChange()
|
|
path = initialPath
|
|
}
|
|
}
|
|
|
|
function setDirty() {
|
|
!dirty && (dirty = true)
|
|
}
|
|
|
|
$effect(() => {
|
|
if (
|
|
path !== undefined &&
|
|
path !== '' &&
|
|
initialPath &&
|
|
!initialPath.startsWith('tmp/') &&
|
|
path !== initialPath &&
|
|
!dirty
|
|
) {
|
|
dirty = true
|
|
}
|
|
})
|
|
|
|
const openSearchWithPrefilledText: (t?: string) => void = getContext(
|
|
'openSearchWithPrefilledText'
|
|
)
|
|
|
|
$effect.pre(() => {
|
|
;[meta?.name, meta?.owner, meta?.ownerKind]
|
|
meta && untrack(() => onMetaChange())
|
|
})
|
|
// Reflect an EXTERNAL `path` change back into `meta` (which drives the
|
|
// owner/name inputs). The effect above is one-way meta→path; without
|
|
// this counterpart, a parent that reassigns `path` — e.g. "Discard"
|
|
// reverting a draft to the deployed value — leaves the inputs showing
|
|
// the stale value until a remount. Guard against a meta↔path loop: skip
|
|
// when `path` already matches what `meta` produces (the meta→path write),
|
|
// and only adopt a derivation that round-trips cleanly (so a malformed
|
|
// path left to `initPath`/`reset` can't oscillate).
|
|
$effect.pre(() => {
|
|
const p = path
|
|
untrack(() => {
|
|
if (p == undefined || p == '' || p.startsWith('tmp/') || p.startsWith('hub/')) return
|
|
if (!meta || metaToPath(meta) === p) return
|
|
const next = pathToMeta(p, hideUser)
|
|
if (metaToPath(next) === p) {
|
|
meta = next
|
|
}
|
|
})
|
|
})
|
|
$effect.pre(() => {
|
|
if (ws && $userStore) {
|
|
untrack(() => {
|
|
loadFolders()
|
|
initPath()
|
|
})
|
|
}
|
|
})
|
|
let displayPathChangedWarning = $derived(
|
|
(['flow', 'script', 'resource', 'variable'] as PathKind[]).includes(kind) &&
|
|
initialPath &&
|
|
initialPath !== path
|
|
)
|
|
let pathUsageInFlowsPromise = $derived(
|
|
(kind == 'script' || kind == 'flow') &&
|
|
ws &&
|
|
initialPath &&
|
|
FlowService.listFlowPathsFromWorkspaceRunnable({
|
|
workspace: ws,
|
|
path: initialPath,
|
|
runnableKind: kind
|
|
})
|
|
)
|
|
let pathUsageInAppsPromise = $derived(
|
|
(kind == 'script' || kind == 'flow') &&
|
|
ws &&
|
|
initialPath &&
|
|
AppService.listAppPathsFromWorkspaceRunnable({
|
|
workspace: ws,
|
|
path: initialPath,
|
|
runnableKind: kind
|
|
})
|
|
)
|
|
let pathUsageInScriptsPromise = $derived(
|
|
kind == 'script' &&
|
|
ws &&
|
|
initialPath &&
|
|
ScriptService.listScriptPathsFromWorkspaceRunnable({
|
|
workspace: ws,
|
|
path: initialPath
|
|
})
|
|
)
|
|
</script>
|
|
|
|
<div>
|
|
<div
|
|
class={twMerge(
|
|
inputBaseClass,
|
|
inputBorderClass({ error: !!error }),
|
|
inputSizeClasses[size],
|
|
'relative flex gap-0 pb-0 mb-1 flex-wrap flex-row items-center',
|
|
disabled && '!bg-surface-disabled cursor-not-allowed border-none'
|
|
)}
|
|
>
|
|
{#if meta != undefined}
|
|
{@const nameDisabled = disabled || disableEditing}
|
|
<!-- svelte-ignore a11y_label_has_associated_control -->
|
|
{#if !hideUser}
|
|
<div class="block">
|
|
<Select
|
|
items={[
|
|
{ value: 'user', label: 'User' },
|
|
{ value: 'folder', label: 'Folder' }
|
|
]}
|
|
RightIcon={ChevronDown}
|
|
transformInputSelectedText={(t) => t.substring(0, 1).toLowerCase()}
|
|
inputClass={twMerge('border-none', disabled && '!bg-transparent')}
|
|
useContentEditable
|
|
bind:value={
|
|
() => meta?.ownerKind,
|
|
(v) => {
|
|
if (!meta || !v) return
|
|
setDirty()
|
|
meta.ownerKind = v
|
|
if (v === 'folder') {
|
|
meta.owner = folders?.[0]?.name ?? ''
|
|
} else {
|
|
// 'group' is unreachable here (Select only offers user/folder)
|
|
// but validateName still accepts it for forward-compat.
|
|
meta.owner = $userStore?.username?.split('@')[0] ?? ''
|
|
}
|
|
}
|
|
}
|
|
disabled={nameDisabled}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
{#if !hideUser}
|
|
<div class="text-sm text-secondary">/</div>
|
|
{/if}
|
|
<div>
|
|
{#if meta.ownerKind === 'user'}
|
|
{@const userOwnerDisabled =
|
|
disabled || !($superadmin || ($userStore?.is_admin ?? false)) || disableEditing}
|
|
<label class="block shrink min-w-0">
|
|
<TextInput
|
|
class={twMerge('!border-none', userOwnerDisabled && '!bg-transparent')}
|
|
{size}
|
|
underlyingInputEl="div"
|
|
bind:value={meta.owner}
|
|
inputProps={{
|
|
placeholder: $userStore?.username ?? '',
|
|
onkeydown: setDirty,
|
|
disabled: userOwnerDisabled
|
|
}}
|
|
/>
|
|
</label>
|
|
{:else if meta.ownerKind === 'folder'}
|
|
<label class="block grow">
|
|
<FolderPicker
|
|
bind:folderName={meta.owner}
|
|
{initialPath}
|
|
{disabled}
|
|
{disableEditing}
|
|
{size}
|
|
{drawerOffset}
|
|
selectInputClass={twMerge('!border-none', disabled && '!bg-transparent')}
|
|
/>
|
|
</label>
|
|
{/if}
|
|
</div>
|
|
<div class="text-sm text-secondary">/</div>
|
|
<label class="block grow mr-3">
|
|
<!-- svelte-ignore a11y_autofocus -->
|
|
<PathNameAutocomplete
|
|
bind:this={inputP}
|
|
bind:value={meta.name}
|
|
prefix={`${meta.ownerKind?.charAt(0) ?? ''}/${meta.owner ?? ''}/`}
|
|
workspace={ws}
|
|
{size}
|
|
{error}
|
|
{autofocus}
|
|
id="path"
|
|
placeholder={namePlaceholder}
|
|
disabled={nameDisabled}
|
|
onkeyup={handleKeyUp}
|
|
textInputClass={twMerge(
|
|
'border-none',
|
|
nameDisabled && '!bg-transparent disabled:!bg-transparent'
|
|
)}
|
|
/>
|
|
</label>
|
|
<Button
|
|
iconOnly
|
|
size="xs2"
|
|
variant="subtle"
|
|
startIcon={{ icon: Copy }}
|
|
title="Copy path"
|
|
wrapperClasses="absolute right-1 top-1/2 -translate-y-1/2"
|
|
on:click={() => copyToClipboard(path)}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
|
|
<InputError {error} />
|
|
|
|
{#if pathUsageInFlowsPromise || pathUsageInAppsPromise || pathUsageInScriptsPromise}
|
|
{#await Promise.all( [pathUsageInAppsPromise, pathUsageInFlowsPromise, pathUsageInScriptsPromise] ) then [apps, flows, scripts]}
|
|
{#if (apps && apps.length) || (flows && flows.length) || (scripts && scripts.length)}
|
|
<p class="text-xs">
|
|
Used by {localeConcatAnd([
|
|
...(scripts && scripts.length ? [pluralize(scripts.length, 'script')] : []),
|
|
...(flows && flows.length ? [pluralize(flows.length, 'flow')] : []),
|
|
...(apps && apps.length ? [pluralize(apps.length, 'app')] : [])
|
|
])}
|
|
<Tooltip>
|
|
<ul>
|
|
{#each scripts || [] as path}
|
|
<li><a target="_blank" href="/scripts/edit/{path}">{path}</a></li>
|
|
{/each}
|
|
{#each flows || [] as path}
|
|
<li><a target="_blank" href="/flows/edit/{path}">{path}</a></li>
|
|
{/each}
|
|
{#each apps || [] as path}
|
|
<li><a target="_blank" href="/apps/edit/{path}">{path}</a></li>
|
|
{/each}
|
|
</ul>
|
|
</Tooltip>
|
|
</p>
|
|
{#if displayPathChangedWarning}
|
|
<Alert
|
|
type="warning"
|
|
class="mt-4"
|
|
title="Moving this item will break the following referencing it:"
|
|
>
|
|
<ul class="list-disc">
|
|
{#each scripts || [] as scriptPath}
|
|
<li>
|
|
<a href={`/scripts/edit/${scriptPath}`} class="text-blue-400" target="_blank">
|
|
{scriptPath}
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
{#each flows || [] as flowPath}
|
|
<li>
|
|
<a href={`/flows/edit/${flowPath}`} class="text-blue-400" target="_blank">
|
|
{flowPath}
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
{#each apps || [] as appPath}
|
|
<li>
|
|
<a href={`/apps/edit/${appPath}`} class="text-blue-400" target="_blank">
|
|
{appPath}
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</Alert>
|
|
{/if}
|
|
{/if}
|
|
{/await}
|
|
{:else if displayPathChangedWarning}
|
|
<Alert type="warning" class="mt-4" title="Moving may break other items relying on it">
|
|
You are renaming an item that may be depended upon by other items. This may break apps, flows
|
|
or resources. Find if it used elsewhere using the content search. Note that linked variables
|
|
and resources (having the same path) are automatically moved together.
|
|
<div class="flex pt-2">
|
|
<Button
|
|
variant="default"
|
|
on:click={() => {
|
|
openSearchWithPrefilledText('#')
|
|
}}
|
|
startIcon={{ icon: SearchCode }}
|
|
>
|
|
Search
|
|
</Button>
|
|
</div>
|
|
</Alert>
|
|
{/if}
|
|
</div>
|