diff --git a/application/single_app/config.py b/application/single_app/config.py index 02872ea66..98bde7c3c 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.270" +VERSION = "0.261.271" 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. */} -