Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Designer): Nested Collapse #6494

Merged
merged 8 commits into from
Jan 28, 2025
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
4 changes: 4 additions & 0 deletions Localize/lang/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@
"JMwMaK": "AI-generated content may be incorrect",
"JNQHws": "Required. A string that contains the time.",
"JQBEOg": "Review + create",
"JSbDfI": "Expand nested",
"JSfWJ0": "Required. The value that is converted to a boolean.",
"JUZ7g5": "Run history",
"JVNRly": "Solution type",
Expand Down Expand Up @@ -1666,6 +1667,7 @@
"_JMwMaK.comment": "Disclaimer message on AI-generated content potentially being incorrect",
"_JNQHws.comment": "Required string parameter that contains the time",
"_JQBEOg.comment": "The tab label for the monitoring review and create tab on the create workflow panel",
"_JSbDfI.comment": "Expand text",
"_JSfWJ0.comment": "Required parameter to be converted using bool function",
"_JUZ7g5.comment": "Pivot item header text for run history",
"_JVNRly.comment": "Solution type of the template",
Expand Down Expand Up @@ -2575,6 +2577,7 @@
"_p1IEXb.comment": "Label for button to open dynamic content token picker",
"_p5ZID0.comment": "Time zone value ",
"_pC2nr2.comment": "Placeholder text for Key",
"_pC7/+m.comment": "Collapse text",
"_pH2uak.comment": "Label to collapse",
"_pH6ubt.comment": "Column header for accessing connection-related details",
"_pIczsS.comment": "Label text for request end time",
Expand Down Expand Up @@ -3348,6 +3351,7 @@
"p1IEXb": "Enter the data from previous step. You can also add data by typing the '/' character.",
"p5ZID0": "(UTC+03:00) Kuwait, Riyadh",
"pC2nr2": "Enter key",
"pC7/+m": "Collapse nested",
"pH2uak": "Collapse",
"pH6ubt": "Details",
"pIczsS": "End time",
Expand Down
32 changes: 32 additions & 0 deletions e2e/designer/graphCollapse.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { test, expect } from '@playwright/test';
import { GoToMockWorkflow } from './utils/GoToWorkflow';

