Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions apps/storybook/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -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';
Comment thread
ayush578 marked this conversation as resolved.
import {
apolloMaterialUiThemeDark,
apolloMaterialUiThemeDarkHC,
Expand Down Expand Up @@ -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> = {
Comment thread
ayush578 marked this conversation as resolved.
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<ThemeMode, typeof apolloMaterialUiThemeLight> = {
light: apolloMaterialUiThemeLight,
Expand Down Expand Up @@ -107,6 +125,7 @@ const preview: Preview = {
initialGlobals: {
theme: 'future-dark',
reactScan: 'off',
locale: 'en',
},
parameters: {
backgrounds: { disable: true },
Expand Down Expand Up @@ -228,11 +247,23 @@ const preview: Preview = {
},
},
}),
// Locale toolbar — sets <html lang>, which `ApI18nProvider` picks up as its
// fallback locale (see packages/apollo-react/src/i18n/ApI18nProvider.tsx).
locale: {
description: 'Locale for ApI18nProvider (sets <html lang>)',
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 <body>. All themes (core and element) use <body>:
// - Core themes match body.light/body.dark in apollo-core theme-variables.css
Expand All @@ -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;
Comment thread
ayush578 marked this conversation as resolved.
}

const isFullscreen = context.parameters?.layout === 'fullscreen';
const useMaterial = context.parameters?.material === true;

Expand All @@ -269,15 +306,15 @@ const preview: Preview = {
<ThemeProvider theme={muiTheme}>
<CssBaseline />
<GlobalStyles />
<div style={{ height: '100%', width: '100%' }}>
<div key={locale} style={{ height: '100%', width: '100%' }}>
<Story />
</div>
</ThemeProvider>
);
}

return (
<div className={isFullscreen ? 'h-screen' : 'p-1'}>
<div key={locale} className={isFullscreen ? 'h-screen' : 'p-1'}>
<Story />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -68,7 +65,6 @@ const DefaultCanvasDecorator = ({
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
mode="design"
locale={locale}
connectionMode={ConnectionMode.Strict}
defaultEdgeOptions={defaultEdgeOptions}
connectionLineComponent={StageConnectionEdge}
Expand All @@ -83,9 +79,7 @@ const DefaultCanvasDecorator = ({
);
};

type StageNodeStoryArgs = StageNodeProps & { locale?: SupportedLocale };

const meta: Meta<StageNodeStoryArgs> = {
const meta: Meta<StageNodeProps> = {
title: 'Components/Nodes/StageNode',
component: StageNode,
parameters: {
Expand Down Expand Up @@ -114,28 +108,14 @@ const meta: Meta<StageNodeStoryArgs> = {

const initialEdges = context.parameters?.edges || [];

return (
<DefaultCanvasDecorator
initialNodes={initialNodes}
initialEdges={initialEdges}
locale={context.args.locale}
/>
);
return <DefaultCanvasDecorator initialNodes={initialNodes} initialEdges={initialEdges} />;
},
],
args: {
stageDetails: {
label: 'Default Stage',
tasks: [],
},
locale: 'en',
},
argTypes: {
locale: {
control: 'select',
options: [...SUPPORTED_LOCALES],
description: 'Locale forwarded to ApI18nProvider.',
},
},
};

Expand Down Expand Up @@ -543,7 +523,6 @@ export const ExecutionStatus: Story = {
execution: {
stageStatus: {
status: 'NotExecuted',
label: 'Not started',
},
taskStatus: {},
},
Expand Down Expand Up @@ -1077,7 +1056,7 @@ const initialTasks: StageTaskItem[][] = [
[{ id: 'task-5', label: 'Final Approval', icon: <ProcessIcon /> }],
];

const DraggableTaskReorderingStory = ({ locale }: { locale?: SupportedLocale }) => {
const DraggableTaskReorderingStory = () => {
const nodeTypes = useMemo(() => ({ stage: StageNodeWrapper }), []);
const edgeTypes = useMemo(() => ({ stage: StageEdge }), []);

Expand Down Expand Up @@ -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}
Expand All @@ -1203,7 +1181,7 @@ export const DraggableTaskReordering: Story = {
parameters: {
useCustomRender: true,
},
render: (args) => <DraggableTaskReorderingStory locale={args.locale} />,
render: () => <DraggableTaskReorderingStory />,
};

const initialTasksForAddReplace: StageTaskItem[][] = [
Expand Down Expand Up @@ -1260,7 +1238,7 @@ const availableTaskOptions: ListItem[] = [
},
];

const AddAndReplaceTasksStory = ({ locale }: { locale?: SupportedLocale }) => {
const AddAndReplaceTasksStory = () => {
const nodeTypes = useMemo(() => ({ stage: StageNodeWrapper }), []);
const edgeTypes = useMemo(() => ({ stage: StageEdge }), []);

Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -1602,10 +1579,10 @@ export const AddAndReplaceTasks: Story = {
parameters: {
useCustomRender: true,
},
render: (args) => <AddAndReplaceTasksStory locale={args.locale} />,
render: () => <AddAndReplaceTasksStory />,
};

const InlineTitleEditStory = ({ locale }: { locale?: SupportedLocale }) => {
const InlineTitleEditStory = () => {
const nodeTypes = useMemo(() => ({ stage: StageNodeWrapper }), []);
const edgeTypes = useMemo(() => ({ stage: StageEdge }), []);

Expand Down Expand Up @@ -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}
Expand All @@ -1717,7 +1693,7 @@ export const EditableStageTitle: Story = {
parameters: {
useCustomRender: true,
},
render: (args) => <InlineTitleEditStory locale={args.locale} />,
render: () => <InlineTitleEditStory />,
};

// Simulate async children fetch (2s delay)
Expand All @@ -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<React.Dispatch<React.SetStateAction<Node[]>>>(null!);
Expand Down Expand Up @@ -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}
Expand All @@ -1999,7 +1974,7 @@ export const AddTaskLoading: Story = {
parameters: {
useCustomRender: true,
},
render: (args) => <AddTaskLoadingStory locale={args.locale} />,
render: () => <AddTaskLoadingStory />,
};

export const AdhocTasks: Story = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof userEvent.setup>) => {
await user.click(screen.getByRole('button', { name: 'Test Stage' }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<StageSlaIcon, { icon: string; iconColor: string }> = {
Expand Down Expand Up @@ -46,6 +47,7 @@ const StageNodeHeaderInner = ({
handleTaskAddClick: (event: React.MouseEvent) => void;
}) => {
const labels = useStageNodeLabels();
const getStatusName = useExecutionStatusLabel();
const {
id,
stageDetails,
Expand All @@ -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 (
<StageHeader isException={isException} data-testid={`stage-header-${id}`}>
Expand All @@ -79,8 +83,8 @@ const StageNodeHeaderInner = ({
</Row>
<Row gap={Spacing.SpacingMicro} align="start" py={Padding.PadS}>
{status && (
<CanvasTooltip content={statusLabel} placement="top">
<Button variant="ghost" size="icon" className="h-6 w-6" aria-label={statusLabel}>
<CanvasTooltip content={statusTooltip} placement="top">
<Button variant="ghost" size="icon" className="h-6 w-6" aria-label={statusTooltip}>
{status === 'NotExecuted' ? (
<CanvasIcon
icon="hourglass"
Expand Down
Loading
Loading