From 3addd861a0a98cebb602395ffe508cd7a64fd7bd Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Tue, 6 Oct 2026 20:21:54 -0400 Subject: [PATCH] Restyle V2 Workflows in the Admin Settings design language - The Workflows list is a Model Catalog-style workbench: one-line rows that lead with how each workflow stands, beside a detail pane with Overview, Runs and Flow tabs, plus search and Status/Trigger filters. Run links select a workflow and open its run. Status reads last_run_status once the server sets a finished workflow back to idle. - The editor is a routed page (/workspace/workflows/ and the group equivalent) in Admin's wide frame: header-band cards with Admin status chips, label-beside-control fields, paired switches, nested dependents and an On this page index. The chat proposal card keeps the dialog. - Shared SectionCard and WorkflowField primitives; card status logic in workflowEditorSections.ts and workbench logic in workflowWorkbench.ts. - The separate Flow dialog is retired; the Flow tab shows the saved flow. - Feature doc, guides, functional and UI tests updated. Version 0.261.266. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- application/single_app/config.py | 2 +- .../v2_ui/src/components/ui/SectionCard.tsx | 117 ++ .../workflows/WorkflowAlertEditor.tsx | 167 +-- .../workflows/WorkflowAlertSummary.tsx | 60 +- .../components/workflows/WorkflowAskAiTab.tsx | 8 +- .../workflows/WorkflowChangeTracking.tsx | 8 +- .../workflows/WorkflowDocumentPicker.tsx | 11 +- .../workflows/WorkflowEditorDialog.tsx | 1128 ++++++++++------- .../components/workflows/WorkflowField.tsx | 215 ++++ .../workflows/WorkflowFileSyncFields.tsx | 282 +++-- .../workflows/WorkflowFlowDialog.tsx | 49 - .../workflows/WorkflowMicrosoft365RunAs.tsx | 34 +- .../workflows/WorkflowScheduleFields.tsx | 273 ++-- .../workflows/WorkflowStatusChip.tsx | 56 + .../workflows/WorkflowStructuredFields.tsx | 34 +- .../workflows/WorkflowTaskFields.tsx | 280 ++-- .../workflows/WorkflowWorkbenchDetail.tsx | 236 ++++ .../workflows/workflowEditorSectionIcons.ts | 21 + application/v2_ui/src/lib/workflowAlerts.ts | 15 + .../v2_ui/src/lib/workflowEditorSections.ts | 273 ++++ application/v2_ui/src/lib/workflowRunLink.ts | 36 +- .../v2_ui/src/lib/workflowWorkbench.ts | 261 ++++ .../v2_ui/src/pages/GroupWorkspacePage.tsx | 31 +- .../pages/workspace/WorkflowEditorPage.tsx | 245 ++++ .../src/pages/workspace/WorkflowsSection.tsx | 713 ++++++----- .../src/pages/workspace/WorkspacePage.tsx | 2 +- .../v2_ui/src/pages/workspace/sections.tsx | 8 +- application/v2_ui/src/styles/theme.css | 60 + .../features/V2_GROUP_WORKFLOWS.md | 5 + .../V2_WORKFLOWS_WORKBENCH_AND_EDITOR.md | 165 +++ .../features/WORKFLOW_FLOW_INSPECTION.md | 5 +- docs/guides/create-a-workflow.md | 43 +- docs/guides/trigger-a-workflow.md | 7 +- ...ration_workflow_run_links_xss_guardrail.py | 25 +- ...test_v2_workflows_admin_design_language.py | 282 +++++ .../test_v2_workflows_workbench_logic.ts | 343 +++++ scripts/check_xss_sinks.py | 2 + ui_tests/fixtures/workflow_workbench.py | 124 ++ ui_tests/test_v2_document_provenance.py | 93 +- ui_tests/test_v2_group_classic_handoffs.py | 12 +- ui_tests/test_v2_group_journeys.py | 9 +- ui_tests/test_v2_group_workflow_file_sync.py | 82 +- ui_tests/test_v2_group_workspace_shell.py | 97 +- .../test_v2_personal_workflow_file_sync.py | 27 +- ui_tests/test_v2_workflow_alerts.py | 18 +- ui_tests/test_v2_workflow_ask_ai.py | 47 +- ui_tests/test_v2_workflow_ask_ai_proposal.py | 3 +- .../test_v2_workflow_authoring_history.py | 43 +- .../test_v2_workflow_calendar_schedules.py | 49 +- ui_tests/test_v2_workflow_change_tracking.py | 39 +- ui_tests/test_v2_workflow_control_flow.py | 46 +- ui_tests/test_v2_workflow_control_runtime.py | 11 +- ui_tests/test_v2_workflow_durable_runtime.py | 36 +- ui_tests/test_v2_workflow_editor.py | 82 +- ui_tests/test_v2_workflow_flow_authoring.py | 78 +- .../test_v2_workflow_flow_canvas_stability.py | 13 +- ui_tests/test_v2_workflow_flow_inspection.py | 57 +- ui_tests/test_v2_workflow_loops.py | 72 +- ui_tests/test_v2_workflow_m365_run_as.py | 41 +- ui_tests/test_v2_workflow_m365_runtime.py | 23 +- ui_tests/test_v2_workflow_merge_task.py | 27 +- ...test_v2_workflow_publication_completion.py | 30 +- ui_tests/test_v2_workflow_repeat_until.py | 54 +- ...st_v2_workflow_saved_output_publication.py | 38 +- ui_tests/test_v2_workflow_settings_errors.py | 20 +- ...2_workflows_workbench_and_editor_layout.py | 365 ++++++ 66 files changed, 5281 insertions(+), 1857 deletions(-) create mode 100644 application/v2_ui/src/components/ui/SectionCard.tsx create mode 100644 application/v2_ui/src/components/workflows/WorkflowField.tsx delete mode 100644 application/v2_ui/src/components/workflows/WorkflowFlowDialog.tsx create mode 100644 application/v2_ui/src/components/workflows/WorkflowStatusChip.tsx create mode 100644 application/v2_ui/src/components/workflows/WorkflowWorkbenchDetail.tsx create mode 100644 application/v2_ui/src/components/workflows/workflowEditorSectionIcons.ts create mode 100644 application/v2_ui/src/lib/workflowEditorSections.ts create mode 100644 application/v2_ui/src/lib/workflowWorkbench.ts create mode 100644 application/v2_ui/src/pages/workspace/WorkflowEditorPage.tsx create mode 100644 docs/explanation/features/V2_WORKFLOWS_WORKBENCH_AND_EDITOR.md create mode 100644 functional_tests/test_v2_workflows_admin_design_language.py create mode 100644 functional_tests/test_v2_workflows_workbench_logic.ts create mode 100644 ui_tests/fixtures/workflow_workbench.py create mode 100644 ui_tests/test_v2_workflows_workbench_and_editor_layout.py diff --git a/application/single_app/config.py b/application/single_app/config.py index 6bd64619f..1e040ce02 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -101,7 +101,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.261.265" +VERSION = "0.261.266" IS_DEVELOPMENT = is_development_env_enabled() # Opt-out for deployments where App Service Easy Auth is active but the platform diff --git a/application/v2_ui/src/components/ui/SectionCard.tsx b/application/v2_ui/src/components/ui/SectionCard.tsx new file mode 100644 index 000000000..d0fea615f --- /dev/null +++ b/application/v2_ui/src/components/ui/SectionCard.tsx @@ -0,0 +1,117 @@ +// SectionCard.tsx +// The distinct card V2 Admin Settings draws every section in, for surfaces outside Admin. +// +// Admin's `SettingsSection` owns its card markup inline, alongside schema-driven fields this +// component knows nothing about. The workflow editor needs the same object -- a header band +// with the section's icon, a title large enough to find while scrolling, a line of facts, and +// the Admin status chip -- around controls it lays out itself. The classes match Admin's card +// (bar one narrow-width allowance, noted below), so a section here and a section there read as +// the same thing, and so `.admin-field` rows inside resolve their container queries against +// this card the way they do in Admin. +// +// The title takes focus (`tabIndex={-1}`) so an "On this page" jump can land a keyboard or +// screen reader user on the card it scrolled to, as Admin's index does. + +import { clsx } from 'clsx'; +import type { LucideIcon } from 'lucide-react'; +import type { ReactNode } from 'react'; +import type { SectionStatus } from '../../lib/adminSections'; +import { presentSectionStatus } from '../admin/sectionStatusPresentation'; + +export function SectionCard({ + id, + title, + icon: Icon, + meta, + status = 'none', + actions, + ariaLabel, + headingLevel = 2, + className, + bodyClassName, + children, + ...data +}: { + /** The card's DOM id. The title is `${id}-title`, which an index jump focuses. */ + id: string; + title: ReactNode; + icon: LucideIcon; + /** One line of facts under the title. */ + meta?: ReactNode; + status?: SectionStatus; + /** Controls drawn in the header band beside the status chip. */ + actions?: ReactNode; + /** + * Names the region instead of its visible title, for a card whose established accessible + * name differs from the title it now shows. + */ + ariaLabel?: string; + headingLevel?: 2 | 3; + className?: string; + bodyClassName?: string; + children?: ReactNode; + [dataAttribute: `data-${string}`]: string | undefined; +}) { + const titleId = `${id}-title`; + const presentation = presentSectionStatus(status); + const Heading = headingLevel === 3 ? 'h3' : 'h2'; + + return ( +
+
+ {/* Admin's header, except that the title column asks for 15rem before the chip may + sit beside it, so a narrow card wraps the chip rather than breaking the facts + line mid-word. */} +
+ +
+ + {title} + + {meta ?

{meta}

: null} +
+
+ + {presentation || actions ? ( +
+ {presentation ? ( + // Free to shrink once it has wrapped onto its own line, so a long + // status at a large text size wraps inside the card instead of past it. + + + ) : null} + {actions} +
+ ) : null} +
+ +
{children}
+
+ ); +} diff --git a/application/v2_ui/src/components/workflows/WorkflowAlertEditor.tsx b/application/v2_ui/src/components/workflows/WorkflowAlertEditor.tsx index b86932eb7..cdfb768b5 100644 --- a/application/v2_ui/src/components/workflows/WorkflowAlertEditor.tsx +++ b/application/v2_ui/src/components/workflows/WorkflowAlertEditor.tsx @@ -2,9 +2,18 @@ // Native alert authoring for personal and group workflows: when to alert, and the rules that decide it. import { useId } from 'react'; -import { ArrowDown, ArrowUp, Plus, Trash2 } from 'lucide-react'; +import { ArrowDown, ArrowUp, BellRing, Plus, Trash2 } from 'lucide-react'; import { GlassButton, Toggle } from '../ui/primitives'; +import { SectionCard } from '../ui/SectionCard'; import { Pill } from '../workspace/primitives'; +import { + WorkflowCardHelp, + WorkflowField, + WorkflowFieldEmphasis, + WorkflowFieldList, + workflowFieldInputClass, + type WorkflowCardFrame, +} from './WorkflowField'; import { describeWorkflowAlertCondition, newWorkflowAlertRule, @@ -167,15 +176,18 @@ function optionValue(value: unknown, allowed: readonly string[], fallback: strin return text && allowed.includes(text) ? text : fallback; } -function Select({ label, value, options, onChange, disabled }: { +function Select({ id, label, value, options, onChange, disabled, className }: { + id?: string; label: string; value: string; options: Option[]; onChange: (value: string) => void; disabled?: boolean; + /** Replaces the rule-row field styling, for a select drawn in a card's field row. */ + className?: string; }) { return ( - @@ -508,10 +520,13 @@ function AlertRuleRow({ export function WorkflowAlertEditor({ workflow, scope, + card, onChange, }: { workflow: WorkflowDefinition; scope: WorkflowScope; + /** The Alerts card's place in the editor and how it reads at a glance. */ + card: WorkflowCardFrame; onChange: (update: (workflow: WorkflowDefinition) => WorkflowDefinition) => void; }) { const titleId = useId(); @@ -548,80 +563,86 @@ export function WorkflowAlertEditor({ return key; }); const atLimit = rules.length >= WORKFLOW_ALERT_MAX_RULES; + const leadsSettings = mode === 'every_run' || mode === 'rules' || rules.length > 0 || usesModel; return ( -
-
-

