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: support multi-line node block in Visual Editor #2005

Merged
merged 37 commits into from
Feb 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
dbe4657
draft: DynamicBlock
yeze322 Feb 17, 2020
9dd4d75
don't setState in effect hook
yeze322 Feb 18, 2020
916a2a8
set default demo to editor
yeze322 Feb 18, 2020
9e2e21c
give decorators unique id
yeze322 Feb 18, 2020
d3bfa9f
implement ElementMeasurer for dynamic size
yeze322 Feb 18, 2020
010b200
pass onResize handler to enable boundary propagation
yeze322 Feb 18, 2020
b05c7c3
update FormCard css to not truncate text
yeze322 Feb 20, 2020
98279e6
apply ElementMeasurer to necessary widgets
yeze322 Feb 20, 2020
b8f7dc7
css: word break in FormCard
yeze322 Feb 20, 2020
35fd475
support smart layout in all input types
yeze322 Feb 20, 2020
2d9e289
revert exp changes in uischema
yeze322 Feb 20, 2020
30b9a92
add new hook useSmartLayout for layout changes
yeze322 Feb 20, 2020
4592bf0
apply useSmartLayouter to PromptWidgets
yeze322 Feb 20, 2020
0ee931d
apply useSmartLayout hook to IfConditionWidget
yeze322 Feb 20, 2020
62485b4
include boundary merge logic in useSmartLayout
yeze322 Feb 20, 2020
76d5a3b
IfCondition, Prompt adapt to new smart layout hook
yeze322 Feb 20, 2020
8b4c97a
apply useSmartLayout to ForeachWidget
yeze322 Feb 20, 2020
152b018
apply useSmartLayout to SwitchConditionWidget
yeze322 Feb 20, 2020
1fa383d
add comments to ElementMeasurer
yeze322 Feb 20, 2020
aa2def6
add comments in useSmartLayout
yeze322 Feb 20, 2020
2bb15f6
revert ObiEditor changes
yeze322 Feb 20, 2020
d155582
update FormCard style
yeze322 Feb 20, 2020
6ea6b58
revert unnecessary changes
yeze322 Feb 20, 2020
988e3b5
revert unnecessary transformer changes
yeze322 Feb 20, 2020
fefd425
Merge branch 'master' into visual/dynamic-layout
yeze322 Feb 20, 2020
a0c7c94
Merge branch 'master' into visual/dynamic-layout
yeze322 Feb 21, 2020
ddd2a17
fix IfConditionWidget layout
yeze322 Feb 21, 2020
440eb65
Merge branch 'master' into visual/dynamic-layout
yeze322 Feb 24, 2020
1dc610d
fix a potential undefined func call
yeze322 Feb 24, 2020
139e843
improve ts type in Switch
yeze322 Feb 24, 2020
1a8eae6
sort Switch case keys
yeze322 Feb 24, 2020
47b99f5
StepGroup apply useSmartLayouter
yeze322 Feb 24, 2020
5e2fcac
feat: boundary cache mechanism to fix flickering
yeze322 Feb 24, 2020
e200c2f
remove size limit in uischema (deprecated by smart layouter)
yeze322 Feb 24, 2020
dfac15d
fix FormCard css compatibility
yeze322 Feb 25, 2020
393de35
Merge branch 'master' into visual/dynamic-layout
yeze322 Feb 25, 2020
1767006
Merge branch 'master' into visual/dynamic-layout
a-b-r-o-w-n Feb 25, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -18,54 +17,51 @@ 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';
import { designerCache } from '../../store/DesignerCache';

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<StepNodeKey> => {
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<StepNodeKey>);
};