test.describe(
'Graph Collapse Tests',
{
tag: '@mock',
},
() => {
test('Should collapse graphs', async ({ page }) => {
await page.goto('/');
await GoToMockWorkflow(page, 'All Scope Nodes');

// Collapse and reopen the foreach condition node, confirming the child node visibility
await expect(page.getByLabel('ForEach operation')).toBeVisible();
await page.getByTestId('ForEach_Case-collapse-toggle').click();
await expect(page.getByLabel('ForEach operation')).not.toBeVisible();
await page.getByTestId('ForEach_Case-collapse-toggle').click();
await expect(page.getByLabel('ForEach operation')).toBeVisible();

// Nested collapse and reopen the foreach condition node
await page.getByTestId('ForEach_Case-collapse-toggle').click({ modifiers: ['Shift'] });
await page.getByTestId('ForEach_Case-collapse-toggle').click();
await expect(page.getByTestId('card-foreach_action_2').getByRole('button', { name: 'ForEach Action' })).not.toBeVisible();

// Collapse and nested reopen the foreach case node
await page.getByTestId('ForEach_Case-collapse-toggle').click();
await page.getByTestId('ForEach_Case-collapse-toggle').click({ modifiers: ['Shift'] });
await expect(page.getByTestId('card-foreach_action_2').getByRole('button', { name: 'ForEach Action' })).toBeVisible();
});
}
);
5 changes: 3 additions & 2 deletions libs/designer-ui/src/lib/card/subgraphCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface SubgraphCardProps {
title: string;
subgraphType: SubgraphType;
collapsed?: boolean;
handleCollapse?: () => void;
handleCollapse?: (includeNested?: boolean) => void;
selectionMode?: CardProps['selectionMode'];
readOnly?: boolean;
onClick?(id?: string): void;
Expand Down Expand Up @@ -179,7 +179,8 @@ export const SubgraphCard: React.FC<SubgraphCardProps> = ({
aria-label={cardAltText}
className={css('msla-subgraph-card', data.size)}
style={colorVars}
onClick={handleCollapse}
onClick={(e) => handleCollapse?.(e.shiftKey)}
onContextMenu={onContextMenu}
onKeyDown={collapseKeyboardInteraction.keyUp}
onKeyUp={collapseKeyboardInteraction.keyDown}
data-automation-id={`${id}-collapse-toggle-small`}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`lib/nodeCollapseToggle > should render collapsed 1`] = `
<StyledTooltipHostBase
content="Expand"
directionalHint={12}
>
<button
aria-label="Expand"
className="msla-collapse-toggle"
data-automation-id="nodeId-collapse-toggle"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
>
<Memo(Icon)
iconName="ChevronDown"
styles={
{
"root": {
"fontSize": "12px",
},
}
}
/>
</button>
</StyledTooltipHostBase>
`;

exports[`lib/nodeCollapseToggle > should render expanded 1`] = `
<StyledTooltipHostBase
content="Collapse"
directionalHint={12}
>
<button
aria-label="Collapse"
className="msla-collapse-toggle"
data-automation-id="nodeId-collapse-toggle"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
>
<Memo(Icon)
iconName="ChevronUp"
styles={
{
"root": {
"fontSize": "12px",
},
}
}
/>
</button>
</StyledTooltipHostBase>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import type { NodeCollapseToggleProps } from '..';
import NodeCollapseToggle from '..';
import * as ReactShallowRenderer from 'react-test-renderer/shallow';

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('lib/nodeCollapseToggle', () => {
let minimal: NodeCollapseToggleProps, renderer: ReactShallowRenderer.ShallowRenderer;

beforeEach(() => {
minimal = {
id: 'nodeId',
};
renderer = ReactShallowRenderer.createRenderer();
});

afterEach(() => {
renderer.unmount();
});

it('should render expanded', () => {
let collapsed = false;
const props: NodeCollapseToggleProps = {
...minimal,
collapsed,
handleCollapse: () => (collapsed = !collapsed),
};
renderer.render(<NodeCollapseToggle {...props} />);
const renderedComponent = renderer.getRenderOutput();
expect(renderedComponent).toMatchSnapshot();
});

it('should render collapsed', () => {
let collapsed = true;
const props: NodeCollapseToggleProps = {
...minimal,
collapsed,
handleCollapse: () => (collapsed = !collapsed),
};
renderer.render(<NodeCollapseToggle {...props} />);
const renderedComponent = renderer.getRenderOutput();
expect(renderedComponent).toMatchSnapshot();
});
});
6 changes: 3 additions & 3 deletions libs/designer-ui/src/lib/nodeCollapseToggle/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { FontSizes } from '@fluentui/theme';
import { useCardKeyboardInteraction } from '../card/hooks';
import { useIntl } from 'react-intl';

interface NodeCollapseToggleProps {
export interface NodeCollapseToggleProps {
disabled?: boolean;
collapsed?: boolean;
onSmallCard?: boolean;
handleCollapse?: () => void;
handleCollapse?: (includeNested?: boolean) => void;
tabIndex?: number;
id: string;
}
Expand Down Expand Up @@ -39,7 +39,7 @@ const NodeCollapseToggle = (props: NodeCollapseToggleProps) => {
aria-label={toggleText}
disabled={disabled}
className={css('msla-collapse-toggle', disabled && 'disabled', onSmallCard && 'small')}
onClick={handleCollapse}
onClick={(e) => handleCollapse?.(e.shiftKey)}
onKeyDown={keyboardInteraction.keyDown}
onKeyUp={keyboardInteraction.keyUp}
tabIndex={disabled ? -1 : tabIndex}
Expand Down
38 changes: 34 additions & 4 deletions libs/designer/src/lib/core/state/workflow/workflowSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,11 +302,41 @@ export const workflowSlice = createSlice({
collapseGraphsToShowNode: (state: WorkflowState, action: PayloadAction<string>) => {
state.collapsedGraphIds = getParentsUncollapseFromGraphState(state, action.payload);
},
toggleCollapsedGraphId: (state: WorkflowState, action: PayloadAction<string>) => {
if (getRecordEntry(state.collapsedGraphIds, action.payload) === true) {
delete state.collapsedGraphIds[action.payload];
toggleCollapsedGraphId: (state: WorkflowState, action: PayloadAction<{ id: string; includeNested?: boolean }>) => {
const expanding = getRecordEntry(state.collapsedGraphIds, action.payload.id) === true;
if (expanding) {
delete state.collapsedGraphIds[action.payload.id];
} else {
state.collapsedGraphIds[action.payload] = true;
state.collapsedGraphIds[action.payload.id] = true;
}

// Iterate over all graph children and set them to the same state
if (action.payload.includeNested) {
const graph = getWorkflowNodeFromGraphState(state, action.payload.id);
if (!graph) {
return;
}
const nestedGraphIds: string[] = [];
const stack: WorkflowNode[] = [graph];
while (stack.length) {
const node = stack.shift();
if (node?.children) {
for (const child of node.children) {
if (child.type === WORKFLOW_NODE_TYPES.GRAPH_NODE || child.type === WORKFLOW_NODE_TYPES.SUBGRAPH_NODE) {
nestedGraphIds.push(child.id);
stack.push(child);
}
}
}
}
for (const id of nestedGraphIds) {
const collapsed = getRecordEntry(state.collapsedGraphIds, id) === true;
if (expanding && collapsed) {
delete state.collapsedGraphIds[id];
} else if (!expanding && !collapsed) {
state.collapsedGraphIds[id] = true;
}
}
}

LoggerService().log({
Expand Down
9 changes: 6 additions & 3 deletions libs/designer/src/lib/ui/CustomNodes/ScopeCardNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,12 @@ const ScopeCardNode = ({ data, targetPosition = Position.Top, sourcePosition = P
}, [dispatch, scopeId]);

const graphCollapsed = useIsGraphCollapsed(scopeId);
const handleGraphCollapse = useCallback(() => {
dispatch(toggleCollapsedGraphId(scopeId));
}, [dispatch, scopeId]);
const handleGraphCollapse = useCallback(
(includeNested?: boolean) => {
dispatch(toggleCollapsedGraphId({ id: scopeId, includeNested }));
},
[dispatch, scopeId]
);

const deleteClick = useCallback(() => {
dispatch(setShowDeleteModalNodeId(scopeId));
Expand Down
9 changes: 6 additions & 3 deletions libs/designer/src/lib/ui/CustomNodes/SubgraphCardNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,12 @@ const SubgraphCardNode = ({ targetPosition = Position.Top, sourcePosition = Posi
);

const graphCollapsed = useIsGraphCollapsed(subgraphId);
const handleGraphCollapse = useCallback(() => {
dispatch(toggleCollapsedGraphId(subgraphId));
}, [dispatch, subgraphId]);
const handleGraphCollapse = useCallback(
(includeNested?: boolean) => {
dispatch(toggleCollapsedGraphId({ id: subgraphId, includeNested }));
},
[dispatch, subgraphId]
);

const showEmptyGraphComponents = isLeaf && !graphCollapsed && !isAddCase;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '@microsoft/logic-apps-shared';

import { useNodeContextMenuData } from '../../../core/state/designerView/designerViewSelectors';
import { DeleteMenuItem, CopyMenuItem, ResubmitMenuItem } from '../../../ui/menuItems';
import { DeleteMenuItem, CopyMenuItem, ResubmitMenuItem, ExpandCollapseMenuItem } from '../../../ui/menuItems';
import { PinMenuItem } from '../../../ui/menuItems/pinMenuItem';
import { RunAfterMenuItem } from '../../../ui/menuItems/runAfterMenuItem';
import { useOperationInfo, type AppDispatch, type RootState } from '../../../core';
Expand Down Expand Up @@ -167,14 +167,25 @@ export const DesignerContextualMenu = () => {
[deleteClick, metadata?.subgraphType]
);

const graphMenuItems: JSX.Element[] = useMemo(() => [<ExpandCollapseMenuItem key={'expand-collapse'} nodeId={nodeId} />], [nodeId]);

const menuItems = useMemo(() => {
// Do-Until is a special case, we show normal action context menu items
const items: JSX.Element[] = [];
if (metadata?.subgraphType === SUBGRAPH_TYPES.UNTIL_DO) {
return actionContextMenuItems;
// Do-Until is a special case, we show normal action context menu items
items.push(...actionContextMenuItems);
} else {
// For all other subgraph types, we show the subgraph context menu items
items.push(...(metadata?.subgraphType ? subgraphMenuItems : actionContextMenuItems));
}
// For all other subgraph types, we show the subgraph context menu items
return metadata?.subgraphType ? subgraphMenuItems : actionContextMenuItems;
}, [metadata, subgraphMenuItems, actionContextMenuItems]);

if (metadata?.subgraphType || isScopeNode) {
// For subgraphs and scope nodes, we show graph context menu items
items.push(...graphMenuItems);
}

return items;
}, [metadata?.subgraphType, isScopeNode, actionContextMenuItems, subgraphMenuItems, graphMenuItems]);

return (
<>
Expand Down
54 changes: 54 additions & 0 deletions libs/designer/src/lib/ui/menuItems/expandCollapseMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { MenuItem } from '@fluentui/react-components';
import {
bundleIcon,
AddSquareMultipleFilled,
AddSquareMultipleRegular,
SubtractSquareMultipleFilled,
SubtractSquareMultipleRegular,
} from '@fluentui/react-icons';
import { removeIdTag } from '@microsoft/logic-apps-shared';
import { toggleCollapsedGraphId } from '../../core/state/workflow/workflowSlice';
import { useIsGraphCollapsed } from '../../core/state/workflow/workflowSelectors';
import { useCallback, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';

const CollapseIcon = bundleIcon(SubtractSquareMultipleFilled, SubtractSquareMultipleRegular);
const ExpandIcon = bundleIcon(AddSquareMultipleFilled, AddSquareMultipleRegular);

export interface ExpandCollapseMenuItemProps {
key: string;
nodeId: string;
}

export const ExpandCollapseMenuItem = (props: ExpandCollapseMenuItemProps) => {
const { key, nodeId } = props;

const graphId = useMemo(() => removeIdTag(nodeId), [nodeId]);
const expanded = !useIsGraphCollapsed(graphId);

const intl = useIntl();
const dispatch = useDispatch();

const expandText = intl.formatMessage({
defaultMessage: 'Expand nested',
id: 'JSbDfI',
description: 'Expand text',
});

const collapseText = intl.formatMessage({
defaultMessage: 'Collapse nested',
id: 'pC7/+m',
description: 'Collapse text',
});

const onClick = useCallback(() => {
dispatch(toggleCollapsedGraphId({ id: nodeId, includeNested: true }));
}, [dispatch, nodeId]);

return (
<MenuItem key={key} icon={expanded ? <CollapseIcon /> : <ExpandIcon />} onClick={onClick}>
{expanded ? collapseText : expandText}
</MenuItem>
);
};
1 change: 1 addition & 0 deletions libs/designer/src/lib/ui/menuItems/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './commentMenuItem';
export * from './copyMenuItem';
export * from './deleteMenuItem';
export * from './resubmitMenuItem';
export * from './expandCollapseMenuItem';
Loading