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/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 9e6d359039..a592d16a25 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 dea2048fa7..b44cce95c5 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 { 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/pages/design/VisualEditor.tsx b/Composer/packages/client/src/pages/design/VisualEditor.tsx index b75bd730c7..74ffc7a710 100644 --- a/Composer/packages/client/src/pages/design/VisualEditor.tsx +++ b/Composer/packages/client/src/pages/design/VisualEditor.tsx @@ -13,12 +13,7 @@ import { useFormConfig, useShellApi } from '@bfc/extension-client'; import cloneDeep from 'lodash/cloneDeep'; import grayComposerIcon from '../../images/grayComposerIcon.svg'; -import { - dispatcherState, - validateDialogsSelectorFamily, - schemasState, - designPageLocationState, -} from '../../recoilModel'; +import { dispatcherState, dialogsSelectorFamily, schemasState, designPageLocationState } from '../../recoilModel'; import { middleTriggerContainer, middleTriggerElements, triggerButton, visualEditor } from './styles'; @@ -72,7 +67,7 @@ const VisualEditor: 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/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 dce9adf08b..18274b064c 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/table-view.tsx b/Composer/packages/client/src/pages/language-understanding/table-view.tsx index 1a146debe0..a974ba276d 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..ef59d2f431 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -47,6 +47,32 @@ const emptyDialog: DialogInfo = { skills: [], isFormDialog: false, }; + +const emptyLg: LgFile = { + id: '', + content: '', + diagnostics: [], + templates: [], + allTemplates: [], + imports: [], +}; + +type LgStateParams = { projectId: string; lgFileId: string }; + +export const lgFileState = atomFamily({ + key: getFullyQualifiedKey('lg'), + default: () => { + return emptyLg; + }, +}); + +export const lgFileIdsState = atomFamily({ + key: getFullyQualifiedKey('lgFileIds'), + default: () => { + return []; + }, +}); + type dialogStateParams = { projectId: string; dialogId: string }; export const dialogState = atomFamily({ key: getFullyQualifiedKey('dialog'), @@ -124,13 +150,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/__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 a51c1d42cf..6c994cde9e 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/dispatchers/dialogs.ts b/Composer/packages/client/src/recoilModel/dispatchers/dialogs.ts index 12c61e6422..283be2a732 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/dialogs.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/dialogs.ts @@ -2,15 +2,12 @@ // 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 { dialogIndexer, autofixReferInDialog } from '@bfc/indexers'; import { DialogInfo, checkForPVASchema } from '@bfc/shared'; import { - lgFilesState, - luFilesState, dialogIdsState, schemasState, - settingsState, onCreateDialogCompleteState, actionsSeedState, showCreateDialogModalState, @@ -83,11 +80,8 @@ 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 = validateDialog(dialog, schemas.sdk.content, settings, lgFiles, luFiles); + 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..333275ea95 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,37 +30,39 @@ const initialBody = '- '; * */ -const lgFilesAtomUpdater = ( +const updateLgFiles = ( + { set }: CallbackInterface, + projectId: string, changes: { adds?: LgFile[]; deletes?: LgFile[]; updates?: LgFile[]; }, - filter?: (oldList: LgFile[]) => (changeItem: LgFile) => boolean + needUpdate?: (current: LgFile, changed: 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; - }); + const { updates, adds, deletes } = changes; - // deletes - newList = newList.filter((file) => !deletes.includes(file.id)); + // updates + updates?.forEach((lgFile) => { + set(lgFileState({ projectId, lgFileId: lgFile.id }), (preFile) => + needUpdate ? (needUpdate(preFile, lgFile) ? lgFile : preFile) : lgFile + ); + }); - // adds - newList = adds.concat(newList); + // deletes + if (deletes?.length) { + set(lgFileIdsState(projectId), (ids) => ids.filter((id) => !deletes.map((file) => file.id).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 }), (preFile) => + needUpdate ? (needUpdate(preFile, lgFile) ? lgFile : preFile) : lgFile + ); + }); + } }; // sync lg file structure across locales, it take times, computed changes may be expired at next tick. @@ -121,8 +124,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 +150,7 @@ export const createLgFileState = async ( }); }); - set(lgFilesState(projectId), lgFilesAtomUpdater({ adds: changes })); + updateLgFiles(callbackHelpers, projectId, { adds: changes }); } catch (error) { setError(callbackHelpers, error); } @@ -157,8 +160,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 +170,7 @@ export const removeLgFileState = async ( return; } - set(lgFilesState(projectId), lgFilesAtomUpdater({ deletes: [targetLgFile] })); + updateLgFiles(callbackHelpers, projectId, { deletes: [targetLgFile] }); }; export const lgDispatcher = () => { @@ -208,40 +211,27 @@ 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); // 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. */ - 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; - } - }) - ); + + updateLgFiles(callbackHelpers, projectId, { updates: updatedFiles }, (current, changed) => { + // compare to drop expired content already setted above. + return current?.content === changed?.content; + }); // if changes happen on common.lg, async re-parse all. if (getBaseName(id) === 'common') { @@ -266,8 +256,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 +284,7 @@ export const lgDispatcher = () => { )) as LgFile; changes.push(updatedFile); } - set(lgFilesState(projectId), lgFilesAtomUpdater({ updates: changes })); + updateLgFiles(callbackHelpers, projectId, { updates: changes }); } else { // body change, only update current locale file const updatedFile = (await LgWorker.updateTemplate( @@ -304,7 +294,7 @@ export const lgDispatcher = () => { { body: template.body }, lgFiles )) as LgFile; - set(lgFilesState(projectId), lgFilesAtomUpdater({ updates: [updatedFile] })); + updateLgFiles(callbackHelpers, projectId, { updates: [updatedFile] }); } } catch (error) { setError(callbackHelpers, error); @@ -320,7 +310,7 @@ export const lgDispatcher = () => { ); const createLgTemplate = useRecoilCallback( - ({ set, snapshot }: CallbackInterface) => async ({ + (callbackHelpers: CallbackInterface) => async ({ id, template, projectId, @@ -329,12 +319,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 })); + updateLgFiles(callbackHelpers, projectId, { updates: updatedFiles }); } ); @@ -349,13 +340,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 })); + updateLgFiles(callbackHelpers, projectId, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -372,15 +363,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 })); + updateLgFiles(callbackHelpers, projectId, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -398,15 +389,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 })); + updateLgFiles(callbackHelpers, projectId, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -426,8 +417,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 +429,7 @@ export const lgDispatcher = () => { lgFiles )) as LgFile; const updatedFiles = await getRelatedLgFileChanges(projectId, lgFiles, updatedFile); - set(lgFilesState(projectId), lgFilesAtomUpdater({ updates: updatedFiles })); + updateLgFiles(callbackHelpers, projectId, { updates: updatedFiles }); } catch (error) { setError(callbackHelpers, error); } @@ -448,24 +439,17 @@ 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; - }; - }) - ); + updateLgFiles(callbackHelpers, projectId, { updates: reparsedLgFiles }, (current, changed) => { + // compare to drop expired content already setted above. + return current?.content === changed?.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 91a15653fd..1df6055f3d 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, @@ -58,7 +58,6 @@ import { formDialogSchemaIdsState, formDialogSchemaState, jsonSchemaFilesState, - lgFilesState, localeState, locationState, luFilesState, @@ -83,6 +82,7 @@ import { undoHistoryState } from '../../undo/history'; import UndoHistory from '../../undo/undoHistory'; import { logMessage, setError } from '../shared'; import { setRootBotSettingState } from '../setting'; +import { lgFilesSelectorFamily } from '../../selectors/lg'; import { crossTrainConfigState } from './../../atoms/botState'; import { recognizersSelectorFamily } from './../../selectors/recognizers'; @@ -357,14 +357,15 @@ 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); - }); + } set(dialogIdsState(projectId), dialogIds); set(recognizersSelectorFamily(projectId), recognizers); @@ -383,7 +384,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..4b2e429718 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,25 @@ 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) => { + const diagnostics = get(dialogDiagnosticsSelectorFamily({ projectId, dialogId })) || []; + diagnostics.forEach((diagnostic) => { + const location = `${dialogId}.dialog`; + diagnosticList.push(new DialogDiagnostic(rootProjectId, projectId, dialogId, location, diagnostic)); }); }); - return diagnosticList; + return []; }, }); @@ -242,7 +244,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/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/selectors/lg.ts b/Composer/packages/client/src/recoilModel/selectors/lg.ts new file mode 100644 index 0000000000..a6e6d272a9 --- /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 lgFileIds = get(lgFileIdsState(projectId)); + + return lgFileIds.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 306d210d34..b203820022 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,11 @@ import { import { dialogsSelectorFamily, buildEssentialsSelector, - validateDialogsSelectorFamily, lgImportsSelectorFamily, luImportsSelectorFamily, } from '../selectors'; +import { lgFilesSelectorFamily } from './lg'; // Selector return types export type TreeDataPerProject = { isRemote: boolean; @@ -127,9 +126,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)); @@ -216,7 +215,7 @@ export const perProjectDiagnosticsSelectorFamily = selectorFamily({ 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)); @@ -277,7 +276,7 @@ export const projectTreeSelectorFamily = selectorFamily< const projectIds = get(botProjectIdsState); return projectIds.map((projectId: string) => { const { isRemote, isRootBot } = get(projectMetaDataState(projectId)); - const dialogs = get(validateDialogsSelectorFamily(projectId)); + const dialogs = get(dialogsSelectorFamily(projectId)); const sortedDialogs = [...dialogs].sort((x, y) => { if (x.isRoot) { return -1; diff --git a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts index c6b829043d..39d0475f64 100644 --- a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts +++ b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts @@ -2,52 +2,54 @@ // Licensed under the MIT License. import { selectorFamily } from 'recoil'; +import { DialogInfo, BotSchemas, LgFile, DialogSetting, RecognizerFile } from '@bfc/shared'; import { validateDialog } from '@bfc/indexers'; -import { DialogInfo, BotSchemas, LgFile, LuFile, DialogSetting, RecognizerFile } from '@bfc/shared'; - -import { - botProjectIdsState, - dialogIdsState, - schemasState, - lgFilesState, - luFilesState, - dialogState, - settingsState, -} from '../atoms'; + +import { schemasState, dialogState, settingsState, localeState, lgFileState, projectMetaDataState } from '../atoms'; 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 }; -const validateDialogSelectorFamily = selectorFamily({ - key: 'validateDialogSelectorFamily', +export const dialogsWithLuProviderSelectorFamily = selectorFamily({ + key: 'dialogLuProviderSelectorFamily', + get: (projectId: string) => ({ get }) => { + const dialogs = get(dialogsSelectorFamily(projectId)); + const recognizers: RecognizerFile[] = get(recognizersSelectorFamily(projectId)); + return dialogs.map((dialog) => { + return { + ...dialog, + luProvider: getLuProvider(dialog.id, recognizers), + }; + }); + }, +}); + +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 lgFiles: LgFile[] = get(lgFilesState(projectId)); - const luFiles: LuFile[] = get(luFilesState(projectId)); + const locale = get(localeState(projectId)); + const lgFile: LgFile = get(lgFileState({ projectId, lgFileId: `${dialogId}.${locale}` })); const settings: DialogSetting = get(settingsState(projectId)); - const recognizers: RecognizerFile[] = get(recognizersSelectorFamily(projectId)); - const luProvider = getLuProvider(dialogId, recognizers); - return { - ...dialog, - diagnostics: validateDialog(dialog, schemas.sdk.content, settings, lgFiles, luFiles), - luProvider, - }; - }, -}); + const cacheId = `${projectId}-${dialogId}`; -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 })); - }); + const { diagnostics, cache } = validateDialog( + dialog, + schemas.sdk.content, + settings, + [lgFile], + [], + dialogCache.get(cacheId) + ); + dialogCache.set(cacheId, cache); + + return diagnostics; }, }); 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) }, 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..af3627dfed 100644 --- a/Composer/packages/client/src/shell/useShell.ts +++ b/Composer/packages/client/src/shell/useShell.ts @@ -27,23 +27,22 @@ import { settingsState, clipboardActionsState, schemasState, - validateDialogsSelectorFamily, focusPathState, localeState, qnaFilesState, designPageLocationState, botDisplayNameState, dialogSchemasState, - lgFilesState, luFilesState, rateInfoState, rootBotProjectIdSelector, 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'; import { useLgApi } from './lgApi'; import { useLuApi } from './luApi'; @@ -79,7 +78,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(dialogsWithLuProviderSelectorFamily(projectId)); const focusPath = useRecoilValue(focusPathState(projectId)); const skills = useRecoilValue(skillsStateSelector); const locale = useRecoilValue(localeState(projectId)); @@ -88,7 +87,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)); diff --git a/Composer/packages/lib/indexers/src/validations/expressionValidation/index.ts b/Composer/packages/lib/indexers/src/validations/expressionValidation/index.ts index e103523010..23013ccd81 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?.[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..d88ead354e 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 @@ -29,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) { @@ -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; -}