Skip to content

Commit

Permalink
feat: support creating actions from ejected schema in Visual Editor (#…
Browse files Browse the repository at this point in the history
…2806)

* bring 'Custom Actions' menu

* move `createStepMenu` into visual editor

* remove EventMenu (outdated)

* refactor: EdgeMenu util

* define 'oneOf' type

* connect to global customSchemas

* load customSchema files

* create a schema hook

* merge custom schemas in FormEditor

* add comments

* rename variable customSchemas

* fix lint

* revert some changes

* calculate schema diff as custom schema

* fix lint

Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com>
  • Loading branch information
yeze322 and cwhitten authored Apr 30, 2020
1 parent 373aefa commit f486808
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 233 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
// Licensed under the MIT License.

/** @jsx jsx */
import { jsx, css } from '@emotion/core';
import { jsx } from '@emotion/core';
import { useCallback, useContext, useState } from 'react';
import classnames from 'classnames';
import formatMessage from 'format-message';
import { createStepMenu, DialogGroup, SDKKinds } from '@bfc/shared';
import { IContextualMenu, ContextualMenuItemType } from 'office-ui-fabric-react/lib/ContextualMenu';
import { FontIcon } from 'office-ui-fabric-react/lib/Icon';
import { DefinitionSummary } from '@bfc/shared';

import { EdgeAddButtonSize } from '../../constants/ElementSizes';
import { NodeRendererContext } from '../../store/NodeRendererContext';
Expand All @@ -19,92 +17,16 @@ import { MenuTypes } from '../../constants/MenuTypes';
import { ObiColors } from '../../constants/ElementColors';

import { IconMenu } from './IconMenu';
import { createActionMenu } from './createSchemaMenu';

interface EdgeMenuProps {
id: string;
onClick: (item: string | null) => void;
addCoachMarkRef?: (ref: { [key: string]: HTMLDivElement }) => void;
}

const buildEdgeMenuItemsFromClipboardContext = (
context,
onClick,
filter?: (t: SDKKinds) => boolean
): IContextualMenu[] => {
const { clipboardActions } = context;
const menuItems = createStepMenu(
[
DialogGroup.RESPONSE,
DialogGroup.INPUT,
DialogGroup.BRANCHING,
DialogGroup.LOOPING,
DialogGroup.STEP,
DialogGroup.MEMORY,
DialogGroup.CODE,
DialogGroup.LOG,
],
true,
(e, item) => onClick(item ? item.data.$kind : null),
context.dialogFactory,
filter
);

const enablePaste = Array.isArray(clipboardActions) && clipboardActions.length > 0;
const menuItemCount = menuItems.length;
menuItems.unshift(
{
key: 'Paste',
name: 'Paste',
ariaLabel: 'Paste',
disabled: !enablePaste,
onRender: () => {
return (
<button
disabled={!enablePaste}
role="menuitem"
name="Paste"
aria-posinset={1}
aria-setsize={menuItemCount + 1}
css={css`
color: ${enablePaste ? '#0078D4' : '#BDBDBD'};
background: #fff;
width: 100%;
height: 36px;
line-height: 36px;
border-style: none;
text-align: left;
padding: 0 8px;
outline: none;
&:hover {
background: rgb(237, 235, 233);
}
`}
onClick={() => onClick('PASTE')}
>
<div>
<FontIcon
iconName="Paste"
css={css`
margin-right: 4px;
`}
/>
<span>Paste</span>
</div>
</button>
);
},
},
{
key: 'divider',
itemType: ContextualMenuItemType.Divider,
}
);

return menuItems;
};

