mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-12 16:02:56 +00:00
feat: the editors name their item in the band, and a pen renames it
The band showed each editor's summary as an inline field, which put editing where the page names what it is working on. The summary is text there now, with its `title` for one the band truncates, and the pen that opens the path for editing sits after it — the same popover the editors' own bar carries, at the end of the name it renames. Every editor already has a field for the summary itself: the script's settings, the flow's Settings tab, and both app editors' deploy drawer. The popover moves into `PathEditPopover` rather than being written out four more times. It owns the open state and hands back the path as it was when it opened: the band's trail reads that while the popover is open, so a path growing under the pointer does not drag the pen — and floating-ui the popover — along with it. `EditorHeader` renders the same component, so the session pane and the band cannot drift apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
522e5ce096
commit
4f3804cf11
@@ -1,10 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ChevronRight, Pencil } from 'lucide-svelte'
|
||||
import { Alert, Button } from '$lib/components/common'
|
||||
import { ChevronRight } from 'lucide-svelte'
|
||||
import EditableInput from '$lib/components/common/EditableInput.svelte'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import PathEditPopover from '$lib/components/PathEditPopover.svelte'
|
||||
import {
|
||||
dirKey,
|
||||
KIND_LABEL_LOWER,
|
||||
@@ -15,17 +12,6 @@
|
||||
} from '$lib/components/workspacePicker'
|
||||
import BreadcrumbSegment from '$lib/components/BreadcrumbSegment.svelte'
|
||||
import { splitItemPath } from '$lib/components/breadcrumbPath'
|
||||
import { isOwner } from '$lib/utils'
|
||||
import { userStore } from '$lib/stores'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
|
||||
interface Props {
|
||||
summary?: string
|
||||
path?: string
|
||||
@@ -76,23 +62,10 @@
|
||||
workspaceId
|
||||
}: Props = $props()
|
||||
|
||||
let pathPopoverOpen = $state(false)
|
||||
/** Snapshot of `path` taken when the pen popover opens; cleared on close.
|
||||
* While set, the breadcrumb derives from it instead of `path` so the pen
|
||||
* anchor doesn't reflow as the user types in the popover (which would drag
|
||||
* the popover with it via floating-ui's auto-update). */
|
||||
/** Held by the pen popover while it is open, so the trail below — and the pen anchored to its
|
||||
* end — does not reflow as the user types. */
|
||||
let snapshotPath = $state<string | undefined>(undefined)
|
||||
|
||||
function setPathPopoverOpen(open: boolean) {
|
||||
pathPopoverOpen = open
|
||||
// Only snapshot when there's a path to freeze. New flows / scripts open
|
||||
// the popover with `path === ''` and rely on `Path.reset()` to seed a
|
||||
// path; if we snapshot the empty value the breadcrumb stays blank for
|
||||
// the whole popover lifetime. Leaving `snapshotPath` undefined lets
|
||||
// `displayPath` track the live seeded path instead.
|
||||
snapshotPath = open && path ? path : undefined
|
||||
}
|
||||
|
||||
// Path segments. e.g. "f/demo/weather_report" → scope "f/demo", slug "weather_report".
|
||||
// `snapshotPath` keeps the breadcrumb frozen while the pen popover is open
|
||||
// so the trigger doesn't drift mid-edit. Outside of that, the breadcrumb
|
||||
@@ -101,11 +74,6 @@
|
||||
let displayPath = $derived(snapshotPath ?? path ?? '')
|
||||
let segments = $derived(splitItemPath(displayPath) ?? null)
|
||||
|
||||
// Treat an empty path as ownable so the pen popover lets a user pick the
|
||||
// path for a brand-new item. `Path.reset()` then synthesizes a default
|
||||
// under their own user/folder scope.
|
||||
let own = $derived(!path || isOwner(path, actingUser, $operatingWorkspace))
|
||||
|
||||
// Virtual entry for the picker: surfaces the currently-edited item at its
|
||||
// live path (which may differ from `savedPath` mid-rename, so the picker
|
||||
// otherwise has no leaf to expand).
|
||||
@@ -174,65 +142,18 @@
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
<!-- Pen → path-edit popover. Skipped entirely when path editing is
|
||||
disabled so the user doesn't see an inert button. -->
|
||||
<!-- Skipped entirely when path editing is disabled so the user doesn't see an inert
|
||||
button. -->
|
||||
{#if pathEditable}
|
||||
<Popover
|
||||
placement="bottom-start"
|
||||
contentClasses="p-4"
|
||||
usePointerDownOutside
|
||||
excludeSelectors=".drawer"
|
||||
disableFocusTrap
|
||||
closeOnOtherPopoverOpen
|
||||
bind:isOpen={() => pathPopoverOpen, setPathPopoverOpen}
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="xs"
|
||||
iconOnly
|
||||
startIcon={{ icon: Pencil }}
|
||||
title="Edit path"
|
||||
aria-label="Edit path"
|
||||
btnClasses={penVisibility === 'hover' && !pathPopoverOpen
|
||||
? 'opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100'
|
||||
: ''}
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="flex flex-col gap-6 w-[480px]">
|
||||
{#if own}
|
||||
<Path
|
||||
autofocus
|
||||
bind:path
|
||||
initialPath={snapshotPath ?? path ?? ''}
|
||||
namePlaceholder={kind}
|
||||
{kind}
|
||||
size="sm"
|
||||
drawerOffset={4000}
|
||||
workspaceOverride={workspaceId}
|
||||
/>
|
||||
{#if savedPath && path && path !== savedPath}
|
||||
<Alert
|
||||
type="info"
|
||||
size="xs"
|
||||
title="Deploy the {kind} to make the path change effective."
|
||||
/>
|
||||
{/if}
|
||||
{#if onBehalfOfEmail}
|
||||
<Alert type="info" title="Run on behalf of" size="xs">
|
||||
This flow will be redeployed on behalf of you ({$userStore?.email}) instead of {onBehalfOfEmail}
|
||||
</Alert>
|
||||
{/if}
|
||||
{:else}
|
||||
<Label label="Path">
|
||||
<span class="text-xs font-mono text-secondary">{path}</span>
|
||||
<p class="text-2xs text-tertiary mt-1">Only the owner can change the path</p>
|
||||
</Label>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
<PathEditPopover
|
||||
bind:path
|
||||
bind:snapshotPath
|
||||
{savedPath}
|
||||
{kind}
|
||||
{workspaceId}
|
||||
{onBehalfOfEmail}
|
||||
{penVisibility}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
import { getContext, onDestroy, setContext, untrack } from 'svelte'
|
||||
import PageHeaderContent from '$lib/components/PageHeaderContent.svelte'
|
||||
import PathEditPopover from '$lib/components/PathEditPopover.svelte'
|
||||
import { pageHeader, PHONE_BAR } from '$lib/components/pageHeaderRegistry.svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import CenteredPage from './CenteredPage.svelte'
|
||||
@@ -70,7 +71,6 @@
|
||||
import FlowHistory from './flows/FlowHistory.svelte'
|
||||
import EditorHeader from './EditorHeader.svelte'
|
||||
import AutosaveIndicator from './AutosaveIndicator.svelte'
|
||||
import EditableInput from './common/EditableInput.svelte'
|
||||
import type { FlowBuilderWhitelabelCustomUi } from './custom_ui'
|
||||
import FlowYamlEditor from './flows/header/FlowYamlEditor.svelte'
|
||||
import { type TriggerContext, type ScheduleTrigger } from './triggers'
|
||||
@@ -223,6 +223,10 @@
|
||||
// Top-bar responsive collapse. Measured via bind:clientWidth — we can't
|
||||
// rely on viewport `md:` because the editor lives inside other panes
|
||||
// (session pane, drawer, etc.) where the viewport stays wide.
|
||||
/** Held by the pen's popover while it is open; the band's trail reads it so the path does not
|
||||
* reflow under the pointer as the user types. */
|
||||
let pathSnapshot = $state<string | undefined>(undefined)
|
||||
|
||||
let topbarWidth = $state(0)
|
||||
// In the page header the buttons share the row with the breadcrumb, so they collapse sooner:
|
||||
// the trail and the summary take ~560px before either truncates, and the full group — the
|
||||
@@ -1564,7 +1568,12 @@
|
||||
<!-- The editor's own top bar is the page header on this route: its breadcrumb and
|
||||
summary are the header's, and its buttons are the header's actions. -->
|
||||
<PageHeaderContent
|
||||
item={{ kind: 'flow', path: $pathStore, summaryContent: flowSummary }}
|
||||
item={{
|
||||
// Frozen while the pen's popover is open so the trail holds still as the user types.
|
||||
kind: 'flow',
|
||||
path: pathSnapshot ?? $pathStore,
|
||||
summaryContent: flowSummary
|
||||
}}
|
||||
actions={flowHeaderActions}
|
||||
contexts={headerContexts}
|
||||
/>
|
||||
@@ -1644,18 +1653,25 @@
|
||||
{/if}
|
||||
|
||||
{#snippet flowSummary()}
|
||||
<!-- The summary stays editable where the editor's own bar had it: the breadcrumb
|
||||
beside it is the path, which this editor renames from its settings tab. -->
|
||||
<EditableInput
|
||||
value={flowStore.val.summary ?? ''}
|
||||
placeholder="Add a summary..."
|
||||
editable={customUi?.topBar?.editableSummary != false}
|
||||
commitOnInput
|
||||
size="sm"
|
||||
onSave={(v) => (flowStore.val.summary = v)}
|
||||
textClass="text-xs font-medium text-emphasis leading-tight"
|
||||
class="max-w-full min-w-0"
|
||||
/>
|
||||
<!-- Read-only here: the summary is a Settings-tab field, and the band is where the flow
|
||||
is named rather than where it is edited. The pen sits after it, at the end of what
|
||||
it renames. -->
|
||||
<div class="group flex items-center gap-1 min-w-0">
|
||||
<span
|
||||
class="min-w-0 truncate text-xs font-medium text-emphasis"
|
||||
title={flowStore.val.summary}>{flowStore.val.summary}</span
|
||||
>
|
||||
{#if customUi?.topBar?.editablePath != false}
|
||||
<PathEditPopover
|
||||
bind:path={$pathStore}
|
||||
bind:snapshotPath={pathSnapshot}
|
||||
savedPath={initialPath}
|
||||
kind="flow"
|
||||
onBehalfOfEmail={$savedOnBehalfOfEmail}
|
||||
workspaceId={autosaveWorkspace}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet flowHeaderActions()}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<!--
|
||||
@component
|
||||
The pen that opens an item's path for editing, beside the breadcrumb that shows it. Renders
|
||||
nothing when the path is not the caller's to change.
|
||||
|
||||
A caller that draws the path itself should render it from `snapshotPath ?? path`: while the
|
||||
popover is open this holds the path as it was when it opened, so the trail — and the pen anchored
|
||||
to it — does not reflow under the pointer as the user types, which floating-ui would follow.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Pencil } from 'lucide-svelte'
|
||||
import { Alert, Button } from '$lib/components/common'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import type { WorkspaceItemKind } from '$lib/components/workspacePicker'
|
||||
import { isOwner } from '$lib/utils'
|
||||
import { userStore } from '$lib/stores'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
|
||||
interface Props {
|
||||
path?: string
|
||||
/** The item's *saved* path on the server, so the popover can say a rename needs deploying. */
|
||||
savedPath?: string
|
||||
kind?: WorkspaceItemKind
|
||||
/** Workspace the path picker scopes to; defaults to the operating workspace. */
|
||||
workspaceId?: string
|
||||
/** When set, warns that a redeploy happens on behalf of the current user instead. */
|
||||
onBehalfOfEmail?: string | undefined
|
||||
penVisibility?: 'hover' | 'always'
|
||||
/** The path as it was when the popover opened, undefined while it is closed. See the
|
||||
* component note: the caller draws its trail from this so it holds still mid-rename. */
|
||||
snapshotPath?: string | undefined
|
||||
}
|
||||
|
||||
let {
|
||||
path = $bindable(),
|
||||
savedPath,
|
||||
kind = 'flow',
|
||||
workspaceId,
|
||||
onBehalfOfEmail,
|
||||
penVisibility = 'hover',
|
||||
snapshotPath = $bindable()
|
||||
}: Props = $props()
|
||||
|
||||
let open = $state(false)
|
||||
|
||||
// Treat an empty path as ownable so the popover lets a user pick the path for a brand-new
|
||||
// item. `Path.reset()` then synthesizes a default under their own user/folder scope.
|
||||
const own = $derived(!path || isOwner(path, actingUser, $operatingWorkspace))
|
||||
|
||||
function setOpen(v: boolean) {
|
||||
open = v
|
||||
// Only snapshot when there is a path to freeze. A new flow or script opens with `path === ''`
|
||||
// and relies on `Path.reset()` to seed one; snapshotting the empty value would leave the
|
||||
// trail blank for the popover's whole lifetime.
|
||||
snapshotPath = v && path ? path : undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popover
|
||||
placement="bottom-start"
|
||||
contentClasses="p-4"
|
||||
usePointerDownOutside
|
||||
excludeSelectors=".drawer"
|
||||
disableFocusTrap
|
||||
closeOnOtherPopoverOpen
|
||||
bind:isOpen={() => open, setOpen}
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="xs"
|
||||
iconOnly
|
||||
startIcon={{ icon: Pencil }}
|
||||
title="Edit path"
|
||||
aria-label="Edit path"
|
||||
btnClasses={penVisibility === 'hover' && !open
|
||||
? 'opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100'
|
||||
: ''}
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="flex flex-col gap-6 w-[480px]">
|
||||
{#if own}
|
||||
<Path
|
||||
autofocus
|
||||
bind:path
|
||||
initialPath={snapshotPath ?? path ?? ''}
|
||||
namePlaceholder={kind}
|
||||
{kind}
|
||||
size="sm"
|
||||
drawerOffset={4000}
|
||||
workspaceOverride={workspaceId}
|
||||
/>
|
||||
{#if savedPath && path && path !== savedPath}
|
||||
<Alert
|
||||
type="info"
|
||||
size="xs"
|
||||
title="Deploy the {kind} to make the path change effective."
|
||||
/>
|
||||
{/if}
|
||||
{#if onBehalfOfEmail}
|
||||
<Alert type="info" title="Run on behalf of" size="xs">
|
||||
This flow will be redeployed on behalf of you ({$userStore?.email}) instead of {onBehalfOfEmail}
|
||||
</Alert>
|
||||
{/if}
|
||||
{:else}
|
||||
<Label label="Path">
|
||||
<span class="text-xs font-mono text-secondary">{path}</span>
|
||||
<p class="text-2xs text-tertiary mt-1">Only the owner can change the path</p>
|
||||
</Label>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
@@ -94,8 +94,8 @@
|
||||
import DefaultScripts from './DefaultScripts.svelte'
|
||||
import { getContext, onDestroy, onMount, setContext, tick, untrack } from 'svelte'
|
||||
import EditorHeader from './EditorHeader.svelte'
|
||||
import EditableInput from './common/EditableInput.svelte'
|
||||
import PageHeaderContent from '$lib/components/PageHeaderContent.svelte'
|
||||
import PathEditPopover from '$lib/components/PathEditPopover.svelte'
|
||||
import { pageHeader, PHONE_BAR } from '$lib/components/pageHeaderRegistry.svelte'
|
||||
import ScriptSettingsBadges from './ScriptSettingsBadges.svelte'
|
||||
import Badge from './common/badge/Badge.svelte'
|
||||
@@ -200,6 +200,10 @@
|
||||
// right group (hide the ~200px tag select, icon-only Diff/Settings) before the
|
||||
// full group crowds the path into heavy truncation; ~900 is where the path
|
||||
// keeps a usable width given the right group's natural ~440px.
|
||||
/** Held by the pen's popover while it is open; the band's trail reads it so the path does not
|
||||
* reflow under the pointer as the user types. */
|
||||
let pathSnapshot = $state<string | undefined>(undefined)
|
||||
|
||||
let topbarWidth = $state(0)
|
||||
// In the page header the buttons share the row with the breadcrumb, so they collapse sooner:
|
||||
// the trail and the summary take ~560px before either truncates, and the full button group is
|
||||
@@ -2280,7 +2284,12 @@
|
||||
<!-- The editor's own top bar is the page header on this route: the script's path and
|
||||
summary are the breadcrumb's, and its buttons are the header's actions. -->
|
||||
<PageHeaderContent
|
||||
item={{ kind: 'script', path: script.path, summaryContent: scriptSummary }}
|
||||
item={{
|
||||
// Frozen while the pen's popover is open so the trail holds still as the user types.
|
||||
kind: 'script',
|
||||
path: pathSnapshot ?? script.path,
|
||||
summaryContent: scriptSummary
|
||||
}}
|
||||
afterName={scriptMarks}
|
||||
actions={scriptHeaderActions}
|
||||
/>
|
||||
@@ -2329,18 +2338,23 @@
|
||||
{/if}
|
||||
|
||||
{#snippet scriptSummary()}
|
||||
<!-- The summary stays editable where the editor's own bar had it; the path beside it is
|
||||
the breadcrumb, which this editor renames from its settings drawer. -->
|
||||
<EditableInput
|
||||
value={script.summary ?? ''}
|
||||
placeholder="Add a summary..."
|
||||
editable={customUi?.topBar?.editableSummary != false}
|
||||
commitOnInput
|
||||
size="sm"
|
||||
onSave={(v) => (script.summary = v.trim())}
|
||||
textClass="text-xs font-medium text-emphasis leading-tight"
|
||||
class="max-w-full min-w-0"
|
||||
/>
|
||||
<!-- Read-only here: the summary is a settings field, and the band is where the script is
|
||||
named rather than where it is edited. The pen sits after it, at the end of what it
|
||||
renames. -->
|
||||
<div class="group flex items-center gap-1 min-w-0">
|
||||
<span class="min-w-0 truncate text-xs font-medium text-emphasis" title={script.summary}
|
||||
>{script.summary}</span
|
||||
>
|
||||
{#if customUi?.topBar?.editablePath != false}
|
||||
<PathEditPopover
|
||||
bind:path={script.path}
|
||||
bind:snapshotPath={pathSnapshot}
|
||||
savedPath={initialPath}
|
||||
kind="script"
|
||||
workspaceId={autosaveWorkspace}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet scriptMarks()}
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
import DebugPanel from './contextPanel/DebugPanel.svelte'
|
||||
|
||||
import EditorHeader from '$lib/components/EditorHeader.svelte'
|
||||
import EditableInput from '$lib/components/common/EditableInput.svelte'
|
||||
import PageHeaderContent from '$lib/components/PageHeaderContent.svelte'
|
||||
import PathEditPopover from '$lib/components/PathEditPopover.svelte'
|
||||
import { pageHeader, PHONE_BAR } from '$lib/components/pageHeaderRegistry.svelte'
|
||||
import AutosaveIndicator from '$lib/components/AutosaveIndicator.svelte'
|
||||
import { editPathFor } from '$lib/components/workspacePicker'
|
||||
@@ -232,6 +232,10 @@
|
||||
// The header's buttons render under the page header, not under this component, so the contexts
|
||||
// they look up have to travel with them. The app's own two carry everything the canvas toggles,
|
||||
// the panel buttons, Debug runs and the preview switch read.
|
||||
/** Held by the pen's popover while it is open; the band's trail reads it so the path does not
|
||||
* reflow under the pointer as the user types. */
|
||||
let pathSnapshot = $state<string | undefined>(undefined)
|
||||
|
||||
const headerContexts = new Map<any, any>([
|
||||
['AppViewerContext', getContext('AppViewerContext')],
|
||||
['AppEditorContext', getContext('AppEditorContext')]
|
||||
@@ -947,8 +951,9 @@
|
||||
// The path being edited, not the stored one: a brand-new app is parked at a
|
||||
// `draft_<uuid>` placeholder, and the trail would name that instead of the path Deploy
|
||||
// will create. Same fallback chain the rename sites read.
|
||||
// Frozen while the pen's popover is open so the trail holds still as the user types.
|
||||
kind: 'app',
|
||||
path: newEditedPath || $appPath || newPath || undefined,
|
||||
path: pathSnapshot ?? newEditedPath ?? $appPath ?? newPath ?? undefined,
|
||||
summaryContent: appSummary
|
||||
}}
|
||||
actions={appHeaderActions}
|
||||
@@ -985,18 +990,18 @@
|
||||
{/if}
|
||||
|
||||
{#snippet appSummary()}
|
||||
<!-- The summary stays editable where the editor's own bar had it; the path beside it is the
|
||||
breadcrumb, which this editor renames from its deploy drawer. `title` so a summary the
|
||||
band truncates can still be read, the way the editor's own bar did it. -->
|
||||
<div class="max-w-full min-w-0" title={$summary}>
|
||||
<EditableInput
|
||||
value={$summary ?? ''}
|
||||
placeholder="Add a summary..."
|
||||
commitOnInput
|
||||
size="sm"
|
||||
onSave={(v) => ($summary = v.trim())}
|
||||
textClass="text-xs font-medium text-emphasis leading-tight"
|
||||
class="max-w-full min-w-0"
|
||||
<!-- Read-only here: the summary is a deploy-drawer field, and the band is where the app is
|
||||
named rather than where it is edited. `title` so one the band truncates can still be read.
|
||||
The pen sits after it, at the end of what it renames. -->
|
||||
<div class="group flex items-center gap-1 min-w-0">
|
||||
<span class="min-w-0 truncate text-xs font-medium text-emphasis" title={$summary}
|
||||
>{$summary}</span
|
||||
>
|
||||
<PathEditPopover
|
||||
bind:path={newEditedPath}
|
||||
bind:snapshotPath={pathSnapshot}
|
||||
savedPath={$appPath || newPath || undefined}
|
||||
kind="app"
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
import PageHeaderContent from '$lib/components/PageHeaderContent.svelte'
|
||||
import { pageHeader, PHONE_BAR } from '$lib/components/pageHeaderRegistry.svelte'
|
||||
import EditableInput from '$lib/components/common/EditableInput.svelte'
|
||||
import PathEditPopover from '$lib/components/PathEditPopover.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
@@ -336,6 +336,10 @@
|
||||
// header uses.
|
||||
const headerBtnSize = $derived(condensedHeader || ownsPageHeader ? 'sm' : 'md')
|
||||
|
||||
/** Held by the pen's popover while it is open; the band's trail reads it so the path does not
|
||||
* reflow under the pointer as the user types. */
|
||||
let pathSnapshot = $state<string | undefined>(undefined)
|
||||
|
||||
// The actions render under the band, outside the subtree RawAppEditor declared the operating
|
||||
// workspace in, so it has to travel with them: OpenInSessionButton asks who the acting user is.
|
||||
const headerContexts = new Map<any, any>([operatingWorkspaceContextEntry()])
|
||||
@@ -931,15 +935,15 @@
|
||||
|
||||
{#if ownsPageHeader}
|
||||
<!-- The editor's own top bar is the page header on this route: the app's path and summary are
|
||||
the breadcrumb's, and everything else the bar carried rides along as the header's actions.
|
||||
The path is a plain label there — this editor renames from its deploy drawer. -->
|
||||
the breadcrumb's, and everything else the bar carried rides along as the header's actions. -->
|
||||
<PageHeaderContent
|
||||
item={{
|
||||
// The path being edited, not the stored one: a brand-new app is parked at a
|
||||
// `draft_<uuid>` placeholder, and the trail would name that instead of the path Deploy
|
||||
// will create — which is why `newEditedPath` refuses the placeholder.
|
||||
// will create — which is why `newEditedPath` refuses the placeholder. Frozen while the
|
||||
// pen's popover is open so the trail holds still as the user types.
|
||||
kind: 'app',
|
||||
path: newEditedPath || appPath || newPath || undefined,
|
||||
path: pathSnapshot ?? newEditedPath ?? appPath ?? newPath ?? undefined,
|
||||
summaryContent: appSummary
|
||||
}}
|
||||
actions={rawAppHeaderActions}
|
||||
@@ -1017,18 +1021,18 @@
|
||||
{/snippet}
|
||||
|
||||
{#snippet appSummary()}
|
||||
<!-- The summary stays editable where the editor's own bar had it; the path beside it is the
|
||||
breadcrumb, which this editor renames from its deploy drawer. `title` so a summary the
|
||||
band truncates can still be read, the way the editor's own bar did it. -->
|
||||
<div class="max-w-full min-w-0" title={summary}>
|
||||
<EditableInput
|
||||
value={summary ?? ''}
|
||||
placeholder="Add a summary..."
|
||||
commitOnInput
|
||||
size="sm"
|
||||
onSave={(v) => (summary = v.trim())}
|
||||
textClass="text-xs font-medium text-emphasis leading-tight"
|
||||
class="max-w-full min-w-0"
|
||||
<!-- Read-only here: the summary is a deploy-drawer field, and the band is where the app is
|
||||
named rather than where it is edited. `title` so one the band truncates can still be read.
|
||||
The pen sits after it, at the end of what it renames. -->
|
||||
<div class="group flex items-center gap-1 min-w-0">
|
||||
<span class="min-w-0 truncate text-xs font-medium text-emphasis" title={summary}>{summary}</span
|
||||
>
|
||||
<PathEditPopover
|
||||
bind:path={newEditedPath}
|
||||
bind:snapshotPath={pathSnapshot}
|
||||
savedPath={appPath || undefined}
|
||||
kind="app"
|
||||
workspaceId={autosaveWorkspace}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
Reference in New Issue
Block a user