diff --git a/apps/storybook/.storybook/preview.tsx b/apps/storybook/.storybook/preview.tsx index f2a80284b..9e9449676 100644 --- a/apps/storybook/.storybook/preview.tsx +++ b/apps/storybook/.storybook/preview.tsx @@ -1,6 +1,7 @@ import CssBaseline from '@mui/material/CssBaseline'; import { ThemeProvider } from '@mui/material/styles'; import type { Preview } from '@storybook/react'; +import { SUPPORTED_LOCALES } from '@uipath/apollo-react/i18n'; import { apolloMaterialUiThemeDark, apolloMaterialUiThemeDarkHC, @@ -61,6 +62,23 @@ const allThemes: ThemeMode[] = [ 'canvas', ]; +// Human-readable display names for the locale toolbar. +const LOCALE_LABELS: Record<(typeof SUPPORTED_LOCALES)[number], string> = { + en: 'English', + es: 'Español', + pt: 'Português', + de: 'Deutsch', + fr: 'Français', + ja: '日本語', + ko: '한국어', + ru: 'Русский', + tr: 'Türkçe', + 'zh-CN': '中文 (简体)', + 'zh-TW': '中文 (繁體)', + 'pt-BR': 'Português (Brasil)', + 'es-MX': 'Español (México)', +}; + // Map every theme to its closest MUI equivalent (used by stories with `parameters.material`) const muiThemeMap: Record = { light: apolloMaterialUiThemeLight, @@ -107,6 +125,7 @@ const preview: Preview = { initialGlobals: { theme: 'future-dark', reactScan: 'off', + locale: 'en', }, parameters: { backgrounds: { disable: true }, @@ -228,11 +247,23 @@ const preview: Preview = { }, }, }), + // Locale toolbar — sets , which `ApI18nProvider` picks up as its + // fallback locale (see packages/apollo-react/src/i18n/ApI18nProvider.tsx). + locale: { + description: 'Locale for ApI18nProvider (sets )', + toolbar: { + title: 'Locale', + icon: 'globe', + items: SUPPORTED_LOCALES.map((code) => ({ value: code, title: LOCALE_LABELS[code] })), + dynamicTitle: true, + }, + }, }, decorators: [ (Story, context) => { const theme = (context.globals.theme ?? 'future-dark') as ThemeMode; const reactScanEnabled = context.globals.reactScan === 'on'; + const locale = (context.globals.locale ?? 'en') as (typeof SUPPORTED_LOCALES)[number]; // Apply theme class to . All themes (core and element) use : // - Core themes match body.light/body.dark in apollo-core theme-variables.css @@ -258,6 +289,12 @@ const preview: Preview = { } }, [reactScanEnabled]); + // Write synchronously during the decorator render so the freshly-mounted ApI18nProvider + // (forced by the `key={locale}` below) sees the new value on its first render. + if (typeof document !== 'undefined' && document.documentElement.lang !== locale) { + document.documentElement.lang = locale; + } + const isFullscreen = context.parameters?.layout === 'fullscreen'; const useMaterial = context.parameters?.material === true; @@ -269,7 +306,7 @@ const preview: Preview = { -
+
@@ -277,7 +314,7 @@ const preview: Preview = { } return ( -
+
); diff --git a/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx b/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx index 8859f137c..b5b009e35 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx +++ b/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx @@ -16,7 +16,6 @@ import { DropdownMenuTrigger, } from '@uipath/apollo-wind'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { SUPPORTED_LOCALES, type SupportedLocale } from '../../../i18n'; import { DefaultCanvasTranslations } from '../../types'; import { createGroupModificationHandlers, @@ -36,11 +35,9 @@ import { StageHeaderChipType, type StageNodeProps, type StageTaskItem } from './ const DefaultCanvasDecorator = ({ initialNodes, initialEdges = [], - locale, }: { initialNodes: Node[]; initialEdges?: Edge[]; - locale?: SupportedLocale; }) => { const [nodes, _setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -68,7 +65,6 @@ const DefaultCanvasDecorator = ({ nodeTypes={nodeTypes} edgeTypes={edgeTypes} mode="design" - locale={locale} connectionMode={ConnectionMode.Strict} defaultEdgeOptions={defaultEdgeOptions} connectionLineComponent={StageConnectionEdge} @@ -83,9 +79,7 @@ const DefaultCanvasDecorator = ({ ); }; -type StageNodeStoryArgs = StageNodeProps & { locale?: SupportedLocale }; - -const meta: Meta = { +const meta: Meta = { title: 'Components/Nodes/StageNode', component: StageNode, parameters: { @@ -114,13 +108,7 @@ const meta: Meta = { const initialEdges = context.parameters?.edges || []; - return ( - - ); + return ; }, ], args: { @@ -128,14 +116,6 @@ const meta: Meta = { label: 'Default Stage', tasks: [], }, - locale: 'en', - }, - argTypes: { - locale: { - control: 'select', - options: [...SUPPORTED_LOCALES], - description: 'Locale forwarded to ApI18nProvider.', - }, }, }; @@ -543,7 +523,6 @@ export const ExecutionStatus: Story = { execution: { stageStatus: { status: 'NotExecuted', - label: 'Not started', }, taskStatus: {}, }, @@ -1077,7 +1056,7 @@ const initialTasks: StageTaskItem[][] = [ [{ id: 'task-5', label: 'Final Approval', icon: }], ]; -const DraggableTaskReorderingStory = ({ locale }: { locale?: SupportedLocale }) => { +const DraggableTaskReorderingStory = () => { const nodeTypes = useMemo(() => ({ stage: StageNodeWrapper }), []); const edgeTypes = useMemo(() => ({ stage: StageEdge }), []); @@ -1182,7 +1161,6 @@ const DraggableTaskReorderingStory = ({ locale }: { locale?: SupportedLocale }) nodeTypes={nodeTypes} edgeTypes={edgeTypes} mode="design" - locale={locale} connectionMode={ConnectionMode.Strict} defaultEdgeOptions={{ type: 'stage' }} connectionLineComponent={StageConnectionEdge} @@ -1203,7 +1181,7 @@ export const DraggableTaskReordering: Story = { parameters: { useCustomRender: true, }, - render: (args) => , + render: () => , }; const initialTasksForAddReplace: StageTaskItem[][] = [ @@ -1260,7 +1238,7 @@ const availableTaskOptions: ListItem[] = [ }, ]; -const AddAndReplaceTasksStory = ({ locale }: { locale?: SupportedLocale }) => { +const AddAndReplaceTasksStory = () => { const nodeTypes = useMemo(() => ({ stage: StageNodeWrapper }), []); const edgeTypes = useMemo(() => ({ stage: StageEdge }), []); @@ -1566,7 +1544,6 @@ const AddAndReplaceTasksStory = ({ locale }: { locale?: SupportedLocale }) => { nodeTypes={nodeTypes} edgeTypes={edgeTypes} mode="design" - locale={locale} connectionMode={ConnectionMode.Strict} defaultEdgeOptions={{ type: 'stage' }} connectionLineComponent={StageConnectionEdge} @@ -1602,10 +1579,10 @@ export const AddAndReplaceTasks: Story = { parameters: { useCustomRender: true, }, - render: (args) => , + render: () => , }; -const InlineTitleEditStory = ({ locale }: { locale?: SupportedLocale }) => { +const InlineTitleEditStory = () => { const nodeTypes = useMemo(() => ({ stage: StageNodeWrapper }), []); const edgeTypes = useMemo(() => ({ stage: StageEdge }), []); @@ -1696,7 +1673,6 @@ const InlineTitleEditStory = ({ locale }: { locale?: SupportedLocale }) => { nodeTypes={nodeTypes} edgeTypes={edgeTypes} mode="design" - locale={locale} connectionMode={ConnectionMode.Strict} defaultEdgeOptions={{ type: 'stage' }} connectionLineComponent={StageConnectionEdge} @@ -1717,7 +1693,7 @@ export const EditableStageTitle: Story = { parameters: { useCustomRender: true, }, - render: (args) => , + render: () => , }; // Simulate async children fetch (2s delay) @@ -1744,7 +1720,7 @@ const loadedTaskOptionsWithChildren: ListItem[] = [ { id: 'script', name: 'Script', data: { type: 'script' }, children: (id) => fetchChildren(id) }, ]; -const AddTaskLoadingStory = ({ locale }: { locale?: SupportedLocale }) => { +const AddTaskLoadingStory = () => { const nodeTypes = useMemo(() => ({ stage: StageNodeWrapper }), []); const edgeTypes = useMemo(() => ({ stage: StageEdge }), []); const setNodesRef = useRef>>(null!); @@ -1978,7 +1954,6 @@ const AddTaskLoadingStory = ({ locale }: { locale?: SupportedLocale }) => { nodeTypes={nodeTypes} edgeTypes={edgeTypes} mode="design" - locale={locale} connectionMode={ConnectionMode.Strict} defaultEdgeOptions={{ type: 'stage' }} connectionLineComponent={StageConnectionEdge} @@ -1999,7 +1974,7 @@ export const AddTaskLoading: Story = { parameters: { useCustomRender: true, }, - render: (args) => , + render: () => , }; export const AdhocTasks: Story = { diff --git a/packages/apollo-react/src/canvas/components/StageNode/StageNode.test.tsx b/packages/apollo-react/src/canvas/components/StageNode/StageNode.test.tsx index 1c2b82e2c..542913b54 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/StageNode.test.tsx +++ b/packages/apollo-react/src/canvas/components/StageNode/StageNode.test.tsx @@ -1037,6 +1037,42 @@ describe('StageNode - Add Task Button', () => { }); }); +describe('StageNode - Stage status icon tooltip', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders the status icon with the status name as aria-label and tooltip when no host label is supplied', () => { + renderStageNode({ + execution: { stageStatus: { status: 'InProgress' }, taskStatus: {} }, + }); + + const statusButton = screen.getByRole('button', { name: 'In progress' }); + expect(statusButton).toBeInTheDocument(); + }); + + it('uses the host-supplied error label as the aria-label so screen readers match the tooltip', () => { + renderStageNode({ + execution: { + stageStatus: { status: 'Failed', label: 'Activity X threw NullReferenceException' }, + taskStatus: {}, + }, + }); + + expect( + screen.getByRole('button', { name: 'Activity X threw NullReferenceException' }) + ).toBeInTheDocument(); + }); + + it('uses "In progress" as the aria-label for the InProgress status', () => { + renderStageNode({ + execution: { stageStatus: { status: 'InProgress' as const }, taskStatus: {} }, + }); + + expect(screen.getByRole('button', { name: 'In progress' })).toBeInTheDocument(); + }); +}); + describe('StageTitleInput - input attributes', () => { const enterEditMode = async (user: ReturnType) => { await user.click(screen.getByRole('button', { name: 'Test Stage' })); diff --git a/packages/apollo-react/src/canvas/components/StageNode/StageNodeHeader.tsx b/packages/apollo-react/src/canvas/components/StageNode/StageNodeHeader.tsx index 84c8071b6..8e15f0c33 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/StageNodeHeader.tsx +++ b/packages/apollo-react/src/canvas/components/StageNode/StageNodeHeader.tsx @@ -12,6 +12,7 @@ import { StageChip, StageHeader } from './StageNode.styles'; import type { StageNodeProps, StageSlaIcon, StageStatus } from './StageNode.types'; import { StageHeaderChipType } from './StageNode.types'; import { StageTitleInput } from './StageTitleInput'; +import { useExecutionStatusLabel } from './useExecutionStatusLabel'; import { useStageNodeLabels } from './useStageNodeLabels'; const SLA_ICON_CONFIG: Record = { @@ -46,6 +47,7 @@ const StageNodeHeaderInner = ({ handleTaskAddClick: (event: React.MouseEvent) => void; }) => { const labels = useStageNodeLabels(); + const getStatusName = useExecutionStatusLabel(); const { id, stageDetails, @@ -64,6 +66,8 @@ const StageNodeHeaderInner = ({ const slaText = execution?.stageStatus?.slaText; const slaIcon = execution?.stageStatus?.slaIcon; const slaIndicator = slaIcon ? SLA_ICON_CONFIG[slaIcon] : undefined; + const statusFallbackName = status ? getStatusName(status) : ''; + const statusTooltip = statusLabel || statusFallbackName; return ( @@ -79,8 +83,8 @@ const StageNodeHeaderInner = ({ {status && ( - -