WF_ACTIONS_PLAN

Plan: Persisted Undo/Redo and Workflow CHANGELOG by Bridging Frontend Actions to Backend Refactor API

Leverages tests in #21113 - is a more concrete plan for implementing #9166 in lieu of all the infrastructure added in https://github.com/galaxyproject/galaxy/pull/17774. I spent a few hours doing research with Claude to come up with this plan but as with any plan I think it is a little front-end heavy in quality. After Iteration 3 - I’ve got some real questions but I do think if we implemented iteration 2 and 3 we could keep the editor and backend capabilities in sync and then work to come up with a more detailed plan for the integration of the two. The E2E tests outlined in #21113 would be really helpful also in terms of ensuring there are no regressions as we implement the integration.

Goal

Serialize client-side atomic actions to the refactor API, apply them incrementally, and record a durable action journal that:

Important Notes

Scope anchors


Step 0 — Comprehensive Undo/Redo Selenium Test Suite

GitHub Issue: https://github.com/galaxyproject/galaxy/issues/21113

Before implementing any backend changes or serialization, establish comprehensive test coverage for all existing undo/redo functionality in the workflow editor. This serves as both documentation of current behavior and regression prevention.

Summary: Create lib/galaxy_test/selenium/test_workflow_editor_undo_redo.py with ~30 comprehensive tests covering:

Deliverables:

Acceptance Criteria:

See the GitHub issue for detailed test specifications.


Step 1 — Inventory, gaps, and architecture decisions

Before implementing anything, thoroughly research and document the current state, gaps, and required changes.

Step 1.0.1: Create action mapping inventory document

Step 1.0.2: Research RemoveStepAction semantics

Step 1.0.3: Research absolute position support

Step 1.0.4: Research comment storage and serialization

Step 1.0.5: Research step reference strategy

Step 1.0.6: Locate connection action handling

Step 1.0.7: Create comprehensive frontend→backend action mapping

Direct Mappings (✅ Ready to serialize):

Frontend ActionFrontend FileBackend ActionNotes
LazySetLabelActionstepActions.ts:83UpdateStepLabelActionDirect mapping
LazySetOutputLabelActionstepActions.ts:124UpdateOutputLabelActionDirect mapping
LazySetValueAction (name)workflowActions.ts:16UpdateNameActionDirect mapping
LazySetValueAction (annotation)workflowActions.ts:16UpdateAnnotationActionDirect mapping
LazySetValueAction (license)workflowActions.ts:16UpdateLicenseActionDirect mapping
InsertStepActionstepActions.ts:254AddStepActionDirect mapping

Needs Enhancement (🔄 Backend changes required):

Frontend ActionFrontend FileBackend ActionRequired Changes
setPosition / LazyMoveMultipleActionstepActions.ts:598UpdateStepPositionActionAdd position_absolute support
RemoveStepActionstepActions.ts:310RemoveStepActionNEW: Must implement in backend

Comment Actions (❌ All new backend actions needed):

Frontend ActionFrontend FileBackend Action NeededPriority
AddCommentActioncommentActions.ts:33AddCommentActionHigh
DeleteCommentActioncommentActions.ts:47DeleteCommentActionHigh
ChangeColorActioncommentActions.ts:61UpdateCommentColorActionMedium
LazyChangeDataActioncommentActions.ts:139UpdateCommentDataActionHigh
LazyChangePositionActioncommentActions.ts:175UpdateCommentPositionActionMedium
LazyChangeSizeActioncommentActions.ts:186UpdateCommentSizeActionMedium
RemoveAllFreehandCommentsActioncommentActions.ts:229RemoveAllFreehandCommentsActionLow

No Backend Equivalent Needed (⚠️ UI-only actions):

