mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-12 16:02:56 +00:00
feat: the raw app editor fills the band like the other three
It was the one editor route still rendering its own bar under the band, so it showed two header rows and the band named the app "Apps raw" — `navPageFor`'s first-segment fallback, because nothing on the route registered an item. Its top bar now becomes the page header the way the script, flow and low-code app editors' do: the path and summary are the breadcrumb's, the file-sidebar toggle, autosave cloud, presence mark, menu, Diff, Jobs, export, AI and Deploy are the header's actions, and the path is a plain label since this editor renames from its deploy drawer. The actions render under the band, outside the subtree `RawAppEditor` declares the operating workspace in, and `OpenInSessionButton` asks that context who the acting user is — so the workspace travels with them. `operatingWorkspaceContextEntry()` hands it over without exporting the key. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ccff8da84b
commit
1d2e032224
@@ -22,6 +22,14 @@ export function setOperatingWorkspace(resolve: () => string | undefined): void {
|
||||
setContext(KEY, outer ? () => resolve() ?? outer() : resolve)
|
||||
}
|
||||
|
||||
/** This subtree's operating workspace as an entry for a `ContextBridge` contexts map. A snippet
|
||||
* handed to the page header's band renders outside the subtree that declared the workspace, so
|
||||
* without this it would quietly act on the navigation workspace instead. Reads context: call
|
||||
* during component initialisation. */
|
||||
export function operatingWorkspaceContextEntry(): [any, any] {
|
||||
return [KEY, getContext(KEY)]
|
||||
}
|
||||
|
||||
/** The workspace this component acts on, as a store. Reads context: call during component
|
||||
* initialisation. Falls back to the navigation workspace where no host set one, or where the
|
||||
* host's resolves to nothing. */
|
||||
|
||||
@@ -187,6 +187,8 @@
|
||||
// EditorHeader's path/breadcrumb row dropped (summary only). Used by the
|
||||
// session preview to save vertical room.
|
||||
condensedHeader?: boolean
|
||||
/** True for the route's own editor: its top bar becomes the page header. */
|
||||
ownsPageHeader?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -226,6 +228,7 @@
|
||||
onRestore,
|
||||
onSavedNewAppPath,
|
||||
condensedHeader = false,
|
||||
ownsPageHeader = false,
|
||||
version = undefined,
|
||||
onTakeLatest = undefined,
|
||||
draftBaseVersion = undefined
|
||||
@@ -2454,6 +2457,7 @@
|
||||
sidebarCollapsed={sidebarCollapsed.val}
|
||||
onToggleSidebar={() => (sidebarCollapsed.val = !sidebarCollapsed.val)}
|
||||
{condensedHeader}
|
||||
{ownsPageHeader}
|
||||
/>
|
||||
|
||||
<RawAppYamlEditor
|
||||
|
||||
@@ -76,9 +76,13 @@
|
||||
import { editInForkAllowed, editInForkLabel, openEditInFork } from '$lib/utils/editInFork'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import {
|
||||
operatingWorkspaceContextEntry,
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace
|
||||
} 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'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
@@ -153,6 +157,8 @@
|
||||
* EditorHeader's path/breadcrumb row dropped (summary only). Used by the
|
||||
* session preview to save vertical room. */
|
||||
condensedHeader?: boolean
|
||||
/** True for the route's own editor: its top bar becomes the page header. */
|
||||
ownsPageHeader?: boolean
|
||||
onNavigate?: (item: import('$lib/components/workspacePicker').WorkspaceItem) => void
|
||||
liveEditorDraftStoragePath?: string
|
||||
/** Indicator-only overrides for the sessions preview: the AutosaveIndicator
|
||||
@@ -223,6 +229,7 @@
|
||||
sidebarCollapsed = false,
|
||||
onToggleSidebar = undefined,
|
||||
condensedHeader = false,
|
||||
ownsPageHeader = false,
|
||||
onNavigate = undefined,
|
||||
liveEditorDraftStoragePath = undefined,
|
||||
autosaveWorkspace = undefined,
|
||||
@@ -308,11 +315,29 @@
|
||||
|
||||
// Top-bar responsive collapse — container width, not viewport.
|
||||
let topbarWidth = $state(0)
|
||||
const compactTopbar = $derived(topbarWidth > 0 && topbarWidth < 720)
|
||||
// In the page header the buttons share the row with the breadcrumb, so they collapse sooner:
|
||||
// the trail and the summary take ~430px of the same row, and the whole group — the menu, Diff,
|
||||
// Jobs, Export, AI and Deploy — is ~560px.
|
||||
const compactBelow = $derived(ownsPageHeader ? 1350 : 720)
|
||||
const compactTopbar = $derived.by(() => {
|
||||
const w = ownsPageHeader ? pageHeader.barWidth : topbarWidth
|
||||
return w > 0 && w < compactBelow
|
||||
})
|
||||
// A phone's bar holds the trail and Deploy. The file sidebar's toggle, Jobs and the export
|
||||
// join the menu that compact already shows.
|
||||
const phoneTopbar = $derived(
|
||||
ownsPageHeader && pageHeader.barWidth > 0 && pageHeader.barWidth < PHONE_BAR
|
||||
)
|
||||
|
||||
// Top-bar button size + bar height. Condensed (session preview) uses the
|
||||
// smallest well-supported unified size (`sm`) so the bar is thinner.
|
||||
const headerBtnSize = $derived(condensedHeader ? 'sm' : 'md')
|
||||
// In the page header the buttons ride a 44px band, the same size the condensed session-pane
|
||||
// header uses.
|
||||
const headerBtnSize = $derived(condensedHeader || ownsPageHeader ? 'sm' : 'md')
|
||||
|
||||
// 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()])
|
||||
|
||||
function closeSaveDrawer() {
|
||||
saveDrawerOpen = false
|
||||
@@ -903,121 +928,174 @@
|
||||
errorByComponent={{}}
|
||||
/>
|
||||
|
||||
<div
|
||||
bind:clientWidth={topbarWidth}
|
||||
class="flex flex-row justify-between gap-2 gap-y-2 px-2 items-center overflow-y-visible overflow-x-auto shrink-0 {condensedHeader
|
||||
? 'max-h-9 h-9'
|
||||
: 'max-h-12 h-12'}"
|
||||
>
|
||||
<!-- Identity block: shrinks/truncates first so the cloud indicator and the
|
||||
{#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. -->
|
||||
<PageHeaderContent
|
||||
item={{ kind: 'app', path: appPath || newPath || undefined, summaryContent: appSummary }}
|
||||
actions={rawAppHeaderActions}
|
||||
contexts={headerContexts}
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
bind:clientWidth={topbarWidth}
|
||||
class="flex flex-row justify-between gap-2 gap-y-2 px-2 items-center overflow-y-visible overflow-x-auto shrink-0 {condensedHeader
|
||||
? 'max-h-9 h-9'
|
||||
: 'max-h-12 h-12'}"
|
||||
>
|
||||
<!-- Identity block: shrinks/truncates first so the cloud indicator and the
|
||||
action buttons stay visible. Without min-w-0 the breadcrumb + summary
|
||||
overflow this box on narrow widths and get overlapped by the (formerly
|
||||
un-pinned) action group, hiding the autosave cloud. -->
|
||||
<div class="flex flex-row gap-2 items-center min-w-0">
|
||||
{#if onToggleSidebar}
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="subtle"
|
||||
iconOnly
|
||||
startIcon={{ icon: sidebarCollapsed ? PanelLeft : PanelLeftClose }}
|
||||
title={`${sidebarCollapsed ? 'Expand' : 'Collapse'} file sidebar (${isMac() ? '⌘' : 'Ctrl+'}B)`}
|
||||
on:click={() => onToggleSidebar?.()}
|
||||
/>
|
||||
{/if}
|
||||
<div class="min-w-0 overflow-hidden">
|
||||
<EditorHeader
|
||||
bind:summary
|
||||
bind:path={newEditedPath}
|
||||
savedPath={appPath || newPath || undefined}
|
||||
kind="app"
|
||||
raw_app
|
||||
hidePath={condensedHeader}
|
||||
workspaceId={autosaveWorkspace}
|
||||
onNavigate={(item) => (onNavigate ? onNavigate(item) : goto(editPathFor(item)))}
|
||||
/>
|
||||
<div class="flex flex-row gap-2 items-center min-w-0">
|
||||
{@render sidebarToggle()}
|
||||
<div class="min-w-0 overflow-hidden">
|
||||
<EditorHeader
|
||||
bind:summary
|
||||
bind:path={newEditedPath}
|
||||
savedPath={appPath || newPath || undefined}
|
||||
kind="app"
|
||||
raw_app
|
||||
hidePath={condensedHeader}
|
||||
workspaceId={autosaveWorkspace}
|
||||
onNavigate={(item) => (onNavigate ? onNavigate(item) : goto(editPathFor(item)))}
|
||||
/>
|
||||
</div>
|
||||
{@render autosaveIndicator()}
|
||||
</div>
|
||||
{#if opWorkspace && indicatorPath !== undefined}
|
||||
<AutosaveIndicator
|
||||
workspace={opWorkspace}
|
||||
itemKind="raw_app"
|
||||
path={indicatorPath}
|
||||
draftOnly={newApp}
|
||||
{onResetToDeployed}
|
||||
{loadedFromDraft}
|
||||
{othersDraftsCount}
|
||||
{onOpenOthersDrafts}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{@render awarenessMark()}
|
||||
<div class="flex flex-row gap-2 justify-end items-center overflow-visible shrink-0">
|
||||
{@render rawAppActions()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#snippet sidebarToggle()}
|
||||
{#if onToggleSidebar}
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="subtle"
|
||||
iconOnly
|
||||
startIcon={{ icon: sidebarCollapsed ? PanelLeft : PanelLeftClose }}
|
||||
title={`${sidebarCollapsed ? 'Expand' : 'Collapse'} file sidebar (${isMac() ? '⌘' : 'Ctrl+'}B)`}
|
||||
on:click={() => onToggleSidebar?.()}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet autosaveIndicator()}
|
||||
{#if opWorkspace && indicatorPath !== undefined}
|
||||
<AutosaveIndicator
|
||||
workspace={opWorkspace}
|
||||
itemKind="raw_app"
|
||||
path={indicatorPath}
|
||||
draftOnly={newApp}
|
||||
{onResetToDeployed}
|
||||
{loadedFromDraft}
|
||||
{othersDraftsCount}
|
||||
{onOpenOthersDrafts}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet awarenessMark()}
|
||||
{#if $enterpriseLicense && appPath != '' && !inSessionPane}
|
||||
<div class="shrink-0">
|
||||
<Awareness />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-row gap-2 justify-end items-center overflow-visible shrink-0">
|
||||
<DropdownV2 items={moreItems} class="h-auto">
|
||||
{#snippet buttonReplacement()}
|
||||
<Button
|
||||
nonCaptureEvent
|
||||
unifiedSize={headerBtnSize}
|
||||
variant="subtle"
|
||||
startIcon={{ icon: EllipsisVertical }}
|
||||
iconOnly
|
||||
></Button>
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
{/snippet}
|
||||
|
||||
<!-- A disabled <button> fires no pointer events, so a title/tooltip on it
|
||||
{#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. -->
|
||||
<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"
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet rawAppHeaderActions()}
|
||||
<!-- The bar's own order kept: the file sidebar, how the draft is doing, who else is here, then
|
||||
what to do with the app. -->
|
||||
{@render sidebarToggle()}
|
||||
{@render autosaveIndicator()}
|
||||
{@render awarenessMark()}
|
||||
{@render rawAppActions()}
|
||||
{/snippet}
|
||||
|
||||
{#snippet rawAppActions()}
|
||||
<DropdownV2 items={moreItems} class="h-auto">
|
||||
{#snippet buttonReplacement()}
|
||||
<Button
|
||||
nonCaptureEvent
|
||||
unifiedSize={headerBtnSize}
|
||||
variant="subtle"
|
||||
startIcon={{ icon: EllipsisVertical }}
|
||||
iconOnly
|
||||
></Button>
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
|
||||
<!-- A disabled <button> fires no pointer events, so a title/tooltip on it
|
||||
never shows on hover. pointer-events-none on the button lets the hover
|
||||
reach this titled wrapper instead. -->
|
||||
<div
|
||||
<div
|
||||
title={newApp || savedApp?.no_deployed === true
|
||||
? 'Deploy this app once to compare against the deployed version'
|
||||
: 'Diff'}
|
||||
class={!savedApp || newApp || savedApp?.no_deployed === true
|
||||
? 'flex cursor-not-allowed'
|
||||
: 'flex'}
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
unifiedSize={headerBtnSize}
|
||||
on:click={() => openDiffDrawer()}
|
||||
disabled={!savedApp || newApp || savedApp?.no_deployed === true}
|
||||
btnClasses={!savedApp || newApp || savedApp?.no_deployed === true
|
||||
? 'pointer-events-none'
|
||||
: undefined}
|
||||
iconOnly={compactTopbar}
|
||||
title={newApp || savedApp?.no_deployed === true
|
||||
? 'Deploy this app once to compare against the deployed version'
|
||||
: 'Diff'}
|
||||
class={!savedApp || newApp || savedApp?.no_deployed === true
|
||||
? 'flex cursor-not-allowed'
|
||||
: 'flex'}
|
||||
startIcon={{ icon: DiffIcon }}
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
unifiedSize={headerBtnSize}
|
||||
on:click={() => openDiffDrawer()}
|
||||
disabled={!savedApp || newApp || savedApp?.no_deployed === true}
|
||||
btnClasses={!savedApp || newApp || savedApp?.no_deployed === true
|
||||
? 'pointer-events-none'
|
||||
: undefined}
|
||||
iconOnly={compactTopbar}
|
||||
title={newApp || savedApp?.no_deployed === true
|
||||
? 'Deploy this app once to compare against the deployed version'
|
||||
: 'Diff'}
|
||||
startIcon={{ icon: DiffIcon }}
|
||||
>
|
||||
Diff
|
||||
</Button>
|
||||
</div>
|
||||
Diff
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="{compactTopbar ? 'hidden' : 'hidden md:inline'} relative overflow-visible">
|
||||
<Button
|
||||
on:click={() => {
|
||||
jobsDrawerOpen = true
|
||||
}}
|
||||
color="light"
|
||||
unifiedSize={headerBtnSize}
|
||||
variant="default"
|
||||
btnClasses="relative"
|
||||
>
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
<Bug size={14} />
|
||||
<div>Jobs</div>
|
||||
<div class="{compactTopbar ? 'hidden' : 'hidden md:inline'} relative overflow-visible">
|
||||
<Button
|
||||
on:click={() => {
|
||||
jobsDrawerOpen = true
|
||||
}}
|
||||
color="light"
|
||||
unifiedSize={headerBtnSize}
|
||||
variant="default"
|
||||
btnClasses="relative"
|
||||
>
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
<Bug size={14} />
|
||||
<div>Jobs</div>
|
||||
|
||||
<div class="text-2xs text-primary"
|
||||
>({jobs?.length > 99 ? '99+' : (jobs?.length ?? 0)})</div
|
||||
>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="text-2xs text-primary">({jobs?.length > 99 ? '99+' : (jobs?.length ?? 0)})</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<!-- Hidden, not unmounted: the menu's own Export entry calls into this instance. -->
|
||||
<span class={phoneTopbar ? 'hidden' : 'contents'}>
|
||||
<AppExportButton bind:this={appExport} />
|
||||
</span>
|
||||
{#if !phoneTopbar}
|
||||
<OpenInSessionButton source={sessionOpen}>
|
||||
{#snippet fallback()}
|
||||
<Button
|
||||
@@ -1032,34 +1110,34 @@
|
||||
</Button>
|
||||
{/snippet}
|
||||
</OpenInSessionButton>
|
||||
<Button
|
||||
loading={loading.save}
|
||||
startIcon={{ icon: Save }}
|
||||
on:click={save}
|
||||
unifiedSize={headerBtnSize}
|
||||
variant="accent"
|
||||
dropdownItems={appPath != ''
|
||||
? () => [
|
||||
{
|
||||
label: 'Fork',
|
||||
onClick: () => {
|
||||
window.open(`/apps/add?template=${appPath}`)
|
||||
}
|
||||
},
|
||||
...(!isCloudHosted() && editInForkAllowed(opWorkspace, $userWorkspaces)
|
||||
? [
|
||||
{
|
||||
label: editInForkLabel(opWorkspace, $userWorkspaces),
|
||||
onClick: () => {
|
||||
openEditInFork('raw_app', appPath, opWorkspace)
|
||||
}
|
||||
{/if}
|
||||
<Button
|
||||
loading={loading.save}
|
||||
startIcon={{ icon: Save }}
|
||||
on:click={save}
|
||||
unifiedSize={headerBtnSize}
|
||||
variant="accent"
|
||||
dropdownItems={appPath != ''
|
||||
? () => [
|
||||
{
|
||||
label: 'Fork',
|
||||
onClick: () => {
|
||||
window.open(`/apps/add?template=${appPath}`)
|
||||
}
|
||||
},
|
||||
...(!isCloudHosted() && editInForkAllowed(opWorkspace, $userWorkspaces)
|
||||
? [
|
||||
{
|
||||
label: editInForkLabel(opWorkspace, $userWorkspaces),
|
||||
onClick: () => {
|
||||
openEditInFork('raw_app', appPath, opWorkspace)
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
: undefined}
|
||||
>
|
||||
Deploy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
: undefined}
|
||||
>
|
||||
Deploy
|
||||
</Button>
|
||||
{/snippet}
|
||||
|
||||
@@ -606,6 +606,7 @@
|
||||
<div class="h-full">
|
||||
<RawAppEditor
|
||||
bind:this={rawAppEditor}
|
||||
ownsPageHeader
|
||||
onSavedNewAppPath={(savedPath) => {
|
||||
draftSync.remove()
|
||||
goto(`/apps_raw/edit/${savedPath}`)
|
||||
|
||||
Reference in New Issue
Block a user