diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f0d096ae..942330234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` +- Added additional actions to `` ## [24.3.0] - 2025-06-05 diff --git a/src/cmem/ActivityControl/ActivityControlWidget.stories.tsx b/src/cmem/ActivityControl/ActivityControlWidget.stories.tsx index 21c1bd13c..952955b33 100644 --- a/src/cmem/ActivityControl/ActivityControlWidget.stories.tsx +++ b/src/cmem/ActivityControl/ActivityControlWidget.stories.tsx @@ -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", @@ -19,7 +27,7 @@ const Template: StoryFn = (args) => = (args) => { + const [isOpen, setIsOpen] = useState(false); + + const params = useMemo( + () => ({ + ...commonWidgetArgs, + ...args, + additionalActions: args.additionalActions ?? [ + setIsOpen(true)} />, + ], + }), + [] + ); + + return ( + + + setIsOpen(false)} + canOutsideClickClose + canEscapeKeyClose + > + Modal content + + + ); +}; diff --git a/src/cmem/ActivityControl/ActivityControlWidget.tsx b/src/cmem/ActivityControl/ActivityControlWidget.tsx index e59ef64ca..efda5060f 100644 --- a/src/cmem/ActivityControl/ActivityControlWidget.tsx +++ b/src/cmem/ActivityControl/ActivityControlWidget.tsx @@ -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[]; } interface IActivityContextMenu extends TestableComponent { @@ -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, @@ -126,10 +132,19 @@ export function ActivityControlWidget(props: ActivityControlWidgetProps) { } = props; const spinnerClassNames = (progressSpinner?.className ?? "") + ` ${eccgui}-spinner--permanent`; const widget = ( - + {progressBar && } {(progressSpinner || progressSpinnerFinishedIcon) && ( - + {progressSpinnerFinishedIcon ? ( React.cloneElement(progressSpinnerFinishedIcon as JSX.Element, { small, large: !small }) ) : ( @@ -145,13 +160,21 @@ export function ActivityControlWidget(props: ActivityControlWidgetProps) { )} {props.label && ( - + {React.cloneElement(labelWrapper, {}, props.label)} {timerExecutionMsg && (props.statusMessage || tags) && <> ({timerExecutionMsg})} )} {(props.statusMessage || tags) && ( - + {tags} {props.statusMessage && ( @@ -172,21 +195,35 @@ export function ActivityControlWidget(props: ActivityControlWidgetProps) { )} {timerExecutionMsg && !(props.statusMessage || tags) && ( - {timerExecutionMsg} + + {timerExecutionMsg} + )} - + {activityActions && activityActions.map((action, idx) => { return ( ); })} + {additionalActions} {activityContextMenu && activityContextMenu.menuItems.length > 0 && ( { + 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( + + ); + + 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 = ( + + Tag one + Other tag + + ); + render(); + + 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 = [ + , + ]; + render(); + + const customButton = screen.getByTestId("additional-action"); + expect(customButton).toBeInTheDocument(); + + fireEvent.click(customButton); + expect(mockAction).toHaveBeenCalledTimes(1); + }); +});