EXTRACT_ICJ_PLAN

Workflow Extraction UI — ICJ-Aware Polish

Date: 2026-05-13 Branch: history_notebook_extract (jmchilton/galaxy, currently at efbd2e1156) Predecessor: ICJ_NATIVE_PLAN (this PR’s parent plan; explicitly punted UI to follow-up) Trigger: CI run 25799914411 — 6 Selenium failures in test_workflow_extraction.py traced to the new strict validator vs. legacy UI payload. Related research:

  • vault/research/Workflow Extraction Issues.md
  • vault/research/Component - Workflow Extraction Models.md

Why this exists

ICJ_NATIVE_PLAN declared WorkflowExtractionForm.vue out of scope on the assumption that the form’s existing payload (job_ids=[mapped_job]) would keep working. Commit 17595cefcb made that payload a 400. The interim fix on this branch (4-file patch — bucket mapped jobs into implicit_collection_jobs_ids client-side) restores Selenium green but leaves the UI with no signal that a card represents an implicit collection vs. a plain job, an overloaded loading ref, errors that wipe the form, and no client test coverage of the new bucket.

This plan hardens the UI so the API contract and the UX agree, and so the failure modes (server 400 on validator, in-flight POST, ICJ identity) are honestly surfaced.

Important correction to the initial review: WorkflowSummary.__summarize already collapses an ICJ to its representative job (jobs.items() keys one entry per ICJ). There is no multi-card-per-ICJ desync. Mapped-tool cards already render as one row. The polish gap is visual identity and error/loading discipline, not selection semantics.


Current state to build on

Reuse as-is:

FileReuse
lib/galaxy/workflow/extract.py WorkflowSummary.__summarizealready collapses ICJs to representative job
lib/galaxy/schema/workflows.py WorkflowExtractionJobnow has implicit_collection_jobs_id
lib/galaxy/webapps/galaxy/services/histories.py create_workflow_extraction_summarynow populates ICJ id from job.implicit_collection_jobs_association
client/src/components/History/WorkflowExtractionForm.vue selectedJobBucketspartitions selected tool jobs into job_ids vs deduped implicit_collection_jobs_ids
client/src/components/History/WorkflowExtractionForm.test.ts16 passing vitest tests
client/src/components/History/WorkflowExtraction/WorkflowExtractionCard.vuestep-type meta, badge generation

Rewrite:

FileRewrite scope
lib/galaxy/schema/workflows.pyadd implicit_collection_jobs_size: Optional[int] to WorkflowExtractionJob
lib/galaxy/webapps/galaxy/services/histories.pypopulate the new size from len(icj_assoc.implicit_collection_jobs.jobs)
client/src/components/History/WorkflowExtraction/types.tsdiscriminated union, isMappedTool narrow helper
client/src/components/History/WorkflowExtraction/WorkflowExtractionCard.vue”Mapped × N” badge for ICJ rows; drop STEP_TYPE_META[...] cast
client/src/components/History/WorkflowExtractionForm.vuesplit loading / submitting; restructure error placement; new data-attrs; drop unnecessary (job as ...) cast
client/src/components/History/WorkflowExtractionForm.test.tsfixtures + assertions for mapped bucketing, dedup, mixed, error/loading discipline
client/src/utils/navigation/navigation.ymldata-icj-id, data-step-kind selectors
lib/galaxy_test/selenium/test_workflow_extraction.pynew assertion: mapped cards show “Mapped” badge

Delete:


Target shape

Schema addition

# lib/galaxy/schema/workflows.py
class WorkflowExtractionJob(Model):
    # ... existing fields ...
    implicit_collection_jobs_id: Optional[EncodedDatabaseIdField] = None  # already added
    implicit_collection_jobs_size: Optional[int] = Field(
        None,
        description="Number of constituent jobs in the ICJ (only set when implicit_collection_jobs_id is non-null).",
    )

Card badges

Card kindBadges (in order)
plain toolView Job (if id), tool-version-warning (conditional), “Workflow Step”
mapped toolView Job (representative), tool-version-warning, “Mapped over N items”, “Workflow Step”
input datasetRenamable, “Input Dataset”
input collectionRenamable, “Input Dataset Collection”

Mapped badge uses faLayerGroup, variant info, class unselectable. Label "Mapped over {N} items" when size known; "Mapped" if size missing. title attr explains the row represents the whole ICJ (no tooltip component for v1).

Form refs

const loading = ref(true);           // initial summary fetch only
const submitting = ref(false);       // POST in flight
const errorMessage = ref<string | null>(null);
const warnings = ref<string[]>([]);

Form template structure (semantic, not literal)

