diff --git a/Composer/packages/client/src/ShellApi.ts b/Composer/packages/client/src/ShellApi.ts index e29575b3aa..b9b855d017 100644 --- a/Composer/packages/client/src/ShellApi.ts +++ b/Composer/packages/client/src/ShellApi.ts @@ -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'; @@ -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; @@ -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); apiClient.registerApi('shellNavigate', ({ shellPage, opts }) => shellNavigator(shellPage, opts)); apiClient.registerApi('isExpression', ({ expression }) => isExpression(expression)); apiClient.registerApi('createDialog', () => { @@ -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(), }; @@ -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; diff --git a/Composer/packages/client/src/constants/index.ts b/Composer/packages/client/src/constants/index.ts index 1dcd4b60b6..bcd258d2ac 100644 --- a/Composer/packages/client/src/constants/index.ts +++ b/Composer/packages/client/src/constants/index.ts @@ -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', diff --git a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx index 9bb140d325..a762c5cc12 100644 --- a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx +++ b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx @@ -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 = {}) => { diff --git a/Composer/packages/client/src/pages/design/index.tsx b/Composer/packages/client/src/pages/design/index.tsx index 10fddb13e6..190bb80427 100644 --- a/Composer/packages/client/src/pages/design/index.tsx +++ b/Composer/packages/client/src/pages/design/index.tsx @@ -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, @@ -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) { @@ -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', @@ -234,7 +223,7 @@ function DesignPage(props) { type: 'action', text: formatMessage('Cut'), buttonProps: { - disabled: !nodeOperationAvailable, + disabled: !visualEditorActive, iconProps: { iconName: 'Cut', }, @@ -246,7 +235,7 @@ function DesignPage(props) { type: 'action', text: formatMessage('Copy'), buttonProps: { - disabled: !nodeOperationAvailable, + disabled: !visualEditorActive, iconProps: { iconName: 'Copy', }, @@ -258,7 +247,7 @@ function DesignPage(props) { type: 'action', text: formatMessage('Delete'), buttonProps: { - disabled: !nodeOperationAvailable, + disabled: !visualEditorActive, iconProps: { iconName: 'Delete', }, diff --git a/Composer/packages/client/src/store/action/editors.ts b/Composer/packages/client/src/store/action/editors.ts index 826d9d0716..702e868612 100644 --- a/Composer/packages/client/src/store/action/editors.ts +++ b/Composer/packages/client/src/store/action/editors.ts @@ -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, + }, + }); +}; diff --git a/Composer/packages/client/src/store/index.tsx b/Composer/packages/client/src/store/index.tsx index cb8b80a9ae..02394aa310 100644 --- a/Composer/packages/client/src/store/index.tsx +++ b/Composer/packages/client/src/store/index.tsx @@ -50,6 +50,7 @@ const initialState: State = { publishVersions: {}, publishStatus: 'inactive', lastPublishChange: null, + visualEditorActive: false, }; interface StoreContextValue { diff --git a/Composer/packages/client/src/store/reducer/index.ts b/Composer/packages/client/src/store/reducer/index.ts index f2fc833fac..d0379d4f7a 100644 --- a/Composer/packages/client/src/store/reducer/index.ts +++ b/Composer/packages/client/src/store/reducer/index.ts @@ -224,6 +224,19 @@ const updatePublishStatus: ReducerFunc = (state, payload) => { return state; }; +const updateVisualEditorState: ReducerFunc = (state, { editorState }) => { + 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, @@ -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 }); diff --git a/Composer/packages/client/src/store/types.ts b/Composer/packages/client/src/store/types.ts index dc7ffac372..cfc2b77cc2 100644 --- a/Composer/packages/client/src/store/types.ts +++ b/Composer/packages/client/src/store/types.ts @@ -67,6 +67,7 @@ export interface State { publishVersions: any; publishStatus: any; lastPublishChange: any; + visualEditorActive: boolean; } export type ReducerFunc = (state: State, payload: T) => State; diff --git a/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx b/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx index 7825f09e9a..9b26e8dbaa 100644 --- a/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx +++ b/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx @@ -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 = props => { diff --git a/Composer/packages/extensions/obiformeditor/src/ObiFormEditor.tsx b/Composer/packages/extensions/obiformeditor/src/ObiFormEditor.tsx index b691ac57ec..edff5355a8 100644 --- a/Composer/packages/extensions/obiformeditor/src/ObiFormEditor.tsx +++ b/Composer/packages/extensions/obiformeditor/src/ObiFormEditor.tsx @@ -36,11 +36,11 @@ const ErrorInfo: React.FC = ({ componentStack, error }) => ( const ObiFormEditor: React.FC = 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 ( diff --git a/Composer/packages/extensions/visual-designer/__tests__/index.test.tsx b/Composer/packages/extensions/visual-designer/__tests__/index.test.tsx index 6b3c7dedef..4441295358 100644 --- a/Composer/packages/extensions/visual-designer/__tests__/index.test.tsx +++ b/Composer/packages/extensions/visual-designer/__tests__/index.test.tsx @@ -20,6 +20,7 @@ describe('', () => { it('should render the visual designer', async () => { const { getByTestId } = render( { return Promise.resolve(true); }, + syncEditorState: state => { + console.log('SyncState:', state); + }, }} /> diff --git a/Composer/packages/extensions/visual-designer/src/actions/clearSelectionState.ts b/Composer/packages/extensions/visual-designer/src/actions/clearSelectionState.ts new file mode 100644 index 0000000000..6671e8f2e3 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/actions/clearSelectionState.ts @@ -0,0 +1,9 @@ +const CLEAR_SELECTIONSTATE = 'VISUAL/SET_SELECTIONSTATE'; + +export default function setSelectionState() { + return { + type: CLEAR_SELECTIONSTATE, + }; +} + +export { CLEAR_SELECTIONSTATE }; diff --git a/Composer/packages/extensions/visual-designer/src/actions/setClipboard.ts b/Composer/packages/extensions/visual-designer/src/actions/setClipboard.ts new file mode 100644 index 0000000000..7948d366d9 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/actions/setClipboard.ts @@ -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 }; diff --git a/Composer/packages/extensions/visual-designer/src/actions/setDragSelection.ts b/Composer/packages/extensions/visual-designer/src/actions/setDragSelection.ts new file mode 100644 index 0000000000..44f6d1d4be --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/actions/setDragSelection.ts @@ -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 }; diff --git a/Composer/packages/extensions/visual-designer/src/actions/setFocusState.ts b/Composer/packages/extensions/visual-designer/src/actions/setFocusState.ts new file mode 100644 index 0000000000..0dd576df78 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/actions/setFocusState.ts @@ -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 }; diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index e40a6fb8cd..f2be5f77bc 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -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, @@ -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'; @@ -33,21 +37,26 @@ export const ObiEditor: FC = ({ 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) { @@ -197,9 +206,6 @@ export const ObiEditor: FC = ({ } else { setKeyBoardStatus('normal'); } - - // Notify container at every selection change. - onSelect(selectionContext.selectedIds); }, [focusedId, selectionContext]); useEffect( @@ -218,15 +224,17 @@ export const ObiEditor: FC = ({ 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); + } }, }); @@ -328,7 +336,7 @@ export const ObiEditor: FC = ({ }} onClick={e => { e.stopPropagation(); - dispatchEvent(NodeEventTypes.Focus, { id: '' }); + dispatch(clearSelectionState()); }} > {}, onOpen: () => {}, onChange: () => {}, - onSelect: () => {}, undo: () => {}, redo: () => {}, }; @@ -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; } diff --git a/Composer/packages/extensions/visual-designer/src/index.tsx b/Composer/packages/extensions/visual-designer/src/index.tsx index 76277a7fb8..9e49cb4568 100644 --- a/Composer/packages/extensions/visual-designer/src/index.tsx +++ b/Composer/packages/extensions/visual-designer/src/index.tsx @@ -8,6 +8,9 @@ import formatMessage from 'format-message'; import { ObiEditor } from './editors/ObiEditor'; import { NodeRendererContext } from './store/NodeRendererContext'; import { SelfHostContext } from './store/SelfHostContext'; +import { StoreContext } from './store/StoreContext'; +import useStore from './store/useStore'; +import setFocusState from './actions/setFocusState'; formatMessage.setup({ missingTranslation: 'ignore', @@ -43,13 +46,13 @@ const VisualDesigner: React.FC = ({ navTo, onFocusEvent, onFocusSteps, - onSelect, saveData, updateLgTemplate, getLgTemplates, removeLgTemplate, undo, redo, + syncEditorState, } = shellApi; const focusedId = Array.isArray(focusedSteps) && focusedSteps[0] ? focusedSteps[0] : ''; @@ -73,28 +76,46 @@ const VisualDesigner: React.FC = ({ }); }, [focusedEvent, focusedSteps, focusedTab]); + const { state, dispatch } = useStore(); + + useEffect(() => { + if (!isEqual(focusedSteps, state.focusedIds)) { + // NOTES: This hook works like 'componentWillReceiveProps'. Syncs props to store. + // TODO: remove this hook after fully adapting store to Shell. + dispatch(setFocusState(focusedSteps)); + } + }, [focusedSteps]); + + useEffect(() => { + syncEditorState(state); + }, [state]); + return ( - - -
- navTo(x, rest)} - onChange={x => saveData(x)} - onSelect={onSelect} - undo={undo} - redo={redo} - /> -
-
-
+ + + +
+ { + dispatch(setFocusState(ids)); + onFocusSteps(ids, fragmentId); + }} + focusedEvent={focusedEvent} + onFocusEvent={onFocusEvent} + onOpen={(x, rest) => navTo(x, rest)} + onChange={x => saveData(x)} + undo={undo} + redo={redo} + /> +
+
+
+
); }; @@ -121,6 +142,7 @@ VisualDesigner.defaultProps = { onFocusSteps: (_stepIds: string[], _fragment?: string) => {}, onSelect: (_ids: string[]) => {}, saveData: () => {}, + syncState: state => {}, }, }; diff --git a/Composer/packages/extensions/visual-designer/src/reducers/index.ts b/Composer/packages/extensions/visual-designer/src/reducers/index.ts new file mode 100644 index 0000000000..447b30802a --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/reducers/index.ts @@ -0,0 +1,41 @@ +import cloneDeep from 'lodash.clonedeep'; + +import { SET_CLIPBOARD } from '../actions/setClipboard'; +import { SET_DRAGSELECTION } from '../actions/setDragSelection'; +import { SET_FOCUSSTATE } from '../actions/setFocusState'; +import { CLEAR_SELECTIONSTATE } from '../actions/clearSelectionState'; +import { StoreState } from '../store/store'; + +const globalReducer = (state: StoreState, { type, payload }) => { + switch (type) { + case SET_CLIPBOARD: + return { + ...state, + clipboardActions: cloneDeep(payload.actions) || [], + }; + case SET_DRAGSELECTION: + return { + ...state, + selectedIds: cloneDeep(payload.ids) || [], + }; + case SET_FOCUSSTATE: + return { + ...state, + focusedIds: cloneDeep(payload.ids) || [], + selectedIds: [], + }; + case CLEAR_SELECTIONSTATE: + if (!state.focusedIds.length && !state.selectedIds.length) { + return state; + } + return { + ...state, + focusedIds: [], + selectedIds: [], + }; + default: + return state; + } +}; + +export default globalReducer; diff --git a/Composer/packages/extensions/visual-designer/src/store/StoreContext.ts b/Composer/packages/extensions/visual-designer/src/store/StoreContext.ts new file mode 100644 index 0000000000..5b836b0eef --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/store/StoreContext.ts @@ -0,0 +1,8 @@ +import { createContext } from 'react'; + +import { initialStore } from './store'; + +export const StoreContext = createContext({ + state: initialStore, + dispatch: action => {}, +}); diff --git a/Composer/packages/extensions/visual-designer/src/store/store.ts b/Composer/packages/extensions/visual-designer/src/store/store.ts new file mode 100644 index 0000000000..33635351f8 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/store/store.ts @@ -0,0 +1,11 @@ +export interface StoreState { + clipboardActions: any[]; + selectedIds: string[]; + focusedIds: string[]; +} + +export const initialStore: StoreState = { + clipboardActions: [], + selectedIds: [], + focusedIds: [], +}; diff --git a/Composer/packages/extensions/visual-designer/src/store/useStore.ts b/Composer/packages/extensions/visual-designer/src/store/useStore.ts new file mode 100644 index 0000000000..7f2a202aa1 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/store/useStore.ts @@ -0,0 +1,25 @@ +import { useReducer, Dispatch } from 'react'; + +import reducer from '../reducers'; + +import { initialStore, StoreState } from './store'; + +const applyMiddleware = (fn, middleware?): Dispatch<{ type: any; payload: any }> => { + if (typeof fn !== 'function') return action => {}; + + if (typeof middleware === 'function') { + return (...params) => { + middleware(...params); + return fn(...params); + }; + } + return fn; +}; + +const useStore = (dispatchMiddleware?: (action: { type: any; payload: any }) => void) => { + const [state, dispatch] = useReducer(reducer, initialStore); + + return { state: state as StoreState, dispatch: applyMiddleware(dispatch, dispatchMiddleware) }; +}; + +export default useStore; diff --git a/Composer/packages/lib/shared/src/types/sdk.ts b/Composer/packages/lib/shared/src/types/sdk.ts index c650ee86e6..66e15f4640 100644 --- a/Composer/packages/lib/shared/src/types/sdk.ts +++ b/Composer/packages/lib/shared/src/types/sdk.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ -interface BaseSchema { +export interface BaseSchema { /** Defines the valid properties for the component you are configuring (from a dialog .schema file) */ $type: string; /** Inline id for reuse of an inline definition */ diff --git a/Composer/packages/lib/shared/src/types/shell.ts b/Composer/packages/lib/shared/src/types/shell.ts index 3c922329a6..293d7ddbdf 100644 --- a/Composer/packages/lib/shared/src/types/shell.ts +++ b/Composer/packages/lib/shared/src/types/shell.ts @@ -89,6 +89,7 @@ export interface ShellData { }; dialogId: string; dialogs: DialogInfo[]; + focusedId: string; focusedEvent: string; focusedSteps: string[]; focusedTab?: string;