Frontend ActionFrontend FileReason
UpdateStepActionstepActions.ts:167Generic step update; serializer inspects changed keys to emit specific backend actions (e.g. position → UpdateStepPositionAction). Connection changes do NOT flow through this action.
SetDataActionstepActions.ts:229Subclass of UpdateStepAction for tool form diffs. Connection changes do NOT flow through this action.
CopyStepActionstepActions.ts:348Combination of AddStep + copy data
ToggleStepSelectedActionstepActions.ts:383UI state only
AutoLayoutActionstepActions.ts:424Results in position updates
ClearSelectionActionworkflowActions.ts:287UI state only
AddToSelectionActionworkflowActions.ts:344UI state only
RemoveFromSelectionActionworkflowActions.ts:353UI state only
DuplicateSelectionActionworkflowActions.ts:362Combination of multiple adds
DeleteSelectionActionworkflowActions.ts:390Combination of multiple removes
ToggleCommentSelectedActioncommentActions.ts:197UI state only
CopyIntoWorkflowActionworkflowActions.ts:115Combination of multiple adds
LazyMoveMultipleActionworkflowActions.ts:184Results in multiple position updates

Connection Handling: ✅ COMPLETE (Iteration 5)

Step 1.0.8: Define implementation scope

Deliverables for Iteration 1:

Acceptance Criteria for Iteration 1:


Iteration 2 — Backend schema and executor enhancements ✅ COMPLETE

Implement all backend changes needed to support core workflow editing actions. This includes step operations, absolute positions, and all comment operations.

Status: All sub-steps complete. 33 unit tests passing. Integration tests added. API docs updated.

Step 2.1: Implement RemoveStepAction schema

Step 2.2: Implement RemoveStepAction executor

Step 2.3: Add absolute position support to schema

Step 2.4: Implement absolute position support in executor

Step 2.5: Improve step reference resolution robustness ✅ COMPLETE

Improve _find_step to use explicit isinstance checks and support sparse step dicts (gaps left by RemoveStepAction).

Step 2.6: Add Size model and comment action schemas

Step 2.7: Implement comment action executors

Step 2.8: Verify workflow JSON comment persistence

Step 2.9: Add backend integration tests for core actions

Step 2.10: Update API documentation

Deliverables for Iteration 2:

Acceptance Criteria for Iteration 2:

Iteration 3 — Define cross-layer action contract and serializer in the client

Create a pure serialization layer that converts frontend undo/redo actions into backend refactor API actions. This iteration has NO API calls - just type-safe serialization with comprehensive tests.

Step 3.0: Add what field to all SetValueActionHandler instances

Step 3.1: Create serializer module structure and import types

Step 3.2: Remove - types already exist in generated schema

Step 3.3: Implement label change serialization

Step 3.4: Implement output label change serialization

Step 3.5: Implement position change serialization (absolute positions)

Step 3.6: Implement add step serialization

Step 3.7: Implement workflow metadata serialization

Step 3.8: Implement comment action serialization

Step 3.9: Implement remove step serialization

Step 3.10: Implement main serialization dispatcher

Step 3.11: Create comprehensive serializer test suite

Step 3.12: Add serialization documentation

Deliverables for Iteration 3:

Acceptance Criteria for Iteration 3:


Iteration 4 — Persisted action journal and CHANGELOG endpoints

Create database persistence for workflow refactoring actions by extending the existing PUT /refactor endpoint, and add new endpoints for changelog and revert.

Key architectural decisions (from review):

Step 4.1: Design action journal database schema

Step 4.2: Create SQLAlchemy model

Step 4.3: Create database migration

Step 4.4: Implement WorkflowActionJournalManager (thin CRUD)

Step 4.5: Register in DI container

Step 4.6: Extend PUT /refactor with journal support

Step 4.7: Wire journal writing into refactor flow (atomic transaction)

Step 4.8: Implement GET /api/workflows/{id}/changelog endpoint

Step 4.9: Implement POST /api/workflows/{id}/revert endpoint

Step 4.10: Add backend tests for journal persistence

Step 4.11: Add API integration tests

Step 4.12: Add API documentation

Deliverables for Iteration 4:

Acceptance Criteria for Iteration 4:


Iteration 5 — Connection Serialization + Action Coverage Gaps ✅ COMPLETE

Replace anonymous FactoryAction connect/disconnect in terminals.ts with dedicated action classes, add their serializers, and close remaining serialization coverage gaps.

