Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions Composer/packages/client/src/ShellApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useContext, useMemo, useState } from 'react';
import React, { useEffect, useContext, useMemo } from 'react';
import { ShellData } from 'shared';
import isEqual from 'lodash.isequal';
import get from 'lodash.get';
Expand Down Expand Up @@ -52,10 +52,6 @@ const shellNavigator = (shellPage: string, opts: { id?: string } = {}) => {
};

export const ShellApi: React.FC = () => {
// HACK: `onSelect` should actually change some states
// TODO: (leilei, ze) fix it when refactoring shell state management.
const [, forceUpdate] = useState();

const { state, actions } = useContext(StoreContext);
const { dialogs, schemas, lgFiles, luFiles, designPageLocation, focusPath, breadcrumb, botName } = state;
const updateDialog = actions.updateDialog;
Expand Down Expand Up @@ -93,7 +89,7 @@ export const ShellApi: React.FC = () => {
apiClient.registerApi('navTo', navTo);
apiClient.registerApi('onFocusEvent', focusEvent);
apiClient.registerApi('onFocusSteps', focusSteps);
apiClient.registerApi('onSelect', onSelect);
apiClient.registerApi('syncEditorState', syncEditorState);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this called syncEditorState? We shouldn't be "syncing" anything, that suggested were mirroring state. The Shell data should hydrate the visual editor.

Copy link
Contributor Author

@yeze322 yeze322 Oct 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe its name should be onStateChange or onStoreChange: on each state change, notify container.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a tricky problem here. Because of the iframe layer breaks change detection, state exchanging between Shell and editors has a really high cost (which resulted us to optimize it in #1106 ), so the normal pattern value + onChange no longer works fine. If we split 3 props into 3 pairs of 'value + onChange' to hydrate with Shell data, it will cause performance issue. Therefore, there is a onStateChange API for handling all state exchangings.

Copy link
Contributor Author

@yeze322 yeze322 Oct 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to comments below, we don't need the syncEditorState api. The interface will be:

<VisualEditor ... selection={selection} onChangeSelection={(selection) => shellApi.onChangeSelection(selection)} />

My only concern here is during a drag selection, the performance will be poor

apiClient.registerApi('shellNavigate', ({ shellPage, opts }) => shellNavigator(shellPage, opts));
apiClient.registerApi('isExpression', ({ expression }) => isExpression(expression));
apiClient.registerApi('createDialog', () => {
Expand Down Expand Up @@ -170,7 +166,8 @@ export const ShellApi: React.FC = () => {
currentDialog,
dialogId,
focusedEvent: selected,
focusedSteps: focused ? [focused] : selected ? [selected] : [],
focusedSteps: focused ? [focused] : [],
focusedId: focused || selected || '',
focusedTab: promptTab,
hosted: !!isAbsHosted(),
};
Expand Down Expand Up @@ -329,8 +326,8 @@ export const ShellApi: React.FC = () => {
actions.focusTo(dataPath, fragment);
}

function onSelect(ids) {
forceUpdate(ids);
function syncEditorState({ editorState }) {
actions.syncVisualEditorState(editorState);
}

return null;
Expand Down
1 change: 1 addition & 0 deletions Composer/packages/client/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export enum ActionTypes {
SET_ERROR = 'SET_ERROR',
TO_START_BOT = 'TO_START_BOT',
EDITOR_RESET_VISUAL = 'EDITOR_RESET_VISUAL',
EDITOR_SYNCSTATE_VISUAL = 'EDITOR_SYNCSTATE_VISUAL',
USER_LOGIN_SUCCESS = 'USER_LOGIN_SUCCESS',
USER_LOGIN_FAILURE = 'USER_LOGIN_FAILURE',
USER_SESSION_EXPIRED = 'USER_SESSION_EXPIRED',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ const shellApi = {
return apiClient.apiCall('onFocusSteps', { subPaths, fragment });
},

onSelect: (ids: string[]) => {
return apiClient.apiCall('onSelect', { ids });
syncEditorState: editorState => {
return apiClient.apiCall('syncEditorState', { editorState });
},

shellNavigate: (shellPage, opts = {}) => {
Expand Down
19 changes: 4 additions & 15 deletions Composer/packages/client/src/pages/design/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ const rootPath = BASEPATH.replace(/\/+$/g, '');

function DesignPage(props) {
const { state, actions } = useContext(StoreContext);
const { dialogs, designPageLocation, breadcrumb } = state;
const { dialogs, designPageLocation, breadcrumb, visualEditorActive } = state;
const {
removeDialog,
setDesignPageLocation,
Expand All @@ -124,7 +124,6 @@ function DesignPage(props) {
const { dialogId, selected } = designPageLocation;
const [triggerModalVisible, setTriggerModalVisibility] = useState(false);
const [triggerButtonVisible, setTriggerButtonVisibility] = useState(false);
const [nodeOperationAvailable, setNodeOperationAvailability] = useState(false);

useEffect(() => {
if (match) {
Expand Down Expand Up @@ -195,16 +194,6 @@ function DesignPage(props) {
}
};

useEffect(() => {
// HACK: wait until visual editor finish rerender.
// TODO: (ze) expose visual editor store to Shell and (leilei) intercept store events.
setTimeout(() => {
VisualEditorAPI.hasElementSelected().then(selected => {
setNodeOperationAvailability(selected);
});
}, 100);
});

const toolbarItems = [
{
type: 'action',
Expand Down Expand Up @@ -234,7 +223,7 @@ function DesignPage(props) {
type: 'action',
text: formatMessage('Cut'),
buttonProps: {
disabled: !nodeOperationAvailable,
disabled: !visualEditorActive,
iconProps: {
iconName: 'Cut',
},
Expand All @@ -246,7 +235,7 @@ function DesignPage(props) {
type: 'action',
text: formatMessage('Copy'),
buttonProps: {
disabled: !nodeOperationAvailable,
disabled: !visualEditorActive,
iconProps: {
iconName: 'Copy',
},
Expand All @@ -258,7 +247,7 @@ function DesignPage(props) {
type: 'action',
text: formatMessage('Delete'),
buttonProps: {
disabled: !nodeOperationAvailable,
disabled: !visualEditorActive,
iconProps: {
iconName: 'Delete',
},
Expand Down
9 changes: 9 additions & 0 deletions Composer/packages/client/src/store/action/editors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,12 @@ export const resetVisualEditor: ActionCreator = ({ dispatch }, isReset) => {
payload: { isReset },
});
};

export const syncVisualEditorState: ActionCreator = ({ dispatch }, editorState) => {
dispatch({
type: ActionTypes.EDITOR_SYNCSTATE_VISUAL,
payload: {
editorState,
},
});
};
1 change: 1 addition & 0 deletions Composer/packages/client/src/store/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const initialState: State = {
publishVersions: {},
publishStatus: 'inactive',
lastPublishChange: null,
visualEditorActive: false,
};

interface StoreContextValue {
Expand Down
14 changes: 14 additions & 0 deletions Composer/packages/client/src/store/reducer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,19 @@ const updatePublishStatus: ReducerFunc = (state, payload) => {
return state;
};

const updateVisualEditorState: ReducerFunc = (state, { editorState }) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like how you've defined this to suggest that a grouping of nodes is a concern of the visual editor. This isn't the case. The visual editor has a mechanism to group nodes and nothing more.

Here is why: If the visual editor gets redrawn for whatever reason, we still want to hold the grouped nodes in state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, This logic is supposed to be moved into lower level, at least down to ExtensionContainer / VisualEditorContainer.

Copy link
Contributor Author

@yeze322 yeze322 Oct 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this one

<VisualEditor clipboardActions={actions} setClipboardActions={(actions) => shellApi.setClipboardActions(actions)} />

const arrayHasElements = (arr: any): boolean => Array.isArray(arr) && arr.length > 0;
const isVisualEditorActive = ({ selectedIds, focusedIds }): boolean => {
return arrayHasElements(selectedIds) || arrayHasElements(focusedIds);
};

const visualEditorActive = isVisualEditorActive(editorState);
if (visualEditorActive !== state.visualEditorActive) {
state.visualEditorActive = visualEditorActive;
}
return state;
};

export const reducer = createReducer({
[ActionTypes.GET_PROJECT_SUCCESS]: getProjectSuccess,
[ActionTypes.GET_RECENT_PROJECTS_SUCCESS]: getRecentProjectsSuccess,
Expand Down Expand Up @@ -261,4 +274,5 @@ export const reducer = createReducer({
[ActionTypes.PUBLISH_ERROR]: updatePublishStatus,
[ActionTypes.PUBLISH_BEGIN]: updatePublishStatus,
[ActionTypes.GET_ENDPOINT_SUCCESS]: updateRemoteEndpoint,
[ActionTypes.EDITOR_SYNCSTATE_VISUAL]: updateVisualEditorState,
} as { [type in ActionTypes]: ReducerFunc });
1 change: 1 addition & 0 deletions Composer/packages/client/src/store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export interface State {
publishVersions: any;
publishStatus: any;
lastPublishChange: any;
visualEditorActive: boolean;
}

export type ReducerFunc<T = any> = (state: State, payload: T) => State;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface FormEditorProps extends ShellData {
onBlur?: () => void;
onChange: (newData: object, updatePath?: string) => void;
shellApi: ShellApi;
focusedId: string;
}

export const FormEditor: React.FunctionComponent<FormEditorProps> = props => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ const ErrorInfo: React.FC<FallbackProps> = ({ componentStack, error }) => (

const ObiFormEditor: React.FC<FormEditorProps> = props => {
const onChange = data => {
props.onChange(data, props.focusedSteps[0]);
props.onChange(data, props.focusedId);
};

// only need to debounce the change handler when focusedSteps change
const debouncedOnChange = useMemo(() => debounce(onChange, 500), [props.focusedSteps[0]]);
const debouncedOnChange = useMemo(() => debounce(onChange, 500), [props.focusedId]);
const key = get(props.data, '$designer.id', props.focusPath);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('<VisualDesigner />', () => {
it('should render the visual designer', async () => {
const { getByTestId } = render(
<VisualDesigner
hosted={false}
data={{ content: '{"json": "some data"}' }}
currentDialog={{ id: 'Main', displayName: 'Main', isRoot: false }}
dialogId="SomeDialog"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ export class VisualEditorDemo extends Component {
removeLgTemplate: () => {
return Promise.resolve(true);
},
syncEditorState: state => {
console.log('SyncState:', state);
},
}}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const CLEAR_SELECTIONSTATE = 'VISUAL/SET_SELECTIONSTATE';

export default function setSelectionState() {
return {
type: CLEAR_SELECTIONSTATE,
};
}

export { CLEAR_SELECTIONSTATE };
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { BaseSchema } from 'shared';

const SET_CLIPBOARD = 'VISUAL/SET_CLIPBOARD';

export default function setClipboard(clipboardActions: BaseSchema[]) {
return {
type: SET_CLIPBOARD,
payload: {
actions: clipboardActions,
},
};
}

export { SET_CLIPBOARD };
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const SET_DRAGSELECTION = 'VISUAL/SET_DRAGSELECTION';

export default function setSelection(seletedIds: string[]) {
return {
type: SET_DRAGSELECTION,
payload: {
ids: seletedIds,
},
};
}

export { SET_DRAGSELECTION };
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const SET_FOCUSSTATE = 'VISUAL/SET_FOCUSSTATE';

export default function setFocusState(focusedIds: string[]) {
return {
type: SET_FOCUSSTATE,
payload: {
ids: focusedIds,
},
};
}

export { SET_FOCUSSTATE };
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AttrNames } from '../constants/ElementAttributes';
import { NodeRendererContext } from '../store/NodeRendererContext';
import { SelectionContext, SelectionContextData } from '../store/SelectionContext';
import { ClipboardContext } from '../store/ClipboardContext';
import { StoreContext } from '../store/StoreContext';
import {
deleteNode,
insert,
Expand All @@ -23,6 +24,9 @@ import { moveCursor } from '../utils/cursorTracker';
import { NodeIndexGenerator } from '../utils/NodeIndexGetter';
import { normalizeSelection } from '../utils/normalizeSelection';
import { KeyboardZone } from '../components/lib/KeyboardZone';
import setClipboard from '../actions/setClipboard';
import setDragSelection from '../actions/setDragSelection';
import clearSelectionState from '../actions/clearSelectionState';

import { AdaptiveDialogEditor } from './AdaptiveDialogEditor';

Expand All @@ -33,21 +37,26 @@ export const ObiEditor: FC<ObiEditorProps> = ({
onFocusSteps,
onOpen,
onChange,
onSelect,
undo,
redo,
}): JSX.Element | null => {
let divRef;

const { state, dispatch } = useContext(StoreContext);
const { focusedId, focusedEvent, updateLgTemplate, getLgTemplates, removeLgTemplate } = useContext(
NodeRendererContext
);
const [clipboardContext, setClipboardContext] = useState({
clipboardActions: [],
setClipboardActions: actions => setClipboardContext({ ...clipboardContext, clipboardActions: actions }),
setClipboardActions: actions => {
// TODO (ze): retire local context in following refactoring PR.
dispatch(setClipboard(actions));
setClipboardContext({ ...clipboardContext, clipboardActions: actions });
},
});

const lgApi = { getLgTemplates, removeLgTemplate, updateLgTemplate };
// TODO: clean this long event dispatcher, migrate to redux-style
const dispatchEvent = (eventName: NodeEventTypes, eventData: any): any => {
let handler;
switch (eventName) {
Expand Down Expand Up @@ -197,9 +206,6 @@ export const ObiEditor: FC<ObiEditorProps> = ({
} else {
setKeyBoardStatus('normal');
}

// Notify container at every selection change.
onSelect(selectionContext.selectedIds);
}, [focusedId, selectionContext]);

useEffect(
Expand All @@ -218,15 +224,17 @@ export const ObiEditor: FC<ObiEditorProps> = ({
const selectedIndices = selection.getSelectedIndices();
const selectedIds = selectedIndices.map(index => nodeItems[index].key as string);

if (selectedIds.length === 1) {
// TODO: Change to focus all selected nodes after Form Editor support showing multiple nodes.
onFocusSteps(selectedIds);
}

// TODO (ze): retire local context in following refactoring PR.
dispatch(setDragSelection(selectedIds));
setSelectionContext({
...selectionContext,
selectedIds,
});

if (selectedIds.length === 1) {
// TODO: Change to focus all selected nodes after Form Editor support showing multiple nodes.
onFocusSteps(selectedIds);
}
},
});

Expand Down Expand Up @@ -328,7 +336,7 @@ export const ObiEditor: FC<ObiEditorProps> = ({
}}
onClick={e => {
e.stopPropagation();
dispatchEvent(NodeEventTypes.Focus, { id: '' });
dispatch(clearSelectionState());
}}
>
<AdaptiveDialogEditor
Expand Down Expand Up @@ -356,7 +364,6 @@ ObiEditor.defaultProps = {
onFocusEvent: () => {},
onOpen: () => {},
onChange: () => {},
onSelect: () => {},
undo: () => {},
redo: () => {},
};
Expand All @@ -366,12 +373,15 @@ interface ObiEditorProps {
// Obi raw json
data: any;
focusedSteps: string[];
/**
* @param {string[]} stepIds A list of Adaptive action's id to be selected.
* @param {string} [fragment] Id of selected fragment in an action. Fragment means small elements.
*/
onFocusSteps: (stepIds: string[], fragment?: string) => any;
focusedEvent: string;
onFocusEvent: (eventId: string) => any;
onOpen: (calleeDialog: string, callerId: string) => any;
onChange: (newDialog: any) => any;
onSelect: (selection: any) => any;
undo?: () => any;
redo?: () => any;
}
Loading