Alerts

-

- Alerts notify you when a run meets a condition you define. A run that matches nothing stays silent. -

-
-
- - {mode === 'every_run' ? ( -
- {mode !== 'rules' ? ( -

- These saved rules send alerts only when you choose “Only when a condition is met”. They are kept, and still checked, when you save. -

- ) : null} - {mode === 'rules' && !rules.length ? ( -

No alert rules yet. Add a rule to choose what should notify you.

- ) : null} - {mode === 'rules' && atLimit ? ( -

A workflow can have up to {WORKFLOW_ALERT_MAX_RULES} alert rules.

- ) : null} - {rules.length ? ( -
    - {rules.map((rule, index) => ( - editRules((current) => current.map((item, itemIndex) => itemIndex === index ? next : item))} - onMove={(direction) => moveRule(index, direction)} - onRemove={() => editRules((current) => current.filter((_, itemIndex) => itemIndex !== index))} /> - ))} -
- ) : null} - {mode === 'rules' && rules.length ? ( -

- When several rules match the same run, the highest severity wins and every matched rule is listed in the alert. - Info and low severities go to the notification bell only. Medium and above open the pop-up alert. -

- ) : null} - - ) : null} - {usesModel ? ( -
+ ); } diff --git a/application/v2_ui/src/components/workflows/WorkflowAlertSummary.tsx b/application/v2_ui/src/components/workflows/WorkflowAlertSummary.tsx index 9a674a045..02de10bee 100644 --- a/application/v2_ui/src/components/workflows/WorkflowAlertSummary.tsx +++ b/application/v2_ui/src/components/workflows/WorkflowAlertSummary.tsx @@ -1,53 +1,39 @@ // WorkflowAlertSummary.tsx // Read-only summary of a workflow's stored alerts, for viewers who cannot manage the workflow. -import { useId } from 'react'; +import { BellRing } from 'lucide-react'; import { workflowAlertSummary, - type WorkflowAlertMode, - type WorkflowAlertPriority, + WORKFLOW_ALERT_MODE_LABELS, + WORKFLOW_ALERT_PRIORITY_LABELS, } from '../../lib/workflowAlerts'; import type { WorkflowDefinition } from '../../lib/workflowEditor'; +import { SectionCard } from '../ui/SectionCard'; +import type { WorkflowCardFrame } from './WorkflowField'; -// The alert editor's own option labels, so both views describe a setting the same way. -const MODE_LABELS: Record = { - off: 'Never notify me', - rules: 'Only when a condition is met', - every_run: 'On every run', -}; -const PRIORITY_LABELS: Record = { - none: 'No notification', - low: 'Low priority', - medium: 'Medium priority', - high: 'High priority', -}; +/** One fact, label beside value on a wide card, the way Admin shows a readout. */ +function SummaryRow({ term, children }: { term: string; children: string }) { + return ( +
+
{term}
+
{children}
+
+ ); +} -export function WorkflowAlertSummary({ workflow }: { workflow: WorkflowDefinition }) { - const titleId = useId(); +export function WorkflowAlertSummary({ workflow, card }: { workflow: WorkflowDefinition; card: WorkflowCardFrame }) { const summary = workflowAlertSummary(workflow); return ( -
-

Alerts

-
-
-
When to alert
-
{MODE_LABELS[summary.mode]}
-
-
-
Pop-up priority
-
{PRIORITY_LABELS[summary.priority]}
-
-
-
Alert rules
-
{summary.ruleCount === 1 ? '1 rule' : `${summary.ruleCount} rules`}
-
+ +
+ {WORKFLOW_ALERT_MODE_LABELS[summary.mode]} + {WORKFLOW_ALERT_PRIORITY_LABELS[summary.priority]} + {summary.ruleCount === 1 ? '1 rule' : `${summary.ruleCount} rules`} {summary.optionSummary.length ? ( -
-
Pop-up options
-
{summary.optionSummary.join(' · ')}
-
+ {summary.optionSummary.join(' · ')} ) : null}
-
+ ); } diff --git a/application/v2_ui/src/components/workflows/WorkflowAskAiTab.tsx b/application/v2_ui/src/components/workflows/WorkflowAskAiTab.tsx index f29e4e67d..394fb9037 100644 --- a/application/v2_ui/src/components/workflows/WorkflowAskAiTab.tsx +++ b/application/v2_ui/src/components/workflows/WorkflowAskAiTab.tsx @@ -43,18 +43,20 @@ export function WorkflowAssistElapsed({ startedAt, testId }: { startedAt: number } /** The footer button that opens the side panel on Ask AI. */ -export function WorkflowAskAiToggle({ id, open, controls, onToggle }: { +export function WorkflowAskAiToggle({ id, open, controls, onToggle, labelAlwaysVisible = false }: { id: string; open: boolean; controls: string; onToggle: () => void; + /** Show the words at every width, where the toggles have a row of their own. */ + labelAlwaysVisible?: boolean; }) { return ( {/* Below sm an icon stands in for the label so Cancel and Save stay on one line. */} - ); } diff --git a/application/v2_ui/src/components/workflows/WorkflowChangeTracking.tsx b/application/v2_ui/src/components/workflows/WorkflowChangeTracking.tsx index da7911536..22da649c9 100644 --- a/application/v2_ui/src/components/workflows/WorkflowChangeTracking.tsx +++ b/application/v2_ui/src/components/workflows/WorkflowChangeTracking.tsx @@ -633,11 +633,13 @@ export function WorkflowEditorSidePanel({ id, tabs, className, selected: selecte } /** The footer button that opens and closes the side panel, with the number of unsaved changes. */ -export function WorkflowChangesToggle({ id, open, controls, onToggle }: { +export function WorkflowChangesToggle({ id, open, controls, onToggle, labelAlwaysVisible = false }: { id: string; open: boolean; controls: string; onToggle: () => void; + /** Show the word at every width, where the toggles have a row of their own. */ + labelAlwaysVisible?: boolean; }) { const tracking = useWorkflowChanges(); if (!tracking) return null; @@ -646,8 +648,8 @@ export function WorkflowChangesToggle({ id, open, controls, onToggle }: { {/* Below sm an icon stands in for the label so Cancel and Save stay on one line. */} -