Status: Complete. 2 commits, all tests passing.

Commits:

Step 5.1: Create ConnectStepAction and DisconnectStepAction ✅

Key decision: Callback pattern over reimplementing store operations. BaseInputTerminal.resetMapping() has complex cascade logic (propagates resets to connected output steps via terminalFactory) that can’t be safely replicated outside the Terminal class.

Step 5.2: Update terminals.ts ✅

Step 5.3: Add Connection Serializers ✅

Step 5.4: Add update_report Serializer ✅

Step 5.5: Tests ✅

Serialization Coverage After Iteration 5

CategoryBeforeAfter
Steps (add, remove, label, output label, position)5/55/5
Comments (all 7 types)7/77/7
Connections (connect, disconnect)0/22/2
Metadata (name, annotation, license, creator, report)4/55/5
Move multiple1/11/1
Total serializable17/2020/20

Remaining unserialized actions are UI-only (selection, clear) or macro actions (copy-into, duplicate-selection, delete-selection, auto-layout) that decompose into serializable primitives.


Iteration 5a — Close Serialization Gaps + Refactor-as-Save ✅ COMPLETE

Close all remaining serialization gaps (CopyStep, CopyIntoWorkflow, AutoLayout, readme/report split), fix ID mismatches and async timing, verify data shape fidelity.

Status: All 25 editor action types serialize. Refactor API is primary save mechanism when persistence enabled. All data shape questions resolved. See REFACTOR_AS_SAVE_PLAN.md for full details.

Commits:

What was done:

Outstanding: TIMEOUT_SAFETY_PLAN.md — async timeout safety net (force fallback if timeout fires while still in-flight) + async-aware redo().


Iteration 6 — Frontend integration (opt-in behind feature flag) — MOSTLY COMPLETE

Wire up the serializer to the undo/redo store and create UI for changelog viewing. This is behind a feature flag for gradual rollout.

Core functionality complete (Steps 6.1-6.7, 6.4a). Remaining: timeout safety (6.4b), unsaved indicator (6.9), E2E tests (6.10), admin UI (6.11), docs (6.12).

Steps 6.1-6.4 (Iteration 6a) ✅ COMPLETE — feature flag, API functions, undo/redo store integration, batch-on-save wiring, report markdown action coverage. 64 tests passing.

Steps 6.4a (Refactor-as-Save) ✅ COMPLETE — All 25 action types serialize. Refactor API is sole save path when persistence enabled. Raw PUT fallback only. See REFACTOR_AS_SAVE_PLAN.md.

Steps 6.5-6.7 (Changelog UI) ✅ COMPLETE — Commit 8ef83ad177. ChangelogPanel component, editor integration, revert handler, 12 unit tests. See CHANGELOG_UI_PLAN.md.

Step 6.1: Add feature flag configuration ✅

Step 6.2: API functions ✅

Step 6.3: Integrate serializer with undo/redo store (batch-on-save) ✅

Step 6.4: Implement batch submission on save ✅

Step 6.4a: Refactor API as primary save mechanism ✅ COMPLETE

Refactor API is now the sole save path when persistence enabled. Raw PUT is fallback only.

See REFACTOR_AS_SAVE_PLAN.md for full details (Phases 1-4, all ✅ COMPLETE).

Step 6.4b: Timeout safety + async-aware redo

Plan: TIMEOUT_SAFETY_PLAN.md

Problem: saveViaRefactor waits up to 5s for async serializations. If timeout fires while still in-flight, code proceeds with allActionsSerialized potentially true but missing the async action — partial refactor-save. Also: store redo() has no async detection (safe today since AutoLayout overrides redo synchronously, but latent bug for future async actions).

Fixes:

  1. After Promise.race, check asyncSerializationsInFlight > 0 → force allActionsSerialized = false (triggers raw-save fallback)
  2. Clean up orphaned watch by hoisting unwatch and calling after race
  3. Mirror applyAction’s async detection in redo()
  4. Update UndoRedoAction.redo() return type to void | Promise<void>

Tests: 1 new test for async redo deferral.

