diff --git a/Composer/packages/client/package.json b/Composer/packages/client/package.json index 05cd3abdd1..3d39cfecb4 100644 --- a/Composer/packages/client/package.json +++ b/Composer/packages/client/package.json @@ -14,7 +14,7 @@ "babel-plugin-named-asset-import": "^0.3.1", "babel-preset-react-app": "^7.0.1", "bfj": "6.1.1", - "botbuilder-lg": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/botbuilder-lg/-/4.6.6.tgz", + "botbuilder-lg": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/botbuilder-lg/-/4.6.8.tgz", "case-sensitive-paths-webpack-plugin": "2.2.0", "code-editor": "*", "codemirror": "^5.45.0", diff --git a/Composer/packages/client/src/ShellApi.ts b/Composer/packages/client/src/ShellApi.ts index 96fe32302b..4aaea0eb43 100644 --- a/Composer/packages/client/src/ShellApi.ts +++ b/Composer/packages/client/src/ShellApi.ts @@ -74,7 +74,7 @@ export const ShellApi: React.FC = () => { const { dialogs, schemas, lgFiles, luFiles, designPageLocation, focusPath, breadcrumb } = state; const updateDialog = actions.updateDialog; const updateLuFile = actions.updateLuFile; //if debounced, error can't pass to form - const updateLgFile = useDebouncedFunc(actions.updateLgFile); + const updateLgFile = actions.updateLgFile; const updateLgTemplate = useDebouncedFunc(actions.updateLgTemplate); const createLuFile = actions.createLuFile; const createLgFile = actions.createLgFile; diff --git a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx index 9a7ef7cfa2..1ae4faf3e1 100644 --- a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx +++ b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx @@ -72,6 +72,10 @@ const shellApi = { return apiClient.apiCall('updateLuFile', luFile); }, + updateLgFile: (id: string, content: string) => { + return apiClient.apiCall('updateLgFile', { id, content }); + }, + getLgTemplates: (id: string) => { return apiClient.apiCall('getLgTemplates', { id }); }, 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 fbc95713a1..f2bc43c4a7 100644 --- a/Composer/packages/client/src/pages/language-generation/code-editor.js +++ b/Composer/packages/client/src/pages/language-generation/code-editor.js @@ -7,7 +7,7 @@ import { get, debounce, isEmpty } from 'lodash'; import * as lgUtil from '../../utils/lgUtil'; export default function CodeEditor(props) { - const { file } = props; + const { file, codeRange } = props; const onChange = debounce(props.onChange, 500); const [diagnostics, setDiagnostics] = useState(get(file, 'diagnostics', [])); const [content, setContent] = useState(get(file, 'content', '')); @@ -41,6 +41,7 @@ export default function CodeEditor(props) { lineDecorationsWidth: undefined, lineNumbersMinChars: false, }} + codeRange={codeRange} errorMsg={errorMsg} value={content} onChange={_onChange} @@ -51,4 +52,5 @@ export default function CodeEditor(props) { CodeEditor.propTypes = { file: PropTypes.object, onChange: PropTypes.func, + codeRange: PropTypes.object, }; diff --git a/Composer/packages/client/src/pages/language-generation/index.js b/Composer/packages/client/src/pages/language-generation/index.js index 603d461382..15aa13a8b4 100644 --- a/Composer/packages/client/src/pages/language-generation/index.js +++ b/Composer/packages/client/src/pages/language-generation/index.js @@ -2,6 +2,7 @@ import React, { useContext, Fragment, useEffect, useState, useMemo } from 'react import formatMessage from 'format-message'; import { Toggle } from 'office-ui-fabric-react/lib/Toggle'; import { Nav } from 'office-ui-fabric-react/lib/Nav'; +import get from 'lodash.get'; import { OpenAlertModal, DialogStyle } from '../../components/Modal'; import { StoreContext } from '../../store'; @@ -25,6 +26,7 @@ export const LGPage = props => { const { state, actions } = useContext(StoreContext); const { lgFiles, dialogs } = state; const [editMode, setEditMode] = useState(false); + const [codeRange, setCodeRange] = useState(null); const subPath = props['*']; const isRoot = subPath === ''; @@ -96,6 +98,11 @@ export const LGPage = props => { } } + function onToggleEditMode() { + setEditMode(!editMode); + setCodeRange(null); + } + async function onChange(newContent) { const payload = { id: lgFile.id, @@ -113,7 +120,11 @@ export const LGPage = props => { // #TODO: get line number from lg parser, then deep link to code editor this // Line - function onTableViewClickEdit() { + function onTableViewClickEdit(template) { + setCodeRange({ + startLineNumber: get(template, 'ParseTree._start._line', 0), + endLineNumber: get(template, 'ParseTree._stop._line', 0), + }); navigateTo(`/language-generation`); setEditMode(true); } @@ -139,7 +150,7 @@ export const LGPage = props => { offText={formatMessage('Edit mode')} checked={editMode} disabled={!isRoot && editMode === false} - onChange={() => setEditMode(!editMode)} + onChange={onToggleEditMode} /> @@ -175,7 +186,7 @@ export const LGPage = props => {
{editMode ? ( - + ) : ( )} diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LgEditorWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LgEditorWidget.tsx index ffadb62162..9bcfdff186 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LgEditorWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LgEditorWidget.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { useState, useEffect } from 'react'; -import { RichEditor } from 'code-editor'; +import { useState } from 'react'; +import { LgEditor } from 'code-editor'; +import debounce from 'lodash.debounce'; import { FormContext } from '../types'; @@ -8,7 +9,7 @@ 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 || ''; + let newTemplate = formData || '- '; if (newTemplate.includes(`bfd${fieldName}-`)) { return ''; @@ -29,57 +30,50 @@ interface LgEditorWidgetProps { 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); + const lgFileId = formContext.currentDialog.lgFile || 'common'; + const lgFile = formContext.lgFiles.find(file => file.id === lgFileId); + const template = lgFile + ? lgFile.templates.find(template => { + return template.Name === lgId; + }) + : undefined; - if (formContext.dialogId && newTemplate) { - formContext.shellApi.updateLgTemplate('common', lgId, newTemplate); - props.onChange(`[${lgId}]`); + // template body code range + const codeRange = template + ? { + startLineNumber: template.Range.startLineNumber + 1, // cut template name + endLineNumber: template.Range.endLineNumber, } - setTemplateToRender({ Name: `# ${lgId}`, Body: newTemplate }); - } else { - if (templateToRender.Name === '') { - setTemplateToRender({ Name: `# ${lgId}`, Body: template.Body }); - } - } - }; + : -1; + + let content = lgFile ? lgFile.content : ''; + if (!template) { + const newTemplateBody = getInitialTemplate(name, value || '-'); + content += ['\n', '# ' + lgId, newTemplateBody].join('\n'); + } - const onChange = (data): void => { + const onChange = debounce((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); - } + formContext.shellApi + .updateLgFile(lgFileId, data) + .then(() => setErrorMsg('')) + .catch(error => setErrorMsg(error)); + props.onChange(`[${lgId}]`); } - }; - - useEffect(() => { - ensureTemplate(value); - }, [formContext.dialogId]); + }, 200); - const { Body } = templateToRender; - return ; + return ( + + ); }; diff --git a/Composer/packages/extensions/obiformeditor/src/types.ts b/Composer/packages/extensions/obiformeditor/src/types.ts index 9ae38c63a2..19d0875ebe 100644 --- a/Composer/packages/extensions/obiformeditor/src/types.ts +++ b/Composer/packages/extensions/obiformeditor/src/types.ts @@ -50,14 +50,22 @@ export interface LuFile { }; } +export interface CodeRange { + startLineNumber: number; + endLineNumber: number; +} + export interface LgFile { id: string; relativePath: string; content: string; + templates: [LgTemplate]; } export interface LgTemplate { Name: string; Body: string; + Parameters: string; + Range: CodeRange; } export interface FormData { @@ -74,6 +82,7 @@ export interface ShellApi { onFocusEvent: (eventId: string) => Promise; createLuFile: (id: string) => Promise; updateLuFile: (id: string, content: string) => Promise; + updateLgFile: (id: string, content: string) => Promise; getLgTemplates: (id: string) => Promise; createLgTemplate: (id: string, template: LgTemplate, position: number) => Promise; updateLgTemplate: (id: string, templateName: string, templateStr: string) => Promise; diff --git a/Composer/packages/lib/code-editor/demo/src/App.tsx b/Composer/packages/lib/code-editor/demo/src/App.tsx index e2024d1780..7a4a51391a 100644 --- a/Composer/packages/lib/code-editor/demo/src/App.tsx +++ b/Composer/packages/lib/code-editor/demo/src/App.tsx @@ -5,14 +5,24 @@ import { RichEditor } from '../../src'; const LU_HELP = 'https://github.com/Microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md#lu-file-format'; +const content = `# Greeting +-Good morning +-Good afternoon +-Good evening`; + export default function App() { - const [value, setValue] = useState(''); + const [value, setValue] = useState(content); const [showError, setShowError] = useState(true); const placeholder = `> To learn more about the LU file format, read the documentation at > ${LU_HELP}`; const errorMsg = showError ? 'example error' : undefined; + const codeRange = { + startLineNumber: 2, + endLineNumber: 3, + }; + return (
@@ -21,6 +31,7 @@ export default function App() { setValue(newVal)} value={value} + codeRange={codeRange} placeholder={placeholder} errorMsg={errorMsg} helpURL="https://dev.botframework.com" diff --git a/Composer/packages/lib/code-editor/package.json b/Composer/packages/lib/code-editor/package.json index 7a14aaf27a..113a0e9100 100644 --- a/Composer/packages/lib/code-editor/package.json +++ b/Composer/packages/lib/code-editor/package.json @@ -55,7 +55,7 @@ }, "dependencies": { "@bfcomposer/monaco-editor-webpack-plugin": "^1.7.2", - "@bfcomposer/react-monaco-editor": "^0.30.4", + "@bfcomposer/react-monaco-editor": "^0.30.5", "lodash.throttle": "^4.1.1" } } diff --git a/Composer/packages/lib/code-editor/src/BaseEditor.tsx b/Composer/packages/lib/code-editor/src/BaseEditor.tsx index ac3fa29fd8..96bf58b3a5 100644 --- a/Composer/packages/lib/code-editor/src/BaseEditor.tsx +++ b/Composer/packages/lib/code-editor/src/BaseEditor.tsx @@ -20,17 +20,27 @@ const defaultOptions: monacoEditor.editor.IEditorConstructionOptions = { renderLineHighlight: 'none', }; +interface ICodeRange { + startLineNumber: number; + endLineNumber: number; +} + export interface BaseEditorProps extends Omit { onChange: (newValue: string) => void; placeholder?: string; value?: string; + codeRange?: ICodeRange | -1; } export function BaseEditor(props: BaseEditorProps) { - const { onChange, placeholder, value } = props; + const { onChange, placeholder, value, codeRange } = props; const options = Object.assign({}, defaultOptions, props.options); - + if (options.folding && codeRange) { + options.folding = false; + } const containerRef = useRef(null); + // editor.setHiddenAreas is an internal api, not included in , so here mark it + const [editor, setEditor] = useState(null); const [rect, setRect] = useState({ height: 0, width: 0 }); const updateRect = throttle(() => { @@ -60,6 +70,57 @@ export function BaseEditor(props: BaseEditorProps) { updateRect(); }, []); + const updateEditorCodeRangeUI = (editor?: monacoEditor.editor.IStandaloneCodeEditor | any) => { + if (codeRange && editor) { + // subtraction a hiddenAreaRange from CodeRange + // Tips, monaco lineNumber start from 1 + const model = editor.getModel(); + const lineCount = model && model.getLineCount(); + + // -1 is end line of file + if (codeRange === -1) { + editor.setHiddenAreas([ + { + startLineNumber: 1, + endLineNumber: lineCount - 1, + }, + ]); + return; + } + const hiddenRanges = [ + { + startLineNumber: 1, + endLineNumber: codeRange.startLineNumber - 1, + }, + { + startLineNumber: codeRange.endLineNumber + 1, + endLineNumber: lineCount, + }, + ]; + + // code range start from first line, update hiddenRanges + if (codeRange.startLineNumber === 1) { + hiddenRanges.shift(); + } + // code range end at last line, update hiddenRanges + if (codeRange.endLineNumber === lineCount) { + hiddenRanges.pop(); + } + editor.setHiddenAreas(hiddenRanges); + } + }; + useEffect(() => { + updateEditorCodeRangeUI(editor); + }, [codeRange]); + + const editorDidMount = (editor: monacoEditor.editor.IStandaloneCodeEditor, monaco: typeof monacoEditor) => { + if (typeof props.editorDidMount === 'function') { + props.editorDidMount(editor, monaco); + } + setEditor(editor); + updateEditorCodeRangeUI(editor); + }; + return (
- +
); } diff --git a/Composer/packages/lib/code-editor/src/RichEditor.tsx b/Composer/packages/lib/code-editor/src/RichEditor.tsx index 0e2abc81ba..df12d76735 100644 --- a/Composer/packages/lib/code-editor/src/RichEditor.tsx +++ b/Composer/packages/lib/code-editor/src/RichEditor.tsx @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React, { Fragment, useMemo } from 'react'; import { SharedColors, NeutralColors } from '@uifabric/fluent-theme'; import formatMessage from 'format-message'; @@ -28,6 +28,11 @@ export function RichEditor(props: RichEditorProps) { } ); + const baseEditor = ; + // CodeRange editing require an non-controled/refresh component, so here make it memoed + const memoEditor = useMemo(() => { + return baseEditor; + }, []); const getHeight = () => { if (height === null || height === undefined) { return '100%'; @@ -50,7 +55,7 @@ export function RichEditor(props: RichEditorProps) { transition: `border-color 0.1s ${isInvalid ? 'ease-out' : 'ease-in'}`, }} > - + {props.codeRange ? memoEditor : baseEditor}
{isInvalid && (
diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index ea03cd1b7f..9e1d6227ce 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -72,7 +72,7 @@ "axios": "^0.18.0", "azure-storage": "^2.10.3", "body-parser": "^1.18.3", - "botbuilder-lg": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/botbuilder-lg/-/4.6.6.tgz", + "botbuilder-lg": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/botbuilder-lg/-/4.6.8.tgz", "cookie-parser": "^1.4.4", "debug": "^4.1.1", "dotenv": "^8.1.0", diff --git a/Composer/packages/server/src/models/bot/indexers/lgIndexer.ts b/Composer/packages/server/src/models/bot/indexers/lgIndexer.ts index 5739a1145a..59f1e0d700 100644 --- a/Composer/packages/server/src/models/bot/indexers/lgIndexer.ts +++ b/Composer/packages/server/src/models/bot/indexers/lgIndexer.ts @@ -1,4 +1,5 @@ import { LGParser, StaticChecker, DiagnosticSeverity, Diagnostic } from 'botbuilder-lg'; +import get from 'lodash.get'; import { Path } from '../../../utility/path'; import { FileInfo, LGFile, LGTemplate } from '../interface'; @@ -48,7 +49,15 @@ export class LGIndexer { public parse(content: string, id: string): LGTemplate[] { const resource = LGParser.parse(content, id); const templates = resource.Templates.map(t => { - return { Name: t.Name, Body: t.Body, Parameters: t.Parameters }; + return { + 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/server/src/models/bot/interface.ts b/Composer/packages/server/src/models/bot/interface.ts index a56196b169..7a03348805 100644 --- a/Composer/packages/server/src/models/bot/interface.ts +++ b/Composer/packages/server/src/models/bot/interface.ts @@ -26,10 +26,16 @@ export interface Dialog { relativePath: string; } +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/yarn.lock b/Composer/yarn.lock index dffabd8633..86823793d3 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -1432,10 +1432,10 @@ dependencies: "@types/webpack" "^4.4.19" -"@bfcomposer/monaco-editor@^0.17.4": - version "0.17.4" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/monaco-editor/-/@bfcomposer/monaco-editor-0.17.4.tgz#bc2d75c48fcbee1121ee4c168c508de81bcd2ee9" - integrity sha1-vC11xI/L7hEh7kwWjFCN6BvNLuk= +"@bfcomposer/monaco-editor@^0.17.5": + version "0.17.5" + resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/monaco-editor/-/@bfcomposer/monaco-editor-0.17.5.tgz#ceefd34199784d3f2fb745191cafab13bc88a268" + integrity sha1-zu/TQZl4TT8vt0UZHK+rE7yIomg= "@bfcomposer/react-jsonschema-form@1.6.5": version "1.6.5" @@ -1448,12 +1448,12 @@ lodash.topath "^4.5.2" prop-types "^15.5.8" -"@bfcomposer/react-monaco-editor@^0.30.4": - version "0.30.4" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/react-monaco-editor/-/@bfcomposer/react-monaco-editor-0.30.4.tgz#3b7a6c60011218d3bc51f7eca5673341376db728" - integrity sha1-O3psYAESGNO8UffspWczQTdttyg= +"@bfcomposer/react-monaco-editor@^0.30.5": + version "0.30.5" + resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/react-monaco-editor/-/@bfcomposer/react-monaco-editor-0.30.5.tgz#f0936110d43b7cbc72dd4bdf2107cb9895ec84c3" + integrity sha1-8JNhENQ7fLxy3UvfIQfLmJXshMM= dependencies: - "@bfcomposer/monaco-editor" "^0.17.4" + "@bfcomposer/monaco-editor" "^0.17.5" "@types/react" "^16.9.2" prop-types "^15.7.2" @@ -5045,9 +5045,9 @@ botbuilder-expression-parser@^4.5.9: xmldom "^0.1.27" xpath "0.0.27" -"botbuilder-lg@https://botbuilder.myget.org/F/botbuilder-declarative/npm/botbuilder-lg/-/4.6.6.tgz": - version "4.6.6" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/botbuilder-lg/-/4.6.6.tgz#2c699f475c05492239d83ab0863c8c85d369a284" +"botbuilder-lg@https://botbuilder.myget.org/F/botbuilder-declarative/npm/botbuilder-lg/-/4.6.8.tgz": + version "4.6.8" + resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/botbuilder-lg/-/4.6.8.tgz#24367ee35e33efdae7a31e390ba7d19a7d9166da" dependencies: antlr4ts "0.5.0-alpha.1" botbuilder-expression "https://botbuilder.myget.org/F/botbuilder-declarative/npm/botbuilder-expression/-/4.5.9.tgz" diff --git a/SampleBots/ToDoBot/ComposerDialogs/Main/Main.dialog b/SampleBots/ToDoBot/ComposerDialogs/Main/Main.dialog index 6fb5ed85b5..b2140617d8 100644 --- a/SampleBots/ToDoBot/ComposerDialogs/Main/Main.dialog +++ b/SampleBots/ToDoBot/ComposerDialogs/Main/Main.dialog @@ -160,6 +160,9 @@ "actions": [ { "$type": "Microsoft.SendActivity", + "$designer": { + "id": "677440" + }, "activity": "ok." }, { @@ -178,10 +181,13 @@ "actions": [ { "$type": "Microsoft.SendActivity", + "$designer": { + "id": "677448" + }, "activity": "Hi! I'm a ToDo bot. Say \"add a todo named first\" to get started." } ] } ], "$schema": "../../app.schema" -} +} \ No newline at end of file