diff --git a/Composer/packages/client/src/components/EditableField.tsx b/Composer/packages/client/src/components/EditableField.tsx new file mode 100644 index 0000000000..4e449d53ae --- /dev/null +++ b/Composer/packages/client/src/components/EditableField.tsx @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React, { useState, useEffect } from 'react'; +import { TextField, ITextFieldStyles, ITextFieldProps } from 'office-ui-fabric-react/lib/TextField'; +import { NeutralColors } from '@uifabric/fluent-theme'; +import { mergeStyleSets } from '@uifabric/styling'; + +interface EditableFieldProps extends Omit { + fontSize?: string; + styles?: Partial; + transparentBorder?: boolean; + ariaLabel?: string; + error?: string | JSX.Element; + + className?: string; + depth: number; + description?: string; + disabled?: boolean; + id: string; + name: string; + placeholder?: string; + readonly?: boolean; + required?: boolean; + value?: string; + + onChange: (newValue?: string) => void; + onFocus?: (id: string, value?: string) => void; + onBlur?: (id: string, value?: string) => void; +} + +const EditableField: React.FC = (props) => { + const { + depth, + styles = {}, + placeholder, + fontSize, + multiline = false, + onChange, + onBlur, + value, + id, + error, + className, + transparentBorder, + ariaLabel, + } = props; + const [editing, setEditing] = useState(false); + const [hasFocus, setHasFocus] = useState(false); + const [localValue, setLocalValue] = useState(value); + const [hasBeenEdited, setHasBeenEdited] = useState(false); + + useEffect(() => { + if (!hasBeenEdited || value !== localValue) { + setLocalValue(value); + } + }, [value]); + + const handleChange = (_e: any, newValue?: string) => { + setLocalValue(newValue); + setHasBeenEdited(true); + onChange(newValue); + }; + + const handleCommit = () => { + setHasFocus(false); + setEditing(false); + onBlur && onBlur(id, localValue); + }; + + let borderColor: string | undefined = undefined; + + if (!editing && !error) { + borderColor = localValue || transparentBorder || depth > 1 ? 'transparent' : NeutralColors.gray30; + } + + return ( +
setEditing(true)} + onMouseLeave={() => !hasFocus && setEditing(false)} + > + + } + value={localValue} + onBlur={handleCommit} + onChange={handleChange} + onFocus={() => setHasFocus(true)} + /> +
+ ); +}; + +export { EditableField }; diff --git a/Composer/packages/client/src/components/MultiLanguage/AddLanguageModal.tsx b/Composer/packages/client/src/components/MultiLanguage/AddLanguageModal.tsx index 6ccf74b185..5f4c7ea304 100644 --- a/Composer/packages/client/src/components/MultiLanguage/AddLanguageModal.tsx +++ b/Composer/packages/client/src/components/MultiLanguage/AddLanguageModal.tsx @@ -8,6 +8,7 @@ import { jsx } from '@emotion/core'; import React, { useState, useCallback, useMemo, useEffect } from 'react'; import formatMessage from 'format-message'; import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; +import { SearchBox } from 'office-ui-fabric-react/lib/SearchBox'; import { ScrollablePane, IScrollablePaneStyles } from 'office-ui-fabric-react/lib/ScrollablePane'; import { Stack, StackItem } from 'office-ui-fabric-react/lib/Stack'; import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; @@ -39,6 +40,7 @@ const AddLanguageModal: React.FC = (props) => { switchTo: false, }; const [formData, setFormData] = useState(initialFormData); + const [searchKeywords, setSearchKeywords] = useState(''); useEffect(() => { setFormData({ ...formData, defaultLang: defaultLanguage }); @@ -94,27 +96,29 @@ const AddLanguageModal: React.FC = (props) => { const formTitles = { ...MultiLanguagesDialog.ADD_DIALOG }; - const languageCheckBoxList = languageListTemplatesSorted(currentLanguages, locale, defaultLanguage).map((item) => { - const { language, isEnabled, isCurrent, isDefault, locale } = item; - let label = language; - if (isDefault) { - label += formatMessage(' - Original'); - } - if (isCurrent) { - label += formatMessage(' - Current'); - } - return ( - - ); - }); + const languageCheckBoxList = languageListTemplatesSorted(currentLanguages, locale, defaultLanguage) + .filter((item) => item.language.toLowerCase().includes(searchKeywords.toLowerCase())) + .map((item) => { + const { language, isEnabled, isCurrent, isDefault, locale } = item; + let label = language; + if (isDefault) { + label += formatMessage(' - Original'); + } + if (isCurrent) { + label += formatMessage(' - Current'); + } + return ( + + ); + }); const defalutLanguageListOptions = useMemo(() => { const languageList = languageListTemplates(currentLanguages, locale, defaultLanguage); @@ -129,6 +133,10 @@ const AddLanguageModal: React.FC = (props) => { }); }, [currentLanguages]); + const onSearch = (_e, newValue) => { + setSearchKeywords(newValue.trim()); + }; + const scrollablePaneStyles: Partial = { root: classNames.pane }; return ( @@ -149,6 +157,12 @@ const AddLanguageModal: React.FC = (props) => { + {languageCheckBoxList} diff --git a/Composer/packages/client/src/constants.ts b/Composer/packages/client/src/constants.ts index de6181e0d5..e3c24ffc21 100644 --- a/Composer/packages/client/src/constants.ts +++ b/Composer/packages/client/src/constants.ts @@ -126,6 +126,7 @@ export const MultiLanguagesDialog = { 'This language will be copied and used as the basis (and fallback language) for the translation.' ), selectionTitle: formatMessage('To which language will you be translating your bot?'), + searchPlaceHolder: formatMessage('Search'), whenDoneText: formatMessage( 'When done, switch to the newly created language and start the (manual) translation process.' ), 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 a86ac3c232..5b81b1da3e 100644 --- a/Composer/packages/client/src/pages/language-generation/table-view.tsx +++ b/Composer/packages/client/src/pages/language-generation/table-view.tsx @@ -5,7 +5,7 @@ import { jsx } from '@emotion/core'; import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react'; import isEmpty from 'lodash/isEmpty'; -import { DetailsList, DetailsListLayoutMode, SelectionMode } from 'office-ui-fabric-react/lib/DetailsList'; +import { DetailsList, DetailsListLayoutMode, SelectionMode, IColumn } from 'office-ui-fabric-react/lib/DetailsList'; import { ActionButton } from 'office-ui-fabric-react/lib/Button'; import { IconButton } from 'office-ui-fabric-react/lib/Button'; import { Icon } from 'office-ui-fabric-react/lib/Icon'; @@ -17,10 +17,11 @@ import { NeutralColors, FontSizes } from '@uifabric/fluent-theme'; import { RouteComponentProps } from '@reach/router'; import { LgTemplate } from '@bfc/shared'; import { useRecoilValue } from 'recoil'; +import { lgUtil } from '@bfc/indexers'; -import { increaseNameUtilNotExist } from '../../utils/lgUtil'; +import { EditableField } from '../../components/EditableField'; import { navigateTo } from '../../utils/navigation'; -import { actionButton, formCell, content } from '../language-understanding/styles'; +import { actionButton, formCell } from '../language-understanding/styles'; import { dispatcherState, lgFilesState, projectIdState, localeState, settingsState } from '../../recoilModel'; import { languageListTemplates } from '../../components/MultiLanguage'; import { validatedDialogsSelector } from '../../recoilModel/selectors/validatedDialogs'; @@ -35,12 +36,15 @@ const TableView: React.FC = (props) => { const projectId = useRecoilValue(projectIdState); const locale = useRecoilValue(localeState); const settings = useRecoilValue(settingsState); - const { createLgTemplate, copyLgTemplate, removeLgTemplate, setMessage } = useRecoilValue(dispatcherState); + const { createLgTemplate, copyLgTemplate, removeLgTemplate, updateLgTemplate, setMessage } = useRecoilValue( + dispatcherState + ); const { languages, defaultLanguage } = settings; const { dialogId } = props; const file = lgFiles.find(({ id }) => id === `${dialogId}.${locale}`); + const defaultLangFile = lgFiles.find(({ id }) => id === `${dialogId}.${defaultLanguage}`); const [templates, setTemplates] = useState([]); const listRef = useRef(null); @@ -67,7 +71,7 @@ const TableView: React.FC = (props) => { const onCreateNewTemplate = useCallback(() => { if (file) { - const newName = increaseNameUtilNotExist(templates, 'TemplateName'); + const newName = lgUtil.increaseNameUtilNotExist(templates, 'TemplateName'); const payload = { id: file.id, template: { @@ -98,7 +102,7 @@ const TableView: React.FC = (props) => { (index) => { if (file) { const name = templates[index].name; - const resolvedName = increaseNameUtilNotExist(templates, `${name}_Copy`); + const resolvedName = lgUtil.increaseNameUtilNotExist(templates, `${name}_Copy`); const payload = { id: file.id, fromTemplateName: name, @@ -111,6 +115,34 @@ const TableView: React.FC = (props) => { [templates, file, projectId] ); + const handleTemplateUpdate = useCallback( + (templateName: string, template: LgTemplate) => { + if (file) { + const payload = { + id: file.id, + templateName, + template, + }; + updateLgTemplate(payload); + } + }, + [templates, file, projectId] + ); + + const handleTemplateUpdateDefaultLocale = useCallback( + (templateName: string, template: LgTemplate) => { + if (defaultLangFile) { + const payload = { + id: defaultLangFile.id, + templateName, + template, + }; + updateLgTemplate(payload); + } + }, + [templates, file, projectId] + ); + const getTemplatesMoreButtons = useCallback( (item, index) => { const buttons = [ @@ -144,7 +176,7 @@ const TableView: React.FC = (props) => { [activeDialog, templates] ); - const getTableColums = useCallback(() => { + const getTableColums = useCallback((): IColumn[] => { const languagesList = languageListTemplates(languages, locale, defaultLanguage); const defaultLangTeamplate = languagesList.find((item) => item.locale === defaultLanguage); const currentLangTeamplate = languagesList.find((item) => item.locale === locale); @@ -159,15 +191,27 @@ const TableView: React.FC = (props) => { name: formatMessage('Name'), fieldName: 'name', minWidth: 100, - maxWidth: 150, + maxWidth: 200, isResizable: true, data: 'string', onRender: (item) => { + const displayName = `#${item.name}`; return (
-
- #{item.name} -
+ { + const newValue = value?.trim().replace(/^#/, ''); + if (newValue) { + handleTemplateUpdate(item.name, { ...item, name: newValue }); + } + }} + onChange={() => {}} + />
); }, @@ -179,17 +223,25 @@ const TableView: React.FC = (props) => { minWidth: 500, isResizable: true, data: 'string', - isPadded: true, onRender: (item) => { + const text = item.body; return (
-
- {item.body} -
+ { + const newValue = value?.trim(); + if (newValue) { + handleTemplateUpdate(item.name, { ...item, body: newValue }); + } + }} + onChange={() => {}} + />
); }, @@ -199,16 +251,25 @@ const TableView: React.FC = (props) => { name: currentLangResponsesHeader, fieldName: 'responses', minWidth: 300, + maxWidth: 500, isResizable: true, data: 'string', - isPadded: true, onRender: (item) => { const text = item.body; return (
-
- {text} -
+ { + handleTemplateUpdate(item.name, { ...item, body: value }); + }} + onChange={() => {}} + />
); }, @@ -220,14 +281,25 @@ const TableView: React.FC = (props) => { minWidth: 300, isResizable: true, data: 'string', - isPadded: true, onRender: (item) => { const text = item[`body-${defaultLanguage}`]; return (
-
- {text} -
+ { + const newValue = value?.trim(); + if (newValue) { + handleTemplateUpdateDefaultLocale(item.name, { ...item, body: newValue }); + } + }} + onChange={() => {}} + />
); }, @@ -297,7 +369,7 @@ const TableView: React.FC = (props) => { } return tableColums; - }, [activeDialog, templates, projectId]); + }, [activeDialog, projectId]); const onRenderDetailsHeader = useCallback((props, defaultRender) => { return ( @@ -336,10 +408,8 @@ const TableView: React.FC = (props) => { const templatesToRender = useMemo(() => { if (locale !== defaultLanguage) { - const defaultLangTeamplates = lgFiles.find(({ id }) => id === `${dialogId}.${defaultLanguage}`)?.templates; - return templates.map((item) => { - const itemInDefaultLang = defaultLangTeamplates?.find(({ name }) => name === item.name); + const itemInDefaultLang = defaultLangFile?.templates?.find(({ name }) => name === item.name); return { ...item, [`body-${defaultLanguage}`]: itemInDefaultLang?.body || '', diff --git a/Composer/packages/client/src/pages/language-understanding/table-view.tsx b/Composer/packages/client/src/pages/language-understanding/table-view.tsx index 45d24c3d30..de5c062662 100644 --- a/Composer/packages/client/src/pages/language-understanding/table-view.tsx +++ b/Composer/packages/client/src/pages/language-understanding/table-view.tsx @@ -4,10 +4,10 @@ /* eslint-disable react/display-name */ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import { useRef, useEffect, useState, useMemo } from 'react'; +import { useRef, useEffect, useState, useMemo, useCallback } from 'react'; import isEmpty from 'lodash/isEmpty'; import get from 'lodash/get'; -import { DetailsList, DetailsListLayoutMode, SelectionMode } from 'office-ui-fabric-react/lib/DetailsList'; +import { DetailsList, DetailsListLayoutMode, SelectionMode, IColumn } from 'office-ui-fabric-react/lib/DetailsList'; import { Link } from 'office-ui-fabric-react/lib/Link'; import { IconButton } from 'office-ui-fabric-react/lib/Button'; import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu'; @@ -20,13 +20,14 @@ import { RouteComponentProps } from '@reach/router'; import { useRecoilValue } from 'recoil'; import { LuFile, LuIntentSection } from '@bfc/shared'; +import { EditableField } from '../../components/EditableField'; import { getExtension } from '../../utils/fileUtil'; import { languageListTemplates } from '../../components/MultiLanguage'; -import { luFilesState, projectIdState, localeState, settingsState } from '../../recoilModel/atoms/botState'; +import { dispatcherState, luFilesState, projectIdState, localeState, settingsState } from '../../recoilModel'; import { navigateTo } from '../../utils/navigation'; import { validatedDialogsSelector } from '../../recoilModel/selectors/validatedDialogs'; -import { formCell, luPhraseCell, tableCell, content } from './styles'; +import { formCell, luPhraseCell, tableCell } from './styles'; interface TableViewProps extends RouteComponentProps<{}> { dialogId: string; } @@ -46,11 +47,15 @@ const TableView: React.FC = (props) => { const projectId = useRecoilValue(projectIdState); const locale = useRecoilValue(localeState); const settings = useRecoilValue(settingsState); + const { updateLuIntent } = useRecoilValue(dispatcherState); const { languages, defaultLanguage } = settings; const { dialogId } = props; const activeDialog = dialogs.find(({ id }) => id === dialogId); + const file = luFiles.find(({ id }) => id === `${dialogId}.${locale}`); + const defaultLangFile = luFiles.find(({ id }) => id === `${dialogId}.${defaultLanguage}`); + const [intents, setIntents] = useState([]); const listRef = useRef(null); @@ -98,6 +103,34 @@ const TableView: React.FC = (props) => { } }, [luFiles, activeDialog, projectId]); + const handleIntentUpdate = useCallback( + (intentName: string, intent: LuIntentSection) => { + if (file) { + const payload = { + id: file.id, + intentName, + intent, + }; + updateLuIntent(payload); + } + }, + [intents, file, projectId] + ); + + const handleTemplateUpdateDefaultLocale = useCallback( + (intentName: string, intent: LuIntentSection) => { + if (defaultLangFile) { + const payload = { + id: defaultLangFile.id, + intentName, + intent, + }; + updateLuIntent(payload); + } + }, + [intents, file, projectId] + ); + const getTemplatesMoreButtons = (item, index): IContextualMenuItem[] => { const buttons = [ { @@ -112,7 +145,7 @@ const TableView: React.FC = (props) => { return buttons; }; - const getTableColums = () => { + const getTableColums = (): IColumn[] => { const languagesList = languageListTemplates(languages, locale, defaultLanguage); const defaultLangTeamplate = languagesList.find((item) => item.locale === defaultLanguage); const currentLangTeamplate = languagesList.find((item) => item.locale === locale); @@ -127,19 +160,28 @@ const TableView: React.FC = (props) => { name: formatMessage('Intent'), fieldName: 'name', minWidth: 100, - maxWidth: 150, + maxWidth: 200, + isResizable: true, data: 'string', onRender: (item: Intent) => { - let displayName = `#${item.name}`; - if (item.name.includes('/')) { - const [, childName] = item.name.split('/'); - displayName = `##${childName}`; - } + const displayName = `#${item.name}`; return (
-
- {displayName} -
+ { + const newValue = value?.trim().replace(/^#/, ''); + if (newValue) { + handleIntentUpdate(item.name, { Name: newValue, Body: item.phrases }); + } + }} + onChange={() => {}} + />
); }, @@ -148,20 +190,28 @@ const TableView: React.FC = (props) => { key: 'phrases', name: formatMessage('Sample Phrases'), fieldName: 'phrases', - minWidth: 100, - maxWidth: 500, + minWidth: 500, isResizable: true, data: 'string', onRender: (item) => { + const text = item.phrases; return (
-
- {item.phrases} -
+ { + const newValue = value?.trim(); + if (newValue) { + handleIntentUpdate(item.name, { Name: item.name, Body: newValue }); + } + }} + onChange={() => {}} + />
); }, @@ -170,7 +220,7 @@ const TableView: React.FC = (props) => { key: 'phrases-lang', name: currentLangResponsesHeader, fieldName: 'phrases', - minWidth: 100, + minWidth: 300, maxWidth: 500, isResizable: true, data: 'string', @@ -178,13 +228,21 @@ const TableView: React.FC = (props) => { const text = item.phrases; return (
-
- {text} -
+ { + const newValue = value?.trim().replace(/^#/, ''); + if (newValue) { + handleIntentUpdate(item.name, { Name: item.name, Body: newValue }); + } + }} + onChange={() => {}} + />
); }, @@ -193,21 +251,31 @@ const TableView: React.FC = (props) => { key: 'phrases-default-lang', name: defaultLangResponsesHeader, fieldName: 'phrases-default-lang', - minWidth: 100, - maxWidth: 500, + minWidth: 300, isResizable: true, data: 'string', onRender: (item) => { const text = item[`body-${defaultLanguage}`]; return (
-
- {text} -
+ { + const newValue = value?.trim().replace(/^#/, ''); + if (newValue) { + handleTemplateUpdateDefaultLocale(item.name, { + Name: item.name, + Body: newValue, + }); + } + }} + onChange={() => {}} + />
); }, @@ -288,7 +356,7 @@ const TableView: React.FC = (props) => { onRender: (item) => { return (
-
+
{item.state}
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialog.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialog.test.tsx index 3953f2cd42..3f07a5deab 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialog.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialog.test.tsx @@ -41,6 +41,18 @@ jest.mock('@bfc/indexers', () => { content, }), }, + lgUtil: { + parse: (id, content) => ({ + id, + content, + }), + }, + luUtil: { + parse: (id, content) => ({ + id, + content, + }), + }, }; }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lu.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lu.test.tsx index 2179a742ce..319e8430a8 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lu.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lu.test.tsx @@ -82,7 +82,6 @@ describe('Lu dispatcher', () => { id: luFiles[0].id, intentName: 'Hello', intent: getLuIntent('Hello', '-IntentValue'), - projectId: '', }); }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts index 22892198df..a791a57d23 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts @@ -5,12 +5,12 @@ import { LgTemplate, LgFile, importResolverGenerator } from '@bfc/shared'; import { useRecoilCallback, CallbackInterface } from 'recoil'; import differenceBy from 'lodash/differenceBy'; import formatMessage from 'format-message'; +import { lgUtil } from '@bfc/indexers'; import { getBaseName, getExtension } from '../../utils/fileUtil'; import LgWorker from './../parsers/lgWorker'; import { lgFilesState, localeState, settingsState } from './../atoms/botState'; -import * as lgUtil from './../../utils/lgUtil'; const templateIsNotEmpty = ({ name, body }) => { return !!name && !!body; @@ -150,8 +150,37 @@ export const lgDispatcher = () => { set(lgFilesState, (lgFiles) => { const lgFile = lgFiles.find((file) => file.id === id); if (!lgFile) return lgFiles; - const updatedFile = lgUtil.updateTemplate(lgFile, templateName, template, lgFileResolver(lgFiles)); - return updateLgFileState(lgFiles, updatedFile); + const sameIdOtherLocaleFiles = lgFiles.filter((file) => getBaseName(file.id) === getBaseName(id)); + + // name change, need update cross multi locale file. + if (template.name !== templateName) { + const changes: LgFile[] = []; + for (const item of sameIdOtherLocaleFiles) { + const updatedFile = lgUtil.updateTemplate( + item, + templateName, + { name: template.name }, + lgFileResolver(lgFiles) + ); + changes.push(updatedFile); + } + return lgFiles.map((file) => { + const changedFile = changes.find(({ id }) => id === file.id); + return changedFile ? changedFile : file; + }); + + // body change, only update current locale file + } else { + const updatedFile = lgUtil.updateTemplate( + lgFile, + templateName, + { body: template.body }, + lgFileResolver(lgFiles) + ); + return lgFiles.map((file) => { + return file.id === id ? updatedFile : file; + }); + } }); } ); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lu.ts b/Composer/packages/client/src/recoilModel/dispatchers/lu.ts index 7adf900b39..3184cd79e9 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/lu.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/lu.ts @@ -5,10 +5,11 @@ import { LuFile, LuIntentSection } from '@bfc/shared'; import { useRecoilCallback, CallbackInterface } from 'recoil'; import differenceBy from 'lodash/differenceBy'; import formatMessage from 'format-message'; +import { luUtil } from '@bfc/indexers'; import luWorker from '../parsers/luWorker'; import { getBaseName, getExtension } from '../../utils/fileUtil'; -import * as luUtil from '../../utils/luUtil'; +import { checkLuisPublish, createCrossTrainConfig } from '../../utils/luUtil'; import luFileStatusStorage from '../../utils/luFileStatusStorage'; import { luFilesState, @@ -146,18 +147,38 @@ export const luDispatcher = () => { id, intentName, intent, - projectId, }: { id: string; intentName: string; intent: LuIntentSection; - projectId: string; }) => { set(luFilesState, (luFiles) => { - const file = luFiles.find((temp) => temp.id === id); - if (!file) return luFiles; - const updatedFile = luUtil.updateIntent(file, intentName, intent); - return updateLuFileState(luFiles, updatedFile, projectId); + const luFile = luFiles.find((temp) => temp.id === id); + if (!luFile) return luFiles; + // const updatedFile = luUtil.updateIntent(file, intentName, intent); + // return updateLuFileState(luFiles, updatedFile, projectId); + + const sameIdOtherLocaleFiles = luFiles.filter((file) => getBaseName(file.id) === getBaseName(id)); + + // name change, need update cross multi locale file. + if (intent.Name !== intentName) { + const changes: LuFile[] = []; + for (const item of sameIdOtherLocaleFiles) { + const updatedFile = luUtil.updateIntent(item, intentName, { Name: intent.Name }); + changes.push(updatedFile); + } + return luFiles.map((file) => { + const changedFile = changes.find(({ id }) => id === file.id); + return changedFile ? changedFile : file; + }); + + // body change, only update current locale file + } else { + const updatedFile = luUtil.updateIntent(luFile, intentName, { Body: intent.Body }); + return luFiles.map((file) => { + return file.id === id ? updatedFile : file; + }); + } }); } ); @@ -205,9 +226,9 @@ export const luDispatcher = () => { const dialogs = await snapshot.getPromise(dialogsState); try { const luFiles = await snapshot.getPromise(luFilesState); - const referred = luUtil.checkLuisPublish(luFiles, dialogs); + const referred = checkLuisPublish(luFiles, dialogs); //TODO crosstrain should add locale - const crossTrainConfig = luUtil.createCrossTrainConfig(dialogs, referred); + const crossTrainConfig = createCrossTrainConfig(dialogs, referred); await httpClient.post(`/projects/${projectId}/luFiles/publish`, { luisConfig, projectId, diff --git a/Composer/packages/lib/indexers/src/utils/luUtil.ts b/Composer/packages/lib/indexers/src/utils/luUtil.ts index 2bef9426ca..5008a6c1e7 100644 --- a/Composer/packages/lib/indexers/src/utils/luUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/luUtil.ts @@ -170,13 +170,16 @@ function updateInSections( * @param intentName intent Name, support subSection naming 'CheckEmail/CheckUnreadEmail'. if #CheckEmail not exist will do recursive add. * @param {Name, Body} intent the updates. if intent is empty will do remove. */ -export function updateIntent(luFile: LuFile, intentName: string, intent: LuIntentSection | null): LuFile { +export function updateIntent( + luFile: LuFile, + intentName: string, + intent: { Name?: string; Body?: string } | null +): LuFile { let targetSection; let targetSectionContent; const { id, resource } = luFile; - - const updatedSectionContent = textFromIntent(intent); const { Sections } = resource; + // if intent is null, do remove // and if remove target not exist return origin content; if (!intent || isEmpty(intent)) { @@ -198,20 +201,27 @@ export function updateIntent(luFile: LuFile, intentName: string, intent: LuInten } } + const orginSection = Sections.find(({ Name }) => Name === intentName); + const intentToUpdate: LuIntentSection = { + ...orginSection, + Name: intent?.Name || orginSection?.Name || '', + Body: intent?.Body || orginSection?.Body || '', + }; + // nestedSection name path if (intentName.includes('/')) { const [parrentName, childName] = intentName.split('/'); targetSection = Sections.find(({ Name }) => Name === parrentName); if (targetSection) { - const updatedSections = updateInSections(targetSection.SimpleIntentSections, childName, intent); + const updatedSections = updateInSections(targetSection.SimpleIntentSections, childName, intentToUpdate); targetSectionContent = textFromIntent({ Name: targetSection.Name, Body: textFromIntents(updatedSections, 2) }); } else { - targetSectionContent = textFromIntent({ Name: parrentName, Body: textFromIntent(intent, 2) }); + targetSectionContent = textFromIntent({ Name: parrentName, Body: textFromIntent(intentToUpdate, 2) }); } } else { targetSection = Sections.find(({ Name }) => Name === intentName); - targetSectionContent = updatedSectionContent; + targetSectionContent = textFromIntent(intentToUpdate); } let newResource; @@ -230,14 +240,14 @@ export function updateIntent(luFile: LuFile, intentName: string, intent: LuInten * @param content origin lu file content * @param {Name, Body} intent the adds. Name support subSection naming 'CheckEmail/CheckUnreadEmail', if #CheckEmail not exist will do recursive add. */ -export function addIntent(luFile: LuFile, { Name, Body, Entities }: LuIntentSection): LuFile { +export function addIntent(luFile: LuFile, { Name, Body }: LuIntentSection): LuFile { const intentName = Name; if (Name.includes('/')) { const [, childName] = Name.split('/'); Name = childName; } // If the invoker doesn't want to carry Entities, don't pass Entities in. - return updateIntent(luFile, intentName, { Name, Body, Entities }); + return updateIntent(luFile, intentName, { Name, Body }); } export function addIntents(luFile: LuFile, intents: LuIntentSection[]): LuFile {