Files: Index.vue, undoRedoStore/index.ts, undoRedoAction.ts, undoRedoStore.test.ts

Step 6.5: Create changelog panel component ✅

Step 6.6: Add changelog panel to workflow editor ✅

Step 6.7: Implement revert functionality ✅

Step 6.5-6.7 Tests ✅

Step 6.8: Handle persistence errors gracefully — MOSTLY DONE

Warning toast on persistence failure already exists (Step 6.4). Remaining:

Step 6.9: Add unsaved-changes indicator

Step 6.10: E2E tests for persistence

See E2E_PERSISTENCE_TEST_PLAN.md for full test plan (19 tests across 4 categories):

Deliverables for Iteration 6 — Status:

Acceptance Criteria for Iteration 6 — Partial:


Iteration 7 — Persistent Undo/Redo

Roadmap from “changelog revert” to “persistent undo/redo across sessions”. The journal already stores full action_payloads per save — the data foundation exists.

Phase A: Undo/Redo Survives Saves (Within Session)

Problem: saveViaRefactor()_loadCurrent()resetStores() → clears undo stack. Every save wipes undo history even though the stores already have correct state.

Fix: On the refactor-as-save happy path (all serialized, no fallback), skip _loadCurrent()/resetStores(). Instead:

  1. Update stateStore.version from refactor response
  2. Refresh versions list
  3. Refresh changelog panel
  4. Clear hasChanges flag
  5. Do NOT reset step/comment/connection/undoRedo stores

Files: Index.vue (saveViaRefactor method)

Tests: 3 E2E tests from E2E_PERSISTENCE_TEST_PLAN.md (undo survives save, undo/redo across multiple saves, full cycle across save boundary)

Risk: After refactor-as-save, the server’s workflow version and the client’s in-memory state could drift. Mitigate by verifying the refactor response’s workflow dict matches expectations (or at minimum, trusting that the refactor API applied the same actions the client already applied locally).

Phase B: Save-Point Undo Across Sessions

Concept: On workflow open, populate the undo stack with SavePointAction wrappers backed by journal entries. Ctrl+Z past current session’s changes undoes one whole save batch — effectively a revert integrated into the undo/redo UX.

How it works:

  1. New API endpoint: GET /api/workflows/{id}/journal_entries?limit=N — returns entries with action_payloads (the changelog API currently omits payloads)
  2. On workflow open (when persistence enabled), fetch last N journal entries
  3. For each entry, create a SavePointAction and push onto undoActionStack:
    SavePointAction {
      journalEntry: JournalEntry
      undo() → revertWorkflow(id, entry.workflow_id_before) + reload editor
      redo() → re-apply entry.action_payloads via refactor API + reload editor
    }
  4. User sees: current session actions at top of undo stack, then save-point boundaries from previous sessions below
  5. Ctrl+Z through current session actions works normally (Phase A)
  6. Ctrl+Z past the session boundary → triggers SavePointAction.undo() → revert + reload

UX considerations:

Files:

Phase C: Action-Level Undo Across Sessions (Future / Optional)

Concept: Deserialize individual refactor actions from journal payloads back into UndoRedoAction objects. Every single action from previous sessions individually undoable via Ctrl+Z.

Why this is hard:

Approach (if pursued):

  1. Build a deserializeAction(payload: RefactorAction, stores: StoreRefs) function — inverse of serializeAction()
  2. For each refactor action type, create a lightweight ReplayAction wrapper:
    • undo() = apply inverse via refactor API (e.g. update_step_label → restore fromValue)
    • redo() = re-apply via refactor API
    • No closures needed — the refactor API is the execution mechanism
  3. Store fromValue alongside toValue in journal payloads (currently only toValue is stored for most actions — would need schema extension)
  4. Handle composite actions (CopyIntoWorkflow = multiple add_steps) as atomic groups

Missing data: The journal stores the “forward” action (toValue) but NOT the “inverse” (fromValue). To undo individual actions, we’d need either:

Recommendation: Phase C is high-effort, moderate-value. Phase B (save-point granularity) covers 90% of the use case. Phase C only needed if users demand individual action undo across sessions.


