diff --git a/Composer/cypress/integration/Breadcrumb.spec.js b/Composer/cypress/integration/Breadcrumb.spec.js index b6f94e0465..9eaf3a7749 100644 --- a/Composer/cypress/integration/Breadcrumb.spec.js +++ b/Composer/cypress/integration/Breadcrumb.spec.js @@ -53,7 +53,7 @@ context('breadcrumb', () => { it('can show action name in breadcrumb', () => { cy.wait(100); cy.get('[data-testid="ProjectTree"]').within(() => { - cy.getByText('ToDoBot.Main').click(); + cy.getByText('Handle ConversationUpdate').click(); cy.wait(500); }); diff --git a/Composer/packages/client/src/ShellApi.ts b/Composer/packages/client/src/ShellApi.ts index d9a4fb1c5b..d7e4fc976f 100644 --- a/Composer/packages/client/src/ShellApi.ts +++ b/Composer/packages/client/src/ShellApi.ts @@ -28,6 +28,7 @@ export interface ShellData { dialogId: string; focusedEvent: string; focusedSteps: string[]; + focusedTab?: string; } const apiClient = new ApiClient(); @@ -78,7 +79,7 @@ export const ShellApi: React.FC = () => { const createLuFile = actions.createLuFile; const createLgFile = actions.createLgFile; - const { dialogId, selected, focused } = designPageLocation; + const { dialogId, selected, focused, promptTab } = designPageLocation; const { LG, LU } = FileTargetTypes; const { CREATE, UPDATE } = FileChangeTypes; @@ -140,7 +141,7 @@ export const ShellApi: React.FC = () => { const editorWindow = window.frames[FORM_EDITOR]; apiClient.apiCall('reset', getState(FORM_EDITOR), editorWindow); } - }, [dialogs, lgFiles, luFiles, focusPath, selected, focused]); + }, [dialogs, lgFiles, luFiles, focusPath, selected, focused, promptTab]); useEffect(() => { const schemaError = get(schemas, 'diagnostics', []); @@ -176,6 +177,7 @@ export const ShellApi: React.FC = () => { dialogId, focusedEvent: selected, focusedSteps: focused ? [focused] : selected ? [selected] : [], + focusedTab: promptTab, }; } @@ -304,13 +306,13 @@ export const ShellApi: React.FC = () => { actions.selectTo(subPath); } - function focusSteps({ subPaths = [] }, event) { + function focusSteps({ subPaths = [], fragment }, event) { cleanData(); let dataPath: string = subPaths[0]; - if (event.source.name === FORM_EDITOR && focused) { + if (event.source.name === FORM_EDITOR && focused && dataPath !== focused) { dataPath = `${focused}.${dataPath}`; } - actions.focusTo(dataPath); + actions.focusTo(dataPath, fragment); } function onSelect(ids) { diff --git a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx index 038e1973cb..8542049eec 100644 --- a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx +++ b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx @@ -52,8 +52,8 @@ const shellApi = { return apiClient.apiCall('onFocusEvent', { subPath }); }, - onFocusSteps: (subPaths: string[]) => { - return apiClient.apiCall('onFocusSteps', { subPaths }); + onFocusSteps: (subPaths: string[], fragment?: string) => { + return apiClient.apiCall('onFocusSteps', { subPaths, fragment }); }, onSelect: (ids: string[]) => { diff --git a/Composer/packages/client/src/pages/design/index.js b/Composer/packages/client/src/pages/design/index.tsx similarity index 82% rename from Composer/packages/client/src/pages/design/index.js rename to Composer/packages/client/src/pages/design/index.tsx index 40b3365b5f..d4710ace99 100644 --- a/Composer/packages/client/src/pages/design/index.js +++ b/Composer/packages/client/src/pages/design/index.tsx @@ -1,25 +1,26 @@ import React, { Fragment, useContext, useEffect, useMemo, useState } from 'react'; -import { ActionButton, Breadcrumb, Icon } from 'office-ui-fabric-react'; +import { ActionButton, Breadcrumb, Icon, IBreadcrumbItem } from 'office-ui-fabric-react'; import formatMessage from 'format-message'; import { globalHistory } from '@reach/router'; import { toLower, get } from 'lodash'; +import { PromptTab } from 'shared-menus'; import { VisualEditorAPI } from '../../messenger/FrameAPI'; import { TestController } from '../../TestController'; import { BASEPATH, DialogDeleting } from '../../constants'; import { createSelectedPath, deleteTrigger, getbreadcrumbLabel } from '../../utils'; import { TriggerCreationModal } from '../../components/ProjectTree/TriggerCreationModal'; +import { Conversation } from '../../components/Conversation'; +import { DialogStyle } from '../../components/Modal/styles'; +import { OpenConfirmModal } from '../../components/Modal/Confirm'; +import { ProjectTree } from '../../components/ProjectTree'; +import { StoreContext } from '../../store'; +import { ToolBar } from '../../components/ToolBar/index'; +import { clearBreadcrumb } from '../../utils/navigation'; +import { getNewDesigner } from '../../utils/dialogUtil'; +import undoHistory from '../../store/middlewares/undo/history'; -import { Conversation } from './../../components/Conversation'; -import { DialogStyle } from './../../components/Modal/styles'; import NewDialogModal from './new-dialog-modal'; -import { OpenConfirmModal } from './../../components/Modal/Confirm'; -import { ProjectTree } from './../../components/ProjectTree'; -import { StoreContext } from './../../store'; -import { ToolBar } from './../../components/ToolBar/index'; -import { clearBreadcrumb } from './../../utils/navigation'; -import { getNewDesigner } from './../../utils/dialogUtil'; -import undoHistory from './../../store/middlewares/undo/history'; import { breadcrumbClass, contentWrapper, @@ -86,7 +87,7 @@ function onRenderBlankVisual(isTriggerEmpty, onClickAddTrigger) { } function getAllRef(targetId, dialogs) { - let refs = []; + let refs: string[] = []; dialogs.forEach(dialog => { if (dialog.id === targetId) { refs = refs.concat(dialog.referredDialogs); @@ -97,6 +98,14 @@ function getAllRef(targetId, dialogs) { return refs; } +const getTabFromFragment = () => { + const tab = window.location.hash.substring(1); + + if (Object.values(PromptTab).includes(tab)) { + return tab; + } +}; + const rootPath = BASEPATH.replace(/\/+$/g, ''); function DesignPage(props) { @@ -127,7 +136,9 @@ function DesignPage(props) { focused: params.get('focused'), breadcrumb: location.state ? location.state.breadcrumb || [] : [], onBreadcrumbItemClick: handleBreadcrumbItemClick, + promptTab: getTabFromFragment(), }); + // @ts-ignore globalHistory._onTransitionComplete(); } else { //leave design page should clear the history @@ -262,21 +273,34 @@ function DesignPage(props) { }, ]; - function handleBreadcrumbItemClick(_event, { dialogId, selected, focused, index }) { - setectAndfocus(dialogId, selected, focused, clearBreadcrumb(breadcrumb, index)); + function handleBreadcrumbItemClick(_event, item) { + if (item) { + const { dialogId, selected, focused, index } = item; + setectAndfocus(dialogId, selected, focused, clearBreadcrumb(breadcrumb, index)); + } } const breadcrumbItems = useMemo(() => { const items = dialogs.length > 0 - ? breadcrumb.reduce((result, item, index) => { - const { dialogId, selected, focused } = item; - const text = getbreadcrumbLabel(dialogs, dialogId, selected, focused); - if (text) { - result.push({ text, isRoot: !selected && !focused, ...item, index, onClick: handleBreadcrumbItemClick }); - } - return result; - }, []) + ? breadcrumb.reduce( + (result, item, index) => { + const { dialogId, selected, focused } = item; + const text = getbreadcrumbLabel(dialogs, dialogId, selected, focused); + if (text) { + result.push({ + // @ts-ignore + index, + isRoot: !selected && !focused, + text, + ...item, + onClick: handleBreadcrumbItemClick, + }); + } + return result; + }, + [] as IBreadcrumbItem[] + ) : []; return ( `${result} ${item} \n`, '')}`; setting = { - onRenderContent: onRenderContent, + onRenderContent, style: DialogStyle.Console, }; } else { @@ -323,20 +347,21 @@ function DesignPage(props) { const content = deleteTrigger(dialogs, id, index); if (content) { await updateDialog({ id, content }); - let current = /\[(\d+)\]/g.exec(selected)[1]; + const match = /\[(\d+)\]/g.exec(selected); + const current = match && match[1]; if (!current) return; - current = parseInt(current); - if (index === current) { - if (current - 1 >= 0) { + const currentIdx = parseInt(current); + if (index === currentIdx) { + if (currentIdx - 1 >= 0) { //if the deleted node is selected and the selected one is not the first one, navTo the previous trigger; - selectTo(createSelectedPath(current - 1)); + selectTo(createSelectedPath(currentIdx - 1)); } else { //if the deleted node is selected and the selected one is the first one, navTo the first trigger; navTo(id); } - } else if (index < current) { + } else if (index < currentIdx) { //if the deleted node is at the front, navTo the current one; - selectTo(createSelectedPath(current - 1)); + selectTo(createSelectedPath(currentIdx - 1)); } } } diff --git a/Composer/packages/client/src/pages/language-generation/code-editor.js b/Composer/packages/client/src/pages/language-generation/code-editor.js index c0c2197630..fbc95713a1 100644 --- a/Composer/packages/client/src/pages/language-generation/code-editor.js +++ b/Composer/packages/client/src/pages/language-generation/code-editor.js @@ -38,6 +38,8 @@ export default function CodeEditor(props) { options={{ lineNumbers: 'on', minimap: 'on', + lineDecorationsWidth: undefined, + lineNumbersMinChars: false, }} errorMsg={errorMsg} value={content} diff --git a/Composer/packages/client/src/pages/language-understanding/code-editor.js b/Composer/packages/client/src/pages/language-understanding/code-editor.js index 70d1283564..6b7e8cdd37 100644 --- a/Composer/packages/client/src/pages/language-understanding/code-editor.js +++ b/Composer/packages/client/src/pages/language-understanding/code-editor.js @@ -37,6 +37,8 @@ export default function CodeEditor(props) { options={{ lineNumbers: 'on', minimap: 'on', + lineDecorationsWidth: undefined, + lineNumbersMinChars: false, }} errorMsg={errorMsg} value={content} diff --git a/Composer/packages/client/src/store/action/navigation.ts b/Composer/packages/client/src/store/action/navigation.ts index dab00b595a..65190a4c27 100644 --- a/Composer/packages/client/src/store/action/navigation.ts +++ b/Composer/packages/client/src/store/action/navigation.ts @@ -6,11 +6,11 @@ import { updateBreadcrumb, navigateTo, checkUrl, getUrlSearch, BreadcrumbUpdateT export const setDesignPageLocation: ActionCreator = ( { dispatch }, - { dialogId = '', selected = '', focused = '', breadcrumb = [], onBreadcrumbItemClick } + { dialogId = '', selected = '', focused = '', breadcrumb = [], onBreadcrumbItemClick, promptTab } ) => { dispatch({ type: ActionTypes.SET_DESIGN_PAGE_LOCATION, - payload: { dialogId, focused, selected, breadcrumb, onBreadcrumbItemClick }, + payload: { dialogId, focused, selected, breadcrumb, onBreadcrumbItemClick, promptTab }, }); }; @@ -35,7 +35,7 @@ export const selectTo: ActionCreator = ({ getState }, selectPath) => { navigateTo(currentUri, { state: { breadcrumb: updateBreadcrumb(breadcrumb, BreadcrumbUpdateType.Selected) } }); }; -export const focusTo: ActionCreator = ({ getState }, focusPath) => { +export const focusTo: ActionCreator = ({ getState }, focusPath, fragment) => { const state = getState(); const { dialogId, selected } = state.designPageLocation; let { breadcrumb } = state; @@ -54,6 +54,9 @@ export const focusTo: ActionCreator = ({ getState }, focusPath) => { breadcrumb = updateBreadcrumb(breadcrumb, BreadcrumbUpdateType.Selected); } + if (fragment && typeof fragment === 'string') { + currentUri += `#${fragment}`; + } if (checkUrl(currentUri, state.designPageLocation)) return; navigateTo(currentUri, { state: { breadcrumb } }); }; diff --git a/Composer/packages/client/src/store/reducer/index.ts b/Composer/packages/client/src/store/reducer/index.ts index 8da589e945..d3b8aa328c 100644 --- a/Composer/packages/client/src/store/reducer/index.ts +++ b/Composer/packages/client/src/store/reducer/index.ts @@ -149,7 +149,7 @@ const setError: ReducerFunc = (state, payload) => { return state; }; -const setDesignPageLocation: ReducerFunc = (state, { dialogId, selected, focused, breadcrumb }) => { +const setDesignPageLocation: ReducerFunc = (state, { dialogId, selected, focused, breadcrumb, promptTab }) => { //generate focusedPath. This will remove when all focusPath related is removed state.focusPath = dialogId + '#'; if (focused) { @@ -162,7 +162,7 @@ const setDesignPageLocation: ReducerFunc = (state, { dialogId, selected, focused breadcrumb.push({ dialogId, selected, focused }); state.breadcrumb = breadcrumb; - state.designPageLocation = { dialogId, selected, focused }; + state.designPageLocation = { dialogId, selected, focused, promptTab }; return state; }; const syncEnvSetting: ReducerFunc = (state, { settings }) => { diff --git a/Composer/packages/client/src/store/types.ts b/Composer/packages/client/src/store/types.ts index 2ebc9278fa..6dc83d5796 100644 --- a/Composer/packages/client/src/store/types.ts +++ b/Composer/packages/client/src/store/types.ts @@ -2,6 +2,7 @@ // TODO: remove this once we can expand the types /* eslint-disable @typescript-eslint/no-explicit-any */ import React from 'react'; +import { PromptTab } from 'shared-menus'; import { CreationFlowStatus, BotStatus } from '../constants'; @@ -151,4 +152,5 @@ export interface DesignPageLocation { dialogId: string; selected: string; focused: string; + promptTab?: PromptTab; } diff --git a/Composer/packages/client/src/utils/navigation.ts b/Composer/packages/client/src/utils/navigation.ts index d0516ca8a3..134942521d 100644 --- a/Composer/packages/client/src/utils/navigation.ts +++ b/Composer/packages/client/src/utils/navigation.ts @@ -1,7 +1,7 @@ import { cloneDeep } from 'lodash'; import { navigate, NavigateOptions } from '@reach/router'; -import { BreadcrumbItem } from '../store/types'; +import { BreadcrumbItem, DesignPageLocation } from '../store/types'; import { BASEPATH } from './../constants/index'; import { resolveToBasePath } from './fileUtil'; @@ -61,11 +61,11 @@ export function getUrlSearch(selected: string, focused: string): string { return result; } -export function checkUrl( - currentUri: string, - { dialogId, selected, focused }: { dialogId: string; selected: string; focused: string } -) { - const lastUri = `/dialogs/${dialogId}${getUrlSearch(selected, focused)}`; +export function checkUrl(currentUri: string, { dialogId, selected, focused, promptTab }: DesignPageLocation) { + let lastUri = `/dialogs/${dialogId}${getUrlSearch(selected, focused)}`; + if (promptTab) { + lastUri += `#${promptTab}`; + } return lastUri === currentUri; } diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/LgEditorField.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/LgEditorField.tsx index f7c30b5026..7bd66ae028 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/LgEditorField.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/LgEditorField.tsx @@ -1,92 +1,19 @@ import React from 'react'; -import { useState, useEffect } from 'react'; -import { RichEditor } from 'code-editor'; import { BFDFieldProps } from '../types'; +import { LgEditorWidget } from '../widgets/LgEditorWidget'; import { BaseField } from './BaseField'; -const LG_HELP = - 'https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/language-generation/docs/lg-file-format.md'; - -const getInitialTemplate = (formData?: string): string => { - let newTemplate = formData || ''; - - if (newTemplate.indexOf('bfdactivity-') !== -1) { - return ''; - } else if (newTemplate && !newTemplate.startsWith('-')) { - newTemplate = `-${newTemplate}`; - } - - return newTemplate; -}; - export const LgEditorField: React.FC = props => { - const { formContext } = props; - const [templateToRender, setTemplateToRender] = useState({ Name: '', Body: '' }); - const lgId = `bfdactivity-${formContext.dialogId}`; - const [errorMsg, setErrorMsg] = useState(''); - - const ensureTemplate = async (newBody?: string): Promise => { - const templates = await formContext.shellApi.getLgTemplates('common'); - const template = templates.find(template => { - return template.Name === lgId; - }); - if (template === null || template === undefined) { - const newTemplate = getInitialTemplate(newBody); - - if (formContext.dialogId && newTemplate) { - formContext.shellApi.updateLgTemplate('common', lgId, newTemplate); - props.onChange(`[${lgId}]`); - } - setTemplateToRender({ Name: `# ${lgId}`, Body: newTemplate }); - } else { - if (templateToRender.Name === '') { - setTemplateToRender({ Name: `# ${lgId}`, Body: template.Body }); - } - } - }; - - const onChange = (data): void => { - // hit the lg api and replace it's Body with data - if (formContext.dialogId) { - let dataToEmit = data.trim(); - if (dataToEmit.length > 0 && dataToEmit[0] !== '-') { - dataToEmit = `-${dataToEmit}`; - } - - if (dataToEmit.length > 0) { - setTemplateToRender({ Name: templateToRender.Name, Body: data }); - formContext.shellApi - .updateLgTemplate('common', lgId, dataToEmit) - .then(() => setErrorMsg('')) - .catch(error => setErrorMsg(error)); - props.onChange(`[${lgId}]`); - } else { - setTemplateToRender({ Name: templateToRender.Name, Body: '' }); - formContext.shellApi.removeLgTemplate('common', lgId); - props.onChange(undefined); - } - } - }; - - useEffect(() => { - ensureTemplate(props.formData); - }, [formContext.dialogId]); - - const { Body } = templateToRender; return ( -
- -
- -
-
-
+ + + ); }; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/BotAsks.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/BotAsks.tsx index 8708435738..1d1704fdc7 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/BotAsks.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/BotAsks.tsx @@ -1,28 +1,29 @@ import React from 'react'; -import { FieldProps } from '@bfcomposer/react-jsonschema-form'; import formatMessage from 'format-message'; -import { TextareaWidget } from '../../widgets'; +import { LgEditorWidget } from '../../widgets/LgEditorWidget'; +import { WidgetLabel } from '../../widgets/WidgetLabel'; +import { BFDFieldProps } from '../../types'; import { GetSchema, PromptFieldChangeHandler } from './types'; -interface BotAsksProps extends FieldProps { +interface BotAsksProps extends BFDFieldProps { onChange: PromptFieldChangeHandler; getSchema: GetSchema; } export const BotAsks: React.FC = props => { - const { onChange, getSchema, idSchema, formData, formContext } = props; + const { onChange, getSchema, formData, formContext } = props; return ( <> - + ); diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/Exceptions.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/Exceptions.tsx index 4e8ab19b4c..d7b3f71f94 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/Exceptions.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/Exceptions.tsx @@ -1,8 +1,11 @@ +/** @jsx jsx */ +import { jsx } from '@emotion/core'; import React from 'react'; import { FieldProps } from '@bfcomposer/react-jsonschema-form'; import formatMessage from 'format-message'; -import { TextareaWidget } from '../../widgets'; +import { WidgetLabel } from '../../widgets/WidgetLabel'; +import { LgEditorWidget } from '../../widgets/LgEditorWidget'; import { Validations } from './Validations'; import { field } from './styles'; @@ -14,19 +17,21 @@ interface ExceptionsProps extends FieldProps { } export const Exceptions: React.FC = props => { - const { onChange, getSchema, idSchema, formData, errorSchema } = props; + const { onChange, getSchema, idSchema, formData } = props; return ( - <> +
- +
= props => { formContext={props.formContext} />
- +
- +
- +
); }; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/index.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/index.tsx index 7c479b2b90..c9b6f0c973 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/index.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/index.tsx @@ -1,12 +1,14 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import React from 'react'; -import { FieldProps, IdSchema } from '@bfcomposer/react-jsonschema-form'; +import { IdSchema } from '@bfcomposer/react-jsonschema-form'; import formatMessage from 'format-message'; import { Pivot, PivotLinkSize, PivotItem } from 'office-ui-fabric-react'; import get from 'lodash.get'; +import { PromptTab } from 'shared-menus'; import { BaseField } from '../BaseField'; +import { BFDFieldProps } from '../../types'; import { tabs, tabsContainer, settingsContainer } from './styles'; import { BotAsks } from './BotAsks'; @@ -15,7 +17,9 @@ import { Exceptions } from './Exceptions'; import { PromptSettings } from './PromptSettings'; import { GetSchema, PromptFieldChangeHandler } from './types'; -export const PromptField: React.FC = props => { +export const PromptField: React.FC = props => { + const { formContext } = props; + const { shellApi, focusedTab, focusedSteps } = formContext; const promptSettingsIdSchema = ({ __id: props.idSchema.__id + 'promptSettings' } as unknown) as IdSchema; const getSchema: GetSchema = field => { @@ -28,17 +32,28 @@ export const PromptField: React.FC = props => { props.onChange({ ...props.formData, [field]: data }); }; + const handleTabChange = (item?: PivotItem) => { + if (item) { + shellApi.onFocusSteps(focusedSteps, item.props.itemKey); + } + }; + return (
- - + + - + - + diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/StepsField.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/StepsField.tsx index a1e1b49c79..9e1261cee8 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/StepsField.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/StepsField.tsx @@ -23,7 +23,7 @@ export const StepsField: React.FC = props => { dialogOptionsOpts={{ exclude: [DialogGroup.EVENTS, DialogGroup.ADVANCED_EVENTS, DialogGroup.SELECTOR, DialogGroup.OTHER], }} - navPrefix={props.name} + navPrefix={`${formContext.focusedEvent}.${props.name}`} > {({ createNewItemAtIndex }) => ( void; } -export interface BFDFieldProps extends FieldProps { +export interface BFDFieldProps extends FieldProps { formContext: FormContext; } diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LgEditorWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LgEditorWidget.tsx new file mode 100644 index 0000000000..20f1ccb6fd --- /dev/null +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LgEditorWidget.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { useState, useEffect } from 'react'; +import { RichEditor } from 'code-editor'; + +import { FormContext } from '../types'; + +const LG_HELP = + 'https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/language-generation/docs/lg-file-format.md'; + +const getInitialTemplate = (fieldName: string, formData?: string): string => { + let newTemplate = formData || ''; + + if (newTemplate.indexOf(`bfd${fieldName}-`) !== -1) { + return ''; + } else if (newTemplate && !newTemplate.startsWith('-')) { + newTemplate = `-${newTemplate}`; + } + + return newTemplate; +}; + +interface LgEditorWidgetProps { + formContext: FormContext; + name: string; + value?: string; + height?: number | string; + onChange: (template?: string) => void; +} + +export const LgEditorWidget: React.FC = props => { + const { formContext, name, value, height = 250 } = props; + const [templateToRender, setTemplateToRender] = useState({ Name: '', Body: '' }); + const lgId = `bfd${name}-${formContext.dialogId}`; + const [errorMsg, setErrorMsg] = useState(''); + + const ensureTemplate = async (newBody?: string): Promise => { + const templates = await formContext.shellApi.getLgTemplates('common'); + const template = templates.find(template => { + return template.Name === lgId; + }); + if (template === null || template === undefined) { + const newTemplate = getInitialTemplate(name, newBody); + + if (formContext.dialogId && newTemplate) { + formContext.shellApi.updateLgTemplate('common', lgId, newTemplate); + props.onChange(`[${lgId}]`); + } + setTemplateToRender({ Name: `# ${lgId}`, Body: newTemplate }); + } else { + if (templateToRender.Name === '') { + setTemplateToRender({ Name: `# ${lgId}`, Body: template.Body }); + } + } + }; + + const onChange = (data): void => { + // hit the lg api and replace it's Body with data + if (formContext.dialogId) { + let dataToEmit = data.trim(); + if (dataToEmit.length > 0 && dataToEmit[0] !== '-') { + dataToEmit = `-${dataToEmit}`; + } + + if (dataToEmit.length > 0) { + setTemplateToRender({ Name: templateToRender.Name, Body: data }); + formContext.shellApi + .updateLgTemplate('common', lgId, dataToEmit) + .then(() => setErrorMsg('')) + .catch(error => setErrorMsg(error)); + props.onChange(`[${lgId}]`); + } else { + setTemplateToRender({ Name: templateToRender.Name, Body: '' }); + formContext.shellApi.removeLgTemplate('common', lgId); + props.onChange(undefined); + } + } + }; + + useEffect(() => { + ensureTemplate(value); + }, [formContext.dialogId]); + + const { Body } = templateToRender; + return ; +}; diff --git a/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx b/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx index e9ff60844d..5ec5b30ecd 100644 --- a/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx +++ b/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx @@ -22,6 +22,7 @@ export interface FormEditorProps { focusPath: string; focusedEvent: string; focusedSteps: string[]; + focusedTab?: string; isRoot: boolean; lgFiles: LgFile[]; luFiles: LuFile[]; @@ -109,10 +110,7 @@ export const FormEditor: React.FunctionComponent = props => { schema={dialogSchema} uiSchema={dialogUiSchema} formContext={{ - shellApi: { - ...shellApi, - onFocusSteps: stepIds => shellApi.onFocusSteps(stepIds), - }, + shellApi, dialogOptions, editorSchema: schemas.editor, rootId: props.focusPath, @@ -121,6 +119,9 @@ export const FormEditor: React.FunctionComponent = props => { currentDialog: props.currentDialog, dialogId: get(data, '$designer.id'), isRoot: props.focusPath.endsWith('#'), + focusedEvent: props.focusedEvent, + focusedSteps: props.focusedSteps, + focusedTab: props.focusedTab, }} idPrefix={props.focusPath} > diff --git a/Composer/packages/extensions/obiformeditor/src/types.ts b/Composer/packages/extensions/obiformeditor/src/types.ts index cf4d670813..9ae38c63a2 100644 --- a/Composer/packages/extensions/obiformeditor/src/types.ts +++ b/Composer/packages/extensions/obiformeditor/src/types.ts @@ -70,7 +70,7 @@ export interface ShellApi { getDialogs: () => Promise; saveData: (newData: T, updatePath: string) => Promise; navTo: (path: string) => Promise; - onFocusSteps: (stepIds: string[]) => Promise; + onFocusSteps: (stepIds: string[], focusedTab?: string) => Promise; onFocusEvent: (eventId: string) => Promise; createLuFile: (id: string) => Promise; updateLuFile: (id: string, content: string) => Promise; diff --git a/Composer/packages/extensions/visual-designer/src/components/decorations/icon.tsx b/Composer/packages/extensions/visual-designer/src/components/decorations/icon.tsx index 2bb2a1cf51..4a1939c627 100644 --- a/Composer/packages/extensions/visual-designer/src/components/decorations/icon.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/decorations/icon.tsx @@ -4,53 +4,61 @@ import { Icon as FabricIcon } from 'office-ui-fabric-react/lib/Icon'; import { iconCss } from './iconStyles'; -const FriendSVG = fill => { +interface SVGProps { + fill?: string; + stroke?: string; + size?: number; +} + +const SIZE_RATIO = 30 / 18; + +const FriendSVG: React.FC = props => { return ( - + ); }; -const UserSVG = fill => { +const UserSVG: React.FC = props => { return ( - + ); }; -const MessageBotSVG = fill => { +const MessageBotSVG: React.FC = props => { return ( - + ); }; -const StopSVG = fill => { +const StopSVG: React.FC = props => { return ( - + ); }; -const PlaySVG = stroke => { +const PlaySVG: React.FC = props => { return ( - + ); @@ -64,11 +72,16 @@ const svgByIconName = { Play: PlaySVG, }; -export const Icon = ({ icon, color, size = 18, fill = 'white' }) => - svgByIconName[icon] ? ( - - {svgByIconName[icon](fill)} - - ) : ( - - ); +export const Icon = ({ icon, color, size = 18, fill = 'white' }) => { + const SVGIcon = svgByIconName[icon]; + + if (SVGIcon) { + return ( + + + + ); + } + + return ; +}; diff --git a/Composer/packages/extensions/visual-designer/src/components/decorations/iconStyles.ts b/Composer/packages/extensions/visual-designer/src/components/decorations/iconStyles.ts index 337bf2dee9..dabb73c195 100644 --- a/Composer/packages/extensions/visual-designer/src/components/decorations/iconStyles.ts +++ b/Composer/packages/extensions/visual-designer/src/components/decorations/iconStyles.ts @@ -1,11 +1,10 @@ import { css } from '@emotion/core'; -export const iconCss = (size, color) => { +export const iconCss = (size: number, color?: string) => { return css` - transform: scale(${size / 18})}; - width: 18px; - height: 18px; - border-radius: 37.5px; + width: ${size}px; + height: ${size}px; + border-radius: 50%; background-color: ${color || 'black'}; text-align: center; display: flex; diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/layout-steps/Foreach.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/layout-steps/Foreach.tsx index 66693ef7cd..7a9d950ce1 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/layout-steps/Foreach.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/layout-steps/Foreach.tsx @@ -89,7 +89,7 @@ export const Foreach: FunctionComponent = ({ id, data, onEvent, onRes .filter(x => !!x) .map((x, index) => ( - onEvent(NodeEventTypes.Focus, id)} /> + onEvent(NodeEventTypes.Focus, { id })} /> ))} {edges ? edges.map(x => ) : null} diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/layout-steps/IfCondition.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/layout-steps/IfCondition.tsx index 7db377ea76..9a3bf8516f 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/layout-steps/IfCondition.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/layout-steps/IfCondition.tsx @@ -73,7 +73,7 @@ export const IfCondition: FunctionComponent = ({ id, data, onEvent, o { - onEvent(NodeEventTypes.Focus, id); + onEvent(NodeEventTypes.Focus, { id }); }} /> diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/layout-steps/SwitchCondition.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/layout-steps/SwitchCondition.tsx index 4eee83564d..b3ab13530b 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/layout-steps/SwitchCondition.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/layout-steps/SwitchCondition.tsx @@ -74,7 +74,7 @@ export const SwitchCondition: FunctionComponent = ({ id, data, onEven { - onEvent(NodeEventTypes.Focus, id); + onEvent(NodeEventTypes.Focus, { id }); }} /> diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/ActivityRenderer.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/ActivityRenderer.tsx index 53909dde11..7f28f73401 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/ActivityRenderer.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/ActivityRenderer.tsx @@ -1,49 +1,16 @@ -import React, { useState, useEffect, useContext } from 'react'; +import React from 'react'; import { NodeEventTypes } from '../../../constants/NodeEventTypes'; -import { NodeRendererContext } from '../../../store/NodeRendererContext'; import { getElementColor, ElementIcon } from '../../../utils/obiPropertyResolver'; import { NodeMenu } from '../../menus/NodeMenu'; import { FormCard } from '../templates/FormCard'; import { NodeProps, defaultNodeProps } from '../nodeProps'; import { getFriendlyName } from '../utils'; - -const isAnonymousTemplateReference = activity => { - return activity && activity.indexOf('bfdactivity-') !== -1; -}; +import { useLgTemplate } from '../../../utils/hooks'; export const ActivityRenderer: React.FC = props => { - const { getLgTemplates } = useContext(NodeRendererContext); - const { id, data, onEvent } = props; - const [templateText, setTemplateText] = useState(''); - - const updateTemplateText = async () => { - const isAnonActivity = isAnonymousTemplateReference(data.activity); - - if (isAnonActivity && data.$designer && data.$designer.id) { - // this is an LG template, go get it's content - if (!getLgTemplates || typeof getLgTemplates !== 'function') { - setTemplateText(data.activity); - } - - const templateName = data.activity.slice(1, data.activity.length - 1); - const templates = getLgTemplates ? await getLgTemplates('common', `${templateName}`) : []; - const [template] = templates.filter(template => { - return template.Name === templateName; - }); - if (template && template.Body) { - const [firstLine] = template.Body.split('\n'); - setTemplateText(firstLine.startsWith('-') ? firstLine.substring(1) : firstLine); - } - } else if (!isAnonActivity) { - setTemplateText(data.activity); - } - }; - - useEffect(() => { - updateTemplateText(); - }); + const templateText = useLgTemplate(data.activity, data.$designer && data.$designer.id); const nodeColors = getElementColor(data.$type); @@ -55,7 +22,7 @@ export const ActivityRenderer: React.FC = props => { icon={ElementIcon.MessageBot} label={templateText} onClick={() => { - onEvent(NodeEventTypes.Focus, id); + onEvent(NodeEventTypes.Focus, { id }); }} /> ); diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/BeginDialog.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/BeginDialog.tsx index e6c8480872..66a95569be 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/BeginDialog.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/BeginDialog.tsx @@ -48,7 +48,7 @@ export class BeginDialog extends React.Component { corner={} label={this.renderCallDialogLink()} onClick={() => { - onEvent(NodeEventTypes.Focus, id); + onEvent(NodeEventTypes.Focus, { id }); }} /> ); diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/BotAsks.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/BotAsks.tsx index 0e0adbe4ab..9022486ed2 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/BotAsks.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/BotAsks.tsx @@ -2,7 +2,7 @@ import { jsx } from '@emotion/core'; import { FC } from 'react'; import formatMessage from 'format-message'; -import { DialogGroup } from 'shared-menus'; +import { DialogGroup, PromptTab } from 'shared-menus'; import { NodeEventTypes } from '../../../constants/NodeEventTypes'; import { NodeColors } from '../../../constants/ElementColors'; @@ -10,17 +10,20 @@ import { ElementIcon } from '../../../utils/obiPropertyResolver'; import { NodeMenu } from '../../menus/NodeMenu'; import { FormCard } from '../templates/FormCard'; import { NodeProps } from '../nodeProps'; +import { useLgTemplate } from '../../../utils/hooks'; export const BotAsks: FC = ({ id, data, onEvent, onResize }): JSX.Element => { + const templateText = useLgTemplate(data.prompt, data.$designer && data.$designer.id); + return ( } - label={data.prompt || ''} + label={templateText || ''} onClick={() => { - onEvent(NodeEventTypes.Focus, id); + onEvent(NodeEventTypes.Focus, { id, tab: PromptTab.BOT_ASKS }); }} /> ); diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/ChoiceInput.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/ChoiceInput.tsx index a755a68c0d..a06a7a4911 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/ChoiceInput.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/ChoiceInput.tsx @@ -1,7 +1,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import { FC } from 'react'; -import { DialogGroup } from 'shared-menus'; +import { DialogGroup, PromptTab } from 'shared-menus'; import { ChoiceInputSize, ChoiceInputMarginTop } from '../../../constants/ElementSizes'; import { NodeEventTypes } from '../../../constants/NodeEventTypes'; @@ -75,7 +75,7 @@ export const ChoiceInput: FC = ({ id, data, onEvent }): JSX.Element = icon={ElementIcon.User} label={data.property || ''} onClick={() => { - onEvent(NodeEventTypes.Focus, id); + onEvent(NodeEventTypes.Focus, { id, tab: PromptTab.USER_ANSWERS }); }} styles={{ width: boundary.width, height: boundary.height }} > diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/DefaultRenderer.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/DefaultRenderer.tsx index 466ba531e3..76b4760d64 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/DefaultRenderer.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/DefaultRenderer.tsx @@ -173,7 +173,7 @@ export class DefaultRenderer extends React.Component { icon={icon} label={label} onClick={() => { - onEvent(NodeEventTypes.Focus, id); + onEvent(NodeEventTypes.Focus, { id }); }} /> ); diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/InvalidPromptBrick.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/InvalidPromptBrick.tsx index 5547c2960c..ede48c6089 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/InvalidPromptBrick.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/InvalidPromptBrick.tsx @@ -1,11 +1,12 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import { FC } from 'react'; +import { PromptTab } from 'shared-menus'; import { NodeProps } from '../nodeProps'; import { IconBrick } from '../../decorations/IconBrick'; import { NodeEventTypes } from '../../../constants/NodeEventTypes'; export const InvalidPromptBrick: FC = ({ id, data, onEvent, onResize }): JSX.Element => { - return onEvent(NodeEventTypes.Focus, id)} />; + return onEvent(NodeEventTypes.Focus, { id, tab: PromptTab.EXCEPTIONS })} />; }; diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/Recognizer.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/Recognizer.tsx index 1d57870c86..9feaff53f5 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/Recognizer.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/Recognizer.tsx @@ -20,7 +20,7 @@ export class Recognizer extends React.Component { label={data.$type.split('.')[1]} icon={ElementIcon.Friend} onClick={() => { - onEvent(NodeEventTypes.Focus, id); + onEvent(NodeEventTypes.Focus, { id }); }} /> ); diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/ReplaceDialog.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/ReplaceDialog.tsx index c947e47042..ae4f53d356 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/ReplaceDialog.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/ReplaceDialog.tsx @@ -52,7 +52,7 @@ export class ReplaceDialog extends React.Component { corner={} label={this.renderCallDialogLink()} onClick={() => { - onEvent(NodeEventTypes.Focus, id); + onEvent(NodeEventTypes.Focus, { id }); }} /> ); diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/TextInput.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/TextInput.tsx index eef82777ea..ff4518066f 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/TextInput.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/TextInput.tsx @@ -14,11 +14,13 @@ import { NodeProps } from '../nodeProps'; import { OffsetContainer } from '../../lib/OffsetContainer'; import { Diamond } from '../templates/Diamond'; import { Edge } from '../../lib/EdgeComponents'; +import { useLgTemplate } from '../../../utils/hooks'; export const TextInput: FC = ({ id, data, onEvent }): JSX.Element => { const layout = textInputLayouter(id); const { boundary, nodeMap, edges } = layout; const { initPrompt, propertyBox, unrecognizedPrompt, invalidPrompt, diamond1, diamond2 } = nodeMap; + const templateText = useLgTemplate(data.prompt, data.$designer && data.$designer.id); return (
@@ -28,9 +30,9 @@ export const TextInput: FC = ({ id, data, onEvent }): JSX.Element => header={formatMessage('Text Input')} corner={} icon={ElementIcon.MessageBot} - label={data.prompt || ''} + label={templateText || ''} onClick={() => { - onEvent(NodeEventTypes.Focus, id); + onEvent(NodeEventTypes.Focus, { id }); }} /> @@ -41,7 +43,7 @@ export const TextInput: FC = ({ id, data, onEvent }): JSX.Element => icon={ElementIcon.User} label={data.property || ''} onClick={() => { - onEvent(NodeEventTypes.Focus, id); + onEvent(NodeEventTypes.Focus, { id }); }} /> @@ -52,7 +54,7 @@ export const TextInput: FC = ({ id, data, onEvent }): JSX.Element => icon={ElementIcon.MessageBot} label={data.unrecognizedPrompt || ''} onClick={() => { - onEvent(NodeEventTypes.Focus, id); + onEvent(NodeEventTypes.Focus, { id }); }} /> @@ -63,7 +65,7 @@ export const TextInput: FC = ({ id, data, onEvent }): JSX.Element => icon={ElementIcon.MessageBot} label={data.invalidPrompt || ''} onClick={() => { - onEvent(NodeEventTypes.Focus, id); + onEvent(NodeEventTypes.Focus, { id }); }} /> diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/UserAnswers.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/UserAnswers.tsx index 68f6c305c3..b17c765b2f 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/steps/UserAnswers.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/steps/UserAnswers.tsx @@ -1,7 +1,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import { FC } from 'react'; -import { DialogGroup } from 'shared-menus'; +import { DialogGroup, PromptTab } from 'shared-menus'; import { NodeEventTypes } from '../../../constants/NodeEventTypes'; import { FormCard } from '../templates/FormCard'; @@ -25,7 +25,7 @@ export const UserAnswers: FC = ({ id, data, onEvent, onResize }): JSX header={getUserAnswersTitle(data._type)} label={data.property || ''} onClick={() => { - onEvent(NodeEventTypes.Focus, id); + onEvent(NodeEventTypes.Focus, { id, tab: PromptTab.USER_ANSWERS }); }} /> ); 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 c56e1a1533..62e517bb43 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 @@ -79,7 +79,7 @@ export const FormCard: FunctionComponent = ({
= ({ }} > {icon && icon !== ElementIcon.None && ( -
+
)} diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/RuleCard.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/RuleCard.tsx index f15082ee0e..dd52d1c1c1 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/RuleCard.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/RuleCard.tsx @@ -26,7 +26,7 @@ const getDirectJumpDialog = data => { export const RuleCard = ({ id, data, label, focused, onEvent }): JSX.Element => { const focusNode = () => { - return onEvent(NodeEventTypes.Focus, id); + return onEvent(NodeEventTypes.Focus, { id }); }; const openNode = () => { diff --git a/Composer/packages/extensions/visual-designer/src/editors/AdaptiveDialogEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/AdaptiveDialogEditor.tsx index 706b933336..fed2440bc8 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/AdaptiveDialogEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/AdaptiveDialogEditor.tsx @@ -62,7 +62,7 @@ export const AdaptiveDialogEditor: FC = ({ id, data, onEvent }): JS }} onClick={e => { e.stopPropagation(); - onEvent(NodeEventTypes.Focus, ''); + onEvent(NodeEventTypes.Focus, { id: '' }); }} > {ruleGroup && ( diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index ff2e7e2210..790a6513b8 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -47,13 +47,13 @@ export const ObiEditor: FC = ({ let handler; switch (eventName) { case NodeEventTypes.Focus: - handler = id => { - const newFocusedIds = id ? [id] : []; + handler = (e: { id: string; tab?: string }) => { + const newFocusedIds = e.id ? [e.id] : []; setSelectionContext({ ...selectionContext, selectedIds: [...newFocusedIds], }); - onFocusSteps([...newFocusedIds]); + onFocusSteps([...newFocusedIds], e.tab); }; break; case NodeEventTypes.FocusEvent: @@ -281,7 +281,7 @@ export const ObiEditor: FC = ({ }} onClick={e => { e.stopPropagation(); - dispatchEvent(NodeEventTypes.Focus, ''); + dispatchEvent(NodeEventTypes.Focus, { id: '' }); }} > any; + onFocusSteps: (stepIds: string[], fragment?: string) => any; focusedEvent: string; onFocusEvent: (eventId: string) => any; onOpen: (calleeDialog: string, callerId: string) => any; diff --git a/Composer/packages/extensions/visual-designer/src/editors/RuleEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/RuleEditor.tsx index 90ddced88a..14023a9769 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/RuleEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/RuleEditor.tsx @@ -57,7 +57,7 @@ export const RuleEditor = ({ id, data, onEvent }): JSX.Element => { }} onClick={e => { e.stopPropagation(); - onEvent(NodeEventTypes.Focus, ''); + onEvent(NodeEventTypes.Focus, { id: '' }); }} > {}, onFocusEvent: (eventId: string) => {}, - onFocusSteps: (stepIds: string[]) => {}, + onFocusSteps: (stepIds: string[], fragment?: string) => {}, onSelect: (ids: string[]) => {}, saveData: () => {}, }, diff --git a/Composer/packages/extensions/visual-designer/src/utils/hooks.ts b/Composer/packages/extensions/visual-designer/src/utils/hooks.ts new file mode 100644 index 0000000000..2b150ef819 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/utils/hooks.ts @@ -0,0 +1,64 @@ +import { useContext, useState, useEffect } from 'react'; + +import { NodeRendererContext } from '../store/NodeRendererContext'; + +// matches [bfd-123456] +const TEMPLATE_PATTERN = /^\[(bfd.+-\d{6})\]$/; + +const getTemplateId = (str?: string): string | null => { + if (!str) { + return null; + } + + const match = TEMPLATE_PATTERN.exec(str); + + if (!match || !match[1]) { + return null; + } + + return match[1]; +}; + +export const useLgTemplate = (str?: string, dialogId?: string) => { + const { getLgTemplates } = useContext(NodeRendererContext); + const [templateText, setTemplateText] = useState(''); + let cancelled = false; + + const updateTemplateText = async () => { + const templateId = getTemplateId(str); + + if (templateId && dialogId) { + // this is an LG template, go get it's content + if (!getLgTemplates || typeof getLgTemplates !== 'function') { + setTemplateText(str || ''); + } + + const templates = getLgTemplates ? await getLgTemplates('common', `${templateId}`) : []; + const [template] = templates.filter(template => { + return template.Name === templateId; + }); + + if (cancelled) { + return; + } + + if (template && template.Body) { + const [firstLine] = template.Body.split('\n'); + setTemplateText(firstLine.startsWith('-') ? firstLine.substring(1) : firstLine); + } + } else if (!templateId) { + // fallback to str passed in + setTemplateText(str || ''); + } + }; + + useEffect(() => { + updateTemplateText(); + + return () => { + cancelled = true; + }; + }); + + return templateText; +}; diff --git a/Composer/packages/lib/code-editor/demo/src/App.tsx b/Composer/packages/lib/code-editor/demo/src/App.tsx index d0d7f1e0c4..e2024d1780 100644 --- a/Composer/packages/lib/code-editor/demo/src/App.tsx +++ b/Composer/packages/lib/code-editor/demo/src/App.tsx @@ -7,13 +7,26 @@ const LU_HELP = export default function App() { const [value, setValue] = useState(''); + const [showError, setShowError] = useState(true); const placeholder = `> To learn more about the LU file format, read the documentation at - > ${LU_HELP}`; +> ${LU_HELP}`; + const errorMsg = showError ? 'example error' : undefined; return ( -
- setValue(newVal)} value={value} placeholder={placeholder} /> +
+
+ +
+ setValue(newVal)} + value={value} + placeholder={placeholder} + errorMsg={errorMsg} + helpURL="https://dev.botframework.com" + height={500} + /> +
bottom
); } diff --git a/Composer/packages/lib/code-editor/src/BaseEditor.tsx b/Composer/packages/lib/code-editor/src/BaseEditor.tsx index 14eb915e39..b696f71660 100644 --- a/Composer/packages/lib/code-editor/src/BaseEditor.tsx +++ b/Composer/packages/lib/code-editor/src/BaseEditor.tsx @@ -1,8 +1,9 @@ import React, { useRef, useState, useLayoutEffect, useEffect } from 'react'; +import * as monacoEditor from '@bfcomposer/monaco-editor/esm/vs/editor/editor.api'; import MonacoEditor, { MonacoEditorProps } from '@bfcomposer/react-monaco-editor'; import throttle from 'lodash.throttle'; -const defaultOptions = { +const defaultOptions: monacoEditor.editor.IEditorConstructionOptions = { scrollBeyondLastLine: false, wordWrap: 'off', fontFamily: 'Segoe UI', @@ -12,6 +13,11 @@ const defaultOptions = { minimap: { enabled: false, }, + lineDecorationsWidth: 10, + lineNumbersMinChars: 0, + glyphMargin: false, + folding: false, + renderLineHighlight: 'none', }; export interface BaseEditorProps extends Omit { diff --git a/Composer/packages/lib/code-editor/src/LgEditor.tsx b/Composer/packages/lib/code-editor/src/LgEditor.tsx index 4cb89d5b39..181a6f176f 100644 --- a/Composer/packages/lib/code-editor/src/LgEditor.tsx +++ b/Composer/packages/lib/code-editor/src/LgEditor.tsx @@ -10,6 +10,7 @@ const placeholder = `> To learn more about the LG file format, read the document export function LgEditor(props: RichEditorProps) { const options = { quickSuggestions: true, + ...props.options, }; return ( { + if (height === null || height === undefined) { + return '100%'; + } + + if (typeof height === 'string') { + return height; + } + + return `${height}px`; + }; + return (
- +
- {isInvalid ? ( + {isInvalid && (
{errorHelp}
- ) : ( - )} ); diff --git a/Composer/packages/lib/shared-menus/src/index.ts b/Composer/packages/lib/shared-menus/src/index.ts index 264c0d486e..d54facd80f 100644 --- a/Composer/packages/lib/shared-menus/src/index.ts +++ b/Composer/packages/lib/shared-menus/src/index.ts @@ -7,3 +7,4 @@ formatMessage.setup({ export * from './labelMap'; export * from './appschema'; export * from './dialogFactory'; +export * from './promptTabs'; diff --git a/Composer/packages/lib/shared-menus/src/promptTabs.ts b/Composer/packages/lib/shared-menus/src/promptTabs.ts new file mode 100644 index 0000000000..cc41ad089a --- /dev/null +++ b/Composer/packages/lib/shared-menus/src/promptTabs.ts @@ -0,0 +1,5 @@ +export enum PromptTab { + BOT_ASKS = 'botAsks', + USER_ANSWERS = 'userAnswers', + EXCEPTIONS = 'exceptions', +}