mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-14 16:02:57 +00:00
5b99cce1751ca1b14d8d19663f00a1beffecdf08
78
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5b99cce175 |
fix(drafts): finish settling a write before anything reads it, and follow late landings
Timing the fan-out against the settle was the wrong direction. The settle now sends its own delete and resolves when it has gone out, so the editors report — and the schedule host remounts — on a cell that is already resolved: a stalled delete used to let that remount load the draft the save had just replaced, and restore it over the deployed value. A write can still land after all of that, when an edit made during the save is kept. A list frame reads its rows from a store of its own and nothing reloads it again, so it now reloads on the write landing rather than on our guess about when it will. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
08386433aa |
fix(sessions): send a write's settled draft before the list frame re-reads it
The settle that follows a write parks its draft delete on the autosave debounce, so the list frame reloaded straight after read the row's `*` and kept it: its markers come from a store inside the iframe, and nothing reloads it a second time when the delete lands. The fan-out sends what is parked for the key first. That needed the variable save to report after settling rather than before — reporting first meant there was nothing parked yet to send, and the flush passed over a cell about to queue a delete. Every editor now resolves its cell before saying so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
fcb25ce416 |
fix(schedules): adopt the enabled state a create deploys
Creating a schedule sends `enabled: true` whatever the form held, so a draft-only save left the form and its new baseline recording a state the server does not have — visible once a kept edit skips the remount that used to hide it. Also moves the settle comment in VariableEditor back onto its call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
c7faaf1956 |
fix(sessions): report a write per workspace, and leave the reporting editor alone
The same-path refresh reached the tab that did the save, remounting it over the edit its own settle had just preserved — the teardown's flush and the fresh editor's read race, and the older value can win. The reporting tab is skipped: it has settled its own state, which is what the report is telling everyone else. A save can also write several workspaces while only one event went out, labelled with the acting one, and a discard deletes the draft of whichever workspace the form is showing while claiming the acting one — so a linked workspace's tabs kept a replaced baseline and the wrong tabs were closed. Each write now reports its own workspace and the path it wrote there, which retires the acting-workspace bookkeeping that stood in for it. A tab re-pointed mid-write no longer swallows the report either: only the local remount was ever its to suppress. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
21ca77e415 |
fix(sessions): tell every tab on an item about any save, not only one that moved it
Editors on the same item share its draft cell but each keeps its own deployed baseline, so a save made in one left the others offering to discard back to the value it replaced — over a deployment. A save now reports the path it wrote to whether or not that moved the item, and the tabs on it that stayed put are re-read instead of re-pointed. The variable drawer reopens on the committed path after a partial multi-workspace failure rather than re-keying: re-keying brought its handles back from the values the drawer opened on, which describe neither what the committed workspace has deployed nor the draft the failed one still holds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
9d7b06ecc4 |
fix(drafts): stop the landed-delete fact from outliving the server state it described
The fact is sticky by design, so a delete that landed earlier for a key kept answering after the server moved out from under it: a discard that conflicted left it standing, and the conflict itself is tracked apart from the state the verdict reads, so both said the draft was gone. It is dropped now wherever the server stops being ours to describe — on a conflict, and on a remote resync. Also corrects what the partial-save comments claim. Following the committed rename re-keys every workspace's handle, so a workspace whose write never happened does lose sight of its draft in this editor; the draft is still on the server under the path that workspace holds the item at, which is where it stays reachable. Saying it stayed reachable here was wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
7a7e30de87 |
fix(drafts): decide a delete landed from the response handler, and from nothing being queued behind it
The draft hint has a second writer: every editor clears it from an effect as soon as a workspace sits at its baseline, which a discard does synchronously — so a delete that then failed read as one that landed, and the host left on an item still there. `postSave` now records which payload last landed for a key, a fact only it can write, and the verdict reads that. A delete winning is also not enough on its own. The form stays editable while the flush is in flight and a flush only submits what it read at its start, so an edit made in between sits on the debouncer and would recreate the draft behind the host's back. The key has to be idle too. The variable drawer no longer closes when a save failed partway: that is the case where the workspace still needing one is retried from it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
83cd40a6dd |
fix(editors): confirm a draft is gone from what landed, and follow a rename that committed
The discard verdict read the sync pipeline's own state, which settles whichever write it ends up sending: the form stays editable after Discard, so typing then replaces the queued delete with an upsert and the pipeline reports success for it. The draft-state hint is written by the POST that actually landed, so it answers what the pipeline cannot — whether the draft is still there. A multi-workspace save writes in sequence and a later workspace throwing aborts the rest, but what the acting workspace wrote is deployed either way. Both saves now report that path, so a host is never left pointing at one the item has already moved off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
1381536ec6 |
fix(editors): report a removal only once its delete lands, and a rename only from the acting workspace
A discard queues its delete through the autosave debouncer and returns, so the host left for the list while the row was still there — and left just the same when the delete failed outright. The three callbacks now wait for it and report removal only when it landed; a delete that didn't means the item is still there. `WsSpecificVersions` can point a form at a linked workspace, where a rename is that workspace's alone. Both editors took the saved path from whichever version was selected and handed it to a host acting on another one, and settled every workspace's draft against that single path. The reported path now comes from the acting workspace's own write, and each workspace settles against its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
67c1faed53 |
fix(editors): read what a save sends before it is sent, and settle its draft in one place
The resource save read its snapshot back out of the live form after the request returned, so the "did anything change meanwhile" test compared the edit against itself and always said no — the mid-request edit became the deployed baseline instead. Its payloads, its submitted path and the schedule's config are now all pinned before the first await, which also makes the schedule's test see edits to `args`, `retry` and `labels` rather than only its scalar fields. The three copies of the settling rule become `settleDraftAfterWrite`. A save that moved the item resets the cell rather than keeping a diverged one: the edit cannot follow — a freshly acquired cell reads only its own default — and the alternative is a draft stranded under a path the item no longer occupies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
0635901e7f |
fix(editors): keep an edit made while a save is in flight, and land baselines per workspace
The forms stay editable during a save, and the post-write reset dropped the local draft back to the state that was sent — so anything typed in between vanished when the request returned. The draft is now dropped only while it still holds what was written; an edit on top of the save survives as the unsaved change it is, and the hosts that remount pick it back up. A multi-workspace variable save also adopted its baselines only after every write had succeeded, so a later workspace throwing left an earlier one's draft dropped below a baseline that never moved: dirty forever, and retried as a create it had already made. Each lands as its own write does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
8202413bb9 |
fix(sessions): mount a hosted entity editor per item, and scope its reports to its workspace
The schedule and variable views re-pointed one live editor at each new path, so the previous item's load — `openEdit`, `editVariable` and everything they await — finished into the new item's form, baseline, draft cell and permissions, last response winning. Keying each on its path gives every item its own instance, with nothing of the next one's to land on. The resource host already worked this way. A report's guard also matched kind and path but not workspace, so re-scoping an unsent session left a rename from the old workspace free to retarget the new one's tab onto a path that exists only where it was written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
5373cc2984 |
fix(sessions): stop a stale write from landing on the item the editor moved to
Guarding the host was not enough: an editor's own continuation runs first, and every piece of state it touches after the await belongs to whatever the editor shows by then. The variable save now pins its payloads, its saved path and its draft key before the first write and only adopts a baseline while it is still the variable it saved; the resource save and the schedule remount stop at the same check. The host guard also matched on path alone, so a late report about a schedule reached a resource or variable that happened to share its path. It now matches the entity kind too, bound at the branch that mounts each editor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
9b37f57503 |
fix(sessions): act on an entity callback only while the tab still shows its item
A tab id outlives what the tab points at. An editor reports a removal or a rename after the write it awaited, and by then the tab may have been re-pointed onto another item — so the report was applied to whatever the tab held, and the URL built out of that unrelated location: discarding schedule A and opening schedule B sent B back to the list. Every such report now carries the path it started from, read before the await, and the host acts only while the tab is still showing that entity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
420beeadd2 |
fix(sessions): leave a hosted editor whose item a direct discard removed, and follow a renamed save
The inline draft banner's Discard calls UserDraft.discard straight off, not the discard_local_draft tool, so nothing told the tab about it. For a draft-only item the draft IS the item: discarding deletes its only representation and the tab kept editing a synthesized stand-in for something that no longer exists. Each editor now reports whether the item survived, and the session views hand that back to the tab, which returns to the list it was opened from. A draft-only schedule also opens with its path editable — saving CREATEs — so the save can land somewhere other than where the tab points. The schedule host discarded onUpdate's saved path and only remounted, re-opening the old path; it now retargets the tab like the resource and variable hosts do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
f79160e598 |
fix(sessions): rebind hosted entity editors on rescope, rename and save
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
9deb95a63e |
fix(sessions): keep hosted entity editors correct across retarget, restore and non-write tools
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
1b840dbd7f |
feat(sessions): offer the way back to the list from an entity editor tab
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
cc8d55eb57 |
feat(sessions): host schedule, resource and variable editors in process
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 |
||
|
|
adc7947579 |
feat: open an AI session from runs, jobs and trigger pages (#10608)
* feat: open an AI session from the runs and trigger pages * feat: tell the chat which page the session preview shows * fix: observe shallow url writes and keep page tabs deduped by path * feat: open an AI session from the resource and variable drawers * fix: re-point page tabs on hash change and follow the drawer's workspace * fix: force a load when a page tab is re-pointed within one document * fix: report a re-pointed preview tab as retargeted, not opened * fix: reload a preview tab re-pointed at the url the frame drifted from * fix: canonicalize runs previews and read drawer anchors per page * fix: dedupe page tabs on the path so self-written filters don't duplicate * perf: carry the active-preview rule only in chats that have a side panel * fix: read a preview tab's hash as a row only where the page deep-links one * fix: focus the preview tab showing the exact location before retargeting by path * refactor: give preview locations one module that says what they mean * fix: report the active preview from what is on screen, not the selected tab * fix: read a preview location's view from the params a request can set * fix: count every filter a request can set, and flush drawer drafts before routing * docs: state each preview-routing constraint once, within four lines * fix: take a page's view params from the filter schema it already declares * docs: describe the filter contract the url builders now follow * fix: describe a preview to the model from addressing fields only * fix: keep a filter value holding a delimiter apart from two filters * fix: keep a preview description to one line the model can trust * fix: materialize the resource editors before persisting the draft * docs: bring the preview-routing constraints back within four lines * fix: refuse to route a preview on state the drawer could not persist * fix: read a resource drawer's validity from the editor, not from draft dirtiness * fix: answer what the user can see from one place in both descriptions * refactor: name each write to a preview tab's two locations, and the read * fix: flush only editors holding a pending change * fix: drop a list page's row anchor when its drawer closes * fix: clear the row anchor on every list page that deep-links one * fix: keep a closed drawer closed, and refuse to leave unparseable text * refactor: register the resource json field in the shared unparseable set * refactor: decide a forced load where the command changes, not in the host * fix: navigate a preview frame only when it is not already there * fix: boot a remounted preview frame where the user left it * fix: carry a list page's filters and open row into the session Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: navigate a preview frame by what it shows, not by its url Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: read a resource's raw-editor validity from the current parse Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: compare preview views without iterating URLSearchParams Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: drop re-exports the path leaf left without readers Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c000bbca28 |
fix(frontend): scope raw-app, flow and script editors to the session workspace (#10015)
* 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> |
||
|
|
9c758df5eb |
reserve more fixed height for unsaved-changes banner to avoid content shift (#9873)
* fix(frontend): reserve fixed height for unsaved-changes banner to avoid content shift Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): remove border around reserved banner slot and shrink it Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): tighten top padding under the unsaved-changes banner Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): give unsaved-changes banner buttons minimal vertical breathing room Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(frontend): drop redundant Metadata section title in trigger and script editors Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): tuck schedule editor labels under summary to match convention Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * revert(frontend): keep Metadata section title in ScriptBuilder for a separate PR Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): drop leftover header-content margin on headless Section Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): drop top padding above resource editor first field Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(frontend): match variable editor bottom padding to resource Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): add Path label in new resource form to match edit Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): reserve half the banner height to halve the idle gap Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): reserve a third of the banner height when idle Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): don't reserve banner slot or tighten top for new entities Gate the reserved-height slot and the tight content top padding on the banner's baseline (bannerReserved) instead of merely on the banner snippet being present, so new-entity drawers keep normal top spacing and add no empty slot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(frontend): trim banner comments to the 4-line invariant limit Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(frontend): describe partial-reserve banner behavior accurately Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1fc355709c |
feat: Db-backed user drafts (#9351)
* Db draft removal * refactor: drop unsaved-changes confirmation modal from editors * fix: remove nodraft from flow row edit link * fix: remove nodraft from app and raw app edit buttons * fix: remove nodraft from all edit links * fix: merge backend defaults into legacy autosaves to avoid spurious restore toast on raw apps * feat: add username column to draft table for user-scoped drafts * feat: add sync_drafts and list_users_with_draft_on_path endpoints * feat: add UserDraftDbSyncer service for bi-directional draft sync * feat: wire UserDraft.save through DbSyncer + conflict modal * refactor: gate useLocalStorageValue nested-update effect behind opt-in flag * refactor: move sync force flag from request-level to per-entry * feat: sync all userdraft kinds, switch draft owner to email FK, add id PK, scope draft list to readable paths * refactor: route draft permission check through authed.folders + RLS, drop client-supplied email * feat: support draft deletion via sync (value: null) with same conflict semantics * feat: surface other users' drafts in editors with diff+fork action * refactor: unify draft schema migrations and type kinds via DRAFT_KIND enum * perf: add (workspace_id, email, created_at) partial index for sync hot path * chore: update ee-repo-ref to a30079e75dc5b7d7413aa8ee20e40e80bfea9cbd This commit updates the EE repository reference after PR #597 was merged in windmill-ee-private. Previous ee-repo-ref: 55c19293232be379a3044eb78f677b545882ffd6 New ee-repo-ref: a30079e75dc5b7d7413aa8ee20e40e80bfea9cbd Automated by sync-ee-ref workflow. * fix(userdraft): trigger sync on deep mutations via readFieldsRecursively * Rollback UserDraft * remove queuing logic * pushDrafts * refactor: remove draft sync layer and conflict modal * feat: add save_draft, list_drafts, get_draft routes * feat: add get_draft overlay to getScriptByPath * feat: extend get_draft overlay to flow, app, resource, variable, schedule, triggers * feat: support null value in save_draft for deletes * readLastSyncMap * feat: redirect /add pages to /edit/draft_uuid with new_draft flag * fix: inline get_draft query field instead of flattening * fix: drop dangling nobackenddraft assignment in flows edit * feat: include user drafts in list endpoints with is_draft flag * fix: prefix draft paths with u/{user} and seed editor state on new_draft * fix: route draft-only deletes through UserDraftDbSyncer on home page * feat: delete user drafts when their underlying item is deleted * fix: empty path seed on new_draft so friendly auto-name fires * feat: re-add Draft and Draft only badges on home page rows * fix: synthesize value wrapper on draft-only raw_app response * fix: tolerate missing latest-version on draft-only flow reload * fix: skip first observable change in DB sync effect to match LS persist * fix: remove URL-hash sync from script editor (already marked TEMP) * refactor: drop localStorage layer from UserDraft * refactor: drop vestigial LS-era code from UserDraft * feat: migrate localStorage drafts to DB on layout mount * fix: migrate session runtime + script view to per-user draft API * feat: add 'Reset to deployed' action on draft-loaded toast * feat: hide 'Reset to deployed' action when no deployed version exists * createCoalescingKeyedRunner * example ts doc * createDebouncerByKey * refactor: drop await on draft-delete in reset flows, refetch deployed directly * fix: bridge saved-draft shape to wire shape in apps/resources/variables loaders * feat: route UserDraftDbSyncer.save through debouncer + coalescing runner * feat: add immediate-save bypass that cancels pending debouncer + runner tasks * fix: seed UserDraft cell from spec defaultValue on acquire * fix: redirect /add routes at load phase to eliminate white flash * fix: drop +page.js files in /add routes that conflicted with +page.ts * refactor: send draft as separate .draft field instead of deep-merging onto deployed * feat: surface draft path in home list when user typed one different from URL * feat: add UserDraft.stopSync/restartSync, wire on script + low-code app /add init * fix: thread URL path into ScriptBuilder.stopSync (was using empty initialPath) * fix: also stopSync in route's new_draft branch + queue pre-acquire suspensions * feat: add AutosaveIndicator backed by reactive UserDraftDbSyncer.getState * refactor: drop draft-loaded toast in non-route editors, banner now compares draft vs deployed * fix: gate per-user draft-only rows in listings on include_draft_only flag * feat: flush pending draft saves via keepalive fetch on tab hide / pagehide * autosave indicator nits * fix: route create-vs-update on /add deploys; seed policy.execution_mode; sync script template * chore: add [draft-sync] console logs to trace script bootstrap autosave * fix: seed auto-generated path in script new-draft route to suppress Path widget's autosave-triggering mutation * fix: defer script restartSync until script.path lands (Path widget gated on $userStore + $workspaceStore) * fix: poll script.path via tick() until Path widget settles before restartSync * chore: log inferArgs underlying error on deploy to diagnose 'Could not parse code' toast * fix: wait for script.path to stabilize across two ticks before restartSync * revert: drop unsuccessful path-stabilization heuristics + leftover [draft-sync] logs * fix: seed new-draft script schema as emptySchema() so inferArgs doesn't trip on undefined properties * fix: heal legacy drafts with schema={} (no .properties) on deploy * autosave indicator * refactor(editors): drop UnsavedConfirmationModal mount + Show diff button * feat(drafts): collaboration banner, cross-tab conflict detection, raw app template picker - Other-users-drafts banner (Modal2): the deployed-overlay response now carries `other_drafts_users` (workspace usernames only, never emails); each row offers View JSON + Fork. Drops the standalone `listUsersWithDraftOnPath` endpoint; `getDraftForUser` now takes a workspace `username` query param (resolved to email server-side). - Cross-tab/browser save conflict detection: the syncer attaches `last_sync` to every save (defaults to non-force); on a `conflict` response it parks a snapshot in a reactive map. Each route mounts a `DraftSyncConflictModal` and seeds the per-tab `last_sync` via `recordRemoteSync(query, draft_saved_at)` on every `get_draft` load. Keepalive flush also respects optimistic concurrency. - Raw app template picker re-added after the /add ⇒ /edit refactor: framework (React 19 / 18 / Svelte 5), data table + schema config, and optional AI prompt — extracted into `RawAppTemplatePicker.svelte` and driven by `new_draft=true` on the edit route. * fix(drafts): suppress autosave during /add template seeding on script + raw app editors - ScriptBuilder: delay `restartSync` 500ms past `initContent` + stores- ready so the Path widget's `$workspaceStore && $userStore`-gated `initPath → reset → onMetaChange → bind:path` cascade lands inside the suspension window. Two `tick()` waits weren't enough — the bind:path mutation fired ~100ms after the prior `restartSync` and posted as a "user edit". - apps_raw route: suspend autosave on `new_draft=true` and resume only after the framework picker closes (via `onStart` or X dismissal), with a two-tick settle so the picker's seeded `files/runnables/data/policy` mirror to `draftHandle.draft` observably advances `lastSerialized` before sync re-arms. * fix(drafts): land /add redirects on the real workspace username, not "me" The `/add` → `/edit/u/{username}/draft_{uuid}` redirects ran during SvelteKit's load phase, BEFORE the (logged) layout's async `getUserExt` populated `userStore`. `get(userStore)?.username` returned undefined and fell back to the `'me'` placeholder on every fresh nav, producing `u/me/draft_{uuid}` paths instead of the user's real namespace — broke ownership checks against `authed.username` and silently scoped autosaves under the wrong path. Layout now persists `username` to localStorage on every successful `getUserExt`, and `getUsernameForNamespace` (new shared helper, used by all four `/add/+page.ts` files) reads the live store first, falls back to the cached value, and only then to `'me'` for true first-ever loads. * fix(drafts): key low-code app autosave on the URL path, not the empty string `AppEditor` keyed its `UserDraft.use` handle on `newApp ? '' : path` — a legacy leftover from when `/apps/add` was its own URL (no path). With the `/add` ⇒ `/edit/u/{user}/draft_{uuid}` redirect, `newApp=true` made autosaves land on the `('app', '')` row instead of the URL path: - The `apps/list?include_draft_only=true` query joins drafts onto `app.path`, surfacing drafts at the URL path. The empty-path row didn't match the user's URL so the draft never appeared in the home list. - Refreshing `/apps/edit/u/{user}/draft_{uuid}` re-fetches at the URL path with `?get_draft=true`, finds nothing, and 404s. Drop the ternary so the handle always uses `path` — the same as scripts/flows/raw_apps. The route's `?new_draft=true` branch already seeds the empty-template baseline, so there's no longer a "the draft sits under '' until first save" race to worry about. * fix(raw_app): propagate template picker X / Esc dismissal so autosave resumes The picker mounted `<Modal kind="X" open ...>` (one-way prop, not `bind:open`). When the user dismissed via X / Esc / click-outside, the inner Modal flipped its own local `open` to false (hiding the UI) but never wrote back to the picker's `open` $bindable. The route's `templatePicker → false` watcher — the one that calls `restartSync` two ticks after the picker closes — never fired, so autosave stayed suspended and the user's edits after dismissal were silently dropped. Switch the inner Modal to `bind:open` so the dismissal bubbles all the way up to the route's state. "Start without AI" already worked because its `onStart` handler explicitly sets the picker's `open = false`. * nit unused * fix(drafts): make the home-page View/Edit JSON action work on draft-only apps The "View/Edit JSON" entry on the home page called `AppService.getAppByPath` without `get_draft=true`, so for draft-only items at `u/{user}/draft_{uuid}` the backend 404'd with "App not found at path …". Pass `get_draft=true` and render the synthesized stand-in's editable shape: - App drafts come back as `{summary, value, path, policy, ...}` — `value` is the App definition the editor was working on; show that. - Raw-app drafts come back as the flattened `{files, runnables, data, summary, policy, ...}` with no nested `value`; show the whole shape. On save, draft-only items can't go through `updateApp` (no deployed row). Route the edit through `UserDraftDbSyncer.save` (with `immediate: true` so `await` resolves after the POST lands) and relabel the button "Save draft" + Save icon. Deployed items keep the existing "Deploy" flow unchanged. * fix(drafts): render the right shape in View/Edit JSON for draft-only items The previous fix landed `fapp.value` into the editor, but the deployed-overlay flattens the bare editable shape into `inner`/the top-level response — drafts have no nested `.value`. So: - App drafts (`{grid, breakpoints, hiddenInlineScripts, …}`) rendered as empty (`fapp.value` was undefined). - Raw-app drafts 404'd outright: `get_draft=true` with no `rawApp` flag can't tell which draft kind to look up, defaults to `app`, doesn't find one. Thread the row's `raw_app` flag from AppRow → `appExport.open(path, rawApp)` → `getAppByPath({..., rawApp})` so raw-app drafts resolve to the right `UserDraftItemKind`. Read `fapp.draft` (the bare editable shape from `fetch_draft_only`) into the JSON editor for draft-only items — clean payload, no `is_draft` / `no_deployed` / overlay noise. Save the same bare shape back through the syncer so the regular editor reads it unchanged on the next mount. * fix(drafts): skip public-secret-URL fetch in the Deploy drawer for draft-only apps Opening the Deploy drawer on a `/edit/u/{user}/draft_{uuid}` app fired `AppService.getPublicSecretOfApp` immediately because the gating effect only checked `appPath != ''` + `savedApp`. The `/secret_of/{path}` route plain-SELECTs `app.id`, so a draft-only path 404'd with "App not found at name …" and the public-URL ClipboardPanel spun forever waiting on `secretUrl`. Thread the existing `newApp` signal (already on `AppEditorHeader` / `RawAppEditorHeader`) into `AppEditorHeaderDeploy`, gate the fetch behind `!newApp`, and render the existing "Deploy this app once to get the public secret URL" placeholder instead of the spinner for draft-only items. * fix(drafts): disable Diff button on draft-only items across the 4 editors Diff has no baseline to compare against on draft-only items — the button used to be gated by the pre-PR `/add` route's own state, but the `/add → /edit` redirect landed everything under the regular `/edit` page where the gate was missing. - ScriptBuilder: gate the topbar Diff on `savedScript.no_deployed`; seed `no_deployed: true` on the route's `new_draft` empty NewScript so the gate fires before the first deploy. - FlowBuilder: gate the topbar Diff on `newFlow` (route already sets it from `backendFlow.no_deployed` and the new-draft branch). - AppEditorHeader: gate both the "Diff" dropdown action and the Deploy-drawer's "Diff" button on `newApp`. - RawAppEditorHeader: gate the topbar Diff + the Deploy-drawer's "Diff" button on `newApp`. Each gate also rewrites the tooltip ("Deploy this … once to compare against the deployed version") so the hover state explains why. * fix(drafts): disable the "No login required" toggle on draft-only apps Flipping the toggle called `setPublishState`, which POSTs the new `policy` through `AppService.updateApp` — that handler's `UPDATE app ... RETURNING path` finds nothing on a draft-only path and `not_found_if_none` 404s with "App not found at name …" (apps.rs:1975). Gate the Toggle on `!newApp` too so the user has to deploy once before configuring the publish state. * refactor(drafts): drop dead draft_path field from list responses The draft-only listing branches in scripts/flows/apps computed a `draft_path` from the draft JSON (when the user-typed path differed from the URL's autogenerated `u/{user}/draft_{uuid}`), and `{Script,Flow,App} Row.svelte` preferred it over `path` for the row title. In practice that path is never written: the app, raw-app and flow editors all warn "Deploy the X to make the path change effective" — the rename only lands on deploy, never in the draft. So the field is always None and the home rows always show the autogenerated slot anyway. Drop the field from the three `Listable*` structs, the three draft-only push sites, the three OpenAPI response schemas, and the three frontend row components. Client regenerated. * fix(drafts): seed a friendly name on /flows/add The flow route passed `initialPath={page.params.path ?? ''}` to FlowBuilder, so on the `/flows/add → /flows/edit/u/{user}/draft_{uuid}` redirect the Path widget's `initPath` saw a non-empty `initialPath` and skipped the `reset()` branch that auto-generates the friendly `<random_adj>_flow` name. The other three editors all clear `initialPath` in their `new_draft` branch for exactly this reason. Track `initialPath` as route-owned state (defaults to the URL path) and clear it to '' inside the `new_draft` branch, then bind it through to FlowBuilder so any post-deploy update from the editor still propagates. * feat(drafts): render friendly user-typed path on home list for all 4 kinds Reinstate `draft_path` on `Listable{Script,Flow,App}` so the home rows prefer the user-typed name over the autogenerated `u/{user}/draft_{uuid}` URL slot, with two source rules — one per how each editor wires the Path widget: - Scripts already work: `ScriptBuilder` binds the Path widget directly to `script.path`, so the typed path round-trips through the draft JSON's own `path` field. Backend extracts `v["path"]` when it differs from `row.path`. - Flows / apps / raw apps don't write the typed path into the autosaved value (`Flow.path` is one-way-bound to `$pathStore`; the bare `App` / raw-app value has no `path` field at all). Introduce an explicit `draft_path` field on the draft JSON, written by the editor ONLY when the typed path differs from the deployed/seeded `savedX.path`: - FlowBuilder: $effect on `$pathStore` mutates `flow.draft_path`. - AppEditorHeader: $effect on `newEditedPath` mutates `$app.draft_path`. - RawAppEditorHeader: $effect surfaces `pendingDraftPath` up via the bind chain (RawAppEditor → route); the route's draftHandle.draft spread includes `draft_path` when set. Backend extracts `v["draft_path"]` and `None` when unchanged or after deploy (deploy clears the whole draft, so the field naturally disappears post-deploy without bookkeeping). Flow route's `new_draft` branch now stops sync around the Path widget cascade, with a 700ms scheduled `restartSync` (mirrors the existing scripts/apps/raw_apps stoppers) — the new draft_path mutation lands inside that window so `/flows/add` no longer fires an autosave before the user's first edit. openapi/sqlx regenerated. * fix(drafts): preserve the user-typed draft_path on reload of draft-only items The flow / app / raw-app editors all dropped the saved `draft_path` back to the URL's `u/{user}/draft_{uuid}` slot the moment the user reloaded a draft-only edit page: the route sourced the Path widget's initial path from `page.params.path` instead of the previously-saved `draft_path`, and the first user edit then mirrored that URL path back into the autosaved draft — silently overwriting the friendly name in both the row and the editor. - Flow route: after computing `effectiveFlow`, override `flowInitialPath` with `effectiveFlow.draft_path` when set. - App route: pass `newPath={(app.value as any)?.draft_path ?? app.path}` through to `AppEditor`; AppEditorHeader's `newEditedPath` default now prefers a non-empty `newPath` over the random `<adj>_app` seed (the `newApp && !newPath` branch keeps the `/apps/add` friendly auto-name). - Raw-app route: surface `savedRawAppDraft.draft_path` onto `backendApp` so the `extractRawApp` path seeds `newPath` with the friendly name. Reload + a subsequent edit now leaves `draft_path` intact for all three kinds; verified end-to-end via the `/drafts/get_draft/...` endpoint. * fix(ui): default Modal2 target to 'body' so omitting the prop doesn't throw Modal2 defaulted `target = ''` and forwarded it to `Portal`, which calls `document.querySelector(target)` — an empty selector throws "Failed to execute 'querySelector' on 'Document': The provided selector is empty" and the modal silently fails to mount. That's why `OtherUsersDraftsModal` (and `DraftSyncConflictModal`) never appeared on editors where another user had a draft — both omit the `target` prop. Other Modal2 callers (StorageSettings, CriticalAlert, CustomInstanceDbWizardModal, …) pass an explicit `target="#content"` and were unaffected. Match Portal's own default of `'body'` so omitting the prop is now a no-op rather than a runtime throw. * fix(drafts): Reset to deployed no longer resurrects the draft The toast's "Reset to deployed" callback POSTed `value: null` to the syncer, then handed control to the route's `onResetToDeployed` (which wipes the in-memory handle and reloads the deployed payload via `getDraft: false`). Both writes flowed through the reactive sync effect: the wipe scheduled a delete, the reload scheduled a re-save of the deployed value as the new draft. Coalescing collapsed them and the draft came back — making the "discard" action effectively a no-op. Wrap the whole callback in `UserDraft.stopSync` / `restartSync`. The explicit `value: null` POST still goes through (it's a direct `UserDraftDbSyncer.save` that doesn't depend on the reactive effect), the route's wipe-then-reload mutations advance `lastSerialized` silently under suspension, and the next user edit (after two ticks past the deployed-seed write) is the first real save again. * ui nit * feat(drafts): autosave-indicator popover with Reset-to-deployed action Click the cloud icon → popover with "All changes are saved as a draft on the server. The draft is per-user — your teammates' editors keep their own." When the editor isn't on a draft-only path AND the user has a draft (UserDraft.has returns true), a "Reset to deployed" button mirrors the load-time toast action — stops sync, POSTs `value: null`, runs the route's reload-without-draft callback, restarts sync past two ticks so the deployed-seed write doesn't resurrect the draft. Threaded `onResetToDeployed` from each route down to its builder (ScriptBuilder / FlowBuilder / AppEditorHeader / RawAppEditorHeader) and into the indicator. `draftOnly` is wired from `savedScript.no_deployed` / `newFlow` / `newApp` so the action hides where there's nothing to fall back to. The indicator's trigger now has a hover affordance + matches Portal's default target ('body') via Modal2's earlier fix. * fix(drafts): wait for the fork POST to land before navigating OtherUsersDraftsModal's Fork action called UserDraft.save, which routes through the autosave debouncer (1500ms). The subsequent goto fired within the same tick, so the destination editor's get_draft=true read ran before the POST landed and 404'd — refreshing worked because by then the debounced save had fired. Call UserDraftDbSyncer.save with immediate: true and await it. The syncer cancels any queued debouncer task for the key and resolves the promise only after the POST completes, so the route load can find the forked draft on the first try. * fix(drafts): conflict detection — keep last_sync map tab-local instead of in localStorage Two tabs editing the same draft both load with last_sync = T0. Tab-1 saves; the server accepts, returns T1, and the syncer wrote T1 into localStorage. Tab-2 then tries to save: it reads the SHARED localStorage map, sees T1 instead of its own baseline T0, sends last_sync = T1, and the backend's WHERE clause (`created_at <= last_sync`) is true → tab-2 clobbers tab-1's edit without ever seeing a conflict. Move the map to tab-local memory (`new Map<string, …>`). Reload of the tab now starts with an empty map; that's fine because the editor's load path calls `recordRemoteSync(query, draft_saved_at)` right after `get_draft=true` returns, reseeding from the authoritative server timestamp before any user edit could fire a save. * fix(drafts): OtherUsersDraftsModal — close on Fork, don't leak clicks through nested JSON Two bugs in the per-editor "another user has a draft" banner: - Fork landed the immediate save but didn't close the banner before navigating. Svelte hadn't torn down the previous route's components by the time goto returned, so the banner lingered on top of the destination editor. Comment the explicit isOpen=false on the happy path so it's clear it MUST run before goto. - Clicking anywhere on the screen while the View JSON drilldown was open closed the underlying banner too. Modal2's clickOutside action fired on every Modal2 instance — both the JSON modal and the underlying banner — because both attach their own listener at the document level. Add `closeOnOutsideClick` opt-out on Modal2 and pass `closeOnOutsideClick={!jsonOpen}` to the outer modal so clicks outside the JSON drilldown only close the drilldown. Drive-by: Modal2's keydown handler now ignores Escape when its own isOpen is false (was a no-op closer that would still preventDefault on every key press, swallowing key events for any siblings). * fix(drafts): conflict modal wording — drafts are user-scoped, not teammate-scoped * fix(drafts): defer reset-to-deployed restart until first user interaction Two-tick `restartSync` was too aggressive: editor remounts emit a tail of cascading writes (Monaco setValue acks, schema re-infer, UI Builder iframe handshakes, schedule-config recomputes, …) that land well after two ticks and would clobber the just-deleted draft with an upsert of the deployed value — making "Reset to deployed" a no-op in practice, the user kept seeing the draft come back. Centralise the suspension lifecycle in a new `runResetToDeployed` helper. It stopSyncs around the reset, POSTs the explicit delete, runs the route's wipe-and-reload, and then arms a one-shot listener on document keydown / input / pointerdown that restartSyncs on the user's next real interaction. A 5-second fallback re-arms sync if the user walks away without touching the editor, so suspensions don't leak. Use it from both the load-time toast (`notifyDraftLoaded`) and the autosave-indicator popover so the two stay in sync — fixes both entry points. * indicator ui nits * fix(drafts): split tab-switch and unload flushes — kill self-conflict on visibility change The single keepalive flush bound to both `visibilitychange → hidden` and `pagehide` self-conflicted on tab switch: visibilitychange fires on every tab/app switch with the page still alive, the keepalive POST advanced the server's `created_at` to a fresh `now()`, the client discarded the response (no listener), the local `lastSync` stayed at the old value, and the next foreground autosave sent that stale timestamp → server saw `created_at > last_sync` → conflict modal for the user's own background-tab write. A still-pending debouncer task made it worse: it fired a second runner POST after the keepalive with the same stale `last_sync`, the second self-conflicted too. Split into two paths: - `visibilitychange → hidden` → `flushOnVisibilityHidden`: route through the normal runner pipeline. The page is alive, so the response can land and `setLastSync` keeps the baseline current. Call `debouncer.cancel(key)` first so a queued keystroke can't double-fire with the same stale `last_sync`. - `pagehide` → `flushOnPageHide`: keep the `keepalive: true` raw fetch for the genuinely-going-away case (the JS context is torn down, the response is necessarily discarded). Same `debouncer.cancel(key)` guard. On the next mount, the route's `recordRemoteSync(query, draft_saved_at)` reseeds `lastSync` from authoritative server state before any user edit can fire a save. * fix(drafts): drop the visibilitychange flush — debouncer keeps running on hidden tabs Tab switching just hides the page; the JS context survives and the debouncer's `setTimeout` keeps counting down. When it fires, the runner POSTs normally and the server's response updates `lastSync`. There's nothing left for a visibilitychange-driven flush to do that the ordinary pipeline doesn't already handle, and adding one only creates extra POSTs to reason about. `pagehide` remains the single trigger for the keepalive flush — that's the case where the JS context is actually being torn down and the runner's pending fetch would otherwise be killed mid-flight. * nit * refactor(drafts): drop LS-era pipeline; backend is canonical on load The PR's iteration left behind a meta/staleness pipeline carried over from the localStorage era — per-rev tracking, a LocalDraftStaleModal, a 'Restored from local storage' toast, and a localDraft-vs-backend comparison branch in every editor loader. With drafts now living in the DB and the optimistic-concurrency lastSync check handling divergence, that whole stack is dead weight. Worse, the comparison branch caused 'Load from server' in the conflict modal to do nothing: the loader preferred the in-memory cell over the backend, so the user-clicked 'load from server' just re-displayed the local edits AND fired two confusing toasts (Restored from local storage + Loaded your saved draft). The rip: * userDraft.svelte.ts: drop UserDraftMeta, StoredDraft.meta, checkStaleness, UserDraftStalenessCause, normalizeForCompare, localDraftDiffers, saveMeta, getMeta, setDraftAndMeta, setMeta, handle.meta/setDraftAndMeta/setMeta, force option. Handle is now just { draft }. * userDraftToast.ts: drop notifyRestoredFromLocal + RestoreFromLocalActions. Update copy. * LocalDraftStaleModal.svelte: deleted. * AppEditor.svelte: drop initialRevs prop and the firstMirror wipe-then-restore dance (it existed only to consume the meta-mismatch skip slot). * All 4 editor routes: backend is canonical on load — the in-memory cell is overwritten with the deployed+draft overlay, the syncer's seed guard swallows the first write so we don't POST it back. * VariableEditor / ResourceEditor: drop the staleness pipeline + rev bookkeeping; backend wins on open. * useTriggerDraftSync.svelte.ts: inline the JSON-normalize + deepEqual utility as a private cfgDiffers helper (kept for the form-vs-deployed dirty check, which is a genuine semantic compare, not LS legacy). * copilot core.ts / userDraftAdapter.ts: drop meta argument from saveAppDraft, loadAppDraftValue, write*Draft. Test assertions on getMeta dropped. Net: -22 typecheck errors, fewer moving parts, conflict modal works. EOF ) * refactor(drafts): remove dead endpoints + UserDraftDbSyncer.getLastSync The list_drafts and get_draft (own) routes were added during PR iteration and never wired up to any frontend caller — the editor overlay path uses the per-kind get-by-path getDraft query parameter, and the home page lists drafts via the per-kind list endpoints, not via /drafts. Drop both routes (+ sqlx caches + OpenAPI entries). UserDraftDbSyncer.getLastSync was a peep-hole for callers that never materialised — the per-tab lastSync map is only ever read by postSave internally, where the bookkeeping already lives inline. * refactor(drafts): extract DraftEditorModals trailer block The four editor routes (scripts/flows/apps/apps_raw) mounted an identical pair of trailer modals — DraftSyncConflictModal + OtherUsersDraftsModal — wrapped in the same guard chain and {#key path} remount. Lift the markup into one component; routes thread their itemKind, path, editPathFor, and loader callback. Pure markup extraction, no state ownership change. Drops the unused userStore import where the trailer was the only consumer. * refactor(drafts): UserDraft.useReactive — kill array-of-one boilerplate The script + flow routes both wanted a handle that re-keys when the URL path changes. UserDraft.use() can't do that (its opts getter is untracked), so each route hand-rolled the same useMany-array-of-one + proxy idiom: const handles = useMany(() => [{ kind, path: reactive }]) const handle = { get draft() { return handles[0]?.draft }, ... } Add UserDraft.useReactive(getSpec) that internally wraps useMany with a single spec and returns the stable proxy. Callers collapse to one line. * refactor(drafts): unify bootstrap suspension via armRestartOnFirstInteraction The flow and raw-app routes each rolled their own end-of-bootstrap resume: a 700ms setTimeout for flows and a templatePicker watcher with double-tick gating for raw-apps. Both are timing-fragile (the comments admit it) and drift from each other. armRestartOnFirstInteraction already existed in userDraftToast.ts for reset-to-deployed: keydown/input/pointerdown listeners (capture phase) that fire restartSync on the first real user touch, with a 5s belt-and-braces fallback. Export it and use it everywhere we'd previously have picked a magic number. For raw-apps this is a tiny behavioural change: the user's template choice now POSTs immediately (the pointerdown that picks the template also resumes sync, so the picker's onStart write rides the wake-up). Previously the choice only persisted on the user's NEXT edit. That's strictly better — navigating away preserves the choice now. * refactor(drafts): type App.draft_path; drop the as-any cast The audit asked for the three editors to converge on one draft_path injection pattern. For App and Flow, the in-builder $effect-mutates- the-store idiom is wedged into a shape that doesn't natively own the field — App's editor type genuinely has no draft_path so the writer had to cast through `as any`, and consumers downstream did the same. The minimum viable fix: declare draft_path on the local App type (it's already a field on the autosaved JSON). Lifting the writes upward into a route-side merger would mean restructuring the AppEditor mirror $effect and the FlowBuilder pathStore plumbing — larger change for the same shape, deferred to a follow-up. Flow already has the typed cast localised at one site. Will get the OpenAPI-level draft_path field as part of task 47 (drop as-any casts on backend overlay reads). * refactor(drafts): extract makeDraftAddLoad helper Four identical /add/+page.ts files differing only by the edit-route prefix. Lift the redirect into a factory, slim each entry point to two lines. * refactor(drafts): type UserDraftOverlay.other_drafts_users in the OpenAPI The backend response carried other_drafts_users on every get-by-path that supports the draft overlay, but the OpenAPI schema didn't declare the field. Each route had to cast the typed response to `any` to read it (and the sibling draft_saved_at), which obscured the real shape from the type system and rotted the discoverability of the draft surface. Add it to UserDraftOverlay. Frontend casts collapse to plain property reads in the three editor routes. * feat(drafts): list & open draft-only items for variables, resources, schedules, triggers For scripts/flows/apps the list and get-by-path endpoints already surface per-user drafts that have no deployed counterpart — that's what gates the home page from 404'ing on an AI-agent-created draft. Extend the same support to the other UserDraftItemKinds: Backend (list endpoints): - Add include_draft_only to ListVariableQuery, ListResourceQuery, ListScheduleQuery, StandardTriggerQuery (the latter covers the 11 trigger kinds via the generic TriggerCrud). - Append per-user draft rows whose path has no deployed row. Same gate as scripts/flows/apps: non-operators, page 0, no narrowing filters. Synthesis is per-kind: ListableVariable/Resource get field-for-field synthesis; ScheduleLight reads NewSchedule shape; Trigger<T> uses a best-effort JSON merge + serde_json::from_value (rows skipped on deserialize failure rather than failing the list). - Add draft_only: Option<bool> with sqlx(default) to each row type so it serializes as the column is opt-in. Backend (get-by-path endpoints): - get_variable, get_resource, get_schedule, get_trigger<T> fall back to fetch_draft_only when the deployed row is missing and the caller passed get_draft=true. Mirrors scripts/flows/apps. OpenAPI: - Shared IncludeDraftOnly parameter under components/parameters, wired into the 11 trigger list endpoints + listRawApps. Inline declarations on listVariable / listResource / listSchedules / listAzureTriggers. - draft_only field on ListableVariable, ListableResource, Schedule, TriggerExtraProperty. Frontend: - variables, resources, schedules, and the 10 trigger list pages (routes + 9 *_triggers) pass includeDraftOnly: true on the initial fetch and render <DraftBadge draft_only> on synthesized rows. Trigger pages got a sed/perl bulk update — pattern is the same across kinds. * fix(drafts): swap crypto.randomUUID() for the project's randomUUID helper crypto.randomUUID() is gated on a secure origin (HTTPS or localhost). Self-hosted Windmill instances often run on a bare HTTP origin or a LAN IP where the WebCrypto API is unavailable, so the /add redirect would throw before issuing the 307. Use the existing RFC4122 v4 helper in FlowChatManager that the rest of the codebase already imports for this exact reason. * fix(editor): leading-edge fire + max-wait cap on Monaco debounce The Editor debounced `onDidChangeModelContent` purely on the trailing edge — every keystroke rescheduled a 500ms timer, and uninterrupted typing held the bindable `code` prop stale until a pause. Stacked behind our 1.5s autosave debouncer that meant our clock didn't even start ticking until 500ms after the user paused, and the `code` binding never updated mid-burst for downstream consumers (lint, live preview, change listeners). Switch to leading + trailing + max-wait: * First keystroke of a burst fires `updateCode` synchronously, then stamps a wall-clock chain start. * Each subsequent keystroke (re)arms a trailing timer at `min(now + changeTimeout, chainStart + maxChangeTimeout)` — the cap is what makes continuous typing materialize at least once per maxChangeTimeout window instead of indefinitely. * When the trailing fires it resets the chain so the next keystroke after a pause is a fresh leading fire. New prop `maxChangeTimeout` (default 1000ms) sits next to the existing `changeTimeout` (default 500ms). Dispose path clears the chain stamp alongside the timer. * feat(drafts): wire Ctrl/Cmd+S to flush the pending autosave immediately Each builder already had a Ctrl/Cmd+S keybinding routed through a saveDraft() no-op left over from the LS-era — the comment said "persistence happens via the page-level UserDraft autosave" but the shortcut was the user's only way to actually force a save without waiting for the 1.5s debounce. Restore the intent. * UserDraftDbSyncer.flush({ workspace, itemKind, path }) — new method that re-submits whatever's queued in pendingSaveOpts with immediate: true. No-op when nothing's pending. * Editor.svelte.flushPendingChanges() — exposes a synchronous updateCode() with chain reset, so callers can drain Monaco's own trailing debounce before asking the syncer to flush. Without this step a Ctrl+S within ~500ms of typing would POST the pre-burst content. * ScriptBuilder.saveDraft() — editor?.flushPendingChanges() → await tick() → UserDraftDbSyncer.flush(). Toast on result. * FlowBuilder.saveDraft() — no direct Monaco ref (flows have many per-module editors); just flushes the syncer. Editor.svelte's new 1s max-wait cap means at most the last <1s of typing in a module Monaco won't be in this POST; it follows in the next autosave round. * RawAppEditor.handleKeydown — adds a 's' case that flushes before the focus guard, so the shortcut fires regardless of where focus is in the editor pane. * fix(drafts): low-code apps — drop spurious autosave on /edit + remount on Load from server Two bugs in low-code app editor (raw apps use a separate code path): 1. Every /edit visit looked like an autosave because loadApp() called UserDraft.discard('app', path, undefined). The comment claimed "this load doesn't POST" but discard always POSTs value: null server-side — that surfaced as a DELETE-my-draft on every page load AND a flash in the AutosaveIndicator. The discard was originally intended to wipe the in-memory cell so AppEditor remounts "fresh". But the path-change $effect upstream already sets app = undefined before each loadApp, which unmounts AppEditor and releases the handle's entry — so a remount via app = backendApp naturally starts with an empty handle. Drop the discard. 2. The conflict modal's "Load from server" called loadApp() but didn't remount AppEditor. Since AppEditor's stateApp is captured once at mount and doesn't react to prop changes, the editor kept showing the conflicting local edits even after a successful reload. Wrap the onLoadFromServer to await loadApp() then bump redraw to force a fresh mount. * feat(drafts): home-page Draft badge — show user-initial circles, drop the '+' The home-page Draft badge previously showed '+Draft' as a flat label. Add per-user awareness: up to 3 user-initial circles render to the left of the label, ordered alphabetically; with 4+ users we collapse to the first 2 + a '+N' overflow circle so rows stay compact. Backend: * New `DraftUserRef { username: Option<String> }` in windmill-types::user_drafts, re-exported from windmill-common so the list endpoints in scripts/flows/apps crates share one import path (windmill-types/windmill-common can't be reordered without a cycle). * ListableScript / ListableFlow / ListableApp gain a `draft_users: Option<sqlx::types::Json<Vec<DraftUserRef>>>` field. The list SQL adds a per-row subquery `SELECT json_agg(...) FROM draft d LEFT JOIN usr u ...` that aggregates the workspace users with a per-user draft at this path. NULL (no drafts) decodes to None; LEFT JOIN against `usr` lets orphaned drafts (user removed from workspace) still surface with username = None. * Synthesized draft-only rows set draft_users to a single-element vector with the authed user (those rows come from `email = $2`). OpenAPI: `draft_users` added to listScripts / listFlows / ListableApp response shapes as an array of `{ username }` with nullable username. Frontend DraftBadge: * Accepts `draft_users: { username?: string | null }[]`. Renders up to MAX_CIRCLES (3) initial circles; at 4+ users renders first 2 + a gray '+N' overflow circle. * Initials: 'john.doe'/'john_doe' → 'JD', 'alice' → 'AL', the legacy NULL-email row → '?'. * Color picked deterministically from a 6-entry palette so the same user gets the same circle color across rows. * Label is now just 'Draft' (dropped the '+'). 'Draft only' is unchanged. * Tooltip lists every user in full. ScriptRow / FlowRow / AppRow thread `draft_users` through their prop types and pass it to DraftBadge. * fix(drafts): suppress 'You have unsaved changes' banner when deployed baseline is null A brand-new variable/resource/trigger (no deployed row yet) has `getDeployed() == null`, but the caller's `show` prop is computed off `current != deployed` which is trivially true while the user types. Result: the banner appeared with 'Show diff' (no-op — the drawer early-returns on null deployed) and a 'Discard' that's semantically backwards (there's nothing to revert to). Gate `show` internally on `getDeployed() != null`. The check sits in the banner rather than each caller because every caller would otherwise need the same boilerplate guard. * fix(drafts): hide LocalDraftBanner when deployed and current match the DiffDrawer's compare Earlier I gated the banner on `getDeployed() != null`, but the user still saw it fire on entries where 'Show diff' opens to 'No changes detected'. That means `show` (the caller's coarse dirty check) flagged a difference the DiffDrawer treats as a no-op — typically toggle defaults (`false ↔ undefined`), removed empty arrays, or key-ordering noise that `cleanValueProperties + orderedYamlStringify` collapses. Replicate the drawer's comparison inside the banner: stringify both sides through the same pipeline and only render when the keys differ. A single `diffKey()` helper keeps the logic local; the catch-and-empty fallback survives a non-serializable side rather than throwing. * ui(drafts): nest user-initial circles inside the Draft badge Previously the circles sat alongside the Badge in a parent flex container; the result read as two separate UI elements. The Badge component already exposes its children as a snippet rendered inside its own flex row, so moving the circles into it makes them feel like part of the same chip. Knock-on tweaks: shrunk the circles from h-4/w-4 to h-3.5/w-3.5 so the badge stays compact, and tinted each circle's ring with the badge's indigo palette (instead of plain white) so the overlap reads as a deliberate stack rather than dots floating on top of the chip. * feat(drafts): drop the authed user's circle, mark own drafts with a '*' suffix Three tweaks to the home-page Draft badge: 1. Filter the authed user out of `draft_users` before rendering circles. The row already signals 'this user has a draft' via the asterisk (below), so a circle for them would be redundant noise. New `currentUsername` prop on DraftBadge — pass `$userStore?.username` from each row. The tooltip still lists every user (with `(you)` next to the authed one) so the full picture is one hover away. 2. The badge already showed whenever `is_draft || draft_users.length > 0` (per-user OR any-user). Spelled the rationale out in a comment — no logic change. 3. Append '*' to the displayed summary when `is_draft` is true. Falls back to `draft_path`/`path` when summary is empty so the marker never decorates an empty string. Threaded the same expression into ScriptRow / FlowRow / AppRow. Slice/overflow math now keys on the post-filter `otherUsers` list, so dropping the authed user doesn't silently shrink the visible count (e.g. 3 users incl. self → 2 circles, not 1 circle + a '+1' bubble). * feat(drafts): clone per-user drafts when forking a workspace `clone_workspace_data` clones every other workspace-scoped table on fork creation (resources, variables, scripts, flows, apps, raw apps, triggers, schedules) but quietly dropped the `draft` table. With per-user drafts that meant any open editor in the parent lost its pending edits the moment a fork was created — surprising and inconsistent with how forks treat the deployed surface. New `clone_drafts` mirrors the existing clone helpers: a single INSERT...SELECT into the target workspace, preserving `path`, `typ`, `value`, `created_at`, and `email`. The `email` FK targets `password.email` which is instance-scoped so it carries across workspaces without remap. `created_at` is preserved on purpose so the per-tab `last_sync` baseline lines up with the parent's timeline — otherwise the fork's next autosave would race a stale `last_sync` and trip the conflict modal on every cloned draft. Plain INSERT (not UPSERT) is safe because the fork target is empty at create time; no conflict against the partial unique indexes (`draft_pkey_with_user` / `draft_pkey_legacy`). The synthetic BIGSERIAL `id` PK is regenerated by the default so it stays out of the column list. * ui(drafts): pin the authed user to the first circle instead of hiding them Previously the authed user was filtered out of the circle row entirely on the theory that the row's '*' suffix already signalled 'this user has a draft'. New requirement: they should always lead the circle row when they have a draft so the visual half of the signal lines up across rows (consistent leading-slot identity, easy scan). Switch from a filter to a sort: `orderedUsers` finds the authed user in `draft_users` and splices them to index 0; everyone else keeps the backend's alphabetical order behind. Slice/overflow math now keys on `orderedUsers`, which guarantees the authed user never falls into the '+N' bubble — they're at position 0 and the slice keeps the head. The popover's '(you)' annotation moves to the circle's title attr too, so hovering the leading circle confirms the identity. * feat(drafts): drop draft_only column from script/flow/app Drafts now live in the `draft` table exclusively — `draft_only` stubs in script/flow/app are redundant. Migration `INSERT INTO draft ... ON CONFLICT (workspace_id, path, typ) WHERE email IS NULL DO NOTHING` so real per-user drafts already at the same path are preserved; only rare stubs that lost their draft get a synthesised workspace-level row. Stubs are then deleted (FKs cascade to *_version) and the column is dropped. List endpoints keep a synthesised `draft_only: true` on rows sourced from the draft table itself (sqlx default on the struct field). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ui(drafts): surface draft state in AutosaveIndicator instead of toast+auto-modal The "Loaded your saved draft" toast and the auto-opening OtherUsersDraftsModal both surprised users on every editor mount. Move both signals into the AutosaveIndicator label: "Loaded from draft" or "Others are working on this {kind}" (priority) sits where Saving/Saved do, with a one-shot light-green flash behind the indicator that fades to transparent. Saving/Saved still win when they fire. The popover gains a "See others' drafts" button that flips the modal open on demand; the modal itself is now externally controlled via a bindable \`isOpen\` threaded through DraftEditorModals. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ui(drafts): per-user View JSON / Fork actions in DraftBadge popover Hover popover used to be a plain text list of usernames. Now each row gets a colored circle icon + name + "(you)" for the authed user, and every OTHER user's row carries View JSON / Fork buttons mirroring the OtherUsersDraftsModal. For draft-only entries owned solely by the authed user, the popover ends with "Only you can see this {kind}" so the row's privacy is obvious. ScriptRow / FlowRow / AppRow thread workspace + itemKind + path + editPathFor through; AppRow switches between app / raw_app on app.raw_app. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * nit * fix(drafts): clone only the forker's per-user drafts on workspace fork clone_drafts copied every user's drafts, but only the forker gets added to the fork's usr table. Drafts owned by absent users LEFT-JOIN to NULL in the home page's draft_users aggregate, surfacing as multiple legacy-style rows at one path and crashing the popover with each_key_duplicate. Filter the clone to email = forker OR email IS NULL, and key the popover's #each by index defensively so future legacy collisions can't crash the page either. Also re-adds `draft_only: None` to NewScript/CreateFlowBody literals in tests — the auto-generated windmill-api-client still carries the field and the previous commit dropped them too aggressively. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): always populate other_drafts_users in maybe_overlay_draft Reset-to-deployed reloads the deployed payload with get_draft=false, which made the backend return other_drafts_users=[]. The route then reassigned otherDraftsUsers to the empty list, dropping the count to 0 and hiding "See others' drafts" in the AutosaveIndicator popover — but the other users' drafts hadn't actually gone anywhere. Fetch the list independently of get_draft so the popover stays accurate across reset reloads. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(drafts): alert user when their draft is older than the latest deploy Open a modal on editor mount when the per-user draft was saved before the latest deploy at the same path — i.e. a teammate deployed a new version while this user's draft was sitting. Two choices: discard the stale draft and pick up the deploy, or keep editing the older draft. DraftEditorModals computes the staleness from the timestamps each route threads in (script.created_at, flow.edited_at, app_version.created_at) and the "Load latest deploy" callback reuses the route's existing reset-to-deployed logic. Wired for script / flow / app / raw_app editors; trigger / resource / variable drawer editors follow a different pattern and aren't covered here. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): deploy only wipes the deployer's draft, not everyone else's Script / flow / app deploys ran an unconditional DELETE on every draft at the path, so a teammate's deploy silently destroyed any other user's pending draft. After the wipe, the other user's tab kept auto-saving — re-creating the row at a NOW timestamp newer than the deploy — and StaleDraftModal never fired because draft_saved_at had been bumped past the deploy. Filter the DELETE to email = deployer (plus the legacy NULL row), so other users' drafts persist and the stale-draft prompt actually fires on their next reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): surface save failures in AutosaveIndicator instead of pretending Saved postSave caught network errors with `console.error` and let the runner finish normally. The indicator read the saving → none transition as a successful save and flashed "Saved" even when the request had thrown. Track failed keys in a SvelteMap, expose `'failed'` as a new UserDraftSyncState, render "Save failed" in red with a CloudOff icon. Failure clears on the next successful save for the same key, or when recordRemoteSync seeds a fresh authoritative timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): surface 'Save failed' inside the AutosaveIndicator popover too The popover used to repeat the cheerful "All changes are saved as a draft on the server..." copy even when the inline label said "Save failed", which read as contradictory. Add a red, text-xs warning at the top of the popover body when the sync state is `failed`, explaining that the latest edits didn't reach the server and that editing again retries the save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): surface the actual error message in the AutosaveIndicator popover Replace the generic "your latest changes did not reach the server" copy with the real failure detail. The syncer now stores the extracted message in the failures map (formatSaveError walks body / message / statusText) and exposes it via the state handle's `failureMessage` getter. Popover renders it in red, monospaced, scrollable so a long server traceback doesn't blow out the popover. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): suppress Saving/Saved indicator during a reset-to-deployed discard A `value: null` POST is a discard, not a save, but it ran through the same runner the indicator watched — so resetting to deployed flashed "Saving..." → "Saved", reading as "your draft just landed" while we were actually wiping it. Track in-flight discards in a SvelteSet, expose a distinct `'discarding'` UserDraftSyncState, and the indicator stays quiet for it: no spinner, no label change, and the `discarding → none` transition deliberately skips the "Saved" flash. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Revert "fix(drafts): suppress Saving/Saved indicator during a reset-to-deployed discard" This reverts commit |
||
|
|
075faabf3b |
feat(frontend): surface local drafts in drawer editors with an unsaved-changes banner (#9335)
* feat(frontend): surface local drafts in drawer editors with an unsaved-changes banner Drawer-based editors (the 11 trigger types, plus resource and variable) restore unsaved edits from browser localStorage on open using the same mechanism as flows/scripts, but only showed a transient "Reset to deployed" toast with no way to review the diff. Add a persistent "You have unsaved changes" banner below the drawer header with Show diff / Discard actions, shown whenever the form diverges from the deployed baseline. Replaces the toast for these editors; flows/scripts/apps (full-page) keep their existing toast. - new shared LocalDraftBanner.svelte (Alert-styled bar + DiffDrawer) - DrawerContent: optional `banner` snippet rendered below the header - useTriggerDraftSync: reactive `hasDraft`, `deployed`/`current` getters and `resetToDeployed`; drop the restore toast (banner supersedes it) - wire the banner into all 11 trigger editors + variable; resource lifts its dirty state up to ResourceEditorDrawer via a callback + accessors - fix ScheduleEditorInner.openNew not resetting initialConfig (reused editor instance kept a stale baseline, wrongly flagging a new schedule dirty) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(frontend): address PR review on local-draft drawer banners - LocalDraftBanner: snapshot diff sides at click time so the diff drawer doesn't keep updating as the user types behind it. - VariableEditor / ResourceEditor: scope the banner and its Discard action to the selected workspace; the cross-workspace dirty state stays surfaced by the existing otherDirty Alert. Forward can_write via a new onCanWriteChange callback so the resource banner hides Discard in read-only mode (matching the trigger editors). - useTriggerDraftSync: drop the now-unused path arg from maybeRestore and update all 11 trigger editor call sites. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): deep-clone fallback in UserDraft.discard to avoid baseline aliasing When a caller passed a live $state proxy as `fallback` (Variable/Resource editors handed `initialStates[selected]` to the banner's Discard), the handle's draft cell ended up sharing the same proxy as the caller's baseline. Subsequent form edits mutated both sides in lock-step and the dirty check kept reporting equal, so the banner never reappeared and the Update button stayed disabled until the drawer was reopened. Cloning the fallback inside `discard` (via `snapshotDraftValue`) gives the handle a fresh tree and decouples the two reactive graphs. Trigger editors already cloned at their call site (resetToDeployed); this just makes the API self-contained for all callers. Also switch the variable form's "Audit log for each access" alert from warning to info — it's informational, not a warning. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): honor disabled prop in LocalDraftBanner's diff drawer The banner's `disabled` prop hid the inline Discard button but the diff drawer's "Discard changes" action was still wired unconditionally, so a read-only user could bypass the hidden inline action via Show diff. Gate the diff-drawer button on the same flag so both surfaces agree. Flagged by cubic and Codex on PR #9335. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0f7dd86e5c |
feat: persistent in-editor drafts via UserDraft (#9121)
* refactor(frontend): remove localStorage-backed autosave drafts
Strip the per-editor localStorage autosave for flows, apps and raw apps,
along with the associated restore toasts and diff actions, so we can
replace them with a unified UserDraft service in a follow-up. The
backend DraftService (DB-backed drafts) is untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): add UserDraft service for per-workspace local drafts
Introduces UserDraft, a key-value store keyed by
`{workspace}/{itemKind}/{path}` and backed by localStorage. Supports
save/get/remove plus a reactive use() handle so multiple component
instances observing the same draft stay in sync via a shared $state
loaded through useLocalStorageValue. Designed to host drafts for
scripts, flows, apps, raw apps, resources, variables, and all trigger
kinds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* tests
* nit schedule_ prefix
* feat(frontend): persist deep mutations in useLocalStorageValue
Track the serialized value alongside the $state and add an $effect that
deep-reads it (via readFieldsRecursively). When a deep mutation produces
a serialization that differs from the last persisted blob, write it to
localStorage. The setter keeps writing synchronously so callers reading
localStorage right after assignment still see the new value; the effect
no-ops on those because lastSerialized was already updated by the setter.
Undefined values are persisted as a removal.
UserDraft no longer needs its own removeItem workarounds for undefined
values — useLocalStorageValue handles that uniformly now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): add defaultValue + empty-path handling to UserDraft
UserDraft.use() accepts an opts.defaultValue used when no localStorage
entry exists yet. It is not persisted on first read — only an actual
mutation writes through.
Empty paths (new items) bypass localStorage entirely. The entry still
lives in the in-memory Map so multiple components on the same /add page
share state, but save/get/remove/use never read or write localStorage
with an empty path. Once the item is saved and the route navigates to
its new URL, a fresh use() on the non-empty path takes over.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): wire script editor to UserDraft
The script editor's top-level state now lives in UserDraft.use(), keyed
on the route's path (page.params.path on /scripts/edit, '' on /scripts/add).
Deep edits inside ScriptBuilder persist automatically; deploy and draft
restore now call UserDraft.remove to clear the local autosave alongside
the backend draft.
Replaces the URL-hash autosave that ScriptBuilder used to write via
replaceStateFn — that prop is now gone, the encodeScriptState debounce
is gone, and Triggers no longer takes a saveSessionDraft callback.
Viewing a specific historical hash (?hash=...) is kept draft-free by
passing '' as the path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): wire flow editor to UserDraft
flows/add and flows/edit drive the flow value through a StateStore
adapter backed by UserDraft.use, so every edit auto-persists at
userdraft/w/{ws}/flow/{path} without touching FlowBuilder's internal
.val convention. On returning visits the local autosave wins and a
toast offers a diff against the latest backend draft/deployed version;
on a fresh visit the backend value is written into the handle. Deploy,
save-as-draft rename, restore-draft and restore-deployed each call
UserDraft.remove on the route path so the local autosave doesn't
outlive the action.
Adds UserDraft.has() for "is there already a local draft?" detection
in the load path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): wire app editor to UserDraft
AppEditor registers a UserDraft.use<App> handle for its current path
(empty path for /apps/add stays in-memory) and a single $effect
deep-tracks the internal stateApp and forwards every mutation to the
handle. useLocalStorageValue's lastSerialized check then dedupes the
actual localStorage writes per tick, so even fast drag/resize loops
only persist when the JSON output really changes.
/apps/edit overlays a local autosave from UserDraft.get on top of the
backend value when one exists, with the existing "Discard / Show diff"
toast wired to UserDraft.remove. Deploy, save-as-draft, restore-draft
and restore-deployed all call UserDraft.remove on the relevant path,
including the JSON editor save paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): wire raw app editor to UserDraft
/apps_raw/edit owns the canonical raw-app state (files, runnables,
data, summary) in four $state vars; a single $effect deep-tracks them
and forwards the bundle to a UserDraft.use<RawAppDraft> handle so each
mutation tick persists at userdraft/w/{ws}/raw_app/{path} (deduped by
useLocalStorageValue's serialized check). On load the route overlays
the local autosave on top of backend.draft/deployed and offers a
"Discard / Show diff" toast when they diverge; matching local entries
are silently dropped. Deploy, save-as-draft rename, restore-draft and
restore-deployed each call UserDraft.remove on the route path.
/apps_raw/add keeps the same shape (UserDraft.use with empty path)
so the draft is in-memory only and we drop it explicitly when the
initial save creates the real path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): wire resource editor to UserDraft
ResourceEditor registers a UserDraft.use<ResourceState> handle keyed
on the initialPath (empty for new resources, in-memory only). A
$effect deep-tracks the current workspace's edit state and forwards
mutations to the handle; on bootstrap and lazy backend-fetch the
local autosave wins over the backend value when they diverge. After
a successful save() we call UserDraft.remove so the local autosave
doesn't outlive the deploy. Cross-workspace deploys always start from
the live backend value rather than the local draft.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): wire variable editor to UserDraft
VariableEditor persists the current workspace's edit state via
UserDraft.save on every mutation, keyed on editPath ('' for new
variables → in-memory only). Backend fetches now overlay a matching
local autosave when one exists, and initNew() rehydrates from the
in-memory empty-path entry so opening a fresh "Add variable" drawer
keeps any unsaved work from the previous open. After a successful
save we drop the corresponding entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* editor external changes sync
* fix(frontend): don't UserDraft.remove flows while route is still mounted
The /flows/add and /flows/edit routes drive FlowBuilder from a flowStore
whose getter reads flowHandle.draft directly. Calling UserDraft.remove
synchronously before goto() therefore wiped the in-memory entry, made
flowStore.val collapse to emptyFlow(), and tripped
UnsavedConfirmationModal against the just-saved value — even though the
deploy/save-draft itself succeeded.
Drop those explicit removes in onSaveInitial, /add onDeploy, and
/edit onDeploy. The empty-path entry self-cleans on unmount via
onDestroy ref counting; for the non-empty edit path the next visit's
load-time diff will silently overwrite localStorage when the local
autosave matches the deployed value. Restore-draft/restore-deployed
keep their explicit remove because they navigate to the same route
(no modal) and loadFlow immediately rehydrates the handle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "fix(frontend): don't UserDraft.remove flows while route is still mounted"
This reverts commit
|
||
|
|
9c28bbfd69 |
feat(frontend): new path component (#9017)
* stash
* ui nits
* Fix contenteditable feedback look (duplicate typing)
* fix right icon wrong position with placeholder
* user editor in Path editor takes correct width
* nits
* nit
* chore: remove assets-operator changes (moved to separate PR)
These files were mistakenly included in this PR and belong in a dedicated PR
("Allow assets page to operators").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: remove sidebar assets-operator change (moved to separate PR)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix disabled
* border nit
* Fix disabled styling
* Apply suggestion from @cubic-dev-ai[bot]
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
* nit
* Update frontend/src/lib/components/text_input/TextInput.svelte
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
* Fix disabled tabindex and aria-disabled on contenteditable Select
The useContentEditable branch had an unconditional tabindex="0", keeping
a disabled Select in the tab order, and was missing aria-disabled.
Mirror the TextInput div branch.
Co-authored-by: Diego Imbert <diegoimbert@users.noreply.github.com>
* fix: drop obsolete hideFullPath prop from EditorHeader Path usage
* invalidate autocomplete paths on deploy
* nit pixel
* use Badge in auto complete
* nit prevent default
* fix(autocomplete): don't let stale fetch clobber forced refresh
A non-forced fetchWorkspacePaths() that started before invalidateWorkspacePaths()
could still resolve afterward, overwrite the cache, and clear forceNextFetch —
making the post-deploy refresh a no-op. Only write back from the promise that
is still the current pending one, and only clear the force flag when the
completing fetch was itself forced.
* refactor(path): drop unreachable 'group' branch in owner-kind setter
The Select only offers user/folder, so the 'group' branch was dead. Leave a
short note pointing at validateName which still accepts 'group' for
forward-compat.
* fix(path): respect disableEditing on owner-kind selector
Other path-editor controls disable on (disabled || disableEditing); the
owner-kind Select only checked `disabled`, so read-only users (trigger
editors with !can_write) could still toggle User/Folder and mutate the
bound path. Reuse the existing nameDisabled flag.
* Revert "fix(autocomplete): don't let stale fetch clobber forced refresh"
This reverts commit
|
||
|
|
4427a3d37f |
feat: add workspace-specific flag for resources and variables (#8836)
* feat: add workspace-specific flag for resources and variables Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove set_ws_specific endpoint and fix rust-client compilation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: fall back to workspace name for ws_specific file naming When wsNameForFiles is not set (no wmill.yaml workspace config), ws_specific items would not get workspace-suffixed filenames during pull. Now falls back to workspace.name/workspaceId. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use workspace ID instead of CLI name for ws_specific file naming Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pass workspace ID fallback to elementsToMap for ws_specific push Without this, workspace-specific files (e.g., a.admins.resource.yaml) were not recognized during push when no wmill.yaml or git branch was available, causing spurious deletions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ui nits * nit * Fix variable edit when only editing ws_specific * mark_linked_variables_ws_specific * Helper label * Support json format alongside yaml * Fix file naming push/pull asymetry & ws_specific orphans * Revert all CLI diffs * CLI now appends the remote ws_specific list to the local specificItems * UI for Env switcher * Refactor Resource/Variable editors to use dumb component * Refactor side effects * Editor works with multi workspaces * Fix can_save * Fix As JSON * nit * UI nits * list_ws_specific_versions as pl sql function to avoid round trips * UI Nits * Per-workspace version read-only check * fix: reset session context in list_ws_specific_versions to prevent RLS leakage The function calls set_session_context() in a loop. Although SET LOCAL is transaction-scoped (so settings revert at autocommit), defending against the function being invoked inside a longer outer transaction: - wrap the loop in a sub-block with EXCEPTION WHEN OTHERS that resets the session to a deny-default (windmill_user, empty session.* GUCs) before re-raising, - on the happy path, reset to the same deny-default at the end of the function. * feat: audit auto-marked ws_specific variables When a resource is saved as ws_specific, every variable referenced via $var: inside its value is auto-INSERTed into ws_specific. Previously this happened silently. Now: - mark_linked_variables_ws_specific takes the authed user, - the INSERT uses RETURNING path so we know exactly which variables were freshly flipped (not the ones already ws_specific), - each newly flipped variable gets a 'variables.set_ws_specific' audit entry pointing at the resource that triggered it. * perf: skip mark_linked_variables_ws_specific when nothing relevant changed update_resource was calling mark_linked_variables_ws_specific on every save when the resource was ws_specific, even on a description-only or label-only edit. Gate the call on `ns.value.is_some() || ns.ws_specific == Some(true)` so we only re-mark when the $var: refs could actually have changed or ws_specific was freshly enabled. * docs: explain asymmetric ws_specific toggle in resource tooltip Enabling the resource's 'Workspace specific' toggle silently marks every variable referenced via $var: inside the value as ws_specific, but disabling it does not un-mark those variables (they may be referenced by other resources). Surface this in the tooltip so users know what to expect. * fix: surface non-404 errors when fetching ws_specific items in CLI sync mergeWsSpecificFromServer was catching every error from listWsSpecific and logging it at debug. That's correct for old servers without the endpoint (404), but a 401/403/network failure would silently produce an incomplete sync. Now distinguish 404 (debug, expected) from everything else (warn with status + message) so users notice when the merge fails for real reasons. * perf: collapse compare_two_variables presence checks into one round-trip The early-return path was issuing four sequential EXISTS queries (ws_specific × {source, fork}, variable × {source, fork}). Combine them into a single SELECT so the per-variable diff cost drops ~4x. * sqlx prepare * docs: clarify has_sql_updates invariant in update_variable The else branch of the npath resolution is only reachable for non-rename edits (labels-only, ws_specific-only) because ns.path being Some always forces has_sql_updates=true at the top of the function. Add a debug_assert and a comment explaining the invariant so a future change that decouples ns.path from has_sql_updates trips immediately. Also use `path` directly instead of unwrap_or_default-ing ns.path, since we know it's None here. * chore: drop redundant ws_specific type augmentations ListableResource and ListableVariable from $lib/gen now include `ws_specific?: boolean` after the openapi.yaml additions in this branch were regenerated. The intersection types in resources/+page and variables/+page were duplicating the field — drop them. * Put WsSpecificVersions toggle in top drawer bar * nit size * feat: detect local-only ws_specific items on sync push When wmill.yaml lists a resource/variable in specificItems but the remote isn't yet marked ws_specific for that item, sync push silently dropped the flag because: 1. file-content diff alone never noticed (ws_specific is metadata, not YAML body) — push{Resource,Variable} were never called for those items; 2. even when called, isSuperset(local, remote) returned true and the early-return skipped the API call. Now: - mergeWsSpecificFromServer returns the raw server list alongside the merged config so push can compare 'in local' vs 'in server'; - a new computeWsSpecificFlagOnlyPushes helper walks the local file map, finds ws_specific-flagged paths absent from the server list, and the push function injects them as synthetic 'edited' changes (same before and after content) so the standard display + apply pipeline picks them up; - push{Resource,Variable} no longer early-return when content matches but the ws_specific flag differs. Pull is unaffected — only the push-side caller of mergeWsSpecific takes the new (merged, serverItems) tuple. * getDeployTo for selected ws * refactor: ws_specific kind handling, support .json files The ws_specific helpers had two warts: 1. computeWsSpecificFlagOnlyPushes hardcoded `.resource.{yaml,json}` / `.variable.{yaml,json}` magic strings, even though the existing getTypeStrFromPath / removeType helpers already do that work and already cover both extensions. 2. isSpecificItem / isItemTypeConfigured only matched `.yaml` paths, so users with opts.json local files got no specificItems coverage at all — patterns from wmill.yaml (and from mergeWsSpecificFromServer) are expressed with `.yaml`, and a `.json` file never matched. Changes: - Replace WS_SPECIFIC_KIND_MAP (a closed enum of resource+variable) with configKeyForItemKind, a generic kind→SpecificItemsConfig key mapping. Triggers fold into 'triggers' via the `_trigger` suffix, so adding a kind to the backend's list_ws_specific_versions doesn't require a CLI change. - mergeWsSpecificFromServer now appends `${item.path}.${item.item_kind}.yaml` through the same helper. - computeWsSpecificFlagOnlyPushes uses getTypeStrFromPath + removeType, gated by configKeyForItemKind. No more magic strings. - isSpecificItem and isItemTypeConfigured normalize trailing `.json` to `.yaml` once at the entry, so a single set of patterns covers both extensions for the same logical item. * refactor: dedicated change type for ws_specific flag-only pushes Previously the sync push code injected a synthetic 'edited' Change with before === after to nudge the apply loop into calling pushResource / pushVariable for ws_specific-flag-only diffs, and a guard inside those two functions skipped the early-return when the flag differed. The contract was implicit and easy to break — any future 'skip identical edits' optimization in the change pipeline would silently drop these pushes. Replace with an explicit Change variant: type WsSpecificFlag = { name: 'ws_specific_flag'; path: string; kind: string; wsSpecific: boolean; }; The push apply loop now has a dedicated branch for it that calls wmill.updateResource / updateVariable with just the ws_specific flag. prettyChanges renders it on its own line. The dry-run JSON output picks it up via the existing change.name / change.path passthrough. The defensive wsSpecificMatches check inside push{Resource,Variable} is no longer needed (sync push doesn't go through them for flag-only diffs) and is reverted. * drop folders * feat(cli): warn on remote ws_specific items missing from local config When 'wmill sync pull' fetches the server's ws_specific list, items the server marks as ws_specific but that aren't matched by the local wmill.yaml's specificItems patterns now produce a warning. The merge already preserves correctness (those items are still treated as ws_specific during this pull), but the user's config drifts from the remote — and a later push from another machine without that config would push the item as non-ws_specific. Surface the drift so the user can update wmill.yaml. Also filter ws_specific_flag changes out before preCheckPermissionedAs (it expects added/edited/deleted only and they have no content payload so on_behalf_of resolution doesn't apply). * fix(cli): scope ws_specific drift warning to items in this pull's changes Previously the warning iterated every ws_specific item the server returned, producing log spam for items unrelated to the current pull (items that exist locally with no change, or items the user has nothing to do with this round). Move the loop after compareDynFSElement and only warn for items whose path appears in the changes list — i.e., items the user is actually pulling right now. * fix: clean up linked-side ws_specific rows on resource/variable delete Three places left orphaned ws_specific rows behind: 1. delete_resource deleted the resource's own ws_specific row and the linked variables, but never the ws_specific 'variable' rows that mark_linked_variables_ws_specific had auto-inserted for those variable paths. 2. delete_variable deleted its own ws_specific row and the linked resource at the same path, but never a ws_specific 'resource' row at that path. 3. delete_resources_bulk didn't even cascade to linked variables, let alone clean up their ws_specific rows. A new resource or variable later created at one of those paths would silently inherit a stale ws_specific flag — list_ws_specific would report it as workspace-specific, workspace diffs would treat it as 'no changes', and CLI sync would skip it. Fix: - delete_resource: DELETE FROM ws_specific WHERE item_kind = 'variable' AND path = ANY(linked_var_paths) before the linked-variable delete. - delete_variable: DELETE FROM ws_specific WHERE item_kind = 'resource' AND path = path before the linked-resource delete. - delete_resources_bulk: collect $var: refs from each bulk-deleted resource (mirror of single delete), then delete ws_specific 'variable' rows AND the variable rows themselves. Brings bulk delete in line with single delete semantics, including the orphan cleanup. * fix: gate list_ws_specific by resource/variable RLS The endpoint queried ws_specific directly under user_db, but ws_specific itself has no per-item RLS — only a workspace-level column. Any workspace member could enumerate every ws_specific path including those in folders they lack read access to (e.g. f/finance/prod_db_creds), revealing path existence that list_resources / list_variables would have hidden. Add EXISTS clauses against resource and variable so the same path-based RLS policies that govern those tables (see_own / see_member / see_extra_perms_user / see_extra_perms_groups / see_folder_extra_perms_user) also gate visibility here. The user transaction already establishes the session context; the joins make the policies apply. * only resources and variables * fix(cli): make workspace-specific path mapping handle .json files isSpecificItem() was extended to normalize .json -> .yaml so .json files could be matched against patterns, but the surrounding helpers remained yaml-only: - toWorkspaceSpecificPath only mapped folder.meta.yaml / settings.yaml / .X.yaml — a foo.resource.json went through unchanged, so the workspace-specific filename was never produced. - fromWorkspaceSpecificPath only matched .yaml extensions — pushing foo.dev.resource.json could not map back to foo.resource.json. - isCurrentWorkspaceFile / isWorkspaceSpecificFile regexes ended in \.yaml$, missing every branch-specific .json file. Replace the literal '.yaml' anchors with '(yaml|json)' alternations, preserve the actual extension on round-trips, and rename the helper buildYamlTypePattern -> buildItemTypePattern (it never had anything extension-specific in it). getFileTypeSuffix now returns the matching suffix for either extension. Changed: - getFileTypeSuffix - toWorkspaceSpecificPath / fromWorkspaceSpecificPath - isCurrentWorkspaceFile / isWorkspaceSpecificFile - isTriggerFile / isScheduleFile isItemTypeConfigured / isSpecificItem don't need touching — their checks run after normalizeJsonToYaml(), which already collapses both extensions to .yaml at the entry. * fix: create_resource?update_if_exists=true honors ws_specific=false The upsert path matched on `unwrap_or(false)`, so an explicit `ws_specific: false` and an absent flag were indistinguishable — both fell through with no DELETE on the existing ws_specific row. Callers trying to clear the flag via PUT-with-update_if_exists silently saw their request ignored. Mirror update_resource's three-way handling: Some(true) -> INSERT (+ mark linked variables) Some(false) -> DELETE (only when update_if_exists, since a pure create has no existing row anyway) None -> leave the existing flag alone create_variable doesn't have an upsert path (no ON CONFLICT), so the same bug doesn't apply there. * sqlx prepare * test: cover ws_specific cleanup, RLS filtering, upsert clearing, and CLI .json paths Backend (backend/tests/ws_specific.rs + fixture): - test_linked_delete_cleanup: creates a ws_specific resource that references a variable via $var:, deletes the resource, asserts the cross-kind ws_specific row for the auto-marked variable is also removed. Then does the inverse for delete_variable, verifying the ws_specific 'resource' row at the same path is cleaned by variable delete. - test_list_ws_specific_filters_by_rls: admin creates ws_specific items in u/test-user/ and u/test-user-2/; verifies admin sees both via list_ws_specific while a non-admin (test-user-2) only sees their own path — the RLS see_own policy on the joined resource/variable tables hides the other. - test_create_resource_upsert_clears_ws_specific: covers the three-way Option<bool> handling on the upsert path: Some(true) inserts, Some(false) clears the existing row, None leaves it alone. CLI: - specific_items_unit.test.ts: add 14 tests covering toWorkspaceSpecificPath / fromWorkspaceSpecificPath / isWorkspaceSpecificFile / isCurrentWorkspaceFile / isSpecificItem / isItemTypeConfigured for .json files (variable, resource, trigger, schedule, folder.meta, settings). - ws_specific_flag_only_unit.test.ts (new): covers computeWsSpecificFlagOnlyPushes — emits flag-only changes only for resource and variable kinds (the backend's list_ws_specific_versions scope), does not emit for schedules or triggers, returns empty when serverItems is null (older server), respects existing server entries, preserves .json extension on filePath. - Export computeWsSpecificFlagOnlyPushes so it can be unit-tested. * perf: index workspace_settings.deploy_to for the recursive CTE list_ws_specific_versions's recursive CTE probes WHERE ws.deploy_to = r.ws_id every iteration; without an index on workspace_settings.deploy_to each iteration seq-scans the table — at 10M workspaces with the depth cap of 32 that's up to 320M row reads per call. deploy_to is sparse (most workspaces don't deploy anywhere), so a partial index WHERE deploy_to IS NOT NULL stays small while still covering every probe. Tucked into the existing migration since the function and the index ship together. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
c4c9ef5fd7 |
feat: add optional labels to scripts, flows, apps, schedules, triggers (#8609)
* feat: add optional labels to scripts, flows, apps, raw apps, schedules, and triggers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update sqlx cache, make labels optional in openapi, regenerate system prompts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add minimal labels input UI to script, flow, and schedule editors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: reduce gap between summary and labels input
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add labels to script/flow detail pages and summary/path popover
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move labels inside SummaryPathDisplay trigger for clickable area, reduce gap
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: display labels inline to the right of summary, not below
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: increase gap between summary and labels
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add labels to resources/variables, make labels nullable, add home page label filter badges
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add labels to workspace export/import, resources, variables + test coverage
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: make migration idempotent, regenerate sqlx cache after merge
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pass labels in script create and flow create/update API calls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add labels input UI to resource and variable editors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove negative margin from LabelsInput to prevent overlap
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add top and left margin to LabelsInput for better spacing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: reduce left margin on LabelsInput
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: widen label input to w-32
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use inline-flex so LabelsInput doesn't stretch full width
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove flex-wrap so label input stays on same line as badges
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add label filter presets to resources, variables, and schedules search
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use max-w-32 on label input to prevent stretching
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pull labels closer to summary with negative top margin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: increase negative margin to pull labels even closer to summary
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pass labels in schedule create/update API calls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use COALESCE to preserve existing labels when not provided in schedule/flow update
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add labels to CreateResource, EditResource, CreateVariable, EditVariable in OpenAPI spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: display label badges on resource and variable list pages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: display label badges on schedule and all trigger list pages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add folder and label presets to schedules search filter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: apply user_folders_only filter on all workspaces including admins
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add label presets to resources and variables search filters
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: derive folder presets from loaded items, not all workspace folders
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add label query parameter to resource and variable list endpoints in OpenAPI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: display label filter badges inline with folder filters on home page
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Revert "feat: display label filter badges inline with folder filters on home page"
This reverts commit
|
||
|
|
dbec70aedd | internal: instant npm run check | ||
|
|
d474679277 |
nit: Asset node popover readability (#6974)
* nit asset node popover * nit Load secret value btn |
||
|
|
032f0c1f8c | feat: UX/UI full overhaul to meet new design system | ||
|
|
23920aee84 | feat: better graph layout algorithm + migrate to svelte 5 almost everywhere + xyflow 1.0 | ||
|
|
3c99b3fdc7 |
feat: migrate to svelte5 + vite6 (#4813)
* runs on svelte 5 * Line component from svelte-chartjs * Replaced all svelte-chartjs occurrences with custom wrapper * Fix props mistake * Fix illegal table structures * self-closing-tags fix * aria labels * Fixed trivial warnings and errors * @tanstack/svelte-table fix * upgrade to vite 6 * svelte-kit sync before running svelte-check * Remove on:clear which is actually on:removeAll and already handled by on:change * fix worker tags not displaying in Autoscaling * Try to fix svelte-kit sync not working during CI * remove warnings * Fix add flow page crashing * access worldStore before assignment fix * fix infinite recursions in App Editor * Replaced JSON.stringify with proper deepEqual * component mount api changed (no longer classes) * fix ci errors * Fix infinite loops in background runnable panel * factored effect on deep equal logic in onObjChange * fix "Add" not working in AgGrid Table * Replaced legacy component.$set api * Fix multiselect infinite value reaction * Fix flow input fields resetting when opening their edit tab * fix date input resetting when typing year * Remove !p-0 affecting subgrid dotted borders * fix missing debounceTemplate causing hundreds of updates * Fix AgGrid action refreshes and disppearing * resolve getItems generating random ids every rerun * fix cannot access items before init * fix sort lambda arguments being undefined * Revert "Remove !p-0 affecting subgrid dotted borders" This reverts commit c62809bb45d682a48376b071680645ed4e1c601b. * fix input not updating in decision tree editor * Update frontend/src/lib/components/schema/EditableSchemaWrapper.svelte Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Re-added padding affecting subgrid dotted borders (#5479) * remove !p-0 in preset components * removed extra padding on accordion tabs subgrid * Fix non-reactive SchemaForm * dirty fix for the oneOf bug * Fix warnings and update svelte-exmarkdown for svelte 5 * fix dnd not working * don't mount component like objects --------- Co-authored-by: Diego Imbert <diegoimbert@protonmail.com> Co-authored-by: Diego Imbert <70353967+diegoimbert@users.noreply.github.com> Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> |
||
|
|
69fc8a98ae |
feat(frontend): migrate toggle to melt (4/4) (#5329)
* use melt menu in sidebar * stop keyboard navigation for disabled items * use melt menu for FavoriteMenu and WorkspaceMenu * fix popover placement for menuButton * use melt menu for operator menu * fix notification * fix operator menu * Use melt menu in FlowJobsMenu * use melt menu for AppMenu * clean code * clean code * add use clickOutside option to Menu * use pointerdown_outside * use pointerdown_outside # Conflicts: # frontend/src/lib/components/meltComponents/Menu.svelte * use pointerdown in menus * add max-h to app dropdown menu * keep more open in operator menu * add a MenuItem component * clean * nit * nit * clean code * put conditionalMelt as utility function * remove unused Portal * Add debounce effect in operator menu * fix component jumping due to z-index * format pages * migrate dropdown to melt * migrate toggle to melt * migrate popup to melt popover * fix missing toggle item * feat: remove `pip` fallback option for python and ansible (#5186) * refactor!: Remove `pip` fallback option for python and ansible BREAKING CHANGE: pip was deprecated since 1.425.0 (2024-11-15) * fix errors in main.rs * fix tests * remove nsjail for pip * fix imports * fix compilation error * reinforce melt types * fix racing condition issue in closing operator menu * nit * fix id conflix with melt element * nit * clean code * use melt dropdown instead of menubar * prevent modal from closing on click outside button in menu * Apply automatic changes * fix nit * nit * close dropdown when opening a new one * replace MenuV2 with melt Menu (1/4) (#5214) * use melt menu in sidebar * stop keyboard navigation for disabled items * use melt menu for FavoriteMenu and WorkspaceMenu * fix popover placement for menuButton * use melt menu for operator menu * fix notification * fix operator menu * Use melt menu in FlowJobsMenu * use melt menu for AppMenu * clean code * clean code * add use clickOutside option to Menu * use pointerdown_outside * use pointerdown_outside # Conflicts: # frontend/src/lib/components/meltComponents/Menu.svelte * use pointerdown in menus * add max-h to app dropdown menu * keep more open in operator menu * add a MenuItem component * clean * nit * nit * clean code * put conditionalMelt as utility function * remove unused Portal * Add debounce effect in operator menu * fix component jumping due to z-index * feat: remove `pip` fallback option for python and ansible (#5186) * refactor!: Remove `pip` fallback option for python and ansible BREAKING CHANGE: pip was deprecated since 1.425.0 (2024-11-15) * fix errors in main.rs * fix tests * remove nsjail for pip * fix imports * fix compilation error * reinforce melt types * fix racing condition issue in closing operator menu * nit * fix id conflix with melt element * nit * prevent modal from closing on click outside button in menu --------- Co-authored-by: pyranota <92104930+pyranota@users.noreply.github.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev> # Conflicts: # frontend/src/lib/components/meltComponents/MenuItem.svelte # frontend/src/lib/utils.ts * clean * fix z index and render * fix initialize of dropdownmenu after melt migration * feat: add support for | None and Optional in python (#5361) * feat: add support for | None and Optional in python * update python parser package * add local rooting for MenuItem * fix z index * clean * nit * nit * clean code * nit * nit * clean code * reinforce melt types * wip * reiforce instance select types for toggleButton * nit * fix double event * fix selectedTable toggle * fix sqs toggleButton * fix potential issue with binding in toggleGroup * Update frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --------- Co-authored-by: pyranota <92104930+pyranota@users.noreply.github.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev> Co-authored-by: HugoCasa <hugo@casademont.ch> Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> |
||
|
|
806d3ad0e7 | remove lightweightschemaform | ||
|
|
1e48746edc | fix: prevent operators from loading secrets through toggle | ||
|
|
421b1aea6f | prevent operators from loading secrets through toggle | ||
|
|
933f24a3ea | improve autosize + add markdown support for scripts and workflows | ||
|
|
cdc76994f8 |
feat(frontend): Buttons refactor (#2545)
* feat(frontend): wip * feat(frontend): button refactor wip * feat(frontend): button refactor wip * feat(frontend): fix build * feat(frontend): fix build * fix(frontend): fix build * fix(frontend): fix buttons styles * fix(frontend): remove dev file * feat(frontend): clean up * feat(frontend): done * feat(frontend): done * feat(frontend): remove duplicated componnet * feat(frontend): wip * feat(frontend): wip * feat(frontend): forked Flow icons * feat(frontend): remove legacy dropdown * feat(frontend): migrated all + remove dependencies * feat(frontend): clean up |
||
|
|
c4c075d029 |
feat(frontend): App polish (#2397)
* fix(frontend): app polish * wip * fix(frontend): rework schedule drawer * fix(frontend): add Label wrapper for consistency * fix(frontend): fix tab inconstiencies * fix(frontend): make kbd modifier visible + fix spacings * fix(frontend): add spinning animation when assistant is loading * wip * fix(frontend): add inner border * feat(frontend): reorganised metadata fields * feat(frontend): fix merge * feat(frontend): fix build * feat(frontend): fix build * Update CenteredPage.svelte * feat(frontend): fix inner-border dark * wip * feat(frontend): Fix buttons --------- Co-authored-by: Ruben Fiszel <ruben@windmill.dev> |
||
|
|
0224c9b7da | feat: resolve dependencies across relative imports for python | ||
|
|
b39f486c91 |
Dark mode v0 (#1893)
* feat(frontend): wip * wip * wip * wip * wip * add toggle * correctly handle monaco theme * wip * wip * wip * wip * wip * feat(frontend): Dark mode v0 * feat(frontend): Adap AI gen poppup * feat(frontend): Adap script metadata labels * feat(frontend): Fix unreadable texts * feat(frontend): Fix unreadable texts * feat(frontend): Fix unreadable texts * feat(frontend): Fix ressource picker * feat(frontend): Fix unreadable texts * feat(frontend): Fix unreadable texts * feat(frontend): Fix unreadable texts * feat(frontend): Add theme toggle in the login page * feat(frontend): Fix unreadable texts * feat(frontend): Fix language selection * feat(frontend): Fix FlowStatusViewer colors * feat(frontend): Fix divide colors * feat(frontend): Fix flow graph buttons * feat(frontend): add theme toggle in login modal * feat(frontend): small ui fix * feat(frontend): fix graph dark mode toggle |
||
|
|
2a235f789f | object viewer copy path instead of content | ||
|
|
36280cffc8 |
feat: expose a react sdk to integrate windmill into react apps (#1605)
* expose react sdk * expose react sdk * iterate * iterate * iterate * nit * update example * update example * small fixes * update all |
||
|
|
52e7e29a61 | load variable on is_secret change | ||
|
|
11227f07f0 | requiring passing new value when updating is_secret | ||
|
|
4c33daaba0 | rework ownership permissions | ||
|
|
bb860f94a5 | various minor fixes | ||
|
|
c84f92f65c | fix subgrids final | ||
|
|
f10e55584c | fix init edit on variable editor |