diff --git a/application/single_app/config.py b/application/single_app/config.py index 1fe91e5f0..05b3790fa 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.227" +VERSION = "0.261.228" 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/notifications/WorkflowAlertCard.tsx b/application/v2_ui/src/components/notifications/WorkflowAlertCard.tsx index 4953237b3..1a176df93 100644 --- a/application/v2_ui/src/components/notifications/WorkflowAlertCard.tsx +++ b/application/v2_ui/src/components/notifications/WorkflowAlertCard.tsx @@ -7,14 +7,17 @@ // so the host below hands focus back itself. // // The card shows one entry at a time: an alert, or every alert one workflow raised together. -// "1 of 3" and Next step through what is waiting. Mark read and Dismiss act on the entry -// shown, and Mark all read on every entry, one alert at a time -- never the bell's own Mark -// all read, which would also clear notices the card never showed. +// "1 of 3" and Next step through what is waiting. Open and Dismiss act on the entry shown, +// and Mark all read on every entry, one alert at a time -- never the bell's own Mark all read, +// which would also clear notices the card never showed. +// +// It says just enough to decide: why the alert came, its summary, and one Open button to the +// place the workflow made. The detail, the facts and every other way in wait under Show more. // // Every word here is the alert's own text, rendered as text. Its links are checked by the // bell's resolver and followed by the bell's navigation, so only this site's pages open. -import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { clsx } from 'clsx'; import { ArrowRight, ChevronDown, ChevronRight, X } from 'lucide-react'; @@ -25,6 +28,7 @@ import { formatRelativeTime } from '../../lib/userStats'; import { growFromRect } from '../../lib/workflowAlertMotion'; import { openWorkflowResultInChat } from '../../lib/workflowResultFollowUp'; import { + WORKFLOW_ALERT_CREATED_LINK_LABEL, WORKFLOW_ALERT_PRIORITY_LABELS, describeWorkflowAlertGroup, describeWorkflowAlertReason, @@ -74,6 +78,11 @@ interface CardLink { target: NotificationTarget; } +/** A way into what the alert is about, with the data attribute that names which kind it is. */ +interface CardAction extends CardLink { + marker: 'data-workflow-alert-link' | 'data-workflow-alert-open-run' | 'data-workflow-alert-open-workflow'; +} + function CardBanner({ entry, onClose }: { entry: WorkflowAlertEntry; onClose: () => void }) { const alert = entry.lead; const tone = workflowAlertTone(entry.priority); @@ -126,12 +135,13 @@ function CardBanner({ entry, onClose }: { entry: WorkflowAlertEntry; onClose: () ); } -function CardBody({ alert }: { alert: WorkflowAlert }) { +function CardBody({ alert, more }: { alert: WorkflowAlert; more: ReactNode }) { const [expanded, setExpanded] = useState(false); - const detailId = useId(); + const moreId = useId(); const whyId = useId(); const chips = chipsFor(alert); const serverReason = workflowAlertServerReason(alert); + const hasMore = Boolean(alert.detail) || chips.length > 0 || Boolean(more); return (
@@ -162,33 +172,6 @@ function CardBody({ alert }: { alert: WorkflowAlert }) {

{alert.summary}

- {alert.detail && ( - <> - - - - )}
{alert.error && ( @@ -202,18 +185,48 @@ function CardBody({ alert }: { alert: WorkflowAlert }) { )} - {chips.length > 0 && ( - + {hasMore && ( +
+ + +
)}
); @@ -272,6 +285,21 @@ function WorkflowAlertCard({ onOpened }: { onOpened: () => void }) { open: (workflowId, runId) => openWorkflowResultInChat(workflowId, runId, { navigate, pathname }), }); + // Open goes to the conversation the workflow created, else to where it posted, its run, or itself. + const primaryLink = links.find((link) => link.label === WORKFLOW_ALERT_CREATED_LINK_LABEL) ?? links[0]; + const routeAction: CardAction | null = runPath + ? { label: 'Open run', target: { kind: 'route', path: runPath }, marker: 'data-workflow-alert-open-run' } + : workflowPath + ? { label: 'Open workflow', target: { kind: 'route', path: workflowPath }, marker: 'data-workflow-alert-open-workflow' } + : null; + const primary: CardAction | null = primaryLink + ? { ...primaryLink, marker: 'data-workflow-alert-link' } + : routeAction; + const otherActions: CardAction[] = [ + ...links.filter((link) => link !== primaryLink).map((link): CardAction => ({ ...link, marker: 'data-workflow-alert-link' })), + ...(routeAction && routeAction !== primary ? [routeAction] : []), + ]; + const open = (target: NotificationTarget) => { if (busy) { return; @@ -289,9 +317,47 @@ function WorkflowAlertCard({ onOpened }: { onOpened: () => void }) { void followUp.run(); }; - const hasOpenRow = links.length > 0 || Boolean(runPath || workflowPath || followUp); const label = WORKFLOW_ALERT_PRIORITY_LABELS[entry.priority]; const groupNote = entry.count > 1 ? `Acts on all ${entry.count} alerts from this workflow.` : undefined; + const more = otherActions.length > 0 || followUp || refusedMessage ? ( +
+ {(otherActions.length > 0 || followUp) && ( +
+ {otherActions.map((action, index) => ( + open(action.target)} + className={BUSY_CLASS} + > + {action.label} + + ))} + {followUp && ( + + {followUp.label} + + )} +
+ )} + {refusedMessage && ( +

{refusedMessage}

+ )} +
+ ) : null; return ( void }) { panelRef={panelRef} banner={} footer={( -
- {hasOpenRow && ( -
- {links.map((link, index) => ( - open(link.target)} - className={BUSY_CLASS} - > - {link.label} - - ))} - {runPath ? ( - open({ kind: 'route', path: runPath })} - className={BUSY_CLASS} - > - Open run - - ) : workflowPath ? ( - open({ kind: 'route', path: workflowPath })} - className={BUSY_CLASS} - > - Open workflow - - ) : null} - {followUp && ( - - {followUp.label} - - )} -
- )} - {refusedMessage && ( -

{refusedMessage}

+
+ {total > 1 && ( + <> + + {cardIndex + 1} of {total} + : {alert.title} + + { + if (!busy) { + nextEntry(); + } + }} + className={BUSY_CLASS} + > + Next + + void markAllRead()} + className={BUSY_CLASS} + > + Mark all read + + )} -
- {total > 1 && ( - <> - - {cardIndex + 1} of {total} - : {alert.title} - - { - if (!busy) { - nextEntry(); - } - }} - className={BUSY_CLASS} - > - Next - - void markAllRead()} - className={BUSY_CLASS} - > - Mark all read - - - )} + void dismissEntry()} + className={BUSY_CLASS} + > + Dismiss + + {primary ? ( void dismissEntry()} - className={BUSY_CLASS} + title={groupNote ?? primary.label} + onClick={() => open(primary.target)} + className={clsx('min-w-36 justify-center', BUSY_CLASS)} > - Dismiss + Open + + ) : ( + // Nothing to open, so reading it is the way to settle it. void markEntryRead()} - className={BUSY_CLASS} + className={clsx('min-w-36 justify-center', BUSY_CLASS)} > Mark read -
+ )}
)} > {/* Keyed on the entry, so Show more starts closed for each one. */} - + ); } diff --git a/application/v2_ui/src/components/ui/primitives.tsx b/application/v2_ui/src/components/ui/primitives.tsx index 07ee98eaf..288ecf016 100644 --- a/application/v2_ui/src/components/ui/primitives.tsx +++ b/application/v2_ui/src/components/ui/primitives.tsx @@ -44,12 +44,14 @@ export function GlassPanel({ ); } -type ButtonVariant = 'primary' | 'ghost' | 'subtle' | 'danger'; +type ButtonVariant = 'primary' | 'success' | 'ghost' | 'subtle' | 'danger'; type ButtonSize = 'sm' | 'md' | 'lg' | 'icon'; const variantClass: Record = { primary: 'bg-accent text-on-accent hover:bg-accent-hover shadow-sm disabled:hover:bg-accent', + success: + 'bg-ok-strong text-on-ok hover:bg-ok-strong-hover shadow-sm disabled:hover:bg-ok-strong', ghost: 'text-text-2 hover:text-text-1 hover:bg-surface-2', subtle: 'glass-flat text-text-1 hover:bg-surface-2 rounded-xl', danger: 'bg-danger-soft text-danger hover:bg-danger hover:text-white', diff --git a/application/v2_ui/src/lib/workflowAlertNotices.ts b/application/v2_ui/src/lib/workflowAlertNotices.ts index 36bc0b005..a8a6ea954 100644 --- a/application/v2_ui/src/lib/workflowAlertNotices.ts +++ b/application/v2_ui/src/lib/workflowAlertNotices.ts @@ -231,6 +231,9 @@ function readLabels(value: unknown): string[] { return labels.slice(0, MAX_CHIPS); } +/** The link to a conversation the workflow created: where the card's Open button goes first. */ +export const WORKFLOW_ALERT_CREATED_LINK_LABEL = 'Open created conversation'; + /** * What a link button says. * @@ -245,7 +248,7 @@ function linkLabel(raw: unknown): string { return 'Open workflow conversation'; } if (lower.startsWith('open created')) { - return 'Open created conversation'; + return WORKFLOW_ALERT_CREATED_LINK_LABEL; } if (lower.startsWith('open updated')) { return 'Open conversation'; diff --git a/application/v2_ui/src/stores/workflowAlertStore.ts b/application/v2_ui/src/stores/workflowAlertStore.ts index fba82b4fc..2cb927d79 100644 --- a/application/v2_ui/src/stores/workflowAlertStore.ts +++ b/application/v2_ui/src/stores/workflowAlertStore.ts @@ -79,8 +79,8 @@ interface WorkflowAlertState { dismissEntry: () => Promise; markAllRead: () => Promise; /** - * Follow a link or an open action from the card. The entry's lead alert -- the one the - * card was showing -- is marked read, and the card closes. + * Follow a link or an open action from the card. Every alert in the entry shown is marked + * read, as Mark read did before Open replaced it, and the card closes. */ openFromCard: (target: NotificationTarget, context: NotificationNavigationContext) => Promise; } @@ -395,10 +395,10 @@ export const useWorkflowAlertStore = create((set, get) => { if (state.phase !== 'card' || !entry || state.busy) { return; } - const leadId = entry.lead.id; - settled.add(leadId); + const ids = workflowAlertEntryIds(entry); + ids.forEach((id) => settled.add(id)); finish(); - const read = actions.markRead([leadId]).catch(() => [] as string[]); + const read = actions.markRead(ids).catch(() => [] as string[]); // A full page load would cancel a request still on its way, so a link that leaves // the application waits for the read first. Inside it the read carries on regardless. if (target.kind === 'classic') { diff --git a/application/v2_ui/src/styles/theme.css b/application/v2_ui/src/styles/theme.css index 457c571d0..9317c0054 100644 --- a/application/v2_ui/src/styles/theme.css +++ b/application/v2_ui/src/styles/theme.css @@ -87,6 +87,10 @@ html[data-font-size='xl'] { --warn-soft: rgb(217 119 6 / 0.12); --ok: #059669; --ok-soft: rgb(5 150 105 / 0.12); + /* A solid green that keeps text on it at 4.5:1 or better. */ + --ok-strong: #047857; + --ok-strong-hover: #065f46; + --on-ok: #ffffff; /* Informational, used for the group workspace badge. Distinct from --accent so a group badge is not mistaken for a selected control. */ --info: #0e7490; @@ -142,6 +146,9 @@ html[data-font-size='xl'] { --warn-soft: rgb(251 191 36 / 0.16); --ok: #34d399; --ok-soft: rgb(52 211 153 / 0.16); + --ok-strong: #34d399; + --ok-strong-hover: #6ee7b7; + --on-ok: #022c22; --info: #22d3ee; --info-soft: rgb(34 211 238 / 0.16); --change-ai: #bfdbfe; @@ -182,6 +189,9 @@ html[data-font-size='xl'] { --color-warn-soft: var(--warn-soft); --color-ok: var(--ok); --color-ok-soft: var(--ok-soft); + --color-ok-strong: var(--ok-strong); + --color-ok-strong-hover: var(--ok-strong-hover); + --color-on-ok: var(--on-ok); --color-info: var(--info); --color-info-soft: var(--info-soft); --color-change-ai: var(--change-ai); diff --git a/docs/explanation/features/CHAT_WORKFLOW_RESULTS_FOLLOW_UP.md b/docs/explanation/features/CHAT_WORKFLOW_RESULTS_FOLLOW_UP.md index 87087cf96..d53e99515 100644 --- a/docs/explanation/features/CHAT_WORKFLOW_RESULTS_FOLLOW_UP.md +++ b/docs/explanation/features/CHAT_WORKFLOW_RESULTS_FOLLOW_UP.md @@ -536,7 +536,8 @@ only while the bootstrap flag is on. Group alerts, alerts without a run, and failed or cancelled runs get no button. An alert doesn't record the workflow's definition version, so a structured workflow's alert still offers it, and the descriptor read then refuses the run with `workflow_result_unsupported`. -Choosing it closes the alert and leaves it unread. +Choosing it closes the alert and leaves it unread. Since 0.261.228 it sits under the +alert's **Show more**, with the alert's other secondary actions. ### Errors diff --git a/docs/explanation/features/V2_WORKFLOW_ALERT_NOTICES.md b/docs/explanation/features/V2_WORKFLOW_ALERT_NOTICES.md index 836d84f4a..0a525555c 100644 --- a/docs/explanation/features/V2_WORKFLOW_ALERT_NOTICES.md +++ b/docs/explanation/features/V2_WORKFLOW_ALERT_NOTICES.md @@ -15,6 +15,8 @@ Implemented in version: **0.261.199**, tracked in [chat orchestration workflows roadmap](CHAT_ORCHESTRATION_WORKFLOWS_ROADMAP.md) (#1553, part of #1543). +Open and Dismiss up front, with everything else under Show more, since version: **0.261.228** (#1624). + Dependencies: - The V2 bell ([V2 Notification Bell and Desktop Notifications](V2_NOTIFICATIONS_BELL.md)). @@ -151,7 +153,7 @@ shell. Escape, the backdrop and the focus trap behave as in every other V2 dialo Focus moves into the card and goes back to where the reader was when it closes, because the notice that opened it is gone by then. -The card shows: +The card shows just enough to decide what to do: - **A header band** in the priority's colors, with the priority, the kind ("Alert" or "Run failed"), the title, the workflow and when it arrived. @@ -162,13 +164,24 @@ The card shows: reason. The runner's own log line ("HIGH alert triggered by: …") is left out, because the sentence and the rules already say it. A reason that says anything else is shown. -- **The summary**, and **Show more** for the detail. +- **The summary.** - **What went wrong**, for a failed run. +- **Open**, a large green button, and **Dismiss**. Open goes to the conversation the + workflow created when it created one; otherwise to the conversation it posted + into, then its run, then the workflow. Its accessible name and tooltip name the + destination, for example "Open created conversation". An alert with nothing to + open offers **Mark read** in its place. + +**Show more** holds everything else, so a busy alert, such as one that stood up a +response group, a meeting, a map and a briefing, doesn't bury its summary and Open +button under a dozen chips and links: + +- **The detail**. - **Chips** for the alert's enrichments and its trigger, runner and agent. -- **Links** the alert carries, checked by the bell's resolver. Classic labels the - link to the conversation a workflow posts into "Open workflow"; V2 names it - **Open workflow conversation**, because the card's own **Open workflow** goes to - the workflow. A link to another site isn't offered; a note says so. +- **The other links** the alert carries, checked by the bell's resolver. Classic + labels the link to the conversation a workflow posts into "Open workflow"; V2 + names it **Open workflow conversation**, because the card's own **Open workflow** + goes to the workflow. A link to another site isn't offered; a note says so. - **Open workflow**, which goes to the workflows list of the workspace the workflow lives in and opens the run that raised the alert: `/workspace/workflows?workflow_id=&run_id=`, or @@ -184,7 +197,10 @@ The card shows: Alerts from before this release have no recorded scope. For those, it is taken from the workspace named on their **Open workflow** conversation link, and when that doesn't name one the button isn't shown rather than guessing. -- **Mark read** and **Dismiss** for the entry shown. +- **Ask about this**, when the alert offers it. + +Below the card's body: + - **"1 of 3", Next and Mark all read** when more than one entry is waiting. Next steps through the entries and wraps around. @@ -192,9 +208,10 @@ The card shows: | Action | Effect | |---|---| -| Mark read, Dismiss | Every alert in the entry shown, so a grouped workflow's alerts are handled together. The button's tooltip says how many, for example "Acts on all 2 alerts from this workflow." | +| Open, Dismiss | Every alert in the entry shown, so a grouped workflow's alerts are handled together. Open marks them read and goes to its destination. The button's tooltip says how many, for example "Acts on all 2 alerts from this workflow." | +| Mark read | Shown in Open's place only when the alert has nothing to open. Every alert in the entry shown. | | Mark all read | Every alert the card holds, one request each. It never calls the bell's own `mark-all-read`, which would also clear notices the card never showed. | -| A link, Open workflow | Opens the page and marks the entry's lead alert read, as opening a notice in the bell does | +| Another link, Open workflow | Opens the page and marks every alert in the entry read, as Open does | | Ask about this | Opens a new chat that answers from the run's stored result ([Phase 6a](CHAT_WORKFLOW_RESULTS_FOLLOW_UP.md)). Nothing is marked, as with Close: the alert stays unread in the bell. | | Close (×, Escape, backdrop) | Nothing is marked. The alerts stay unread in the bell and don't pop up again. | @@ -396,7 +413,9 @@ as Flask does in production. Before, it answered those with Vite's "did you mean minutes to appear. Focusing the window or returning to the tab reads it at once. - Open run arrives with Phase 6b. Ask about this (Phase 6a) is offered for personal workflows only, and only while **Use Workflow Results In Chat** is on. -- The card's primary button (**Mark read**) pairs `--accent` with `--on-accent` at +- The card's **Open** button uses `--ok-strong` with `--on-ok`, a green chosen to keep + its text at 5.5:1 in the light theme and 7.9:1 in the dark theme. **Mark read**, + shown only when there is nothing to open, pairs `--accent` with `--on-accent` at 4.49:1 in the light theme, just under 4.5:1. That is a design-token matter shared by every V2 primary button. @@ -408,7 +427,7 @@ as Flask does in production. Before, it answered those with Vite's "did you mean | `functional_tests/route_tests/test_workflow_alert_since_hours_policy.py` | 8 functions | The route keeps its Blueprint, Swagger and authentication policy; classic's request is unchanged; V2's window is validated and passed through; out-of-range limits fall back as before; invalid windows are refused without reading; a reader failure keeps the existing error shape; through the real reader with a failing Cosmos query, V2's read answers `500` and classic's still answers an empty list | | `functional_tests/test_v2_alert_lab_excluded_from_build.py` | 4 | The lab's markers exist only in lab code; the reference scanner recognizes every import form; the only reference to the lab is App.tsx's lazy import inside the `import.meta.env.DEV` branch; an existing production build has no file named for the lab and no lab marker (skipped without a build) | | `functional_tests/test_workflow_priority_alerts.py` | Existing | Classic's workflow alert contract, with the new signature | -| `ui_tests/test_v2_workflow_alert_notices.py` | 28 | Pop-up versus notify-only and the 24-hour window against a server that leaves both filters out; one claim across two tabs of one browser, and a tab opened later; waiting behind a dialog, the bell's panel and a hidden tab; only a successful read retiring an alert, through a failed read on return and a zero count whose confirming read fails; a rise on return read behind a read already on its way; the eight-second tuck, its hover and focus pause, and high and critical staying; storm grouping; every card action; keyboard focus, Escape and the tuck on covering focus; motion with and without reduced motion, including the Web Animations' properties and every `wf-*` keyframe; the rail expanded, collapsed and on a 360 px phone in both themes; text contrast for every priority and category in both themes, with and without reduced transparency; hostile text rendered as text; refused off-site links; and Open workflow's personal, group and unplaceable cases | +| `ui_tests/test_v2_workflow_alert_notices.py` | 29 | Pop-up versus notify-only and the 24-hour window against a server that leaves both filters out; one claim across two tabs of one browser, and a tab opened later; waiting behind a dialog, the bell's panel and a hidden tab; only a successful read retiring an alert, through a failed read on return and a zero count whose confirming read fails; a rise on return read behind a read already on its way; the eight-second tuck, its hover and focus pause, and high and critical staying; storm grouping; every card action; only Close, Show more, Dismiss and the green Open shown up front, with Open going to the created conversation and settling every alert of the entry, and the detail, chips and other actions under Show more; keyboard focus, Escape and the tuck on covering focus; motion with and without reduced motion, including the Web Animations' properties and every `wf-*` keyframe; the rail expanded, collapsed and on a 360 px phone in both themes; text contrast for every priority and category in both themes, with and without reduced transparency; hostile text rendered as text; refused off-site links; and Open workflow's personal, group and unplaceable cases | | `ui_tests/test_v2_document_provenance.py` | 6 added | Against the real workflows section, personal and group: Open workflow while the list is open expands the run; a second Open workflow for the same run opens it again; running another workflow afterwards doesn't reopen it; a workflow created after the list was read is found with exactly one more list read, which opens its run; a workflow missing from that read too costs one list read per navigation and no more, opens nothing, shows no error and leaves the list usable | The UI suite mounts the real V2 frame in a harness build, following diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index b80cff6a2..8021104c3 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -2,6 +2,17 @@ For feature-focused and fix-focused drill-downs by version, see [Features by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/features) and [Fixes by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/fixes). +### **(v0.261.228)** + +#### User Interface Enhancements + +* **V2 Workflow Alerts Lead With One Open Button** + * The full workflow alert now shows just what's needed to act: why it came, its summary, and a large green **Open** button next to **Dismiss**. Open goes to the conversation the workflow created, or else to the conversation it posted into, its run or the workflow, and marks the alert read. + * **Mark read** is gone from alerts that have somewhere to open, because Open marks the alert read and takes you there. When one workflow raised several alerts together, Open and Dismiss act on all of them. An alert with nothing to open still offers Mark read. + * The detail, the pills for the alert's enrichments, trigger, runner and agent, and the other ways in (other links, **Open workflow** and **Ask about this**) are under **Show more**. + * Open's green keeps its text at 4.5:1 contrast or better in the light and dark themes. + * (Ref: #1624, `WorkflowAlertCard.tsx`, `workflowAlertStore.ts`, `theme.css`, [V2 Workflow Alert Notices](features/V2_WORKFLOW_ALERT_NOTICES.md)) + ### **(v0.261.227)** #### New Features diff --git a/docs/guides/manage-notifications.md b/docs/guides/manage-notifications.md index f675c8afa..47b179a13 100644 --- a/docs/guides/manage-notifications.md +++ b/docs/guides/manage-notifications.md @@ -115,17 +115,22 @@ tucks it into the bell, where it stays unread; it doesn't pop up again. capture="Capture the V2 rail with a high priority workflow alert notice under My Workspace, using realistic sample alerts. Redact workflow and conversation names." %} Select the notice to open the full alert. It says why you are seeing it and which -rules matched, what went wrong for a failed run, the summary and details, and the -links the alert carries. From there you can: - -- Select **Open workflow** to go to the workflow in its personal or group workspace, - with the run that raised the alert open. This marks the alert read. -- Select **Ask about this**, on an alert about a completed run of one of your - personal workflows, to ask chat about that run's stored result without running - the workflow again. The alert closes and stays unread. See - [Ask about workflow results]({{ '/guides/ask-about-workflow-results/' | relative_url }}). -- Select **Mark read** or **Dismiss**. When the notice stands for several alerts - from one workflow, both act on all of them. +rules matched, the summary, and what went wrong for a failed run. From there you +can: + +- Select **Open** to go to what the workflow made: the conversation it created, or + else the conversation it posted into, its run or the workflow itself. This marks + the alert read. An alert with nothing to open offers **Mark read** instead. +- Select **Dismiss** to dismiss it. When the notice stands for several alerts from + one workflow, Open and Dismiss both act on all of them. +- Select **Show more** for the details, the alert's facts, and the other ways in: + - **Open workflow** goes to the workflow in its personal or group workspace, + with the run that raised the alert open. This marks the alert read. + - Other links the alert carries, such as **Open workflow conversation**. + - **Ask about this**, on an alert about a completed run of one of your personal + workflows, asks chat about that run's stored result without running the + workflow again. The alert closes and stays unread. See + [Ask about workflow results]({{ '/guides/ask-about-workflow-results/' | relative_url }}). - Select **Next** to step through the other waiting alerts, or **Mark all read** to clear them all. Both appear when more than one alert is waiting. diff --git a/docs/reference/chat-controls.md b/docs/reference/chat-controls.md index bec6e366c..2e19f4fce 100644 --- a/docs/reference/chat-controls.md +++ b/docs/reference/chat-controls.md @@ -605,7 +605,7 @@ answer ends with a line that names the run. See | Control | What it does | Why you would use it | Enabled by | | --- | --- | --- | --- | | Ask in chat | On a run in a personal workflow's run history, opens a new chat with that run selected. | Ask what a run found without copying its output into chat or running the workflow again. | `allow_user_workflows` and `enable_chat_workflow_results`, plus the `WorkflowUser` role while `require_member_of_workflow_user` is on, for a completed or partially completed run that isn't a structured run | -| Ask about this | In the full workflow alert, opens a new chat with the alert's run selected. The alert closes and stays unread. | Go straight from an alert to asking what the run found. | The same settings, for a personal workflow's alert about a completed or partially completed run | +| Ask about this | In the full workflow alert, under **Show more**, opens a new chat with the alert's run selected. The alert closes and stays unread. | Go straight from an alert to asking what the run found. | The same settings, for a personal workflow's alert about a completed or partially completed run | | Workflow result notice | Names the run your next question is about, for example "Answering from the Weekly digest run of Mon, Jun 2, 9:02 AM — not re-running the workflow". While it's shown, every other source and **Orchestrate** are off, and the message box reads "Ask about the workflow results…". | Confirm which result the answer will use before you send. | A run selected with Ask in chat or Ask about this, or inherited from the chat's latest answer | | Remove workflow result context | Removes the notice, so the next question is an ordinary chat question. | Go back to normal chat in the same conversation. | The notice is shown | | Unavailable answer | Shows "This answer is unavailable because access to the workflow result it used could not be confirmed." in place of an answer that used a workflow result. Your question stays. | Know that an answer relied on a result you can no longer read, such as a deleted run's. | The result is no longer available to you, or the chat was shared or converted to a collaboration | diff --git a/ui_tests/test_chat_workflow_results.py b/ui_tests/test_chat_workflow_results.py index 81dd6536b..d58b80dcc 100644 --- a/ui_tests/test_chat_workflow_results.py +++ b/ui_tests/test_chat_workflow_results.py @@ -894,6 +894,7 @@ def test_ask_about_this_on_a_personal_alert_opens_a_chat_about_its_run(tab, serv assert shown == 1 expect(tab.alert_card).to_have_count(1) expect(tab.follow_up).to_have_text("Ask about this") + tab.alert_card.locator("[data-workflow-alert-show-more]").click() tab.follow_up.click() expect(tab.chip).to_have_attribute("data-workflow-result-chip", "selected") expect(tab.chip_text).to_have_text(CHIP) diff --git a/ui_tests/test_v2_workflow_alert_notices.py b/ui_tests/test_v2_workflow_alert_notices.py index e39d039d0..2692134fd 100644 --- a/ui_tests/test_v2_workflow_alert_notices.py +++ b/ui_tests/test_v2_workflow_alert_notices.py @@ -1,8 +1,9 @@ # test_v2_workflow_alert_notices.py """ Browser regressions for the V2 workflow alert notice, its alert card and their runtime. -Version: 0.261.199 +Version: 0.261.228 Implemented in: 0.261.199 +Open and Dismiss up front, everything else under Show more: 0.261.228 Exercises the real rail, bell, notice, card, live region, stores and both notification runtimes, bundled by fixtures/workflow_alerts. Only HTTP answers and the browser APIs a @@ -1308,22 +1309,20 @@ def test_card_actions_act_on_every_alert_of_an_entry(tab, server, alert): position = card.locator("[data-workflow-alert-position]") expect(card).to_be_visible() expect(position).to_contain_text("1 of 4") - mark_read = card.locator("[data-workflow-alert-mark-read]") - expect(mark_read).to_have_attribute("title", "Acts on all 2 alerts from this workflow.") - mark_read.click() - tab.wait_for(lambda: sorted(server.read_calls) == ["a1", "a2"], f"Mark read sent {server.read_calls}.") + # Open took Mark read's place, and like Dismiss it acts on every alert of the entry. + expect(card.locator("[data-workflow-alert-mark-read]")).to_have_count(0) + expect(card.locator("[data-workflow-alert-primary]")).to_have_attribute("title", "Acts on all 2 alerts from this workflow.") + dismiss = card.locator("[data-workflow-alert-dismiss]") + expect(dismiss).to_have_attribute("title", "Acts on all 2 alerts from this workflow.") + dismiss.click() + tab.wait_for(lambda: sorted(server.dismiss_calls) == ["a1", "a2"], f"Dismiss sent {server.dismiss_calls}.") tab.wait_not_busy() expect(position).to_contain_text("1 of 3") expect(card.get_by_role("heading", level=2)).to_have_text("Deploy gate is red") - card.locator("[data-workflow-alert-dismiss]").click() - tab.wait_for(lambda: server.dismiss_calls == ["b1"], f"Dismiss sent {server.dismiss_calls}.") - tab.wait_not_busy() - expect(position).to_contain_text("1 of 2") - card.locator("[data-workflow-alert-mark-all]").click() tab.wait_for( - lambda: sorted(server.read_calls) == ["a1", "a2", "c1", "d1"], + lambda: sorted(server.read_calls) == ["b1", "c1", "d1"], f"Mark all read sent {server.read_calls}.", ) expect(card).to_have_count(0) @@ -1335,6 +1334,49 @@ def test_card_actions_act_on_every_alert_of_an_entry(tab, server, alert): expect(tab.bell).to_have_attribute("data-unread-count", "0") +def test_open_is_the_one_big_action_and_the_rest_waits_under_show_more(tab, server, alert): + tab.open() + posted = workflow_alert_conversation_target("conv-wf-1") + created = workflow_alert_conversation_target("conv-created", label="Open created conversation") + server.add( + alert("s1", workflow_id="wf-1", priority="critical", title="Response cell stood up", + detail="Group, conversation and briefing are ready.", + enrichments=["Group conversation: Leadership Coordination", "Word file: Initial Briefing.docx"], + link_targets=[posted, created]), + alert("s2", minutes_ago=2, workflow_id="wf-1", title="An earlier run", link_targets=[posted]), + ) + tab.poll() + tab.open_button.click() + card = tab.card + expect(card).to_be_visible() + + # Up front: the reason, the summary, Dismiss, and one Open to what the workflow created. + primary = card.locator("[data-workflow-alert-primary]") + expect(primary).to_have_text("Open") + expect(primary).to_have_accessible_name("Open created conversation") + expect(primary).to_have_class(re.compile(r"(^|\s)bg-ok-strong(\s|$)")) + expect(card.locator("[data-workflow-alert-mark-read]")).to_have_count(0) + expect(card.get_by_role("button")).to_have_count(4) + for name in ("Close alert", "Show more", "Dismiss", "Open created conversation"): + expect(card.get_by_role("button", name=name, exact=True)).to_be_visible() + for waiting in ("[data-workflow-alert-detail]", "[data-workflow-alert-chip]", "[data-workflow-alert-links]"): + expect(card.locator(waiting).first).to_be_hidden() + + # Show more holds the detail, the pills and every other way in. + card.locator("[data-workflow-alert-show-more]").click() + expect(card.locator("[data-workflow-alert-detail]")).to_have_text("Group, conversation and briefing are ready.") + expect(card.locator("[data-workflow-alert-chip]").filter(has_text="Word file: Initial Briefing.docx")).to_be_visible() + others = card.locator("[data-workflow-alert-links]") + expect(others.locator("[data-workflow-alert-link]")).to_have_text(["Open workflow conversation"]) + expect(others.locator("[data-workflow-alert-open-workflow]")).to_have_text("Open workflow") + + # Open settles every alert of the entry and goes to the conversation the workflow created. + primary.click() + tab.wait_for(lambda: tab.route_text() == "/chat?conversationId=conv-created", f"Open went to {tab.route_text()}.") + tab.wait_for(lambda: sorted(server.read_calls) == ["s1", "s2"], f"Open marked {server.read_calls} as read.") + expect(card).to_have_count(0) + + # Keyboard and focus ---------------------------------------------------------------------------- @@ -1552,7 +1594,8 @@ def test_alert_text_is_plain_text_and_links_stay_on_this_site(tab, server, alert expect(card.locator("[data-workflow-alert-link-note]")).to_have_text(OFF_SITE) links = card.locator("[data-workflow-alert-link]") expect(links).to_have_count(1) - expect(links).to_contain_text("Open workflow conversation") + # The one link this site can open is the alert's Open button. + expect(links).to_have_accessible_name("Open workflow conversation") card.locator("[data-workflow-alert-next]").click() expect(card.locator("[data-workflow-alert-server-reason]")).to_have_text("Deploy freeze window is active.") card.locator("[data-workflow-alert-next]").click() @@ -1577,6 +1620,10 @@ def open_card(notice_id, **fields): # In Chat, which this bootstrap leaves off (ui_tests/test_chat_workflow_results.py). expect(tab.card.locator("[data-workflow-alert-open-run]")).to_have_count(0) expect(tab.card.locator("[data-workflow-alert-follow-up]")).to_have_count(0) + # Open goes to the conversation; Open workflow waits under Show more. + show_more = tab.card.locator("[data-workflow-alert-show-more]") + if show_more.count(): + show_more.click() return tab.card.locator("[data-workflow-alert-open-workflow]") personal = open_card("o1", workflow_id="wf-nightly", title="Deploy gate is red")