From dd15db44a003b570a7a4f92614494fa7c61dd735 Mon Sep 17 00:00:00 2001 From: leilzh Date: Fri, 8 Jan 2021 08:09:20 +0800 Subject: [PATCH 01/12] async selector --- .../src/recoilModel/dispatchers/dialogs.ts | 14 +++++++-- .../src/recoilModel/dispatchers/project.ts | 1 + .../recoilModel/dispatchers/utils/project.ts | 25 ++++++++++++---- .../client/src/recoilModel/parsers/types.ts | 25 +++++++++++++++- .../src/recoilModel/parsers/validateWorker.ts | 14 +++++++++ .../parsers/workers/validator.worker.ts | 29 +++++++++++++++++++ .../recoilModel/selectors/validatedDialogs.ts | 14 ++++++--- 7 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 Composer/packages/client/src/recoilModel/parsers/validateWorker.ts create mode 100644 Composer/packages/client/src/recoilModel/parsers/workers/validator.worker.ts diff --git a/Composer/packages/client/src/recoilModel/dispatchers/dialogs.ts b/Composer/packages/client/src/recoilModel/dispatchers/dialogs.ts index 12c61e6422..0f648f7fee 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/dialogs.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/dialogs.ts @@ -2,8 +2,8 @@ // Licensed under the MIT License. /* eslint-disable react-hooks/rules-of-hooks */ import { useRecoilCallback, CallbackInterface } from 'recoil'; -import { dialogIndexer, autofixReferInDialog, validateDialog } from '@bfc/indexers'; -import { DialogInfo, checkForPVASchema } from '@bfc/shared'; +import { dialogIndexer, autofixReferInDialog } from '@bfc/indexers'; +import { DialogInfo, checkForPVASchema, Diagnostic } from '@bfc/shared'; import { lgFilesState, @@ -18,6 +18,7 @@ import { } from '../atoms'; import { dispatcherState } from '../DispatcherWrapper'; +import validateWorker from './../parsers/validateWorker'; import { createLgFileState, removeLgFileState } from './lg'; import { createLuFileState, removeLuFileState } from './lu'; import { createQnAFileState, removeQnAFileState } from './qna'; @@ -87,7 +88,14 @@ export const dialogsDispatcher = () => { const luFiles = await snapshot.getPromise(luFilesState(projectId)); const settings = await snapshot.getPromise(settingsState(projectId)); const dialog = { isRoot: false, displayName: id, ...dialogIndexer.parse(id, fixedContent) }; - dialog.diagnostics = validateDialog(dialog, schemas.sdk.content, settings, lgFiles, luFiles); + dialog.diagnostics = (await validateWorker.validateDialog({ + dialog, + schema: schemas.sdk.content, + settings, + lgFiles, + luFiles, + })) as Diagnostic[]; + if (typeof dialog.content === 'object') { dialog.content.id = id; } diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 86b235aad7..84f157c32a 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -251,6 +251,7 @@ export const projectDispatcher = () => { callback(projectId); } } catch (ex) { + console.log(ex); set(botProjectIdsState, []); removeRecentProject(callbackHelpers, path); handleProjectFailure(callbackHelpers, ex); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 91a15653fd..584441c7bc 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -3,7 +3,7 @@ import path from 'path'; -import { indexer, validateDialog } from '@bfc/indexers'; +import { indexer } from '@bfc/indexers'; import { BotProjectFile, BotProjectSpace, @@ -20,6 +20,7 @@ import { SensitiveProperties, RootBotManagedProperties, defaultPublishConfig, + Diagnostic, } from '@bfc/shared'; import formatMessage from 'format-message'; import camelCase from 'lodash/camelCase'; @@ -83,6 +84,7 @@ import { undoHistoryState } from '../../undo/history'; import UndoHistory from '../../undo/undoHistory'; import { logMessage, setError } from '../shared'; import { setRootBotSettingState } from '../setting'; +import validateWorker from '../../parsers/validateWorker'; import { crossTrainConfigState } from './../../atoms/botState'; import { recognizersSelectorFamily } from './../../selectors/recognizers'; @@ -357,14 +359,25 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any let mainDialog = ''; const dialogIds: string[] = []; - dialogs.forEach((dialog) => { + + for (const dialog of dialogs) { if (dialog.isRoot) { mainDialog = dialog.id; } - dialog.diagnostics = validateDialog(dialog, schemas.sdk.content, settings, lgFiles, luFiles); - set(dialogState({ projectId, dialogId: dialog.id }), dialog); - dialogIds.push(dialog.id); - }); + try { + dialog.diagnostics = (await validateWorker.validateDialog({ + dialog, + schema: schemas.sdk.content, + settings, + lgFiles, + luFiles, + })) as Diagnostic[]; + set(dialogState({ projectId, dialogId: dialog.id }), dialog); + dialogIds.push(dialog.id); + } catch (error) { + console.log(error); + } + } set(dialogIdsState(projectId), dialogIds); set(recognizersSelectorFamily(projectId), recognizers); diff --git a/Composer/packages/client/src/recoilModel/parsers/types.ts b/Composer/packages/client/src/recoilModel/parsers/types.ts index a5e0c484fb..3d164fb6df 100644 --- a/Composer/packages/client/src/recoilModel/parsers/types.ts +++ b/Composer/packages/client/src/recoilModel/parsers/types.ts @@ -1,6 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { LuIntentSection, LgFile, LuFile, QnASection, FileInfo, LgTemplate, ILUFeaturesConfig } from '@bfc/shared'; +import { + LuIntentSection, + LgFile, + LuFile, + QnASection, + FileInfo, + LgTemplate, + ILUFeaturesConfig, + DialogInfo, + DialogSetting, + SchemaDefinitions, +} from '@bfc/shared'; export type LuParsePayload = { id: string; @@ -144,3 +155,15 @@ export enum QnAActionType { UpdateSection = 'update-section', RemoveSection = 'remove-section', } + +export type ValidatorPayload = { + dialog: DialogInfo; + schema: SchemaDefinitions; + settings: DialogSetting; + lgFiles: LgFile[]; + luFiles: LuFile[]; +}; + +export enum ValidateActionType { + DialogValidator = 'dialog', +} diff --git a/Composer/packages/client/src/recoilModel/parsers/validateWorker.ts b/Composer/packages/client/src/recoilModel/parsers/validateWorker.ts new file mode 100644 index 0000000000..b97a32cbe4 --- /dev/null +++ b/Composer/packages/client/src/recoilModel/parsers/validateWorker.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import Worker from './workers/validator.worker.ts'; +import { BaseWorker } from './baseWorker'; +import { ValidateActionType, ValidatorPayload } from './types'; + +// Wrapper class +class ValidateWorker extends BaseWorker { + validateDialog(data: ValidatorPayload) { + return this.sendMsg(ValidateActionType.DialogValidator, data); + } +} + +export default new ValidateWorker(new Worker()); diff --git a/Composer/packages/client/src/recoilModel/parsers/workers/validator.worker.ts b/Composer/packages/client/src/recoilModel/parsers/workers/validator.worker.ts new file mode 100644 index 0000000000..2e70e87d89 --- /dev/null +++ b/Composer/packages/client/src/recoilModel/parsers/workers/validator.worker.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { validateDialog } from '@bfc/indexers'; + +import { ValidateActionType, ValidatorPayload } from '../types'; + +const ctx: Worker = self as any; + +type Message = { + id: string; + type: ValidateActionType.DialogValidator; + payload: ValidatorPayload; +}; + +export const handleMessage = (msg: Message) => { + const { dialog, schema, settings, lgFiles, luFiles } = msg.payload; + return validateDialog(dialog, schema, settings, lgFiles, luFiles); +}; + +ctx.onmessage = function (event) { + const msg = event.data as Message; + + try { + const payload = handleMessage(msg); + ctx.postMessage({ id: msg.id, payload }); + } catch (error) { + ctx.postMessage({ id: msg.id, error: error.message }); + } +}; diff --git a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts index c6b829043d..e3217e3d44 100644 --- a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts +++ b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts @@ -2,8 +2,7 @@ // Licensed under the MIT License. import { selectorFamily } from 'recoil'; -import { validateDialog } from '@bfc/indexers'; -import { DialogInfo, BotSchemas, LgFile, LuFile, DialogSetting, RecognizerFile } from '@bfc/shared'; +import { DialogInfo, BotSchemas, LgFile, LuFile, DialogSetting, RecognizerFile, Diagnostic } from '@bfc/shared'; import { botProjectIdsState, @@ -17,11 +16,12 @@ import { import { getLuProvider } from '../../utils/dialogUtil'; import { recognizersSelectorFamily } from './recognizers'; +import validateWorker from './../parsers/validateWorker'; type validateDialogSelectorFamilyParams = { projectId: string; dialogId: string }; const validateDialogSelectorFamily = selectorFamily({ key: 'validateDialogSelectorFamily', - get: ({ projectId, dialogId }: validateDialogSelectorFamilyParams) => ({ get }) => { + get: ({ projectId, dialogId }: validateDialogSelectorFamilyParams) => async ({ get }) => { const dialog: DialogInfo = get(dialogState({ projectId, dialogId })); const schemas: BotSchemas = get(schemasState(projectId)); const lgFiles: LgFile[] = get(lgFilesState(projectId)); @@ -31,7 +31,13 @@ const validateDialogSelectorFamily = selectorFamily({ const luProvider = getLuProvider(dialogId, recognizers); return { ...dialog, - diagnostics: validateDialog(dialog, schemas.sdk.content, settings, lgFiles, luFiles), + diagnostics: (await validateWorker.validateDialog({ + dialog, + schema: schemas.sdk.content, + settings, + lgFiles, + luFiles, + })) as Diagnostic[], luProvider, }; }, From 1e64054bf422009ba7f980ed6189473b029c1f23 Mon Sep 17 00:00:00 2001 From: leilzh Date: Mon, 11 Jan 2021 15:15:20 +0800 Subject: [PATCH 02/12] update lg files state --- .../client/src/Onboarding/Onboarding.tsx | 4 +- .../BotRuntimeController/publishDialog.tsx | 4 +- .../ProjectTree/TriggerCreationModal.tsx | 4 +- .../packages/client/src/hooks/useResolver.ts | 5 +- .../botProject/RootBotExternalService.tsx | 4 +- .../botProject/SkillBotExternalService.tsx | 4 +- .../client/src/pages/design/DesignPage.tsx | 4 +- .../client/src/pages/design/VisualEditor.tsx | 9 +- .../src/pages/design/createDialogModal.tsx | 4 +- .../content/SelectDialogs.tsx | 6 +- .../src/pages/language-generation/LGPage.tsx | 4 +- .../pages/language-generation/code-editor.tsx | 5 +- .../pages/language-generation/table-view.tsx | 13 +- .../pages/language-understanding/LUPage.tsx | 4 +- .../language-understanding/table-view.tsx | 10 +- .../src/recoilModel/DispatcherWrapper.tsx | 4 +- .../client/src/recoilModel/atoms/botState.ts | 23 ++- .../src/recoilModel/dispatchers/dialogs.ts | 16 +- .../client/src/recoilModel/dispatchers/lg.ts | 167 +++++++++--------- .../src/recoilModel/dispatchers/multilang.ts | 6 +- .../src/recoilModel/dispatchers/trigger.ts | 5 +- .../recoilModel/dispatchers/utils/project.ts | 21 +-- .../selectors/diagnosticsPageSelector.ts | 29 +-- .../recoilModel/selectors/dialogImports.ts | 6 +- .../client/src/recoilModel/selectors/lg.ts | 27 +++ .../selectors/localRuntimeBuilder.ts | 8 +- .../src/recoilModel/selectors/project.ts | 10 +- .../recoilModel/selectors/validatedDialogs.ts | 59 ++----- .../src/recoilModel/undo/trackedAtoms.ts | 5 +- .../packages/client/src/shell/useShell.ts | 8 +- 30 files changed, 225 insertions(+), 253 deletions(-) create mode 100644 Composer/packages/client/src/recoilModel/selectors/lg.ts diff --git a/Composer/packages/client/src/Onboarding/Onboarding.tsx b/Composer/packages/client/src/Onboarding/Onboarding.tsx index 0259a12a9a..2a80ba13f7 100644 --- a/Composer/packages/client/src/Onboarding/Onboarding.tsx +++ b/Composer/packages/client/src/Onboarding/Onboarding.tsx @@ -9,7 +9,7 @@ import { OpenConfirmModal } from '@bfc/ui-shared'; import onboardingStorage from '../utils/onboardingStorage'; import { useLocation } from '../utils/hooks'; -import { dispatcherState, onboardingState, botProjectIdsState, validateDialogsSelectorFamily } from '../recoilModel'; +import { dispatcherState, onboardingState, botProjectIdsState, dialogsSelectorFamily } from '../recoilModel'; import OnboardingContext from './OnboardingContext'; import TeachingBubbles from './TeachingBubbles/TeachingBubbles'; @@ -22,7 +22,7 @@ const Onboarding: React.FC = () => { const didMount = useRef(false); const botProjects = useRecoilValue(botProjectIdsState); const rootBotProjectId = botProjects[0]; - const dialogs = useRecoilValue(validateDialogsSelectorFamily(rootBotProjectId)); + const dialogs = useRecoilValue(dialogsSelectorFamily(rootBotProjectId)); const { onboardingSetComplete } = useRecoilValue(dispatcherState); const onboarding = useRecoilValue(onboardingState); const complete = onboarding.complete; diff --git a/Composer/packages/client/src/components/BotRuntimeController/publishDialog.tsx b/Composer/packages/client/src/components/BotRuntimeController/publishDialog.tsx index 7130024b0d..ac076566d0 100644 --- a/Composer/packages/client/src/components/BotRuntimeController/publishDialog.tsx +++ b/Composer/packages/client/src/components/BotRuntimeController/publishDialog.tsx @@ -22,7 +22,7 @@ import { Text, Tips, Links, nameRegex, LUIS_REGIONS } from '../../constants'; import { FieldConfig, useForm } from '../../hooks/useForm'; import { getReferredQnaFiles } from '../../utils/qnaUtil'; import { getLuisBuildLuFiles } from '../../utils/luUtil'; -import { luFilesState, qnaFilesState, validateDialogsSelectorFamily } from '../../recoilModel'; +import { luFilesState, qnaFilesState, dialogsSelectorFamily } from '../../recoilModel'; // -------------------- Styles -------------------- // const textFieldLabel = css` @@ -90,7 +90,7 @@ interface IPublishDialogProps { export const PublishDialog: React.FC = (props) => { const { isOpen, onDismiss, onPublish, botName, config, projectId } = props; - const dialogs = useRecoilValue(validateDialogsSelectorFamily(projectId)); + const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); const luFiles = useRecoilValue(luFilesState(projectId)); const qnaFiles = useRecoilValue(qnaFilesState(projectId)); const qnaConfigShow = getReferredQnaFiles(qnaFiles, dialogs).length > 0; diff --git a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx index 948b967db2..0f20a0ba41 100644 --- a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx +++ b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx @@ -38,7 +38,7 @@ import { import { schemasState, userSettingsState } from '../../recoilModel/atoms'; import { nameRegex } from '../../constants'; import { isRegExRecognizerType, isLUISnQnARecognizerType } from '../../utils/dialogValidator'; -import { validateDialogsSelectorFamily } from '../../recoilModel'; +import { dialogsSelectorFamily } from '../../recoilModel'; import TelemetryClient from '../../telemetry/TelemetryClient'; // -------------------- Styles -------------------- // @@ -210,7 +210,7 @@ interface TriggerCreationModalProps { export const TriggerCreationModal: React.FC = (props) => { const { isOpen, onDismiss, onSubmit, dialogId, projectId } = props; - const dialogs = useRecoilValue(validateDialogsSelectorFamily(projectId)); + const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); const schemas = useRecoilValue(schemasState(projectId)); const userSettings = useRecoilValue(userSettingsState); const dialogFile = dialogs.find((dialog) => dialog.id === dialogId); diff --git a/Composer/packages/client/src/hooks/useResolver.ts b/Composer/packages/client/src/hooks/useResolver.ts index f9eb2cf9c4..ac89aa885c 100644 --- a/Composer/packages/client/src/hooks/useResolver.ts +++ b/Composer/packages/client/src/hooks/useResolver.ts @@ -4,12 +4,13 @@ import { useRef } from 'react'; import { lgImportResolverGenerator } from '@bfc/shared'; import { useRecoilValue } from 'recoil'; -import { dialogsSelectorFamily, luFilesState, lgFilesState, localeState, qnaFilesState } from '../recoilModel'; +import { dialogsSelectorFamily, luFilesState, localeState, qnaFilesState } from '../recoilModel'; +import { lgFilesSelectorFamily } from '../recoilModel/selectors/lg'; export const useResolvers = (projectId: string) => { const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); const luFiles = useRecoilValue(luFilesState(projectId)); - const lgFiles = useRecoilValue(lgFilesState(projectId)); + const lgFiles = useRecoilValue(lgFilesSelectorFamily(projectId)); const locale = useRecoilValue(localeState(projectId)); const qnaFiles = useRecoilValue(qnaFilesState(projectId)); diff --git a/Composer/packages/client/src/pages/botProject/RootBotExternalService.tsx b/Composer/packages/client/src/pages/botProject/RootBotExternalService.tsx index d455f706cc..ecebaeb300 100644 --- a/Composer/packages/client/src/pages/botProject/RootBotExternalService.tsx +++ b/Composer/packages/client/src/pages/botProject/RootBotExternalService.tsx @@ -22,7 +22,7 @@ import { settingsState, luFilesState, qnaFilesState, - validateDialogsSelectorFamily, + dialogsSelectorFamily, botDisplayNameState, } from '../../recoilModel'; import settingStorage from '../../utils/dialogSettingStorage'; @@ -171,7 +171,7 @@ export const RootBotExternalService: React.FC = (pr const groupQnAKey = get(sensitiveGroupManageProperty, 'qna.subscriptionKey', {}); const rootqnaKey = groupQnAKey.root; - const dialogs = useRecoilValue(validateDialogsSelectorFamily(projectId)); + const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); const luFiles = useRecoilValue(luFilesState(projectId)); const qnaFiles = useRecoilValue(qnaFilesState(projectId)); const botName = useRecoilValue(botDisplayNameState(projectId)); diff --git a/Composer/packages/client/src/pages/botProject/SkillBotExternalService.tsx b/Composer/packages/client/src/pages/botProject/SkillBotExternalService.tsx index 8d240b9551..53b535753f 100644 --- a/Composer/packages/client/src/pages/botProject/SkillBotExternalService.tsx +++ b/Composer/packages/client/src/pages/botProject/SkillBotExternalService.tsx @@ -17,7 +17,7 @@ import { settingsState, luFilesState, qnaFilesState, - validateDialogsSelectorFamily, + dialogsSelectorFamily, botDisplayNameState, } from '../../recoilModel'; import settingStorage from '../../utils/dialogSettingStorage'; @@ -71,7 +71,7 @@ export const SkillBotExternalService: React.FC = ( const rootQnAKey = groupQnAKey.root; const skillQnAKey = groupQnAKey[projectId]; - const dialogs = useRecoilValue(validateDialogsSelectorFamily(projectId)); + const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); const luFiles = useRecoilValue(luFilesState(projectId)); const qnaFiles = useRecoilValue(qnaFilesState(projectId)); diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 6ba2579986..56daf9c9a1 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -33,7 +33,7 @@ import { userSettingsState, dispatcherState, schemasState, - validateDialogsSelectorFamily, + dialogsSelectorFamily, focusPathState, showCreateDialogModalState, localeState, @@ -133,7 +133,7 @@ const DesignPage: React.FC = (props) => { const { openNewTriggerModal, onFocus, onBlur, isRemoteSkill } = props; const [triggerButtonVisible, setTriggerButtonVisibility] = useState(false); const { onboardingAddCoachMarkRef } = useRecoilValue(dispatcherState); - const dialogs = useRecoilValue(validateDialogsSelectorFamily(projectId)); + const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); const schemas = useRecoilValue(schemasState(projectId)); const designPageLocation = useRecoilValue(designPageLocationState(projectId)); const { dialogId, selected } = designPageLocation; diff --git a/Composer/packages/client/src/pages/design/createDialogModal.tsx b/Composer/packages/client/src/pages/design/createDialogModal.tsx index efaa623203..987f28ccdd 100644 --- a/Composer/packages/client/src/pages/design/createDialogModal.tsx +++ b/Composer/packages/client/src/pages/design/createDialogModal.tsx @@ -14,7 +14,7 @@ import { DialogWrapper, DialogTypes } from '@bfc/ui-shared'; import { DialogCreationCopy } from '../../constants'; import { StorageFolder } from '../../recoilModel/types'; import { FieldConfig, useForm } from '../../hooks/useForm'; -import { actionsSeedState, botDisplayNameState, schemasState, validateDialogsSelectorFamily } from '../../recoilModel'; +import { actionsSeedState, botDisplayNameState, schemasState, dialogsSelectorFamily } from '../../recoilModel'; import TelemetryClient from '../../telemetry/TelemetryClient'; import { name, description, styles as wizardStyles } from './styles'; @@ -37,7 +37,7 @@ export const CreateDialogModal: React.FC = (props) => { const { onSubmit, onDismiss, isOpen, projectId } = props; const schemas = useRecoilValue(schemasState(projectId)); - const dialogs = useRecoilValue(validateDialogsSelectorFamily(projectId)); + const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); const botName = useRecoilValue(botDisplayNameState(projectId)); const actionsSeed = useRecoilValue(actionsSeedState(projectId)); const { shellApi, ...shellData } = useShellApi(); diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectDialogs.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectDialogs.tsx index 7df85c3eab..ee90f5fd68 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectDialogs.tsx +++ b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectDialogs.tsx @@ -11,7 +11,7 @@ import debounce from 'lodash/debounce'; import formatMessage from 'format-message'; import { ContentProps } from '../constants'; -import { dispatcherState, validateDialogsSelectorFamily } from '../../../../recoilModel'; +import { dispatcherState, dialogsSelectorFamily } from '../../../../recoilModel'; import { SelectItems } from './SelectItems'; @@ -35,7 +35,7 @@ interface DescriptionColumnProps extends DialogInfo { const DescriptionColumn: React.FC = (props) => { const { id, displayName, projectId } = props; - const items = useRecoilValue(validateDialogsSelectorFamily(projectId)); + const items = useRecoilValue(dialogsSelectorFamily(projectId)); const { content } = items.find(({ id: dialogId }) => dialogId === id) || {}; const [value, setValue] = useState(content?.$designer?.description); @@ -94,7 +94,7 @@ const DescriptionColumn: React.FC = (props) => { }; export const SelectDialogs: React.FC = ({ setSelectedDialogs, projectId }) => { - const dialogs = useRecoilValue(validateDialogsSelectorFamily(projectId)); + const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); const items = useMemo(() => dialogs.map(({ id, content, displayName }) => ({ id, content, displayName })), [ projectId, ]); diff --git a/Composer/packages/client/src/pages/language-generation/LGPage.tsx b/Composer/packages/client/src/pages/language-generation/LGPage.tsx index e832128fc9..2718b006c6 100644 --- a/Composer/packages/client/src/pages/language-generation/LGPage.tsx +++ b/Composer/packages/client/src/pages/language-generation/LGPage.tsx @@ -12,7 +12,7 @@ import { useRecoilValue } from 'recoil'; import { LoadingSpinner } from '../../components/LoadingSpinner'; import { navigateTo } from '../../utils/navigation'; import { Page } from '../../components/Page'; -import { validateDialogsSelectorFamily } from '../../recoilModel'; +import { dialogsSelectorFamily } from '../../recoilModel'; import TelemetryClient from '../../telemetry/TelemetryClient'; import TableView from './table-view'; @@ -25,7 +25,7 @@ const LGPage: React.FC> = (props) => { const { dialogId = '', projectId = '', skillId, lgFileId = '' } = props; - const dialogs = useRecoilValue(validateDialogsSelectorFamily(skillId ?? projectId ?? '')); + const dialogs = useRecoilValue(dialogsSelectorFamily(skillId ?? projectId ?? '')); const path = props.location?.pathname ?? ''; diff --git a/Composer/packages/client/src/pages/language-generation/code-editor.tsx b/Composer/packages/client/src/pages/language-generation/code-editor.tsx index 405fa796d8..088f3eed09 100644 --- a/Composer/packages/client/src/pages/language-generation/code-editor.tsx +++ b/Composer/packages/client/src/pages/language-generation/code-editor.tsx @@ -15,9 +15,10 @@ import { CodeEditorSettings } from '@bfc/shared'; import { useRecoilValue } from 'recoil'; import { LgFile } from '@bfc/extension-client'; -import { localeState, lgFilesState, settingsState } from '../../recoilModel/atoms/botState'; +import { localeState, settingsState } from '../../recoilModel/atoms/botState'; import { userSettingsState, dispatcherState } from '../../recoilModel'; import { DiffCodeEditor } from '../language-understanding/diff-editor'; +import { lgFilesSelectorFamily } from '../../recoilModel/selectors/lg'; const lspServerPath = '/lg-language-server'; @@ -34,7 +35,7 @@ const CodeEditor: React.FC = (props) => { const userSettings = useRecoilValue(userSettingsState); const locale = useRecoilValue(localeState(actualProjectId)); - const lgFiles = useRecoilValue(lgFilesState(actualProjectId)); + const lgFiles = useRecoilValue(lgFilesSelectorFamily(actualProjectId)); const settings = useRecoilValue(settingsState(actualProjectId)); const { languages, defaultLanguage } = settings; 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 ef794fd3a3..36c6000a5a 100644 --- a/Composer/packages/client/src/pages/language-generation/table-view.tsx +++ b/Composer/packages/client/src/pages/language-generation/table-view.tsx @@ -22,15 +22,10 @@ import { lgUtil } from '@bfc/indexers'; import { EditableField } from '../../components/EditableField'; import { navigateTo } from '../../utils/navigation'; import { actionButton, formCell, editableFieldContainer } from '../language-understanding/styles'; -import { - dispatcherState, - lgFilesState, - localeState, - settingsState, - validateDialogsSelectorFamily, -} from '../../recoilModel'; +import { dispatcherState, localeState, settingsState, dialogsSelectorFamily } from '../../recoilModel'; import { languageListTemplates } from '../../components/MultiLanguage'; import TelemetryClient from '../../telemetry/TelemetryClient'; +import { lgFilesSelectorFamily } from '../../recoilModel/selectors/lg'; interface TableViewProps extends RouteComponentProps<{ dialogId: string; skillId: string; projectId: string }> { projectId?: string; @@ -44,10 +39,10 @@ const TableView: React.FC = (props) => { const actualProjectId = skillId ?? projectId ?? ''; - const lgFiles = useRecoilValue(lgFilesState(actualProjectId)); + const lgFiles = useRecoilValue(lgFilesSelectorFamily(actualProjectId)); const locale = useRecoilValue(localeState(actualProjectId)); const settings = useRecoilValue(settingsState(actualProjectId)); - const dialogs = useRecoilValue(validateDialogsSelectorFamily(actualProjectId)); + const dialogs = useRecoilValue(dialogsSelectorFamily(actualProjectId)); const { createLgTemplate, copyLgTemplate, removeLgTemplate, setMessage, updateLgTemplate } = useRecoilValue( dispatcherState ); diff --git a/Composer/packages/client/src/pages/language-understanding/LUPage.tsx b/Composer/packages/client/src/pages/language-understanding/LUPage.tsx index 723f6871e1..f7332ddae8 100644 --- a/Composer/packages/client/src/pages/language-understanding/LUPage.tsx +++ b/Composer/packages/client/src/pages/language-understanding/LUPage.tsx @@ -11,7 +11,7 @@ import { useRecoilValue } from 'recoil'; import { navigateTo, buildURL } from '../../utils/navigation'; import { LoadingSpinner } from '../../components/LoadingSpinner'; import { Page } from '../../components/Page'; -import { validateDialogsSelectorFamily } from '../../recoilModel'; +import { dialogsSelectorFamily } from '../../recoilModel'; import TableView from './table-view'; @@ -24,7 +24,7 @@ const LUPage: React.FC> = (props) => { const { dialogId = '', projectId = '', skillId, luFileId = '' } = props; - const dialogs = useRecoilValue(validateDialogsSelectorFamily(skillId ?? projectId ?? '')); + const dialogs = useRecoilValue(dialogsSelectorFamily(skillId ?? projectId ?? '')); const path = props.location?.pathname ?? ''; const edit = /\/edit(\/)?$/.test(path); 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 bfe3140148..e26afb4599 100644 --- a/Composer/packages/client/src/pages/language-understanding/table-view.tsx +++ b/Composer/packages/client/src/pages/language-understanding/table-view.tsx @@ -24,13 +24,7 @@ import { EditableField } from '../../components/EditableField'; import { getExtension } from '../../utils/fileUtil'; import { languageListTemplates } from '../../components/MultiLanguage'; import { navigateTo } from '../../utils/navigation'; -import { - dispatcherState, - luFilesState, - localeState, - settingsState, - validateDialogsSelectorFamily, -} from '../../recoilModel'; +import { dispatcherState, luFilesState, localeState, settingsState, dialogsSelectorFamily } from '../../recoilModel'; import { formCell, luPhraseCell, tableCell, editableFieldContainer } from './styles'; interface TableViewProps extends RouteComponentProps<{ dialogId: string; skillId: string; projectId: string }> { @@ -60,7 +54,7 @@ const TableView: React.FC = (props) => { const luFiles = useRecoilValue(luFilesState(actualProjectId)); const locale = useRecoilValue(localeState(actualProjectId)); const settings = useRecoilValue(settingsState(actualProjectId)); - const dialogs = useRecoilValue(validateDialogsSelectorFamily(actualProjectId)); + const dialogs = useRecoilValue(dialogsSelectorFamily(actualProjectId)); const { languages, defaultLanguage } = settings; diff --git a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx index fd44c2b34d..3bef276548 100644 --- a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx +++ b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx @@ -17,7 +17,6 @@ import createDispatchers, { Dispatcher } from './dispatchers'; import { luFilesState, qnaFilesState, - lgFilesState, skillManifestsState, dialogSchemasState, settingsState, @@ -29,11 +28,12 @@ import { import { localBotsWithoutErrorsSelector, formDialogSchemasSelectorFamily } from './selectors'; import { Recognizer } from './Recognizers'; import { recognizersSelectorFamily } from './selectors/recognizers'; +import { lgFilesSelectorFamily } from './selectors/lg'; const getBotAssets = async (projectId, snapshot: Snapshot): Promise => { const dialogs = await snapshot.getPromise(dialogsSelectorFamily(projectId)); const luFiles = await snapshot.getPromise(luFilesState(projectId)); - const lgFiles = await snapshot.getPromise(lgFilesState(projectId)); + const lgFiles = await snapshot.getPromise(lgFilesSelectorFamily(projectId)); const skillManifests = await snapshot.getPromise(skillManifestsState(projectId)); const setting = await snapshot.getPromise(settingsState(projectId)); const botProjectFile = await snapshot.getPromise(botProjectFileState(projectId)); diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index b9c94bff5c..9248aeaae5 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -47,6 +47,22 @@ const emptyDialog: DialogInfo = { skills: [], isFormDialog: false, }; +type lgStateParams = { projectId: string; lgFileId: string }; + +export const lgFileState = atomFamily({ + key: getFullyQualifiedKey('lg'), + default: () => { + return {} as LgFile; + }, +}); + +export const lgFileIdsState = atomFamily({ + key: getFullyQualifiedKey('lgFileIds'), + default: () => { + return []; + }, +}); + type dialogStateParams = { projectId: string; dialogId: string }; export const dialogState = atomFamily({ key: getFullyQualifiedKey('dialog'), @@ -124,13 +140,6 @@ export const botRuntimeErrorState = atomFamily({ }, }); -export const lgFilesState = atomFamily({ - key: getFullyQualifiedKey('lgFiles'), - default: (id) => { - return []; - }, -}); - export const luFilesState = atomFamily({ key: getFullyQualifiedKey('luFiles'), default: (id) => { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/dialogs.ts b/Composer/packages/client/src/recoilModel/dispatchers/dialogs.ts index 0f648f7fee..283be2a732 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/dialogs.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/dialogs.ts @@ -3,14 +3,11 @@ /* eslint-disable react-hooks/rules-of-hooks */ import { useRecoilCallback, CallbackInterface } from 'recoil'; import { dialogIndexer, autofixReferInDialog } from '@bfc/indexers'; -import { DialogInfo, checkForPVASchema, Diagnostic } from '@bfc/shared'; +import { DialogInfo, checkForPVASchema } from '@bfc/shared'; import { - lgFilesState, - luFilesState, dialogIdsState, schemasState, - settingsState, onCreateDialogCompleteState, actionsSeedState, showCreateDialogModalState, @@ -18,7 +15,6 @@ import { } from '../atoms'; import { dispatcherState } from '../DispatcherWrapper'; -import validateWorker from './../parsers/validateWorker'; import { createLgFileState, removeLgFileState } from './lg'; import { createLuFileState, removeLuFileState } from './lu'; import { createQnAFileState, removeQnAFileState } from './qna'; @@ -84,17 +80,7 @@ export const dialogsDispatcher = () => { const { set, snapshot } = callbackHelpers; const fixedContent = JSON.parse(autofixReferInDialog(id, JSON.stringify(content))); const schemas = await snapshot.getPromise(schemasState(projectId)); - const lgFiles = await snapshot.getPromise(lgFilesState(projectId)); - const luFiles = await snapshot.getPromise(luFilesState(projectId)); - const settings = await snapshot.getPromise(settingsState(projectId)); const dialog = { isRoot: false, displayName: id, ...dialogIndexer.parse(id, fixedContent) }; - dialog.diagnostics = (await validateWorker.validateDialog({ - dialog, - schema: schemas.sdk.content, - settings, - lgFiles, - luFiles, - })) as Diagnostic[]; if (typeof dialog.content === 'object') { dialog.content.id = id; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts index b9948763cc..fcafcbf369 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts @@ -8,11 +8,12 @@ import formatMessage from 'format-message'; import { getBaseName, getExtension } from '../../utils/fileUtil'; import { dispatcherState } from '../DispatcherWrapper'; +import { lgFilesSelectorFamily } from '../selectors/lg'; import { setError } from './shared'; import LgWorker from './../parsers/lgWorker'; import LgDiagnosticWorker from './../parsers/lgDiagnosticWorker'; -import { lgFilesState, localeState, settingsState } from './../atoms/botState'; +import { lgFileIdsState, lgFileState, localeState, settingsState } from './../atoms/botState'; const templateIsNotEmpty = ({ name, body }) => { return !!name && !!body; @@ -29,7 +30,10 @@ const initialBody = '- '; * */ -const lgFilesAtomUpdater = ( +const lgFilesAtomUpdate = ( + { set }: CallbackInterface, + projectId: string, + oldList: LgFile[], changes: { adds?: LgFile[]; deletes?: LgFile[]; @@ -37,29 +41,31 @@ const lgFilesAtomUpdater = ( }, filter?: (oldList: LgFile[]) => (changeItem: LgFile) => boolean ) => { - return (oldList: LgFile[]) => { - const updates = changes.updates ? (filter ? changes.updates.filter(filter(oldList)) : changes.updates) : []; - const adds = changes.adds ? (filter ? changes.adds.filter(filter(oldList)) : changes.adds) : []; - const deletes = changes.deletes - ? filter - ? changes.deletes.filter(filter(oldList)).map(({ id }) => id) - : changes.deletes.map(({ id }) => id) - : []; - - // updates - let newList = oldList.map((file) => { - const changedFile = updates.find(({ id }) => id === file.id); - return changedFile ?? file; - }); - - // deletes - newList = newList.filter((file) => !deletes.includes(file.id)); + const updates = changes.updates ? (filter ? changes.updates.filter(filter(oldList)) : changes.updates) : []; + const adds = changes.adds ? (filter ? changes.adds.filter(filter(oldList)) : changes.adds) : []; + const deletes = changes.deletes + ? filter + ? changes.deletes.filter(filter(oldList)).map(({ id }) => id) + : changes.deletes.map(({ id }) => id) + : []; + + // updates + updates.forEach((lgFile) => { + set(lgFileState({ projectId, lgFileId: lgFile.id }), lgFile); + }); - // adds - newList = adds.concat(newList); + // deletes + if (deletes.length) { + set(lgFileIdsState(projectId), (ids) => ids.filter((id) => !deletes.includes(id))); + } - return newList; - }; + // adds + if (adds.length) { + set(lgFileIdsState(projectId), (ids) => ids.concat(adds.map((file) => file.id))); + adds.forEach((lgFile) => { + set(lgFileState({ projectId, lgFileId: lgFile.id }), lgFile); + }); + } }; // sync lg file structure across locales, it take times, computed changes may be expired at next tick. @@ -121,8 +127,8 @@ export const createLgFileState = async ( { id, content, projectId }: { id: string; content: string; projectId: string } ) => { try { - const { set, snapshot } = callbackHelpers; - const lgFiles = await snapshot.getPromise(lgFilesState(projectId)); + const { snapshot } = callbackHelpers; + const lgFiles = await snapshot.getPromise(lgFilesSelectorFamily(projectId)); const locale = await snapshot.getPromise(localeState(projectId)); const { languages } = await snapshot.getPromise(settingsState(projectId)); const createdLgId = `${id}.${locale}`; @@ -147,7 +153,7 @@ export const createLgFileState = async ( }); }); - set(lgFilesState(projectId), lgFilesAtomUpdater({ adds: changes })); + lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { adds: changes }); } catch (error) { setError(callbackHelpers, error); } @@ -157,8 +163,8 @@ export const removeLgFileState = async ( callbackHelpers: CallbackInterface, { id, projectId }: { id: string; projectId: string } ) => { - const { set, snapshot } = callbackHelpers; - const lgFiles = await snapshot.getPromise(lgFilesState(projectId)); + const { snapshot } = callbackHelpers; + const lgFiles = await snapshot.getPromise(lgFilesSelectorFamily(projectId)); const locale = await snapshot.getPromise(localeState(projectId)); const targetLgFile = lgFiles.find((item) => item.id === id) || lgFiles.find((item) => item.id === `${id}.${locale}`); @@ -167,7 +173,7 @@ export const removeLgFileState = async ( return; } - set(lgFilesState(projectId), lgFilesAtomUpdater({ deletes: [targetLgFile] })); + lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { deletes: [targetLgFile] }); }; export const lgDispatcher = () => { @@ -208,19 +214,14 @@ export const lgDispatcher = () => { try { const { set, snapshot } = callbackHelpers; //set content first - set(lgFilesState(projectId), (prevLgFiles) => { - return prevLgFiles.map((file) => { - if (file.id === id) { - return { - ...file, - content, - }; - } - return file; - }); + set(lgFileState({ projectId, lgFileId: id }), (prevLgFile) => { + return { + ...prevLgFile, + content, + }; }); - const lgFiles = await snapshot.getPromise(lgFilesState(projectId)); + const lgFiles = await snapshot.getPromise(lgFilesSelectorFamily(projectId)); const updatedFile = (await LgWorker.parse(projectId, id, content, lgFiles)) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); @@ -229,19 +230,17 @@ export const lgDispatcher = () => { * Why other methods do not need double check content? * Because this method already did set content before call lgFilesAtomUpdater. */ - set( - lgFilesState(projectId), - lgFilesAtomUpdater({ updates: updatedFiles }, (prevLgFiles) => { - const targetInState = prevLgFiles.find((file) => file.id === updatedFile.id); - const targetInCurrentChange = updatedFiles.find((file) => file.id === updatedFile.id); - // compare to drop expired content already setted above. - if (targetInState?.content !== targetInCurrentChange?.content) { - return (lgFile) => lgFile.id !== updatedFile.id; - } else { - return () => true; - } - }) - ); + + lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }, (prevLgFiles) => { + const targetInState = prevLgFiles.find((file) => file.id === updatedFile.id); + const targetInCurrentChange = updatedFiles.find((file) => file.id === updatedFile.id); + // compare to drop expired content already setted above. + if (targetInState?.content !== targetInCurrentChange?.content) { + return (lgFile) => lgFile.id !== updatedFile.id; + } else { + return () => true; + } + }); // if changes happen on common.lg, async re-parse all. if (getBaseName(id) === 'common') { @@ -266,8 +265,8 @@ export const lgDispatcher = () => { template: LgTemplate; projectId: string; }) => { - const { set, snapshot } = callbackHelpers; - const lgFiles = await snapshot.getPromise(lgFilesState(projectId)); + const { snapshot } = callbackHelpers; + const lgFiles = await snapshot.getPromise(lgFilesSelectorFamily(projectId)); const lgFile = lgFiles.find((file) => file.id === id); if (!lgFile) return lgFiles; const sameIdOtherLocaleFiles = lgFiles.filter((file) => getBaseName(file.id) === getBaseName(id)); @@ -294,7 +293,7 @@ export const lgDispatcher = () => { )) as LgFile; changes.push(updatedFile); } - set(lgFilesState(projectId), lgFilesAtomUpdater({ updates: changes })); + lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: changes }); } else { // body change, only update current locale file const updatedFile = (await LgWorker.updateTemplate( @@ -304,7 +303,7 @@ export const lgDispatcher = () => { { body: template.body }, lgFiles )) as LgFile; - set(lgFilesState(projectId), lgFilesAtomUpdater({ updates: [updatedFile] })); + lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: [updatedFile] }); } } catch (error) { setError(callbackHelpers, error); @@ -320,7 +319,7 @@ export const lgDispatcher = () => { ); const createLgTemplate = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async ({ + (callbackHelpers: CallbackInterface) => async ({ id, template, projectId, @@ -329,12 +328,13 @@ export const lgDispatcher = () => { template: LgTemplate; projectId: string; }) => { - const lgFiles = await snapshot.getPromise(lgFilesState(projectId)); + const { snapshot } = callbackHelpers; + const lgFiles = await snapshot.getPromise(lgFilesSelectorFamily(projectId)); const lgFile = lgFiles.find((file) => file.id === id); if (!lgFile) return lgFiles; const updatedFile = (await LgWorker.addTemplate(projectId, lgFile, template, lgFiles)) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - set(lgFilesState(projectId), lgFilesAtomUpdater({ updates: updatedFiles })); + lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); } ); @@ -349,13 +349,13 @@ export const lgDispatcher = () => { projectId: string; }) => { try { - const { set, snapshot } = callbackHelpers; - const lgFiles = await snapshot.getPromise(lgFilesState(projectId)); + const { snapshot } = callbackHelpers; + const lgFiles = await snapshot.getPromise(lgFilesSelectorFamily(projectId)); const lgFile = lgFiles.find((file) => file.id === id); if (!lgFile) return lgFiles; const updatedFile = (await LgWorker.addTemplates(projectId, lgFile, templates, lgFiles)) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - set(lgFilesState(projectId), lgFilesAtomUpdater({ updates: updatedFiles })); + lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -372,15 +372,15 @@ export const lgDispatcher = () => { templateName: string; projectId: string; }) => { - const { set, snapshot } = callbackHelpers; - const lgFiles = await snapshot.getPromise(lgFilesState(projectId)); + const { snapshot } = callbackHelpers; + const lgFiles = await snapshot.getPromise(lgFilesSelectorFamily(projectId)); const lgFile = lgFiles.find((file) => file.id === id); if (!lgFile) return lgFiles; try { const updatedFile = (await LgWorker.removeTemplate(projectId, lgFile, templateName, lgFiles)) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - set(lgFilesState(projectId), lgFilesAtomUpdater({ updates: updatedFiles })); + lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -398,15 +398,15 @@ export const lgDispatcher = () => { projectId: string; }) => { try { - const { set, snapshot } = callbackHelpers; - const lgFiles = await snapshot.getPromise(lgFilesState(projectId)); + const { snapshot } = callbackHelpers; + const lgFiles = await snapshot.getPromise(lgFilesSelectorFamily(projectId)); const lgFile = lgFiles.find((file) => file.id === id); if (!lgFile) return lgFiles; const updatedFile = (await LgWorker.removeTemplates(projectId, lgFile, templateNames, lgFiles)) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - set(lgFilesState(projectId), lgFilesAtomUpdater({ updates: updatedFiles })); + lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -426,8 +426,8 @@ export const lgDispatcher = () => { projectId: string; }) => { try { - const { set, snapshot } = callbackHelpers; - const lgFiles = await snapshot.getPromise(lgFilesState(projectId)); + const { snapshot } = callbackHelpers; + const lgFiles = await snapshot.getPromise(lgFilesSelectorFamily(projectId)); const lgFile = lgFiles.find((file) => file.id === id); if (!lgFile) return lgFiles; const updatedFile = (await LgWorker.copyTemplate( @@ -438,7 +438,7 @@ export const lgDispatcher = () => { lgFiles )) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - set(lgFilesState(projectId), lgFilesAtomUpdater({ updates: updatedFiles })); + lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -448,24 +448,21 @@ export const lgDispatcher = () => { const reparseAllLgFiles = useRecoilCallback( (callbackHelpers: CallbackInterface) => async ({ projectId }: { projectId: string }) => { try { - const { set, snapshot } = callbackHelpers; - const lgFiles = await snapshot.getPromise(lgFilesState(projectId)); + const { snapshot } = callbackHelpers; + const lgFiles = await snapshot.getPromise(lgFilesSelectorFamily(projectId)); const reparsedLgFiles: LgFile[] = []; for (const file of lgFiles) { const reparsedFile = (await LgDiagnosticWorker.parse(projectId, file.id, file.content, lgFiles)) as LgFile; reparsedLgFiles.push({ ...file, diagnostics: reparsedFile.diagnostics }); } - set( - lgFilesState(projectId), - lgFilesAtomUpdater({ updates: reparsedLgFiles }, (prevLgFiles) => { - // compare to drop expired content already setted above. - return (target) => { - const targetInState = prevLgFiles.find((file) => file.id === target.id); - const targetInCurrentChange = reparsedLgFiles.find((file) => file.id === target.id); - return targetInState?.content === targetInCurrentChange?.content; - }; - }) - ); + lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: reparsedLgFiles }, (prevLgFiles) => { + // compare to drop expired content already setted above. + return (target) => { + const targetInState = prevLgFiles.find((file) => file.id === target.id); + const targetInCurrentChange = reparsedLgFiles.find((file) => file.id === target.id); + return targetInState?.content === targetInCurrentChange?.content; + }; + }); } catch (error) { setError(callbackHelpers, error); } diff --git a/Composer/packages/client/src/recoilModel/dispatchers/multilang.ts b/Composer/packages/client/src/recoilModel/dispatchers/multilang.ts index d4519b5856..459cf85e39 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/multilang.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/multilang.ts @@ -9,9 +9,9 @@ import difference from 'lodash/difference'; import languageStorage from '../../utils/languageStorage'; import { getExtension } from '../../utils/fileUtil'; import { localBotsDataSelector, rootBotProjectIdSelector } from '../selectors/project'; +import { lgFilesSelectorFamily } from '../selectors/lg'; import { - lgFilesState, luFilesState, localeState, settingsState, @@ -93,7 +93,7 @@ export const multilangDispatcher = () => { const onAddLanguageDialogComplete = (await snapshot.getPromise(onAddLanguageDialogCompleteState(projectId))).func; // copy files from default language - set(lgFilesState(projectId), (prevlgFiles) => { + set(lgFilesSelectorFamily(projectId), (prevlgFiles) => { const addedLgFiles = copyLanguageResources(prevlgFiles, defaultLang, languages); return [...prevlgFiles, ...addedLgFiles]; }); @@ -136,7 +136,7 @@ export const multilangDispatcher = () => { const onDelLanguageDialogComplete = (await snapshot.getPromise(onDelLanguageDialogCompleteState(projectId))).func; // copy files from default language - set(lgFilesState(projectId), (prevlgFiles) => { + set(lgFilesSelectorFamily(projectId), (prevlgFiles) => { const { left: leftLgFiles } = deleteLanguageResources(prevlgFiles, languages); return leftLgFiles; }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/trigger.ts b/Composer/packages/client/src/recoilModel/dispatchers/trigger.ts index 20d096ba31..f5173fc3f6 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/trigger.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/trigger.ts @@ -5,7 +5,7 @@ import { useRecoilCallback, CallbackInterface } from 'recoil'; import { BaseSchema, deleteActions, ITriggerCondition, LgTemplate, LgTemplateSamples, SDKKinds } from '@bfc/shared'; import get from 'lodash/get'; -import { lgFilesState, luFilesState, schemasState, dialogState, localeState } from '../atoms/botState'; +import { luFilesState, schemasState, dialogState, localeState } from '../atoms/botState'; import { dispatcherState } from '../DispatcherWrapper'; import { dialogsSelectorFamily } from '../selectors'; import { @@ -15,6 +15,7 @@ import { qnaMatcherKey, TriggerFormData, } from '../../utils/dialogUtil'; +import { lgFilesSelectorFamily } from '../selectors/lg'; import { setError } from './shared'; @@ -47,7 +48,7 @@ export const triggerDispatcher = () => { try { const { snapshot } = callbackHelpers; const dispatcher = await snapshot.getPromise(dispatcherState); - const lgFiles = await snapshot.getPromise(lgFilesState(projectId)); + const lgFiles = await snapshot.getPromise(lgFilesSelectorFamily(projectId)); const luFiles = await snapshot.getPromise(luFilesState(projectId)); const dialogs = await snapshot.getPromise(dialogsSelectorFamily(projectId)); const dialog = await snapshot.getPromise(dialogState({ projectId, dialogId })); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 584441c7bc..77c1ccd6ca 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -59,7 +59,6 @@ import { formDialogSchemaIdsState, formDialogSchemaState, jsonSchemaFilesState, - lgFilesState, localeState, locationState, luFilesState, @@ -84,7 +83,7 @@ import { undoHistoryState } from '../../undo/history'; import UndoHistory from '../../undo/undoHistory'; import { logMessage, setError } from '../shared'; import { setRootBotSettingState } from '../setting'; -import validateWorker from '../../parsers/validateWorker'; +import { lgFilesSelectorFamily } from '../../selectors/lg'; import { crossTrainConfigState } from './../../atoms/botState'; import { recognizersSelectorFamily } from './../../selectors/recognizers'; @@ -364,19 +363,9 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any if (dialog.isRoot) { mainDialog = dialog.id; } - try { - dialog.diagnostics = (await validateWorker.validateDialog({ - dialog, - schema: schemas.sdk.content, - settings, - lgFiles, - luFiles, - })) as Diagnostic[]; - set(dialogState({ projectId, dialogId: dialog.id }), dialog); - dialogIds.push(dialog.id); - } catch (error) { - console.log(error); - } + + set(dialogState({ projectId, dialogId: dialog.id }), dialog); + dialogIds.push(dialog.id); } set(dialogIdsState(projectId), dialogIds); @@ -396,7 +385,7 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any set(skillManifestsState(projectId), skillManifests); set(luFilesState(projectId), initLuFilesStatus(botName, luFiles, dialogs)); - set(lgFilesState(projectId), lgFiles); + set(lgFilesSelectorFamily(projectId), lgFiles); set(jsonSchemaFilesState(projectId), jsonSchemaFiles); set(dialogSchemasState(projectId), dialogSchemas); diff --git a/Composer/packages/client/src/recoilModel/selectors/diagnosticsPageSelector.ts b/Composer/packages/client/src/recoilModel/selectors/diagnosticsPageSelector.ts index fb6a4d0587..3eef1f8500 100644 --- a/Composer/packages/client/src/recoilModel/selectors/diagnosticsPageSelector.ts +++ b/Composer/packages/client/src/recoilModel/selectors/diagnosticsPageSelector.ts @@ -8,7 +8,7 @@ import formatMessage from 'format-message'; import { getReferredLuFiles } from '../../utils/luUtil'; import { INavTreeItem } from '../../components/NavTree'; -import { botDisplayNameState, qnaFilesState } from '../atoms/botState'; +import { botDisplayNameState, dialogIdsState, qnaFilesState } from '../atoms/botState'; import { DialogDiagnostic, LgDiagnostic, @@ -25,7 +25,6 @@ import { botProjectIdsState, dialogSchemasState, jsonSchemaFilesState, - lgFilesState, luFilesState, projectMetaDataState, settingsState, @@ -34,8 +33,10 @@ import { import { crossTrainConfigState } from './../atoms/botState'; import { formDialogSchemasSelectorFamily, rootBotProjectIdSelector } from './project'; -import { validateDialogsSelectorFamily } from './validatedDialogs'; +import { dialogsSelectorFamily } from './dialogs'; import { recognizersSelectorFamily } from './recognizers'; +import { dialogDiagnosticsSelectorFamily } from './validatedDialogs'; +import { lgFilesSelectorFamily } from './lg'; export const botAssetsSelectFamily = selectorFamily({ key: 'botAssetsSelectFamily', @@ -43,9 +44,9 @@ export const botAssetsSelectFamily = selectorFamily({ const projectsMetaData = get(projectMetaDataState(projectId)); if (!projectsMetaData || projectsMetaData.isRemote) return null; - const dialogs = get(validateDialogsSelectorFamily(projectId)); + const dialogs = get(dialogsSelectorFamily(projectId)); const luFiles = get(luFilesState(projectId)); - const lgFiles = get(lgFilesState(projectId)); + const lgFiles = get(lgFilesSelectorFamily(projectId)); const setting = get(settingsState(projectId)); const skillManifests = get(skillManifestsState(projectId)); const dialogSchemas = get(dialogSchemasState(projectId)); @@ -151,24 +152,24 @@ export const settingDiagnosticsSelectorFamily = selectorFamily({ }, }); -export const dialogDiagnosticsSelectorFamily = selectorFamily({ - key: 'dialogDiagnosticsSelectorFamily', +export const dialogsDiagnosticsSelectorFamily = selectorFamily({ + key: 'dialogsDiagnosticsSelectorFamily', get: (projectId: string) => ({ get }) => { const botAssets = get(botAssetsSelectFamily(projectId)); if (botAssets === null) return []; const rootProjectId = get(rootBotProjectIdSelector) ?? projectId; + const dialogIds = get(dialogIdsState(projectId)); const diagnosticList: DiagnosticInfo[] = []; - const { dialogs } = botAssets; - dialogs.forEach((dialog) => { - dialog.diagnostics.forEach((diagnostic) => { - const location = `${dialog.id}.dialog`; - diagnosticList.push(new DialogDiagnostic(rootProjectId, projectId, dialog.id, location, diagnostic)); + dialogIds.forEach((dialogId: string) => { + get(dialogDiagnosticsSelectorFamily({ projectId, dialogId })).forEach((diagnostic) => { + const location = `${dialogId}.dialog`; + diagnosticList.push(new DialogDiagnostic(rootProjectId, projectId, dialogId, location, diagnostic)); }); }); - return diagnosticList; + return []; }, }); @@ -242,7 +243,7 @@ export const qnaDiagnosticsSelectorFamily = selectorFamily({ export const diagnosticsSelectorFamily = selectorFamily({ key: 'diagnosticsSelector', get: (projectId: string) => ({ get }) => [ - ...get(dialogDiagnosticsSelectorFamily(projectId)), + ...get(dialogsDiagnosticsSelectorFamily(projectId)), ...get(botDiagnosticsSelectorFamily(projectId)), ...get(skillSettingDiagnosticsSelectorFamily(projectId)), ...get(settingDiagnosticsSelectorFamily(projectId)), diff --git a/Composer/packages/client/src/recoilModel/selectors/dialogImports.ts b/Composer/packages/client/src/recoilModel/selectors/dialogImports.ts index ff92849130..fc49a8a07a 100644 --- a/Composer/packages/client/src/recoilModel/selectors/dialogImports.ts +++ b/Composer/packages/client/src/recoilModel/selectors/dialogImports.ts @@ -6,7 +6,9 @@ import uniqBy from 'lodash/uniqBy'; import { selectorFamily } from 'recoil'; import { getBaseName } from '../../utils/fileUtil'; -import { localeState, lgFilesState, luFilesState } from '../atoms'; +import { localeState, luFilesState } from '../atoms'; + +import { lgFilesSelectorFamily } from './lg'; // Finds all the file imports starting from a given dialog file. export const getLanguageFileImports = ( @@ -57,7 +59,7 @@ export const lgImportsSelectorFamily = selectorFamily - get(lgFilesState(projectId)).find((f) => f.id === fileId || f.id === `${fileId}.${locale}`) as LgFile; + get(lgFilesSelectorFamily(projectId)).find((f) => f.id === fileId || f.id === `${fileId}.${locale}`) as LgFile; // Have to exclude common as a special case return getLanguageFileImports(dialogId, getFile).filter((i) => i.id !== 'common'); diff --git a/Composer/packages/client/src/recoilModel/selectors/lg.ts b/Composer/packages/client/src/recoilModel/selectors/lg.ts new file mode 100644 index 0000000000..a6c3580dae --- /dev/null +++ b/Composer/packages/client/src/recoilModel/selectors/lg.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { LgFile } from '@bfc/shared'; +import { selectorFamily } from 'recoil'; + +import { lgFileIdsState, lgFileState } from '../atoms'; + +export const lgFilesSelectorFamily = selectorFamily({ + key: 'lgFiles', + get: (projectId: string) => ({ get }) => { + const dialogIds = get(lgFileIdsState(projectId)); + + return dialogIds.map((lgFileId) => { + return get(lgFileState({ projectId, lgFileId })); + }); + }, + set: (projectId: string) => ({ set }, newLgFiles) => { + const newLgFileArray = newLgFiles as LgFile[]; + + set( + lgFileIdsState(projectId), + newLgFileArray.map((lgFile) => lgFile.id) + ); + newLgFileArray.forEach((lgFile) => set(lgFileState({ projectId, lgFileId: lgFile.id }), lgFile)); + }, +}); diff --git a/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts b/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts index c93b349c72..ead81344dc 100644 --- a/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts +++ b/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts @@ -20,7 +20,7 @@ import { dispatcherState } from '../DispatcherWrapper'; import { isBuildConfigComplete as isBuildConfigurationComplete, needsBuild } from '../../utils/buildUtil'; import { getSensitiveProperties } from '../dispatchers/utils/project'; -import { validateDialogsSelectorFamily } from './validatedDialogs'; +import { dialogsSelectorFamily } from './dialogs'; import { localBotsWithoutErrorsSelector, rootBotProjectIdSelector } from './project'; export const trackBotStatusesSelector = selectorFamily({ @@ -40,7 +40,7 @@ export const trackBotStatusesSelector = selectorFamily({ export const botBuildRequiredSelector = selectorFamily({ key: 'botBuildRequiredSelector', get: (projectId: string) => ({ get }) => { - const dialogs = get(validateDialogsSelectorFamily(projectId)); + const dialogs = get(dialogsSelectorFamily(projectId)); return !isAbsHosted() && needsBuild(dialogs); }, }); @@ -53,7 +53,7 @@ export const buildEssentialsSelector = selectorFamily({ luis: settings.luis, qna: settings.qna, }; - const dialogs = get(validateDialogsSelectorFamily(projectId)); + const dialogs = get(dialogsSelectorFamily(projectId)); const luFiles = get(luFilesState(projectId)); const qnaFiles = get(qnaFilesState(projectId)); const buildRequired = get(botBuildRequiredSelector(projectId)); @@ -84,7 +84,7 @@ export const buildConfigurationSelector = selector({ .map((projectId: string) => { const result = get(buildEssentialsSelector(projectId)); const name = get(botDisplayNameState(projectId)); - const dialogs = get(validateDialogsSelectorFamily(projectId)); + const dialogs = get(dialogsSelectorFamily(projectId)); const settings = get(settingsState(projectId)); let sensitiveSettings = {}; if (rootBotId) { diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts index cca4b21178..52ae1876a5 100644 --- a/Composer/packages/client/src/recoilModel/selectors/project.ts +++ b/Composer/packages/client/src/recoilModel/selectors/project.ts @@ -16,7 +16,6 @@ import { formDialogSchemaIdsState, formDialogSchemaState, luFilesState, - lgFilesState, qnaFilesState, skillManifestsState, dialogSchemasState, @@ -32,11 +31,12 @@ import { import { dialogsSelectorFamily, buildEssentialsSelector, - validateDialogsSelectorFamily, lgImportsSelectorFamily, luImportsSelectorFamily, } from '../selectors'; +import { lgFilesSelectorFamily } from './lg'; + // Actions export const localBotsWithoutErrorsSelector = selector({ key: 'localBotsWithoutErrorsSelector', @@ -97,9 +97,9 @@ export const botProjectSpaceSelector = selector({ const botProjects = get(botProjectIdsState); const result = botProjects.map((projectId: string) => { const { isRemote, isRootBot } = get(projectMetaDataState(projectId)); - const dialogs = get(validateDialogsSelectorFamily(projectId)); + const dialogs = get(dialogsSelectorFamily(projectId)); const luFiles = get(luFilesState(projectId)); - const lgFiles = get(lgFilesState(projectId)); + const lgFiles = get(lgFilesSelectorFamily(projectId)); const qnaFiles = get(qnaFilesState(projectId)); const formDialogSchemas = get(formDialogSchemasSelectorFamily(projectId)); const botProjectFile = get(botProjectFileState(projectId)); @@ -202,7 +202,7 @@ export const botProjectDiagnosticsSelector = selector({ const dialogs = get(dialogsSelectorFamily(projectId)); const formDialogSchemas = get(formDialogSchemasSelectorFamily(projectId)); const luFiles = get(luFilesState(projectId)); - const lgFiles = get(lgFilesState(projectId)); + const lgFiles = get(lgFilesSelectorFamily(projectId)); const setting = get(settingsState(projectId)); const skillManifests = get(skillManifestsState(projectId)); const dialogSchemas = get(dialogSchemasState(projectId)); diff --git a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts index e3217e3d44..788a24cfa9 100644 --- a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts +++ b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts @@ -2,58 +2,31 @@ // Licensed under the MIT License. import { selectorFamily } from 'recoil'; -import { DialogInfo, BotSchemas, LgFile, LuFile, DialogSetting, RecognizerFile, Diagnostic } from '@bfc/shared'; +import { DialogInfo, BotSchemas, LgFile, DialogSetting, RecognizerFile, Diagnostic } from '@bfc/shared'; +import { validateDialog } from '@bfc/indexers'; -import { - botProjectIdsState, - dialogIdsState, - schemasState, - lgFilesState, - luFilesState, - dialogState, - settingsState, -} from '../atoms'; +import { schemasState, dialogState, settingsState, localeState, lgFileState } from '../atoms'; import { getLuProvider } from '../../utils/dialogUtil'; import { recognizersSelectorFamily } from './recognizers'; -import validateWorker from './../parsers/validateWorker'; type validateDialogSelectorFamilyParams = { projectId: string; dialogId: string }; -const validateDialogSelectorFamily = selectorFamily({ - key: 'validateDialogSelectorFamily', - get: ({ projectId, dialogId }: validateDialogSelectorFamilyParams) => async ({ get }) => { - const dialog: DialogInfo = get(dialogState({ projectId, dialogId })); - const schemas: BotSchemas = get(schemasState(projectId)); - const lgFiles: LgFile[] = get(lgFilesState(projectId)); - const luFiles: LuFile[] = get(luFilesState(projectId)); - const settings: DialogSetting = get(settingsState(projectId)); +export const dialogLuProviderSelectorFamily = selectorFamily({ + key: 'dialogLuProviderSelectorFamily', + get: ({ projectId, dialogId }: validateDialogSelectorFamilyParams) => ({ get }) => { const recognizers: RecognizerFile[] = get(recognizersSelectorFamily(projectId)); - const luProvider = getLuProvider(dialogId, recognizers); - return { - ...dialog, - diagnostics: (await validateWorker.validateDialog({ - dialog, - schema: schemas.sdk.content, - settings, - lgFiles, - luFiles, - })) as Diagnostic[], - luProvider, - }; + return getLuProvider(dialogId, recognizers); }, }); -export const validateDialogsSelectorFamily = selectorFamily({ - key: 'validateDialogsSelectorFamily', - get: (projectId: string) => ({ get }) => { - const loadedProjects = get(botProjectIdsState); - if (!loadedProjects.includes(projectId)) { - return []; - } - const dialogIds = get(dialogIdsState(projectId)); - - return dialogIds.map((dialogId) => { - return get(validateDialogSelectorFamily({ projectId, dialogId })); - }); +export const dialogDiagnosticsSelectorFamily = selectorFamily({ + key: 'dialogDiagnosticsSelectorFamily', + get: ({ projectId, dialogId }: validateDialogSelectorFamilyParams) => ({ get }) => { + const dialog: DialogInfo = get(dialogState({ projectId, dialogId })); + const schemas: BotSchemas = get(schemasState(projectId)); + const locale = get(localeState(projectId)); + const lgFile: LgFile = get(lgFileState({ projectId, lgFileId: `${dialogId}.${locale}` })); + const settings: DialogSetting = get(settingsState(projectId)); + return validateDialog(dialog, schemas.sdk.content, settings, [lgFile], []) as Diagnostic[]; }, }); diff --git a/Composer/packages/client/src/recoilModel/undo/trackedAtoms.ts b/Composer/packages/client/src/recoilModel/undo/trackedAtoms.ts index 3ed1f116d0..566f6c493f 100644 --- a/Composer/packages/client/src/recoilModel/undo/trackedAtoms.ts +++ b/Composer/packages/client/src/recoilModel/undo/trackedAtoms.ts @@ -3,11 +3,12 @@ import { RecoilState } from 'recoil'; -import { luFilesState, lgFilesState } from '../atoms'; +import { luFilesState } from '../atoms'; import { dialogsSelectorFamily } from '../selectors'; +import { lgFilesSelectorFamily } from '../selectors/lg'; export type AtomAssetsMap = Map, any>; export const trackedAtoms = (projectId: string): RecoilState[] => { - return [dialogsSelectorFamily(projectId), luFilesState(projectId), lgFilesState(projectId)]; + return [dialogsSelectorFamily(projectId), luFilesState(projectId), lgFilesSelectorFamily(projectId)]; }; diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts index 65ae0c9dde..bd43df2dd7 100644 --- a/Composer/packages/client/src/shell/useShell.ts +++ b/Composer/packages/client/src/shell/useShell.ts @@ -27,14 +27,13 @@ import { settingsState, clipboardActionsState, schemasState, - validateDialogsSelectorFamily, + dialogsSelectorFamily, focusPathState, localeState, qnaFilesState, designPageLocationState, botDisplayNameState, dialogSchemasState, - lgFilesState, luFilesState, rateInfoState, rootBotProjectIdSelector, @@ -44,6 +43,7 @@ import { undoFunctionState } from '../recoilModel/undo/history'; import { skillsStateSelector } from '../recoilModel/selectors'; import { navigateTo } from '../utils/navigation'; import TelemetryClient from '../telemetry/TelemetryClient'; +import { lgFilesSelectorFamily } from '../recoilModel/selectors/lg'; import { useLgApi } from './lgApi'; import { useLuApi } from './luApi'; @@ -79,7 +79,7 @@ export function useShell(source: EventSource, projectId: string): Shell { const dialogMapRef = useRef({}); const schemas = useRecoilValue(schemasState(projectId)); - const dialogs = useRecoilValue(validateDialogsSelectorFamily(projectId)); + const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); const focusPath = useRecoilValue(focusPathState(projectId)); const skills = useRecoilValue(skillsStateSelector); const locale = useRecoilValue(localeState(projectId)); @@ -88,7 +88,7 @@ export function useShell(source: EventSource, projectId: string): Shell { const designPageLocation = useRecoilValue(designPageLocationState(projectId)); const { undo, redo, commitChanges } = undoFunction; const luFiles = useRecoilValue(luFilesState(projectId)); - const lgFiles = useRecoilValue(lgFilesState(projectId)); + const lgFiles = useRecoilValue(lgFilesSelectorFamily(projectId)); const dialogSchemas = useRecoilValue(dialogSchemasState(projectId)); const botName = useRecoilValue(botDisplayNameState(projectId)); const settings = useRecoilValue(settingsState(projectId)); From 85423f26fe647443388c2ca46951ed7cd4c35a70 Mon Sep 17 00:00:00 2001 From: leilzh Date: Mon, 11 Jan 2021 15:49:54 +0800 Subject: [PATCH 03/12] update the luProvider for dialog --- .../src/pages/design/PropertyEditor.tsx | 6 ++-- .../client/src/recoilModel/parsers/types.ts | 12 -------- .../src/recoilModel/parsers/validateWorker.ts | 14 --------- .../parsers/workers/validator.worker.ts | 29 ------------------- .../recoilModel/selectors/validatedDialogs.ts | 13 +++++++-- .../packages/client/src/shell/useShell.ts | 5 ++-- 6 files changed, 15 insertions(+), 64 deletions(-) delete mode 100644 Composer/packages/client/src/recoilModel/parsers/validateWorker.ts delete mode 100644 Composer/packages/client/src/recoilModel/parsers/workers/validator.worker.ts diff --git a/Composer/packages/client/src/pages/design/PropertyEditor.tsx b/Composer/packages/client/src/pages/design/PropertyEditor.tsx index b87c1c0e91..8d69431d18 100644 --- a/Composer/packages/client/src/pages/design/PropertyEditor.tsx +++ b/Composer/packages/client/src/pages/design/PropertyEditor.tsx @@ -14,7 +14,7 @@ import { MicrosoftAdaptiveDialog } from '@bfc/shared'; import { useRecoilValue } from 'recoil'; import { css } from '@emotion/core'; -import { botDisplayNameState, projectMetaDataState } from '../../recoilModel'; +import { botDisplayNameState, dialogDiagnosticsSelectorFamily, projectMetaDataState } from '../../recoilModel'; import { PropertyEditorHeader } from './PropertyEditorHeader'; import { formEditor } from './styles'; @@ -41,6 +41,7 @@ const PropertyEditor: React.FC = () => { const { onFocusSteps } = shellApi; const botName = useRecoilValue(botDisplayNameState(projectId)); const projectData = useRecoilValue(projectMetaDataState(projectId)); + const diagnostics = useRecoilValue(dialogDiagnosticsSelectorFamily({ projectId, dialogId: currentDialog.id })); const dialogData = useMemo(() => { if (currentDialog?.content) { @@ -81,8 +82,7 @@ const PropertyEditor: React.FC = () => { }, [$schema, formUIOptions]); const errors = useMemo(() => { - const diagnostics = currentDialog?.diagnostics; - if (diagnostics) { + if (diagnostics.length) { const currentPath = focusPath.replace('#', ''); return diagnostics.reduce((errors, d) => { diff --git a/Composer/packages/client/src/recoilModel/parsers/types.ts b/Composer/packages/client/src/recoilModel/parsers/types.ts index 3d164fb6df..ce1f39d367 100644 --- a/Composer/packages/client/src/recoilModel/parsers/types.ts +++ b/Composer/packages/client/src/recoilModel/parsers/types.ts @@ -155,15 +155,3 @@ export enum QnAActionType { UpdateSection = 'update-section', RemoveSection = 'remove-section', } - -export type ValidatorPayload = { - dialog: DialogInfo; - schema: SchemaDefinitions; - settings: DialogSetting; - lgFiles: LgFile[]; - luFiles: LuFile[]; -}; - -export enum ValidateActionType { - DialogValidator = 'dialog', -} diff --git a/Composer/packages/client/src/recoilModel/parsers/validateWorker.ts b/Composer/packages/client/src/recoilModel/parsers/validateWorker.ts deleted file mode 100644 index b97a32cbe4..0000000000 --- a/Composer/packages/client/src/recoilModel/parsers/validateWorker.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -import Worker from './workers/validator.worker.ts'; -import { BaseWorker } from './baseWorker'; -import { ValidateActionType, ValidatorPayload } from './types'; - -// Wrapper class -class ValidateWorker extends BaseWorker { - validateDialog(data: ValidatorPayload) { - return this.sendMsg(ValidateActionType.DialogValidator, data); - } -} - -export default new ValidateWorker(new Worker()); diff --git a/Composer/packages/client/src/recoilModel/parsers/workers/validator.worker.ts b/Composer/packages/client/src/recoilModel/parsers/workers/validator.worker.ts deleted file mode 100644 index 2e70e87d89..0000000000 --- a/Composer/packages/client/src/recoilModel/parsers/workers/validator.worker.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -import { validateDialog } from '@bfc/indexers'; - -import { ValidateActionType, ValidatorPayload } from '../types'; - -const ctx: Worker = self as any; - -type Message = { - id: string; - type: ValidateActionType.DialogValidator; - payload: ValidatorPayload; -}; - -export const handleMessage = (msg: Message) => { - const { dialog, schema, settings, lgFiles, luFiles } = msg.payload; - return validateDialog(dialog, schema, settings, lgFiles, luFiles); -}; - -ctx.onmessage = function (event) { - const msg = event.data as Message; - - try { - const payload = handleMessage(msg); - ctx.postMessage({ id: msg.id, payload }); - } catch (error) { - ctx.postMessage({ id: msg.id, error: error.message }); - } -}; diff --git a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts index 788a24cfa9..d024790320 100644 --- a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts +++ b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts @@ -9,13 +9,20 @@ import { schemasState, dialogState, settingsState, localeState, lgFileState } fr import { getLuProvider } from '../../utils/dialogUtil'; import { recognizersSelectorFamily } from './recognizers'; +import { dialogsSelectorFamily } from './dialogs'; type validateDialogSelectorFamilyParams = { projectId: string; dialogId: string }; -export const dialogLuProviderSelectorFamily = selectorFamily({ +export const dialogsWithLuProviderSelectorFamily = selectorFamily({ key: 'dialogLuProviderSelectorFamily', - get: ({ projectId, dialogId }: validateDialogSelectorFamilyParams) => ({ get }) => { + get: (projectId: string) => ({ get }) => { + const dialogs = get(dialogsSelectorFamily(projectId)); const recognizers: RecognizerFile[] = get(recognizersSelectorFamily(projectId)); - return getLuProvider(dialogId, recognizers); + return dialogs.map((dialog) => { + return { + ...dialog, + luProvider: getLuProvider(dialog.id, recognizers), + }; + }); }, }); diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts index bd43df2dd7..af3627dfed 100644 --- a/Composer/packages/client/src/shell/useShell.ts +++ b/Composer/packages/client/src/shell/useShell.ts @@ -27,7 +27,6 @@ import { settingsState, clipboardActionsState, schemasState, - dialogsSelectorFamily, focusPathState, localeState, qnaFilesState, @@ -40,7 +39,7 @@ import { featureFlagsState, } from '../recoilModel'; import { undoFunctionState } from '../recoilModel/undo/history'; -import { skillsStateSelector } from '../recoilModel/selectors'; +import { dialogsWithLuProviderSelectorFamily, skillsStateSelector } from '../recoilModel/selectors'; import { navigateTo } from '../utils/navigation'; import TelemetryClient from '../telemetry/TelemetryClient'; import { lgFilesSelectorFamily } from '../recoilModel/selectors/lg'; @@ -79,7 +78,7 @@ export function useShell(source: EventSource, projectId: string): Shell { const dialogMapRef = useRef({}); const schemas = useRecoilValue(schemasState(projectId)); - const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); + const dialogs = useRecoilValue(dialogsWithLuProviderSelectorFamily(projectId)); const focusPath = useRecoilValue(focusPathState(projectId)); const skills = useRecoilValue(skillsStateSelector); const locale = useRecoilValue(localeState(projectId)); From 02d6396ddb6984d05090015e9d25409e553e766e Mon Sep 17 00:00:00 2001 From: leilzh Date: Mon, 11 Jan 2021 18:50:17 +0800 Subject: [PATCH 04/12] fix unit test --- .../pages/language-generation/LGPage.test.tsx | 4 ++-- .../pages/notifications/diagnosticList.test.tsx | 4 ++-- .../pages/notifications/diagnostics.test.tsx | 4 ++-- .../client/__tests__/shell/lgApi.test.tsx | 4 ++-- .../client/__tests__/shell/triggerApi.test.tsx | 4 ++-- .../dispatchers/__tests__/dialog.test.tsx | 7 +++---- .../recoilModel/dispatchers/__tests__/lg.test.tsx | 7 ++++--- .../dispatchers/__tests__/multilang.test.tsx | 7 +++---- .../dispatchers/__tests__/project.test.tsx | 5 ++--- .../dispatchers/__tests__/trigger.test.tsx | 15 ++++----------- .../client/src/recoilModel/selectors/index.ts | 1 + .../recoilModel/undo/__test__/history.test.tsx | 5 ++--- 12 files changed, 29 insertions(+), 38 deletions(-) diff --git a/Composer/packages/client/__tests__/pages/language-generation/LGPage.test.tsx b/Composer/packages/client/__tests__/pages/language-generation/LGPage.test.tsx index 34e95360e0..45b296d81a 100644 --- a/Composer/packages/client/__tests__/pages/language-generation/LGPage.test.tsx +++ b/Composer/packages/client/__tests__/pages/language-generation/LGPage.test.tsx @@ -9,7 +9,7 @@ import TableView from '../../../src/pages/language-generation/table-view'; import CodeEditor from '../../../src/pages/language-generation/code-editor'; import { localeState, - lgFilesState, + lgFilesSelectorFamily, settingsState, schemasState, dialogsSelectorFamily, @@ -51,7 +51,7 @@ const initRecoilState = ({ set }) => { set(currentProjectIdState, state.projectId); set(localeState(state.projectId), state.locale); set(dialogsSelectorFamily(state.projectId), state.dialogs); - set(lgFilesState(state.projectId), state.lgFiles); + set(lgFilesSelectorFamily(state.projectId), state.lgFiles); set(settingsState(state.projectId), state.settings); set(schemasState(state.projectId), mockProjectResponse.schemas); }; diff --git a/Composer/packages/client/__tests__/pages/notifications/diagnosticList.test.tsx b/Composer/packages/client/__tests__/pages/notifications/diagnosticList.test.tsx index 109dd25c97..91e6c46e38 100644 --- a/Composer/packages/client/__tests__/pages/notifications/diagnosticList.test.tsx +++ b/Composer/packages/client/__tests__/pages/notifications/diagnosticList.test.tsx @@ -13,7 +13,7 @@ import { dialogIdsState, formDialogSchemaIdsState, jsonSchemaFilesState, - lgFilesState, + lgFilesSelectorFamily, luFilesState, schemasState, settingsState, @@ -111,7 +111,7 @@ describe('', () => { set(botProjectIdsState, [state.projectId]); set(dialogIdsState(state.projectId), []); set(luFilesState(state.projectId), state.luFiles); - set(lgFilesState(state.projectId), state.lgFiles); + set(lgFilesSelectorFamily(state.projectId), state.lgFiles); set(jsonSchemaFilesState(state.projectId), state.jsonSchemaFiles); set(botDiagnosticsState(state.projectId), state.diagnostics); set(settingsState(state.projectId), state.settings); diff --git a/Composer/packages/client/__tests__/pages/notifications/diagnostics.test.tsx b/Composer/packages/client/__tests__/pages/notifications/diagnostics.test.tsx index ffe2d9e7b8..99bef63d4d 100644 --- a/Composer/packages/client/__tests__/pages/notifications/diagnostics.test.tsx +++ b/Composer/packages/client/__tests__/pages/notifications/diagnostics.test.tsx @@ -13,7 +13,7 @@ import { dialogState, formDialogSchemaIdsState, jsonSchemaFilesState, - lgFilesState, + lgFilesSelectorFamily, luFilesState, qnaFilesState, schemasState, @@ -142,7 +142,7 @@ describe('', () => { set(dialogIdsState(state.projectId), ['test']); set(dialogState({ projectId: state.projectId, dialogId: 'test' }), state.dialogs[0]); set(luFilesState(state.projectId), state.luFiles); - set(lgFilesState(state.projectId), state.lgFiles); + set(lgFilesSelectorFamily(state.projectId), state.lgFiles); set(qnaFilesState(state.projectId), state.qnaFiles); set(jsonSchemaFilesState(state.projectId), state.jsonSchemaFiles); set(botDiagnosticsState(state.projectId), state.diagnostics); diff --git a/Composer/packages/client/__tests__/shell/lgApi.test.tsx b/Composer/packages/client/__tests__/shell/lgApi.test.tsx index 007c0f6da7..5dd26e2814 100644 --- a/Composer/packages/client/__tests__/shell/lgApi.test.tsx +++ b/Composer/packages/client/__tests__/shell/lgApi.test.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { RecoilRoot } from 'recoil'; import { useLgApi } from '../../src/shell/lgApi'; -import { lgFilesState, localeState, dispatcherState, currentProjectIdState } from '../../src/recoilModel'; +import { lgFilesSelectorFamily, localeState, dispatcherState, currentProjectIdState } from '../../src/recoilModel'; import { Dispatcher } from '../../src/recoilModel/dispatchers'; const state = { @@ -42,7 +42,7 @@ describe('use lgApi hooks', () => { initRecoilState = ({ set }) => { set(currentProjectIdState, state.projectId); set(localeState(state.projectId), 'en-us'); - set(lgFilesState(state.projectId), state.lgFiles); + set(lgFilesSelectorFamily(state.projectId), state.lgFiles); set(dispatcherState, (current: Dispatcher) => ({ ...current, updateLgTemplate: updateLgTemplateMock, diff --git a/Composer/packages/client/__tests__/shell/triggerApi.test.tsx b/Composer/packages/client/__tests__/shell/triggerApi.test.tsx index 4557f5df7d..f40e61ce11 100644 --- a/Composer/packages/client/__tests__/shell/triggerApi.test.tsx +++ b/Composer/packages/client/__tests__/shell/triggerApi.test.tsx @@ -9,7 +9,7 @@ import { useTriggerApi } from '../../src/shell/triggerApi'; import { localeState, luFilesState, - lgFilesState, + lgFilesSelectorFamily, dialogsSelectorFamily, schemasState, dispatcherState, @@ -53,7 +53,7 @@ describe('use triggerApi hooks', () => { set(currentProjectIdState, state.projectId); set(localeState(state.projectId), 'en-us'); set(luFilesState(state.projectId), state.luFiles); - set(lgFilesState(state.projectId), state.lgFiles); + set(lgFilesSelectorFamily(state.projectId), state.lgFiles); set(dialogsSelectorFamily(state.projectId), state.dialogs); set(schemasState(state.projectId), state.schemas); set(dispatcherState, (current: Dispatcher) => ({ 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 1b4d232dc0..f650216c1f 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialog.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialog.test.tsx @@ -8,7 +8,6 @@ import { act, HookResult } from '@botframework-composer/test-utils/lib/hooks'; import { dialogsDispatcher } from '../dialogs'; import { renderRecoilHook } from '../../../../__tests__/testUtils'; import { - lgFilesState, luFilesState, schemasState, dialogSchemasState, @@ -17,7 +16,7 @@ import { showCreateDialogModalState, qnaFilesState, } from '../../atoms'; -import { dialogsSelectorFamily } from '../../selectors'; +import { dialogsSelectorFamily, lgFilesSelectorFamily } from '../../selectors'; import { dispatcherState } from '../../../recoilModel/DispatcherWrapper'; import { Dispatcher } from '..'; @@ -106,7 +105,7 @@ describe('dialog dispatcher', () => { const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); const dialogSchemas = useRecoilValue(dialogSchemasState(projectId)); const luFiles = useRecoilValue(luFilesState(projectId)); - const lgFiles = useRecoilValue(lgFilesState(projectId)); + const lgFiles = useRecoilValue(lgFilesSelectorFamily(projectId)); const actionsSeed = useRecoilValue(actionsSeedState(projectId)); const onCreateDialogComplete = useRecoilValue(onCreateDialogCompleteState(projectId)); const showCreateDialogModal = useRecoilValue(showCreateDialogModalState); @@ -133,7 +132,7 @@ describe('dialog dispatcher', () => { states: [ { recoilState: dialogsSelectorFamily(projectId), initialValue: [{ id: '1' }, { id: '2' }] }, { recoilState: dialogSchemasState(projectId), initialValue: [{ id: '1' }, { id: '2' }] }, - { recoilState: lgFilesState(projectId), initialValue: [{ id: '1.en-us' }, { id: '2.en-us' }] }, + { recoilState: lgFilesSelectorFamily(projectId), initialValue: [{ id: '1.en-us' }, { id: '2.en-us' }] }, { recoilState: luFilesState(projectId), initialValue: [{ id: '1.en-us' }, { id: '2.en-us' }] }, { recoilState: qnaFilesState(projectId), initialValue: [{ id: '1.en-us' }, { id: '2.en-us' }] }, { recoilState: schemasState(projectId), initialValue: { sdk: { content: '' } } }, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lg.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lg.test.tsx index f066164769..3dd15f7e20 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lg.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lg.test.tsx @@ -8,9 +8,10 @@ import { act, HookResult } from '@botframework-composer/test-utils/lib/hooks'; import { lgDispatcher } from '../lg'; import { renderRecoilHook } from '../../../../__tests__/testUtils'; -import { lgFilesState, currentProjectIdState } from '../../atoms'; +import { currentProjectIdState } from '../../atoms'; import { dispatcherState } from '../../../recoilModel/DispatcherWrapper'; import { Dispatcher } from '..'; +import { lgFilesSelectorFamily } from '../../selectors'; const projectId = '123asad.123sad'; @@ -57,7 +58,7 @@ const getLgTemplate = (name, body): LgTemplate => describe('Lg dispatcher', () => { const useRecoilTestHook = () => { - const [lgFiles, setLgFiles] = useRecoilState(lgFilesState(projectId)); + const [lgFiles, setLgFiles] = useRecoilState(lgFilesSelectorFamily(projectId)); const currentDispatcher = useRecoilValue(dispatcherState); return { @@ -72,7 +73,7 @@ describe('Lg dispatcher', () => { beforeEach(() => { const { result } = renderRecoilHook(useRecoilTestHook, { states: [ - { recoilState: lgFilesState(projectId), initialValue: lgFiles }, + { recoilState: lgFilesSelectorFamily(projectId), initialValue: lgFiles }, { recoilState: currentProjectIdState, initialValue: projectId }, ], dispatcher: { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/multilang.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/multilang.test.tsx index c6ba250880..a82ba6e692 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/multilang.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/multilang.test.tsx @@ -7,7 +7,6 @@ import { act, HookResult } from '@botframework-composer/test-utils/lib/hooks'; import { renderRecoilHook } from '../../../../__tests__/testUtils'; import { luFilesState, - lgFilesState, settingsState, localeState, actionsSeedState, @@ -15,7 +14,7 @@ import { onDelLanguageDialogCompleteState, currentProjectIdState, } from '../../atoms'; -import { dialogsSelectorFamily } from '../../selectors'; +import { dialogsSelectorFamily, lgFilesSelectorFamily } from '../../selectors'; import { dispatcherState } from '../../../recoilModel/DispatcherWrapper'; import { Dispatcher } from '..'; import { multilangDispatcher } from '../multilang'; @@ -45,7 +44,7 @@ describe('Multilang dispatcher', () => { const locale = useRecoilValue(localeState(state.projectId)); const settings = useRecoilValue(settingsState(state.projectId)); const luFiles = useRecoilValue(luFilesState(state.projectId)); - const lgFiles = useRecoilValue(lgFilesState(state.projectId)); + const lgFiles = useRecoilValue(lgFilesSelectorFamily(state.projectId)); const onAddLanguageDialogComplete = useRecoilValue(onAddLanguageDialogCompleteState(state.projectId)); const onDelLanguageDialogComplete = useRecoilValue(onDelLanguageDialogCompleteState(state.projectId)); @@ -72,7 +71,7 @@ describe('Multilang dispatcher', () => { { recoilState: currentProjectIdState, initialValue: state.projectId }, { recoilState: dialogsSelectorFamily(state.projectId), initialValue: state.dialogs }, { recoilState: localeState(state.projectId), initialValue: state.locale }, - { recoilState: lgFilesState(state.projectId), initialValue: state.lgFiles }, + { recoilState: lgFilesSelectorFamily(state.projectId), initialValue: state.lgFiles }, { recoilState: luFilesState(state.projectId), initialValue: state.luFiles }, { recoilState: settingsState(state.projectId), initialValue: state.settings }, ], diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx index 3126536cb2..0b193c7953 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx @@ -25,7 +25,6 @@ import { currentProjectIdState, skillManifestsState, luFilesState, - lgFilesState, settingsState, botEnvironmentState, botDiagnosticsState, @@ -41,7 +40,7 @@ import { botErrorState, botProjectSpaceLoadedState, } from '../../atoms'; -import { dialogsSelectorFamily } from '../../selectors'; +import { dialogsSelectorFamily, lgFilesSelectorFamily } from '../../selectors'; import { dispatcherState } from '../../../recoilModel/DispatcherWrapper'; import { Dispatcher } from '../../dispatchers'; import { BotStatus } from '../../../constants'; @@ -115,7 +114,7 @@ describe('Project dispatcher', () => { const botName = useRecoilValue(botDisplayNameState(projectId)); const skillManifests = useRecoilValue(skillManifestsState(projectId)); const luFiles = useRecoilValue(luFilesState(projectId)); - const lgFiles = useRecoilValue(lgFilesState(projectId)); + const lgFiles = useRecoilValue(lgFilesSelectorFamily(projectId)); const settings = useRecoilValue(settingsState(projectId)); const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); const botEnvironment = useRecoilValue(botEnvironmentState(projectId)); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/trigger.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/trigger.test.tsx index b9ab88e66e..0353097c5d 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/trigger.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/trigger.test.tsx @@ -10,15 +10,8 @@ import { lgDispatcher } from '../lg'; import { luDispatcher } from '../lu'; import { navigationDispatcher } from '../navigation'; import { renderRecoilHook } from '../../../../__tests__/testUtils'; -import { - lgFilesState, - luFilesState, - schemasState, - dialogSchemasState, - actionsSeedState, - qnaFilesState, -} from '../../atoms'; -import { dialogsSelectorFamily } from '../../selectors'; +import { luFilesState, schemasState, dialogSchemasState, actionsSeedState, qnaFilesState } from '../../atoms'; +import { dialogsSelectorFamily, lgFilesSelectorFamily } from '../../selectors'; import { dispatcherState } from '../../../recoilModel/DispatcherWrapper'; import { Dispatcher } from '..'; import { DialogInfo } from '../../../../../types'; @@ -106,7 +99,7 @@ describe('trigger dispatcher', () => { const dialogs: DialogInfo[] = useRecoilValue(dialogsSelectorFamily(projectId)); const dialogSchemas = useRecoilValue(dialogSchemasState(projectId)); const luFiles = useRecoilValue(luFilesState(projectId)); - const lgFiles = useRecoilValue(lgFilesState(projectId)); + const lgFiles = useRecoilValue(lgFilesSelectorFamily(projectId)); const actionsSeed = useRecoilValue(actionsSeedState(projectId)); const qnaFiles = useRecoilValue(qnaFilesState(projectId)); const currentDispatcher = useRecoilValue(dispatcherState); @@ -135,7 +128,7 @@ describe('trigger dispatcher', () => { }, { recoilState: dialogSchemasState(projectId), initialValue: [{ id: '1' }, { id: '2' }] }, { - recoilState: lgFilesState(projectId), + recoilState: lgFilesSelectorFamily(projectId), initialValue: [ { id: '1.en-us', content: '' }, { id: '2.en-us', content: '' }, diff --git a/Composer/packages/client/src/recoilModel/selectors/index.ts b/Composer/packages/client/src/recoilModel/selectors/index.ts index 573cc0c578..f3c6ed49af 100644 --- a/Composer/packages/client/src/recoilModel/selectors/index.ts +++ b/Composer/packages/client/src/recoilModel/selectors/index.ts @@ -11,3 +11,4 @@ export * from './projectTemplates'; export * from './skills'; export * from './localRuntimeBuilder'; export * from './diagnosticsPageSelector'; +export * from './lg'; diff --git a/Composer/packages/client/src/recoilModel/undo/__test__/history.test.tsx b/Composer/packages/client/src/recoilModel/undo/__test__/history.test.tsx index d4e781d4e9..26f6f9f443 100644 --- a/Composer/packages/client/src/recoilModel/undo/__test__/history.test.tsx +++ b/Composer/packages/client/src/recoilModel/undo/__test__/history.test.tsx @@ -9,7 +9,6 @@ import { HookResult } from '@botframework-composer/test-utils/lib/hooks'; import { UndoRoot, undoFunctionState, undoHistoryState } from '../history'; import { - lgFilesState, luFilesState, projectMetaDataState, currentProjectIdState, @@ -18,7 +17,7 @@ import { canUndoState, canRedoState, } from '../../atoms'; -import { dialogsSelectorFamily } from '../../selectors'; +import { dialogsSelectorFamily, lgFilesSelectorFamily } from '../../selectors'; import { renderRecoilHook } from '../../../../__tests__/testUtils/react-recoil-hooks-testing-library'; import UndoHistory from '../undoHistory'; import { undoStatusSelectorFamily } from '../../selectors/undo'; @@ -72,7 +71,7 @@ describe('', () => { states: [ { recoilState: botProjectIdsState, initialValue: [projectId] }, { recoilState: dialogsSelectorFamily(projectId), initialValue: [{ id: '1', content: '' }] }, - { recoilState: lgFilesState(projectId), initialValue: [{ id: '1.lg' }, { id: '2' }] }, + { recoilState: lgFilesSelectorFamily(projectId), initialValue: [{ id: '1.lg' }, { id: '2' }] }, { recoilState: luFilesState(projectId), initialValue: [{ id: '1.lu' }, { id: '2' }] }, { recoilState: currentProjectIdState, initialValue: projectId }, { recoilState: undoHistoryState(projectId), initialValue: new UndoHistory(projectId) }, From ffea593a89225da44254fee370262e68a1014503 Mon Sep 17 00:00:00 2001 From: leilzh Date: Mon, 11 Jan 2021 21:03:27 +0800 Subject: [PATCH 05/12] add cache for dialog validate --- .../recoilModel/selectors/validatedDialogs.ts | 16 +++++- .../validations/expressionValidation/index.ts | 31 +++++++---- .../validations/expressionValidation/types.ts | 7 ++- .../expressionValidation/validation.ts | 27 ++-------- .../lib/indexers/src/validations/index.ts | 52 +++++++++---------- 5 files changed, 71 insertions(+), 62 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts index d024790320..709fbae682 100644 --- a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts +++ b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts @@ -10,6 +10,8 @@ import { getLuProvider } from '../../utils/dialogUtil'; import { recognizersSelectorFamily } from './recognizers'; import { dialogsSelectorFamily } from './dialogs'; +import { ClientStorage } from './../../utils/storage'; +const dialogCache = new ClientStorage(window.sessionStorage, 'dialogCache'); type validateDialogSelectorFamilyParams = { projectId: string; dialogId: string }; export const dialogsWithLuProviderSelectorFamily = selectorFamily({ @@ -34,6 +36,18 @@ export const dialogDiagnosticsSelectorFamily = selectorFamily({ const locale = get(localeState(projectId)); const lgFile: LgFile = get(lgFileState({ projectId, lgFileId: `${dialogId}.${locale}` })); const settings: DialogSetting = get(settingsState(projectId)); - return validateDialog(dialog, schemas.sdk.content, settings, [lgFile], []) as Diagnostic[]; + const cacheId = `${projectId}-${dialogId}`; + + const { diagnostics, cache } = validateDialog( + dialog, + schemas.sdk.content, + settings, + [lgFile], + [], + dialogCache.get(cacheId) + ); + dialogCache.set(cacheId, cache); + + return diagnostics; }, }); diff --git a/Composer/packages/lib/indexers/src/validations/expressionValidation/index.ts b/Composer/packages/lib/indexers/src/validations/expressionValidation/index.ts index e103523010..094a8ec5a5 100644 --- a/Composer/packages/lib/indexers/src/validations/expressionValidation/index.ts +++ b/Composer/packages/lib/indexers/src/validations/expressionValidation/index.ts @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { MicrosoftIDialog, Diagnostic, LgFile, DialogSetting } from '@bfc/shared'; +import { MicrosoftIDialog, Diagnostic, LgFile, DialogSetting, DiagnosticSeverity, LuFile } from '@bfc/shared'; import { SchemaDefinitions } from '@bfc/shared/lib/schemaUtils/types'; import { extractOptionByKey } from '../../utils/lgUtil'; import { searchExpressions } from './searchExpression'; -import { ValidateFunc } from './types'; -import { validate } from './validation'; +import { ExpressionParseResult, ValidateFunc } from './types'; +import { checkExpression, checkReturnType, filterCustomFunctionError } from './validation'; const NamespaceKey = '@namespace'; const ExportsKey = '@exports'; @@ -37,16 +37,29 @@ export const validateExpressions: ValidateFunc = ( type: string, schema: SchemaDefinitions, settings: DialogSetting, - lgFiles: LgFile[] + lgFiles: LgFile[], + luFiles: LuFile[], + cache?: ExpressionParseResult ) => { const expressions = searchExpressions(path, value, type, schema); const customFunctions = searchLgCustomFunction(lgFiles).concat(settings.customFunctions); - + const newCache = {}; const diagnostics = expressions.reduce((diagnostics: Diagnostic[], expression) => { - const diagnostic = validate(expression, customFunctions); - if (diagnostic) diagnostics.push(diagnostic); + const { required, path, types, value } = expression; + let errorMessage = ''; + + try { + newCache[value] = cache && cache[value] ? cache[value] : checkExpression(value, required); + errorMessage = checkReturnType(newCache[value], types); + } catch (error) { + errorMessage = filterCustomFunctionError(error.message, customFunctions); + } + + if (!errorMessage) return diagnostics; + + diagnostics.push(new Diagnostic(errorMessage, '', DiagnosticSeverity.Error, path)); + return diagnostics; }, []); - - return diagnostics; + return { diagnostics, cache: newCache }; }; diff --git a/Composer/packages/lib/indexers/src/validations/expressionValidation/types.ts b/Composer/packages/lib/indexers/src/validations/expressionValidation/types.ts index 8915244b6d..21201f655d 100644 --- a/Composer/packages/lib/indexers/src/validations/expressionValidation/types.ts +++ b/Composer/packages/lib/indexers/src/validations/expressionValidation/types.ts @@ -13,6 +13,8 @@ export const StringMapExpressionType = { integer: ReturnType.Number, }; +export type ExpressionParseResult = { [content: string]: number }; + export type ValidateFunc = ( path: string, value: MicrosoftIDialog, @@ -20,8 +22,9 @@ export type ValidateFunc = ( schema: SchemaDefinitions, setting: DialogSetting, lgFiles: LgFile[], - luFiles: LuFile[] -) => Diagnostic[] | null; // error msg + luFiles: LuFile[], + cache?: ExpressionParseResult +) => { diagnostics: Diagnostic[] | null; cache: ExpressionParseResult }; // error msg export type ExpressionProperty = { value: any; diff --git a/Composer/packages/lib/indexers/src/validations/expressionValidation/validation.ts b/Composer/packages/lib/indexers/src/validations/expressionValidation/validation.ts index 2ee43207bc..f7e4b2b9f6 100644 --- a/Composer/packages/lib/indexers/src/validations/expressionValidation/validation.ts +++ b/Composer/packages/lib/indexers/src/validations/expressionValidation/validation.ts @@ -4,9 +4,6 @@ /* eslint-disable no-bitwise */ import { Expression, ReturnType } from 'adaptive-expressions'; import formatMessage from 'format-message'; -import { Diagnostic } from '@bfc/shared'; - -import { ExpressionProperty } from './types'; const EMPTY = formatMessage(`is missing or empty`); const RETURNTYPE_NOT_MATCH = formatMessage('the return type does not match'); @@ -69,7 +66,7 @@ export const checkExpression = (exp: any, required: boolean): number => { //The return type should match the schema type // the return type use binary number to store // if returnType = 24, the expression result is 16+8. so the type is string or array -const checkReturnType = (returnType: number, types: number[]): string => { +export const checkReturnType = (returnType: number, types: number[]): string => { // if return type contain object do nothing. if (returnType & ReturnType.Object) return ''; @@ -77,7 +74,7 @@ const checkReturnType = (returnType: number, types: number[]): string => { }; //string match or * match -const checkCustomFunctions = (currentFunction: string, customFunction: string) => { +export const checkCustomFunctions = (currentFunction: string, customFunction: string) => { //.* or * => # let customReg = customFunction.replace(/\.\*|\*/g, '#'); //. => [.] @@ -90,7 +87,7 @@ const checkCustomFunctions = (currentFunction: string, customFunction: string) = return reg.test(currentFunction); }; -const filterCustomFunctionError = (error: string, CustomFunctions: string[]): string => { +export const filterCustomFunctionError = (error: string, CustomFunctions: string[]): string => { //Now all customFunctions is from lg file content. //If the custom functions are defined in runtime, use the field from settings to filter if (error.endsWith(BUILT_IN_FUNCTION_ERROR)) { @@ -104,21 +101,3 @@ const filterCustomFunctionError = (error: string, CustomFunctions: string[]): st return expressionErrorMessage(error); }; - -export const validate = (expression: ExpressionProperty, customFunctions: string[]): Diagnostic | null => { - const { required, path, types, value } = expression; - let errorMessage = ''; - - try { - const returnType = checkExpression(value, required); - errorMessage = checkReturnType(returnType, types); - } catch (error) { - errorMessage = filterCustomFunctionError(error.message, customFunctions); - } - - if (!errorMessage) return null; - - const diagnostic = new Diagnostic(errorMessage, ''); - diagnostic.path = path; - return diagnostic; -}; diff --git a/Composer/packages/lib/indexers/src/validations/index.ts b/Composer/packages/lib/indexers/src/validations/index.ts index 4a8787ab65..c07268c7bf 100644 --- a/Composer/packages/lib/indexers/src/validations/index.ts +++ b/Composer/packages/lib/indexers/src/validations/index.ts @@ -6,7 +6,7 @@ import has from 'lodash/has'; import { JsonWalk, VisitorFunc } from '..'; import { validateExpressions } from './expressionValidation/index'; -import { ValidateFunc } from './expressionValidation/types'; +import { ExpressionParseResult, ValidateFunc } from './expressionValidation/types'; export const validateFuncs: { [type: string]: ValidateFunc[] } = { '.': [validateExpressions], //this will check all types @@ -17,11 +17,13 @@ export function validateDialog( schema: SchemaDefinitions, settings: DialogSetting, lgFiles: LgFile[], - luFiles: LuFile[] -): Diagnostic[] { + luFiles: LuFile[], + cache?: ExpressionParseResult +): { diagnostics: Diagnostic[] | null; cache?: ExpressionParseResult } { const { id, content } = dialog; try { const diagnostics: Diagnostic[] = []; + let newCache: ExpressionParseResult = {}; /** * * @param path , jsonPath string @@ -37,35 +39,33 @@ export function validateDialog( } allChecks.forEach((func) => { - const result = func(path, value, value.$kind, schema.definitions[value.$kind], settings, lgFiles, luFiles); - if (result) { - diagnostics.push(...result); + const result = func( + path, + value, + value.$kind, + schema.definitions[value.$kind], + settings, + lgFiles, + luFiles, + cache + ); + if (result.diagnostics) { + diagnostics.push(...result.diagnostics); } + newCache = { ...newCache, ...result.cache }; }); } return false; }; JsonWalk(id, content, visitor); - return diagnostics.map((e) => { - e.source = id; - return e; - }); + return { + diagnostics: diagnostics.map((e) => { + e.source = id; + return e; + }), + cache: newCache, + }; } catch (error) { - return [new Diagnostic(error.message, id)]; + return { diagnostics: [new Diagnostic(error.message, id)], cache }; } } - -export function validateDialogs( - dialogs: DialogInfo[], - schema: SchemaDefinitions, - lgFiles: LgFile[], - luFiles: LuFile[], - settings: DialogSetting -): { [id: string]: Diagnostic[] } { - const diagnosticsMap = {}; - dialogs.forEach((dialog) => { - diagnosticsMap[dialog.id] = validateDialog(dialog, schema, settings, lgFiles, luFiles); - }); - - return diagnosticsMap; -} From 6b2c1dba335c79d5872075eae1b9db0843fdcb52 Mon Sep 17 00:00:00 2001 From: leilzh Date: Tue, 12 Jan 2021 11:41:42 +0800 Subject: [PATCH 06/12] fix lint --- .../client/src/pages/design/PropertyEditor.tsx | 2 +- .../src/recoilModel/dispatchers/utils/project.ts | 1 - .../client/src/recoilModel/parsers/types.ts | 13 +------------ .../selectors/diagnosticsPageSelector.ts | 3 ++- .../src/recoilModel/selectors/validatedDialogs.ts | 2 +- 5 files changed, 5 insertions(+), 16 deletions(-) diff --git a/Composer/packages/client/src/pages/design/PropertyEditor.tsx b/Composer/packages/client/src/pages/design/PropertyEditor.tsx index 8d69431d18..a42d74dddc 100644 --- a/Composer/packages/client/src/pages/design/PropertyEditor.tsx +++ b/Composer/packages/client/src/pages/design/PropertyEditor.tsx @@ -82,7 +82,7 @@ const PropertyEditor: React.FC = () => { }, [$schema, formUIOptions]); const errors = useMemo(() => { - if (diagnostics.length) { + if (diagnostics && diagnostics.length) { const currentPath = focusPath.replace('#', ''); return diagnostics.reduce((errors, d) => { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 77c1ccd6ca..1df6055f3d 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -20,7 +20,6 @@ import { SensitiveProperties, RootBotManagedProperties, defaultPublishConfig, - Diagnostic, } from '@bfc/shared'; import formatMessage from 'format-message'; import camelCase from 'lodash/camelCase'; diff --git a/Composer/packages/client/src/recoilModel/parsers/types.ts b/Composer/packages/client/src/recoilModel/parsers/types.ts index ce1f39d367..a5e0c484fb 100644 --- a/Composer/packages/client/src/recoilModel/parsers/types.ts +++ b/Composer/packages/client/src/recoilModel/parsers/types.ts @@ -1,17 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { - LuIntentSection, - LgFile, - LuFile, - QnASection, - FileInfo, - LgTemplate, - ILUFeaturesConfig, - DialogInfo, - DialogSetting, - SchemaDefinitions, -} from '@bfc/shared'; +import { LuIntentSection, LgFile, LuFile, QnASection, FileInfo, LgTemplate, ILUFeaturesConfig } from '@bfc/shared'; export type LuParsePayload = { id: string; diff --git a/Composer/packages/client/src/recoilModel/selectors/diagnosticsPageSelector.ts b/Composer/packages/client/src/recoilModel/selectors/diagnosticsPageSelector.ts index 3eef1f8500..4b2e429718 100644 --- a/Composer/packages/client/src/recoilModel/selectors/diagnosticsPageSelector.ts +++ b/Composer/packages/client/src/recoilModel/selectors/diagnosticsPageSelector.ts @@ -163,7 +163,8 @@ export const dialogsDiagnosticsSelectorFamily = selectorFamily({ const diagnosticList: DiagnosticInfo[] = []; dialogIds.forEach((dialogId: string) => { - get(dialogDiagnosticsSelectorFamily({ projectId, dialogId })).forEach((diagnostic) => { + const diagnostics = get(dialogDiagnosticsSelectorFamily({ projectId, dialogId })) || []; + diagnostics.forEach((diagnostic) => { const location = `${dialogId}.dialog`; diagnosticList.push(new DialogDiagnostic(rootProjectId, projectId, dialogId, location, diagnostic)); }); diff --git a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts index 709fbae682..77289b5881 100644 --- a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts +++ b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { selectorFamily } from 'recoil'; -import { DialogInfo, BotSchemas, LgFile, DialogSetting, RecognizerFile, Diagnostic } from '@bfc/shared'; +import { DialogInfo, BotSchemas, LgFile, DialogSetting, RecognizerFile } from '@bfc/shared'; import { validateDialog } from '@bfc/indexers'; import { schemasState, dialogState, settingsState, localeState, lgFileState } from '../atoms'; From a1f5a82b6676ed8f108b1b374657301f1a458ec8 Mon Sep 17 00:00:00 2001 From: leilzh Date: Tue, 12 Jan 2021 12:30:58 +0800 Subject: [PATCH 07/12] fix comments --- .../client/src/recoilModel/atoms/botState.ts | 16 +++++++++--- .../client/src/recoilModel/dispatchers/lg.ts | 26 +++++++++---------- .../src/recoilModel/selectors/dialogs.ts | 7 ++++- .../recoilModel/selectors/validatedDialogs.ts | 4 ++- .../lib/indexers/src/validations/index.ts | 2 +- 5 files changed, 36 insertions(+), 19 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index 9248aeaae5..ef59d2f431 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -47,12 +47,22 @@ const emptyDialog: DialogInfo = { skills: [], isFormDialog: false, }; -type lgStateParams = { projectId: string; lgFileId: string }; -export const lgFileState = atomFamily({ +const emptyLg: LgFile = { + id: '', + content: '', + diagnostics: [], + templates: [], + allTemplates: [], + imports: [], +}; + +type LgStateParams = { projectId: string; lgFileId: string }; + +export const lgFileState = atomFamily({ key: getFullyQualifiedKey('lg'), default: () => { - return {} as LgFile; + return emptyLg; }, }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts index fcafcbf369..44ec3c2874 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts @@ -30,7 +30,7 @@ const initialBody = '- '; * */ -const lgFilesAtomUpdate = ( +const updateLgFiles = ( { set }: CallbackInterface, projectId: string, oldList: LgFile[], @@ -153,7 +153,7 @@ export const createLgFileState = async ( }); }); - lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { adds: changes }); + updateLgFiles(callbackHelpers, projectId, lgFiles, { adds: changes }); } catch (error) { setError(callbackHelpers, error); } @@ -173,7 +173,7 @@ export const removeLgFileState = async ( return; } - lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { deletes: [targetLgFile] }); + updateLgFiles(callbackHelpers, projectId, lgFiles, { deletes: [targetLgFile] }); }; export const lgDispatcher = () => { @@ -228,10 +228,10 @@ export const lgDispatcher = () => { // compare to drop expired change on current id lg file. /** * Why other methods do not need double check content? - * Because this method already did set content before call lgFilesAtomUpdater. + * Because this method already did set content before call updateLgFiles. */ - lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }, (prevLgFiles) => { + updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }, (prevLgFiles) => { const targetInState = prevLgFiles.find((file) => file.id === updatedFile.id); const targetInCurrentChange = updatedFiles.find((file) => file.id === updatedFile.id); // compare to drop expired content already setted above. @@ -293,7 +293,7 @@ export const lgDispatcher = () => { )) as LgFile; changes.push(updatedFile); } - lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: changes }); + updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: changes }); } else { // body change, only update current locale file const updatedFile = (await LgWorker.updateTemplate( @@ -303,7 +303,7 @@ export const lgDispatcher = () => { { body: template.body }, lgFiles )) as LgFile; - lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: [updatedFile] }); + updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: [updatedFile] }); } } catch (error) { setError(callbackHelpers, error); @@ -334,7 +334,7 @@ export const lgDispatcher = () => { if (!lgFile) return lgFiles; const updatedFile = (await LgWorker.addTemplate(projectId, lgFile, template, lgFiles)) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); + updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); } ); @@ -355,7 +355,7 @@ export const lgDispatcher = () => { if (!lgFile) return lgFiles; const updatedFile = (await LgWorker.addTemplates(projectId, lgFile, templates, lgFiles)) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); + updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -380,7 +380,7 @@ export const lgDispatcher = () => { const updatedFile = (await LgWorker.removeTemplate(projectId, lgFile, templateName, lgFiles)) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); + updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -406,7 +406,7 @@ export const lgDispatcher = () => { const updatedFile = (await LgWorker.removeTemplates(projectId, lgFile, templateNames, lgFiles)) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); + updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -438,7 +438,7 @@ export const lgDispatcher = () => { lgFiles )) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); + updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -455,7 +455,7 @@ export const lgDispatcher = () => { const reparsedFile = (await LgDiagnosticWorker.parse(projectId, file.id, file.content, lgFiles)) as LgFile; reparsedLgFiles.push({ ...file, diagnostics: reparsedFile.diagnostics }); } - lgFilesAtomUpdate(callbackHelpers, projectId, lgFiles, { updates: reparsedLgFiles }, (prevLgFiles) => { + updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: reparsedLgFiles }, (prevLgFiles) => { // compare to drop expired content already setted above. return (target) => { const targetInState = prevLgFiles.find((file) => file.id === target.id); diff --git a/Composer/packages/client/src/recoilModel/selectors/dialogs.ts b/Composer/packages/client/src/recoilModel/selectors/dialogs.ts index 06f0173b3f..91019b0afd 100644 --- a/Composer/packages/client/src/recoilModel/selectors/dialogs.ts +++ b/Composer/packages/client/src/recoilModel/selectors/dialogs.ts @@ -4,11 +4,16 @@ import { DialogInfo } from '@bfc/shared'; import { selectorFamily } from 'recoil'; -import { dialogIdsState, dialogState } from '../atoms'; +import { botProjectIdsState, dialogIdsState, dialogState } from '../atoms'; export const dialogsSelectorFamily = selectorFamily({ key: 'dialogs', get: (projectId: string) => ({ get }) => { + const loadedProjects = get(botProjectIdsState); + if (!loadedProjects.includes(projectId)) { + return []; + } + const dialogIds = get(dialogIdsState(projectId)); return dialogIds.map((dialogId) => { diff --git a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts index 77289b5881..39d0475f64 100644 --- a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts +++ b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts @@ -5,7 +5,7 @@ import { selectorFamily } from 'recoil'; import { DialogInfo, BotSchemas, LgFile, DialogSetting, RecognizerFile } from '@bfc/shared'; import { validateDialog } from '@bfc/indexers'; -import { schemasState, dialogState, settingsState, localeState, lgFileState } from '../atoms'; +import { schemasState, dialogState, settingsState, localeState, lgFileState, projectMetaDataState } from '../atoms'; import { getLuProvider } from '../../utils/dialogUtil'; import { recognizersSelectorFamily } from './recognizers'; @@ -31,6 +31,8 @@ export const dialogsWithLuProviderSelectorFamily = selectorFamily({ export const dialogDiagnosticsSelectorFamily = selectorFamily({ key: 'dialogDiagnosticsSelectorFamily', get: ({ projectId, dialogId }: validateDialogSelectorFamilyParams) => ({ get }) => { + if (get(projectMetaDataState(projectId)).isRemote) return []; + const dialog: DialogInfo = get(dialogState({ projectId, dialogId })); const schemas: BotSchemas = get(schemasState(projectId)); const locale = get(localeState(projectId)); diff --git a/Composer/packages/lib/indexers/src/validations/index.ts b/Composer/packages/lib/indexers/src/validations/index.ts index c07268c7bf..d88ead354e 100644 --- a/Composer/packages/lib/indexers/src/validations/index.ts +++ b/Composer/packages/lib/indexers/src/validations/index.ts @@ -31,7 +31,7 @@ export function validateDialog( * @return boolean, true to stop walk * */ const visitor: VisitorFunc = (path: string, value: any): boolean => { - if (has(value, '$kind')) { + if (has(value, '$kind') && value.$kind) { const allChecks = [...validateFuncs['.']]; const checkerFunc = validateFuncs[value.$kind]; if (checkerFunc) { From 1b6a7e3dbcda570f8dc2a0f3d5940c7f9ecba11f Mon Sep 17 00:00:00 2001 From: leilzh Date: Tue, 12 Jan 2021 13:35:06 +0800 Subject: [PATCH 08/12] fix tests --- .../client/src/recoilModel/dispatchers/lg.ts | 61 ++++++++----------- .../src/recoilModel/selectors/dialogs.ts | 7 +-- 2 files changed, 25 insertions(+), 43 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts index 44ec3c2874..4ad66ef272 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts @@ -33,37 +33,34 @@ const initialBody = '- '; const updateLgFiles = ( { set }: CallbackInterface, projectId: string, - oldList: LgFile[], changes: { adds?: LgFile[]; deletes?: LgFile[]; updates?: LgFile[]; }, - filter?: (oldList: LgFile[]) => (changeItem: LgFile) => boolean + needUpdate?: (current: LgFile, changed: LgFile) => boolean ) => { - const updates = changes.updates ? (filter ? changes.updates.filter(filter(oldList)) : changes.updates) : []; - const adds = changes.adds ? (filter ? changes.adds.filter(filter(oldList)) : changes.adds) : []; - const deletes = changes.deletes - ? filter - ? changes.deletes.filter(filter(oldList)).map(({ id }) => id) - : changes.deletes.map(({ id }) => id) - : []; + const { updates, adds, deletes } = changes; // updates - updates.forEach((lgFile) => { - set(lgFileState({ projectId, lgFileId: lgFile.id }), lgFile); + updates?.forEach((lgFile) => { + set(lgFileState({ projectId, lgFileId: lgFile.id }), (preFile) => + needUpdate ? (needUpdate(preFile, lgFile) ? lgFile : preFile) : lgFile + ); }); // deletes - if (deletes.length) { + if (deletes?.length) { set(lgFileIdsState(projectId), (ids) => ids.filter((id) => !deletes.includes(id))); } // adds - if (adds.length) { + if (adds?.length) { set(lgFileIdsState(projectId), (ids) => ids.concat(adds.map((file) => file.id))); adds.forEach((lgFile) => { - set(lgFileState({ projectId, lgFileId: lgFile.id }), lgFile); + set(lgFileState({ projectId, lgFileId: lgFile.id }), (preFile) => + needUpdate ? (needUpdate(preFile, lgFile) ? lgFile : preFile) : lgFile + ); }); } }; @@ -153,7 +150,7 @@ export const createLgFileState = async ( }); }); - updateLgFiles(callbackHelpers, projectId, lgFiles, { adds: changes }); + updateLgFiles(callbackHelpers, projectId, { adds: changes }); } catch (error) { setError(callbackHelpers, error); } @@ -173,7 +170,7 @@ export const removeLgFileState = async ( return; } - updateLgFiles(callbackHelpers, projectId, lgFiles, { deletes: [targetLgFile] }); + updateLgFiles(callbackHelpers, projectId, { deletes: [targetLgFile] }); }; export const lgDispatcher = () => { @@ -231,15 +228,9 @@ export const lgDispatcher = () => { * Because this method already did set content before call updateLgFiles. */ - updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }, (prevLgFiles) => { - const targetInState = prevLgFiles.find((file) => file.id === updatedFile.id); - const targetInCurrentChange = updatedFiles.find((file) => file.id === updatedFile.id); + updateLgFiles(callbackHelpers, projectId, { updates: updatedFiles }, (current, changed) => { // compare to drop expired content already setted above. - if (targetInState?.content !== targetInCurrentChange?.content) { - return (lgFile) => lgFile.id !== updatedFile.id; - } else { - return () => true; - } + return current?.content === changed?.content; }); // if changes happen on common.lg, async re-parse all. @@ -293,7 +284,7 @@ export const lgDispatcher = () => { )) as LgFile; changes.push(updatedFile); } - updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: changes }); + updateLgFiles(callbackHelpers, projectId, { updates: changes }); } else { // body change, only update current locale file const updatedFile = (await LgWorker.updateTemplate( @@ -303,7 +294,7 @@ export const lgDispatcher = () => { { body: template.body }, lgFiles )) as LgFile; - updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: [updatedFile] }); + updateLgFiles(callbackHelpers, projectId, { updates: [updatedFile] }); } } catch (error) { setError(callbackHelpers, error); @@ -334,7 +325,7 @@ export const lgDispatcher = () => { if (!lgFile) return lgFiles; const updatedFile = (await LgWorker.addTemplate(projectId, lgFile, template, lgFiles)) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); + updateLgFiles(callbackHelpers, projectId, { updates: updatedFiles }); } ); @@ -355,7 +346,7 @@ export const lgDispatcher = () => { if (!lgFile) return lgFiles; const updatedFile = (await LgWorker.addTemplates(projectId, lgFile, templates, lgFiles)) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); + updateLgFiles(callbackHelpers, projectId, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -380,7 +371,7 @@ export const lgDispatcher = () => { const updatedFile = (await LgWorker.removeTemplate(projectId, lgFile, templateName, lgFiles)) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); + updateLgFiles(callbackHelpers, projectId, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -406,7 +397,7 @@ export const lgDispatcher = () => { const updatedFile = (await LgWorker.removeTemplates(projectId, lgFile, templateNames, lgFiles)) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); + updateLgFiles(callbackHelpers, projectId, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -438,7 +429,7 @@ export const lgDispatcher = () => { lgFiles )) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: updatedFiles }); + updateLgFiles(callbackHelpers, projectId, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -455,13 +446,9 @@ export const lgDispatcher = () => { const reparsedFile = (await LgDiagnosticWorker.parse(projectId, file.id, file.content, lgFiles)) as LgFile; reparsedLgFiles.push({ ...file, diagnostics: reparsedFile.diagnostics }); } - updateLgFiles(callbackHelpers, projectId, lgFiles, { updates: reparsedLgFiles }, (prevLgFiles) => { + updateLgFiles(callbackHelpers, projectId, { updates: reparsedLgFiles }, (current, changed) => { // compare to drop expired content already setted above. - return (target) => { - const targetInState = prevLgFiles.find((file) => file.id === target.id); - const targetInCurrentChange = reparsedLgFiles.find((file) => file.id === target.id); - return targetInState?.content === targetInCurrentChange?.content; - }; + return current?.content === changed?.content; }); } catch (error) { setError(callbackHelpers, error); diff --git a/Composer/packages/client/src/recoilModel/selectors/dialogs.ts b/Composer/packages/client/src/recoilModel/selectors/dialogs.ts index 91019b0afd..06f0173b3f 100644 --- a/Composer/packages/client/src/recoilModel/selectors/dialogs.ts +++ b/Composer/packages/client/src/recoilModel/selectors/dialogs.ts @@ -4,16 +4,11 @@ import { DialogInfo } from '@bfc/shared'; import { selectorFamily } from 'recoil'; -import { botProjectIdsState, dialogIdsState, dialogState } from '../atoms'; +import { dialogIdsState, dialogState } from '../atoms'; export const dialogsSelectorFamily = selectorFamily({ key: 'dialogs', get: (projectId: string) => ({ get }) => { - const loadedProjects = get(botProjectIdsState); - if (!loadedProjects.includes(projectId)) { - return []; - } - const dialogIds = get(dialogIdsState(projectId)); return dialogIds.map((dialogId) => { From c172424976b540df2cbe0335213a592a8113433c Mon Sep 17 00:00:00 2001 From: leilzh Date: Tue, 12 Jan 2021 16:17:10 +0800 Subject: [PATCH 09/12] fix lint --- Composer/packages/client/src/recoilModel/dispatchers/lg.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts index 4ad66ef272..333275ea95 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts @@ -51,7 +51,7 @@ const updateLgFiles = ( // deletes if (deletes?.length) { - set(lgFileIdsState(projectId), (ids) => ids.filter((id) => !deletes.includes(id))); + set(lgFileIdsState(projectId), (ids) => ids.filter((id) => !deletes.map((file) => file.id).includes(id))); } // adds From 9217c486044a05f475ad2c659e2a03ccada7c463 Mon Sep 17 00:00:00 2001 From: leilzh Date: Tue, 12 Jan 2021 18:06:19 +0800 Subject: [PATCH 10/12] update text --- Composer/packages/client/src/recoilModel/selectors/lg.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/selectors/lg.ts b/Composer/packages/client/src/recoilModel/selectors/lg.ts index a6c3580dae..a6e6d272a9 100644 --- a/Composer/packages/client/src/recoilModel/selectors/lg.ts +++ b/Composer/packages/client/src/recoilModel/selectors/lg.ts @@ -9,9 +9,9 @@ import { lgFileIdsState, lgFileState } from '../atoms'; export const lgFilesSelectorFamily = selectorFamily({ key: 'lgFiles', get: (projectId: string) => ({ get }) => { - const dialogIds = get(lgFileIdsState(projectId)); + const lgFileIds = get(lgFileIdsState(projectId)); - return dialogIds.map((lgFileId) => { + return lgFileIds.map((lgFileId) => { return get(lgFileState({ projectId, lgFileId })); }); }, From 19d9652f0f76209f3bcc4405e7e555489384aac7 Mon Sep 17 00:00:00 2001 From: leilzh Date: Wed, 13 Jan 2021 09:07:49 +0800 Subject: [PATCH 11/12] fix lint --- Composer/packages/client/src/pages/design/PropertyEditor.tsx | 2 +- Composer/packages/client/src/recoilModel/dispatchers/project.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Composer/packages/client/src/pages/design/PropertyEditor.tsx b/Composer/packages/client/src/pages/design/PropertyEditor.tsx index a42d74dddc..771f184e10 100644 --- a/Composer/packages/client/src/pages/design/PropertyEditor.tsx +++ b/Composer/packages/client/src/pages/design/PropertyEditor.tsx @@ -82,7 +82,7 @@ const PropertyEditor: React.FC = () => { }, [$schema, formUIOptions]); const errors = useMemo(() => { - if (diagnostics && diagnostics.length) { + if (diagnostics?.length) { const currentPath = focusPath.replace('#', ''); return diagnostics.reduce((errors, d) => { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 84f157c32a..86b235aad7 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -251,7 +251,6 @@ export const projectDispatcher = () => { callback(projectId); } } catch (ex) { - console.log(ex); set(botProjectIdsState, []); removeRecentProject(callbackHelpers, path); handleProjectFailure(callbackHelpers, ex); From d6ebe578adbb52eddbd4154c1c0869a663d18b2f Mon Sep 17 00:00:00 2001 From: leilzh Date: Wed, 13 Jan 2021 10:02:08 +0800 Subject: [PATCH 12/12] fix lint --- .../lib/indexers/src/validations/expressionValidation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/lib/indexers/src/validations/expressionValidation/index.ts b/Composer/packages/lib/indexers/src/validations/expressionValidation/index.ts index 094a8ec5a5..23013ccd81 100644 --- a/Composer/packages/lib/indexers/src/validations/expressionValidation/index.ts +++ b/Composer/packages/lib/indexers/src/validations/expressionValidation/index.ts @@ -49,7 +49,7 @@ export const validateExpressions: ValidateFunc = ( let errorMessage = ''; try { - newCache[value] = cache && cache[value] ? cache[value] : checkExpression(value, required); + newCache[value] = cache?.[value] ? cache[value] : checkExpression(value, required); errorMessage = checkReturnType(newCache[value], types); } catch (error) { errorMessage = filterCustomFunctionError(error.message, customFunctions);