const calculateLayout = (nodes, boundaryMap): GraphLayout => {
nodes.forEach((x): void => (x.boundary = boundaryMap[x.id] || x.boundary));
const calculateLayout = (nodeMap: GraphNodeMap<StepNodeKey>): GraphLayout => {
const nodes = Object.keys(nodeMap)
.sort()
.map(stepName => nodeMap[stepName]);
return sequentialLayouter(nodes);
};

export const StepGroup: FunctionComponent<NodeProps> = ({ 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 (
<div css={{ width: boundary.width, height: boundary.height, position: 'relative' }}>
<SVGContainer>{Array.isArray(edges) ? edges.map(x => renderEdge(x)) : null}</SVGContainer>
{nodes
? nodes.map(x => (
? nodes.map((x, index) => (
<OffsetContainer key={`stepGroup/${x.id}/offset`} offset={x.offset}>
<StepRenderer
key={`stepGroup/${x.id}`}
id={x.id}
data={x.data}
onEvent={onEvent}
onResize={size => {
patchBoundary(x.id, size);
designerCache.cacheBoundary(x.data, size);
updateNodeBoundary(getStepKey(index), size);
}}
/>
</OffsetContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const contentHeight = boxHeight - headerHeight;

const containerStyle = {
width: boxWidth,
height: boxHeight,
minHeight: boxHeight,
fontSize: '12px',
cursor: 'pointer',
overflow: 'hidden',
Expand Down Expand Up @@ -92,17 +92,18 @@ export const FormCard: FunctionComponent<NodeProps> = ({
className="card__content"
css={{
width: '100%',
height: contentHeight,
minHeight: contentHeight,
display: 'inline-block',
}}
>
<div
css={{
fontWeight: 400,
paddingLeft: '5px',
margin: '3px 5px',
margin: '2px 5px',
fontSize: '14px',
lineHeight: '19px',
display: 'flex',
display: 'inline-flex',
alignItems: 'center',
}}
>
Expand All @@ -124,12 +125,13 @@ export const FormCard: FunctionComponent<NodeProps> = ({
css={{
height: '100%',
width: 'calc(100% - 20px)',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'initial',
fontSize: '12px',
lineHeight: '19px',
fontFamily: 'Segoe UI',
overflowWrap: 'break-word',
wordBreak: 'break-all',
display: 'inline-block',
}}
title={typeof label === 'string' ? label : ''}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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;
}

/**
* 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<ElementMeasurerProps> = ({ children, style, onResize }) => {
return (
<Measure
bounds
onResize={({ bounds: { width, height } }) => {
onResize(new Boundary(width, height));
}}
>
{({ measureRef }) => (
<div ref={measureRef} style={style}>
{children}
</div>
)}
</Measure>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -27,19 +28,19 @@ const TypesWithoutWrapper = [
SDKTypes.ChoiceInput,
];

export const StepRenderer: FC<NodeProps> = ({ id, data, onEvent }): JSX.Element => {
export const StepRenderer: FC<NodeProps> = ({ 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;
}
return (
<ElementWrapper id={id} onEvent={onEvent}>
{content}
<ElementMeasurer onResize={boundary => onResize(boundary)}>{content}</ElementMeasurer>
</ElementWrapper>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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<KeyType extends string, ValueType> = { [key in KeyType]: ValueType };

type BoundaryMap<T extends string> = MapWithEnumKey<T, Boundary>;

export type GraphNodeMap<T extends string> = MapWithEnumKey<T, GraphNode>;

export function useSmartLayout<T extends string>(
nodeMap: GraphNodeMap<T>,
layouter: (nodeMap: GraphNodeMap<T>) => GraphLayout,
onResize: (boundary: Boundary) => void
): {
layout: GraphLayout;
updateNodeBoundary: (nodeName: T, boundary: Boundary) => void;
} {
const [boundaryMap, setBoundaryMap] = useState<BoundaryMap<T>>({} as BoundaryMap<T>);
/**
* 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)) {
accumulatedPatches[nodeName] = boundary;
setBoundaryMap({
...boundaryMap,
...accumulatedPatches,
});
}
};

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 && onResize(layout.boundary);
}, [layout]);

return {
layout,
updateNodeBoundary: patchBoundary,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -41,7 +40,6 @@ const BaseInputSchema: UIWidget = {
menu: 'none',
content: data => data.property || '<property>',
children: data => (data.$type === SDKTypes.ChoiceInput ? <ChoiceInputChoices choices={data.choices} /> : null),
size: data => measureChoiceInputDetailBoundary(data),
colors: {
theme: ObiColors.LightBlue,
icon: ObiColors.AzureBlue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -15,6 +17,9 @@ export interface UIWidgetContext {

/** Handle UI events */
onEvent: WidgetEventHandler;

/** Report widget boundary */
onResize: (boundary: Boundary) => void;
}

const parseWidgetSchema = (widgetSchema: UIWidget) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
Loading