mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-27 16:02:43 +00:00
* feat(sessions): show an item's deployed page in the preview panel The preview panel showed an item's editor and nothing else. It now shows the deployed page too, as a tab of its own. **Two sides, two tabs.** The editor and the deployed page are different things to look at, so `isItemTabFor` keys on the side rather than the item: asking for a side already open focuses it, asking for the other opens it alongside. Two editors for one item still cannot coexist — they would race its single (kind, path) cell — but an editor beside a viewer is safe, since the viewer reads the deployed version over the API and holds no cell. The tab breadcrumb keeps re-pointing in place; `Exit & see details` and the detail page's `Edit` open the other side instead of consuming the one you are on. A tab's label carries `(edit)` when it is the editor side. **The detail pages moved out of their routes.** `/scripts/get` and `/flows/get` are now thin wrappers over `ScriptDetail` and `FlowDetail`, which the panel renders too. Everything they reach — drawers, triggers, saved inputs — is scoped to the viewer's workspace through `setOperatingWorkspace`, and every navigation they attempt is caught by `interceptNav` and turned into a move inside the panel, so nothing takes the browser out of the session. A plain click is intercepted; ⌘-click and middle-click still open a real tab, which is why the pages keep their `href`s. **The picker opens what exists.** A row opens the deployed page, or the editor when nothing is deployed there, and carries a Draft / Draft only badge in the review dock's words. `WorkspaceItem` gained `draftOnly` and `hasDraft` from the listers, which already returned both; deploys now invalidate the picker cache through `itemDeployed`, so a just-deployed item stops reading as a draft. Rows also carry a hover Edit action, reachable from the keyboard with the pick modifier. `open_preview` gained a `mode`, narrowed out of the advertised schema for a session that cannot write drafts and re-checked per path in the handler, where a refusal reports "couldn't check" apart from "denied". The mode is resolved before those checks, so a call that omits it is gated as the editor it will become. Alongside, the workspace a detail page acts on is now the one it is showing rather than the one the browser is navigated to: `MoveDrawer`, `toggleWorkspaceErrorHandler` and the pages' own permission gates read the operating workspace and its acting user, and `RunForm` mints a password argument's ephemeral variable there too — in a fork session all of these previously answered for the parent. `InWorkspaceAppViewer` hands the app's `ctx` user down as a prop instead of writing the global `userStore`, which from a session tab was re-pointing every permission check on the page. Fixes found on the way: `DetailPageLayout` claimed `h-screen` inside a panel that is not the viewport; Edit on a historical script version dropped the version from the intercepted click; the Run button stayed spinning after a run that left the page mounted; and the window-level run shortcut fired from a collapsed panel and from keys another handler had already claimed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(sessions): historical edits, stale not-found, shared edit rights, tab labels Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): keep deployed-page links and workspace reads in the session Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): scope detail hrefs to the session, re-point the viewer's own tab Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): keep a previewed raw app's route out of the session URL Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): resolve edit-in-fork against the session workspace Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): read advanced run tags from the session workspace Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): fork from the session workspace, star only navigation items Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): mount only the side a preview tab shows Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): address CI review round 1 findings Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): route links in a deployed page's drawers through the panel Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): tell the chat which deployed page the panel shows Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): only a 404 reads as an undeployed raw app Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): scope the unparseable-JSON run gate to the form Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): a flow deleted from the panel stays in its tab Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): the picker's edit shortcut works on the current row Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): load-error toasts say what failed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): keep an oversized invalid JSON editor in its form's run gate Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * revert: keep main's draft_only description in openapi.yaml Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
42 lines
2.3 KiB
TypeScript
42 lines
2.3 KiB
TypeScript
// Every mounted `SimpleEditor`, so a caller can materialise what the user typed without
|
|
// knowing which editors a page contains — a drawer nests them through SchemaForm and
|
|
// ArgInput, so enumerating them from the container does not scale. Plain module rather
|
|
// than the editor component: importing that pulls Monaco's side-effect imports into every
|
|
// graph that reaches this, and the components around it defer Monaco deliberately.
|
|
const liveEditors = new Set<{ flushPendingChanges: () => void }>()
|
|
|
|
/** Register a mounted editor; the returned function deregisters it. */
|
|
export function registerPendingEditor(editor: { flushPendingChanges: () => void }): () => void {
|
|
liveEditors.add(editor)
|
|
return () => liveEditors.delete(editor)
|
|
}
|
|
|
|
/** Drain every mounted editor's debounced buffer. For code that must act on what is on
|
|
* screen before leaving it — persisting a draft before routing to a session. */
|
|
export function flushAllPendingEditorChanges(): void {
|
|
for (const editor of liveEditors) editor.flushPendingChanges()
|
|
}
|
|
|
|
// Editors whose current text does not parse. Their value never reaches the bound field, so
|
|
// a caller persisting "what is on screen" would save the last value that did parse and
|
|
// leave without it. Registered by the editors that parse, not by the ones that only hold text.
|
|
const unparseable = new Map<object, Element | undefined>()
|
|
|
|
/** Mark or clear this editor as holding text that does not parse. `el` is where the editor
|
|
* sits, for a caller that checks only its own part of the page. */
|
|
export function setEditorUnparseable(key: object, invalid: boolean, el?: Element): void {
|
|
if (invalid) unparseable.set(key, el)
|
|
else unparseable.delete(key)
|
|
}
|
|
|
|
/** Whether any editor holds text that cannot be persisted as written. Registry-wide rather
|
|
* than per-item: the editors that parse are nested arbitrarily deep and none of them knows
|
|
* which draft it belongs to. `within` narrows it to the editors inside one element, for a
|
|
* form that shares the page with others: a session keeps hidden preview tabs mounted, and
|
|
* one tab's broken JSON must not stop another tab's form from running. */
|
|
export function anyEditorUnparseable(within?: Element): boolean {
|
|
if (!within) return unparseable.size > 0
|
|
for (const el of unparseable.values()) if (el && within.contains(el)) return true
|
|
return false
|
|
}
|