export const EdgeMenu: React.FC<EdgeMenuProps> = ({ id, addCoachMarkRef, onClick, ...rest }) => {
const nodeContext = useContext(NodeRendererContext);
const { clipboardActions, customSchemas } = useContext(NodeRendererContext);
const selfHosted = useContext(SelfHostContext);
const { selectedIds } = useContext(SelectionContext);
const nodeSelected = selectedIds.includes(`${id}${MenuTypes.EdgeMenu}`);
Expand All @@ -124,6 +46,19 @@ export const EdgeMenu: React.FC<EdgeMenuProps> = ({ id, addCoachMarkRef, onClick
const handleMenuShow = menuSelected => {
setMenuSelected(menuSelected);
};

const menuItems = createActionMenu(
item => {
if (!item) return;
onClick(item.key);
},
{
isSelfHosted: selfHosted,
enablePaste: Array.isArray(clipboardActions) && !!clipboardActions.length,
},
// Custom Action 'oneOf' arrays from schema file
customSchemas.map(x => x.oneOf).filter(oneOf => Array.isArray(oneOf) && oneOf.length) as DefinitionSummary[][]
);
return (
<div
ref={addRef}
Expand Down Expand Up @@ -157,11 +92,7 @@ export const EdgeMenu: React.FC<EdgeMenuProps> = ({ id, addCoachMarkRef, onClick
}}
iconSize={7}
nodeSelected={nodeSelected}
menuItems={buildEdgeMenuItemsFromClipboardContext(
nodeContext,
onClick,
selfHosted ? x => x !== SDKKinds.LogAction : undefined
)}
menuItems={menuItems}
label={formatMessage('Add')}
handleMenuShow={handleMenuShow}
{...rest}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
import { jsx, css } from '@emotion/core';
import {
IContextualMenuItem,
ContextualMenuItemType,
} from 'office-ui-fabric-react/lib/components/ContextualMenu/ContextualMenu.types';
import { ConceptLabels, DialogGroup, dialogGroups, SDKKinds, DefinitionSummary } from '@bfc/shared';
import { FontIcon } from 'office-ui-fabric-react/lib/Icon';
import formatMessage from 'format-message';

const resolveMenuTitle = ($kind: SDKKinds): string => {
const conceptLabel = ConceptLabels[$kind];
return conceptLabel?.title || $kind;
};

type ActionMenuItemClickHandler = (item?: IContextualMenuItem) => any;
type ActionKindFilter = ($kind: SDKKinds) => boolean;

const createBaseActionMenu = (
onClick: ActionMenuItemClickHandler,
filter?: ActionKindFilter
): IContextualMenuItem[] => {
const pickedGroups: DialogGroup[] = [
DialogGroup.RESPONSE,
DialogGroup.INPUT,
DialogGroup.BRANCHING,
DialogGroup.LOOPING,
DialogGroup.STEP,
DialogGroup.MEMORY,
DialogGroup.CODE,
DialogGroup.LOG,
];
const stepMenuItems = pickedGroups
.map(key => dialogGroups[key])
.filter(groupItem => groupItem && Array.isArray(groupItem.types) && groupItem.types.length)
.map(({ label, types: actionKinds }) => {
const subMenuItems: IContextualMenuItem[] = actionKinds
.filter($kind => (filter ? filter($kind) : true))
.map($kind => ({
key: $kind,
name: resolveMenuTitle($kind),
onClick: (e, itemData) => onClick(itemData),
}));

if (subMenuItems.length === 1) {
// hoists the only item to upper level
return subMenuItems[0];
}
return createSubMenu(label, onClick, subMenuItems);
});
return stepMenuItems;
};

const createDivider = () => ({
key: 'divider',
itemType: ContextualMenuItemType.Divider,
});

const get$kindFrom$ref = ($ref: string): SDKKinds => {
return $ref.replace('#/definitions/', '') as SDKKinds;
};

const createCustomActionSubMenu = (
customizedActionGroups: DefinitionSummary[][],
onClick: ActionMenuItemClickHandler
): IContextualMenuItem[] => {
if (!Array.isArray(customizedActionGroups) || customizedActionGroups.length === 0) {
return [];
}

const itemGroups: IContextualMenuItem[][] = customizedActionGroups
.filter(actionGroup => Array.isArray(actionGroup) && actionGroup.length)
.map(actionGroup => {
return actionGroup.map(
({ title, $ref }) =>
({
key: get$kindFrom$ref($ref),
name: title,
onClick: (e, itemData) => onClick(itemData),
} as IContextualMenuItem)
);
});

const flatMenuItems: IContextualMenuItem[] = itemGroups.reduce((resultItems, currentGroup, currentIndex) => {
if (currentIndex !== 0) {
// push a sep line ahead.
resultItems.push(createDivider());
}
resultItems.push(...currentGroup);
return resultItems;
}, []);

return flatMenuItems;
};

const createPasteButtonItem = (
menuItemCount: number,
disabled: boolean,
onClick: ActionMenuItemClickHandler
): IContextualMenuItem => {
return {
key: 'Paste',
name: 'Paste',
ariaLabel: 'Paste',
disabled: disabled,
onRender: () => {
return (
<button
disabled={disabled}
role="menuitem"
name="Paste"
aria-posinset={1}
aria-setsize={menuItemCount + 1}
css={css`
color: ${disabled ? '#BDBDBD' : '#0078D4'};
background: #fff;
width: 100%;
height: 36px;
line-height: 36px;
border-style: none;
text-align: left;
padding: 0 8px;
outline: none;
&:hover {
background: rgb(237, 235, 233);
}
`}
onClick={() => onClick({ key: 'Paste' })}
>
<div>
<FontIcon
iconName="Paste"
css={css`
margin-right: 4px;
`}
/>
<span>Paste</span>
</div>
</button>
);
},
};
};

interface ActionMenuOptions {
isSelfHosted: boolean;
enablePaste: boolean;
}

const createSubMenu = (
label: string,
onClick: ActionMenuItemClickHandler,
subItems: IContextualMenuItem[]
): IContextualMenuItem => {
return {
key: label,
text: label,
subMenuProps: {
items: subItems,
onItemClick: (e, itemData) => onClick(itemData),
},
};
};

export const createActionMenu = (
onClick: ActionMenuItemClickHandler,
options: ActionMenuOptions,
customActionGroups?: DefinitionSummary[][]
) => {
const resultItems: IContextualMenuItem[] = [];

// base SDK menu
const baseMenuItems = createBaseActionMenu(
onClick,
options.isSelfHosted ? ($kind: SDKKinds) => $kind !== SDKKinds.LogAction : undefined
);
resultItems.push(...baseMenuItems);

// Append a 'Custom Actions' item conditionally.
if (customActionGroups) {
const customActionItems = createCustomActionSubMenu(customActionGroups, onClick);
if (customActionItems.length) {
const customActionTitle = formatMessage('Custom Actions');
resultItems.push(createSubMenu(customActionTitle, onClick, customActionItems));
}
}

// paste button
const pasteButtonDisabled = !options.enablePaste;
const pasteButton = createPasteButtonItem(resultItems.length, pasteButtonDisabled, onClick);
resultItems.unshift(pasteButton, createDivider());

return resultItems;
};
Loading

0 comments on commit f486808

Please sign in to comment.