Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Added

- Extended existing height and readOnly props from `CodeEditorProps` to `AutoSuggestionProps` & `ExtendedCodeEditorProps` to be configurable from `<CodeAutocompleteField />`
- Added additional actions to `<ActivityControlWidget />`

## [24.3.0] - 2025-06-05

Expand Down
48 changes: 43 additions & 5 deletions src/cmem/ActivityControl/ActivityControlWidget.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import React from "react";
import React, { useMemo, useState } from "react";
import { loremIpsum } from "react-lorem-ipsum";
import { OverlaysProvider } from "@blueprintjs/core";
import { Meta, StoryFn } from "@storybook/react";

import { helpersArgTypes } from "../../../.storybook/helpers";
import { ActivityControlWidget, Tag, TagList } from "../../../index";
import {
ActivityControlWidget,
ActivityControlWidgetAction,
IconButton,
SimpleDialog,
Tag,
TagList,
} from "../../../index";

export default {
title: "Cmem/ActivityControlWidget",
Expand All @@ -19,7 +27,7 @@ const Template: StoryFn<typeof ActivityControlWidget> = (args) => <ActivityContr

export const FullExample = Template.bind({});

const actions = [
const actions: ActivityControlWidgetAction[] = [
{
"data-test-id": "activity-reload-activity",
icon: "item-reload",
Expand Down Expand Up @@ -57,8 +65,8 @@ const commonWidgetArgs = {
progressSpinner: {
intent: "none",
value: 0.5,
},
};
} as const,
} as const;

FullExample.args = {
...commonWidgetArgs,
Expand All @@ -80,3 +88,33 @@ WidgetWithTags.args = {
...commonWidgetArgs,
tags: widgetTags,
};

export const WidgetWithAdditionalActions: StoryFn<typeof ActivityControlWidget> = (args) => {
const [isOpen, setIsOpen] = useState(false);

const params = useMemo(
() => ({
...commonWidgetArgs,
...args,
additionalActions: args.additionalActions ?? [
<IconButton name="application-explore" onClick={() => setIsOpen(true)} />,
],
}),
[]
);

return (
<OverlaysProvider>
<ActivityControlWidget {...params} />
<SimpleDialog
title="Additional actions dialog"
isOpen={isOpen}
onClose={() => setIsOpen(false)}
canOutsideClickClose
canEscapeKeyClose
>
Modal content
</SimpleDialog>
</OverlaysProvider>
);
};
53 changes: 44 additions & 9 deletions src/cmem/ActivityControl/ActivityControlWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ export interface ActivityControlWidgetProps extends TestableComponent {
* execution timer messages for waiting and running times.
*/
timerExecutionMsg?: JSX.Element | null;
/**
* additional actions that can serve as a complex component, positioned between the default actions and the context menu
*/
additionalActions?: React.ReactElement<unknown>[];
}

interface IActivityContextMenu extends TestableComponent {
Expand Down Expand Up @@ -110,11 +114,13 @@ interface IActivityMenuAction extends ActivityControlWidgetAction {
/** Shows the status of activities and supports actions on these activities. */
export function ActivityControlWidget(props: ActivityControlWidgetProps) {
const {
"data-test-id": dataTestId,
"data-test-id": dataTestIdLegacy,
"data-testid": dataTestId,
progressBar,
progressSpinner,
activityActions,
activityContextMenu,
additionalActions,
small,
border,
hasSpacing,
Expand All @@ -126,10 +132,19 @@ export function ActivityControlWidget(props: ActivityControlWidgetProps) {
} = props;
const spinnerClassNames = (progressSpinner?.className ?? "") + ` ${eccgui}-spinner--permanent`;
const widget = (
<OverviewItem data-test-id={dataTestId} hasSpacing={border || hasSpacing} densityHigh={small}>
<OverviewItem
data-test-id={dataTestIdLegacy}
data-testid={dataTestId}
hasSpacing={border || hasSpacing}
densityHigh={small}
>
{progressBar && <ProgressBar {...progressBar} />}
{(progressSpinner || progressSpinnerFinishedIcon) && (
<OverviewItemDepiction keepColors>
<OverviewItemDepiction
data-testid={`${dataTestId}-progress-spinner`}
Comment thread
fiorsaoirse marked this conversation as resolved.
Outdated
data-test-id={`${dataTestIdLegacy}-progress-spinner`}
keepColors
>
{progressSpinnerFinishedIcon ? (
React.cloneElement(progressSpinnerFinishedIcon as JSX.Element, { small, large: !small })
) : (
Expand All @@ -145,13 +160,21 @@ export function ActivityControlWidget(props: ActivityControlWidgetProps) {
)}
<OverviewItemDescription>
{props.label && (
<OverviewItemLine small={small}>
<OverviewItemLine
data-testid={`${dataTestId}-label`}
Comment thread
fiorsaoirse marked this conversation as resolved.
Outdated
data-test-id={`${dataTestIdLegacy}-label`}
small={small}
>
{React.cloneElement(labelWrapper, {}, props.label)}
{timerExecutionMsg && (props.statusMessage || tags) && <>&nbsp;({timerExecutionMsg})</>}
</OverviewItemLine>
)}
{(props.statusMessage || tags) && (
<OverviewItemLine small>
<OverviewItemLine
data-testid={`${dataTestId}-status-message`}
Comment thread
fiorsaoirse marked this conversation as resolved.
Outdated
data-test-id={`${dataTestIdLegacy}-status-message`}
small
>
{tags}
{props.statusMessage && (
<OverflowText passDown>
Expand All @@ -172,28 +195,40 @@ export function ActivityControlWidget(props: ActivityControlWidgetProps) {
</OverviewItemLine>
)}
{timerExecutionMsg && !(props.statusMessage || tags) && (
<OverviewItemLine small>{timerExecutionMsg}</OverviewItemLine>
<OverviewItemLine
data-testid={`${dataTestId}-status-message`}
Comment thread
fiorsaoirse marked this conversation as resolved.
Outdated
data-test-id={`${dataTestIdLegacy}-status-message`}
small
>
{timerExecutionMsg}
</OverviewItemLine>
)}
</OverviewItemDescription>
<OverviewItemActions>
<OverviewItemActions data-testid={`${dataTestId}-actions`} data-test-id={`${dataTestIdLegacy}-actions`}>
Comment thread
fiorsaoirse marked this conversation as resolved.
Outdated
{activityActions &&
activityActions.map((action, idx) => {
return (
<IconButton
key={typeof action.icon === "string" ? action.icon : action["data-test-id"] ?? idx}
key={
typeof action.icon === "string"
? action.icon
: action["data-test-id"] ?? action["data-testid"] ?? idx
}
data-test-id={action["data-test-id"]}
data-testid={action["data-testid"]}
name={action.icon}
text={action.tooltip}
onClick={action.action}
disabled={action.disabled}
hasStateWarning={action.hasStateWarning}
intent={action.hasStateWarning ? "warning" : undefined}
tooltipProps={{
hoverOpenDelay: 200,
placement: "bottom",
}}
/>
);
})}
{additionalActions}
{activityContextMenu && activityContextMenu.menuItems.length > 0 && (
<ContextMenu
data-test-id={activityContextMenu["data-test-id"]}
Expand Down
99 changes: 99 additions & 0 deletions src/cmem/ActivityControl/tests/ActivityControlWidget.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from "react";
Comment thread
haschek marked this conversation as resolved.
import { fireEvent, render, screen } from "@testing-library/react";

import "@testing-library/jest-dom";

import { IconButton, Tag, TagList } from "../../../index";
import { ActivityControlWidget, ActivityControlWidgetAction } from "../ActivityControlWidget";

describe("ActivityControlWidget", () => {
it("Renders basic widget with actions and handles clicks", () => {
const mockAction1 = jest.fn();
const mockAction2 = jest.fn();
const actions: ActivityControlWidgetAction[] = [
{
"data-testid": "action-1",
icon: "item-reload",
action: mockAction1,
tooltip: "Action 1",
},
{
"data-testid": "action-2",
icon: "item-start",
action: mockAction2,
tooltip: "Action 2",
},
];

render(
<ActivityControlWidget
label="Basic widget"
data-testid="basic-widget"
activityActions={actions}
statusMessage="Status message"
/>
);

const button1 = screen.getByTestId("action-1");
const button2 = screen.getByTestId("action-2");

const label = screen.getByTestId("basic-widget-label");
const statusMessage = screen.getByTestId("basic-widget-status-message");
const actionsContainer = screen.getByTestId("basic-widget-actions");

expect(label).toBeInTheDocument();
expect(statusMessage).toBeInTheDocument();
expect(actionsContainer).toBeInTheDocument();

expect(label).toHaveTextContent("Basic widget");
expect(statusMessage).toHaveTextContent("Status message");

expect(button1).toBeInTheDocument();
expect(button2).toBeInTheDocument();

fireEvent.click(button1);
expect(mockAction1).toHaveBeenCalledTimes(1);

fireEvent.click(button2);
expect(mockAction2).toHaveBeenCalledTimes(1);
});

it("Renders widget with tags", () => {
const tags = (
<TagList>
<Tag>Tag one</Tag>
<Tag>Other tag</Tag>
</TagList>
);
render(<ActivityControlWidget label="Widget with tags" tags={tags} data-testid="widget-with-tags" />);

const label = screen.getByTestId("widget-with-tags-label");
const statusMessage = screen.getByTestId("widget-with-tags-status-message");

expect(label).toBeInTheDocument();
expect(statusMessage).toBeInTheDocument();

expect(label).toHaveTextContent("Widget with tags");
expect(statusMessage).toHaveTextContent("Tag one");
expect(statusMessage).toHaveTextContent("Other tag");
});

it("Renders widget with additional actions and handles click", () => {
const mockAction = jest.fn();
const additionalActions = [
<IconButton
key="add-btn"
name="application-explore"
onClick={mockAction}
data-testid="additional-action"
/>,
];
render(<ActivityControlWidget additionalActions={additionalActions} />);

const customButton = screen.getByTestId("additional-action");
expect(customButton).toBeInTheDocument();

fireEvent.click(customButton);
expect(mockAction).toHaveBeenCalledTimes(1);
});
});