feat(workflows): trigger-picker UX overhaul (labels, filtering, groups, entity_types)#369
Open
mvkonchits-db wants to merge 6 commits into
Open
feat(workflows): trigger-picker UX overhaul (labels, filtering, groups, entity_types)#369mvkonchits-db wants to merge 6 commits into
mvkonchits-db wants to merge 6 commits into
Conversation
Adds a parametrized test that asserts every TriggerType member keeps its exact wire-format string (e.g. TriggerType.FOR_SUBSCRIBE.value == 'for_subscribe'). Renaming any of these is a breaking change for every stored workflow trigger config in customer databases — pinning catches it at PR time instead of at upgrade time. Also asserts that every enum member is represented in the pin table, so new triggers get vetted before they ship.
The workflow trigger dropdown today shows ~25 trigger types in a flat
list, with for_* and on_* variants for the same action appearing as if
they were duplicates. Authors get confused which to pick, and the
entity_types scope is invisible — workflows end up with entity_types=[]
('fires for everything') by accident.
This commit adds the backend half of the fix: a single read-only
endpoint that returns the full UI catalog the workflow-authoring form
needs. Each entry carries:
- label: user-approved display string
- workflow_type: 'approval' (for_*) or 'process' (everything else),
so the picker can filter by the workflow being authored
- group: lifecycle / request_flow / validation_gates /
system_scheduled, so the picker can render <SelectGroup>s instead
of a flat list
- entity_types: which entity types this trigger CAN fire for, used
to populate a multiselect under the picker
- is_advanced: true for for_approval_response only — gated behind
an explicit 'show advanced' toggle
Authorization mirrors the other workflow read endpoints
(settings/READ_ONLY).
Tests:
- Contract test: every TriggerType is represented exactly once;
shape and value spaces are validated.
- Round-trip test: create → get → update unchanged → get again,
asserting trigger.type is byte-identical at every hop. Covers
every for_* trigger plus one representative from each process
category.
Extends the existing workflow-labels module with TRIGGER_LABELS — the canonical, user-approved display string for every TriggerType. Mirrors the backend _TRIGGER_LABELS dict in workflows_routes.py so the new picker can fall back gracefully if the trigger-types endpoint is unavailable, and so anywhere we render a trigger without an i18n context (the legacy getTriggerTypeLabel needs a TFunction) gets the same strings. getTriggerLabel(value) is the new public helper. Unknown values title-case the raw string for forward-compat with future enum members. Tests parametrize every TriggerType member against its canonical label string, plus a fallback case for unknown triggers.
Replaces the flat ~25-entry trigger dropdown in the workflow designer
with two cooperating components:
TriggerPicker
- Fetches the catalog from GET /api/workflows/trigger-types.
- Filters visible entries by the workflow being authored
(workflow_type === 'approval' shows only for_*; 'process' shows
everything else). Today the dropdown shows both halves, which
reads as duplicates ('on_subscribe' vs 'for_subscribe').
- Groups visible entries with <SelectGroup> headers — Lifecycle
events / Request flow / Validation gates / System & scheduled.
- Hides 'for_approval_response' behind an explicit 'Show advanced
triggers' toggle (off by default). This trigger is a system
hook that typical authors should never pick directly.
- Hover-tooltip on every option surfaces the raw enum value
(e.g. 'for_subscribe') so power users can still grep for it.
- Pure filter/group logic is exported as partitionTriggers() and
unit-tested directly (Radix <Select> is unreliable in jsdom,
so we don't render the full component in tests).
EntityTypeMultiselect
- Rendered under the picker once a trigger is chosen.
- Populated from the chosen trigger's entity_types (from the
catalog). Today this control doesn't exist; workflows save with
entity_types=[] ('fires for everything') by accident — observed
live on FEVM Ontos with 4 on_request_access workflows differing
only in entity_types (access_grant / project / role) that
authors had no way to configure deliberately.
- Auto-prefills when the trigger supports exactly one entity
type, but still renders the (single, checked) row so the
scope choice is visible/auditable.
- For triggers with no supported entity types (scheduled,
for_approval_response), renders a muted explanatory line
instead of an empty box.
workflow-designer.tsx
- Swaps the inline <Select>+Badge grid for the two components
above. Passes the workflow_type field through so the picker
filters correctly when switching between approval and process
authoring. Resets entity_types on trigger change so the
multiselect re-prefills against the new supported set.
- Falls back to the existing SUPPORTED_TRIGGER_ENTITY_MAP if
the trigger-types endpoint hasn't returned yet, so the form
renders something sensible on first paint.
Tests cover the partitioning logic (filter by workflow_type, advanced
toggle, group ordering, empty-group elision) and the multiselect
render/toggle/auto-prefill paths.
Three user-testing follow-ups on top of PR #369: 1. Drop the "Show advanced triggers" Switch. for_approval_response was the only entry it gated, and the toggle confused users. The trigger now lists in the Request flow group like every other approval trigger. The is_advanced field is removed from the GET /api/workflows/trigger-types response and the contract test asserts it is absent. 2. Strip the redundant "(wizard)" suffix from every for_* label (and the "(advanced)" suffix from for_approval_response). After the workflow_type filter is applied the suffix added no information. Backend _TRIGGER_LABELS and frontend TRIGGER_LABELS are updated in lockstep (they must stay byte-identical); the label-table test in workflow-labels.test.ts is updated to match. Validation triggers keep their "(validation)" suffix — not flagged. 3. Pretty-print entity-type values in the multiselect (access_grant -> "Access grant", data_asset_review -> "Data asset review", etc.). The conversion is a pure helper prettyEntityTypeLabel exported alongside the component for testability. The wire format is unchanged — values passed to onChange remain snake_case, so workflow.trigger.entity_types round-trips byte-identically. Tests: - Backend unit/: 782 passing (was 754 before, label/contract drift caught) - Frontend: 583 passing (added 5 helper tests, updated 6 label assertions) - Type-check: clean for changed files (knowledge-graph.tsx / slider.tsx errors are pre-existing and unrelated) Co-authored-by: Isaac
…s_grant display override UX feedback on PR #369 final round: 1. Trigger-picker field label "Type" → "Fires on" with helper "What action makes this workflow run". Label now lives inside <TriggerPicker> so the parent form doesn't need a duplicate. 2. Entity-type multiselect field label "Category" → "Applies to" with helper "Which kinds of objects this fires on". Label now lives inside <EntityTypeMultiselect> for the same reason. 3. prettyEntityTypeLabel: add a small OVERRIDES map and remap access_grant → "Data object". All other entity types still flow through the snake_case → Sentence case rule. Wire format is unchanged — only the visible label moves. End result reads naturally in the picker: Fires on: When a user requests access Applies to: Data object / Project / App role Tests: - entity-type-multiselect.test.tsx: assert override case and new "Applies to" field label. - trigger-picker.test.tsx: assert exported TRIGGER_PICKER_LABEL constant (Radix <Select> still hangs in jsdom, so we can't render the picker; the constant is the surface the component renders). Co-authored-by: Isaac
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Today's workflow trigger dropdown lists ~32 trigger types in a flat list, with
for_*(Approval / wizard) andon_*(Process / after-effect) entries for the same action appearing as if they were duplicates. Authors also have no UI forentity_types, so workflows accidentally end up with[](= "fires for everything").This PR replaces the inline
<Select>+ Badge grid inworkflow-designer.tsxwith:<TriggerPicker>— backed by a newGET /api/workflows/trigger-typesendpoint, filters by the workflow'sworkflow_type, groups Process triggers (Lifecycle / Request flow / Validation gates / System & scheduled), hidesfor_approval_responsebehind a Show advanced triggers toggle, and shows the raw enum value in a tooltip on hover.<EntityTypeMultiselect>— renders beneath the picker once a trigger is chosen, populated from the chosen trigger's supportedentity_types. Persists intoworkflow.trigger.entity_types.Critically, no
TriggerTypeenum values are renamed — only display labels change. A new enum-pin test guarantees this.What changed
Backend
GET /api/workflows/trigger-typesinsrc/backend/src/routes/workflows_routes.py(+178 lines). Returns one entry perTriggerTypewith{value, label, workflow_type, entity_types, is_advanced, group}.test_trigger_enum_pin.py) — parametrized over everyTriggerTypemember, asserts.valuebyte-identical to the wire-format contract.test_workflow_trigger_roundtrip.py) — 10 representative triggers: create → GET → PUT no-op → GET, asserttrigger.typebyte-identical.test_trigger_types_endpoint.py) — shape + invariant that every TriggerType is represented exactly once.Frontend
src/frontend/src/lib/workflow-labels.tswithTRIGGER_LABELS+getTriggerLabel()(the canonical user-approved labels — same strings the BE endpoint returns).trigger-picker.tsx(170 lines) + 7 tests on the exportedpartitionTriggerspure function.entity-type-multiselect.tsx(97 lines) + 6 tests.workflow-designer.tsxswapped inline<Select>+ Badge grid for the two new components.workflow-labels.test.tswith 34 label cases + fallback test.Three enum members the user-approved label table missed
manual,on_certify,on_decertifyweren't in the original label table; I added neutral fallbacks ("Manually triggered" / "After entity is certified" / "After entity is decertified") in both_TRIGGER_LABELS(backend) andTRIGGER_LABELS(frontend) so the every-enum-member-covered contract test passes. Flag for review if different copy is preferred. Group assignments:manual→system_scheduled;on_certify/on_decertify→lifecycle.Safety contract
TriggerTypeenum values ("for_subscribe", …)workflow.trigger.typeand in customer webhook payloads — never changed.workflow_steps.config/workflow.trigger.typeDB columns/api/workflows*request/response shapesgetTriggerLabel(value)helper. Backend untouched.Test plan
hatch -e dev run pytest src/backend/src/tests/unit/— 754 passed, incl. 48 new (enum-pin: 33, contract: 5, round-trip: 10)cd src/frontend && yarn test --run— 447 passed / 6 pre-existing skipscd src/frontend && yarn type-check— cleanfor_*triggers visible only on Approval workflows,for_approval_responsehidden until Show advanced triggers toggled on. Select a trigger — Applies to multiselect appears beneath, populated from the trigger's supported entity types.GET /api/workflows/trigger-typesreturns 32 entries.trigger.typeandtrigger.entity_typesbyte-identical (already covered by the new BE unit test).This pull request and its description were written by Isaac.