From dbe465798526ea0ff354cc77a4028c747d91a1e1 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Mon, 17 Feb 2020 19:57:08 +0800 Subject: [PATCH 01/32] draft: DynamicBlock --- .../extensions/visual-designer/package.json | 1 + .../visual-designer/src/schema/uischema.tsx | 3 ++- .../src/widgets/DynamicBlock.tsx | 23 +++++++++++++++++++ .../src/widgets/IfConditionWidget.tsx | 10 +++++++- 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 Composer/packages/extensions/visual-designer/src/widgets/DynamicBlock.tsx diff --git a/Composer/packages/extensions/visual-designer/package.json b/Composer/packages/extensions/visual-designer/package.json index 10909e14cc..acb0e94a99 100644 --- a/Composer/packages/extensions/visual-designer/package.json +++ b/Composer/packages/extensions/visual-designer/package.json @@ -36,6 +36,7 @@ "lodash": "^4.17.15", "office-ui-fabric-react": "7.62.0", "prop-types": "^15.7.2", + "react-measure": "^2.3.0", "source-map-loader": "^0.2.4" }, "peerDependencies": { diff --git a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx index 879cce4f58..53bb18da59 100644 --- a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx +++ b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx @@ -17,6 +17,7 @@ import { ChoiceInputChoices } from '../widgets/ChoiceInput'; import { ElementIcon } from '../utils/obiPropertyResolver'; import { ObiColors } from '../constants/ElementColors'; import { measureChoiceInputDetailBoundary } from '../layouters/measureJsonBoundary'; +import { DynamicBlock } from '../widgets/DynamicBlock'; import { UISchema, UIWidget } from './uischema.types'; @@ -56,7 +57,7 @@ export const uiSchema: UISchema = { [SDKTypes.IfCondition]: { 'ui:widget': IfConditionWidget, judgement: { - 'ui:widget': ActionCard, + 'ui:widget': DynamicBlock, title: formatMessage('Branch'), content: data => data.condition, }, diff --git a/Composer/packages/extensions/visual-designer/src/widgets/DynamicBlock.tsx b/Composer/packages/extensions/visual-designer/src/widgets/DynamicBlock.tsx new file mode 100644 index 0000000000..d6bbc607eb --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/widgets/DynamicBlock.tsx @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React, { useState } from 'react'; + +import { WidgetContainerProps } from '../schema/uischema.types'; + +export type DynamicBlockProps = WidgetContainerProps; + +export const DynamicBlock = () => { + const [n, setN] = useState(0); + const texts = new Array(n).fill('hello'); + return ( +
+ {n} + + + {texts.map((text, index) => ( +

{text}

+ ))} +
+ ); +}; diff --git a/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx index e991d35025..319f114de4 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx @@ -4,6 +4,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import { FunctionComponent, useEffect, useState, useMemo } from 'react'; +import Measure from 'react-measure'; import { transformIfCondtion } from '../transformers/transformIfCondition'; import { ifElseLayouter } from '../layouters/ifelseLayouter'; @@ -78,7 +79,14 @@ export const IfConditionWidget: FunctionComponent = ({
- {judgement} + { + console.log(contentRect); + }} + > + {({ measureRef }) =>
{judgement}
} +
From 9dd4d758fb6195ef51170748b84d11497ef7e282 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Tue, 18 Feb 2020 17:40:06 +0800 Subject: [PATCH 02/32] don't setState in effect hook --- Composer/packages/client/.gitignore | 1 + .../extensions/visual-designer/src/editors/ObiEditor.tsx | 8 -------- 2 files changed, 1 insertion(+), 8 deletions(-) create mode 100644 Composer/packages/client/.gitignore diff --git a/Composer/packages/client/.gitignore b/Composer/packages/client/.gitignore new file mode 100644 index 0000000000..d16386367f --- /dev/null +++ b/Composer/packages/client/.gitignore @@ -0,0 +1 @@ +build/ \ No newline at end of file diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index aa77e2a25f..d01ffbbcd2 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -185,14 +185,6 @@ export const ObiEditor: FC = ({ const [keyboardStatus, setKeyBoardStatus] = useState('normal'); useEffect((): void => { - if (selectionContext.selectedIds.length > 0) { - setKeyBoardStatus('selected'); - } else if (focusedId) { - setKeyBoardStatus('focused'); - } else { - setKeyBoardStatus('normal'); - } - // Notify container at every selection change. onSelect(selectionContext.selectedIds.length ? selectionContext.selectedIds : focusedId ? [focusedId] : []); }, [focusedId, selectionContext]); From 916a2a8a960116f5722ca4bbcd64c437e32d97df Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Tue, 18 Feb 2020 17:40:59 +0800 Subject: [PATCH 03/32] set default demo to editor --- Composer/packages/extensions/visual-designer/demo/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/extensions/visual-designer/demo/src/index.js b/Composer/packages/extensions/visual-designer/demo/src/index.js index c9f24d18fe..34e853d250 100644 --- a/Composer/packages/extensions/visual-designer/demo/src/index.js +++ b/Composer/packages/extensions/visual-designer/demo/src/index.js @@ -36,7 +36,7 @@ const DemoMaps = { class Demo extends Component { state = { - selectedItem: DemoMaps.VisualSDKDemo.key, + selectedItem: DemoMaps.VisualEditorDemo.key, }; renderNav() { From 9e2e21c23af81d598490b53f20179a07d0b49edb Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Tue, 18 Feb 2020 22:27:51 +0800 Subject: [PATCH 04/32] give decorators unique id --- .../visual-designer/src/transformers/transformForeach.ts | 4 ++-- .../visual-designer/src/transformers/transformIfCondition.ts | 2 +- .../src/transformers/transformSwitchCondition.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/transformers/transformForeach.ts b/Composer/packages/extensions/visual-designer/src/transformers/transformForeach.ts index c9df80d61f..f8fa634bb8 100644 --- a/Composer/packages/extensions/visual-designer/src/transformers/transformForeach.ts +++ b/Composer/packages/extensions/visual-designer/src/transformers/transformForeach.ts @@ -27,7 +27,7 @@ export function transformForeach( return { foreachDetail: foreachDetailNode, stepGroup: stepsNode, - loopBegin: new IndexedNode(jsonpath, { $type: ObiTypes.LoopIndicator }), - loopEnd: new IndexedNode(jsonpath, { $type: ObiTypes.LoopIndicator }), + loopBegin: new IndexedNode(`${jsonpath}.begin`, { $type: ObiTypes.LoopIndicator }), + loopEnd: new IndexedNode(`${jsonpath}.end`, { $type: ObiTypes.LoopIndicator }), }; } diff --git a/Composer/packages/extensions/visual-designer/src/transformers/transformIfCondition.ts b/Composer/packages/extensions/visual-designer/src/transformers/transformIfCondition.ts index 4bb35f323b..e9cc1ae6d7 100644 --- a/Composer/packages/extensions/visual-designer/src/transformers/transformIfCondition.ts +++ b/Composer/packages/extensions/visual-designer/src/transformers/transformIfCondition.ts @@ -19,7 +19,7 @@ export function transformIfCondtion( ...input, $type: ObiTypes.ConditionNode, }), - choice: new IndexedNode(`${jsonpath}`, { + choice: new IndexedNode(`${jsonpath}.choice`, { $type: ObiTypes.ChoiceDiamond, text: input.condition, }), diff --git a/Composer/packages/extensions/visual-designer/src/transformers/transformSwitchCondition.ts b/Composer/packages/extensions/visual-designer/src/transformers/transformSwitchCondition.ts index 42dcfbc4c6..0321bc81fb 100644 --- a/Composer/packages/extensions/visual-designer/src/transformers/transformSwitchCondition.ts +++ b/Composer/packages/extensions/visual-designer/src/transformers/transformSwitchCondition.ts @@ -25,7 +25,7 @@ export function transformSwitchCondition( ...input, $type: ObiTypes.ConditionNode, }), - choice: new IndexedNode(`${jsonpath}`, { + choice: new IndexedNode(`${jsonpath}.choice`, { $type: ObiTypes.ChoiceDiamond, text: condition, }), From d3bfa9f3cba79a9c78e0cba9e20188fa3bda7b2e Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Tue, 18 Feb 2020 23:17:21 +0800 Subject: [PATCH 05/32] implement ElementMeasurer for dynamic size --- .../components/renderers/ElementMeasurer.tsx | 29 +++++++++++++++++++ .../src/widgets/DynamicBlock.tsx | 3 +- .../src/widgets/IfConditionWidget.tsx | 11 ++----- 3 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 Composer/packages/extensions/visual-designer/src/components/renderers/ElementMeasurer.tsx diff --git a/Composer/packages/extensions/visual-designer/src/components/renderers/ElementMeasurer.tsx b/Composer/packages/extensions/visual-designer/src/components/renderers/ElementMeasurer.tsx new file mode 100644 index 0000000000..16b67ff34e --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/components/renderers/ElementMeasurer.tsx @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React from 'react'; +import Measure from 'react-measure'; + +import { Boundary } from '../../models/Boundary'; + +export interface ElementMeasurerProps { + children: React.ReactNode; + style?: React.CSSProperties; + onResize: (boundary: Boundary) => void; +} +export const ElementMeasurer: React.FC = ({ children, style, onResize }) => { + return ( + { + onResize(new Boundary(width, height)); + }} + > + {({ measureRef }) => ( +
+ {children} +
+ )} +
+ ); +}; diff --git a/Composer/packages/extensions/visual-designer/src/widgets/DynamicBlock.tsx b/Composer/packages/extensions/visual-designer/src/widgets/DynamicBlock.tsx index d6bbc607eb..787355595e 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/DynamicBlock.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/DynamicBlock.tsx @@ -10,8 +10,9 @@ export type DynamicBlockProps = WidgetContainerProps; export const DynamicBlock = () => { const [n, setN] = useState(0); const texts = new Array(n).fill('hello'); + return ( -
+
= 2 ? undefined : 48 }}> {n} diff --git a/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx index 319f114de4..e7703a5102 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx @@ -4,7 +4,6 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import { FunctionComponent, useEffect, useState, useMemo } from 'react'; -import Measure from 'react-measure'; import { transformIfCondtion } from '../transformers/transformIfCondition'; import { ifElseLayouter } from '../layouters/ifelseLayouter'; @@ -15,6 +14,7 @@ import { OffsetContainer } from '../components/lib/OffsetContainer'; import { StepGroup } from '../components/groups'; import { Diamond } from '../components/nodes/templates/Diamond'; import { ElementWrapper } from '../components/renderers/ElementWrapper'; +import { ElementMeasurer } from '../components/renderers/ElementMeasurer'; import { NodeMap, BoundaryMap } from '../components/nodes/types'; import { WidgetContainerProps } from '../schema/uischema.types'; import { SVGContainer } from '../components/lib/SVGContainer'; @@ -79,14 +79,7 @@ export const IfConditionWidget: FunctionComponent = ({
- { - console.log(contentRect); - }} - > - {({ measureRef }) =>
{judgement}
} -
+ patchBoundary(condition.id, boundary)}>{judgement}
From 010b2009073973164e261a8a26cbaf5d9e17b5e7 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Tue, 18 Feb 2020 23:25:22 +0800 Subject: [PATCH 06/32] pass onResize handler to enable boundary propagation --- .../src/components/renderers/StepRenderer.tsx | 4 ++-- .../visual-designer/src/schema/uischemaRenderer.tsx | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/components/renderers/StepRenderer.tsx b/Composer/packages/extensions/visual-designer/src/components/renderers/StepRenderer.tsx index 0f1c9fcd65..448aa39ffa 100644 --- a/Composer/packages/extensions/visual-designer/src/components/renderers/StepRenderer.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/renderers/StepRenderer.tsx @@ -27,13 +27,13 @@ const TypesWithoutWrapper = [ SDKTypes.ChoiceInput, ]; -export const StepRenderer: FC = ({ id, data, onEvent }): JSX.Element => { +export const StepRenderer: FC = ({ id, data, onEvent, onResize }): JSX.Element => { const schemaProvider = useContext(UISchemaContext); const $type = get(data, '$type', ''); const widgetSchema = schemaProvider.get($type); - const content = renderUIWidget(widgetSchema, { id, data, onEvent }); + const content = renderUIWidget(widgetSchema, { id, data, onEvent, onResize }); if (TypesWithoutWrapper.some(x => $type === x)) { return content; } diff --git a/Composer/packages/extensions/visual-designer/src/schema/uischemaRenderer.tsx b/Composer/packages/extensions/visual-designer/src/schema/uischemaRenderer.tsx index 56f9c1594b..8e5841aecc 100644 --- a/Composer/packages/extensions/visual-designer/src/schema/uischemaRenderer.tsx +++ b/Composer/packages/extensions/visual-designer/src/schema/uischemaRenderer.tsx @@ -4,6 +4,8 @@ import React from 'react'; import { BaseSchema } from '@bfc/shared'; +import { Boundary } from '../models/Boundary'; + import { UIWidget, UI_WIDGET_KEY, UIWidgetProp, WidgetEventHandler } from './uischema.types'; export interface UIWidgetContext { @@ -15,6 +17,9 @@ export interface UIWidgetContext { /** Handle UI events */ onEvent: WidgetEventHandler; + + /** Report widget boundary */ + onResize: (boundary: Boundary) => void; } const parseWidgetSchema = (widgetSchema: UIWidget) => { From b05c7c3ea95b979a431733ffcd3d636368cf6638 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 11:43:06 +0800 Subject: [PATCH 07/32] update FormCard css to not truncate text --- .../src/components/nodes/templates/FormCard.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx index 792d708c6d..fe5cb3f5de 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx @@ -16,7 +16,7 @@ const contentHeight = boxHeight - headerHeight; const containerStyle = { width: boxWidth, - height: boxHeight, + minHeight: boxHeight, fontSize: '12px', cursor: 'pointer', overflow: 'hidden', @@ -92,7 +92,8 @@ export const FormCard: FunctionComponent = ({ className="card__content" css={{ width: '100%', - height: contentHeight, + minHeight: contentHeight, + display: 'inline-block', }} >
= ({ css={{ height: '100%', width: 'calc(100% - 20px)', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', + whiteSpace: 'initial', fontSize: '12px', lineHeight: '19px', fontFamily: 'Segoe UI', From 98279e65fcfa79783293111d064cfa6c52338159 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 12:06:12 +0800 Subject: [PATCH 08/32] apply ElementMeasurer to necessary widgets --- .../src/components/renderers/StepRenderer.tsx | 3 ++- .../extensions/visual-designer/src/widgets/ForeachWidget.tsx | 3 ++- .../visual-designer/src/widgets/SwitchConditionWidget.tsx | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/components/renderers/StepRenderer.tsx b/Composer/packages/extensions/visual-designer/src/components/renderers/StepRenderer.tsx index 448aa39ffa..6674904de7 100644 --- a/Composer/packages/extensions/visual-designer/src/components/renderers/StepRenderer.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/renderers/StepRenderer.tsx @@ -12,6 +12,7 @@ import { renderUIWidget } from '../../schema/uischemaRenderer'; import { UISchemaContext } from '../../store/UISchemaContext'; import { ElementWrapper } from './ElementWrapper'; +import { ElementMeasurer } from './ElementMeasurer'; /** TODO: (zeye) integrate this array into UISchema */ const TypesWithoutWrapper = [ @@ -39,7 +40,7 @@ export const StepRenderer: FC = ({ id, data, onEvent, onResize }): JS } return ( - {content} + onResize(boundary)}>{content} ); }; diff --git a/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx index 8aabdba14f..95189c1ce5 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx @@ -14,6 +14,7 @@ import { OffsetContainer } from '../components/lib/OffsetContainer'; import { LoopIndicator } from '../components/decorations/LoopIndicator'; import { StepGroup } from '../components/groups'; import { ElementWrapper } from '../components/renderers/ElementWrapper'; +import { ElementMeasurer } from '../components/renderers/ElementMeasurer'; import { NodeMap, BoundaryMap } from '../components/nodes/types'; import { WidgetContainerProps } from '../schema/uischema.types'; import { renderEdge } from '../components/lib/EdgeUtil'; @@ -76,7 +77,7 @@ export const ForeachWidget: FunctionComponent = ({ id, data,
- {loop} + patchBoundary(id, boundary)}>{loop} diff --git a/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx index 66a74a2775..1d3fa7ee75 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx @@ -14,6 +14,7 @@ import { OffsetContainer } from '../components/lib/OffsetContainer'; import { StepGroup } from '../components/groups'; import { Diamond } from '../components/nodes/templates/Diamond'; import { ElementWrapper } from '../components/renderers/ElementWrapper'; +import { ElementMeasurer } from '../components/renderers/ElementMeasurer'; import { WidgetContainerProps } from '../schema/uischema.types'; import { renderEdge } from '../components/lib/EdgeUtil'; import { SVGContainer } from '../components/lib/SVGContainer'; @@ -77,7 +78,9 @@ export const SwitchConditionWidget: FunctionComponent - {judgement} + patchBoundary(conditionNode.id, boundary)}> + {judgement} + From b8f7dc718b8e14dcc9fd08bfbc734b28dfda8fcf Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 12:35:53 +0800 Subject: [PATCH 09/32] css: word break in FormCard --- .../visual-designer/src/components/nodes/templates/FormCard.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx index fe5cb3f5de..d2ded4e4c9 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx @@ -129,6 +129,8 @@ export const FormCard: FunctionComponent = ({ fontSize: '12px', lineHeight: '19px', fontFamily: 'Segoe UI', + overflowWrap: 'break-word', + wordBreak: 'break-all', }} title={typeof label === 'string' ? label : ''} > From 35fd4752ffb7311a321770c4db79411f0ef8f67f Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 13:18:47 +0800 Subject: [PATCH 10/32] support smart layout in all input types --- .../src/widgets/PromptWidget.tsx | 69 ++++++++++++++++--- 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx index 1355a2c3f0..c1a20453a5 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx @@ -3,7 +3,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import { FC } from 'react'; +import { FC, useState, useMemo, useEffect } from 'react'; import { PromptTab } from '@bfc/shared'; import { baseInputLayouter } from '../layouters/baseInputLayouter'; @@ -16,24 +16,69 @@ import { NodeEventTypes } from '../constants/NodeEventTypes'; import { IconBrick } from '../components/decorations/IconBrick'; import { renderEdge } from '../components/lib/EdgeUtil'; import { SVGContainer } from '../components/lib/SVGContainer'; +import { NodeMap, BoundaryMap } from '../components/nodes/types'; +import { GraphLayout } from '../models/GraphLayout'; +import { Boundary, areBoundariesEqual } from '../models/Boundary'; +import { ElementMeasurer } from '../components/renderers/ElementMeasurer'; -const calculateNodes = (data, jsonpath: string) => { +enum PromptNodes { + BotAsks = 'BotAsksNode', + UserAnswers = 'UserAnswersNode', + InvalidPrompt = 'InvalidPromptyNode', +} + +const calculateNodes = (jsonpath: string, data) => { const { botAsks, userAnswers, invalidPrompt } = transformBaseInput(data, jsonpath); return { - botAsksNode: GraphNode.fromIndexedJson(botAsks), - userAnswersNode: GraphNode.fromIndexedJson(userAnswers), - invalidPromptNode: GraphNode.fromIndexedJson(invalidPrompt), + [PromptNodes.BotAsks]: GraphNode.fromIndexedJson(botAsks), + [PromptNodes.UserAnswers]: GraphNode.fromIndexedJson(userAnswers), + [PromptNodes.InvalidPrompt]: GraphNode.fromIndexedJson(invalidPrompt), }; }; +const calculateLayout = (nodeMap: NodeMap, boundaryMap: BoundaryMap): GraphLayout => { + Object.keys(nodeMap).map(nodeName => { + const node = nodeMap[nodeName]; + if (node) { + node.boundary = boundaryMap[nodeName] || node.boundary; + } + }); + + const { BotAsks, UserAnswers, InvalidPrompt } = PromptNodes; + return baseInputLayouter(nodeMap[BotAsks], nodeMap[UserAnswers], nodeMap[InvalidPrompt]); +}; + export interface PromptWdigetProps extends WidgetContainerProps { botAsks: JSX.Element; userInput: JSX.Element; } -export const PromptWidget: FC = ({ id, data, onEvent, botAsks, userInput }): JSX.Element => { - const nodes = calculateNodes(data, id); - const layout = baseInputLayouter(nodes.botAsksNode, nodes.userAnswersNode, nodes.invalidPromptNode); +export const PromptWidget: FC = ({ + id, + data, + onEvent, + onResize, + botAsks, + userInput, +}): JSX.Element => { + const [boundaryMap, setBoundaryMap] = useState({}); + const nodes = useMemo(() => calculateNodes(id, data), [id, data]); + const layout = useMemo(() => calculateLayout(nodes, boundaryMap), [nodes, boundaryMap]); + + const accumulatedPatches = {}; + const patchBoundary = (id, boundary?: Boundary) => { + if (!boundaryMap[id] || !areBoundariesEqual(boundaryMap[id], boundary)) { + accumulatedPatches[id] = boundary; + setBoundaryMap({ + ...boundaryMap, + ...accumulatedPatches, + }); + } + }; + + useEffect(() => { + onResize(layout.boundary); + }, [layout]); const { boundary, nodeMap, edges } = layout; const { botAsksNode, userAnswersNode, invalidPromptNode: brickNode } = nodeMap; @@ -42,12 +87,16 @@ export const PromptWidget: FC = ({ id, data, onEvent, botAsks
- {botAsks} + patchBoundary(PromptNodes.BotAsks, boundary)}> + {botAsks} + - {userInput} + patchBoundary(PromptNodes.UserAnswers, boundary)}> + {userInput} + From 2d9e28961b6ea217d6d29ba2be25cd11ff08851d Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 13:23:38 +0800 Subject: [PATCH 11/32] revert exp changes in uischema --- .../extensions/visual-designer/src/schema/uischema.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx index 53bb18da59..879cce4f58 100644 --- a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx +++ b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx @@ -17,7 +17,6 @@ import { ChoiceInputChoices } from '../widgets/ChoiceInput'; import { ElementIcon } from '../utils/obiPropertyResolver'; import { ObiColors } from '../constants/ElementColors'; import { measureChoiceInputDetailBoundary } from '../layouters/measureJsonBoundary'; -import { DynamicBlock } from '../widgets/DynamicBlock'; import { UISchema, UIWidget } from './uischema.types'; @@ -57,7 +56,7 @@ export const uiSchema: UISchema = { [SDKTypes.IfCondition]: { 'ui:widget': IfConditionWidget, judgement: { - 'ui:widget': DynamicBlock, + 'ui:widget': ActionCard, title: formatMessage('Branch'), content: data => data.condition, }, From 30b9a924c4caa2595514ce1efab1ee265c8327b6 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 14:34:25 +0800 Subject: [PATCH 12/32] add new hook useSmartLayout for layout changes --- .../src/hooks/useSmartLayout.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts diff --git a/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts b/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts new file mode 100644 index 0000000000..6576a49321 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useState, useEffect, useMemo } from 'react'; + +import { Boundary, areBoundariesEqual } from '../models/Boundary'; +import { GraphLayout } from '../models/GraphLayout'; +import { GraphNode } from '../models/GraphNode'; + +// T extends string means an Enum. Reference: https://github.com/microsoft/TypeScript/issues/30611#issuecomment-565384924 +type MapWithEnumKey = { [key in KeyType]: ValueType }; +type GraphNodeMap = MapWithEnumKey; +type BoundaryMap = MapWithEnumKey; + +export function useSmartLayout( + nodeMap: GraphNodeMap, + layouter: (nodeMap: GraphNodeMap, boundaryMap: BoundaryMap) => GraphLayout, + onResize: (boundary: Boundary) => void +): { + layout: GraphLayout; + updateNodeBoundary: (nodeName: T, boundary: Boundary) => void; +} { + const [boundaryMap, setBoundaryMap] = useState>({} as BoundaryMap); + const accumulatedPatches = {}; + const patchBoundary = (nodeName: string, boundary: Boundary) => { + if (!boundaryMap[nodeName] || !areBoundariesEqual(boundaryMap[nodeName], boundary)) { + accumulatedPatches[nodeName] = boundary; + setBoundaryMap({ + ...boundaryMap, + ...accumulatedPatches, + }); + } + }; + + const layout = useMemo(() => layouter(nodeMap, boundaryMap), [nodeMap, boundaryMap]); + + useEffect(() => { + onResize(layout.boundary); + }, [layout]); + + return { + layout, + updateNodeBoundary: patchBoundary, + }; +} From 4592bf0d0ac2a24a50fbcf685ecadf3f955927b6 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 14:37:23 +0800 Subject: [PATCH 13/32] apply useSmartLayouter to PromptWidgets --- .../src/widgets/PromptWidget.tsx | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx index c1a20453a5..f4aa939b67 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx @@ -3,7 +3,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import { FC, useState, useMemo, useEffect } from 'react'; +import { FC, useMemo } from 'react'; import { PromptTab } from '@bfc/shared'; import { baseInputLayouter } from '../layouters/baseInputLayouter'; @@ -18,8 +18,8 @@ import { renderEdge } from '../components/lib/EdgeUtil'; import { SVGContainer } from '../components/lib/SVGContainer'; import { NodeMap, BoundaryMap } from '../components/nodes/types'; import { GraphLayout } from '../models/GraphLayout'; -import { Boundary, areBoundariesEqual } from '../models/Boundary'; import { ElementMeasurer } from '../components/renderers/ElementMeasurer'; +import { useSmartLayout } from '../hooks/useSmartLayout'; enum PromptNodes { BotAsks = 'BotAsksNode', @@ -61,24 +61,8 @@ export const PromptWidget: FC = ({ botAsks, userInput, }): JSX.Element => { - const [boundaryMap, setBoundaryMap] = useState({}); const nodes = useMemo(() => calculateNodes(id, data), [id, data]); - const layout = useMemo(() => calculateLayout(nodes, boundaryMap), [nodes, boundaryMap]); - - const accumulatedPatches = {}; - const patchBoundary = (id, boundary?: Boundary) => { - if (!boundaryMap[id] || !areBoundariesEqual(boundaryMap[id], boundary)) { - accumulatedPatches[id] = boundary; - setBoundaryMap({ - ...boundaryMap, - ...accumulatedPatches, - }); - } - }; - - useEffect(() => { - onResize(layout.boundary); - }, [layout]); + const { layout, updateNodeBoundary } = useSmartLayout(nodes, calculateLayout, onResize); const { boundary, nodeMap, edges } = layout; const { botAsksNode, userAnswersNode, invalidPromptNode: brickNode } = nodeMap; @@ -87,14 +71,14 @@ export const PromptWidget: FC = ({
- patchBoundary(PromptNodes.BotAsks, boundary)}> + updateNodeBoundary(PromptNodes.BotAsks, boundary)}> {botAsks} - patchBoundary(PromptNodes.UserAnswers, boundary)}> + updateNodeBoundary(PromptNodes.UserAnswers, boundary)}> {userInput} From 0ee931dab740892b039b7833fd0daf709956bcaf Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 17:19:20 +0800 Subject: [PATCH 14/32] apply useSmartLayout hook to IfConditionWidget --- .../src/components/nodes/nodeProps.ts | 2 +- .../src/hooks/useSmartLayout.ts | 4 +- .../src/widgets/IfConditionWidget.tsx | 99 +++++++++---------- 3 files changed, 49 insertions(+), 56 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/nodeProps.ts b/Composer/packages/extensions/visual-designer/src/components/nodes/nodeProps.ts index f6a5a9c010..b3471220c2 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/nodeProps.ts +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/nodeProps.ts @@ -9,7 +9,7 @@ export interface NodeProps { data: any; focused?: boolean; onEvent: (action, id, ...rest) => object | void; - onResize: (boundary?: Boundary, id?) => object | void; + onResize: (boundary: Boundary, id?) => object | void; isRoot?: boolean; } diff --git a/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts b/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts index 6576a49321..8f62732d51 100644 --- a/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts +++ b/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts @@ -9,8 +9,8 @@ import { GraphNode } from '../models/GraphNode'; // T extends string means an Enum. Reference: https://github.com/microsoft/TypeScript/issues/30611#issuecomment-565384924 type MapWithEnumKey = { [key in KeyType]: ValueType }; -type GraphNodeMap = MapWithEnumKey; -type BoundaryMap = MapWithEnumKey; +export type GraphNodeMap = MapWithEnumKey; +export type BoundaryMap = MapWithEnumKey; export function useSmartLayout( nodeMap: GraphNodeMap, diff --git a/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx index e7703a5102..214b58fc0e 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx @@ -3,42 +3,54 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import { FunctionComponent, useEffect, useState, useMemo } from 'react'; +import { FunctionComponent, useMemo } from 'react'; import { transformIfCondtion } from '../transformers/transformIfCondition'; import { ifElseLayouter } from '../layouters/ifelseLayouter'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { GraphNode } from '../models/GraphNode'; -import { areBoundariesEqual, Boundary } from '../models/Boundary'; import { OffsetContainer } from '../components/lib/OffsetContainer'; import { StepGroup } from '../components/groups'; import { Diamond } from '../components/nodes/templates/Diamond'; import { ElementWrapper } from '../components/renderers/ElementWrapper'; import { ElementMeasurer } from '../components/renderers/ElementMeasurer'; -import { NodeMap, BoundaryMap } from '../components/nodes/types'; import { WidgetContainerProps } from '../schema/uischema.types'; import { SVGContainer } from '../components/lib/SVGContainer'; import { renderEdge } from '../components/lib/EdgeUtil'; +import { useSmartLayout, GraphNodeMap, BoundaryMap } from '../hooks/useSmartLayout'; -const calculateNodeMap = (path, data): NodeMap => { +enum IfElseNodes { + Condition = 'ConditionNode', + Choice = 'ChoiceNode', + IfBranch = 'IfBranchNode', + ElseBranch = 'ElseBranchNode', +} + +const calculateNodeMap = (path: string, data): GraphNodeMap => { const result = transformIfCondtion(data, path); - if (!result) return {}; + if (!result) + return { + [IfElseNodes.Condition]: new GraphNode(), + [IfElseNodes.Choice]: new GraphNode(), + [IfElseNodes.IfBranch]: new GraphNode(), + [IfElseNodes.ElseBranch]: new GraphNode(), + }; const { condition, choice, ifGroup, elseGroup } = result; return { - conditionNode: GraphNode.fromIndexedJson(condition), - choiceNode: GraphNode.fromIndexedJson(choice), - ifGroupNode: GraphNode.fromIndexedJson(ifGroup), - elseGroupNode: GraphNode.fromIndexedJson(elseGroup), + [IfElseNodes.Condition]: GraphNode.fromIndexedJson(condition), + [IfElseNodes.Choice]: GraphNode.fromIndexedJson(choice), + [IfElseNodes.IfBranch]: GraphNode.fromIndexedJson(ifGroup), + [IfElseNodes.ElseBranch]: GraphNode.fromIndexedJson(elseGroup), }; }; -const calculateLayout = (nodeMap: NodeMap, boundaryMap: BoundaryMap) => { +const calculateIfElseLayout = (nodeMap: GraphNodeMap, boundaryMap: BoundaryMap) => { Object.values(nodeMap) .filter(x => !!x) .forEach((x: GraphNode) => (x.boundary = boundaryMap[x.id] || x.boundary)); - return ifElseLayouter(nodeMap.conditionNode, nodeMap.choiceNode, nodeMap.ifGroupNode, nodeMap.elseGroupNode); + return ifElseLayouter(nodeMap.ConditionNode, nodeMap.ChoiceNode, nodeMap.IfBranchNode, nodeMap.ElseBranchNode); }; export interface IfConditionWidgetProps extends WidgetContainerProps { @@ -52,60 +64,41 @@ export const IfConditionWidget: FunctionComponent = ({ onResize, judgement, }) => { - const [boundaryMap, setBoundaryMap] = useState({}); - const initialNodeMap = useMemo(() => calculateNodeMap(id, data), [id, data]); - const layout = useMemo(() => calculateLayout(initialNodeMap, boundaryMap), [initialNodeMap, boundaryMap]); - const accumulatedPatches = {}; - - const patchBoundary = (id, boundary?: Boundary) => { - if (!boundaryMap[id] || !areBoundariesEqual(boundaryMap[id], boundary)) { - accumulatedPatches[id] = boundary; - setBoundaryMap({ - ...boundaryMap, - ...accumulatedPatches, - }); - } - }; - - useEffect(() => { - onResize(layout.boundary); - }, [layout]); + const nodeMap = useMemo(() => calculateNodeMap(id, data), [id, data]); + const { layout, updateNodeBoundary } = useSmartLayout(nodeMap, calculateIfElseLayout, onResize); - const { boundary, nodeMap, edges } = layout; - const condition = nodeMap.condition || new GraphNode(); - const choice = nodeMap.choice || new GraphNode(); + const { boundary, edges } = layout; + const { ConditionNode, ChoiceNode, IfBranchNode, ElseBranchNode } = nodeMap; return (
- - - patchBoundary(condition.id, boundary)}>{judgement} + + + updateNodeBoundary(IfElseNodes.Condition, boundary)}> + {judgement} + - + { onEvent(NodeEventTypes.Focus, { id }); }} /> - {nodeMap - ? [nodeMap.if, nodeMap.else] - .filter(x => !!x) - .map(x => ( - - { - patchBoundary(x.id, size); - }} - /> - - )) - : null} + {[IfBranchNode, ElseBranchNode].map(x => ( + + { + updateNodeBoundary(IfElseNodes.IfBranch, size); + }} + /> + + ))} {Array.isArray(edges) ? edges.map(x => renderEdge(x)) : null}
); From 62485b40100e661915abb6aaeb842b5c2e3b4bb3 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 20:53:49 +0800 Subject: [PATCH 15/32] include boundary merge logic in useSmartLayout --- .../src/hooks/useSmartLayout.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts b/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts index 8f62732d51..d023a4b3c4 100644 --- a/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts +++ b/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts @@ -7,14 +7,16 @@ import { Boundary, areBoundariesEqual } from '../models/Boundary'; import { GraphLayout } from '../models/GraphLayout'; import { GraphNode } from '../models/GraphNode'; -// T extends string means an Enum. Reference: https://github.com/microsoft/TypeScript/issues/30611#issuecomment-565384924 +// 'T extends string' means an Enum. Reference: https://github.com/microsoft/TypeScript/issues/30611#issuecomment-565384924 type MapWithEnumKey = { [key in KeyType]: ValueType }; + +type BoundaryMap = MapWithEnumKey; + export type GraphNodeMap = MapWithEnumKey; -export type BoundaryMap = MapWithEnumKey; export function useSmartLayout( nodeMap: GraphNodeMap, - layouter: (nodeMap: GraphNodeMap, boundaryMap: BoundaryMap) => GraphLayout, + layouter: (nodeMap: GraphNodeMap) => GraphLayout, onResize: (boundary: Boundary) => void ): { layout: GraphLayout; @@ -32,7 +34,16 @@ export function useSmartLayout( } }; - const layout = useMemo(() => layouter(nodeMap, boundaryMap), [nodeMap, boundaryMap]); + const layout = useMemo(() => { + // write updated boundaries to nodes + Object.keys(nodeMap).map(nodeName => { + const node = nodeMap[nodeName]; + if (node) { + node.boundary = boundaryMap[nodeName] || node.boundary; + } + }); + return layouter(nodeMap); + }, [nodeMap, boundaryMap]); useEffect(() => { onResize(layout.boundary); From 76d5a3bdaa29651916a272ce29640632b7cedf40 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 20:59:45 +0800 Subject: [PATCH 16/32] IfCondition, Prompt adapt to new smart layout hook --- .../src/widgets/IfConditionWidget.tsx | 29 +++++++++---------- .../src/widgets/PromptWidget.tsx | 22 +++++--------- 2 files changed, 20 insertions(+), 31 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx index 214b58fc0e..1f74a76b60 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx @@ -17,13 +17,13 @@ import { ElementMeasurer } from '../components/renderers/ElementMeasurer'; import { WidgetContainerProps } from '../schema/uischema.types'; import { SVGContainer } from '../components/lib/SVGContainer'; import { renderEdge } from '../components/lib/EdgeUtil'; -import { useSmartLayout, GraphNodeMap, BoundaryMap } from '../hooks/useSmartLayout'; +import { useSmartLayout, GraphNodeMap } from '../hooks/useSmartLayout'; enum IfElseNodes { - Condition = 'ConditionNode', - Choice = 'ChoiceNode', - IfBranch = 'IfBranchNode', - ElseBranch = 'ElseBranchNode', + Condition = 'conditionNode', + Choice = 'choiceNode', + IfBranch = 'ifBranchNode', + ElseBranch = 'elseBranchNode', } const calculateNodeMap = (path: string, data): GraphNodeMap => { @@ -45,12 +45,9 @@ const calculateNodeMap = (path: string, data): GraphNodeMap => { }; }; -const calculateIfElseLayout = (nodeMap: GraphNodeMap, boundaryMap: BoundaryMap) => { - Object.values(nodeMap) - .filter(x => !!x) - .forEach((x: GraphNode) => (x.boundary = boundaryMap[x.id] || x.boundary)); - - return ifElseLayouter(nodeMap.ConditionNode, nodeMap.ChoiceNode, nodeMap.IfBranchNode, nodeMap.ElseBranchNode); +const calculateIfElseLayout = (nodeMap: GraphNodeMap) => { + const { conditionNode, choiceNode, ifBranchNode, elseBranchNode } = nodeMap; + return ifElseLayouter(conditionNode, choiceNode, ifBranchNode, elseBranchNode); }; export interface IfConditionWidgetProps extends WidgetContainerProps { @@ -68,25 +65,25 @@ export const IfConditionWidget: FunctionComponent = ({ const { layout, updateNodeBoundary } = useSmartLayout(nodeMap, calculateIfElseLayout, onResize); const { boundary, edges } = layout; - const { ConditionNode, ChoiceNode, IfBranchNode, ElseBranchNode } = nodeMap; + const { conditionNode, choiceNode, ifBranchNode, elseBranchNode } = nodeMap; return (
- - + + updateNodeBoundary(IfElseNodes.Condition, boundary)}> {judgement} - + { onEvent(NodeEventTypes.Focus, { id }); }} /> - {[IfBranchNode, ElseBranchNode].map(x => ( + {[ifBranchNode, elseBranchNode].map(x => ( { @@ -36,16 +35,9 @@ const calculateNodes = (jsonpath: string, data) => { }; }; -const calculateLayout = (nodeMap: NodeMap, boundaryMap: BoundaryMap): GraphLayout => { - Object.keys(nodeMap).map(nodeName => { - const node = nodeMap[nodeName]; - if (node) { - node.boundary = boundaryMap[nodeName] || node.boundary; - } - }); - - const { BotAsks, UserAnswers, InvalidPrompt } = PromptNodes; - return baseInputLayouter(nodeMap[BotAsks], nodeMap[UserAnswers], nodeMap[InvalidPrompt]); +const calculateLayout = (nodeMap: GraphNodeMap): GraphLayout => { + const { botAsksNode, userAnswersNode, invalidPromptyNode } = nodeMap; + return baseInputLayouter(botAsksNode, userAnswersNode, invalidPromptyNode); }; export interface PromptWdigetProps extends WidgetContainerProps { From 8b4c97a73d00f63ff9d4ef7d57fbf08c388d49fa Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 21:12:49 +0800 Subject: [PATCH 17/32] apply useSmartLayout to ForeachWidget --- .../src/widgets/ForeachWidget.tsx | 79 +++++++++---------- 1 file changed, 36 insertions(+), 43 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx index 95189c1ce5..43020b25e8 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx @@ -3,11 +3,10 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import { useMemo, useEffect, useState, FunctionComponent } from 'react'; +import { useMemo, FunctionComponent } from 'react'; import { transformForeach } from '../transformers/transformForeach'; import { foreachLayouter } from '../layouters/foreachLayouter'; -import { areBoundariesEqual, Boundary } from '../models/Boundary'; import { GraphNode } from '../models/GraphNode'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { OffsetContainer } from '../components/lib/OffsetContainer'; @@ -15,32 +14,40 @@ import { LoopIndicator } from '../components/decorations/LoopIndicator'; import { StepGroup } from '../components/groups'; import { ElementWrapper } from '../components/renderers/ElementWrapper'; import { ElementMeasurer } from '../components/renderers/ElementMeasurer'; -import { NodeMap, BoundaryMap } from '../components/nodes/types'; import { WidgetContainerProps } from '../schema/uischema.types'; import { renderEdge } from '../components/lib/EdgeUtil'; import { SVGContainer } from '../components/lib/SVGContainer'; +import { useSmartLayout, GraphNodeMap } from '../hooks/useSmartLayout'; -const calculateNodeMap = (jsonpath, data): NodeMap => { +enum ForeachNodes { + Foreach = 'foreachNode', + LoopBegin = 'loopBeginNode', + LoopEnd = 'loopEndNode', + LoopActions = 'loopActionsNode', +} + +const calculateNodeMap = (jsonpath, data): GraphNodeMap => { const result = transformForeach(data, jsonpath); - if (!result) return {}; + if (!result) + return { + [ForeachNodes.Foreach]: new GraphNode(), + [ForeachNodes.LoopActions]: new GraphNode(), + [ForeachNodes.LoopBegin]: new GraphNode(), + [ForeachNodes.LoopEnd]: new GraphNode(), + }; const { foreachDetail, stepGroup, loopBegin, loopEnd } = result; return { - foreachNode: GraphNode.fromIndexedJson(foreachDetail), - stepGroupNode: GraphNode.fromIndexedJson(stepGroup), - loopBeginNode: GraphNode.fromIndexedJson(loopBegin), - loopEndNode: GraphNode.fromIndexedJson(loopEnd), + [ForeachNodes.Foreach]: GraphNode.fromIndexedJson(foreachDetail), + [ForeachNodes.LoopActions]: GraphNode.fromIndexedJson(stepGroup), + [ForeachNodes.LoopBegin]: GraphNode.fromIndexedJson(loopBegin), + [ForeachNodes.LoopEnd]: GraphNode.fromIndexedJson(loopEnd), }; }; -const calculateLayout = (nodeMap: NodeMap, boundaryMap: BoundaryMap) => { - Object.values(nodeMap) - .filter(x => !!x) - .forEach((x: GraphNode) => { - x.boundary = boundaryMap[x.id] || x.boundary; - }); - - return foreachLayouter(nodeMap.foreachNode, nodeMap.stepGroupNode, nodeMap.loopBeginNode, nodeMap.loopEndNode); +const calculateForeachLayout = (nodeMap: GraphNodeMap) => { + const { foreachNode, loopActionsNode, loopBeginNode, loopEndNode } = nodeMap; + return foreachLayouter(foreachNode, loopActionsNode, loopBeginNode, loopEndNode); }; export interface ForeachWidgetProps extends WidgetContainerProps { @@ -48,46 +55,32 @@ export interface ForeachWidgetProps extends WidgetContainerProps { } export const ForeachWidget: FunctionComponent = ({ id, data, onEvent, onResize, loop }) => { - const [boundaryMap, setBoundaryMap] = useState({}); - const initialNodeMap = useMemo(() => calculateNodeMap(id, data), [id, data]); - const layout = useMemo(() => calculateLayout(initialNodeMap, boundaryMap), [initialNodeMap, boundaryMap]); - const accumulatedPatches = {}; - - const patchBoundary = (id, boundary?: Boundary) => { - if (!boundaryMap[id] || !areBoundariesEqual(boundaryMap[id], boundary)) { - accumulatedPatches[id] = boundary; - setBoundaryMap({ - ...boundaryMap, - ...accumulatedPatches, - }); - } - }; - - useEffect(() => { - onResize(layout.boundary); - }, [layout]); + const nodeMap = useMemo(() => calculateNodeMap(id, data), [id, data]); + const { layout, updateNodeBoundary } = useSmartLayout(nodeMap, calculateForeachLayout, onResize); - const { boundary, nodeMap, edges } = layout; + const { boundary, edges } = layout; if (!nodeMap) { return null; } - const { foreachNode, stepsNode, loopBeginNode, loopEndNode } = nodeMap; + const { foreachNode, loopActionsNode, loopBeginNode, loopEndNode } = nodeMap; return (
- patchBoundary(id, boundary)}>{loop} + updateNodeBoundary(ForeachNodes.Foreach, boundary)}> + {loop} + - + { - patchBoundary(stepsNode.id, size); + updateNodeBoundary(ForeachNodes.LoopActions, size); }} /> From 152b018e8968314e75e13956b68d5f5a735f263e Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 21:39:08 +0800 Subject: [PATCH 18/32] apply useSmartLayout to SwitchConditionWidget --- .../src/widgets/SwitchConditionWidget.tsx | 83 ++++++++++--------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx index 1d3fa7ee75..fc3534f06f 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx @@ -3,13 +3,12 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import { FunctionComponent, useEffect, useState, useMemo } from 'react'; +import { FunctionComponent, useMemo } from 'react'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { transformSwitchCondition } from '../transformers/transformSwitchCondition'; import { switchCaseLayouter } from '../layouters/switchCaseLayouter'; import { GraphNode } from '../models/GraphNode'; -import { areBoundariesEqual } from '../models/Boundary'; import { OffsetContainer } from '../components/lib/OffsetContainer'; import { StepGroup } from '../components/groups'; import { Diamond } from '../components/nodes/templates/Diamond'; @@ -18,25 +17,44 @@ import { ElementMeasurer } from '../components/renderers/ElementMeasurer'; import { WidgetContainerProps } from '../schema/uischema.types'; import { renderEdge } from '../components/lib/EdgeUtil'; import { SVGContainer } from '../components/lib/SVGContainer'; +import { GraphNodeMap, useSmartLayout } from '../hooks/useSmartLayout'; -const calculateNodeMap = (path, data) => { +enum SwitchNodes { + Switch = 'switchNode', + Choice = 'choiceNode', +} + +type CaseNodeKey = string; + +const getCaseKey = (caseIndex: number): string => `cases[${caseIndex}]`; + +const calculateNodeMap = (path: string, data): GraphNodeMap => { const result = transformSwitchCondition(data, path); - if (!result) return {}; + if (!result) + return { + [SwitchNodes.Switch]: new GraphNode(), + [SwitchNodes.Choice]: new GraphNode(), + }; const { condition, choice, branches } = result; - return { - conditionNode: GraphNode.fromIndexedJson(condition), - choiceNode: GraphNode.fromIndexedJson(choice), - branchNodes: branches.map(x => GraphNode.fromIndexedJson(x)), + const nodeMap = { + [SwitchNodes.Switch]: GraphNode.fromIndexedJson(condition), + [SwitchNodes.Choice]: GraphNode.fromIndexedJson(choice), }; -}; -const calculateLayout = (nodeMap, boundaryMap) => { - [nodeMap.conditionNode, nodeMap.choiceNode, ...nodeMap.branchNodes] - .filter(x => !!x) - .forEach(x => (x.boundary = boundaryMap[x.id] || x.boundary)); + branches.forEach((branch, index) => { + const key = getCaseKey(index); + const value = GraphNode.fromIndexedJson(branch); + nodeMap[key] = value; + }); - return switchCaseLayouter(nodeMap.conditionNode, nodeMap.choiceNode, nodeMap.branchNodes); + return nodeMap; +}; + +const calculateLayout = (nodeMap: GraphNodeMap) => { + const { switchNode, choiceNode, ...cases } = nodeMap as GraphNodeMap; + const casesNodes = Object.keys(cases).map(caseName => nodeMap[caseName]); + return switchCaseLayouter(switchNode, choiceNode, casesNodes); }; export interface SwitchConditionWidgetProps extends WidgetContainerProps { @@ -50,35 +68,18 @@ export const SwitchConditionWidget: FunctionComponent { - const [boundaryMap, setBoundaryMap] = useState({}); - const initialNodeMap = useMemo(() => calculateNodeMap(id, data), [id, data]); - const layout = useMemo(() => calculateLayout(initialNodeMap, boundaryMap), [initialNodeMap, boundaryMap]); - const accumulatedPatches = {}; - - const patchBoundary = (id, boundary) => { - if (!boundaryMap[id] || !areBoundariesEqual(boundaryMap[id], boundary)) { - accumulatedPatches[id] = boundary; - setBoundaryMap({ - ...boundaryMap, - ...accumulatedPatches, - }); - } - }; - - useEffect(() => { - onResize(layout.boundary); - }, [layout]); + const nodeMap = useMemo(() => calculateNodeMap(id, data), [id, data]); + const { layout, updateNodeBoundary } = useSmartLayout(nodeMap, calculateLayout, onResize); - const { boundary, nodeMap, edges } = layout; - const conditionNode = nodeMap.conditionNode; - const choiceNode = nodeMap.choiceNode; - const branchNodes = nodeMap.branchNodes || []; + const { boundary, edges } = layout; + const { switchNode, choiceNode, ...cases } = nodeMap as GraphNodeMap; + const casesNodes = Object.keys(cases).map(x => nodeMap[x]); return (
- - - patchBoundary(conditionNode.id, boundary)}> + + + updateNodeBoundary(SwitchNodes.Switch, boundary)}> {judgement} @@ -91,7 +92,7 @@ export const SwitchConditionWidget: FunctionComponent - {(branchNodes as any).map(x => ( + {(casesNodes as any).map((x, index) => ( { - patchBoundary(x.id, size); + updateNodeBoundary(getCaseKey(index), size); }} /> From 1fa383db6e510b73612a98924becb1bde12790a4 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 21:49:43 +0800 Subject: [PATCH 19/32] add comments to ElementMeasurer --- .../src/components/renderers/ElementMeasurer.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Composer/packages/extensions/visual-designer/src/components/renderers/ElementMeasurer.tsx b/Composer/packages/extensions/visual-designer/src/components/renderers/ElementMeasurer.tsx index 16b67ff34e..17d7a5f346 100644 --- a/Composer/packages/extensions/visual-designer/src/components/renderers/ElementMeasurer.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/renderers/ElementMeasurer.tsx @@ -11,6 +11,11 @@ export interface ElementMeasurerProps { style?: React.CSSProperties; onResize: (boundary: Boundary) => void; } + +/** + * Notify a ReactNode's size once its size has been changed. + * Remember to use it inside the focus border component (ElementWrapper). + */ export const ElementMeasurer: React.FC = ({ children, style, onResize }) => { return ( Date: Thu, 20 Feb 2020 22:17:48 +0800 Subject: [PATCH 20/32] add comments in useSmartLayout --- .../visual-designer/src/hooks/useSmartLayout.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts b/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts index d023a4b3c4..b86d57d574 100644 --- a/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts +++ b/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts @@ -23,6 +23,19 @@ export function useSmartLayout( updateNodeBoundary: (nodeName: T, boundary: Boundary) => void; } { const [boundaryMap, setBoundaryMap] = useState>({} as BoundaryMap); + /** + * The object `accumulatedPatches` is used to collect all accumulated + * boundary changes happen in a same JS event cyle. After collecting + * them together, they will be submitted to component states to guide + * next redraw. + * + * We shouldn't use `setState()` here because of `patchBoundary` may be + * fired multiple times (especially at the init render cycle), changes + * will be lost by using `setState()`; + * + * We shouldn't use `useRef` here since `accumulatedPatches` as a local + * cache needs to be cleared after taking effect in one redraw. + */ const accumulatedPatches = {}; const patchBoundary = (nodeName: string, boundary: Boundary) => { if (!boundaryMap[nodeName] || !areBoundariesEqual(boundaryMap[nodeName], boundary)) { From 2bb15f64df04fae137272d416e28e05a2d0a06f0 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 22:27:09 +0800 Subject: [PATCH 21/32] revert ObiEditor changes --- .../extensions/visual-designer/src/editors/ObiEditor.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index d01ffbbcd2..432de17a6c 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -190,6 +190,14 @@ export const ObiEditor: FC = ({ }, [focusedId, selectionContext]); useEffect((): void => { + if (selectionContext.selectedIds.length > 0) { + setKeyBoardStatus('selected'); + } else if (focusedId) { + setKeyBoardStatus('focused'); + } else { + setKeyBoardStatus('normal'); + } + selection.setItems(nodeIndexGenerator.current.getItemList()); }); From d155582a33dbcce8f7ae160564e8b4cf0513f40d Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 22:59:58 +0800 Subject: [PATCH 22/32] update FormCard style --- .../src/components/nodes/templates/FormCard.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx index d2ded4e4c9..459340bfb0 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx @@ -100,10 +100,10 @@ export const FormCard: FunctionComponent = ({ css={{ fontWeight: 400, paddingLeft: '5px', - margin: '3px 5px', + margin: '2px 5px', fontSize: '14px', lineHeight: '19px', - display: 'flex', + display: 'inline-flex', alignItems: 'center', }} > @@ -131,6 +131,7 @@ export const FormCard: FunctionComponent = ({ fontFamily: 'Segoe UI', overflowWrap: 'break-word', wordBreak: 'break-all', + display: 'contents', }} title={typeof label === 'string' ? label : ''} > From 6ea6b58682de9f2e003510a3cc116aa51bab94a7 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Thu, 20 Feb 2020 23:54:43 +0800 Subject: [PATCH 23/32] revert unnecessary changes --- Composer/packages/client/.gitignore | 1 - .../visual-designer/demo/src/index.js | 2 +- .../src/widgets/DynamicBlock.tsx | 24 ------------------- 3 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 Composer/packages/client/.gitignore delete mode 100644 Composer/packages/extensions/visual-designer/src/widgets/DynamicBlock.tsx diff --git a/Composer/packages/client/.gitignore b/Composer/packages/client/.gitignore deleted file mode 100644 index d16386367f..0000000000 --- a/Composer/packages/client/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build/ \ No newline at end of file diff --git a/Composer/packages/extensions/visual-designer/demo/src/index.js b/Composer/packages/extensions/visual-designer/demo/src/index.js index 34e853d250..c9f24d18fe 100644 --- a/Composer/packages/extensions/visual-designer/demo/src/index.js +++ b/Composer/packages/extensions/visual-designer/demo/src/index.js @@ -36,7 +36,7 @@ const DemoMaps = { class Demo extends Component { state = { - selectedItem: DemoMaps.VisualEditorDemo.key, + selectedItem: DemoMaps.VisualSDKDemo.key, }; renderNav() { diff --git a/Composer/packages/extensions/visual-designer/src/widgets/DynamicBlock.tsx b/Composer/packages/extensions/visual-designer/src/widgets/DynamicBlock.tsx deleted file mode 100644 index 787355595e..0000000000 --- a/Composer/packages/extensions/visual-designer/src/widgets/DynamicBlock.tsx +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import React, { useState } from 'react'; - -import { WidgetContainerProps } from '../schema/uischema.types'; - -export type DynamicBlockProps = WidgetContainerProps; - -export const DynamicBlock = () => { - const [n, setN] = useState(0); - const texts = new Array(n).fill('hello'); - - return ( -
= 2 ? undefined : 48 }}> - {n} - - - {texts.map((text, index) => ( -

{text}

- ))} -
- ); -}; From 988e3b5025131901404333e160042b5e2c7e3e41 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Fri, 21 Feb 2020 00:26:58 +0800 Subject: [PATCH 24/32] revert unnecessary transformer changes --- .../visual-designer/src/transformers/transformForeach.ts | 4 ++-- .../visual-designer/src/transformers/transformIfCondition.ts | 2 +- .../src/transformers/transformSwitchCondition.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/transformers/transformForeach.ts b/Composer/packages/extensions/visual-designer/src/transformers/transformForeach.ts index f8fa634bb8..c9df80d61f 100644 --- a/Composer/packages/extensions/visual-designer/src/transformers/transformForeach.ts +++ b/Composer/packages/extensions/visual-designer/src/transformers/transformForeach.ts @@ -27,7 +27,7 @@ export function transformForeach( return { foreachDetail: foreachDetailNode, stepGroup: stepsNode, - loopBegin: new IndexedNode(`${jsonpath}.begin`, { $type: ObiTypes.LoopIndicator }), - loopEnd: new IndexedNode(`${jsonpath}.end`, { $type: ObiTypes.LoopIndicator }), + loopBegin: new IndexedNode(jsonpath, { $type: ObiTypes.LoopIndicator }), + loopEnd: new IndexedNode(jsonpath, { $type: ObiTypes.LoopIndicator }), }; } diff --git a/Composer/packages/extensions/visual-designer/src/transformers/transformIfCondition.ts b/Composer/packages/extensions/visual-designer/src/transformers/transformIfCondition.ts index e9cc1ae6d7..4bb35f323b 100644 --- a/Composer/packages/extensions/visual-designer/src/transformers/transformIfCondition.ts +++ b/Composer/packages/extensions/visual-designer/src/transformers/transformIfCondition.ts @@ -19,7 +19,7 @@ export function transformIfCondtion( ...input, $type: ObiTypes.ConditionNode, }), - choice: new IndexedNode(`${jsonpath}.choice`, { + choice: new IndexedNode(`${jsonpath}`, { $type: ObiTypes.ChoiceDiamond, text: input.condition, }), diff --git a/Composer/packages/extensions/visual-designer/src/transformers/transformSwitchCondition.ts b/Composer/packages/extensions/visual-designer/src/transformers/transformSwitchCondition.ts index 0321bc81fb..42dcfbc4c6 100644 --- a/Composer/packages/extensions/visual-designer/src/transformers/transformSwitchCondition.ts +++ b/Composer/packages/extensions/visual-designer/src/transformers/transformSwitchCondition.ts @@ -25,7 +25,7 @@ export function transformSwitchCondition( ...input, $type: ObiTypes.ConditionNode, }), - choice: new IndexedNode(`${jsonpath}.choice`, { + choice: new IndexedNode(`${jsonpath}`, { $type: ObiTypes.ChoiceDiamond, text: condition, }), From ddd2a17aaab3209cbe55d513ab22c15e0cca9ca2 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Fri, 21 Feb 2020 19:40:59 +0800 Subject: [PATCH 25/32] fix IfConditionWidget layout --- .../src/widgets/IfConditionWidget.tsx | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx index 1f74a76b60..6bb9a7d203 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx @@ -65,7 +65,7 @@ export const IfConditionWidget: FunctionComponent = ({ const { layout, updateNodeBoundary } = useSmartLayout(nodeMap, calculateIfElseLayout, onResize); const { boundary, edges } = layout; - const { conditionNode, choiceNode, ifBranchNode, elseBranchNode } = nodeMap; + const { conditionNode, choiceNode } = nodeMap; return (
@@ -83,19 +83,22 @@ export const IfConditionWidget: FunctionComponent = ({ }} /> - {[ifBranchNode, elseBranchNode].map(x => ( - - { - updateNodeBoundary(IfElseNodes.IfBranch, size); - }} - /> - - ))} + {[IfElseNodes.IfBranch, IfElseNodes.ElseBranch].map(nodeName => { + const node = nodeMap[nodeName]; + return ( + + { + updateNodeBoundary(nodeName, size); + }} + /> + + ); + })} {Array.isArray(edges) ? edges.map(x => renderEdge(x)) : null}
); From 1dc610d19ce1dbfc9c381ee41bd81f35f3e4512e Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Mon, 24 Feb 2020 12:42:52 +0800 Subject: [PATCH 26/32] fix a potential undefined func call --- .../extensions/visual-designer/src/hooks/useSmartLayout.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts b/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts index b86d57d574..496ec79591 100644 --- a/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts +++ b/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts @@ -59,7 +59,7 @@ export function useSmartLayout( }, [nodeMap, boundaryMap]); useEffect(() => { - onResize(layout.boundary); + onResize && onResize(layout.boundary); }, [layout]); return { From 139e843058679ab9057b8aee0c5c4cf29aa40ee2 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Mon, 24 Feb 2020 12:51:52 +0800 Subject: [PATCH 27/32] improve ts type in Switch --- .../visual-designer/src/widgets/SwitchConditionWidget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx index fc3534f06f..9a77b1a47a 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx @@ -26,7 +26,7 @@ enum SwitchNodes { type CaseNodeKey = string; -const getCaseKey = (caseIndex: number): string => `cases[${caseIndex}]`; +const getCaseKey = (caseIndex: number): CaseNodeKey => `cases[${caseIndex}]`; const calculateNodeMap = (path: string, data): GraphNodeMap => { const result = transformSwitchCondition(data, path); From 1a8eae652c82de1579ece8e0b778bab946b5c0fb Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Mon, 24 Feb 2020 12:54:58 +0800 Subject: [PATCH 28/32] sort Switch case keys --- .../visual-designer/src/widgets/SwitchConditionWidget.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx index 9a77b1a47a..7c03bd474e 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx @@ -53,7 +53,9 @@ const calculateNodeMap = (path: string, data): GraphNodeMap) => { const { switchNode, choiceNode, ...cases } = nodeMap as GraphNodeMap; - const casesNodes = Object.keys(cases).map(caseName => nodeMap[caseName]); + const casesNodes = Object.keys(cases) + .sort() + .map(caseName => nodeMap[caseName]); return switchCaseLayouter(switchNode, choiceNode, casesNodes); }; From 47b99f50363142a3919374f6861e85e733e10808 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Mon, 24 Feb 2020 13:21:37 +0800 Subject: [PATCH 29/32] StepGroup apply useSmartLayouter --- .../src/components/groups/StepGroup.tsx | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx b/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx index b852607f9e..e3596e5d74 100644 --- a/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx @@ -3,10 +3,9 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import { useState, useMemo, useEffect, FunctionComponent } from 'react'; +import { useMemo, FunctionComponent } from 'react'; import { GraphNode } from '../../models/GraphNode'; -import { areBoundariesEqual } from '../../models/Boundary'; import { sequentialLayouter } from '../../layouters/sequentialLayouter'; import { ElementInterval, EdgeAddButtonSize } from '../../constants/ElementSizes'; import { NodeEventTypes } from '../../constants/NodeEventTypes'; @@ -18,46 +17,41 @@ import { GraphLayout } from '../../models/GraphLayout'; import { EdgeMenu } from '../menus/EdgeMenu'; import { SVGContainer } from '../lib/SVGContainer'; import { renderEdge } from '../lib/EdgeUtil'; +import { GraphNodeMap, useSmartLayout } from '../../hooks/useSmartLayout'; const StepInterval = ElementInterval.y; -const calculateNodes = (groupId: string, data): GraphNode[] => { +type StepNodeKey = string; + +const getStepKey = (stepOrder: number): StepNodeKey => `steps[${stepOrder}]`; + +const calculateNodes = (groupId: string, data): GraphNodeMap => { const steps = transformStepGroup(data, groupId); - return steps.map((x): GraphNode => GraphNode.fromIndexedJson(x)); + const stepNodes = steps.map((x): GraphNode => GraphNode.fromIndexedJson(x)); + return stepNodes.reduce((result, node, index) => { + result[getStepKey(index)] = node; + return result; + }, {} as GraphNodeMap); }; -const calculateLayout = (nodes, boundaryMap): GraphLayout => { - nodes.forEach((x): void => (x.boundary = boundaryMap[x.id] || x.boundary)); +const calculateLayout = (nodeMap: GraphNodeMap): GraphLayout => { + const nodes = Object.keys(nodeMap) + .sort() + .map(stepName => nodeMap[stepName]); return sequentialLayouter(nodes); }; export const StepGroup: FunctionComponent = ({ id, data, onEvent, onResize }: NodeProps): JSX.Element => { - const [boundaryMap, setBoundaryMap] = useState({}); - const initialNodes = useMemo((): GraphNode[] => calculateNodes(id, data), [id, data]); - const layout = useMemo((): GraphLayout => calculateLayout(initialNodes, boundaryMap), [initialNodes, boundaryMap]); - const accumulatedPatches = {}; - - const patchBoundary = (id, boundary): void => { - if (!boundaryMap[id] || !areBoundariesEqual(boundaryMap[id], boundary)) { - accumulatedPatches[id] = boundary; - setBoundaryMap({ - ...boundaryMap, - ...accumulatedPatches, - }); - } - }; + const initialNodes = useMemo(() => calculateNodes(id, data), [id, data]); + const { layout, updateNodeBoundary } = useSmartLayout(initialNodes, calculateLayout, onResize); const { boundary, nodes, edges } = layout; - useEffect(() => { - onResize(layout.boundary); - }, [layout]); - return (
{Array.isArray(edges) ? edges.map(x => renderEdge(x)) : null} {nodes - ? nodes.map(x => ( + ? nodes.map((x, index) => ( = ({ id, data, onEvent, onR data={x.data} onEvent={onEvent} onResize={size => { - patchBoundary(x.id, size); + updateNodeBoundary(getStepKey(index), size); }} /> From 5e2fcaca9012017423d6f5dcd18ce2035b0ecaa3 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Mon, 24 Feb 2020 19:30:50 +0800 Subject: [PATCH 30/32] feat: boundary cache mechanism to fix flickering --- .../src/components/groups/StepGroup.tsx | 2 + .../src/layouters/measureJsonBoundary.ts | 6 +++ .../src/store/DesignerCache.ts | 47 +++++++++++++++++++ .../src/widgets/ForeachWidget.tsx | 8 +++- .../src/widgets/IfConditionWidget.tsx | 8 +++- .../src/widgets/PromptWidget.tsx | 15 +++++- .../src/widgets/SwitchConditionWidget.tsx | 8 +++- 7 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 Composer/packages/extensions/visual-designer/src/store/DesignerCache.ts diff --git a/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx b/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx index e3596e5d74..83d4b57ebc 100644 --- a/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx @@ -18,6 +18,7 @@ import { EdgeMenu } from '../menus/EdgeMenu'; import { SVGContainer } from '../lib/SVGContainer'; import { renderEdge } from '../lib/EdgeUtil'; import { GraphNodeMap, useSmartLayout } from '../../hooks/useSmartLayout'; +import { designerCache } from '../../store/DesignerCache'; const StepInterval = ElementInterval.y; @@ -59,6 +60,7 @@ export const StepGroup: FunctionComponent = ({ id, data, onEvent, onR data={x.data} onEvent={onEvent} onResize={size => { + designerCache.cacheBoundary(x.data, size); updateNodeBoundary(getStepKey(index), size); }} /> diff --git a/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts b/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts index 03420424b2..4937873417 100644 --- a/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts +++ b/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts @@ -16,6 +16,7 @@ import { transformIfCondtion } from '../transformers/transformIfCondition'; import { transformSwitchCondition } from '../transformers/transformSwitchCondition'; import { transformForeach } from '../transformers/transformForeach'; import { transformBaseInput } from '../transformers/transformBaseInput'; +import { designerCache } from '../store/DesignerCache'; import { calculateIfElseBoundary, @@ -81,6 +82,11 @@ export function measureJsonBoundary(json): Boundary { let boundary = new Boundary(); if (!json || !json.$type) return boundary; + const cachedBoundary = designerCache.loadBounary(json); + if (cachedBoundary) { + return cachedBoundary; + } + switch (json.$type) { case ObiTypes.ChoiceDiamond: boundary = new Boundary(DiamondSize.width, DiamondSize.height); diff --git a/Composer/packages/extensions/visual-designer/src/store/DesignerCache.ts b/Composer/packages/extensions/visual-designer/src/store/DesignerCache.ts new file mode 100644 index 0000000000..39e27f9bb7 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/store/DesignerCache.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { BaseSchema } from '@bfc/shared'; +import get from 'lodash/get'; + +import { Boundary } from '../models/Boundary'; + +const MAX_CACHE_SIZE = 99999; + +export class DesignerCache { + private boundaryCache = {}; + private cacheSize = 0; + + private getActionDataHash(actionData: BaseSchema): string | null { + const designerId = get(actionData, '$designer.id', ''); + if (!designerId) return null; + + const $type = get(actionData, '$type'); + return `${$type}-${designerId}`; + } + + cacheBoundary(actionData: BaseSchema, boundary: Boundary): boolean { + const key = this.getActionDataHash(actionData); + if (!key) { + return false; + } + + if (this.cacheSize > MAX_CACHE_SIZE) { + delete this.boundaryCache; + this.boundaryCache = {}; + this.cacheSize = 0; + } + this.boundaryCache[key] = boundary; + this.cacheSize += 1; + return true; + } + + loadBounary(actionData: BaseSchema): Boundary | undefined { + const key = this.getActionDataHash(actionData); + if (key) { + return this.boundaryCache[key]; + } + } +} + +export const designerCache = new DesignerCache(); diff --git a/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx index 43020b25e8..a31786e8fa 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx @@ -18,6 +18,7 @@ import { WidgetContainerProps } from '../schema/uischema.types'; import { renderEdge } from '../components/lib/EdgeUtil'; import { SVGContainer } from '../components/lib/SVGContainer'; import { useSmartLayout, GraphNodeMap } from '../hooks/useSmartLayout'; +import { designerCache } from '../store/DesignerCache'; enum ForeachNodes { Foreach = 'foreachNode', @@ -68,7 +69,12 @@ export const ForeachWidget: FunctionComponent = ({ id, data,
- updateNodeBoundary(ForeachNodes.Foreach, boundary)}> + { + designerCache.cacheBoundary(foreachNode.data, boundary); + updateNodeBoundary(ForeachNodes.Foreach, boundary); + }} + > {loop} diff --git a/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx index 6bb9a7d203..10426593c9 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx @@ -18,6 +18,7 @@ import { WidgetContainerProps } from '../schema/uischema.types'; import { SVGContainer } from '../components/lib/SVGContainer'; import { renderEdge } from '../components/lib/EdgeUtil'; import { useSmartLayout, GraphNodeMap } from '../hooks/useSmartLayout'; +import { designerCache } from '../store/DesignerCache'; enum IfElseNodes { Condition = 'conditionNode', @@ -71,7 +72,12 @@ export const IfConditionWidget: FunctionComponent = ({
- updateNodeBoundary(IfElseNodes.Condition, boundary)}> + { + designerCache.cacheBoundary(conditionNode.data, boundary); + updateNodeBoundary(IfElseNodes.Condition, boundary); + }} + > {judgement} diff --git a/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx index b04701943c..248da13dac 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx @@ -19,6 +19,7 @@ import { SVGContainer } from '../components/lib/SVGContainer'; import { GraphLayout } from '../models/GraphLayout'; import { ElementMeasurer } from '../components/renderers/ElementMeasurer'; import { useSmartLayout, GraphNodeMap } from '../hooks/useSmartLayout'; +import { designerCache } from '../store/DesignerCache'; enum PromptNodes { BotAsks = 'botAsksNode', @@ -63,14 +64,24 @@ export const PromptWidget: FC = ({
- updateNodeBoundary(PromptNodes.BotAsks, boundary)}> + { + designerCache.cacheBoundary(botAsksNode.data, boundary); + updateNodeBoundary(PromptNodes.BotAsks, boundary); + }} + > {botAsks} - updateNodeBoundary(PromptNodes.UserAnswers, boundary)}> + { + designerCache.cacheBoundary(userAnswersNode.data, boundary); + updateNodeBoundary(PromptNodes.UserAnswers, boundary); + }} + > {userInput} diff --git a/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx index 7c03bd474e..6d3c3b6f86 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx @@ -18,6 +18,7 @@ import { WidgetContainerProps } from '../schema/uischema.types'; import { renderEdge } from '../components/lib/EdgeUtil'; import { SVGContainer } from '../components/lib/SVGContainer'; import { GraphNodeMap, useSmartLayout } from '../hooks/useSmartLayout'; +import { designerCache } from '../store/DesignerCache'; enum SwitchNodes { Switch = 'switchNode', @@ -81,7 +82,12 @@ export const SwitchConditionWidget: FunctionComponent - updateNodeBoundary(SwitchNodes.Switch, boundary)}> + { + designerCache.cacheBoundary(switchNode.data, boundary); + updateNodeBoundary(SwitchNodes.Switch, boundary); + }} + > {judgement} From e200c2fc1ae4657b6566328168230599dd25c440 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Mon, 24 Feb 2020 20:26:45 +0800 Subject: [PATCH 31/32] remove size limit in uischema (deprecated by smart layouter) --- .../packages/extensions/visual-designer/src/schema/uischema.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx index 879cce4f58..a8177ac85f 100644 --- a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx +++ b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx @@ -16,7 +16,6 @@ import { ForeachWidget } from '../widgets/ForeachWidget'; import { ChoiceInputChoices } from '../widgets/ChoiceInput'; import { ElementIcon } from '../utils/obiPropertyResolver'; import { ObiColors } from '../constants/ElementColors'; -import { measureChoiceInputDetailBoundary } from '../layouters/measureJsonBoundary'; import { UISchema, UIWidget } from './uischema.types'; @@ -41,7 +40,6 @@ const BaseInputSchema: UIWidget = { menu: 'none', content: data => data.property || '', children: data => (data.$type === SDKTypes.ChoiceInput ? : null), - size: data => measureChoiceInputDetailBoundary(data), colors: { theme: ObiColors.LightBlue, icon: ObiColors.AzureBlue, From dfac15dbd6e8e149d7fa3cd47d6e8cb9d237ca25 Mon Sep 17 00:00:00 2001 From: Ze Ye Date: Tue, 25 Feb 2020 10:15:11 +0800 Subject: [PATCH 32/32] fix FormCard css compatibility --- .../visual-designer/src/components/nodes/templates/FormCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx index 459340bfb0..116595a902 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx @@ -131,7 +131,7 @@ export const FormCard: FunctionComponent = ({ fontFamily: 'Segoe UI', overflowWrap: 'break-word', wordBreak: 'break-all', - display: 'contents', + display: 'inline-block', }} title={typeof label === 'string' ? label : ''} >