<header>
  <breadcrumb/>
  <error-alert v-if="errorMessage"/>            ← inline, never hides list
  <loading-alert v-if="loading"/>
  <actions v-if="!loading && jobsList.length">
    <name-input :disabled="submitting"/>
    <create-button :disabled="submissionDisabled || submitting">
      <FontAwesomeIcon :icon="submitting ? faSpinner : faCheck" :spin="submitting"/>
      {{ submitting ? "Creating..." : "Create Workflow" }}
    </create-button>
  </actions>
  <warnings-row v-if="!loading && jobsList.length"/>
  <empty-alert v-if="!loading && !errorMessage && !jobsList.length"/>
</header>
<list v-if="jobsList.length">
  ...cards...
</list>

Submission errors do not hide jobsList. Loading (initial fetch) still does — there’s nothing else to show.

Note on submitting UX: GButton has no :busy / :loading prop (verified in client/src/components/BaseComponents/GButton.vue). Follow the established pattern from client/src/components/Form/FormGeneric.vue:16-17: :disabled="submitting" plus a faSpinner swap-in via FontAwesomeIcon :spin="submitting". Disable the name input via :disabled="submitting". Cards stay interactive during the POST window — re-toggling them does no harm, and the button is the only path to re-submit.

data-attrs on each card

AttrSourceNotes
data-job-idjob.idexisting; representative-job id for mapped tool rows
data-step-typejob.step_typeexisting; one of tool / input_dataset / input_collection
data-icj-idjob.implicit_collection_jobs_idnew; omitted when null
data-step-kindcomputednew; one of tool / mapped-tool / input-dataset / input-collection

Types

// types.ts
export type WorkflowExtractionToolJob = WorkflowExtractionJob & { step_type: "tool" };
export type WorkflowExtractionInput = WorkflowExtractionJob & {
    step_type: "input_dataset" | "input_collection";
    newName: string;
};
export type WorkflowExtractionRow = WorkflowExtractionToolJob | WorkflowExtractionInput;

export function isWorkflowExtractionInput(row: WorkflowExtractionRow): row is WorkflowExtractionInput;
export function isMappedTool(
    row: WorkflowExtractionRow,
): row is WorkflowExtractionToolJob & { implicit_collection_jobs_id: string };

selectedJobBuckets uses isMappedTool instead of (job as WorkflowExtractionJob).implicit_collection_jobs_id.


Implementation per file

lib/galaxy/schema/workflows.py

lib/galaxy/webapps/galaxy/services/histories.py

_schema.yaml + client/src/api/schema/schema.ts

client/src/components/History/WorkflowExtraction/types.ts

client/src/components/History/WorkflowExtraction/WorkflowExtractionCard.vue

client/src/components/History/WorkflowExtractionForm.vue

client/src/components/History/WorkflowExtractionForm.test.ts

client/src/utils/navigation/navigation.yml

lib/galaxy_test/selenium/test_workflow_extraction.py


Red-to-green test order

  1. Commit 1 — server-side size field. Add implicit_collection_jobs_size to schema. Populate in service. Regenerate _schema.yaml + schema.ts. Existing tests stay green. Optional: API-level assertion that summary jobs for a mapped flow carry a non-null size.
  2. Commit 2 — vitest fixtures + RED tests. Add MAPPED_TOOL_JOB, MAPPED_TOOL_JOB_2. Add the 5 new tests. Mapped-bucketing tests pass immediately (logic exists). The error-keeps-list-visible and submitting-state tests FAIL — current form replaces list on error and on submit.
  3. Commit 3 — form discipline. Split loading/submitting. Restructure template so error alert and list coexist. Disable interactions during submit. Vitest commit-2 RED tests turn GREEN.
  4. Commit 4 — types + card badge. Convert types.ts to discriminated union. Drop the cast in selectedJobBuckets. Add mappedBadge(size) to WorkflowExtractionCard.vue. Run vue-tsc --noEmit (must stay green) + vitest (must stay green).
  5. Commit 5 — data-attrs + Selenium badge assertion. Add data-icj-id / data-step-kind to card rendering. Update navigation.yml. Add the Selenium assertion for the mapped badge. Run targeted Selenium subset locally if possible; otherwise rely on CI.

Run after each commit:


Out of scope (do not pull into this PR)


What this PR fixes downstream

IssueHow
CI run 25799914411 — 6 Selenium failsThe 4-file UI fix on this branch already lands them; commits 1-5 polish on top.
User confusion: “is this card a single job or a map?”Mapped × N badge, data-step-kind attribute.
User loses selections when submit failsError alert no longer replaces the list.
User can’t tell submit is in-flight without losing contextsubmitting state shows on the button only.
Selenium can’t target ICJ-level selectionsdata-icj-id, data-step-kind attrs + navigation.yml entries.
Client test suite silently green despite weak objectContaining assertionExplicit assertions on both job_ids and implicit_collection_jobs_ids.

References (in-repo)


Resolved questions (from initial draft)

Unresolved questions