From 67ef376c8f5da0eec6fac109e5254c80e608c0e7 Mon Sep 17 00:00:00 2001 From: Guilhem Date: Tue, 17 Feb 2026 00:10:05 +0000 Subject: [PATCH] feat(frontend): inline edit summary & path from header (#7968) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * allow editing flow/script summary * feat(frontend): wire up edit summary/path on flow detail page - Fix on:click → onclick (Svelte 5) and add title on Save button - Make can_write reactive ($state) so onEdit prop updates correctly - Wire onEdit in flow detail page to call FlowService.updateFlow Co-Authored-By: Claude Opus 4.6 * feat(frontend): use Path component for path editing in detail page header Co-Authored-By: Claude Opus 4.5 * feat(frontend): extract SummaryPathDisplay component with edit popover Consolidate the summary+path display and edit popover into a reusable SummaryPathDisplay component, used in both the detail page header and the flow editor toolbar. Co-Authored-By: Claude Opus 4.6 * feat(frontend): add size prop to Path/FolderPicker, compact popover Add size prop ('sm' | 'md') to Path and FolderPicker components, passed through to ToggleButton, TextInput, and Button children. Use hideFullPath and size="sm" in the SummaryPathDisplay popover for a compact inline path editor. Widen popover to 480px. Co-Authored-By: Claude Opus 4.6 * Fix add folder in path editor * fix(frontend): disable focus trap on edit popover for drawer access Disable melt-ui's focus trap on the SummaryPathDisplay popover so that inputs inside drawers (e.g. New Folder) can receive focus. Co-Authored-By: Claude Opus 4.6 * nit * feat(frontend): auto-create folder and render drawer above popover Co-Authored-By: Claude Opus 4.6 * feat(frontend): show placeholder and hover-reveal pencil in SummaryPathDisplay Co-Authored-By: Claude Opus 4.6 * feat(frontend): click-to-edit SummaryPathDisplay with inline layout Co-Authored-By: Claude Opus 4.6 * feat(frontend): move undo/redo and tutorials into dropdown submenu with notification dot Co-Authored-By: Claude Opus 4.5 * feat(frontend): stack path above summary in SummaryPathDisplay Co-Authored-By: Claude Opus 4.5 * feat(frontend): bind summary/path directly in flow builder popover Co-Authored-By: Claude Opus 4.5 * nit * chore: add PR screenshots (to be removed before merge) Co-Authored-By: Claude Opus 4.5 * chore: remove PR screenshots (moved to release assets) Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.6 --- .../lib/components/DropdownSubmenuItem.svelte | 83 +++++++ frontend/src/lib/components/DropdownV2.svelte | 3 +- .../src/lib/components/DropdownV2Inner.svelte | 77 +++--- .../src/lib/components/FlowBuilder.svelte | 221 +++++++++++------- .../src/lib/components/FolderPicker.svelte | 66 +++--- frontend/src/lib/components/Path.svelte | 69 ++++-- .../lib/components/SummaryPathDisplay.svelte | 151 ++++++++++++ .../details/DetailPageHeader.svelte | 31 +-- .../components/meltComponents/Popover.svelte | 9 +- frontend/src/lib/utils.ts | 2 + .../(logged)/flows/get/[...path]/+page.svelte | 28 ++- 11 files changed, 541 insertions(+), 199 deletions(-) create mode 100644 frontend/src/lib/components/DropdownSubmenuItem.svelte create mode 100644 frontend/src/lib/components/SummaryPathDisplay.svelte diff --git a/frontend/src/lib/components/DropdownSubmenuItem.svelte b/frontend/src/lib/components/DropdownSubmenuItem.svelte new file mode 100644 index 0000000000..b9204b7ae9 --- /dev/null +++ b/frontend/src/lib/components/DropdownSubmenuItem.svelte @@ -0,0 +1,83 @@ + + + + +{#if $subOpen} +
+ {#each subItems as subItem} + {#if subItem.separatorTop} +
+ {/if} + subItem?.action?.(e)} + href={subItem?.href} + target={subItem?.hrefTarget} + disabled={subItem?.disabled} + class={twMerge( + 'px-4 py-2 text-primary font-normal hover:bg-surface-hover cursor-pointer text-xs transition-colors w-full', + 'data-[highlighted]:bg-surface-hover', + 'flex flex-row gap-2 items-center rounded-sm', + subItem?.disabled && 'text-disabled cursor-not-allowed' + )} + item={meltItem} + > + {#if subItem.icon} + + {/if} +

+ {subItem.displayName} +

+ {@render subItem.extra?.()} + {#if subItem.tooltip} + + {#snippet text()} + {subItem.tooltip} + {/snippet} + + {/if} +
+ {/each} +
+{/if} diff --git a/frontend/src/lib/components/DropdownV2.svelte b/frontend/src/lib/components/DropdownV2.svelte index baea52c9cf..493dec8f1f 100644 --- a/frontend/src/lib/components/DropdownV2.svelte +++ b/frontend/src/lib/components/DropdownV2.svelte @@ -71,6 +71,7 @@ const { elements: { menu: menuEl, item, trigger }, + builders, states, ids: { menu: dropdownId } } = createDropdownMenu({ @@ -177,7 +178,7 @@ class="bg-surface-tertiary dark:border w-56 origin-top-right rounded-lg shadow-lg focus:outline-none overflow-y-auto py-1" style={`${customWidth ? `width: ${customWidth}px;` : ''} max-height: ${maxHeight || '50vh'};`} > - + {/if} diff --git a/frontend/src/lib/components/DropdownV2Inner.svelte b/frontend/src/lib/components/DropdownV2Inner.svelte index ebb12077d1..35fedd660e 100644 --- a/frontend/src/lib/components/DropdownV2Inner.svelte +++ b/frontend/src/lib/components/DropdownV2Inner.svelte @@ -1,5 +1,6 @@ - + { newFolder?.closeDrawer() folderCreated = undefined }} > - {#if !folderCreated} -
- - -
- {:else} + {#if folderCreated} {/if}
- +
- + { - currentTarget.select() - }} - /> - + {#if !hideFullPath} +
+
+ + Full path + + { + currentTarget.select() + }} + /> + +
+
{error}
-
{error}
-
+ {/if} {#if pathUsageInFlowsPromise || pathUsageInAppsPromise || pathUsageInScriptsPromise} {#await Promise.all( [pathUsageInAppsPromise, pathUsageInFlowsPromise, pathUsageInScriptsPromise] )} diff --git a/frontend/src/lib/components/SummaryPathDisplay.svelte b/frontend/src/lib/components/SummaryPathDisplay.svelte new file mode 100644 index 0000000000..ed2387167e --- /dev/null +++ b/frontend/src/lib/components/SummaryPathDisplay.svelte @@ -0,0 +1,151 @@ + + +{#if editable || onEdit} + + {#snippet trigger()} +
+ {path} + + {emptyString(summary) ? 'Add a summary...' : summary} + +
+ {/snippet} + {#snippet content({ close })} +
+ {#if onEdit} + +
+
Path
+ +
+ + {:else} + +
+
Path
+ +
+ {/if} +
+ {/snippet} +
+{:else} +
+ {#if !emptyString(summary)} + {path} + {/if} + + {emptyString(summary) ? (path ?? '') : summary} + +
+{/if} diff --git a/frontend/src/lib/components/details/DetailPageHeader.svelte b/frontend/src/lib/components/details/DetailPageHeader.svelte index 04ebcfb1f7..2c8f43d145 100644 --- a/frontend/src/lib/components/details/DetailPageHeader.svelte +++ b/frontend/src/lib/components/details/DetailPageHeader.svelte @@ -6,9 +6,9 @@ import { twMerge } from 'tailwind-merge' import { userStore } from '$lib/stores' import { createEventDispatcher, getContext, tick } from 'svelte' + import SummaryPathDisplay from '$lib/components/SummaryPathDisplay.svelte' import type { TriggerContext } from '../triggers' import { Calendar } from 'lucide-svelte' - import { emptyString } from '$lib/utils' type MainButton = { label: string @@ -35,6 +35,7 @@ errorHandlerKind: 'flow' | 'script' scriptOrFlowPath: string errorHandlerMuted: boolean | undefined + onEdit?: (summary: string, path: string) => void children?: import('svelte').Snippet trigger_badges?: import('svelte').Snippet } @@ -48,6 +49,7 @@ errorHandlerKind, scriptOrFlowPath, errorHandlerMuted = $bindable(), + onEdit, children, trigger_badges }: Props = $props() @@ -55,29 +57,14 @@ const dispatch = createEventDispatcher() -
+
-
-
- - {emptyString(summary) ? (path ?? '') : summary} - - {#if !emptyString(summary)} - {path} - {/if} +
+
+
{#if tag} tag: {tag} @@ -104,7 +91,7 @@ {/if} {@render trigger_badges?.()}
-
+
{#if menuItems.length > 0} {#key menuItems} { - return Array.from(document.querySelectorAll('[data-popover]')) as HTMLElement[] + const selector = excludeSelectors + ? `[data-popover], ${excludeSelectors}` + : '[data-popover]' + return Array.from(document.querySelectorAll(selector)) as HTMLElement[] } let { debounced: debounceClose, clearDebounce: clearDebounceClose } = debounce( diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index a7de8a5424..1ddebf1aec 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1504,6 +1504,8 @@ export type Item = { extra?: Snippet id?: string tooltip?: string + separatorTop?: boolean + submenuItems?: Item[] } export function isObjectTooBig(obj: any): boolean { diff --git a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte index e6d1fecf73..314a330a07 100644 --- a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte @@ -69,7 +69,7 @@ import NoDirectDeployAlert from '$lib/components/NoDirectDeployAlert.svelte' let flow: Flow | undefined = $state() - let can_write = false + let can_write = $state(false) let shareModal: ShareModal | undefined = $state() let scheduledForStr: string | undefined = $state(undefined) @@ -497,6 +497,32 @@ tag={flow?.tag ?? ''} summary={flow?.summary} path={flow?.path} + onEdit={can_write + ? async (newSummary, newPath) => { + if (!flow || !$workspaceStore) return + try { + await FlowService.updateFlow({ + workspace: $workspaceStore, + path: flow.path, + requestBody: { + path: newPath, + summary: newSummary, + description: flow.description, + value: flow.value, + schema: flow.schema + } + }) + sendUserToast('Flow updated') + if (newPath !== flow.path) { + await goto(`/flows/get/${newPath}?workspace=${$workspaceStore}`) + } else { + loadFlow() + } + } catch (e) { + sendUserToast('Could not update flow: ' + e.body, true) + } + } + : undefined} > {#snippet trigger_badges()}