Remaining Steps Summary

Immediate (Iteration 6 completion)

  1. ✅ Steps 6.1-6.7 — done
  2. ⬜ Step 6.4b — timeout safety (TIMEOUT_SAFETY_PLAN.md)
  3. ⬜ Step 6.8 — error handling polish (mostly done, verify edge cases)
  4. ⬜ Step 6.9 — unsaved changes indicator badge

Next: E2E Tests

  1. ⬜ E2E persistence tests — E2E_PERSISTENCE_TEST_PLAN.md (19 tests across 4 categories)
    • Changelog panel tests (entry after save, multiple entries, empty state, revert, revert badge)
    • Action roundtrip tests (label, add/remove step, name/annotation, connection, comment, license, auto-layout)
    • Refactor-as-save tests (single version per save, batch title)

Persistent Undo/Redo Roadmap

  1. ⬜ Phase A — undo/redo survives saves within session
  2. ⬜ Phase B — save-point undo across sessions
  3. ⬜ Phase C — action-level undo across sessions (future/optional)

Future: Production Hardening


Action mapping details (initial)

Frontend → Backend


Concrete file touchpoints

Client (✅ = done, ⬜ = not yet)

Server


Risk management


Test strategy


Resolved questions

  1. Extend existing PUT /refactor or create separate endpoint?Extend PUT /refactor with optional title/source_action_type fields
  2. Store Workflow.id (DB PK) or positional version index in journal?Workflow.id (DB PK) — stable across reverts
  3. Should journal FK point to stored_workflow or workflow?stored_workflow — journal tracks the logical workflow container
  4. How to route LazySetValueAction serialization when what is null?Add what to all handlers — 6 one-line changes in Index.vue
  5. Re-index steps after removal or tolerate gaps?Tolerate gapsorder_index/label refs work fine with sparse step dicts. Fixed add_step to use max(keys)+1 to avoid collision.
  6. Default mode: persist immediately per action, or batch on save?Batch on save — fewer versions, simpler error handling
  7. Shared-with users: can they create journal entries, or owner-only? → Open — depends on Galaxy’s access model (currently ownership-only for writes)
  8. Use database step IDs in refactoring schema?No — the refactoring API is a stateless document transformation. order_index/label only. See “Step References — No Database IDs by Design” in research notes.

Unresolved questions

  1. Batch-on-save: add_step + connect ID predictionPartially resolved. CopyIntoWorkflow uses input_connections on add_step with remapped IDs (avoids separate connect actions). InsertStepAction + later ConnectStepAction in same save batch: the ConnectStepAction serializer uses the step’s order_index from the store (already assigned by frontend). Backend _apply_add_step uses integer order_index = max(keys)+1 which matches frontend assignment. Works in practice; edge cases with concurrent step removal untested.
  2. _iterate_over_step_pairs (execute.py) assumes contiguous indices — needs fixing when fill_defaults/extract_untyped_parameter serialization is added.
  3. Shared-with users: can they create journal entries, or owner-only?
  4. Refactor-as-save: 100% action coverageResolved (Iteration 5a). All 25 action types serialize. saveViaRefactor uses refactor API as sole save when all serialized. See REFACTOR_AS_SAVE_PLAN.md.
  5. Timeout safety: TIMEOUT_SAFETY_PLAN.md — async timeout can leave allActionsSerialized incorrectly true. Planned fix in Step 6.4b.
  6. Frame comment child_steps/child_comments — not remapped in CopyIntoWorkflow serialization. Documented limitation, low impact (frame containment is cosmetic).
  7. Phase A state drift: after skipping _loadCurrent(), how to handle version number? Extract from refactor response?
  8. Phase B undo stack size: how many journal entries to pre-populate? Suggest configurable, default 10-20 save points.
  9. Phase B + Phase A interaction: if undo stack survives saves (A) AND we pre-populate from journal (B), what happens on save? Collapse current session actions into single save-point entry?
  10. Phase C inverse data: storing fromValue in journal doubles payload size. Worth it? Or rely on version-level revert?