diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 7af231980e..433f6baf11 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -4,7 +4,7 @@
/runtime/ @boydc2014 @luhan2017 @carlosscastro @a-b-r-o-w-n
-/Composer/ @cwhitten @boydc2014 @a-b-r-o-w-n @corinagum @beyackle @srinaath
+/Composer/ @cwhitten @boydc2014 @a-b-r-o-w-n @corinagum @beyackle @srinaath @tonyanziano
/Composer/packages/extensions/visual-designer @yeze322 @cwhitten @boydc2014 @a-b-r-o-w-n
diff --git a/Composer/packages/client/src/components/CreationFlow/LocationBrowser/FileSelector.tsx b/Composer/packages/client/src/components/CreationFlow/LocationBrowser/FileSelector.tsx
index 70147ce330..72999b202f 100644
--- a/Composer/packages/client/src/components/CreationFlow/LocationBrowser/FileSelector.tsx
+++ b/Composer/packages/client/src/components/CreationFlow/LocationBrowser/FileSelector.tsx
@@ -7,6 +7,7 @@ import path from 'path';
import { jsx } from '@emotion/core';
import { useMemo, useState } from 'react';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
+import { Link } from 'office-ui-fabric-react/lib/Link';
import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip';
import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky';
import { ScrollablePane, ScrollbarVisibility } from 'office-ui-fabric-react/lib/ScrollablePane';
@@ -58,13 +59,23 @@ const _renderIcon = (file: File) => {
return
;
};
-const _renderNameColumn = (file: File) => {
+const _renderNameColumn = (onFileChosen: (file: File) => void) => (file: File) => {
const iconName = getFileIconName(file);
return (
-
+ onFileChosen(file)}
+ >
{file.name}
-
+
);
};
@@ -99,7 +110,7 @@ export const FileSelector: React.FC = props => {
sortAscendingAriaLabel: formatMessage('Sorted A to Z'),
sortDescendingAriaLabel: formatMessage('Sorted Z to A'),
data: 'string',
- onRender: _renderNameColumn,
+ onRender: _renderNameColumn(onFileChosen),
isPadded: true,
},
{
diff --git a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx
index cd6233d6c2..9312952bc2 100644
--- a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx
+++ b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx
@@ -16,7 +16,7 @@ import { luIndexer, combineMessage } from '@bfc/indexers';
import { PlaceHolderSectionName } from '@bfc/indexers/lib/utils/luUtil';
import get from 'lodash/get';
import { DialogInfo } from '@bfc/shared';
-import { LuEditor } from '@bfc/code-editor';
+import { LuEditor, inlineModePlaceholder } from '@bfc/code-editor';
import {
generateNewDialog,
@@ -262,20 +262,22 @@ export const TriggerCreationModal: React.FC = props =
data-testid={'RegExDropDown'}
/>
)}
- {showTriggerPhrase && }
{showTriggerPhrase && (
-
+
+
+
+
)}
diff --git a/Composer/packages/client/src/pages/design/PropertyEditor.tsx b/Composer/packages/client/src/pages/design/PropertyEditor.tsx
index 11ec29d717..527064b160 100644
--- a/Composer/packages/client/src/pages/design/PropertyEditor.tsx
+++ b/Composer/packages/client/src/pages/design/PropertyEditor.tsx
@@ -4,7 +4,7 @@
/** @jsx jsx */
import { jsx } from '@emotion/core';
import React, { useEffect, useMemo, useRef, useState } from 'react';
-import AdaptiveForm, { resolveBaseSchema, getUISchema, mergePluginConfigs } from '@bfc/adaptive-form';
+import AdaptiveForm, { resolveBaseSchema, getUIOptions, mergePluginConfigs } from '@bfc/adaptive-form';
import Extension, { FormErrors } from '@bfc/extension';
import formatMessage from 'format-message';
import isEqual from 'lodash/isEqual';
@@ -57,7 +57,7 @@ const PropertyEditor: React.FC = () => {
}, []);
const $uiSchema = useMemo(() => {
- return getUISchema($schema, pluginConfig.formSchema);
+ return getUIOptions($schema, pluginConfig.formSchema, pluginConfig.roleSchema);
}, [$schema, pluginConfig]);
const errors = useMemo(() => {
diff --git a/Composer/packages/client/src/pages/design/index.tsx b/Composer/packages/client/src/pages/design/index.tsx
index 38321a5f3c..bac0231326 100644
--- a/Composer/packages/client/src/pages/design/index.tsx
+++ b/Composer/packages/client/src/pages/design/index.tsx
@@ -13,6 +13,7 @@ import { PromptTab } from '@bfc/shared';
import { DialogFactory, SDKKinds, DialogInfo } from '@bfc/shared';
import { Link } from 'office-ui-fabric-react/lib/Link';
import { JsonEditor } from '@bfc/code-editor';
+import { useTriggerApi } from '@bfc/extension';
import { LoadingSpinner } from '../../components/LoadingSpinner';
import { TestController } from '../../components/TestController';
@@ -28,6 +29,7 @@ import { ToolBar } from '../../components/ToolBar/index';
import { clearBreadcrumb } from '../../utils/navigation';
import undoHistory from '../../store/middlewares/undo/history';
import { navigateTo } from '../../utils';
+import { useShell } from '../../shell';
import { VisualEditorAPI } from './FrameAPI';
import {
@@ -106,6 +108,8 @@ function DesignPage(props) {
const [dialogJsonVisible, setDialogJsonVisibility] = useState(false);
const [currentDialog, setCurrentDialog] = useState(dialogs[0]);
const [exportSkillModalVisible, setExportSkillModalVisible] = useState(false);
+ const shell = useShell('ProjectTree');
+ const triggerApi = useTriggerApi(shell.api);
useEffect(() => {
const currentDialog = dialogs.find(({ id }) => id === dialogId);
@@ -357,7 +361,8 @@ function DesignPage(props) {
}
async function handleDeleteTrigger(id, index) {
- const content = deleteTrigger(dialogs, id, index);
+ const content = deleteTrigger(dialogs, id, index, trigger => triggerApi.deleteTrigger(id, trigger));
+
if (content) {
await updateDialog({ id, projectId, content });
const match = /\[(\d+)\]/g.exec(selected);
diff --git a/Composer/packages/client/src/pages/home/RecentBotList.tsx b/Composer/packages/client/src/pages/home/RecentBotList.tsx
index 2c630d506d..7abc68fac2 100644
--- a/Composer/packages/client/src/pages/home/RecentBotList.tsx
+++ b/Composer/packages/client/src/pages/home/RecentBotList.tsx
@@ -5,6 +5,7 @@
/** @jsx jsx */
import { jsx } from '@emotion/core';
import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip';
+import { Link } from 'office-ui-fabric-react/lib/Link';
import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky';
import { ScrollablePane, ScrollbarVisibility } from 'office-ui-fabric-react/lib/ScrollablePane';
import { IObjectWithKey } from 'office-ui-fabric-react/lib/MarqueeSelection';
@@ -44,13 +45,12 @@ export function RecentBotList(props: RecentBotListProps): JSX.Element {
onRender: item => {
return (
-
onItemChosen(item)}
>
{item.name}
-
+
);
},
diff --git a/Composer/packages/client/src/pages/language-understanding/code-editor.tsx b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx
index cfedafc187..34acf35161 100644
--- a/Composer/packages/client/src/pages/language-understanding/code-editor.tsx
+++ b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx
@@ -158,7 +158,6 @@ const CodeEditor: React.FC = props => {
return (
throttle(fn, 1000, { leading: true, trailing: true });
function createLgApi(state: State, actions: BoundActionHandlers, lgFileResolver: (id: string) => LgFile | undefined) {
- const api = {
- getLgTemplates: id => {
- if (id === undefined) throw new Error('must have a file id');
- const focusedDialogId = state.focusPath.split('#').shift() || id;
- const file = lgFileResolver(focusedDialogId);
- if (!file) throw new Error(`lg file ${id} not found`);
- return file.templates;
- },
-
- updateLgTemplate: async (id: string, templateName: string, templateBody: string) => {
- const file = lgFileResolver(id);
- if (!file) throw new Error(`lg file ${id} not found`);
- if (!templateName) throw new Error(`templateName is missing or empty`);
- const template = { name: templateName, body: templateBody, parameters: [] };
-
- const projectId = state.projectId;
-
- lgUtil.checkSingleLgTemplate(template);
-
- await actions.updateLgTemplate({
- file,
- projectId,
- templateName,
- template,
- });
- },
+ const getLgTemplates = id => {
+ if (id === undefined) throw new Error('must have a file id');
+ const focusedDialogId = state.focusPath.split('#').shift() || id;
+ const file = lgFileResolver(focusedDialogId);
+ if (!file) throw new Error(`lg file ${id} not found`);
+ return file.templates;
+ };
- copyLgTemplate: async (id, fromTemplateName, toTemplateName) => {
- const file = lgFileResolver(id);
- if (!file) throw new Error(`lg file ${id} not found`);
- if (!fromTemplateName || !toTemplateName) throw new Error(`templateName is missing or empty`);
+ const updateLgTemplate = async (id: string, templateName: string, templateBody: string) => {
+ const file = lgFileResolver(id);
+ if (!file) throw new Error(`lg file ${id} not found`);
+ if (!templateName) throw new Error(`templateName is missing or empty`);
+ const template = { name: templateName, body: templateBody, parameters: [] };
- const projectId = state.projectId;
+ const projectId = state.projectId;
- return actions.copyLgTemplate({
- file,
- projectId,
- fromTemplateName,
- toTemplateName,
- });
- },
-
- removeLgTemplate: async (id, templateName) => {
- const file = lgFileResolver(id);
- if (!file) throw new Error(`lg file ${id} not found`);
- if (!templateName) throw new Error(`templateName is missing or empty`);
- const projectId = state.projectId;
-
- return actions.removeLgTemplate({
- file,
- projectId,
- templateName,
- });
- },
-
- removeLgTemplates: async (id, templateNames) => {
- const file = lgFileResolver(id);
- if (!file) throw new Error(`lg file ${id} not found`);
- if (!templateNames) throw new Error(`templateName is missing or empty`);
- const projectId = state.projectId;
-
- return actions.removeLgTemplates({
- file,
- projectId,
- templateNames,
- });
- },
+ lgUtil.checkSingleLgTemplate(template);
+
+ await actions.updateLgTemplate({
+ file,
+ projectId,
+ templateName,
+ template,
+ });
};
- return mapValues(api, fn => createThrottledFunc(fn));
+ const copyLgTemplate = async (id, fromTemplateName, toTemplateName) => {
+ const file = lgFileResolver(id);
+ if (!file) throw new Error(`lg file ${id} not found`);
+ if (!fromTemplateName || !toTemplateName) throw new Error(`templateName is missing or empty`);
+
+ const projectId = state.projectId;
+
+ return actions.copyLgTemplate({
+ file,
+ projectId,
+ fromTemplateName,
+ toTemplateName,
+ });
+ };
+
+ const removeLgTemplate = async (id, templateName) => {
+ const file = lgFileResolver(id);
+ if (!file) throw new Error(`lg file ${id} not found`);
+ if (!templateName) throw new Error(`templateName is missing or empty`);
+ const projectId = state.projectId;
+
+ return actions.removeLgTemplate({
+ file,
+ projectId,
+ templateName,
+ });
+ };
+
+ const removeLgTemplates = async (id, templateNames) => {
+ const file = lgFileResolver(id);
+ if (!file) throw new Error(`lg file ${id} not found`);
+ if (!templateNames) throw new Error(`templateName is missing or empty`);
+ const projectId = state.projectId;
+
+ return actions.removeLgTemplates({
+ file,
+ projectId,
+ templateNames,
+ });
+ };
+
+ return {
+ addLgTemplate: updateLgTemplate,
+ getLgTemplates,
+ updateLgTemplate: createThrottledFunc(updateLgTemplate),
+ removeLgTemplate,
+ removeLgTemplates,
+ copyLgTemplate,
+ };
}
export function useLgApi() {
@@ -97,7 +101,9 @@ export function useLgApi() {
return () => {
Object.keys(newApi).forEach(apiName => {
- newApi[apiName].flush();
+ if (typeof newApi[apiName].flush === 'function') {
+ newApi[apiName].flush();
+ }
});
};
}, [projectId, focusPath]);
diff --git a/Composer/packages/client/src/shell/luApi.ts b/Composer/packages/client/src/shell/luApi.ts
index 286e11958e..72ccb8f48a 100644
--- a/Composer/packages/client/src/shell/luApi.ts
+++ b/Composer/packages/client/src/shell/luApi.ts
@@ -4,61 +4,65 @@
import { useContext, useEffect, useState } from 'react';
import { LuFile, LuIntentSection } from '@bfc/shared';
import throttle from 'lodash/throttle';
-import mapValues from 'lodash/mapValues';
import * as luUtil from '../utils/luUtil';
import { State, BoundActionHandlers } from '../store/types';
import { StoreContext } from '../store';
-const createThrottledFunc = fn => throttle(fn, 0, { leading: true, trailing: true });
+const createThrottledFunc = fn => throttle(fn, 1000, { leading: true, trailing: true });
function createLuApi(state: State, actions: BoundActionHandlers, luFileResolver: (id: string) => LuFile | undefined) {
- const api = {
- addLuIntent: async (id: string, intentName: string, intent: LuIntentSection) => {
- const file = luFileResolver(id);
- if (!file) throw new Error(`lu file ${id} not found`);
- if (!intentName) throw new Error(`intentName is missing or empty`);
-
- const content = luUtil.addIntent(file.content, intent);
- const projectId = state.projectId;
- return await actions.updateLuFile({ id: file.id, projectId, content });
- },
- updateLuIntent: async (id: string, intentName: string, intent: LuIntentSection) => {
- const file = luFileResolver(id);
- if (!file) throw new Error(`lu file ${id} not found`);
- if (!intentName) throw new Error(`intentName is missing or empty`);
-
- const content = luUtil.updateIntent(file.content, intentName, intent);
- const projectId = state.projectId;
- return await actions.updateLuFile({ id: file.id, projectId, content });
- },
-
- removeLuIntent: async (id: string, intentName: string) => {
- const file = luFileResolver(id);
- if (!file) throw new Error(`lu file ${id} not found`);
- if (!intentName) throw new Error(`intentName is missing or empty`);
-
- const content = luUtil.removeIntent(file.content, intentName);
- const projectId = state.projectId;
- return await actions.updateLuFile({ id: file.id, projectId, content });
- },
-
- getLuIntents: (id: string): LuIntentSection[] => {
- if (id === undefined) throw new Error('must have a file id');
- const focusedDialogId = state.focusPath.split('#').shift() || id;
- const file = luFileResolver(focusedDialogId);
- if (!file) throw new Error(`lu file ${id} not found`);
- return file.intents;
- },
-
- getLuIntent: (id: string, intentName: string): LuIntentSection | undefined => {
- const file = luFileResolver(id);
- if (!file) throw new Error(`lu file ${id} not found`);
- return file.intents.find(({ Name }) => Name === intentName);
- },
+ const addLuIntent = async (id: string, intentName: string, intent: LuIntentSection) => {
+ const file = luFileResolver(id);
+ if (!file) throw new Error(`lu file ${id} not found`);
+ if (!intentName) throw new Error(`intentName is missing or empty`);
+
+ const content = luUtil.addIntent(file.content, intent);
+ const projectId = state.projectId;
+ return await actions.updateLuFile({ id: file.id, projectId, content });
+ };
+
+ const updateLuIntent = async (id: string, intentName: string, intent: LuIntentSection) => {
+ const file = luFileResolver(id);
+ if (!file) throw new Error(`lu file ${id} not found`);
+ if (!intentName) throw new Error(`intentName is missing or empty`);
+
+ const content = luUtil.updateIntent(file.content, intentName, intent);
+ const projectId = state.projectId;
+ return await actions.updateLuFile({ id: file.id, projectId, content });
+ };
+
+ const removeLuIntent = async (id: string, intentName: string) => {
+ const file = luFileResolver(id);
+ if (!file) throw new Error(`lu file ${id} not found`);
+ if (!intentName) throw new Error(`intentName is missing or empty`);
+
+ const content = luUtil.removeIntent(file.content, intentName);
+ const projectId = state.projectId;
+ return await actions.updateLuFile({ id: file.id, projectId, content });
+ };
+
+ const getLuIntents = (id: string): LuIntentSection[] => {
+ if (id === undefined) throw new Error('must have a file id');
+ const focusedDialogId = state.focusPath.split('#').shift() || id;
+ const file = luFileResolver(focusedDialogId);
+ if (!file) throw new Error(`lu file ${id} not found`);
+ return file.intents;
};
- return mapValues(api, fn => createThrottledFunc(fn));
+ const getLuIntent = (id: string, intentName: string): LuIntentSection | undefined => {
+ const file = luFileResolver(id);
+ if (!file) throw new Error(`lu file ${id} not found`);
+ return file.intents.find(({ Name }) => Name === intentName);
+ };
+
+ return {
+ addLuIntent,
+ getLuIntents,
+ getLuIntent,
+ updateLuIntent: createThrottledFunc(updateLuIntent),
+ removeLuIntent,
+ };
}
export function useLuApi() {
@@ -73,7 +77,9 @@ export function useLuApi() {
return () => {
Object.keys(newApi).forEach(apiName => {
- newApi[apiName].flush();
+ if (typeof newApi[apiName].flush === 'function') {
+ newApi[apiName].flush();
+ }
});
};
}, [projectId, focusPath]);
diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts
index 67a9fe9538..5ee1f113db 100644
--- a/Composer/packages/client/src/shell/useShell.ts
+++ b/Composer/packages/client/src/shell/useShell.ts
@@ -18,7 +18,7 @@ import { useLuApi } from './luApi';
const FORM_EDITOR = 'PropertyEditor';
-type EventSource = 'VisualEditor' | 'PropertyEditor';
+type EventSource = 'VisualEditor' | 'PropertyEditor' | 'ProjectTree';
export function useShell(source: EventSource): { api: ShellApi; data: ShellData } {
const { state, actions } = useContext(StoreContext);
diff --git a/Composer/packages/client/src/utils/dialogUtil.ts b/Composer/packages/client/src/utils/dialogUtil.ts
index 5efa9ce647..a56597f9bf 100644
--- a/Composer/packages/client/src/utils/dialogUtil.ts
+++ b/Composer/packages/client/src/utils/dialogUtil.ts
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-import { ConceptLabels, DialogGroup, SDKKinds, dialogGroups, DialogInfo, DialogFactory } from '@bfc/shared';
+import { ConceptLabels, DialogGroup, SDKKinds, dialogGroups, DialogInfo, DialogFactory, ITrigger } from '@bfc/shared';
import get from 'lodash/get';
import set from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep';
@@ -128,7 +128,12 @@ export function createSelectedPath(selected: number) {
return `triggers[${selected}]`;
}
-export function deleteTrigger(dialogs: DialogInfo[], dialogId: string, index: number) {
+export function deleteTrigger(
+ dialogs: DialogInfo[],
+ dialogId: string,
+ index: number,
+ callbackOnDeletedTrigger?: (trigger: ITrigger) => any
+) {
let dialogCopy = getDialog(dialogs, dialogId);
if (!dialogCopy) return null;
const isRegEx = get(dialogCopy, 'content.recognizer.$kind', '') === regexRecognizerKey;
@@ -137,7 +142,8 @@ export function deleteTrigger(dialogs: DialogInfo[], dialogId: string, index: nu
dialogCopy = deleteRegExIntent(dialogCopy, regExIntent);
}
const triggers = get(dialogCopy, 'content.triggers');
- triggers.splice(index, 1);
+ const removedTriggers = triggers.splice(index, 1);
+ callbackOnDeletedTrigger && callbackOnDeletedTrigger(removedTriggers[0]);
return dialogCopy.content;
}
diff --git a/Composer/packages/electron-server/electron-builder-config.json b/Composer/packages/electron-server/electron-builder-config.json
index 2183ff21cf..85ccb49db1 100644
--- a/Composer/packages/electron-server/electron-builder-config.json
+++ b/Composer/packages/electron-server/electron-builder-config.json
@@ -32,6 +32,7 @@
{
"target": "nsis",
"arch": [
+ "ia32",
"x64"
]
}
@@ -40,15 +41,18 @@
"artifactName": "BotFramework-Composer-${version}-windows-setup.${ext}"
},
"nsis": {
+ "include": "resources/installer.nsh",
"perMachine": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"packElevateHelper": true,
"unicode": true,
"runAfterFinish": true,
+ "installerIcon": "resources/composerIcon.ico",
+ "uninstallerIcon": "resources/composerIcon.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
- "shortcutName": "Bot Framework Composer (preview)",
+ "shortcutName": "Bot Framework Composer",
"oneClick": false
},
"linux": {
diff --git a/Composer/packages/electron-server/resources/installer.nsh b/Composer/packages/electron-server/resources/installer.nsh
new file mode 100644
index 0000000000..453668e176
--- /dev/null
+++ b/Composer/packages/electron-server/resources/installer.nsh
@@ -0,0 +1,15 @@
+!macro customInstall
+ DetailPrint "Register bfcomposer URI Handler"
+ DeleteRegKey HKCU "SOFTWARE\Classes\bfcomposer"
+ WriteRegStr HKCU "SOFTWARE\Classes\bfcomposer" "" "URL:Bot Framework Composer"
+ WriteRegStr HKCU "SOFTWARE\Classes\bfcomposer" "URL Protocol" ""
+ WriteRegStr HKCU "SOFTWARE\Classes\bfcomposer\DefaultIcon" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME},1"
+ WriteRegStr HKCU "SOFTWARE\Classes\bfcomposer\shell" "" ""
+ WriteRegStr HKCU "SOFTWARE\Classes\bfcomposer\shell\open" "" ""
+ WriteRegStr HKCU "SOFTWARE\Classes\bfcomposer\shell\open\command" "" `"$INSTDIR\${APP_EXECUTABLE_FILENAME}" "%1"`
+!macroend
+
+!macro customUninstall
+ DetailPrint "Unregister bfcomposer URI Handler"
+ DeleteRegKey HKCU "SOFTWARE\Classes\bfcomposer"
+!macroend
diff --git a/Composer/packages/electron-server/src/main.ts b/Composer/packages/electron-server/src/main.ts
index 7ac5e299dc..7c1dc07195 100644
--- a/Composer/packages/electron-server/src/main.ts
+++ b/Composer/packages/electron-server/src/main.ts
@@ -29,7 +29,7 @@ const getBaseUrl = () => {
if (!serverPort) {
throw new Error('getBaseUrl() called before serverPort is defined.');
}
- return `http://localhost:${serverPort}`;
+ return `http://localhost:${serverPort}/`;
};
function processArgsForWindows(args: string[]): string {
diff --git a/Composer/packages/extensions/adaptive-form/package.json b/Composer/packages/extensions/adaptive-form/package.json
index b4c2b4d3eb..7756c1366d 100644
--- a/Composer/packages/extensions/adaptive-form/package.json
+++ b/Composer/packages/extensions/adaptive-form/package.json
@@ -42,4 +42,4 @@
"lodash": "^4.17.15",
"react-error-boundary": "^1.2.5"
}
-}
+}
\ No newline at end of file
diff --git a/Composer/packages/extensions/adaptive-form/src/components/ErrorMessage.tsx b/Composer/packages/extensions/adaptive-form/src/components/ErrorMessage.tsx
index f4e2199085..1fe6352d54 100644
--- a/Composer/packages/extensions/adaptive-form/src/components/ErrorMessage.tsx
+++ b/Composer/packages/extensions/adaptive-form/src/components/ErrorMessage.tsx
@@ -3,25 +3,30 @@
import React from 'react';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
+import { Link } from 'office-ui-fabric-react/lib/Link';
import formatMessage from 'format-message';
interface ErrorMessageProps {
label?: string | false;
error?: string;
+ helpLink?: string;
}
const ErrorMessage: React.FC = props => {
- const { error, label } = props;
+ const { error, label, helpLink } = props;
return (
{[label, error].filter(Boolean).join(' ')}
+ {helpLink && (
+
+ {formatMessage('Refer to the syntax documentation here.')}
+
+ )}
);
};
diff --git a/Composer/packages/extensions/adaptive-form/src/components/SchemaField.tsx b/Composer/packages/extensions/adaptive-form/src/components/SchemaField.tsx
index 6cfdccf156..23de6681cd 100644
--- a/Composer/packages/extensions/adaptive-form/src/components/SchemaField.tsx
+++ b/Composer/packages/extensions/adaptive-form/src/components/SchemaField.tsx
@@ -3,9 +3,9 @@
/** @jsx jsx */
import { jsx, css } from '@emotion/core';
import React, { useEffect } from 'react';
-import { FieldProps } from '@bfc/extension';
+import { FieldProps, UIOptions } from '@bfc/extension';
-import { getUISchema, resolveFieldWidget, resolveRef, getUiLabel, getUiPlaceholder, getUiDescription } from '../utils';
+import { getUIOptions, resolveFieldWidget, resolveRef, getUiLabel, getUiPlaceholder, getUiDescription } from '../utils';
import { usePluginConfig } from '../hooks';
import { ErrorMessage } from './ErrorMessage';
@@ -34,9 +34,10 @@ const SchemaField: React.FC = props => {
...rest
} = props;
const pluginConfig = usePluginConfig();
+
const schema = resolveRef(baseSchema, definitions);
- const uiOptions = {
- ...getUISchema(schema, pluginConfig.formSchema),
+ const uiOptions: UIOptions = {
+ ...getUIOptions(schema, pluginConfig.formSchema, pluginConfig.roleSchema),
...baseUIOptions,
};
@@ -50,7 +51,9 @@ const SchemaField: React.FC = props => {
}
}, []);
- const error = typeof rawErrors === 'string' && ;
+ const error = typeof rawErrors === 'string' && (
+
+ );
if (!schema || name.startsWith('$')) {
return null;
diff --git a/Composer/packages/extensions/adaptive-form/src/utils/__tests__/getUIOptions.test.ts b/Composer/packages/extensions/adaptive-form/src/utils/__tests__/getUIOptions.test.ts
new file mode 100644
index 0000000000..c924422e4b
--- /dev/null
+++ b/Composer/packages/extensions/adaptive-form/src/utils/__tests__/getUIOptions.test.ts
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+import { UISchema, JSONSchema7, RoleSchema } from '@bfc/extension';
+import { SDKKinds } from '@bfc/shared';
+
+import { getUIOptions } from '../getUIOptions';
+
+describe('getUIOptions', () => {
+ it('returns empty object when type schema not found', () => {
+ // @ts-ignore - Intentionally passing in an invalid value
+ expect(getUIOptions('SomeDialog')).toEqual({});
+ });
+
+ it('returns UI schema for $kind', () => {
+ const schema: JSONSchema7 = {
+ properties: {
+ $kind: {
+ const: SDKKinds.AdaptiveDialog,
+ },
+ },
+ };
+ expect(getUIOptions(schema)).toMatchInlineSnapshot(`Object {}`);
+ });
+
+ it('merges overrides into default schema', () => {
+ const schema: JSONSchema7 = {
+ properties: {
+ $kind: {
+ const: SDKKinds.AdaptiveDialog,
+ },
+ },
+ };
+
+ const uiSchema: UISchema = {
+ [SDKKinds.AdaptiveDialog]: {
+ order: ['*', 'recognizer'],
+ label: 'First Label',
+ },
+ };
+ expect(getUIOptions(schema, uiSchema)).toMatchInlineSnapshot(`
+Object {
+ "label": "First Label",
+ "order": Array [
+ "*",
+ "recognizer",
+ ],
+}
+`);
+ });
+
+ it('merges overrides and a plugin into default schema', () => {
+ const schema: JSONSchema7 = {
+ $role: 'expression',
+ properties: {
+ $kind: {
+ const: SDKKinds.AdaptiveDialog,
+ },
+ },
+ };
+
+ const uiSchema: UISchema = {
+ [SDKKinds.AdaptiveDialog]: {
+ order: ['*', 'recognizer'],
+ label: 'First Label',
+ },
+ };
+
+ const plugin: RoleSchema = {
+ expression: {
+ helpLink: 'https://example.com/plugin',
+ },
+ };
+
+ expect(getUIOptions(schema, uiSchema, plugin)).toMatchInlineSnapshot(`
+Object {
+ "helpLink": "https://example.com/plugin",
+ "label": "First Label",
+ "order": Array [
+ "*",
+ "recognizer",
+ ],
+}
+`);
+ });
+});
diff --git a/Composer/packages/extensions/adaptive-form/src/utils/__tests__/getUISchema.test.ts b/Composer/packages/extensions/adaptive-form/src/utils/__tests__/getUISchema.test.ts
deleted file mode 100644
index 6e4be3ade8..0000000000
--- a/Composer/packages/extensions/adaptive-form/src/utils/__tests__/getUISchema.test.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-import { UISchema, JSONSchema7 } from '@bfc/extension';
-import { SDKKinds } from '@bfc/shared';
-
-import { getUISchema } from '../getUISchema';
-
-describe('getUISchema', () => {
- it('returns empty object when type schema not found', () => {
- // @ts-ignore - Intentionally passing in an invalid value
- expect(getUISchema('SomeDialog')).toEqual({});
- });
-
- it('returns UI schema for $kind', () => {
- const schema: JSONSchema7 = {
- properties: {
- $kind: {
- const: SDKKinds.AdaptiveDialog,
- },
- },
- };
- expect(getUISchema(schema)).toMatchInlineSnapshot(`Object {}`);
- });
-
- it('merges overrides into default schema', () => {
- const schema: JSONSchema7 = {
- properties: {
- $kind: {
- const: SDKKinds.AdaptiveDialog,
- },
- },
- };
-
- const uiSchema: UISchema = {
- [SDKKinds.AdaptiveDialog]: {
- order: ['*', 'recognizer'],
- label: 'First Label',
- },
- };
- expect(getUISchema(schema, uiSchema)).toMatchInlineSnapshot(`
-Object {
- "label": "First Label",
- "order": Array [
- "*",
- "recognizer",
- ],
-}
-`);
- });
-});
diff --git a/Composer/packages/extensions/adaptive-form/src/utils/getUIOptions.ts b/Composer/packages/extensions/adaptive-form/src/utils/getUIOptions.ts
new file mode 100644
index 0000000000..80796996d3
--- /dev/null
+++ b/Composer/packages/extensions/adaptive-form/src/utils/getUIOptions.ts
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+import { UISchema, UIOptions, JSONSchema7, RoleSchema } from '@bfc/extension';
+/**
+ * Merges overrides and plugins into default ui schema and returns the UIOptions
+ */
+export function getUIOptions(schema?: JSONSchema7, uiSchema?: UISchema, roleSchema?: RoleSchema): UIOptions {
+ const { $kind } = schema?.properties || {};
+ const { $role } = schema || {};
+
+ const kind = $kind && typeof $kind === 'object' && ($kind.const as string);
+
+ const formOptions = uiSchema && kind && uiSchema[kind] ? uiSchema[kind] : {};
+ const roleOptions = roleSchema && $role && roleSchema[$role] ? roleSchema[$role] : {};
+
+ return { ...roleOptions, ...formOptions };
+}
diff --git a/Composer/packages/extensions/adaptive-form/src/utils/getUISchema.ts b/Composer/packages/extensions/adaptive-form/src/utils/getUISchema.ts
deleted file mode 100644
index b057f9ddf2..0000000000
--- a/Composer/packages/extensions/adaptive-form/src/utils/getUISchema.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-import { UISchema, UIOptions, JSONSchema7 } from '@bfc/extension';
-/**
- * Merges overrides into default ui schema and returns the UIOptions
- */
-export function getUISchema(schema?: JSONSchema7, uiSchema?: UISchema): UIOptions {
- const { $kind } = schema?.properties || {};
-
- const kind = $kind && typeof $kind === 'object' && ($kind.const as string);
-
- return uiSchema && kind && uiSchema[kind] ? uiSchema[kind] : {};
-}
diff --git a/Composer/packages/extensions/adaptive-form/src/utils/index.ts b/Composer/packages/extensions/adaptive-form/src/utils/index.ts
index d2bad4fa73..5bbc33ae84 100644
--- a/Composer/packages/extensions/adaptive-form/src/utils/index.ts
+++ b/Composer/packages/extensions/adaptive-form/src/utils/index.ts
@@ -3,7 +3,7 @@
export * from './arrayUtils';
export * from './getAdaptiveType';
export * from './getOrderedProperties';
-export * from './getUISchema';
+export * from './getUIOptions';
export * from './getValueType';
export * from './mergePluginConfigs';
export * from './resolveBaseSchema';
diff --git a/Composer/packages/extensions/adaptive-form/src/utils/resolveRef.ts b/Composer/packages/extensions/adaptive-form/src/utils/resolveRef.ts
index cf08679fcf..46c9f13723 100644
--- a/Composer/packages/extensions/adaptive-form/src/utils/resolveRef.ts
+++ b/Composer/packages/extensions/adaptive-form/src/utils/resolveRef.ts
@@ -8,7 +8,7 @@ export function resolveRef(
): JSONSchema7 {
if (typeof schema?.$ref === 'string') {
const defName = schema.$ref.replace('#/definitions/', '');
- const defSchema = typeof definitions?.[defName] === 'object' ? definitions?.[defName] : {};
+ const defSchema = typeof definitions?.[defName] === 'object' ? (definitions?.[defName] as JSONSchema7) : {};
const resolvedSchema = {
...defSchema,
diff --git a/Composer/packages/extensions/extension/src/hooks/index.ts b/Composer/packages/extensions/extension/src/hooks/index.ts
index 22e225d8d6..7bc26ac152 100644
--- a/Composer/packages/extensions/extension/src/hooks/index.ts
+++ b/Composer/packages/extensions/extension/src/hooks/index.ts
@@ -5,5 +5,6 @@ export * from './useShellApi';
export * from './useLgApi';
export * from './useLuApi';
export * from './useActionApi';
+export * from './useTriggerApi';
export * from './useDialogApi';
export * from './useDialogEditApi';
diff --git a/Composer/packages/extensions/extension/src/hooks/useActionApi.ts b/Composer/packages/extensions/extension/src/hooks/useActionApi.ts
index c12832b239..5cc8012697 100644
--- a/Composer/packages/extensions/extension/src/hooks/useActionApi.ts
+++ b/Composer/packages/extensions/extension/src/hooks/useActionApi.ts
@@ -8,14 +8,15 @@ import {
deleteActions as destructActions,
FieldProcessorAsync,
walkAdaptiveActionList,
+ ShellApi,
} from '@bfc/shared';
import { useLgApi } from './useLgApi';
import { useLuApi } from './useLuApi';
-export const useActionApi = () => {
- const { createLgTemplate, readLgTemplate, deleteLgTemplates } = useLgApi();
- const { createLuIntent, readLuIntent, deleteLuIntents } = useLuApi();
+export const useActionApi = (shellApi: ShellApi) => {
+ const { createLgTemplate, readLgTemplate, deleteLgTemplates } = useLgApi(shellApi);
+ const { createLuIntent, readLuIntent, deleteLuIntents } = useLuApi(shellApi);
const luFieldName = '_lu';
diff --git a/Composer/packages/extensions/extension/src/hooks/useDialogApi.ts b/Composer/packages/extensions/extension/src/hooks/useDialogApi.ts
index b637de8eff..302d52eb85 100644
--- a/Composer/packages/extensions/extension/src/hooks/useDialogApi.ts
+++ b/Composer/packages/extensions/extension/src/hooks/useDialogApi.ts
@@ -1,12 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-import { useShellApi } from './useShellApi';
+import { ShellApi } from '@bfc/shared';
-export const useDialogApi = () => {
- const {
- shellApi: { getDialog, saveDialog, createDialog },
- } = useShellApi();
+export const useDialogApi = (shellApi: ShellApi) => {
+ const { getDialog, saveDialog, createDialog } = shellApi;
return {
createDialog: () => createDialog([]),
diff --git a/Composer/packages/extensions/extension/src/hooks/useDialogEditApi.ts b/Composer/packages/extensions/extension/src/hooks/useDialogEditApi.ts
index 64f5fa7903..6fa4de130e 100644
--- a/Composer/packages/extensions/extension/src/hooks/useDialogEditApi.ts
+++ b/Composer/packages/extensions/extension/src/hooks/useDialogEditApi.ts
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-import { BaseSchema, DialogUtils } from '@bfc/shared';
+import { BaseSchema, DialogUtils, ShellApi } from '@bfc/shared';
import { useActionApi } from './useActionApi';
@@ -14,8 +14,8 @@ export interface DialogApiContext {
const { appendNodesAfter, queryNodes, insertNodes, deleteNode, deleteNodes } = DialogUtils;
-export function useDialogEditApi() {
- const { constructActions, copyActions, deleteAction, deleteActions } = useActionApi();
+export function useDialogEditApi(shellApi: ShellApi) {
+ const { constructActions, copyActions, deleteAction, deleteActions } = useActionApi(shellApi);
async function insertActions(
dialogId: string,
diff --git a/Composer/packages/extensions/extension/src/hooks/useLgApi.ts b/Composer/packages/extensions/extension/src/hooks/useLgApi.ts
index 1035d18161..d4233d3cd4 100644
--- a/Composer/packages/extensions/extension/src/hooks/useLgApi.ts
+++ b/Composer/packages/extensions/extension/src/hooks/useLgApi.ts
@@ -1,16 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-import { LgTemplateRef, LgMetaData, BaseSchema, LgType } from '@bfc/shared';
-
-import { useShellApi } from './useShellApi';
+import { LgTemplateRef, LgMetaData, BaseSchema, LgType, ShellApi } from '@bfc/shared';
/**
* LG CRUD lib
*/
-export const useLgApi = () => {
- const { shellApi } = useShellApi();
- const { removeLgTemplates, getLgTemplates, updateLgTemplate } = shellApi;
+export const useLgApi = (shellApi: ShellApi) => {
+ const { removeLgTemplates, getLgTemplates, addLgTemplate } = shellApi;
const deleteLgTemplates = (lgFileId: string, lgTemplates: string[]) => {
const normalizedLgTemplates = lgTemplates
@@ -46,7 +43,7 @@ export const useLgApi = () => {
const newLgType = new LgType(hostActionData.$kind, hostFieldName).toString();
const newLgTemplateName = new LgMetaData(newLgType, hostActionId).toString();
const newLgTemplateRefStr = new LgTemplateRef(newLgTemplateName).toString();
- await updateLgTemplate(lgFileId, newLgTemplateName, lgText);
+ await addLgTemplate(lgFileId, newLgTemplateName, lgText);
return newLgTemplateRefStr;
};
diff --git a/Composer/packages/extensions/extension/src/hooks/useLuApi.ts b/Composer/packages/extensions/extension/src/hooks/useLuApi.ts
index f809d5fca6..31580f855b 100644
--- a/Composer/packages/extensions/extension/src/hooks/useLuApi.ts
+++ b/Composer/packages/extensions/extension/src/hooks/useLuApi.ts
@@ -1,15 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-import { LuIntentSection, BaseSchema, LuMetaData, LuType } from '@bfc/shared';
-
-import { useShellApi } from './useShellApi';
+import { LuIntentSection, BaseSchema, LuMetaData, LuType, ShellApi } from '@bfc/shared';
/**
* LU CRUD API
*/
-export const useLuApi = () => {
- const { shellApi } = useShellApi();
+export const useLuApi = (shellApi: ShellApi) => {
const { addLuIntent, removeLuIntent, getLuIntent } = shellApi;
const createLuIntent = async (
diff --git a/Composer/packages/extensions/extension/src/hooks/useTriggerApi.ts b/Composer/packages/extensions/extension/src/hooks/useTriggerApi.ts
new file mode 100644
index 0000000000..56461064fa
--- /dev/null
+++ b/Composer/packages/extensions/extension/src/hooks/useTriggerApi.ts
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { ShellApi, SDKKinds } from '@bfc/shared';
+import get from 'lodash/get';
+
+import { useActionApi } from './useActionApi';
+import { useLuApi } from './useLuApi';
+
+export const useTriggerApi = (shellAPi: ShellApi) => {
+ const { deleteActions } = useActionApi(shellAPi);
+ const { deleteLuIntent } = useLuApi(shellAPi);
+
+ const deleteTrigger = (dialogId: string, trigger) => {
+ // Clean the lu resource on intent trigger
+ if (trigger.$kind === SDKKinds.OnIntent) {
+ const triggerIntent = get(trigger, 'intent', '');
+ deleteLuIntent(dialogId, triggerIntent);
+ }
+
+ // Clean action resources
+ const actions = get(trigger, 'actions');
+ if (!actions || !Array.isArray(actions)) return;
+
+ deleteActions(dialogId, actions);
+ };
+
+ return {
+ deleteTrigger,
+ };
+};
diff --git a/Composer/packages/extensions/visual-designer/src/components/menus/EdgeMenu.tsx b/Composer/packages/extensions/visual-designer/src/components/menus/EdgeMenu.tsx
index 7065463c66..b89118b961 100644
--- a/Composer/packages/extensions/visual-designer/src/components/menus/EdgeMenu.tsx
+++ b/Composer/packages/extensions/visual-designer/src/components/menus/EdgeMenu.tsx
@@ -2,13 +2,11 @@
// Licensed under the MIT License.
/** @jsx jsx */
-import { jsx, css } from '@emotion/core';
+import { jsx } from '@emotion/core';
import { useCallback, useContext, useState } from 'react';
import classnames from 'classnames';
import formatMessage from 'format-message';
-import { createStepMenu, DialogGroup, SDKKinds } from '@bfc/shared';
-import { IContextualMenu, ContextualMenuItemType } from 'office-ui-fabric-react/lib/ContextualMenu';
-import { FontIcon } from 'office-ui-fabric-react/lib/Icon';
+import { DefinitionSummary } from '@bfc/shared';
import { EdgeAddButtonSize } from '../../constants/ElementSizes';
import { NodeRendererContext } from '../../store/NodeRendererContext';
@@ -19,6 +17,7 @@ import { MenuTypes } from '../../constants/MenuTypes';
import { ObiColors } from '../../constants/ElementColors';
import { IconMenu } from './IconMenu';
+import { createActionMenu } from './createSchemaMenu';
interface EdgeMenuProps {
id: string;
@@ -26,85 +25,8 @@ interface EdgeMenuProps {
addCoachMarkRef?: (ref: { [key: string]: HTMLDivElement }) => void;
}
-const buildEdgeMenuItemsFromClipboardContext = (
- context,
- onClick,
- filter?: (t: SDKKinds) => boolean
-): IContextualMenu[] => {
- const { clipboardActions } = context;
- const menuItems = createStepMenu(
- [
- DialogGroup.RESPONSE,
- DialogGroup.INPUT,
- DialogGroup.BRANCHING,
- DialogGroup.LOOPING,
- DialogGroup.STEP,
- DialogGroup.MEMORY,
- DialogGroup.CODE,
- DialogGroup.LOG,
- ],
- true,
- (e, item) => onClick(item ? item.data.$kind : null),
- context.dialogFactory,
- filter
- );
-
- const enablePaste = Array.isArray(clipboardActions) && clipboardActions.length > 0;
- const menuItemCount = menuItems.length;
- menuItems.unshift(
- {
- key: 'Paste',
- name: 'Paste',
- ariaLabel: 'Paste',
- disabled: !enablePaste,
- onRender: () => {
- return (
-
- );
- },
- },
- {
- key: 'divider',
- itemType: ContextualMenuItemType.Divider,
- }
- );
-
- return menuItems;
-};
-
export const EdgeMenu: React.FC = ({ id, addCoachMarkRef, onClick, ...rest }) => {
- const nodeContext = useContext(NodeRendererContext);
+ const { clipboardActions, customSchemas } = useContext(NodeRendererContext);
const selfHosted = useContext(SelfHostContext);
const { selectedIds } = useContext(SelectionContext);
const nodeSelected = selectedIds.includes(`${id}${MenuTypes.EdgeMenu}`);
@@ -124,6 +46,19 @@ export const EdgeMenu: React.FC = ({ id, addCoachMarkRef, onClick
const handleMenuShow = menuSelected => {
setMenuSelected(menuSelected);
};
+
+ const menuItems = createActionMenu(
+ item => {
+ if (!item) return;
+ onClick(item.key);
+ },
+ {
+ isSelfHosted: selfHosted,
+ enablePaste: Array.isArray(clipboardActions) && !!clipboardActions.length,
+ },
+ // Custom Action 'oneOf' arrays from schema file
+ customSchemas.map(x => x.oneOf).filter(oneOf => Array.isArray(oneOf) && oneOf.length) as DefinitionSummary[][]
+ );
return (
= ({ id, addCoachMarkRef, onClick
}}
iconSize={7}
nodeSelected={nodeSelected}
- menuItems={buildEdgeMenuItemsFromClipboardContext(
- nodeContext,
- onClick,
- selfHosted ? x => x !== SDKKinds.LogAction : undefined
- )}
+ menuItems={menuItems}
label={formatMessage('Add')}
handleMenuShow={handleMenuShow}
{...rest}
diff --git a/Composer/packages/extensions/visual-designer/src/components/menus/EventMenu.tsx b/Composer/packages/extensions/visual-designer/src/components/menus/EventMenu.tsx
deleted file mode 100644
index 7067aebeb2..0000000000
--- a/Composer/packages/extensions/visual-designer/src/components/menus/EventMenu.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-import React, { useContext } from 'react';
-import { createStepMenu, DialogGroup } from '@bfc/shared';
-
-import { NodeRendererContext } from '../../store/NodeRendererContext';
-
-import { IconMenu } from './IconMenu';
-
-interface EventMenuProps {
- label?: string;
- onClick: (item: string | null) => void;
-}
-
-export const EventMenu: React.FC
= ({ label, onClick, ...rest }): JSX.Element => {
- const { dialogFactory } = useContext(NodeRendererContext);
- const eventMenuItems = createStepMenu(
- [DialogGroup.EVENTS],
- false,
- (e, item): any => onClick(item ? item.data.$kind : null),
- dialogFactory
- );
-
- return (
-
- );
-};
diff --git a/Composer/packages/extensions/visual-designer/src/components/menus/createSchemaMenu.tsx b/Composer/packages/extensions/visual-designer/src/components/menus/createSchemaMenu.tsx
new file mode 100644
index 0000000000..6fc67fc17b
--- /dev/null
+++ b/Composer/packages/extensions/visual-designer/src/components/menus/createSchemaMenu.tsx
@@ -0,0 +1,197 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+/** @jsx jsx */
+import { jsx, css } from '@emotion/core';
+import {
+ IContextualMenuItem,
+ ContextualMenuItemType,
+} from 'office-ui-fabric-react/lib/components/ContextualMenu/ContextualMenu.types';
+import { ConceptLabels, DialogGroup, dialogGroups, SDKKinds, DefinitionSummary } from '@bfc/shared';
+import { FontIcon } from 'office-ui-fabric-react/lib/Icon';
+import formatMessage from 'format-message';
+
+const resolveMenuTitle = ($kind: SDKKinds): string => {
+ const conceptLabel = ConceptLabels[$kind];
+ return conceptLabel?.title || $kind;
+};
+
+type ActionMenuItemClickHandler = (item?: IContextualMenuItem) => any;
+type ActionKindFilter = ($kind: SDKKinds) => boolean;
+
+const createBaseActionMenu = (
+ onClick: ActionMenuItemClickHandler,
+ filter?: ActionKindFilter
+): IContextualMenuItem[] => {
+ const pickedGroups: DialogGroup[] = [
+ DialogGroup.RESPONSE,
+ DialogGroup.INPUT,
+ DialogGroup.BRANCHING,
+ DialogGroup.LOOPING,
+ DialogGroup.STEP,
+ DialogGroup.MEMORY,
+ DialogGroup.CODE,
+ DialogGroup.LOG,
+ ];
+ const stepMenuItems = pickedGroups
+ .map(key => dialogGroups[key])
+ .filter(groupItem => groupItem && Array.isArray(groupItem.types) && groupItem.types.length)
+ .map(({ label, types: actionKinds }) => {
+ const subMenuItems: IContextualMenuItem[] = actionKinds
+ .filter($kind => (filter ? filter($kind) : true))
+ .map($kind => ({
+ key: $kind,
+ name: resolveMenuTitle($kind),
+ onClick: (e, itemData) => onClick(itemData),
+ }));
+
+ if (subMenuItems.length === 1) {
+ // hoists the only item to upper level
+ return subMenuItems[0];
+ }
+ return createSubMenu(label, onClick, subMenuItems);
+ });
+ return stepMenuItems;
+};
+
+const createDivider = () => ({
+ key: 'divider',
+ itemType: ContextualMenuItemType.Divider,
+});
+
+const get$kindFrom$ref = ($ref: string): SDKKinds => {
+ return $ref.replace('#/definitions/', '') as SDKKinds;
+};
+
+const createCustomActionSubMenu = (
+ customizedActionGroups: DefinitionSummary[][],
+ onClick: ActionMenuItemClickHandler
+): IContextualMenuItem[] => {
+ if (!Array.isArray(customizedActionGroups) || customizedActionGroups.length === 0) {
+ return [];
+ }
+
+ const itemGroups: IContextualMenuItem[][] = customizedActionGroups
+ .filter(actionGroup => Array.isArray(actionGroup) && actionGroup.length)
+ .map(actionGroup => {
+ return actionGroup.map(
+ ({ title, $ref }) =>
+ ({
+ key: get$kindFrom$ref($ref),
+ name: title,
+ onClick: (e, itemData) => onClick(itemData),
+ } as IContextualMenuItem)
+ );
+ });
+
+ const flatMenuItems: IContextualMenuItem[] = itemGroups.reduce((resultItems, currentGroup, currentIndex) => {
+ if (currentIndex !== 0) {
+ // push a sep line ahead.
+ resultItems.push(createDivider());
+ }
+ resultItems.push(...currentGroup);
+ return resultItems;
+ }, []);
+
+ return flatMenuItems;
+};
+
+const createPasteButtonItem = (
+ menuItemCount: number,
+ disabled: boolean,
+ onClick: ActionMenuItemClickHandler
+): IContextualMenuItem => {
+ return {
+ key: 'Paste',
+ name: 'Paste',
+ ariaLabel: 'Paste',
+ disabled: disabled,
+ onRender: () => {
+ return (
+
+ );
+ },
+ };
+};
+
+interface ActionMenuOptions {
+ isSelfHosted: boolean;
+ enablePaste: boolean;
+}
+
+const createSubMenu = (
+ label: string,
+ onClick: ActionMenuItemClickHandler,
+ subItems: IContextualMenuItem[]
+): IContextualMenuItem => {
+ return {
+ key: label,
+ text: label,
+ subMenuProps: {
+ items: subItems,
+ onItemClick: (e, itemData) => onClick(itemData),
+ },
+ };
+};
+
+export const createActionMenu = (
+ onClick: ActionMenuItemClickHandler,
+ options: ActionMenuOptions,
+ customActionGroups?: DefinitionSummary[][]
+) => {
+ const resultItems: IContextualMenuItem[] = [];
+
+ // base SDK menu
+ const baseMenuItems = createBaseActionMenu(
+ onClick,
+ options.isSelfHosted ? ($kind: SDKKinds) => $kind !== SDKKinds.LogAction : undefined
+ );
+ resultItems.push(...baseMenuItems);
+
+ // Append a 'Custom Actions' item conditionally.
+ if (customActionGroups) {
+ const customActionItems = createCustomActionSubMenu(customActionGroups, onClick);
+ if (customActionItems.length) {
+ const customActionTitle = formatMessage('Custom Actions');
+ resultItems.push(createSubMenu(customActionTitle, onClick, customActionItems));
+ }
+ }
+
+ // paste button
+ const pasteButtonDisabled = !options.enablePaste;
+ const pasteButton = createPasteButtonItem(resultItems.length, pasteButtonDisabled, onClick);
+ resultItems.unshift(pasteButton, createDivider());
+
+ return resultItems;
+};
diff --git a/Composer/packages/extensions/visual-designer/src/editors/EventsEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/EventsEditor.tsx
index 380553aa20..1765e3c1bd 100644
--- a/Composer/packages/extensions/visual-designer/src/editors/EventsEditor.tsx
+++ b/Composer/packages/extensions/visual-designer/src/editors/EventsEditor.tsx
@@ -5,7 +5,6 @@ import React, { FC } from 'react';
import { Panel } from '../components/lib/Panel';
import { RuleGroup, CollapsedRuleGroup } from '../components/groups';
-import { EventMenu } from '../components/menus/EventMenu';
import { NodeEventTypes } from '../constants/NodeEventTypes';
import { EditorProps } from './editorProps';
@@ -14,8 +13,6 @@ export const EventsEditor: FC = ({ id, data, onEvent }): JSX.Elemen
const ruleCount = data.children.length;
const title = `Events (${ruleCount})`;
- const onClick = $kind => onEvent(NodeEventTypes.Insert, { id, $kind, position: ruleCount });
-
return (
= ({ id, data, onEvent }): JSX.Elemen
onEvent(NodeEventTypes.FocusEvent, '');
}}
collapsedItems={}
- addMenu={}
+ addMenu={null}
>
diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx
index 2b6a20e29e..d8540f08f2 100644
--- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx
+++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx
@@ -6,7 +6,7 @@ import { jsx } from '@emotion/core';
import { useContext, FC, useEffect, useState, useRef } from 'react';
import { MarqueeSelection, Selection } from 'office-ui-fabric-react/lib/MarqueeSelection';
import { SDKKinds, DialogUtils } from '@bfc/shared';
-import { useDialogApi, useDialogEditApi, useActionApi } from '@bfc/extension';
+import { useDialogApi, useDialogEditApi, useActionApi, useShellApi } from '@bfc/extension';
import get from 'lodash/get';
import { NodeEventTypes } from '../constants/NodeEventTypes';
@@ -41,6 +41,7 @@ export const ObiEditor: FC = ({
let divRef;
const { focusedId, focusedEvent, clipboardActions, dialogFactory } = useContext(NodeRendererContext);
+ const { shellApi } = useShellApi();
const {
insertAction,
insertActions,
@@ -50,9 +51,9 @@ export const ObiEditor: FC = ({
deleteSelectedAction,
deleteSelectedActions,
updateRecognizer,
- } = useDialogEditApi();
- const { createDialog, readDialog, updateDialog } = useDialogApi();
- const { actionsContainLuIntent } = useActionApi();
+ } = useDialogEditApi(shellApi);
+ const { createDialog, readDialog, updateDialog } = useDialogApi(shellApi);
+ const { actionsContainLuIntent } = useActionApi(shellApi);
const trackActionChange = (actionPath: string) => {
const affectedPaths = DialogUtils.getParentPaths(actionPath);
diff --git a/Composer/packages/extensions/visual-designer/src/index.tsx b/Composer/packages/extensions/visual-designer/src/index.tsx
index 906149c786..0e7313021d 100644
--- a/Composer/packages/extensions/visual-designer/src/index.tsx
+++ b/Composer/packages/extensions/visual-designer/src/index.tsx
@@ -4,7 +4,7 @@
/** @jsx jsx */
import { jsx, css, CacheProvider } from '@emotion/core';
import createCache from '@emotion/cache';
-import React, { useRef } from 'react';
+import React, { useRef, useMemo } from 'react';
import isEqual from 'lodash/isEqual';
import formatMessage from 'format-message';
import { DialogFactory } from '@bfc/shared';
@@ -16,6 +16,7 @@ import { SelfHostContext } from './store/SelfHostContext';
import { FlowSchemaContext } from './store/FlowSchemaContext';
import { FlowSchemaProvider } from './schema/flowSchemaProvider';
import { mergePluginConfig } from './utils/mergePluginConfig';
+import { getCustomSchema } from './utils/getCustomSchema';
formatMessage.setup({
missingTranslation: 'ignore',
@@ -41,7 +42,16 @@ export interface VisualDesignerProps {
}
const VisualDesigner: React.FC = ({ schema }): JSX.Element => {
const { shellApi, plugins, ...shellData } = useShellApi();
- const { dialogId, focusedEvent, focusedActions, focusedTab, clipboardActions, data: inputData, hosted } = shellData;
+ const {
+ dialogId,
+ focusedEvent,
+ focusedActions,
+ focusedTab,
+ clipboardActions,
+ data: inputData,
+ hosted,
+ schemas,
+ } = shellData;
const dataCache = useRef({});
@@ -77,6 +87,12 @@ const VisualDesigner: React.FC = ({ schema }): JSX.Element
const focusedId = Array.isArray(focusedActions) && focusedActions[0] ? focusedActions[0] : '';
+ // Compute schema diff
+ const customSchema = useMemo(() => getCustomSchema(schemas?.default, schemas?.sdk?.content), [
+ schemas?.sdk?.content,
+ schemas?.default,
+ ]);
+
const nodeContext: NodeRendererContextValue = {
focusedId,
focusedEvent,
@@ -89,6 +105,7 @@ const VisualDesigner: React.FC = ({ schema }): JSX.Element
removeLgTemplates,
removeLuIntent,
dialogFactory: new DialogFactory(schema),
+ customSchemas: customSchema ? [customSchema] : [],
};
const visualEditorConfig = mergePluginConfig(...plugins);
diff --git a/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts b/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts
index 4e7aadd7be..0d45eee0c9 100644
--- a/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts
+++ b/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts
@@ -2,7 +2,7 @@
// Licensed under the MIT License.
import React from 'react';
-import { ShellApi, DialogFactory } from '@bfc/shared';
+import { ShellApi, DialogFactory, OBISchema } from '@bfc/shared';
type ShellApiFuncs =
| 'getLgTemplates'
@@ -18,6 +18,7 @@ export interface NodeRendererContextValue extends Pick
focusedTab?: string;
clipboardActions: any[];
dialogFactory: DialogFactory;
+ customSchemas: OBISchema[];
}
export const NodeRendererContext = React.createContext({
@@ -32,4 +33,5 @@ export const NodeRendererContext = React.createContext
updateLgTemplate: () => Promise.resolve(),
removeLuIntent: () => Promise.resolve(),
dialogFactory: new DialogFactory(),
+ customSchemas: [],
});
diff --git a/Composer/packages/extensions/visual-designer/src/utils/getCustomSchema.ts b/Composer/packages/extensions/visual-designer/src/utils/getCustomSchema.ts
new file mode 100644
index 0000000000..dd6dac5737
--- /dev/null
+++ b/Composer/packages/extensions/visual-designer/src/utils/getCustomSchema.ts
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { OBISchema } from '@bfc/shared';
+
+export const getCustomSchema = (baseSchema?: OBISchema, ejectedSchema?: OBISchema): OBISchema | undefined => {
+ if (!baseSchema || !ejectedSchema) return;
+
+ const baseDefinitions = baseSchema.definitions;
+ const baseKindHash = Object.keys(baseDefinitions).reduce((hash, $kind) => {
+ hash[$kind] = true;
+ return hash;
+ }, {});
+
+ const ejectedDefinitions = ejectedSchema.definitions;
+ const diffKinds = Object.keys(ejectedDefinitions).filter($kind => !baseKindHash[$kind]);
+ if (diffKinds.length === 0) return;
+
+ const diffSchema = diffKinds.reduce(
+ (schema, $kind) => {
+ const definition = ejectedDefinitions[$kind];
+ schema.definitions[$kind] = definition;
+ schema.oneOf?.push({
+ title: definition.title || $kind,
+ description: definition.description || '',
+ $ref: `#/definitions/${$kind}`,
+ });
+ return schema;
+ },
+ {
+ oneOf: [],
+ definitions: {},
+ } as OBISchema
+ );
+
+ return diffSchema;
+};
diff --git a/Composer/packages/lib/code-editor/src/BaseEditor.tsx b/Composer/packages/lib/code-editor/src/BaseEditor.tsx
index 5bf3697054..772346afb1 100644
--- a/Composer/packages/lib/code-editor/src/BaseEditor.tsx
+++ b/Composer/packages/lib/code-editor/src/BaseEditor.tsx
@@ -225,7 +225,6 @@ const BaseEditor: React.FC = props => {
messageBarType={hasError ? MessageBarType.error : hasWarning ? MessageBarType.warning : MessageBarType.info}
isMultiline={true}
dismissButtonAriaLabel={formatMessage('Close')}
- overflowButtonAriaLabel={formatMessage('See more')}
>
{messageHelp}
{syntaxLink}
diff --git a/Composer/packages/lib/code-editor/src/LuEditor.tsx b/Composer/packages/lib/code-editor/src/LuEditor.tsx
index dfb3133465..6250fefa1c 100644
--- a/Composer/packages/lib/code-editor/src/LuEditor.tsx
+++ b/Composer/packages/lib/code-editor/src/LuEditor.tsx
@@ -10,10 +10,7 @@ import { EditorDidMount, Monaco } from '@monaco-editor/react';
import { registerLULanguage } from './languages';
import { createUrl, createWebSocket, createLanguageClient } from './utils/lspUtil';
import { BaseEditor, BaseEditorProps, OnInit } from './BaseEditor';
-
-const LU_HELP = 'https://aka.ms/lu-file-format';
-const placeholder = `> To learn more about the LU file format, read the documentation at
-> ${LU_HELP}`;
+import { defaultPlaceholder, LU_HELP } from './constants';
export interface LUOption {
projectId?: string;
@@ -85,7 +82,7 @@ const LuEditor: React.FC = props => {
...props.options,
};
- const { luOption, languageServer, onInit: onInitProp, ...restProps } = props;
+ const { luOption, languageServer, onInit: onInitProp, placeholder = defaultPlaceholder, ...restProps } = props;
const luServer = languageServer || defaultLUServer;
const onInit: OnInit = monaco => {
diff --git a/Composer/packages/lib/code-editor/src/constants.ts b/Composer/packages/lib/code-editor/src/constants.ts
new file mode 100644
index 0000000000..1fa17530b1
--- /dev/null
+++ b/Composer/packages/lib/code-editor/src/constants.ts
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import formatMessage from 'format-message';
+
+export const inlineModePlaceholder = formatMessage(`> add some example phrases to trigger this intent:
+> - please tell me the weather
+> - what is the weather like in '{city=Seattle}'
+>
+> entity definitions:
+> @ ml city`);
+
+export const LU_HELP = 'https://aka.ms/lu-file-format';
+export const defaultPlaceholder = formatMessage(
+ `> To learn more about the LU file format, read the documentation at
+> {LU_HELP}`,
+ { LU_HELP }
+);
diff --git a/Composer/packages/lib/code-editor/src/index.ts b/Composer/packages/lib/code-editor/src/index.ts
index 71dfe2decd..effdf10018 100644
--- a/Composer/packages/lib/code-editor/src/index.ts
+++ b/Composer/packages/lib/code-editor/src/index.ts
@@ -11,3 +11,4 @@ export * from './BaseEditor';
export * from './JsonEditor';
export * from './LgEditor';
export * from './LuEditor';
+export * from './constants';
diff --git a/Composer/packages/lib/shared/src/types/schema.ts b/Composer/packages/lib/shared/src/types/schema.ts
index 583585bf80..df93d1da8b 100644
--- a/Composer/packages/lib/shared/src/types/schema.ts
+++ b/Composer/packages/lib/shared/src/types/schema.ts
@@ -121,6 +121,12 @@ export enum SDKRoles {
// union_*_ = 'union(*)',
}
+export interface DefinitionSummary {
+ title: string;
+ description: string;
+ $ref: string;
+}
+
export interface OBISchema extends JSONSchema6 {
$schema?: string;
$role?: string;
@@ -132,6 +138,7 @@ export interface OBISchema extends JSONSchema6 {
[key: string]: any;
};
description?: string;
+ oneOf?: DefinitionSummary[];
definitions?: any;
title?: string;
__additional_property?: boolean;
diff --git a/Composer/packages/lib/shared/src/types/shell.ts b/Composer/packages/lib/shared/src/types/shell.ts
index ce276bb98b..1e4ed15331 100644
--- a/Composer/packages/lib/shared/src/types/shell.ts
+++ b/Composer/packages/lib/shared/src/types/shell.ts
@@ -4,6 +4,7 @@
import { DialogInfo, LuFile, LgFile, LuIntentSection, LgTemplate } from './indexers';
import { UserSettings } from './settings';
+import { OBISchema } from './schema';
/** Recursively marks all properties as optional. */
type AllPartial = {
@@ -18,6 +19,7 @@ export interface EditorSchema {
}
export interface BotSchemas {
+ default?: OBISchema;
sdk?: any;
diagnostics?: any[];
}
@@ -59,13 +61,14 @@ export interface ShellApi {
onSelect: (ids: string[]) => void;
getLgTemplates: (id: string) => LgTemplate[];
copyLgTemplate: (id: string, fromTemplateName: string, toTemplateName?: string) => Promise;
+ addLgTemplate: (id: string, templateName: string, templateStr: string) => Promise;
updateLgTemplate: (id: string, templateName: string, templateStr: string) => Promise;
removeLgTemplate: (id: string, templateName: string) => Promise;
removeLgTemplates: (id: string, templateNames: string[]) => Promise;
getLuIntent: (id: string, intentName: string) => LuIntentSection | undefined;
getLuIntents: (id: string) => LuIntentSection[];
- addLuIntent: (id: string, intentName: string, intent: LuIntentSection | undefined) => Promise;
- updateLuIntent: (id: string, intentName: string, intent: LuIntentSection | undefined) => Promise;
+ addLuIntent: (id: string, intentName: string, intent: LuIntentSection) => Promise;
+ updateLuIntent: (id: string, intentName: string, intent: LuIntentSection) => Promise;
removeLuIntent: (id: string, intentName: string) => void;
updateRegExIntent: (id: string, intentName: string, pattern: string) => void;
createDialog: (actions: any) => Promise;
diff --git a/Composer/packages/lib/shared/src/viewUtils.ts b/Composer/packages/lib/shared/src/viewUtils.ts
index 81df4043ad..97224f1a6e 100644
--- a/Composer/packages/lib/shared/src/viewUtils.ts
+++ b/Composer/packages/lib/shared/src/viewUtils.ts
@@ -1,15 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-import {
- IContextualMenuItem,
- IContextualMenuProps,
-} from 'office-ui-fabric-react/lib/components/ContextualMenu/ContextualMenu.types';
import get from 'lodash/get';
import { SDKKinds } from './types';
import { ConceptLabels } from './labelMap';
-import { DialogFactory } from './dialogFactory';
export const PROMPT_TYPES = [
SDKKinds.AttachmentInput,
@@ -161,106 +156,6 @@ export const dialogGroups: DialogGroupsMap = {
},
};
-const menuItemHandler = (
- factory: DialogFactory,
- handleType: (
- e: React.MouseEvent | React.KeyboardEvent | undefined,
- item: IContextualMenuItem
- ) => void
-) => (
- e: React.MouseEvent | React.KeyboardEvent | undefined,
- item: IContextualMenuItem | undefined
-) => {
- if (item) {
- const name =
- ConceptLabels[item.$kind] && ConceptLabels[item.$kind].title ? ConceptLabels[item.$kind].title : item.$kind;
- item = {
- ...item,
- data: {
- ...factory.create(item.$kind, {
- $designer: { name },
- }),
- },
- };
- return handleType(e, item);
- }
-};
-
-export const createStepMenu = (
- stepLabels: DialogGroup[],
- subMenu = true,
- handleType: (
- e: React.MouseEvent | React.KeyboardEvent | undefined,
- item: IContextualMenuItem
- ) => void,
- factory: DialogFactory,
- filter?: (x: SDKKinds) => boolean
-): IContextualMenuItem[] => {
- if (subMenu) {
- const stepMenuItems = stepLabels.map(x => {
- const item = dialogGroups[x];
- if (item.types.length === 1) {
- const conceptLabel = ConceptLabels[item.types[0]];
- return {
- key: item.types[0],
- name: conceptLabel && conceptLabel.title ? conceptLabel.title : item.types[0],
- $kind: item.types[0],
- onClick: menuItemHandler(factory, handleType),
- };
- }
- const subMenu: IContextualMenuProps = {
- items: item.types.filter(filter || (() => true)).map($kind => {
- const conceptLabel = ConceptLabels[$kind];
-
- return {
- key: $kind,
- name: conceptLabel && conceptLabel.title ? conceptLabel.title : $kind,
- $kind: $kind,
- };
- }),
- onItemClick: menuItemHandler(factory, handleType),
- };
-
- const menuItem: IContextualMenuItem = {
- key: item.label,
- text: item.label,
- name: item.label,
- subMenuProps: subMenu,
- };
- return menuItem;
- });
-
- return stepMenuItems;
- } else {
- const stepMenuItems = dialogGroups[stepLabels[0]].types.map(item => {
- const conceptLabel = ConceptLabels[item];
- const name = conceptLabel && conceptLabel.title ? conceptLabel.title : item;
- const menuItem: IContextualMenuItem = {
- key: item,
- text: name,
- name: name,
- $kind: item,
- ...factory.create(item, {
- $designer: { name },
- }),
- data: {
- $kind: item,
- ...factory.create(item, {
- $designer: { name },
- }),
- },
- onClick: (e, item: IContextualMenuItem | undefined) => {
- if (item) {
- return handleType(e, item);
- }
- },
- };
- return menuItem;
- });
- return stepMenuItems;
- }
-};
-
export function getDialogGroupByType(type) {
let dialogType = DialogGroup.OTHER;
diff --git a/Composer/packages/server/__tests__/controllers/project.test.ts b/Composer/packages/server/__tests__/controllers/project.test.ts
index def2c60f4b..c85e08af94 100644
--- a/Composer/packages/server/__tests__/controllers/project.test.ts
+++ b/Composer/packages/server/__tests__/controllers/project.test.ts
@@ -159,7 +159,7 @@ describe('dialog operation', () => {
const mockReq = {
params: { projectId },
query: {},
- body: { name: 'test.dialog', content: '' },
+ body: { name: 'test2.dialog', content: '' },
} as Request;
await ProjectController.createFile(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
@@ -167,7 +167,7 @@ describe('dialog operation', () => {
it('should remove dialog', async () => {
const mockReq = {
- params: { name: 'test.dialog', projectId },
+ params: { name: 'test2.dialog', projectId },
query: {},
body: {},
} as Request;
diff --git a/Composer/packages/server/src/controllers/project.ts b/Composer/packages/server/src/controllers/project.ts
index 8f5cdd32c3..faebddd8e9 100644
--- a/Composer/packages/server/src/controllers/project.ts
+++ b/Composer/packages/server/src/controllers/project.ts
@@ -80,7 +80,6 @@ async function getProjectById(req: Request, res: Response) {
const currentProject = await BotProjectService.getProjectById(projectId, user);
if (currentProject !== undefined && (await currentProject.exists())) {
- await currentProject.init();
const project = currentProject.getProject();
res.status(200).json({
id: projectId,
diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts
index 622f27943d..963f170823 100644
--- a/Composer/packages/server/src/models/bot/botProject.ts
+++ b/Composer/packages/server/src/models/bot/botProject.ts
@@ -178,6 +178,7 @@ export class BotProject {
sdk: {
content: sdkSchema,
},
+ default: this.defaultSDKSchema,
diagnostics,
};
};
@@ -250,7 +251,7 @@ export class BotProject {
};
public deleteFile = async (name: string) => {
- if (Path.resolve(name) === 'Main') {
+ if (Path.basename(name, '.dialog') === this.name) {
throw new Error(`Main dialog can't be removed`);
}
@@ -333,7 +334,7 @@ export class BotProject {
try {
await this.fileStorage.rmDir(folderPath);
} catch (e) {
- console.log(e);
+ // pass
}
}
};
@@ -410,10 +411,12 @@ export class BotProject {
if (index === -1) {
throw new Error(`no such file at ${relativePath}`);
}
+
const absolutePath = `${this.dir}/${relativePath}`;
// only write if the file has actually changed
if (this.files[index].content !== content) {
+ this.files[index].content = content;
await this.fileStorage.writeFile(absolutePath, content);
}
@@ -421,8 +424,6 @@ export class BotProject {
// instead of calling stat again which could be expensive
const stats = await this.fileStorage.stat(absolutePath);
- this.files[index].content = content;
-
return stats.lastModified;
};
@@ -433,10 +434,10 @@ export class BotProject {
if (index === -1) {
throw new Error(`no such file at ${relativePath}`);
}
+ this.files.splice(index, 1);
const absolutePath = `${this.dir}/${relativePath}`;
await this.fileStorage.removeFile(absolutePath);
- this.files.splice(index, 1);
};
// ensure dir exist, dir is a absolute dir path
diff --git a/Composer/packages/server/src/services/project.ts b/Composer/packages/server/src/services/project.ts
index 0576609c6c..5af12caa27 100644
--- a/Composer/packages/server/src/services/project.ts
+++ b/Composer/packages/server/src/services/project.ts
@@ -4,6 +4,7 @@
import merge from 'lodash/merge';
import find from 'lodash/find';
import { importResolverGenerator, ResolverResource } from '@bfc/shared';
+import extractMemoryPaths from '@bfc/indexers/lib/dialogUtils/extractMemoryPaths';
import { UserIdentity } from '@bfc/plugin-loader';
import { BotProject } from '../models/bot/botProject';
@@ -35,7 +36,7 @@ export class BotProjectService {
public static lgImportResolver(source: string, id: string, projectId: string): ResolverResource {
BotProjectService.initialize();
- const project = BotProjectService.currentBotProjects.find(({ id }) => id === projectId);
+ const project = BotProjectService.getIndexedProjectById(projectId);
if (!project) throw new Error('project not found');
const resource = project.files.reduce((result: ResolverResource[], file) => {
const { name, content } = file;
@@ -50,7 +51,7 @@ export class BotProjectService {
public static luImportResolver(source: string, id: string, projectId: string): ResolverResource {
BotProjectService.initialize();
- const project = BotProjectService.currentBotProjects.find(({ id }) => id === projectId);
+ const project = BotProjectService.getIndexedProjectById(projectId);
if (!project) throw new Error('project not found');
const resource = project.files.reduce((result: ResolverResource[], file) => {
const { name, content } = file;
@@ -88,9 +89,13 @@ export class BotProjectService {
'turn.repeatedIds',
'turn.activityProcessed',
];
+ const projectVariables = BotProjectService.getIndexedProjectById(projectId)
+ ?.files.filter(file => file.name.endsWith('.dialog'))
+ .map(({ content }) => extractMemoryPaths(content));
+
const userDefined: string[] =
- BotProjectService.currentBotProjects[projectId]?.dialogs.reduce((result: string[], dialog) => {
- result = [...dialog.userDefinedVariables, ...result];
+ projectVariables?.reduce((result: string[], variables) => {
+ result = [...variables, ...result];
return result;
}, []) || [];
return [...defaultProperties, ...userDefined];
@@ -180,9 +185,18 @@ export class BotProjectService {
Store.set('projectLocationMap', BotProjectService.projectLocationMap);
};
- public static getProjectById = async (projectId: string, user?: UserIdentity) => {
+ public static getIndexedProjectById(projectId): BotProject | undefined {
+ // use indexed project
+ const indexedCurrentProject = BotProjectService.currentBotProjects.find(({ id }) => id === projectId);
+ if (indexedCurrentProject) return indexedCurrentProject;
+ }
+
+ public static getProjectById = async (projectId: string, user?: UserIdentity): Promise => {
BotProjectService.initialize();
+ const cachedProject = BotProjectService.getIndexedProjectById(projectId);
+ if (cachedProject) return cachedProject;
+
if (!BotProjectService.projectLocationMap?.[projectId]) {
throw new Error('project not found in cache');
} else {
diff --git a/Composer/packages/ui-plugins/expressions/src/index.ts b/Composer/packages/ui-plugins/expressions/src/index.ts
index 991521568b..e2c9d34412 100644
--- a/Composer/packages/ui-plugins/expressions/src/index.ts
+++ b/Composer/packages/ui-plugins/expressions/src/index.ts
@@ -10,6 +10,7 @@ const config: PluginConfig = {
roleSchema: {
[SDKRoles.expression]: {
field: ExpressionField,
+ helpLink: 'https://github.com/microsoft/BotBuilder-Samples/tree/master/experimental/common-expression-language',
},
},
};
diff --git a/Composer/packages/ui-plugins/lg/src/LgField.tsx b/Composer/packages/ui-plugins/lg/src/LgField.tsx
index 7f094320cc..ec3ef16ca5 100644
--- a/Composer/packages/ui-plugins/lg/src/LgField.tsx
+++ b/Composer/packages/ui-plugins/lg/src/LgField.tsx
@@ -112,7 +112,7 @@ const LgField: React.FC> = props => {
> = props => {
- const { onChange, value, schema } = props;
+ const { onChange, value, schema, placeholder } = props;
const { currentDialog, designerId, luFiles, shellApi, locale, projectId, userSettings } = useShellApi();
const luFile = luFiles.find(f => f.id === `${currentDialog.id}.${locale}`);
@@ -51,13 +51,14 @@ const LuisIntentEditor: React.FC> = props => {
return (
);
};
diff --git a/Composer/packages/ui-plugins/prompts/src/PromptField/UserInput.tsx b/Composer/packages/ui-plugins/prompts/src/PromptField/UserInput.tsx
index 1f18ca6b48..377fd69c70 100644
--- a/Composer/packages/ui-plugins/prompts/src/PromptField/UserInput.tsx
+++ b/Composer/packages/ui-plugins/prompts/src/PromptField/UserInput.tsx
@@ -20,16 +20,26 @@ const getOptions = (enumSchema: JSONSchema7) => {
return enumSchema.enum.map(o => o as string);
};
+const expectedResponsesPlaceholder = () =>
+ formatMessage(`> add some expected user responses:
+> - Please remind me to '{itemTitle=buy milk}'
+> - remind me to '{itemTitle}'
+> - add '{itemTitle}' to my todo list
+>
+> entity definitions:
+> @ ml itemTitle
+`);
+
const UserInput: React.FC> = props => {
const { onChange, getSchema, value, id, uiOptions, getError, definitions, depth, schema = {} } = props;
const { currentDialog, designerId } = useShellApi();
const { recognizers } = usePluginConfig();
- const { const: $kind } = (schema?.properties?.['$kind'] as any) || {};
+ const { const: $kind } = (schema?.properties?.$kind as { const: string }) || {};
const intentName = new LuMetaData(new LuType($kind).toString(), designerId).toString();
const type = recognizerType(currentDialog);
- const Editor: any = type === SDKKinds.LuisRecognizer && recognizers.find(r => r.id === type)?.editor;
+ const Editor = type === SDKKinds.LuisRecognizer && recognizers.find(r => r.id === type)?.editor;
const intentLabel = formatMessage('Expected responses (intent: #{intentName})', { intentName });
return (
@@ -72,7 +82,7 @@ const UserInput: React.FC> = props => {
{Editor && $kind !== SDKKinds.AttachmentInput && (
- {}} />
+ {}} placeholder={expectedResponsesPlaceholder()} />
)}
{getSchema('defaultLocale') && (
diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx
index b1d8e6bcb7..5e43fee025 100644
--- a/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx
+++ b/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx
@@ -7,7 +7,7 @@ import React from 'react';
import { FieldProps } from '@bfc/extension';
import {
getUiLabel,
- getUISchema,
+ getUIOptions,
getUiPlaceholder,
getUiDescription,
schemaField,
@@ -20,7 +20,7 @@ export const SkillEndpointField: React.FC = props => {
const pluginConfig = usePluginConfig();
const uiOptions = {
- ...getUISchema(schema, pluginConfig.formSchema),
+ ...getUIOptions(schema, pluginConfig.formSchema),
...baseUIOptions,
};