diff --git a/Composer/packages/client/package.json b/Composer/packages/client/package.json index 1a7e6c64b6..68376ca49a 100644 --- a/Composer/packages/client/package.json +++ b/Composer/packages/client/package.json @@ -24,6 +24,7 @@ "@reach/router": "^1.2.1", "axios": "^0.18.0", "botbuilder-lg": "4.7.0-preview.93464", + "botframework-expressions": "4.7.0-preview.93464", "format-message": "^6.2.3", "immer": "^2.1.4", "jwt-decode": "^2.2.0", diff --git a/Composer/packages/client/src/pages/language-generation/index.tsx b/Composer/packages/client/src/pages/language-generation/index.tsx index d8cd74ff51..2b255ae1ab 100644 --- a/Composer/packages/client/src/pages/language-generation/index.tsx +++ b/Composer/packages/client/src/pages/language-generation/index.tsx @@ -98,7 +98,7 @@ const LGPage: React.FC = props => { useEffect(() => { // dialog lg templates is part of commong.lg. By restricting edit in root view, user would aware that the changes they made may affect other dialogs. - if (!isRoot) { + if (!isRoot && fileValid) { setEditMode(false); } diff --git a/Composer/packages/client/src/pages/language-generation/table-view.tsx b/Composer/packages/client/src/pages/language-generation/table-view.tsx index da1b557b85..8b01f84c35 100644 --- a/Composer/packages/client/src/pages/language-generation/table-view.tsx +++ b/Composer/packages/client/src/pages/language-generation/table-view.tsx @@ -16,8 +16,9 @@ import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky'; import formatMessage from 'format-message'; import { NeutralColors, FontSizes } from '@uifabric/fluent-theme'; import { DialogInfo, LgFile } from '@bfc/indexers'; -import { LGTemplate } from 'botbuilder-lg'; +import { LGTemplate, LGParser } from 'botbuilder-lg'; import { lgIndexer } from '@bfc/indexers'; +import get from 'lodash/get'; import { StoreContext } from '../../store'; import * as lgUtil from '../../utils/lgUtil'; @@ -44,7 +45,8 @@ const TableView: React.FC = props => { if (isEmpty(lgFile)) return; let allTemplates: LGTemplate[] = []; if (lgIndexer.isValid(lgFile.diagnostics) === true) { - allTemplates = lgIndexer.parse(lgFile.content) as LGTemplate[]; + const resource = LGParser.parse(lgFile.content, ''); + allTemplates = get(resource, 'templates', []); } if (!activeDialog) { setTemplates(allTemplates); diff --git a/Composer/packages/client/src/utils/dialogUtil.ts b/Composer/packages/client/src/utils/dialogUtil.ts index a9044aed67..e388decfb0 100644 --- a/Composer/packages/client/src/utils/dialogUtil.ts +++ b/Composer/packages/client/src/utils/dialogUtil.ts @@ -5,7 +5,7 @@ import { ConceptLabels, DialogGroup, SDKTypes, dialogGroups, seedNewDialog } fro import get from 'lodash/get'; import set from 'lodash/set'; import cloneDeep from 'lodash/cloneDeep'; -import { ExpressionEngine } from 'botbuilder-expression-parser'; +import { ExpressionEngine } from 'botframework-expressions'; import { IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import { DialogInfo } from '@bfc/indexers'; diff --git a/Composer/packages/extensions/obiformeditor/demo/src/index.tsx b/Composer/packages/extensions/obiformeditor/demo/src/index.tsx index 8c537a6458..df29a7ae94 100644 --- a/Composer/packages/extensions/obiformeditor/demo/src/index.tsx +++ b/Composer/packages/extensions/obiformeditor/demo/src/index.tsx @@ -5,7 +5,7 @@ import { PrimaryButton, DefaultButton, DirectionalHint } from 'office-ui-fabric- import debounce from 'lodash/debounce'; import nanoid from 'nanoid'; import { initializeIcons } from '@uifabric/icons'; -import { ExpressionEngine } from 'botbuilder-expression-parser'; +import { ExpressionEngine } from 'botframework-expressions'; import { seedNewDialog, ShellApi } from '@bfc/shared'; import { LuFile, DialogInfo } from '@bfc/indexers'; diff --git a/Composer/packages/extensions/obiformeditor/package.json b/Composer/packages/extensions/obiformeditor/package.json index f282ebb250..a74aae2939 100644 --- a/Composer/packages/extensions/obiformeditor/package.json +++ b/Composer/packages/extensions/obiformeditor/package.json @@ -1,6 +1,5 @@ { "name": "@bfc/obiformeditor", - "license": "MIT", "version": "1.0.0", "description": "obieditortest React component", "engines": { @@ -62,7 +61,7 @@ "@types/react": "16.9.0", "@types/react-dom": "16.9.0", "autoprefixer": "^9.5.1", - "botbuilder-expression-parser": "^4.5.11", + "botframework-expressions": "4.7.0-preview.93464", "codemirror": "^5.44.0", "copyfiles": "^2.1.0", "css-loader": "^2.1.1", @@ -92,4 +91,4 @@ "keywords": [ "react-component" ] -} \ No newline at end of file +} diff --git a/Composer/packages/extensions/obiformeditor/src/Form/types.ts b/Composer/packages/extensions/obiformeditor/src/Form/types.ts index 657ef86324..4d7f1a63f8 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/types.ts +++ b/Composer/packages/extensions/obiformeditor/src/Form/types.ts @@ -12,6 +12,7 @@ export interface FormContext dialogOptions: { value: string; label: string }[]; dialogId?: string; isRoot: boolean; + formErrors: any; } interface EnumOption { diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/ExpressionWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/ExpressionWidget.tsx index fcc04efe77..41e48a6d00 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/ExpressionWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/ExpressionWidget.tsx @@ -6,6 +6,7 @@ import { TextField, ITextFieldProps, ITextFieldStyles } from 'office-ui-fabric-r import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar'; import { JSONSchema6 } from 'json-schema'; import formatMessage from 'format-message'; +import get from 'lodash/get'; import { FormContext } from '../types'; import { EditableField } from '../fields/EditableField'; @@ -25,8 +26,9 @@ interface ExpresionWidgetProps extends ITextFieldProps { options?: any; } -const getDefaultErrorMessage = () => { - return formatMessage.rich('Invalid expression syntax. Refer to the syntax documentationhere', { +const getDefaultErrorMessage = errMessage => { + return formatMessage.rich('{errMessage}. Refer to the syntax documentationhere', { + errMessage, a: ({ children }) => ( { }; export const ExpressionWidget: React.FC = props => { - const { - rawErrors, - formContext, - schema, - id, - label, - editable, - hiddenErrMessage, - onValidate, - options = {}, - ...rest - } = props; - const { shellApi } = formContext; + const { formContext, schema, id, label, editable, hiddenErrMessage, onValidate, options = {}, ...rest } = props; const { description } = schema; const { hideLabel } = options; + const name = props.id?.split('_')[props.id?.split('_').length - 1]; - const onGetErrorMessage = async (value: string): Promise => { - if (!value) { - if (hiddenErrMessage) { - onValidate && onValidate(); - } - return ''; - } - - const isValid = await shellApi.validateExpression(value); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let errMessage: string | any[] = ''; - if (!isValid) { - errMessage = getDefaultErrorMessage(); - } else if (rawErrors && rawErrors.length > 0) { - errMessage = rawErrors[0]; - } + const onGetErrorMessage = (): JSX.Element | string => { + const errMessage = name && get(formContext, ['formErrors', name], ''); const messageBar = errMessage ? ( = props => { truncated overflowButtonAriaLabel={formatMessage('See more')} > - {errMessage} + {getDefaultErrorMessage(`${label} ${errMessage}`)} ) : ( '' diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LgEditorWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LgEditorWidget.tsx index 591721cdc2..c75ceb053d 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LgEditorWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LgEditorWidget.tsx @@ -45,7 +45,6 @@ interface LgEditorWidgetProps { export const LgEditorWidget: React.FC = props => { const { formContext, name, value, height = 250 } = props; - const [errorMsg, setErrorMsg] = useState(''); const lgName = new LgMetaData(name, formContext.dialogId || '').toString(); const lgFileId = formContext.currentDialog.lgFile || 'common'; const lgFile = formContext.lgFiles && formContext.lgFiles.find(file => file.id === lgFileId); @@ -53,10 +52,7 @@ export const LgEditorWidget: React.FC = props => { const updateLgTemplate = useMemo( () => debounce((body: string) => { - formContext.shellApi - .updateLgTemplate(lgFileId, lgName, body) - .then(() => setErrorMsg('')) - .catch(error => setErrorMsg(error)); + formContext.shellApi.updateLgTemplate(lgFileId, lgName, body).catch(() => {}); }, 500), [lgName, lgFileId] ); @@ -68,8 +64,25 @@ export const LgEditorWidget: React.FC = props => { })) || { name: lgName, body: getInitialTemplate(name, value), + range: { + startLineNumber: 0, + endLineNumber: 2, + }, }; + const diagnostic = + lgFile && + lgFile.diagnostics.find(d => { + return ( + d.range && + d.range.start.line >= template.range.startLineNumber && + d.range.end.line <= template.range.endLineNumber + ); + }); + + const errorMsg = diagnostic + ? diagnostic.message.split('error message: ')[diagnostic.message.split('error message: ').length - 1] + : ''; const [localValue, setLocalValue] = useState(template.body); const lgOption = { inline: true, diff --git a/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx b/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx index 4c39f3eaf8..c198c6688d 100644 --- a/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx +++ b/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx @@ -3,13 +3,14 @@ /** @jsx jsx */ import { Global, jsx } from '@emotion/core'; -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { Dropdown } from 'office-ui-fabric-react/lib/Dropdown'; import { JSONSchema6Definition, JSONSchema6 } from 'json-schema'; import merge from 'lodash/merge'; import get from 'lodash/get'; import isEqual from 'lodash/isEqual'; import { appschema, ShellData, ShellApi } from '@bfc/shared'; +import { Diagnostic } from '@bfc/indexers'; import Form from './Form'; import { uiSchema } from './schema/uischema'; @@ -33,6 +34,25 @@ export const FormEditor: React.FunctionComponent = props => { const [localData, setLocalData] = useState(data); const type = getType(localData); + const formErrors = useMemo(() => { + if (props.currentDialog && props.currentDialog.diagnostics) { + const currentPath = props.focusPath.replace('#', ''); + const diagnostics = get(props.currentDialog, 'diagnostics', [] as Diagnostic[]); + + return diagnostics.reduce((errors, d) => { + const [dPath, dType, dProp] = d.path?.split('#') || []; + + if (dPath === currentPath && dType === type && dProp) { + errors[dProp] = d.message; + } + + return errors; + }, {}); + } + + return {}; + }, [props.currentDialog]); + if (!type) { return (
@@ -112,6 +132,7 @@ export const FormEditor: React.FunctionComponent = props => { focusedEvent: props.focusedEvent, focusedSteps: props.focusedSteps, focusedTab: props.focusedTab, + formErrors, }} idPrefix={props.focusPath} > diff --git a/Composer/packages/lib/code-editor/src/RichEditor.tsx b/Composer/packages/lib/code-editor/src/RichEditor.tsx index dc4dcf3ccc..389b7b8d22 100644 --- a/Composer/packages/lib/code-editor/src/RichEditor.tsx +++ b/Composer/packages/lib/code-editor/src/RichEditor.tsx @@ -51,16 +51,14 @@ export function RichEditor(props: RichEditorProps) { } }, [editor]); - const errorHelp = formatMessage.rich( - 'This text has errors in the syntax. Refer to the syntax documentationhere.', - { - a: ({ children }) => ( - - {children} - - ), - } - ); + const errorHelp = formatMessage.rich('{errorMsg}. Refer to the syntax documentationhere.', { + errorMsg, + a: ({ children }) => ( + + {children} + + ), + }); const getHeight = () => { if (height === null || height === undefined) { diff --git a/Composer/packages/lib/indexers/package.json b/Composer/packages/lib/indexers/package.json index d9383d70f9..ede73be87b 100644 --- a/Composer/packages/lib/indexers/package.json +++ b/Composer/packages/lib/indexers/package.json @@ -28,8 +28,8 @@ "ts-jest": "^24.1.0" }, "dependencies": { - "botbuilder-expression-parser": "^4.5.11", "botbuilder-lg": "4.7.0-preview.93464", + "botframework-expressions": "4.7.0-preview.93464", "lodash": "^4.17.15", "ludown": "^1.3.4" } diff --git a/Composer/packages/lib/indexers/src/dialogUtils/dialogChecker.ts b/Composer/packages/lib/indexers/src/dialogUtils/dialogChecker.ts index 82e0acd849..9e27fa6eaa 100644 --- a/Composer/packages/lib/indexers/src/dialogUtils/dialogChecker.ts +++ b/Composer/packages/lib/indexers/src/dialogUtils/dialogChecker.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import get from 'lodash/get'; -import { ExpressionEngine } from 'botbuilder-expression-parser'; +import { ExpressionEngine } from 'botframework-expressions'; import formatMessage from 'format-message'; import { Diagnostic } from '../diagnostic'; @@ -26,7 +26,7 @@ export const IsExpression: CheckerFunc = ( try { ExpressionParser.parse(exp); } catch (error) { - message = formatMessage(`must be an expression`); + message = `${formatMessage('must be an expression:')} ${error})`; } } if (message) { diff --git a/Composer/packages/lib/indexers/src/lgIndexer.ts b/Composer/packages/lib/indexers/src/lgIndexer.ts index d33107bf7f..ab1f0ab125 100644 --- a/Composer/packages/lib/indexers/src/lgIndexer.ts +++ b/Composer/packages/lib/indexers/src/lgIndexer.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { LGParser, StaticChecker, Diagnostic as LGDiagnostic, ImportResolver } from 'botbuilder-lg'; +import get from 'lodash/get'; import { FileInfo, LgFile, LgTemplate } from './type'; import { getBaseName } from './utils/help'; @@ -40,6 +41,10 @@ function parse(content: string, id?: string): LgTemplate[] { name: t.name, body: t.body, parameters: t.parameters, + range: { + startLineNumber: get(t, 'parseTree.start.line', 0), + endLineNumber: get(t, 'parseTree.stop.line', 0), + }, }; }); return templates; diff --git a/Composer/packages/lib/indexers/src/type.ts b/Composer/packages/lib/indexers/src/type.ts index 1d0c0cb508..46c50b5a33 100644 --- a/Composer/packages/lib/indexers/src/type.ts +++ b/Composer/packages/lib/indexers/src/type.ts @@ -58,11 +58,16 @@ export interface LuFile { diagnostics: LuDiagnostic[]; [key: string]: any; } +export interface CodeRange { + startLineNumber: number; + endLineNumber: number; +} export interface LgTemplate { name: string; body: string; parameters: string[]; + range: CodeRange; } export interface LgFile { diff --git a/Composer/packages/tools/language-servers/language-generation/src/builtinFunctionsMap.ts b/Composer/packages/tools/language-servers/language-generation/src/builtinFunctionsMap.ts index 96af98d82e..6e7b7a8e4d 100644 --- a/Composer/packages/tools/language-servers/language-generation/src/builtinFunctionsMap.ts +++ b/Composer/packages/tools/language-servers/language-generation/src/builtinFunctionsMap.ts @@ -6,7 +6,7 @@ * Licensed under the MIT License. */ -import { ReturnType } from 'botbuilder-expression'; +import { ReturnType } from 'botframework-expressions'; export class FunctionEntity { public constructor(params: string[], returntype: ReturnType, introduction: string) { this.Params = params; diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 16e4fcb8cc..cf73cdd724 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -4388,28 +4388,6 @@ botbuilder-core@4.7.0-preview.93464: assert "^1.4.1" botframework-schema "4.7.0-preview.93464" -botbuilder-expression-parser@^4.5.11: - version "4.5.11" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/botbuilder-expression-parser/-/botbuilder-expression-parser-4.5.11.tgz#fcce377a2b2c1a2c2345b2c77d9cf4289e171f9f" - integrity sha1-/M43eissGiwjRbLHfZz0KJ4XH58= - dependencies: - antlr4ts "0.5.0-alpha.1" - botbuilder-expression "https://botbuilder.myget.org/F/botbuilder-declarative/npm/botbuilder-expression/-/4.5.11.tgz" - -"botbuilder-expression@https://botbuilder.myget.org/F/botbuilder-declarative/npm/botbuilder-expression/-/4.5.11.tgz": - version "4.5.11" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/botbuilder-expression/-/4.5.11.tgz#d15384417be9c0e17d46bf729fe707b81b66ef68" - dependencies: - "@microsoft/recognizers-text-data-types-timex-expression" "^1.1.4" - antlr4ts "0.5.0-alpha.1" - jspath "^0.4.0" - lru-cache "^5.1.1" - moment "2.24.0" - moment-timezone "^0.5.25" - xml2js "^0.4.19" - xmldom "^0.1.27" - xpath "0.0.27" - botbuilder-lg@4.7.0-preview.93464: version "4.7.0-preview.93464" resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botbuilder-lg/-/botbuilder-lg-4.7.0-preview.93464.tgz#504c647aca501998a0bcf89da33612c808334825" @@ -17138,21 +17116,11 @@ xmlbuilder@^9.0.7, xmlbuilder@~9.0.1: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= -xmldom@^0.1.27: - version "0.1.27" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" - integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk= - xmlhttprequest@1: version "1.8.0" resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw= -xpath@0.0.27: - version "0.0.27" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92" - integrity sha1-3TQh+9zFZGrDLEhTG01+nQws+pI= - xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"