diff --git a/Composer/packages/client/__tests__/pages/botProjectsSettings/RootBotExternalService.test.tsx b/Composer/packages/client/__tests__/pages/botProjectsSettings/RootBotExternalService.test.tsx
index acee253a04..a2986a5986 100644
--- a/Composer/packages/client/__tests__/pages/botProjectsSettings/RootBotExternalService.test.tsx
+++ b/Composer/packages/client/__tests__/pages/botProjectsSettings/RootBotExternalService.test.tsx
@@ -13,7 +13,7 @@ import {
projectMetaDataState,
botProjectIdsState,
dialogState,
- luFilesState,
+ luFilesSelectorFamily,
} from '../../../src/recoilModel';
const state = {
@@ -63,7 +63,7 @@ describe('Root Bot External Service', () => {
set(dialogState({ projectId: state.projectId, dialogId: state.dialogs[0].id }), state.dialogs[0]);
set(dialogState({ projectId: state.projectId, dialogId: state.dialogs[1].id }), state.dialogs[1]);
set(botProjectIdsState, state.botProjectIdsState);
- set(luFilesState(state.projectId), state.luFiles);
+ set(luFilesSelectorFamily(state.projectId), state.luFiles);
set(projectMetaDataState(state.projectId), state.projectMetaDataState);
set(settingsState(state.projectId), state.settings);
set(dispatcherState, {
diff --git a/Composer/packages/client/__tests__/pages/design/DebugPanel/diagnosticList.test.tsx b/Composer/packages/client/__tests__/pages/design/DebugPanel/diagnosticList.test.tsx
index 302aaf7ae0..a8d24a5c0d 100644
--- a/Composer/packages/client/__tests__/pages/design/DebugPanel/diagnosticList.test.tsx
+++ b/Composer/packages/client/__tests__/pages/design/DebugPanel/diagnosticList.test.tsx
@@ -14,7 +14,7 @@ import {
formDialogSchemaIdsState,
jsonSchemaFilesState,
lgFilesSelectorFamily,
- luFilesState,
+ luFilesSelectorFamily,
schemasState,
settingsState,
} from '../../../../src/recoilModel';
@@ -110,7 +110,7 @@ describe('', () => {
set(currentProjectIdState, state.projectId);
set(botProjectIdsState, [state.projectId]);
set(dialogIdsState(state.projectId), []);
- set(luFilesState(state.projectId), state.luFiles);
+ set(luFilesSelectorFamily(state.projectId), state.luFiles);
set(lgFilesSelectorFamily(state.projectId), state.lgFiles);
set(jsonSchemaFilesState(state.projectId), state.jsonSchemaFiles);
set(botDiagnosticsState(state.projectId), state.diagnostics);
diff --git a/Composer/packages/client/__tests__/pages/design/useAssetsParsingState.test.tsx b/Composer/packages/client/__tests__/pages/design/useAssetsParsingState.test.tsx
new file mode 100644
index 0000000000..20af6191bf
--- /dev/null
+++ b/Composer/packages/client/__tests__/pages/design/useAssetsParsingState.test.tsx
@@ -0,0 +1,92 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { renderHook } from '@botframework-composer/test-utils/lib/hooks';
+import * as React from 'react';
+import { useSetRecoilState } from 'recoil';
+import { RecoilRoot } from 'recoil';
+import { act } from '@botframework-composer/test-utils/lib/hooks';
+
+import { useAssetsParsingState } from '../../../src/pages/design/useAssetsParsingState';
+import {
+ currentProjectIdState,
+ designPageLocationState,
+ lgFileState,
+ localeState,
+ luFileState,
+ qnaFileState,
+} from '../../../src/recoilModel';
+
+const state = {
+ luFiles: [
+ {
+ id: 'test.en-us',
+ isContentUnparsed: false,
+ },
+ ],
+ lgFiles: [
+ {
+ id: 'test.en-us',
+ isContentUnparsed: false,
+ },
+ ],
+ qnaFiles: [
+ {
+ id: 'test.en-us',
+ isContentUnparsed: false,
+ },
+ ],
+ focusPath: '',
+ locale: 'en-us',
+ projectId: 'test',
+};
+
+const useRecoilTestHook = (projectId: string) => {
+ const setLuFile = useSetRecoilState(luFileState({ projectId, luFileId: 'test.en-us' }));
+ const isParsing = useAssetsParsingState(state.projectId);
+
+ return { setLuFile, isParsing };
+};
+
+describe('useAssetsParsingState', () => {
+ let result;
+ beforeEach(() => {
+ const initRecoilState = ({ set }) => {
+ set(designPageLocationState(state.projectId), { dialogId: 'test' });
+ set(currentProjectIdState, state.projectId);
+ set(localeState(state.projectId), 'en-us');
+ set(luFileState({ projectId: state.projectId, luFileId: 'test.en-us' }), state.luFiles);
+ set(lgFileState({ projectId: state.projectId, lgFileId: 'test.en-us' }), state.luFiles);
+ set(qnaFileState({ projectId: state.projectId, qnaFileId: 'test.en-us' }), state.luFiles);
+ };
+
+ const wrapper = (props: { children?: React.ReactNode }) => {
+ const { children } = props;
+ return {children};
+ };
+
+ const rendered = renderHook(() => useRecoilTestHook(state.projectId), {
+ wrapper,
+ });
+ result = rendered.result;
+ });
+
+ it('should be true if the file content is unparsed', async () => {
+ const { setLuFile } = result.current;
+ expect(result.current.isParsing).toBeFalsy();
+ act(() => {
+ setLuFile({
+ id: 'test.en-us',
+ isContentUnparsed: true,
+ });
+ });
+ expect(result.current.isParsing).toBeTruthy();
+ act(() => {
+ setLuFile({
+ id: 'test.en-us',
+ isContentUnparsed: false,
+ });
+ });
+ expect(result.current.isParsing).toBeFalsy();
+ });
+});
diff --git a/Composer/packages/client/__tests__/pages/knowledge-base/QnAPage.test.tsx b/Composer/packages/client/__tests__/pages/knowledge-base/QnAPage.test.tsx
index 98edc46d0c..ef5d85281e 100644
--- a/Composer/packages/client/__tests__/pages/knowledge-base/QnAPage.test.tsx
+++ b/Composer/packages/client/__tests__/pages/knowledge-base/QnAPage.test.tsx
@@ -10,7 +10,7 @@ import { renderWithRecoil } from '../../testUtils';
import {
localeState,
dialogsSelectorFamily,
- qnaFilesState,
+ qnaFilesSelectorFamily,
settingsState,
schemasState,
dispatcherState,
@@ -59,7 +59,7 @@ const initRecoilState = ({ set }) => {
set(currentProjectIdState, state.projectId);
set(localeState(state.projectId), state.locale);
set(dialogsSelectorFamily(state.projectId), state.dialogs);
- set(qnaFilesState(state.projectId), state.qnaFiles);
+ set(qnaFilesSelectorFamily(state.projectId), state.qnaFiles);
set(settingsState(state.projectId), state.settings);
set(schemasState(state.projectId), mockProjectResponse.schemas);
set(dispatcherState, {
diff --git a/Composer/packages/client/__tests__/pages/language-understanding/LUPage.test.tsx b/Composer/packages/client/__tests__/pages/language-understanding/LUPage.test.tsx
index 3e1bf307c3..909950eb8b 100644
--- a/Composer/packages/client/__tests__/pages/language-understanding/LUPage.test.tsx
+++ b/Composer/packages/client/__tests__/pages/language-understanding/LUPage.test.tsx
@@ -10,7 +10,7 @@ import { renderWithRecoil } from '../../testUtils';
import {
localeState,
dialogsSelectorFamily,
- luFilesState,
+ luFilesSelectorFamily,
settingsState,
schemasState,
currentProjectIdState,
@@ -50,7 +50,7 @@ const initRecoilState = ({ set }) => {
set(currentProjectIdState, state.projectId);
set(localeState(state.projectId), state.locale);
set(dialogsSelectorFamily(state.projectId), state.dialogs);
- set(luFilesState(state.projectId), state.luFiles);
+ set(luFilesSelectorFamily(state.projectId), state.luFiles);
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 820ec06c2b..eb788bc841 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 {
formDialogSchemaIdsState,
jsonSchemaFilesState,
lgFilesSelectorFamily,
- luFilesState,
+ luFilesSelectorFamily,
schemasState,
settingsState,
} from '../../../src/recoilModel';
@@ -110,7 +110,7 @@ describe('', () => {
set(currentProjectIdState, state.projectId);
set(botProjectIdsState, [state.projectId]);
set(dialogIdsState(state.projectId), []);
- set(luFilesState(state.projectId), state.luFiles);
+ set(luFilesSelectorFamily(state.projectId), state.luFiles);
set(lgFilesSelectorFamily(state.projectId), state.lgFiles);
set(jsonSchemaFilesState(state.projectId), state.jsonSchemaFiles);
set(botDiagnosticsState(state.projectId), state.diagnostics);
diff --git a/Composer/packages/client/__tests__/pages/notifications/diagnostics.test.tsx b/Composer/packages/client/__tests__/pages/notifications/diagnostics.test.tsx
index 02efef8bae..792e59b3b3 100644
--- a/Composer/packages/client/__tests__/pages/notifications/diagnostics.test.tsx
+++ b/Composer/packages/client/__tests__/pages/notifications/diagnostics.test.tsx
@@ -14,8 +14,8 @@ import {
formDialogSchemaIdsState,
jsonSchemaFilesState,
lgFilesSelectorFamily,
- luFilesState,
- qnaFilesState,
+ luFilesSelectorFamily,
+ qnaFilesSelectorFamily,
schemasState,
settingsState,
} from '../../../src/recoilModel';
@@ -142,9 +142,9 @@ describe('', () => {
set(botProjectIdsState, [state.projectId]);
set(dialogIdsState(state.projectId), ['test']);
set(dialogState({ projectId: state.projectId, dialogId: 'test' }), state.dialogs[0]);
- set(luFilesState(state.projectId), state.luFiles);
+ set(luFilesSelectorFamily(state.projectId), state.luFiles);
set(lgFilesSelectorFamily(state.projectId), state.lgFiles);
- set(qnaFilesState(state.projectId), state.qnaFiles);
+ set(qnaFilesSelectorFamily(state.projectId), state.qnaFiles);
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__/shell/luApi.test.tsx b/Composer/packages/client/__tests__/shell/luApi.test.tsx
index 4c910d47eb..cfb1d1646c 100644
--- a/Composer/packages/client/__tests__/shell/luApi.test.tsx
+++ b/Composer/packages/client/__tests__/shell/luApi.test.tsx
@@ -6,7 +6,7 @@ import * as React from 'react';
import { RecoilRoot } from 'recoil';
import { useLuApi } from '../../src/shell/luApi';
-import { localeState, luFilesState, dispatcherState, currentProjectIdState } from '../../src/recoilModel';
+import { localeState, luFilesSelectorFamily, dispatcherState, currentProjectIdState } from '../../src/recoilModel';
import { Dispatcher } from '../../src/recoilModel/dispatchers';
jest.mock('../../src/recoilModel/parsers/luWorker', () => {
@@ -34,7 +34,7 @@ describe('use luApi hooks', () => {
const initRecoilState = ({ set }) => {
set(currentProjectIdState, state.projectId);
set(localeState(state.projectId), 'en-us');
- set(luFilesState(state.projectId), state.luFiles);
+ set(luFilesSelectorFamily(state.projectId), state.luFiles);
set(dispatcherState, (current: Dispatcher) => ({
...current,
updateLuFile: updateLuFileMockMock,
diff --git a/Composer/packages/client/__tests__/shell/triggerApi.test.tsx b/Composer/packages/client/__tests__/shell/triggerApi.test.tsx
index f40e61ce11..0b527c9507 100644
--- a/Composer/packages/client/__tests__/shell/triggerApi.test.tsx
+++ b/Composer/packages/client/__tests__/shell/triggerApi.test.tsx
@@ -8,7 +8,7 @@ import { RecoilRoot } from 'recoil';
import { useTriggerApi } from '../../src/shell/triggerApi';
import {
localeState,
- luFilesState,
+ luFilesSelectorFamily,
lgFilesSelectorFamily,
dialogsSelectorFamily,
schemasState,
@@ -52,7 +52,7 @@ describe('use triggerApi hooks', () => {
const initRecoilState = ({ set }) => {
set(currentProjectIdState, state.projectId);
set(localeState(state.projectId), 'en-us');
- set(luFilesState(state.projectId), state.luFiles);
+ set(luFilesSelectorFamily(state.projectId), state.luFiles);
set(lgFilesSelectorFamily(state.projectId), state.lgFiles);
set(dialogsSelectorFamily(state.projectId), state.dialogs);
set(schemasState(state.projectId), state.schemas);
diff --git a/Composer/packages/client/src/components/BotRuntimeController/publishDialog.tsx b/Composer/packages/client/src/components/BotRuntimeController/publishDialog.tsx
index 9d57ba68bf..d60362b8f7 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, dialogsSelectorFamily } from '../../recoilModel';
+import { luFilesSelectorFamily, qnaFilesSelectorFamily, dialogsSelectorFamily } from '../../recoilModel';
// -------------------- Styles -------------------- //
const textFieldLabel = css`
@@ -91,8 +91,8 @@ interface IPublishDialogProps {
export const PublishDialog: React.FC = (props) => {
const { isOpen, onDismiss, onPublish, botName, config, projectId } = props;
const dialogs = useRecoilValue(dialogsSelectorFamily(projectId));
- const luFiles = useRecoilValue(luFilesState(projectId));
- const qnaFiles = useRecoilValue(qnaFilesState(projectId));
+ const luFiles = useRecoilValue(luFilesSelectorFamily(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesSelectorFamily(projectId));
const qnaConfigShow = getReferredQnaFiles(qnaFiles, dialogs).length > 0;
const luConfigShow = getLuisBuildLuFiles(luFiles, dialogs).length > 0;
diff --git a/Composer/packages/client/src/components/TriggerCreationModal/TriggerCreationModal.tsx b/Composer/packages/client/src/components/TriggerCreationModal/TriggerCreationModal.tsx
index c1a8b0634f..9ff23c05a0 100644
--- a/Composer/packages/client/src/components/TriggerCreationModal/TriggerCreationModal.tsx
+++ b/Composer/packages/client/src/components/TriggerCreationModal/TriggerCreationModal.tsx
@@ -12,7 +12,7 @@ import { useRecoilValue } from 'recoil';
import { TriggerFormData, TriggerFormDataErrors } from '../../utils/dialogUtil';
import { userSettingsState } from '../../recoilModel/atoms';
-import { currentDialogState, dialogsSelectorFamily, localeState, luFilesState } from '../../recoilModel';
+import { currentDialogState, dialogsSelectorFamily, localeState, luFilesSelectorFamily } from '../../recoilModel';
import { isRegExRecognizerType, resolveRecognizer$kind } from '../../utils/dialogValidator';
import TelemetryClient from '../../telemetry/TelemetryClient';
@@ -50,7 +50,7 @@ export const TriggerCreationModal: React.FC = (props)
const isRegEx = isRegExRecognizerType(dialogFile);
const regexIntents = (dialogFile?.content?.recognizer as RegexRecognizer)?.intents ?? [];
- const luFiles = useRecoilValue(luFilesState(projectId));
+ const luFiles = useRecoilValue(luFilesSelectorFamily(projectId));
const locale = useRecoilValue(localeState(projectId));
const currentDialog = useRecoilValue(currentDialogState({ projectId, dialogId }));
const luFile = luFiles.find((f) => f.id === `${currentDialog?.id}.${locale}`);
diff --git a/Composer/packages/client/src/hooks/useResolver.ts b/Composer/packages/client/src/hooks/useResolver.ts
index ac89aa885c..c64e986cc7 100644
--- a/Composer/packages/client/src/hooks/useResolver.ts
+++ b/Composer/packages/client/src/hooks/useResolver.ts
@@ -4,15 +4,15 @@ import { useRef } from 'react';
import { lgImportResolverGenerator } from '@bfc/shared';
import { useRecoilValue } from 'recoil';
-import { dialogsSelectorFamily, luFilesState, localeState, qnaFilesState } from '../recoilModel';
+import { dialogsSelectorFamily, luFilesSelectorFamily, localeState, qnaFilesSelectorFamily } from '../recoilModel';
import { lgFilesSelectorFamily } from '../recoilModel/selectors/lg';
export const useResolvers = (projectId: string) => {
const dialogs = useRecoilValue(dialogsSelectorFamily(projectId));
- const luFiles = useRecoilValue(luFilesState(projectId));
+ const luFiles = useRecoilValue(luFilesSelectorFamily(projectId));
const lgFiles = useRecoilValue(lgFilesSelectorFamily(projectId));
const locale = useRecoilValue(localeState(projectId));
- const qnaFiles = useRecoilValue(qnaFilesState(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesSelectorFamily(projectId));
const lgFilesRef = useRef(lgFiles);
lgFilesRef.current = lgFiles;
diff --git a/Composer/packages/client/src/pages/botProject/RootBotExternalService.tsx b/Composer/packages/client/src/pages/botProject/RootBotExternalService.tsx
index 39f9c1bc22..644b93e521 100644
--- a/Composer/packages/client/src/pages/botProject/RootBotExternalService.tsx
+++ b/Composer/packages/client/src/pages/botProject/RootBotExternalService.tsx
@@ -20,8 +20,8 @@ import { NeutralColors, SharedColors } from '@uifabric/fluent-theme';
import {
dispatcherState,
settingsState,
- luFilesState,
- qnaFilesState,
+ luFilesSelectorFamily,
+ qnaFilesSelectorFamily,
botDisplayNameState,
dialogsWithLuProviderSelectorFamily,
} from '../../recoilModel';
@@ -167,8 +167,8 @@ export const RootBotExternalService: React.FC = (pr
const rootqnaKey = groupQnAKey.root;
const dialogs = useRecoilValue(dialogsWithLuProviderSelectorFamily(projectId));
- const luFiles = useRecoilValue(luFilesState(projectId));
- const qnaFiles = useRecoilValue(qnaFilesState(projectId));
+ const luFiles = useRecoilValue(luFilesSelectorFamily(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesSelectorFamily(projectId));
const botName = useRecoilValue(botDisplayNameState(projectId));
const isLUISKeyNeeded = BotIndexer.shouldUseLuis(dialogs, luFiles);
const isQnAKeyNeeded = BotIndexer.shouldUseQnA(dialogs, qnaFiles);
diff --git a/Composer/packages/client/src/pages/botProject/SkillBotExternalService.tsx b/Composer/packages/client/src/pages/botProject/SkillBotExternalService.tsx
index 8f3c2217f1..25d4fad1db 100644
--- a/Composer/packages/client/src/pages/botProject/SkillBotExternalService.tsx
+++ b/Composer/packages/client/src/pages/botProject/SkillBotExternalService.tsx
@@ -14,8 +14,8 @@ import { TextField } from 'office-ui-fabric-react/lib/TextField';
import {
dispatcherState,
settingsState,
- luFilesState,
- qnaFilesState,
+ luFilesSelectorFamily,
+ qnaFilesSelectorFamily,
dialogsSelectorFamily,
botDisplayNameState,
} from '../../recoilModel';
@@ -66,8 +66,8 @@ export const SkillBotExternalService: React.FC = (
const skillQnAKey = groupQnAKey[projectId];
const dialogs = useRecoilValue(dialogsSelectorFamily(projectId));
- const luFiles = useRecoilValue(luFilesState(projectId));
- const qnaFiles = useRecoilValue(qnaFilesState(projectId));
+ const luFiles = useRecoilValue(luFilesSelectorFamily(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesSelectorFamily(projectId));
const isLUISKeyNeeded = BotIndexer.shouldUseLuis(dialogs, luFiles);
const isQnAKeyNeeded = BotIndexer.shouldUseQnA(dialogs, qnaFiles);
diff --git a/Composer/packages/client/src/pages/design/Modals.tsx b/Composer/packages/client/src/pages/design/Modals.tsx
index dd8968e656..b20dab9bc3 100644
--- a/Composer/packages/client/src/pages/design/Modals.tsx
+++ b/Composer/packages/client/src/pages/design/Modals.tsx
@@ -15,7 +15,7 @@ import {
schemasState,
showCreateDialogModalState,
localeState,
- qnaFilesState,
+ qnaFilesSelectorFamily,
displaySkillManifestState,
brokenSkillInfoState,
brokenSkillRepairCallbackState,
@@ -42,7 +42,7 @@ type ModalsProps = {
projectId: string;
};
const Modals: React.FC = ({ projectId = '' }) => {
- const qnaFiles = useRecoilValue(qnaFilesState(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesSelectorFamily(projectId));
const schemas = useRecoilValue(schemasState(projectId));
const displaySkillManifestNameIdentifier = useRecoilValue(displaySkillManifestState);
diff --git a/Composer/packages/client/src/pages/design/PropertyPanel.tsx b/Composer/packages/client/src/pages/design/PropertyPanel.tsx
index cd7580239e..3b521cf0c9 100644
--- a/Composer/packages/client/src/pages/design/PropertyPanel.tsx
+++ b/Composer/packages/client/src/pages/design/PropertyPanel.tsx
@@ -17,9 +17,11 @@ import {
skillNameIdentifierByProjectIdSelector,
} from '../../recoilModel';
import { undoVersionState } from '../../recoilModel/undo/history';
+import { LoadingSpinner } from '../../components/LoadingSpinner';
import { PropertyEditor } from './PropertyEditor';
import { ManifestEditor } from './ManifestEditor';
+import useAssetsParsingState from './useAssetsParsingState';
type PropertyViewProps = {
projectId: string;
@@ -33,7 +35,7 @@ const PropertyPanel: React.FC = React.memo(({ projectId = '',
const { isRemote: isRemoteSkill } = useRecoilValue(projectMetaDataState(projectId));
const skillsByProjectId = useRecoilValue(skillNameIdentifierByProjectIdSelector);
const skills = useRecoilValue(skillsStateSelector);
-
+ const loading = useAssetsParsingState(projectId);
const skillManifestFile = useMemo(() => {
if (!isSkill) return undefined;
@@ -51,11 +53,13 @@ const PropertyPanel: React.FC = React.memo(({ projectId = '',
return (
- {isRemoteSkill && skillManifestFile ? (
-
- ) : (
-
- )}
+ {loading && }
+ {!loading &&
+ (isRemoteSkill && skillManifestFile ? (
+
+ ) : (
+
+ ))}
);
});
diff --git a/Composer/packages/client/src/pages/design/VisualPanel.tsx b/Composer/packages/client/src/pages/design/VisualPanel.tsx
index 5ba6c31c9e..d231efc0a6 100644
--- a/Composer/packages/client/src/pages/design/VisualPanel.tsx
+++ b/Composer/packages/client/src/pages/design/VisualPanel.tsx
@@ -21,11 +21,13 @@ import {
import { triggerNotSupported } from '../../utils/dialogValidator';
import { decodeDesignerPathToArrayPath } from '../../utils/convertUtils/designerPathEncoder';
import TelemetryClient from '../../telemetry/TelemetryClient';
+import { LoadingSpinner } from '../../components/LoadingSpinner';
import { WarningMessage } from './WarningMessage';
import { visualPanel } from './styles';
import VisualPanelHeader from './VisualPanelHeader';
import VisualEditorWrapper from './VisualEditorWrapper';
+import useAssetsParsingState from './useAssetsParsingState';
type VisualPanelProps = {
projectId: string;
@@ -37,7 +39,7 @@ const VisualPanel: React.FC = React.memo(({ projectId }) => {
const currentDialog = useRecoilValue(currentDialogState({ dialogId, projectId }));
const schemas = useRecoilValue(schemasState(projectId));
const { isRemote: isRemoteSkill } = useRecoilValue(projectMetaDataState(projectId));
-
+ const loading = useAssetsParsingState(projectId);
const { updateDialog, navTo } = useRecoilValue(dispatcherState);
const selected = decodeDesignerPathToArrayPath(currentDialog?.content, encodedSelected || '');
@@ -76,29 +78,31 @@ const VisualPanel: React.FC = React.memo(({ projectId }) => {
/>
)}
- {dialogJsonVisible && currentDialog ? (
- {
- updateDialog({ id: currentDialog.id, content: data, projectId });
- }}
- />
- ) : withWarning ? (
- {
- setWarningIsVisible(false);
- }}
- onOk={() => navTo(projectId, dialogId ?? null)}
- />
- ) : (
-
- )}
+ {loading && }
+ {!loading &&
+ (dialogJsonVisible && currentDialog ? (
+ {
+ updateDialog({ id: currentDialog.id, content: data, projectId });
+ }}
+ />
+ ) : withWarning ? (
+ {
+ setWarningIsVisible(false);
+ }}
+ onOk={() => navTo(projectId, dialogId ?? null)}
+ />
+ ) : (
+
+ ))}
);
});
diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx
index 41be8bcf9d..55fcbc39a5 100644
--- a/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx
+++ b/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx
@@ -15,10 +15,10 @@ import { SkillManifestFile } from '@bfc/shared';
import {
dispatcherState,
skillManifestsState,
- qnaFilesState,
+ qnaFilesSelectorFamily,
dialogsSelectorFamily,
dialogSchemasState,
- luFilesState,
+ luFilesSelectorFamily,
} from '../../../recoilModel';
import { editorSteps, ManifestEditorSteps, order } from './constants';
@@ -35,8 +35,8 @@ interface ExportSkillModalProps {
const ExportSkillModal: React.FC = ({ onSubmit, onDismiss: handleDismiss, projectId }) => {
const dialogs = useRecoilValue(dialogsSelectorFamily(projectId));
const dialogSchemas = useRecoilValue(dialogSchemasState(projectId));
- const luFiles = useRecoilValue(luFilesState(projectId));
- const qnaFiles = useRecoilValue(qnaFilesState(projectId));
+ const luFiles = useRecoilValue(luFilesSelectorFamily(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesSelectorFamily(projectId));
const skillManifests = useRecoilValue(skillManifestsState(projectId));
const { updateSkillManifest } = useRecoilValue(dispatcherState);
diff --git a/Composer/packages/client/src/pages/design/useAssetsParsingState.tsx b/Composer/packages/client/src/pages/design/useAssetsParsingState.tsx
new file mode 100644
index 0000000000..08d3ccae52
--- /dev/null
+++ b/Composer/packages/client/src/pages/design/useAssetsParsingState.tsx
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { useEffect, useState } from 'react';
+import { useRecoilValue } from 'recoil';
+
+import { localeState, lgFileState, designPageLocationState, luFileState, qnaFileState } from '../../recoilModel';
+
+//check if all the files needed in current page have parse result.
+//If some of the files still are still doing the parsing, return true.
+// This is use to show during the parsing stage.
+export const useAssetsParsingState = (projectId: string) => {
+ const { dialogId } = useRecoilValue(designPageLocationState(projectId));
+ const locale = useRecoilValue(localeState(projectId));
+ const currentLg = useRecoilValue(lgFileState({ projectId, lgFileId: `${dialogId}.${locale}` }));
+ const currentLu = useRecoilValue(luFileState({ projectId, luFileId: `${dialogId}.${locale}` }));
+ const currentQna = useRecoilValue(qnaFileState({ projectId, qnaFileId: `${dialogId}.${locale}` }));
+ const [isParsing, setIsParsing] = useState(false);
+
+ useEffect(() => {
+ const currentAssets = [currentLg, currentLu, currentQna].filter((item) => item.id);
+
+ if (!currentAssets.length) return;
+
+ if (currentAssets.some((item) => item.id && item.isContentUnparsed)) {
+ !isParsing && setIsParsing(true);
+ } else {
+ isParsing && setIsParsing(false);
+ }
+ }, [currentLg, currentLu, currentQna]);
+
+ return isParsing;
+};
+
+export default useAssetsParsingState;
diff --git a/Composer/packages/client/src/pages/design/useEmptyPropsHandler.tsx b/Composer/packages/client/src/pages/design/useEmptyPropsHandler.tsx
index a46a478f7c..f3da0450cd 100644
--- a/Composer/packages/client/src/pages/design/useEmptyPropsHandler.tsx
+++ b/Composer/packages/client/src/pages/design/useEmptyPropsHandler.tsx
@@ -3,13 +3,25 @@
import { useEffect } from 'react';
import { globalHistory, WindowLocation } from '@reach/router';
-import { PromptTab } from '@bfc/shared';
-import { useRecoilValue } from 'recoil';
+import { LgFile, LuFile, PromptTab, QnAFile } from '@bfc/shared';
+import { useRecoilState, useRecoilValue } from 'recoil';
import { getDialogData } from '../../utils/dialogUtil';
import { getFocusPath } from '../../utils/navigation';
-import { dispatcherState, currentDialogState } from '../../recoilModel';
+import {
+ dispatcherState,
+ currentDialogState,
+ localeState,
+ lgFileState,
+ lgFilesSelectorFamily,
+ luFileState,
+ qnaFileState,
+ settingsState,
+} from '../../recoilModel';
import { decodeDesignerPathToArrayPath } from '../../utils/convertUtils/designerPathEncoder';
+import lgDiagnosticWorker from '../../recoilModel/parsers/lgDiagnosticWorker';
+import luWorker from '../../recoilModel/parsers/luWorker';
+import qnaWorker from '../../recoilModel/parsers/qnaWorker';
const getTabFromFragment = () => {
const tab = window.location.hash.substring(1);
@@ -26,7 +38,18 @@ export const useEmptyPropsHandler = (
const activeBot = skillId ?? projectId;
const currentDialog = useRecoilValue(currentDialogState({ dialogId, projectId: activeBot }));
-
+ const locale = useRecoilValue(localeState(activeBot));
+ const settings = useRecoilValue(settingsState(activeBot));
+ const [currentLg, setCurrentLg] = useRecoilState(
+ lgFileState({ projectId: activeBot, lgFileId: `${dialogId}.${locale}` })
+ );
+ const [currentLu, setCurrentLu] = useRecoilState(
+ luFileState({ projectId: activeBot, luFileId: `${dialogId}.${locale}` })
+ );
+ const [currentQna, setCurrentQna] = useRecoilState(
+ qnaFileState({ projectId: activeBot, qnaFileId: `${dialogId}.${locale}` })
+ );
+ const lgFiles = useRecoilValue(lgFilesSelectorFamily(projectId));
const { updateDialog, setDesignPageLocation, navTo } = useRecoilValue(dispatcherState);
// migration: add id to dialog when dialog doesn't have id
@@ -41,6 +64,49 @@ export const useEmptyPropsHandler = (
}
}, [currentDialog]);
+ useEffect(() => {
+ if (!currentDialog || !currentLg.id) return;
+ let isMounted = true;
+ if (currentLg.isContentUnparsed) {
+ //for current dialog, check the lg file to make sure the file is parsed.
+ lgDiagnosticWorker.parse(activeBot, currentLg.id, currentLg.content, lgFiles).then((result) => {
+ isMounted ? setCurrentLg(result as LgFile) : null;
+ });
+ }
+
+ return () => {
+ isMounted = false;
+ };
+ }, [currentDialog, currentLg, lgFiles]);
+
+ useEffect(() => {
+ if (!currentDialog || !currentLu.id) return;
+ let isMounted = true;
+ if (currentLu.isContentUnparsed) {
+ //for current dialog, check the lu file to make sure the file is parsed.
+ luWorker.parse(currentLu.id, currentLu.content, settings.luFeatures).then((result) => {
+ isMounted ? setCurrentLu(result as LuFile) : null;
+ });
+ return () => {
+ isMounted = false;
+ };
+ }
+ }, [currentDialog, currentLu]);
+
+ useEffect(() => {
+ if (!currentDialog || !currentQna.id) return;
+ let isMounted = true;
+ if (currentQna.isContentUnparsed) {
+ //for current dialog, check the qna file to make sure the file is parsed.
+ qnaWorker.parse(currentQna.id, currentQna.content).then((result) => {
+ isMounted ? setCurrentQna(result as QnAFile) : null;
+ });
+ }
+ return () => {
+ isMounted = false;
+ };
+ }, [currentDialog, currentQna]);
+
useEffect(() => {
if (!location || !currentDialog || !activeBot) return;
diff --git a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx
index 502af634a8..1671f4c2ae 100644
--- a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx
+++ b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx
@@ -12,7 +12,7 @@ import { RouteComponentProps, Router } from '@reach/router';
import { LoadingSpinner } from '../../components/LoadingSpinner';
import { navigateTo } from '../../utils/navigation';
import { Page } from '../../components/Page';
-import { dialogIdsState, qnaFilesState, dispatcherState, createQnAOnState } from '../../recoilModel';
+import { dialogIdsState, qnaFilesSelectorFamily, dispatcherState, createQnAOnState } from '../../recoilModel';
import { CreateQnAModal } from '../../components/QnA';
import TelemetryClient from '../../telemetry/TelemetryClient';
@@ -33,7 +33,7 @@ const QnAPage: React.FC = (props) => {
const baseURL = skillId == null ? `/bot/${projectId}/` : `/bot/${projectId}/skill/${skillId}/`;
const actions = useRecoilValue(dispatcherState);
- const qnaFiles = useRecoilValue(qnaFilesState(actualProjectId));
+ const qnaFiles = useRecoilValue(qnaFilesSelectorFamily(actualProjectId));
//To do: support other languages
const locale = 'en-us';
//const locale = useRecoilValue(localeState);
diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx
index ba88fa1e56..5cb2f420b7 100644
--- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx
+++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx
@@ -34,7 +34,7 @@ import { NeutralColors } from '@uifabric/fluent-theme';
import emptyQnAIcon from '../../images/emptyQnAIcon.svg';
import { navigateTo } from '../../utils/navigation';
-import { dialogsSelectorFamily, qnaFilesState, localeState } from '../../recoilModel';
+import { dialogsSelectorFamily, qnaFilesSelectorFamily, localeState } from '../../recoilModel';
import { dispatcherState } from '../../recoilModel';
import { getBaseName } from '../../utils/fileUtil';
import { EditableField } from '../../components/EditableField';
@@ -92,7 +92,7 @@ const TableView: React.FC = (props) => {
const actions = useRecoilValue(dispatcherState);
const dialogs = useRecoilValue(dialogsSelectorFamily(actualProjectId));
- const qnaFiles = useRecoilValue(qnaFilesState(actualProjectId));
+ const qnaFiles = useRecoilValue(qnaFilesSelectorFamily(actualProjectId));
const locale = useRecoilValue(localeState(actualProjectId));
const {
removeQnAImport,
diff --git a/Composer/packages/client/src/pages/language-understanding/LUPage.tsx b/Composer/packages/client/src/pages/language-understanding/LUPage.tsx
index 64a2d3df50..dfdf0af878 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 { localeState, luFilesState } from '../../recoilModel';
+import { localeState, luFilesSelectorFamily } from '../../recoilModel';
import TableView from './table-view';
@@ -26,7 +26,7 @@ const LUPage: React.FC = (props) => {
const { dialogId, projectId, skillId, luFileId, file } = props;
const actualProjectId = skillId ?? projectId;
- const luFiles = useRecoilValue(luFilesState(actualProjectId));
+ const luFiles = useRecoilValue(luFilesSelectorFamily(actualProjectId));
const locale = useRecoilValue(localeState(actualProjectId));
const settings = useRecoilValue(settingsState(actualProjectId));
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 e79ebec04a..8dd80df04e 100644
--- a/Composer/packages/client/src/pages/language-understanding/table-view.tsx
+++ b/Composer/packages/client/src/pages/language-understanding/table-view.tsx
@@ -24,7 +24,13 @@ 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, dialogsSelectorFamily } from '../../recoilModel';
+import {
+ dispatcherState,
+ localeState,
+ settingsState,
+ dialogsSelectorFamily,
+ luFilesSelectorFamily,
+} from '../../recoilModel';
import { formCell, luPhraseCell, tableCell, editableFieldContainer } from './styles';
interface TableViewProps extends RouteComponentProps<{ dialogId: string; skillId: string; projectId: string }> {
@@ -52,7 +58,7 @@ const TableView: React.FC = (props) => {
const { updateLuIntent } = useRecoilValue(dispatcherState);
- const luFiles = useRecoilValue(luFilesState(actualProjectId));
+ const luFiles = useRecoilValue(luFilesSelectorFamily(actualProjectId));
const locale = useRecoilValue(localeState(actualProjectId));
const settings = useRecoilValue(settingsState(actualProjectId));
const dialogs = useRecoilValue(dialogsSelectorFamily(actualProjectId));
diff --git a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx
index e5fd3a79b8..ccd557c032 100644
--- a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx
+++ b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx
@@ -10,13 +10,13 @@ import { BotAssets } from '@bfc/shared';
import { useRecoilValue } from 'recoil';
import isEmpty from 'lodash/isEmpty';
-import { dialogsSelectorFamily } from './selectors';
+import { createMissingLgTemplatesForDialogs } from '../utils/lgUtil';
+
+import { dialogsSelectorFamily, luFilesSelectorFamily, qnaFilesSelectorFamily } from './selectors';
import { UndoRoot } from './undo/history';
import { prepareAxios } from './../utils/auth';
import createDispatchers, { Dispatcher } from './dispatchers';
import {
- luFilesState,
- qnaFilesState,
skillManifestsState,
dialogSchemasState,
settingsState,
@@ -25,6 +25,7 @@ import {
jsonSchemaFilesState,
crossTrainConfigState,
dispatcherState,
+ projectIndexingState,
} from './atoms';
import { localBotsWithoutErrorsSelector, formDialogSchemasSelectorFamily } from './selectors';
import { Recognizer } from './Recognizers';
@@ -33,7 +34,7 @@ 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 luFiles = await snapshot.getPromise(luFilesSelectorFamily(projectId));
const lgFiles = await snapshot.getPromise(lgFilesSelectorFamily(projectId));
const skillManifests = await snapshot.getPromise(skillManifestsState(projectId));
const setting = await snapshot.getPromise(settingsState(projectId));
@@ -43,7 +44,7 @@ const getBotAssets = async (projectId, snapshot: Snapshot): Promise =
const jsonSchemaFiles = await snapshot.getPromise(jsonSchemaFilesState(projectId));
const recognizers = await snapshot.getPromise(recognizersSelectorFamily(projectId));
const crossTrainConfig = await snapshot.getPromise(crossTrainConfigState(projectId));
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
return {
projectId,
@@ -93,6 +94,19 @@ const InitDispatcher = ({ onLoad }) => {
return null;
};
+const repairBotProject = async (projectId: string, snapshot: Snapshot, previousSnapshot: Snapshot) => {
+ const indexingState = await snapshot.getPromise(projectIndexingState(projectId));
+ const preIndexingState = await previousSnapshot.getPromise(projectIndexingState(projectId));
+ if (indexingState === false && preIndexingState == true) {
+ const dialogs = await snapshot.getPromise(dialogsSelectorFamily(projectId));
+ const lgFiles = await snapshot.getPromise(lgFilesSelectorFamily(projectId));
+
+ const updatedLgFiles = await createMissingLgTemplatesForDialogs(projectId, dialogs, lgFiles);
+ const { updateAllLgFiles } = await snapshot.getPromise(dispatcherState);
+ updateAllLgFiles({ projectId, lgFiles: updatedLgFiles });
+ }
+};
+
export const DispatcherWrapper = ({ children }) => {
const [loaded, setLoaded] = useState(false);
const botProjects = useRecoilValue(localBotsWithoutErrorsSelector);
@@ -105,6 +119,7 @@ export const DispatcherWrapper = ({ children }) => {
const previousAssets = await getBotAssets(projectId, previousSnapshot);
const filePersistence = await snapshot.getPromise(filePersistenceState(projectId));
if (!isEmpty(filePersistence)) {
+ await repairBotProject(projectId, snapshot, previousSnapshot);
if (filePersistence.isErrorHandlerEmpty()) {
filePersistence.registerErrorHandler(setProjectError);
}
diff --git a/Composer/packages/client/src/recoilModel/Recognizers.tsx b/Composer/packages/client/src/recoilModel/Recognizers.tsx
index d2acaaa4dc..5955b4ac33 100644
--- a/Composer/packages/client/src/recoilModel/Recognizers.tsx
+++ b/Composer/packages/client/src/recoilModel/Recognizers.tsx
@@ -13,8 +13,8 @@ import { getLuProvider } from '../utils/dialogUtil';
import * as luUtil from './../utils/luUtil';
import * as buildUtil from './../utils/buildUtil';
-import { crossTrainConfigState, filePersistenceState, luFilesState, qnaFilesState, settingsState } from './atoms';
-import { dialogsSelectorFamily } from './selectors';
+import { crossTrainConfigState, filePersistenceState, settingsState } from './atoms';
+import { dialogsSelectorFamily, luFilesSelectorFamily, qnaFilesSelectorFamily } from './selectors';
import { recognizersSelectorFamily } from './selectors/recognizers';
export const LuisRecognizerTemplate = (target: string, fileName: string) => ({
@@ -164,8 +164,8 @@ export const Recognizer = React.memo((props: { projectId: string }) => {
const setRecognizers = useSetRecoilState(recognizersSelectorFamily(projectId));
const [crossTrainConfig, setCrossTrainConfig] = useRecoilState(crossTrainConfigState(projectId));
const dialogs = useRecoilValue(dialogsSelectorFamily(projectId));
- const luFiles = useRecoilValue(luFilesState(projectId));
- const qnaFiles = useRecoilValue(qnaFilesState(projectId));
+ const luFiles = useRecoilValue(luFilesSelectorFamily(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesSelectorFamily(projectId));
const settings = useRecoilValue(settingsState(projectId));
const curRecognizers = useRecoilValue(recognizersSelectorFamily(projectId));
const curRecognizersRef = useRef(curRecognizers);
@@ -210,6 +210,9 @@ export const Recognizer = React.memo((props: { projectId: string }) => {
useEffect(() => {
try {
+ //if the lu file still in the loading stage, do nothing
+ if (luFiles.some((item) => item.isContentUnparsed)) return;
+
const referredLuFiles = luUtil.checkLuisBuild(luFiles, dialogs);
const curCrossTrainConfig = buildUtil.createCrossTrainConfig(dialogs, referredLuFiles, settings.languages);
diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts
index 1a6d4b316f..bb2729a3f6 100644
--- a/Composer/packages/client/src/recoilModel/atoms/botState.ts
+++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts
@@ -56,9 +56,43 @@ const emptyLg: LgFile = {
templates: [],
allTemplates: [],
imports: [],
+ isContentUnparsed: true,
+};
+
+const emptyLu: LuFile = {
+ id: '',
+ content: '',
+ diagnostics: [],
+ intents: [],
+ empty: true,
+ resource: {
+ Sections: [],
+ Errors: [],
+ Content: '',
+ },
+ imports: [],
+ isContentUnparsed: true,
+};
+
+const emptyQna: QnAFile = {
+ id: '',
+ content: '',
+ diagnostics: [],
+ qnaSections: [],
+ imports: [],
+ options: [],
+ empty: true,
+ resource: {
+ Sections: [],
+ Errors: [],
+ Content: '',
+ },
+ isContentUnparsed: true,
};
type LgStateParams = { projectId: string; lgFileId: string };
+type LuStateParams = { projectId: string; luFileId: string };
+type QnaStateParams = { projectId: string; qnaFileId: string };
export const lgFileState = atomFamily({
key: getFullyQualifiedKey('lg'),
@@ -74,6 +108,34 @@ export const lgFileIdsState = atomFamily({
},
});
+export const qnaFileState = atomFamily({
+ key: getFullyQualifiedKey('qna'),
+ default: () => {
+ return emptyQna;
+ },
+});
+
+export const qnaFileIdsState = atomFamily({
+ key: getFullyQualifiedKey('qnaFileIds'),
+ default: () => {
+ return [];
+ },
+});
+
+export const luFileState = atomFamily({
+ key: getFullyQualifiedKey('lu'),
+ default: () => {
+ return emptyLu;
+ },
+});
+
+export const luFileIdsState = atomFamily({
+ key: getFullyQualifiedKey('luFileIds'),
+ default: () => {
+ return [];
+ },
+});
+
type dialogStateParams = { projectId: string; dialogId: string };
export const dialogState = atomFamily({
key: getFullyQualifiedKey('dialog'),
@@ -151,13 +213,6 @@ export const botRuntimeErrorState = atomFamily({
},
});
-export const luFilesState = atomFamily({
- key: getFullyQualifiedKey('luFiles'),
- default: (id) => {
- return [];
- },
-});
-
export const recognizerIdsState = atomFamily({
key: getFullyQualifiedKey('recognizerIds'),
default: (id) => {
@@ -307,11 +362,6 @@ export const isEjectRuntimeExistState = atomFamily({
default: false,
});
-export const qnaFilesState = atomFamily({
- key: getFullyQualifiedKey('qnaFiles'),
- default: [],
-});
-
export const jsonSchemaFilesState = atomFamily({
key: getFullyQualifiedKey('jsonSchemaFiles'),
default: [],
@@ -376,3 +426,8 @@ export const webChatLogsState = atomFamily({
key: getFullyQualifiedKey('webChatLogs'),
default: [],
});
+
+export const projectIndexingState = atomFamily({
+ key: getFullyQualifiedKey('projectIndexing'),
+ default: false,
+});
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 989a2e0eca..4a77ce7529 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialog.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialog.test.tsx
@@ -8,16 +8,19 @@ import { act, HookResult } from '@botframework-composer/test-utils/lib/hooks';
import { dialogsDispatcher } from '../dialogs';
import { renderRecoilHook } from '../../../../__tests__/testUtils';
import {
- luFilesState,
schemasState,
dialogSchemasState,
actionsSeedState,
onCreateDialogCompleteState,
showCreateDialogModalState,
- qnaFilesState,
dispatcherState,
} from '../../atoms';
-import { dialogsSelectorFamily, lgFilesSelectorFamily } from '../../selectors';
+import {
+ dialogsSelectorFamily,
+ lgFilesSelectorFamily,
+ luFilesSelectorFamily,
+ qnaFilesSelectorFamily,
+} from '../../selectors';
import { Dispatcher } from '..';
const projectId = '42345.23432';
@@ -93,12 +96,12 @@ describe('dialog dispatcher', () => {
const useRecoilTestHook = () => {
const dialogs = useRecoilValue(dialogsSelectorFamily(projectId));
const dialogSchemas = useRecoilValue(dialogSchemasState(projectId));
- const luFiles = useRecoilValue(luFilesState(projectId));
+ const luFiles = useRecoilValue(luFilesSelectorFamily(projectId));
const lgFiles = useRecoilValue(lgFilesSelectorFamily(projectId));
const actionsSeed = useRecoilValue(actionsSeedState(projectId));
const onCreateDialogComplete = useRecoilValue(onCreateDialogCompleteState(projectId));
const showCreateDialogModal = useRecoilValue(showCreateDialogModalState);
- const qnaFiles = useRecoilValue(qnaFilesState(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesSelectorFamily(projectId));
const currentDispatcher = useRecoilValue(dispatcherState);
return {
@@ -122,8 +125,8 @@ describe('dialog dispatcher', () => {
{ recoilState: dialogsSelectorFamily(projectId), initialValue: [{ id: '1' }, { id: '2' }] },
{ recoilState: dialogSchemasState(projectId), initialValue: [{ id: '1' }, { id: '2' }] },
{ 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: luFilesSelectorFamily(projectId), initialValue: [{ id: '1.en-us' }, { id: '2.en-us' }] },
+ { recoilState: qnaFilesSelectorFamily(projectId), initialValue: [{ id: '1.en-us' }, { id: '2.en-us' }] },
{ recoilState: schemasState(projectId), initialValue: { sdk: { content: '' } } },
],
dispatcher: {
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 ef9f6ac21b..993d3bc347 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lg.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lg.test.tsx
@@ -46,6 +46,7 @@ const lgFiles = [
diagnostics: [],
imports: [],
allTemplates: [{ name: 'Hello', body: '-hi', parameters: [] }],
+ isContentUnparsed: false,
},
] as LgFile[];
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lu.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lu.test.tsx
index b65846179e..7510f85939 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lu.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lu.test.tsx
@@ -8,9 +8,10 @@ import { act, HookResult } from '@botframework-composer/test-utils/lib/hooks';
import { luUtil } from '@bfc/indexers';
import { renderRecoilHook } from '../../../../__tests__/testUtils';
-import { luFilesState, currentProjectIdState, dispatcherState } from '../../atoms';
+import { currentProjectIdState, dispatcherState } from '../../atoms';
import { Dispatcher } from '..';
import { luDispatcher } from '../lu';
+import { luFilesSelectorFamily } from '../../selectors';
const luFeatures = {};
@@ -40,7 +41,7 @@ const getLuIntent = (Name, Body): LuIntentSection =>
describe('Lu dispatcher', () => {
const useRecoilTestHook = () => {
- const [luFiles, setLuFiles] = useRecoilState(luFilesState(projectId));
+ const [luFiles, setLuFiles] = useRecoilState(luFilesSelectorFamily(projectId));
const currentDispatcher = useRecoilValue(dispatcherState);
return {
@@ -55,7 +56,7 @@ describe('Lu dispatcher', () => {
beforeEach(() => {
const { result } = renderRecoilHook(useRecoilTestHook, {
states: [
- { recoilState: luFilesState(projectId), initialValue: luFiles },
+ { recoilState: luFilesSelectorFamily(projectId), initialValue: luFiles },
{ 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 8be42b0d4a..945bf977b2 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/multilang.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/multilang.test.tsx
@@ -6,7 +6,6 @@ import { act, HookResult } from '@botframework-composer/test-utils/lib/hooks';
import { renderRecoilHook } from '../../../../__tests__/testUtils';
import {
- luFilesState,
settingsState,
localeState,
actionsSeedState,
@@ -15,7 +14,7 @@ import {
currentProjectIdState,
dispatcherState,
} from '../../atoms';
-import { dialogsSelectorFamily, lgFilesSelectorFamily } from '../../selectors';
+import { dialogsSelectorFamily, lgFilesSelectorFamily, luFilesSelectorFamily } from '../../selectors';
import { Dispatcher } from '..';
import { multilangDispatcher } from '../multilang';
@@ -43,7 +42,7 @@ describe('Multilang dispatcher', () => {
const dialogs = useRecoilValue(dialogsSelectorFamily(state.projectId));
const locale = useRecoilValue(localeState(state.projectId));
const settings = useRecoilValue(settingsState(state.projectId));
- const luFiles = useRecoilValue(luFilesState(state.projectId));
+ const luFiles = useRecoilValue(luFilesSelectorFamily(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: dialogsSelectorFamily(state.projectId), initialValue: state.dialogs },
{ recoilState: localeState(state.projectId), initialValue: state.locale },
{ recoilState: lgFilesSelectorFamily(state.projectId), initialValue: state.lgFiles },
- { recoilState: luFilesState(state.projectId), initialValue: state.luFiles },
+ { recoilState: luFilesSelectorFamily(state.projectId), initialValue: state.luFiles },
{ recoilState: settingsState(state.projectId), initialValue: state.settings },
],
dispatcher: {
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 10c66dda54..d0344008c1 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx
@@ -24,7 +24,6 @@ import {
runtimeTemplatesState,
currentProjectIdState,
skillManifestsState,
- luFilesState,
settingsState,
botEnvironmentState,
botDiagnosticsState,
@@ -41,7 +40,7 @@ import {
botProjectSpaceLoadedState,
dispatcherState,
} from '../../atoms';
-import { dialogsSelectorFamily, lgFilesSelectorFamily } from '../../selectors';
+import { dialogsSelectorFamily, lgFilesSelectorFamily, luFilesSelectorFamily } from '../../selectors';
import { Dispatcher } from '../../dispatchers';
import { BotStatus } from '../../../constants';
@@ -68,12 +67,37 @@ jest.mock('../../parsers/lgWorker', () => {
return {
flush: () => new Promise((resolve) => resolve(null)),
addProject: () => new Promise((resolve) => resolve(null)),
+
+ parseAll: (id, files) =>
+ new Promise((resolve) =>
+ resolve(
+ files.map(({ id, content }) => {
+ const result = require('@bfc/indexers').lgUtil.parse(id, content, files);
+ delete result.parseResult;
+ return result;
+ })
+ )
+ ),
};
});
jest.mock('../../parsers/luWorker', () => {
return {
flush: () => new Promise((resolve) => resolve(null)),
+ parseAll: (files, luFeatures) =>
+ new Promise((resolve) =>
+ resolve(files.map(({ id, content }) => require('@bfc/indexers').luUtil.parse(id, content, luFeatures)))
+ ),
+ };
+});
+
+jest.mock('../../parsers/qnaWorker', () => {
+ return {
+ flush: () => new Promise((resolve) => resolve(null)),
+ parseAll: (files) =>
+ new Promise((resolve) =>
+ resolve(files.map(({ id, content }) => require('@bfc/indexers').qnaUtil.parse(id, content)))
+ ),
};
});
@@ -113,7 +137,7 @@ describe('Project dispatcher', () => {
const location = useRecoilValue(locationState(projectId));
const botName = useRecoilValue(botDisplayNameState(projectId));
const skillManifests = useRecoilValue(skillManifestsState(projectId));
- const luFiles = useRecoilValue(luFilesState(projectId));
+ const luFiles = useRecoilValue(luFilesSelectorFamily(projectId));
const lgFiles = useRecoilValue(lgFilesSelectorFamily(projectId));
const settings = useRecoilValue(settingsState(projectId));
const dialogs = useRecoilValue(dialogsSelectorFamily(projectId));
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/qna.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/qna.test.tsx
index 9842306749..c1e3085333 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/qna.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/qna.test.tsx
@@ -9,8 +9,9 @@ import { act, HookResult } from '@botframework-composer/test-utils/lib/hooks';
import { qnaDispatcher } from '../qna';
import { renderRecoilHook } from '../../../../__tests__/testUtils';
-import { qnaFilesState, currentProjectIdState, localeState, dispatcherState } from '../../atoms';
+import { currentProjectIdState, localeState, dispatcherState } from '../../atoms';
import { Dispatcher } from '..';
+import { qnaFilesSelectorFamily } from '../../selectors';
jest.mock('../../parsers/qnaWorker', () => {
const filterParseResult = (qnaFile: QnAFile) => {
@@ -55,7 +56,7 @@ const qnaFiles = [qna1];
describe('QnA dispatcher', () => {
const useRecoilTestHook = () => {
- const [qnaFiles, setQnAFiles] = useRecoilState(qnaFilesState(projectId));
+ const [qnaFiles, setQnAFiles] = useRecoilState(qnaFilesSelectorFamily(projectId));
const currentDispatcher = useRecoilValue(dispatcherState);
return {
@@ -70,7 +71,7 @@ describe('QnA dispatcher', () => {
beforeEach(() => {
const { result } = renderRecoilHook(useRecoilTestHook, {
states: [
- { recoilState: qnaFilesState(projectId), initialValue: qnaFiles },
+ { recoilState: qnaFilesSelectorFamily(projectId), initialValue: qnaFiles },
{ recoilState: currentProjectIdState, initialValue: projectId },
{ recoilState: localeState(projectId), initialValue: locale },
],
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 6e7160638f..404c180a75 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,13 @@ import { lgDispatcher } from '../lg';
import { luDispatcher } from '../lu';
import { navigationDispatcher } from '../navigation';
import { renderRecoilHook } from '../../../../__tests__/testUtils';
+import { schemasState, dialogSchemasState, actionsSeedState, dispatcherState } from '../../atoms';
import {
- luFilesState,
- schemasState,
- dialogSchemasState,
- actionsSeedState,
- qnaFilesState,
- dispatcherState,
-} from '../../atoms';
-import { dialogsSelectorFamily, lgFilesSelectorFamily } from '../../selectors';
+ dialogsSelectorFamily,
+ lgFilesSelectorFamily,
+ luFilesSelectorFamily,
+ qnaFilesSelectorFamily,
+} from '../../selectors';
import { Dispatcher } from '..';
import { DialogInfo } from '../../../../../types';
@@ -104,10 +102,10 @@ describe('trigger dispatcher', () => {
const useRecoilTestHook = () => {
const dialogs: DialogInfo[] = useRecoilValue(dialogsSelectorFamily(projectId));
const dialogSchemas = useRecoilValue(dialogSchemasState(projectId));
- const luFiles = useRecoilValue(luFilesState(projectId));
+ const luFiles = useRecoilValue(luFilesSelectorFamily(projectId));
const lgFiles = useRecoilValue(lgFilesSelectorFamily(projectId));
const actionsSeed = useRecoilValue(actionsSeedState(projectId));
- const qnaFiles = useRecoilValue(qnaFilesState(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesSelectorFamily(projectId));
const currentDispatcher = useRecoilValue(dispatcherState);
return {
@@ -141,14 +139,14 @@ describe('trigger dispatcher', () => {
],
},
{
- recoilState: luFilesState(projectId),
+ recoilState: luFilesSelectorFamily(projectId),
initialValue: [
{ id: '1.en-us', content: '' },
{ id: '2.en-us', content: '' },
],
},
{
- recoilState: qnaFilesState(projectId),
+ recoilState: qnaFilesSelectorFamily(projectId),
initialValue: [
{ id: '1.en-us', content: '' },
{ id: '2.en-us', content: '' },
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/builder.ts b/Composer/packages/client/src/recoilModel/dispatchers/builder.ts
index c0f3a9afcc..b47ae1162b 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/builder.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/builder.ts
@@ -10,8 +10,8 @@ import { Text, BotStatus } from '../../constants';
import httpClient from '../../utils/httpUtil';
import luFileStatusStorage from '../../utils/luFileStatusStorage';
import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage';
-import { luFilesState, qnaFilesState, botStatusState, botRuntimeErrorState } from '../atoms';
-import { dialogsWithLuProviderSelectorFamily } from '../selectors';
+import { botStatusState, botRuntimeErrorState } from '../atoms';
+import { dialogsWithLuProviderSelectorFamily, luFilesSelectorFamily, qnaFilesSelectorFamily } from '../selectors';
import { getReferredQnaFiles } from '../../utils/qnaUtil';
const checkEmptyQuestionOrAnswerInQnAFile = (sections) => {
@@ -27,8 +27,8 @@ export const builderDispatcher = () => {
) => {
const { set, snapshot } = callbackHelpers;
const dialogs = await snapshot.getPromise(dialogsWithLuProviderSelectorFamily(projectId));
- const luFiles = await snapshot.getPromise(luFilesState(projectId));
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const luFiles = await snapshot.getPromise(luFilesSelectorFamily(projectId));
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
const referredLuFiles = luUtil.checkLuisBuild(luFiles, dialogs);
const referredQnaFiles = getReferredQnaFiles(qnaFiles, dialogs, false);
const errorMsg = referredQnaFiles.reduce(
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts
index b9bf94143b..ce8f827d5b 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts
@@ -475,6 +475,12 @@ export const lgDispatcher = () => {
}
);
+ const updateAllLgFiles = useRecoilCallback(
+ ({ set }: CallbackInterface) => ({ projectId, lgFiles }: { projectId: string; lgFiles: LgFile[] }) => {
+ set(lgFilesSelectorFamily(projectId), lgFiles);
+ }
+ );
+
return {
updateLgFile,
createLgFile,
@@ -486,5 +492,6 @@ export const lgDispatcher = () => {
removeLgTemplates,
copyLgTemplate,
reparseAllLgFiles,
+ updateAllLgFiles,
};
};
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lu.ts b/Composer/packages/client/src/recoilModel/dispatchers/lu.ts
index 1b4e0e1ae1..61449a9cf8 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/lu.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/lu.ts
@@ -9,8 +9,10 @@ import formatMessage from 'format-message';
import luWorker from '../parsers/luWorker';
import { getBaseName, getExtension } from '../../utils/fileUtil';
import luFileStatusStorage from '../../utils/luFileStatusStorage';
-import { luFilesState, localeState, settingsState } from '../atoms/botState';
+import { localeState, settingsState, luFileIdsState } from '../atoms/botState';
+import { luFilesSelectorFamily } from '../selectors/lu';
+import { luFileState } from './../atoms/botState';
import { setError } from './shared';
const intentIsNotEmpty = ({ Name, Body }) => {
@@ -27,37 +29,41 @@ const initialBody = '- ';
*
*/
-const luFilesAtomUpdater = (
+const updateLuFiles = (
+ { set }: CallbackInterface,
+ projectId: string,
changes: {
adds?: LuFile[];
deletes?: LuFile[];
updates?: LuFile[];
},
- filter?: (oldList: LuFile[]) => (changeItem: LuFile) => boolean
+ getLatestFile?: (current: LuFile, changed: LuFile) => LuFile
) => {
- return (oldList: LuFile[]) => {
- 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((luFile) => {
+ set(luFileState({ projectId, luFileId: luFile.id }), (oldLuFile) =>
+ getLatestFile ? getLatestFile(oldLuFile, luFile) : luFile
+ );
+ });
- // adds
- newList = adds.concat(newList);
+ // deletes
+ if (deletes?.length) {
+ const deletedIds = deletes.map((file) => file.id);
+ set(luFileIdsState(projectId), (ids) => ids.filter((id) => !deletedIds.includes(id)));
+ }
- return newList;
- };
+ // adds
+ if (adds?.length) {
+ const addedIds = adds.map((file) => file.id);
+ adds.forEach((luFile) => {
+ set(luFileState({ projectId, luFileId: luFile.id }), (oldLuFile) =>
+ getLatestFile ? getLatestFile(oldLuFile, luFile) : luFile
+ );
+ });
+ set(luFileIdsState(projectId), (ids) => [...ids, ...addedIds]);
+ }
};
const getRelatedLuFileChanges = async (
luFiles: LuFile[],
@@ -119,8 +125,8 @@ export const createLuFileState = async (
callbackHelpers: CallbackInterface,
{ id, content, projectId }: { id: string; content: string; projectId: string }
) => {
- const { set, snapshot } = callbackHelpers;
- const luFiles = await snapshot.getPromise(luFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const luFiles = await snapshot.getPromise(luFilesSelectorFamily(projectId));
const locale = await snapshot.getPromise(localeState(projectId));
const { languages, luFeatures } = await snapshot.getPromise(settingsState(projectId));
const createdLuId = `${id}.${locale}`;
@@ -141,15 +147,15 @@ export const createLuFileState = async (
});
});
- set(luFilesState(projectId), luFilesAtomUpdater({ adds: changes }));
+ updateLuFiles(callbackHelpers, projectId, { adds: changes });
};
export const removeLuFileState = async (
callbackHelpers: CallbackInterface,
{ id, projectId }: { id: string; projectId: string }
) => {
- const { set, snapshot } = callbackHelpers;
- const luFiles = await snapshot.getPromise(luFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const luFiles = await snapshot.getPromise(luFilesSelectorFamily(projectId));
const locale = await snapshot.getPromise(localeState(projectId));
const targetLuFile = luFiles.find((item) => item.id === id) || luFiles.find((item) => item.id === `${id}.${locale}`);
@@ -164,7 +170,7 @@ export const removeLuFileState = async (
luFileStatusStorage.removeFileStatus(projectId, targetLuFile.id);
}
});
- set(luFilesState(projectId), luFilesAtomUpdater({ deletes: [targetLuFile] }));
+ updateLuFiles(callbackHelpers, projectId, { deletes: [targetLuFile] });
};
export const luDispatcher = () => {
@@ -180,19 +186,14 @@ export const luDispatcher = () => {
}) => {
const { set, snapshot } = callbackHelpers;
//set content first
- set(luFilesState(projectId), (prevLuFiles) => {
- return prevLuFiles.map((file) => {
- if (file.id === id) {
- return {
- ...file,
- content,
- };
- }
- return file;
- });
+ set(luFileState({ projectId, luFileId: id }), (prevLuFile) => {
+ return {
+ ...prevLuFile,
+ content,
+ };
});
- const luFiles = await snapshot.getPromise(luFilesState(projectId));
+ const luFiles = await snapshot.getPromise(luFilesSelectorFamily(projectId));
const { luFeatures } = await snapshot.getPromise(settingsState(projectId));
try {
@@ -203,19 +204,11 @@ export const luDispatcher = () => {
* Why other methods do not need double check content?
* Because this method already did set content before call luFilesAtomUpdater.
*/
- set(
- luFilesState(projectId),
- luFilesAtomUpdater({ updates: updatedFiles }, (prevLuFiles) => {
- const targetInState = prevLuFiles.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 (luFile) => luFile.id !== updatedFile.id;
- } else {
- return () => true;
- }
- })
- );
+ updateLuFiles(callbackHelpers, projectId, { updates: updatedFiles }, (current, changed) => {
+ // compare to drop expired content already setted above.
+ if (current.id === id && current?.content !== changed?.content) return current;
+ return changed;
+ });
} catch (error) {
setError(callbackHelpers, error);
}
@@ -234,8 +227,8 @@ export const luDispatcher = () => {
intent: LuIntentSection;
projectId: string;
}) => {
- const { set, snapshot } = callbackHelpers;
- const luFiles = await snapshot.getPromise(luFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const luFiles = await snapshot.getPromise(luFilesSelectorFamily(projectId));
const { luFeatures } = await snapshot.getPromise(settingsState(projectId));
const luFile = luFiles.find((temp) => temp.id === id);
@@ -263,8 +256,7 @@ export const luDispatcher = () => {
)) as LuFile;
changes.push(updatedFile);
}
- set(luFilesState(projectId), luFilesAtomUpdater({ updates: changes }));
-
+ updateLuFiles(callbackHelpers, projectId, { updates: changes });
// body change, only update current locale file
} else {
const updatedFile = (await luWorker.updateIntent(
@@ -273,7 +265,7 @@ export const luDispatcher = () => {
{ Body: intent.Body },
luFeatures
)) as LuFile;
- set(luFilesState(projectId), luFilesAtomUpdater({ updates: [updatedFile] }));
+ updateLuFiles(callbackHelpers, projectId, { updates: [updatedFile] });
}
} catch (error) {
setError(callbackHelpers, error);
@@ -291,8 +283,8 @@ export const luDispatcher = () => {
intent: LuIntentSection;
projectId: string;
}) => {
- const { set, snapshot } = callbackHelpers;
- const luFiles = await snapshot.getPromise(luFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const luFiles = await snapshot.getPromise(luFilesSelectorFamily(projectId));
const { luFeatures } = await snapshot.getPromise(settingsState(projectId));
const file = luFiles.find((temp) => temp.id === id);
@@ -300,7 +292,7 @@ export const luDispatcher = () => {
try {
const updatedFile = (await luWorker.addIntent(file, intent, luFeatures)) as LuFile;
const updatedFiles = await getRelatedLuFileChanges(luFiles, updatedFile, projectId, luFeatures);
- set(luFilesState(projectId), luFilesAtomUpdater({ updates: updatedFiles }));
+ updateLuFiles(callbackHelpers, projectId, { updates: updatedFiles });
} catch (error) {
setError(callbackHelpers, error);
}
@@ -317,8 +309,8 @@ export const luDispatcher = () => {
intentName: string;
projectId: string;
}) => {
- const { set, snapshot } = callbackHelpers;
- const luFiles = await snapshot.getPromise(luFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const luFiles = await snapshot.getPromise(luFilesSelectorFamily(projectId));
const { luFeatures } = await snapshot.getPromise(settingsState(projectId));
const file = luFiles.find((temp) => temp.id === id);
@@ -326,7 +318,7 @@ export const luDispatcher = () => {
try {
const updatedFile = (await luWorker.removeIntent(file, intentName, luFeatures)) as LuFile;
const updatedFiles = await getRelatedLuFileChanges(luFiles, updatedFile, projectId, luFeatures);
- set(luFilesState(projectId), luFilesAtomUpdater({ updates: updatedFiles }));
+ updateLuFiles(callbackHelpers, projectId, { updates: updatedFiles });
} 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 459cf85e39..2723307bdf 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/multilang.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/multilang.ts
@@ -10,9 +10,9 @@ import languageStorage from '../../utils/languageStorage';
import { getExtension } from '../../utils/fileUtil';
import { localBotsDataSelector, rootBotProjectIdSelector } from '../selectors/project';
import { lgFilesSelectorFamily } from '../selectors/lg';
+import { luFilesSelectorFamily } from '../selectors';
import {
- luFilesState,
localeState,
settingsState,
showAddLanguageModalState,
@@ -20,7 +20,6 @@ import {
onDelLanguageDialogCompleteState,
showDelLanguageModalState,
botDisplayNameState,
- // qnaFilesState,
} from './../atoms/botState';
const copyLanguageResources = (files: any[], fromLanguage: string, toLanguages: string[]): any[] => {
@@ -93,19 +92,14 @@ export const multilangDispatcher = () => {
const onAddLanguageDialogComplete = (await snapshot.getPromise(onAddLanguageDialogCompleteState(projectId))).func;
// copy files from default language
- set(lgFilesSelectorFamily(projectId), (prevlgFiles) => {
- const addedLgFiles = copyLanguageResources(prevlgFiles, defaultLang, languages);
- return [...prevlgFiles, ...addedLgFiles];
+ set(lgFilesSelectorFamily(projectId), (oldLgFiles) => {
+ const addedLgFiles = copyLanguageResources(oldLgFiles, defaultLang, languages);
+ return [...oldLgFiles, ...addedLgFiles];
});
- set(luFilesState(projectId), (prevluFiles) => {
+ set(luFilesSelectorFamily(projectId), (prevluFiles) => {
const addedLuFiles = copyLanguageResources(prevluFiles, defaultLang, languages);
return [...prevluFiles, ...addedLuFiles];
});
- //TODO: support QnA multilang in future.
- // set(qnaFilesState(projectId), (prevQnAFiles) => {
- // const addedQnAFiles = copyLanguageResources(prevQnAFiles, defaultLang, languages);
- // return [...prevQnAFiles, ...addedQnAFiles];
- // });
set(settingsState(projectId), (prevSettings) => {
const settings: any = cloneDeep(prevSettings);
if (Array.isArray(settings.languages)) {
@@ -136,18 +130,14 @@ export const multilangDispatcher = () => {
const onDelLanguageDialogComplete = (await snapshot.getPromise(onDelLanguageDialogCompleteState(projectId))).func;
// copy files from default language
- set(lgFilesSelectorFamily(projectId), (prevlgFiles) => {
- const { left: leftLgFiles } = deleteLanguageResources(prevlgFiles, languages);
+ set(lgFilesSelectorFamily(projectId), (prevLgFiles) => {
+ const { left: leftLgFiles } = deleteLanguageResources(prevLgFiles, languages);
return leftLgFiles;
});
- set(luFilesState(projectId), (prevluFiles) => {
- const { left: leftLuFiles } = deleteLanguageResources(prevluFiles, languages);
+ set(luFilesSelectorFamily(projectId), (prevLuFiles) => {
+ const { left: leftLuFiles } = deleteLanguageResources(prevLuFiles, languages);
return leftLuFiles;
});
- // set(qnaFilesState(projectId), (prevQnAFiles) => {
- // const { left: leftQnAFiles } = deleteLanguageResources(prevQnAFiles, languages);
- // return leftQnAFiles;
- // });
set(settingsState(projectId), (prevSettings) => {
const settings: any = cloneDeep(prevSettings);
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts
index a41633c861..3f274f2100 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts
@@ -533,7 +533,7 @@ export const projectDispatcher = () => {
const creationFlowType = await callbackHelpers.snapshot.getPromise(creationFlowTypeState);
callbackHelpers.set(botOpeningMessage, response.data.latestMessage);
- const { botFiles, projectData } = loadProjectData(response.data.result);
+ const { botFiles, projectData } = await loadProjectData(response.data.result);
const projectId = response.data.result.id;
if (creationFlowType === 'Skill') {
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts
index 8e56a86edc..76a5786352 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts
@@ -14,12 +14,15 @@ import {
isEjectRuntimeExistState,
filePersistenceState,
settingsState,
- luFilesState,
- qnaFilesState,
} from '../atoms/botState';
import { openInEmulator } from '../../utils/navigation';
import { botEndpointsState } from '../atoms';
-import { rootBotProjectIdSelector, dialogsSelectorFamily } from '../selectors';
+import {
+ rootBotProjectIdSelector,
+ dialogsSelectorFamily,
+ luFilesSelectorFamily,
+ qnaFilesSelectorFamily,
+} from '../selectors';
import * as luUtil from '../../utils/luUtil';
import { ClientStorage } from '../../utils/storage';
@@ -187,8 +190,8 @@ export const publisherDispatcher = () => {
try {
const { snapshot } = callbackHelpers;
const dialogs = await snapshot.getPromise(dialogsSelectorFamily(projectId));
- const luFiles = await snapshot.getPromise(luFilesState(projectId));
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const luFiles = await snapshot.getPromise(luFilesSelectorFamily(projectId));
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
const referredLuFiles = luUtil.checkLuisBuild(luFiles, dialogs);
const response = await httpClient.post(`/publish/${projectId}/publish/${target.name}`, {
accessToken: token,
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts
index 2f37de8cca..5f03be956d 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts
@@ -7,12 +7,13 @@ import { qnaUtil } from '@bfc/indexers';
import qnaWorker from '../parsers/qnaWorker';
import {
- qnaFilesState,
settingsState,
showCreateQnAFromScratchDialogState,
showCreateQnAFromUrlDialogState,
onCreateQnAFromScratchDialogCompleteState,
onCreateQnAFromUrlDialogCompleteState,
+ qnaFileState,
+ qnaFileIdsState,
} from '../atoms/botState';
import { createQnAOnState } from '../atoms/appState';
import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage';
@@ -24,7 +25,7 @@ import {
getQnaPendingNotification,
} from '../../utils/notifications';
import httpClient from '../../utils/httpUtil';
-import { rootBotProjectIdSelector } from '../selectors';
+import { qnaFilesSelectorFamily, rootBotProjectIdSelector } from '../selectors';
import { addNotificationInternal, deleteNotificationInternal, createNotification } from './notification';
@@ -36,57 +37,60 @@ import { addNotificationInternal, deleteNotificationInternal, createNotification
*
*/
-const qnaFilesAtomUpdater = (
+const updateQnaFiles = (
+ { set }: CallbackInterface,
+ projectId: string,
changes: {
adds?: QnAFile[];
deletes?: QnAFile[];
updates?: QnAFile[];
},
- filter?: (oldList: QnAFile[]) => (changeItem: QnAFile) => boolean
+ getLatestFile?: (current: QnAFile, changed: QnAFile) => QnAFile
) => {
- return (oldList: QnAFile[]) => {
- 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((qnaFile) => {
+ set(qnaFileState({ projectId, qnaFileId: qnaFile.id }), (oldQnaFile) =>
+ getLatestFile ? getLatestFile(oldQnaFile, qnaFile) : qnaFile
+ );
+ });
- // adds
- newList = adds.concat(newList);
+ // deletes
+ if (deletes?.length) {
+ const deletedIds = deletes.map((file) => file.id);
+ set(qnaFileIdsState(projectId), (ids) => ids.filter((id) => !deletedIds.includes(id)));
+ }
- return newList;
- };
+ // adds
+ if (adds?.length) {
+ const addedIds = adds.map((file) => file.id);
+ adds.forEach((qnaFile) => {
+ set(qnaFileState({ projectId, qnaFileId: qnaFile.id }), (oldQnaFile) =>
+ getLatestFile ? getLatestFile(oldQnaFile, qnaFile) : qnaFile
+ );
+ });
+ set(qnaFileIdsState(projectId), (ids) => [...ids, ...addedIds]);
+ }
};
export const updateQnAFileState = async (
callbackHelpers: CallbackInterface,
{ id, content, projectId }: { id: string; content: string; projectId: string }
) => {
- const { set } = callbackHelpers;
//To do: support other languages on qna
id = id.endsWith('.source') ? id : `${getBaseName(id)}.en-us`;
const updatedQnAFile = (await qnaWorker.parse(id, content)) as QnAFile;
- set(qnaFilesState(projectId), qnaFilesAtomUpdater({ updates: [updatedQnAFile] }));
+ updateQnaFiles(callbackHelpers, projectId, { updates: [updatedQnAFile] });
};
export const createQnAFileState = async (
callbackHelpers: CallbackInterface,
{ id, content, projectId }: { id: string; content: string; projectId: string }
) => {
- const { set, snapshot } = callbackHelpers;
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
//const locale = await snapshot.getPromise(localeState(projectId));
//To do: support other languages on qna
const locale = 'en-us';
@@ -107,7 +111,7 @@ export const createQnAFileState = async (
id: fileId,
});
});
- set(qnaFilesState(projectId), qnaFilesAtomUpdater({ adds: changes }));
+ updateQnaFiles(callbackHelpers, projectId, { adds: changes });
};
/**
@@ -120,8 +124,8 @@ export const removeQnAFileState = async (
callbackHelpers: CallbackInterface,
{ id, projectId }: { id: string; projectId: string }
) => {
- const { set, snapshot } = callbackHelpers;
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
//const locale = await snapshot.getPromise(localeState(projectId));
//To do: support other languages on qna
const locale = 'en-us';
@@ -137,15 +141,15 @@ export const removeQnAFileState = async (
qnaFileStatusStorage.removeFileStatus(projectId, targetQnAFile.id);
}
});
- set(qnaFilesState(projectId), qnaFilesAtomUpdater({ deletes: [targetQnAFile] }));
+ updateQnaFiles(callbackHelpers, projectId, { deletes: [targetQnAFile] });
};
export const createKBFileState = async (
callbackHelpers: CallbackInterface,
{ id, name, content, projectId }: { id: string; name: string; content: string; projectId: string }
) => {
- const { set, snapshot } = callbackHelpers;
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
const createdSourceQnAId = `${name}.source`;
if (qnaFiles.find((qna) => qna.id === createdSourceQnAId)) {
@@ -173,15 +177,15 @@ export const createKBFileState = async (
}
qnaFileStatusStorage.updateFileStatus(projectId, createdSourceQnAId);
- set(qnaFilesState(projectId), qnaFilesAtomUpdater({ updates: updatedQnAFiles, adds: [createdQnAFile] }));
+ updateQnaFiles(callbackHelpers, projectId, { updates: updatedQnAFiles, adds: [createdQnAFile] });
};
export const removeKBFileState = async (
callbackHelpers: CallbackInterface,
{ id, projectId }: { id: string; projectId: string }
) => {
- const { set, snapshot } = callbackHelpers;
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
// const locale = await snapshot.getPromise(localeState(projectId));
//To do: support other languages on qna
const locale = 'en-us';
@@ -197,7 +201,7 @@ export const removeKBFileState = async (
qnaFileStatusStorage.removeFileStatus(projectId, targetQnAFile.id);
}
});
- set(qnaFilesState(projectId), qnaFilesAtomUpdater({ deletes: [targetQnAFile] }));
+ updateQnaFiles(callbackHelpers, projectId, { deletes: [targetQnAFile] });
};
export const renameKBFileState = async (
@@ -205,7 +209,7 @@ export const renameKBFileState = async (
{ id, name, projectId }: { id: string; name: string; projectId: string }
) => {
const { set, snapshot } = callbackHelpers;
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
//const locale = await snapshot.getPromise(localeState(projectId));
//To do: support other languages
const locale = 'en-us';
@@ -223,16 +227,11 @@ export const renameKBFileState = async (
}
qnaFileStatusStorage.removeFileStatus(projectId, targetQnAFile.id);
- set(qnaFilesState(projectId), (prevQnAFiles) => {
- return prevQnAFiles.map((file) => {
- if (file.id === targetQnAFile.id) {
- return {
- ...file,
- id: name,
- };
- }
- return file;
- });
+ set(qnaFileState({ projectId, qnaFileId: id }), (prevQnAFile) => {
+ return {
+ ...prevQnAFile,
+ id: name,
+ };
});
};
@@ -465,7 +464,7 @@ ${response.data}
);
const updateQnAQuestion = useRecoilCallback(
- ({ set, snapshot }: CallbackInterface) => async ({
+ (callbackHelpers: CallbackInterface) => async ({
id,
sectionId,
questionId,
@@ -478,18 +477,19 @@ ${response.data}
content: string;
projectId: string;
}) => {
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
const qnaFile = qnaFiles.find((temp) => temp.id === id);
if (!qnaFile) return qnaFiles;
// const updatedFile = await updateQnAFileState(callbackHelpers, { id, content });
const updatedFile = qnaUtil.updateQnAQuestion(qnaFile, sectionId, questionId, content);
- set(qnaFilesState(projectId), qnaFilesAtomUpdater({ updates: [updatedFile] }));
+ updateQnaFiles(callbackHelpers, projectId, { updates: [updatedFile] });
}
);
const updateQnAAnswer = useRecoilCallback(
- ({ set, snapshot }: CallbackInterface) => async ({
+ (callbackHelpers: CallbackInterface) => async ({
id,
sectionId,
content,
@@ -500,17 +500,18 @@ ${response.data}
content: string;
projectId: string;
}) => {
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
const qnaFile = qnaFiles.find((temp) => temp.id === id);
if (!qnaFile) return qnaFiles;
const updatedFile = qnaUtil.updateQnAAnswer(qnaFile, sectionId, content);
- set(qnaFilesState(projectId), qnaFilesAtomUpdater({ updates: [updatedFile] }));
+ updateQnaFiles(callbackHelpers, projectId, { updates: [updatedFile] });
}
);
const createQnAQuestion = useRecoilCallback(
- ({ set, snapshot }: CallbackInterface) => async ({
+ (callbackHelpers: CallbackInterface) => async ({
id,
sectionId,
content,
@@ -521,17 +522,18 @@ ${response.data}
content: string;
projectId: string;
}) => {
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
const qnaFile = qnaFiles.find((temp) => temp.id === id);
if (!qnaFile) return qnaFiles;
const updatedFile = qnaUtil.createQnAQuestion(qnaFile, sectionId, content);
- set(qnaFilesState(projectId), qnaFilesAtomUpdater({ updates: [updatedFile] }));
+ updateQnaFiles(callbackHelpers, projectId, { updates: [updatedFile] });
}
);
const removeQnAQuestion = useRecoilCallback(
- ({ set, snapshot }: CallbackInterface) => async ({
+ (callbackHelpers: CallbackInterface) => async ({
id,
sectionId,
questionId,
@@ -542,17 +544,18 @@ ${response.data}
questionId: string;
projectId: string;
}) => {
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
const qnaFile = qnaFiles.find((temp) => temp.id === id);
if (!qnaFile) return qnaFiles;
const updatedFile = qnaUtil.removeQnAQuestion(qnaFile, sectionId, questionId);
- set(qnaFilesState(projectId), qnaFilesAtomUpdater({ updates: [updatedFile] }));
+ updateQnaFiles(callbackHelpers, projectId, { updates: [updatedFile] });
}
);
const createQnAPairs = useRecoilCallback(
- ({ set, snapshot }: CallbackInterface) => async ({
+ (callbackHelpers: CallbackInterface) => async ({
id,
content,
projectId,
@@ -561,18 +564,19 @@ ${response.data}
content: string;
projectId: string;
}) => {
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
const qnaFile = qnaFiles.find((temp) => temp.id === id);
if (!qnaFile) return qnaFiles;
// insert into head, need investigate
const updatedFile = qnaUtil.insertSection(qnaFile, 0, content);
- set(qnaFilesState(projectId), qnaFilesAtomUpdater({ updates: [updatedFile] }));
+ updateQnaFiles(callbackHelpers, projectId, { updates: [updatedFile] });
}
);
const removeQnAPairs = useRecoilCallback(
- ({ set, snapshot }: CallbackInterface) => async ({
+ (callbackHelpers: CallbackInterface) => async ({
id,
sectionId,
projectId,
@@ -581,17 +585,18 @@ ${response.data}
sectionId: string;
projectId: string;
}) => {
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
const qnaFile = qnaFiles.find((temp) => temp.id === id);
if (!qnaFile) return qnaFiles;
const updatedFile = qnaUtil.removeSection(qnaFile, sectionId);
- set(qnaFilesState(projectId), qnaFilesAtomUpdater({ updates: [updatedFile] }));
+ updateQnaFiles(callbackHelpers, projectId, { updates: [updatedFile] });
}
);
const createQnAImport = useRecoilCallback(
- ({ set, snapshot }: CallbackInterface) => async ({
+ (callbackHelpers: CallbackInterface) => async ({
id,
sourceId,
projectId,
@@ -600,16 +605,17 @@ ${response.data}
sourceId: string;
projectId: string;
}) => {
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
const qnaFile = qnaFiles.find((temp) => temp.id === id);
if (!qnaFile) return qnaFiles;
const updatedFile = qnaUtil.addImport(qnaFile, `${sourceId}.qna`);
- set(qnaFilesState(projectId), qnaFilesAtomUpdater({ updates: [updatedFile] }));
+ updateQnaFiles(callbackHelpers, projectId, { updates: [updatedFile] });
}
);
const removeQnAImport = useRecoilCallback(
- ({ set, snapshot }: CallbackInterface) => async ({
+ (callbackHelpers: CallbackInterface) => async ({
id,
sourceId,
projectId,
@@ -618,16 +624,17 @@ ${response.data}
sourceId: string;
projectId: string;
}) => {
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
const qnaFile = qnaFiles.find((temp) => temp.id === id);
if (!qnaFile) return qnaFiles;
const updatedFile = qnaUtil.removeImport(qnaFile, `${sourceId}.qna`);
- set(qnaFilesState(projectId), qnaFilesAtomUpdater({ updates: [updatedFile] }));
+ updateQnaFiles(callbackHelpers, projectId, { updates: [updatedFile] });
}
);
const updateQnAImport = useRecoilCallback(
- ({ set, snapshot }: CallbackInterface) => async ({
+ (callbackHelpers: CallbackInterface) => async ({
id,
sourceId,
newSourceId,
@@ -638,13 +645,14 @@ ${response.data}
newSourceId: string;
projectId: string;
}) => {
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const { snapshot } = callbackHelpers;
+ const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
const qnaFile = qnaFiles.find((temp) => temp.id === id);
if (!qnaFile) return qnaFiles;
let updatedFile = qnaUtil.removeImport(qnaFile, `${sourceId}.qna`);
updatedFile = qnaUtil.addImport(updatedFile, `${newSourceId}.qna`);
- set(qnaFilesState(projectId), qnaFilesAtomUpdater({ updates: [updatedFile] }));
+ updateQnaFiles(callbackHelpers, projectId, { updates: [updatedFile] });
}
);
const removeQnAKB = useRecoilCallback(
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/trigger.ts b/Composer/packages/client/src/recoilModel/dispatchers/trigger.ts
index a1104ae9e4..38a0a8897c 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/trigger.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/trigger.ts
@@ -5,8 +5,8 @@ import { useRecoilCallback, CallbackInterface } from 'recoil';
import { BaseSchema, deleteActions, ITriggerCondition, LgTemplate, LgTemplateSamples, SDKKinds } from '@bfc/shared';
import get from 'lodash/get';
-import { luFilesState, schemasState, dialogState, localeState } from '../atoms/botState';
-import { dialogsSelectorFamily } from '../selectors';
+import { schemasState, dialogState, localeState } from '../atoms/botState';
+import { dialogsSelectorFamily, luFilesSelectorFamily } from '../selectors';
import {
onChooseIntentKey,
generateNewDialog,
@@ -49,7 +49,7 @@ export const triggerDispatcher = () => {
const { snapshot } = callbackHelpers;
const dispatcher = await snapshot.getPromise(dispatcherState);
const lgFiles = await snapshot.getPromise(lgFilesSelectorFamily(projectId));
- const luFiles = await snapshot.getPromise(luFilesState(projectId));
+ const luFiles = await snapshot.getPromise(luFilesSelectorFamily(projectId));
const dialogs = await snapshot.getPromise(dialogsSelectorFamily(projectId));
const dialog = await snapshot.getPromise(dialogState({ projectId, dialogId }));
const schemas = await snapshot.getPromise(schemasState(projectId));
@@ -130,7 +130,7 @@ export const triggerDispatcher = () => {
const { snapshot } = callbackHelpers;
const dispatcher = await snapshot.getPromise(dispatcherState);
const locale = await snapshot.getPromise(localeState(projectId));
- const luFiles = await snapshot.getPromise(luFilesState(projectId));
+ const luFiles = await snapshot.getPromise(luFilesSelectorFamily(projectId));
const luFile = luFiles.find((file) => file.id === `${dialogId}.${locale}`);
const { removeLuIntent, removeLgTemplates } = dispatcher;
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts
index 34ec545479..9b2c140c43 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 } from '@bfc/indexers';
+import { BotIndexer, indexer } from '@bfc/indexers';
import {
BotProjectFile,
BotProjectSpace,
@@ -62,9 +62,7 @@ import {
jsonSchemaFilesState,
localeState,
locationState,
- luFilesState,
projectMetaDataState,
- qnaFilesState,
recentProjectsState,
schemasState,
settingsState,
@@ -80,30 +78,22 @@ import lgWorker from '../../parsers/lgWorker';
import luWorker from '../../parsers/luWorker';
import qnaWorker from '../../parsers/qnaWorker';
import FilePersistence from '../../persistence/FilePersistence';
-import { botRuntimeOperationsSelector, rootBotProjectIdSelector } from '../../selectors';
+import {
+ botRuntimeOperationsSelector,
+ luFilesSelectorFamily,
+ qnaFilesSelectorFamily,
+ rootBotProjectIdSelector,
+} from '../../selectors';
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 { createMissingLgTemplatesForDialogs } from '../../../utils/lgUtil';
import { getPublishProfileFromPayload } from '../../../utils/electronUtil';
-import { crossTrainConfigState } from './../../atoms/botState';
+import { crossTrainConfigState, projectIndexingState } from './../../atoms/botState';
import { recognizersSelectorFamily } from './../../selectors/recognizers';
-const repairBotProject = async (
- callbackHelpers: CallbackInterface,
- { projectId, botFiles }: { projectId: string; botFiles: any }
-) => {
- const { set } = callbackHelpers;
- const lgFiles: LgFile[] = botFiles.lgFiles;
- const dialogs: DialogInfo[] = botFiles.dialogs;
-
- const updatedLgFiles = await createMissingLgTemplatesForDialogs(projectId, dialogs, lgFiles);
- set(lgFilesSelectorFamily(projectId), updatedLgFiles);
-};
-
export const resetBotStates = async ({ reset }: CallbackInterface, projectId: string) => {
const botStates = Object.keys(botstates);
botStates.forEach((state) => {
@@ -239,19 +229,115 @@ export const navigateToSkillBot = (rootProjectId: string, skillId: string, mainD
}
};
-export const loadProjectData = (data) => {
+const emptyLgFile = (id: string, content: string): LgFile => {
+ return {
+ id,
+ content,
+ diagnostics: [],
+ templates: [],
+ allTemplates: [],
+ imports: [],
+ isContentUnparsed: true,
+ };
+};
+
+const emptyLuFile = (id: string, content: string): LuFile => {
+ return {
+ id,
+ content,
+ diagnostics: [],
+ intents: [],
+ empty: true,
+ resource: {
+ Sections: [],
+ Errors: [],
+ Content: '',
+ },
+ imports: [],
+ isContentUnparsed: true,
+ };
+};
+
+const emptyQnaFile = (id: string, content: string): QnAFile => {
+ return {
+ id,
+ content,
+ diagnostics: [],
+ qnaSections: [],
+ imports: [],
+ options: [],
+ empty: true,
+ resource: {
+ Sections: [],
+ Errors: [],
+ Content: '',
+ },
+ isContentUnparsed: true,
+ };
+};
+
+const parseAllAssets = async ({ set }: CallbackInterface, projectId: string, botFiles: any) => {
+ const { luFiles, lgFiles, qnaFiles, mergedSettings } = botFiles;
+
+ const [parsedLgFiles, parsedLuFiles, parsedQnaFiles] = await Promise.all([
+ lgWorker.parseAll(projectId, lgFiles),
+ luWorker.parseAll(luFiles, mergedSettings.luFeatures),
+ qnaWorker.parseAll(qnaFiles),
+ ]);
+
+ set(lgFilesSelectorFamily(projectId), (oldFiles) => {
+ return oldFiles.map((item) => {
+ const file = (parsedLgFiles as LgFile[]).find((file) => file.id === item.id);
+ return file && item.isContentUnparsed ? file : item;
+ });
+ });
+
+ set(luFilesSelectorFamily(projectId), (oldFiles) => {
+ return oldFiles.map((item) => {
+ const file = (parsedLuFiles as LuFile[]).find((file) => file.id === item.id);
+ return file && item.isContentUnparsed ? file : item;
+ });
+ });
+
+ set(qnaFilesSelectorFamily(projectId), (oldFiles) => {
+ return oldFiles.map((item) => {
+ const file = (parsedQnaFiles as QnAFile[]).find((file) => file.id === item.id);
+ return file && item.isContentUnparsed ? file : item;
+ });
+ });
+
+ set(projectIndexingState(projectId), false);
+};
+
+export const loadProjectData = async (data) => {
const { files, botName, settings, id: projectId } = data;
const mergedSettings = getMergedSettings(projectId, settings, botName);
- const storedLocale = languageStorage.get(botName)?.locale;
- const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage;
- const indexedFiles = indexer.index(files, botName, locale, mergedSettings);
+ const indexedFiles = indexer.index(files, botName);
+
+ const { lgResources, luResources, qnaResources } = indexedFiles;
+ //parse all resources with worker
+ lgWorker.addProject(projectId);
+
+ const lgFiles = lgResources.map(({ id, content }) => emptyLgFile(id, content));
+ const luFiles = luResources.map(({ id, content }) => emptyLuFile(id, content));
+ const qnaFiles = qnaResources.map(({ id, content }) => emptyQnaFile(id, content));
// migrate script move qna pairs in *.qna to *-manual.source.qna.
// TODO: remove after a period of time.
- const updateQnAFiles = reformQnAToContainerKB(projectId, indexedFiles.qnaFiles);
+ const updateQnAFiles = reformQnAToContainerKB(projectId, qnaFiles);
+
+ const assets = { ...indexedFiles, lgFiles, luFiles, qnaFiles: updateQnAFiles };
+ //Validate all files
+ const diagnostics = BotIndexer.validate({
+ ...assets,
+ setting: settings,
+ botProjectFile: assets.botProjectSpaceFiles[0],
+ });
+
+ const botFiles = { ...assets, mergedSettings, diagnostics };
return {
- botFiles: { ...indexedFiles, qnaFiles: updateQnAFiles, mergedSettings },
+ botFiles,
projectData: data,
error: undefined,
};
@@ -263,7 +349,7 @@ export const fetchProjectDataByPath = async (
): Promise<{ botFiles: any; projectData: any; error: any }> => {
try {
const response = await httpClient.put(`/projects/open`, { path, storageId });
- const projectData = loadProjectData(response.data);
+ const projectData = await loadProjectData(response.data);
return projectData;
} catch (ex) {
return {
@@ -277,7 +363,7 @@ export const fetchProjectDataByPath = async (
export const fetchProjectDataById = async (projectId): Promise<{ botFiles: any; projectData: any; error: any }> => {
try {
const response = await httpClient.get(`/projects/${projectId}`);
- const projectData = loadProjectData(response.data);
+ const projectData = await loadProjectData(response.data);
return projectData;
} catch (ex) {
return {
@@ -389,8 +475,6 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any
set(recognizersSelectorFamily(projectId), recognizers);
set(crossTrainConfigState(projectId), crossTrainConfig);
- await lgWorker.addProject(projectId, lgFiles);
-
// Form dialogs
set(
formDialogSchemaIdsState(projectId),
@@ -401,14 +485,14 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any
});
set(skillManifestsState(projectId), skillManifests);
- set(luFilesState(projectId), initLuFilesStatus(botName, luFiles, dialogs));
+ set(luFilesSelectorFamily(projectId), initLuFilesStatus(botName, luFiles, dialogs));
set(lgFilesSelectorFamily(projectId), lgFiles);
set(jsonSchemaFilesState(projectId), jsonSchemaFiles);
set(dialogSchemasState(projectId), dialogSchemas);
set(botEnvironmentState(projectId), botEnvironment);
set(botDisplayNameState(projectId), botName);
- set(qnaFilesState(projectId), initQnaFilesStatus(botName, qnaFiles, dialogs));
+ set(qnaFilesSelectorFamily(projectId), initQnaFilesStatus(botName, qnaFiles, dialogs));
set(botStatusState(projectId), BotStatus.inactive);
set(locationState(projectId), location);
set(schemasState(projectId), schemas);
@@ -419,9 +503,8 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any
set(filePersistenceState(projectId), new FilePersistence(projectId));
set(undoHistoryState(projectId), new UndoHistory(projectId));
-
- // async repair bot assets, add missing lg templates
- repairBotProject(callbackHelpers, { projectId, botFiles });
+ set(projectIndexingState(projectId), true);
+ parseAllAssets(callbackHelpers, projectId, botFiles);
return mainDialog;
};
@@ -529,7 +612,7 @@ export const createNewBotFromTemplate = async (
alias,
preserveRoot,
});
- const { botFiles, projectData } = loadProjectData(response.data);
+ const { botFiles, projectData } = await loadProjectData(response.data);
const projectId = response.data.id;
if (settingStorage.get(projectId)) {
settingStorage.remove(projectId);
@@ -757,7 +840,7 @@ export const saveProject = async (callbackHelpers, oldProjectData) => {
description,
location,
});
- const data = loadProjectData(response.data);
+ const data = await loadProjectData(response.data);
if (data.error) {
throw data.error;
}
diff --git a/Composer/packages/client/src/recoilModel/parsers/__test__/lgWorker.test.ts b/Composer/packages/client/src/recoilModel/parsers/__test__/lgWorker.test.ts
index 1d8e6fc5de..4b08dca445 100644
--- a/Composer/packages/client/src/recoilModel/parsers/__test__/lgWorker.test.ts
+++ b/Composer/packages/client/src/recoilModel/parsers/__test__/lgWorker.test.ts
@@ -24,6 +24,7 @@ const lgFiles = [
{
id: 'common.en-us',
content: `\r\n# Hello\r\n-hi`,
+ isContentUnparsed: true,
},
] as LgFile[];
@@ -35,7 +36,7 @@ const getLgTemplate = (name, body): LgTemplate =>
describe('test lg worker', () => {
it('cache the new project', async () => {
- await lgWorker.addProject('test', lgFiles);
+ await lgWorker.addProject('test');
expect(lgCache.projects.has('test')).toBeTruthy();
});
diff --git a/Composer/packages/client/src/recoilModel/parsers/__test__/qnaWorker.test.ts b/Composer/packages/client/src/recoilModel/parsers/__test__/qnaWorker.test.ts
new file mode 100644
index 0000000000..0e699905e7
--- /dev/null
+++ b/Composer/packages/client/src/recoilModel/parsers/__test__/qnaWorker.test.ts
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import qnaWorker from '../qnaWorker';
+
+jest.mock('./../workers/qnaParser.worker.ts', () => {
+ class Test {
+ onmessage = (data) => data;
+
+ postMessage = (data) => {
+ const payload = require('../workers/qnaParser.worker').handleMessage(data);
+ this.onmessage({ data: { id: data.id, payload } });
+ };
+ }
+
+ return Test;
+});
+
+describe('test qna worker', () => {
+ it('get expected parse result', async () => {
+ const content = `# ? Hi
+ ${'```'}
+ Hello
+ ${'```'}`;
+ const result: any = await qnaWorker.parse('', content);
+ expect(result.qnaSections.length).toBe(1);
+ expect(result.isContentUnparsed).toBe(false);
+ });
+
+ it('get expected parse result', async () => {
+ const content = `# ? Hi
+ ${'```'}
+ Hello
+ ${'```'}`;
+ const content1 = `# ? How
+ ${'```'}
+ No
+ ${'```'}`;
+ const result: any = await qnaWorker.parseAll([
+ {
+ id: '1',
+ content,
+ },
+ {
+ id: '2',
+ content: content1,
+ },
+ ]);
+ expect(result[0].qnaSections.length).toBe(1);
+ });
+});
diff --git a/Composer/packages/client/src/recoilModel/parsers/lgWorker.ts b/Composer/packages/client/src/recoilModel/parsers/lgWorker.ts
index b808f66d49..83a246df1a 100644
--- a/Composer/packages/client/src/recoilModel/parsers/lgWorker.ts
+++ b/Composer/packages/client/src/recoilModel/parsers/lgWorker.ts
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-import { LgFile, LgTemplate } from '@bfc/shared';
+import { LgFile, LgTemplate, TextFile } from '@bfc/shared';
import Worker from './workers/lgParser.worker.ts';
import { BaseWorker } from './baseWorker';
@@ -15,12 +15,13 @@ import {
LgCopyTemplatePayload,
LgNewCachePayload,
LgCleanCachePayload,
+ LgParseAllPayload,
} from './types';
// Wrapper class
class LgWorker extends BaseWorker {
- addProject(projectId: string, lgFiles: LgFile[]) {
- return this.sendMsg(LgActionType.NewCache, { projectId, lgFiles });
+ addProject(projectId: string) {
+ return this.sendMsg(LgActionType.NewCache, { projectId });
}
removeProject(projectId: string) {
@@ -31,6 +32,10 @@ class LgWorker extends BaseWorker {
return this.sendMsg(LgActionType.Parse, { id, content, lgFiles, projectId });
}
+ parseAll(projectId: string, lgResources: TextFile[]) {
+ return this.sendMsg(LgActionType.ParseAll, { lgResources, projectId });
+ }
+
addTemplate(projectId: string, lgFile: LgFile, template: LgTemplate, lgFiles: LgFile[]) {
return this.sendMsg(LgActionType.AddTemplate, { lgFile, template, lgFiles, projectId });
}
diff --git a/Composer/packages/client/src/recoilModel/parsers/luWorker.ts b/Composer/packages/client/src/recoilModel/parsers/luWorker.ts
index fd43f85c8b..0b15f1b4b5 100644
--- a/Composer/packages/client/src/recoilModel/parsers/luWorker.ts
+++ b/Composer/packages/client/src/recoilModel/parsers/luWorker.ts
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-import { LuIntentSection, LuFile } from '@bfc/shared';
+import { LuIntentSection, LuFile, TextFile } from '@bfc/shared';
import Worker from './workers/luParser.worker.ts';
import { BaseWorker } from './baseWorker';
@@ -12,6 +12,7 @@ import {
LuRemoveIntentsPayload,
LuRemoveIntentPayload,
LuUpdateIntentPayload,
+ LuParseAllPayload,
} from './types';
// Wrapper class
class LuWorker extends BaseWorker {
@@ -20,6 +21,11 @@ class LuWorker extends BaseWorker {
return this.sendMsg(LuActionType.Parse, payload);
}
+ parseAll(luResources: TextFile[], luFeatures) {
+ const payload = { luResources, luFeatures };
+ return this.sendMsg(LuActionType.ParseAll, payload);
+ }
+
addIntent(luFile: LuFile, intent: LuIntentSection, luFeatures) {
const payload = { luFile, intent, luFeatures };
return this.sendMsg(LuActionType.AddIntent, payload);
diff --git a/Composer/packages/client/src/recoilModel/parsers/qnaWorker.ts b/Composer/packages/client/src/recoilModel/parsers/qnaWorker.ts
index 44bef644c0..134b1b05de 100644
--- a/Composer/packages/client/src/recoilModel/parsers/qnaWorker.ts
+++ b/Composer/packages/client/src/recoilModel/parsers/qnaWorker.ts
@@ -1,30 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+import { TextFile } from '@bfc/shared';
+
import Worker from './workers/qnaParser.worker.ts';
import { BaseWorker } from './baseWorker';
-import { QnAPayload, QnAActionType } from './types';
+import { QnAActionType, QnAParseAllPayload, QnAParsePayload } from './types';
// Wrapper class
class QnAWorker extends BaseWorker {
parse(id: string, content: string) {
const payload = { id, content };
- return this.sendMsg(QnAActionType.Parse, payload);
- }
-
- addSection(content: string, newContent: string) {
- const payload = { content, newContent };
- return this.sendMsg(QnAActionType.AddSection, payload);
- }
-
- updateSection(indexId: number, content: string, newContent: string) {
- const payload = { indexId, content, newContent };
- return this.sendMsg(QnAActionType.UpdateSection, payload);
+ return this.sendMsg(QnAActionType.Parse, payload);
}
- removeSection(indexId: number, content: string) {
- const payload = { content, indexId };
- return this.sendMsg(QnAActionType.RemoveSection, payload);
+ parseAll(qnaResources: TextFile[]) {
+ const payload = { qnaResources };
+ return this.sendMsg(QnAActionType.ParseAll, payload);
}
}
diff --git a/Composer/packages/client/src/recoilModel/parsers/types.ts b/Composer/packages/client/src/recoilModel/parsers/types.ts
index 77311b6dfd..34a74a85de 100644
--- a/Composer/packages/client/src/recoilModel/parsers/types.ts
+++ b/Composer/packages/client/src/recoilModel/parsers/types.ts
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-import { LuIntentSection, LgFile, LuFile, QnASection, FileInfo, LgTemplate, ILUFeaturesConfig } from '@bfc/shared';
+import { LuIntentSection, LgFile, LuFile, FileInfo, LgTemplate, ILUFeaturesConfig, TextFile } from '@bfc/shared';
import { FileAsset } from '../persistence/types';
@@ -10,6 +10,11 @@ export type LuParsePayload = {
luFeatures: ILUFeaturesConfig;
};
+export type LuParseAllPayload = {
+ luResources: TextFile[];
+ luFeatures: ILUFeaturesConfig;
+};
+
export type LuAddIntentPayload = {
luFile: LuFile;
intent: LuIntentSection;
@@ -86,13 +91,17 @@ export interface LgRemoveAllTemplatesPayload {
export interface LgNewCachePayload {
projectId: string;
- lgFiles: LgFile[];
}
export interface LgCleanCachePayload {
projectId: string;
}
+export type LgParseAllPayload = {
+ projectId: string;
+ lgResources: TextFile[];
+};
+
export interface LgCopyTemplatePayload {
projectId: string;
lgFile: LgFile;
@@ -109,10 +118,13 @@ export type IndexPayload = {
luFeatures: { key: string; value: boolean };
};
-export type QnAPayload = {
+export type QnAParsePayload = {
content: string;
- id?: string;
- section?: QnASection;
+ id: string;
+};
+
+export type QnAParseAllPayload = {
+ qnaResources: TextFile[];
};
export enum LuActionType {
@@ -122,6 +134,7 @@ export enum LuActionType {
RemoveIntent = 'remove-intent',
AddIntents = 'add-intents',
RemoveIntents = 'remove-intents',
+ ParseAll = 'parse-all',
}
export enum LgActionType {
@@ -134,6 +147,7 @@ export enum LgActionType {
RemoveTemplate = 'remove-template',
RemoveAllTemplates = 'remove-all-templates',
CopyTemplate = 'copy-template',
+ ParseAll = 'parse-all',
}
export enum IndexerActionType {
@@ -142,9 +156,7 @@ export enum IndexerActionType {
export enum QnAActionType {
Parse = 'parse',
- AddSection = 'add-section',
- UpdateSection = 'update-section',
- RemoveSection = 'remove-section',
+ ParseAll = 'parse-all',
}
export type FilesDifferencePayload = {
diff --git a/Composer/packages/client/src/recoilModel/parsers/workers/calculator.worker.ts b/Composer/packages/client/src/recoilModel/parsers/workers/calculator.worker.ts
index 610da18579..b724a07bda 100644
--- a/Composer/packages/client/src/recoilModel/parsers/workers/calculator.worker.ts
+++ b/Composer/packages/client/src/recoilModel/parsers/workers/calculator.worker.ts
@@ -17,9 +17,18 @@ type MessageEvent = DifferenceMessage;
const ctx: Worker = self as any;
+const getCompareFields = (value: FileAsset) => {
+ const { id, content } = value;
+ return { id, content };
+};
+
+const comparator = (value: FileAsset, other: FileAsset) => {
+ return isEqual(getCompareFields(value), getCompareFields(other));
+};
+
export function getDifferenceItems(target: FileAsset[], origin: FileAsset[]) {
- const changes1 = differenceWith(target, origin, isEqual);
- const changes2 = differenceWith(origin, target, isEqual);
+ const changes1 = differenceWith(target, origin, comparator);
+ const changes2 = differenceWith(origin, target, comparator);
const deleted = changes2.filter((change) => !target.some((file) => change.id === file.id));
const { updated, added } = changes1.reduce(
(result: { updated: FileAsset[]; added: FileAsset[] }, change) => {
diff --git a/Composer/packages/client/src/recoilModel/parsers/workers/lgParser.worker.ts b/Composer/packages/client/src/recoilModel/parsers/workers/lgParser.worker.ts
index 67bc1377f4..9003ec80dd 100644
--- a/Composer/packages/client/src/recoilModel/parsers/workers/lgParser.worker.ts
+++ b/Composer/packages/client/src/recoilModel/parsers/workers/lgParser.worker.ts
@@ -14,6 +14,7 @@ import {
LgCopyTemplatePayload,
LgNewCachePayload,
LgCleanCachePayload,
+ LgParseAllPayload,
} from '../types';
const ctx: Worker = self as any;
@@ -78,6 +79,12 @@ interface CleanCacheMeassage {
payload: LgCleanCachePayload;
}
+type ParseAllMessage = {
+ id: string;
+ type: LgActionType.ParseAll;
+ payload: LgParseAllPayload;
+};
+
type LgMessageEvent =
| NewCacheMessage
| CleanCacheMeassage
@@ -87,7 +94,8 @@ type LgMessageEvent =
| UpdateMessage
| RemoveMessage
| RemoveAllMessage
- | CopyMessage;
+ | CopyMessage
+ | ParseAllMessage;
type LgResources = Map;
@@ -127,11 +135,8 @@ export class LgCache {
this.projects.delete(projectId);
}
- public addProject(projectId: string, lgFiles: LgFile[]) {
+ public addProject(projectId: string) {
const lgResources = new Map();
- lgFiles.forEach((file) => {
- lgResources.set(file.id, lgUtil.parse(file.id, file.content, lgFiles));
- });
this.projects.set(projectId, lgResources);
}
}
@@ -159,8 +164,8 @@ export const handleMessage = (msg: LgMessageEvent) => {
let payload: any = null;
switch (msg.type) {
case LgActionType.NewCache: {
- const { projectId, lgFiles } = msg.payload;
- cache.addProject(projectId, lgFiles);
+ const { projectId } = msg.payload;
+ cache.addProject(projectId);
break;
}
@@ -179,6 +184,18 @@ export const handleMessage = (msg: LgMessageEvent) => {
break;
}
+ case LgActionType.ParseAll: {
+ const { lgResources, projectId } = msg.payload;
+
+ payload = lgResources.map(({ id, content }) => {
+ const lgFile = lgUtil.parse(id, content, lgResources);
+ cache.set(projectId, lgFile);
+ return filterParseResult(lgFile);
+ });
+
+ break;
+ }
+
case LgActionType.AddTemplate: {
const { lgFile, template, lgFiles, projectId } = msg.payload;
const result = lgUtil.addTemplate(getTargetFile(projectId, lgFile), template, lgFileResolver(lgFiles));
diff --git a/Composer/packages/client/src/recoilModel/parsers/workers/luParser.worker.ts b/Composer/packages/client/src/recoilModel/parsers/workers/luParser.worker.ts
index c304e994f9..97872fadf3 100644
--- a/Composer/packages/client/src/recoilModel/parsers/workers/luParser.worker.ts
+++ b/Composer/packages/client/src/recoilModel/parsers/workers/luParser.worker.ts
@@ -11,6 +11,7 @@ import {
LuUpdateIntentPayload,
LuAddIntentsPayload,
LuAddIntentPayload,
+ LuParseAllPayload,
} from '../types';
const ctx: Worker = self as any;
@@ -50,7 +51,20 @@ interface RemoveIntentsMessage {
payload: LuRemoveIntentsPayload;
}
-type LuMessageEvent = ParseMessage | AddMessage | AddsMessage | UpdateMessage | RemoveMessage | RemoveIntentsMessage;
+type ParseAllMessage = {
+ id: string;
+ type: LuActionType.ParseAll;
+ payload: LuParseAllPayload;
+};
+
+type LuMessageEvent =
+ | ParseMessage
+ | AddMessage
+ | AddsMessage
+ | UpdateMessage
+ | RemoveMessage
+ | RemoveIntentsMessage
+ | ParseAllMessage;
export const handleMessage = (msg: LuMessageEvent) => {
let result: any = null;
@@ -61,6 +75,12 @@ export const handleMessage = (msg: LuMessageEvent) => {
break;
}
+ case LuActionType.ParseAll: {
+ const { luResources, luFeatures } = msg.payload;
+ result = luResources.map(({ id, content }) => luUtil.parse(id, content, luFeatures));
+ break;
+ }
+
case LuActionType.AddIntent: {
const { luFile, intent, luFeatures } = msg.payload;
result = luUtil.addIntent(luFile, intent, luFeatures);
diff --git a/Composer/packages/client/src/recoilModel/parsers/workers/qnaParser.worker.ts b/Composer/packages/client/src/recoilModel/parsers/workers/qnaParser.worker.ts
index cc463d5f9d..48367856e8 100644
--- a/Composer/packages/client/src/recoilModel/parsers/workers/qnaParser.worker.ts
+++ b/Composer/packages/client/src/recoilModel/parsers/workers/qnaParser.worker.ts
@@ -2,34 +2,47 @@
// Licensed under the MIT License.
import * as qnaUtil from '@bfc/indexers/lib/utils/qnaUtil';
-import { QnAActionType } from './../types';
+import { QnAActionType, QnAParseAllPayload, QnAParsePayload } from './../types';
const ctx: Worker = self as any;
-ctx.onmessage = function (msg) {
- const { id: msgId, type, payload } = msg.data;
- const { content, id, file, indexId } = payload;
+type ParseMessage = {
+ id: string;
+ type: QnAActionType.Parse;
+ payload: QnAParsePayload;
+};
+
+type ParseAllMessage = {
+ id: string;
+ type: QnAActionType.ParseAll;
+ payload: QnAParseAllPayload;
+};
+
+type QnaMessageEvent = ParseMessage | ParseAllMessage;
+
+export const handleMessage = (msg: QnaMessageEvent) => {
let result: any = null;
- try {
- switch (type) {
- case QnAActionType.Parse: {
- result = qnaUtil.parse(id, content);
- break;
- }
- case QnAActionType.AddSection: {
- result = qnaUtil.addSection(file.content, content);
- break;
- }
- case QnAActionType.UpdateSection: {
- result = qnaUtil.updateSection(indexId, file.content, content);
- break;
- }
- case QnAActionType.RemoveSection: {
- result = qnaUtil.removeSection(indexId, file.content);
- break;
- }
+ switch (msg.type) {
+ case QnAActionType.Parse: {
+ const { id, content } = msg.payload;
+ result = qnaUtil.parse(id, content);
+ break;
}
- ctx.postMessage({ id: msgId, payload: result });
+
+ case QnAActionType.ParseAll: {
+ const { qnaResources } = msg.payload;
+ result = qnaResources.map(({ id, content }) => qnaUtil.parse(id, content));
+ break;
+ }
+ }
+ return result;
+};
+
+ctx.onmessage = function (event) {
+ const msg = event.data as QnaMessageEvent;
+ try {
+ const payload = handleMessage(msg);
+ ctx.postMessage({ id: msg.id, payload });
} catch (error) {
- ctx.postMessage({ id: msgId, error: error.message });
+ ctx.postMessage({ id: msg.id, error: error.message });
}
};
diff --git a/Composer/packages/client/src/recoilModel/selectors/diagnosticsPageSelector.ts b/Composer/packages/client/src/recoilModel/selectors/diagnosticsPageSelector.ts
index b3ee13ac74..9d55b64ddc 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, dialogIdsState, qnaFilesState } from '../atoms/botState';
+import { botDisplayNameState, dialogIdsState } from '../atoms/botState';
import {
DialogDiagnostic,
LgDiagnostic,
@@ -25,7 +25,6 @@ import {
botProjectIdsState,
dialogSchemasState,
jsonSchemaFilesState,
- luFilesState,
projectMetaDataState,
settingsState,
skillManifestsState,
@@ -36,6 +35,8 @@ import { formDialogSchemasSelectorFamily, rootBotProjectIdSelector } from './pro
import { recognizersSelectorFamily } from './recognizers';
import { dialogDiagnosticsSelectorFamily, dialogsWithLuProviderSelectorFamily } from './validatedDialogs';
import { lgFilesSelectorFamily } from './lg';
+import { luFilesSelectorFamily } from './lu';
+import { qnaFilesSelectorFamily } from './qna';
export const botAssetsSelectFamily = selectorFamily({
key: 'botAssetsSelectFamily',
@@ -44,12 +45,12 @@ export const botAssetsSelectFamily = selectorFamily({
if (!projectsMetaData || projectsMetaData.isRemote) return null;
const dialogs = get(dialogsWithLuProviderSelectorFamily(projectId));
- const luFiles = get(luFilesState(projectId));
+ const luFiles = get(luFilesSelectorFamily(projectId));
const lgFiles = get(lgFilesSelectorFamily(projectId));
const setting = get(settingsState(projectId));
const skillManifests = get(skillManifestsState(projectId));
const dialogSchemas = get(dialogSchemasState(projectId));
- const qnaFiles = get(qnaFilesState(projectId));
+ const qnaFiles = get(qnaFilesSelectorFamily(projectId));
const formDialogSchemas = get(formDialogSchemasSelectorFamily(projectId));
const botProjectFile = get(botProjectFileState(projectId));
const jsonSchemaFiles = get(jsonSchemaFilesState(projectId));
diff --git a/Composer/packages/client/src/recoilModel/selectors/dialogImports.ts b/Composer/packages/client/src/recoilModel/selectors/dialogImports.ts
index f0cc3b176e..a487d90f05 100644
--- a/Composer/packages/client/src/recoilModel/selectors/dialogImports.ts
+++ b/Composer/packages/client/src/recoilModel/selectors/dialogImports.ts
@@ -6,9 +6,10 @@ import uniqBy from 'lodash/uniqBy';
import { selectorFamily } from 'recoil';
import { getBaseName } from '../../utils/fileUtil';
-import { localeState, luFilesState } from '../atoms';
+import { localeState } from '../atoms';
import { lgFilesSelectorFamily } from './lg';
+import { luFilesSelectorFamily } from './lu';
// Finds all the file imports starting from a given dialog file.
export const getLanguageFileImports = (
@@ -75,7 +76,7 @@ export const luImportsSelectorFamily = selectorFamily
- get(luFilesState(projectId)).find((f) => f.id === fileId || f.id === `${fileId}.${locale}`) as LuFile;
+ get(luFilesSelectorFamily(projectId)).find((f) => f.id === fileId || f.id === `${fileId}.${locale}`) as LuFile;
return getLanguageFileImports(dialogId, getFile);
},
diff --git a/Composer/packages/client/src/recoilModel/selectors/index.ts b/Composer/packages/client/src/recoilModel/selectors/index.ts
index dbf78c2b31..25164ad6fe 100644
--- a/Composer/packages/client/src/recoilModel/selectors/index.ts
+++ b/Composer/packages/client/src/recoilModel/selectors/index.ts
@@ -11,3 +11,5 @@ export * from './skills';
export * from './localRuntimeBuilder';
export * from './diagnosticsPageSelector';
export * from './lg';
+export * from './lu';
+export * from './qna';
diff --git a/Composer/packages/client/src/recoilModel/selectors/lg.ts b/Composer/packages/client/src/recoilModel/selectors/lg.ts
index 45fb854e85..c4a45d7694 100644
--- a/Composer/packages/client/src/recoilModel/selectors/lg.ts
+++ b/Composer/packages/client/src/recoilModel/selectors/lg.ts
@@ -19,7 +19,6 @@ export const lgFilesSelectorFamily = selectorFamily({
},
set: (projectId: string) => ({ set }, newLgFiles) => {
const newLgFileArray = newLgFiles as LgFile[];
-
set(
lgFileIdsState(projectId),
newLgFileArray.map((lgFile) => lgFile.id)
diff --git a/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts b/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts
index bcc270fcb1..31f0d36666 100644
--- a/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts
+++ b/Composer/packages/client/src/recoilModel/selectors/localRuntimeBuilder.ts
@@ -7,21 +7,15 @@ import { checkForPVASchema } from '@bfc/shared';
import { BotStatus } from '../../constants';
import { isAbsHosted } from '../../utils/envUtil';
-import {
- botDisplayNameState,
- botStatusState,
- dispatcherState,
- luFilesState,
- qnaFilesState,
- schemasState,
- settingsState,
-} from '../atoms';
+import { botDisplayNameState, botStatusState, dispatcherState, schemasState, settingsState } from '../atoms';
import { Dispatcher } from '../dispatchers';
import { isBuildConfigComplete as isBuildConfigurationComplete, needsBuild } from '../../utils/buildUtil';
import { getSensitiveProperties } from '../dispatchers/utils/project';
import { dialogsSelectorFamily } from './dialogs';
import { localBotsWithoutErrorsSelector, rootBotProjectIdSelector } from './project';
+import { luFilesSelectorFamily } from './lu';
+import { qnaFilesSelectorFamily } from './qna';
export const trackBotStatusesSelector = selectorFamily({
key: 'trackBotStatusesSelector',
@@ -54,8 +48,8 @@ export const buildEssentialsSelector = selectorFamily({
qna: settings.qna,
};
const dialogs = get(dialogsSelectorFamily(projectId));
- const luFiles = get(luFilesState(projectId));
- const qnaFiles = get(qnaFilesState(projectId));
+ const luFiles = get(luFilesSelectorFamily(projectId));
+ const qnaFiles = get(qnaFilesSelectorFamily(projectId));
const buildRequired = get(botBuildRequiredSelector(projectId));
const status = get(botStatusState(projectId));
diff --git a/Composer/packages/client/src/recoilModel/selectors/lu.ts b/Composer/packages/client/src/recoilModel/selectors/lu.ts
new file mode 100644
index 0000000000..555118b23e
--- /dev/null
+++ b/Composer/packages/client/src/recoilModel/selectors/lu.ts
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { LuFile } from '@bfc/shared';
+import { selectorFamily, DefaultValue } from 'recoil';
+
+import { luFileIdsState, luFileState } from '../atoms';
+
+export const luFilesSelectorFamily = selectorFamily({
+ key: 'luFiles',
+ get: (projectId: string) => ({ get }) => {
+ const luFileIds = get(luFileIdsState(projectId));
+
+ return luFileIds.map((luFileId) => {
+ return get(luFileState({ projectId, luFileId }));
+ });
+ },
+ set: (projectId: string) => ({ set }, newLuFiles: LuFile[] | DefaultValue) => {
+ if (newLuFiles instanceof DefaultValue) return;
+ set(
+ luFileIdsState(projectId),
+ newLuFiles.map((luFile) => luFile.id)
+ );
+ newLuFiles.forEach((luFile) => set(luFileState({ projectId, luFileId: luFile.id }), luFile));
+ },
+});
diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts
index e049c5d721..0de67fe70f 100644
--- a/Composer/packages/client/src/recoilModel/selectors/project.ts
+++ b/Composer/packages/client/src/recoilModel/selectors/project.ts
@@ -16,8 +16,6 @@ import {
botProjectIdsState,
formDialogSchemaIdsState,
formDialogSchemaState,
- luFilesState,
- qnaFilesState,
skillManifestsState,
dialogSchemasState,
jsonSchemaFilesState,
@@ -37,6 +35,8 @@ import {
buildEssentialsSelector,
lgImportsSelectorFamily,
luImportsSelectorFamily,
+ luFilesSelectorFamily,
+ qnaFilesSelectorFamily,
dialogsWithLuProviderSelectorFamily,
} from '../selectors';
@@ -132,9 +132,9 @@ export const botProjectSpaceSelector = selector({
const result = botProjects.map((projectId: string) => {
const { isRemote, isRootBot } = get(projectMetaDataState(projectId));
const dialogs = get(dialogsWithLuProviderSelectorFamily(projectId));
- const luFiles = get(luFilesState(projectId));
+ const luFiles = get(luFilesSelectorFamily(projectId));
const lgFiles = get(lgFilesSelectorFamily(projectId));
- const qnaFiles = get(qnaFilesState(projectId));
+ const qnaFiles = get(qnaFilesSelectorFamily(projectId));
const formDialogSchemas = get(formDialogSchemasSelectorFamily(projectId));
const botProjectFile = get(botProjectFileState(projectId));
const metaData = get(projectMetaDataState(projectId));
@@ -221,12 +221,12 @@ export const perProjectDiagnosticsSelectorFamily = selectorFamily({
const rootSetting = get(settingsState(rootBotId));
const dialogs = get(dialogsWithLuProviderSelectorFamily(projectId));
const formDialogSchemas = get(formDialogSchemasSelectorFamily(projectId));
- const luFiles = get(luFilesState(projectId));
+ const luFiles = get(luFilesSelectorFamily(projectId));
const lgFiles = get(lgFilesSelectorFamily(projectId));
const setting = get(settingsState(projectId));
const skillManifests = get(skillManifestsState(projectId));
const dialogSchemas = get(dialogSchemasState(projectId));
- const qnaFiles = get(qnaFilesState(projectId));
+ const qnaFiles = get(qnaFilesSelectorFamily(projectId));
const botProjectFile = get(botProjectFileState(projectId));
const jsonSchemaFiles = get(jsonSchemaFilesState(projectId));
const botAssets: BotAssets = {
diff --git a/Composer/packages/client/src/recoilModel/selectors/qna.ts b/Composer/packages/client/src/recoilModel/selectors/qna.ts
new file mode 100644
index 0000000000..6b2be2148a
--- /dev/null
+++ b/Composer/packages/client/src/recoilModel/selectors/qna.ts
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { QnAFile } from '@bfc/shared';
+import { DefaultValue, selectorFamily } from 'recoil';
+
+import { qnaFileIdsState, qnaFileState } from '../atoms';
+
+export const qnaFilesSelectorFamily = selectorFamily({
+ key: 'qnaFiles',
+ get: (projectId: string) => ({ get }) => {
+ const qnaFileIds = get(qnaFileIdsState(projectId));
+
+ return qnaFileIds.map((qnaFileId) => {
+ return get(qnaFileState({ projectId, qnaFileId }));
+ });
+ },
+ set: (projectId: string) => ({ set }, newQnaFiles: QnAFile[] | DefaultValue) => {
+ if (newQnaFiles instanceof DefaultValue) return;
+
+ set(
+ qnaFileIdsState(projectId),
+ newQnaFiles.map((qnaFile) => qnaFile.id)
+ );
+ newQnaFiles.forEach((qnaFile) => set(qnaFileState({ projectId, qnaFileId: qnaFile.id }), qnaFile));
+ },
+});
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 281f3a793f..abac532093 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 {
- luFilesState,
projectMetaDataState,
currentProjectIdState,
botProjectIdsState,
@@ -18,7 +17,7 @@ import {
canRedoState,
dispatcherState,
} from '../../atoms';
-import { dialogsSelectorFamily, lgFilesSelectorFamily } from '../../selectors';
+import { dialogsSelectorFamily, lgFilesSelectorFamily, luFilesSelectorFamily } 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('', () => {
{ recoilState: botProjectIdsState, initialValue: [projectId] },
{ recoilState: dialogsSelectorFamily(projectId), initialValue: [{ id: '1', content: '' }] },
{ recoilState: lgFilesSelectorFamily(projectId), initialValue: [{ id: '1.lg' }, { id: '2' }] },
- { recoilState: luFilesState(projectId), initialValue: [{ id: '1.lu' }, { id: '2' }] },
+ { recoilState: luFilesSelectorFamily(projectId), initialValue: [{ id: '1.lu' }, { id: '2' }] },
{ recoilState: currentProjectIdState, initialValue: projectId },
{ recoilState: undoHistoryState(projectId), initialValue: new UndoHistory(projectId) },
{ recoilState: canUndoState(projectId), initialValue: false },
diff --git a/Composer/packages/client/src/recoilModel/undo/trackedAtoms.ts b/Composer/packages/client/src/recoilModel/undo/trackedAtoms.ts
index 566f6c493f..6cc6f5355c 100644
--- a/Composer/packages/client/src/recoilModel/undo/trackedAtoms.ts
+++ b/Composer/packages/client/src/recoilModel/undo/trackedAtoms.ts
@@ -3,12 +3,11 @@
import { RecoilState } from 'recoil';
-import { luFilesState } from '../atoms';
-import { dialogsSelectorFamily } from '../selectors';
+import { dialogsSelectorFamily, luFilesSelectorFamily } from '../selectors';
import { lgFilesSelectorFamily } from '../selectors/lg';
export type AtomAssetsMap = Map, any>;
export const trackedAtoms = (projectId: string): RecoilState[] => {
- return [dialogsSelectorFamily(projectId), luFilesState(projectId), lgFilesSelectorFamily(projectId)];
+ return [dialogsSelectorFamily(projectId), luFilesSelectorFamily(projectId), lgFilesSelectorFamily(projectId)];
};
diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts
index 483619f7be..71a4b693c1 100644
--- a/Composer/packages/client/src/shell/useShell.ts
+++ b/Composer/packages/client/src/shell/useShell.ts
@@ -29,11 +29,11 @@ import {
schemasState,
focusPathState,
localeState,
- qnaFilesState,
+ qnaFilesSelectorFamily,
designPageLocationState,
botDisplayNameState,
dialogSchemasState,
- luFilesState,
+ luFilesSelectorFamily,
rateInfoState,
rootBotProjectIdSelector,
featureFlagsState,
@@ -83,11 +83,11 @@ export function useShell(source: EventSource, projectId: string): Shell {
const focusPath = useRecoilValue(focusPathState(projectId));
const skills = useRecoilValue(skillsStateSelector);
const locale = useRecoilValue(localeState(projectId));
- const qnaFiles = useRecoilValue(qnaFilesState(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesSelectorFamily(projectId));
const undoFunction = useRecoilValue(undoFunctionState(projectId));
const designPageLocation = useRecoilValue(designPageLocationState(projectId));
const { undo, redo, commitChanges } = undoFunction;
- const luFiles = useRecoilValue(luFilesState(projectId));
+ const luFiles = useRecoilValue(luFilesSelectorFamily(projectId));
const lgFiles = useRecoilValue(lgFilesSelectorFamily(projectId));
const dialogSchemas = useRecoilValue(dialogSchemasState(projectId));
const botName = useRecoilValue(botDisplayNameState(projectId));
diff --git a/Composer/packages/lib/code-editor/src/lu/__test__/InsertEntityButton.test.tsx b/Composer/packages/lib/code-editor/src/lu/__test__/InsertEntityButton.test.tsx
index 00c9af1111..8e12cc9b79 100644
--- a/Composer/packages/lib/code-editor/src/lu/__test__/InsertEntityButton.test.tsx
+++ b/Composer/packages/lib/code-editor/src/lu/__test__/InsertEntityButton.test.tsx
@@ -39,6 +39,7 @@ const luFile: LuFile = {
content: '',
imports: [],
resource: { Content: '', Errors: [], Sections: [] },
+ isContentUnparsed: false,
};
jest.useFakeTimers();
diff --git a/Composer/packages/lib/indexers/src/index.ts b/Composer/packages/lib/indexers/src/index.ts
index 029b870241..90b4ff43d4 100644
--- a/Composer/packages/lib/indexers/src/index.ts
+++ b/Composer/packages/lib/indexers/src/index.ts
@@ -1,21 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-import { DialogSetting, FileInfo, lgImportResolverGenerator } from '@bfc/shared';
+import { FileInfo } from '@bfc/shared';
import { recognizerIndexer } from './recognizerIndexer';
import { dialogIndexer } from './dialogIndexer';
import { dialogSchemaIndexer } from './dialogSchemaIndexer';
import { jsonSchemaFileIndexer } from './jsonSchemaFileIndexer';
-import { lgIndexer } from './lgIndexer';
-import { luIndexer } from './luIndexer';
-import { qnaIndexer } from './qnaIndexer';
import { skillManifestIndexer } from './skillManifestIndexer';
import { botProjectSpaceIndexer } from './botProjectSpaceIndexer';
import { FileExtensions } from './utils/fileExtensions';
import { getExtension, getBaseName } from './utils/help';
import { formDialogSchemaIndexer } from './formDialogSchemaIndexer';
import { crossTrainConfigIndexer } from './crossTrainConfigIndexer';
-import { BotIndexer } from './botIndexer';
class Indexer {
private classifyFile(files: FileInfo[]) {
@@ -69,28 +65,23 @@ class Indexer {
);
};
- private getLgImportResolver = (files: FileInfo[], locale: string) => {
- const lgFiles = files.map(({ name, content }) => {
- return {
- id: getBaseName(name, '.lg'),
- content,
- };
- });
-
- return lgImportResolverGenerator(lgFiles, '.lg', locale);
+ private getResources = (files: FileInfo[], extension: string) => {
+ return files.map(({ name, content }) => ({
+ id: getBaseName(name, extension),
+ content,
+ }));
};
- public index(files: FileInfo[], botName: string, locale: string, settings: DialogSetting) {
+ public index(files: FileInfo[], botName: string) {
const result = this.classifyFile(files);
- const luFeatures = settings.luFeatures;
const { dialogs, recognizers } = this.separateDialogsAndRecognizers(result[FileExtensions.Dialog]);
const { skillManifestFiles, crossTrainConfigs } = this.separateConfigAndManifests(result[FileExtensions.Manifest]);
const assets = {
dialogs: dialogIndexer.index(dialogs, botName),
dialogSchemas: dialogSchemaIndexer.index(result[FileExtensions.DialogSchema]),
- lgFiles: lgIndexer.index(result[FileExtensions.lg], this.getLgImportResolver(result[FileExtensions.lg], locale)),
- luFiles: luIndexer.index(result[FileExtensions.Lu], luFeatures),
- qnaFiles: qnaIndexer.index(result[FileExtensions.QnA]),
+ lgResources: this.getResources(result[FileExtensions.lg], FileExtensions.lg),
+ luResources: this.getResources(result[FileExtensions.Lu], FileExtensions.Lu),
+ qnaResources: this.getResources(result[FileExtensions.QnA], FileExtensions.QnA),
skillManifests: skillManifestIndexer.index(skillManifestFiles),
botProjectSpaceFiles: botProjectSpaceIndexer.index(result[FileExtensions.BotProjectSpace]),
jsonSchemaFiles: jsonSchemaFileIndexer.index(result[FileExtensions.Json]),
@@ -98,9 +89,7 @@ class Indexer {
recognizers: recognizerIndexer.index(recognizers),
crossTrainConfig: crossTrainConfigIndexer.index(crossTrainConfigs),
};
- const botProjectFile = assets.botProjectSpaceFiles[0];
- const diagnostics = BotIndexer.validate({ ...assets, setting: settings, botProjectFile });
- return { ...assets, diagnostics };
+ return { ...assets };
}
}
diff --git a/Composer/packages/lib/indexers/src/utils/lgUtil.ts b/Composer/packages/lib/indexers/src/utils/lgUtil.ts
index d120e8e0f3..a20e12f5aa 100644
--- a/Composer/packages/lib/indexers/src/utils/lgUtil.ts
+++ b/Composer/packages/lib/indexers/src/utils/lgUtil.ts
@@ -84,7 +84,17 @@ export function convertTemplatesToLgFile(id = '', content: string, parseResult:
};
});
- return { id, content, templates, allTemplates, diagnostics, imports, options: parseResult.options, parseResult };
+ return {
+ id,
+ content,
+ templates,
+ allTemplates,
+ diagnostics,
+ imports,
+ options: parseResult.options,
+ parseResult,
+ isContentUnparsed: false,
+ };
}
export function increaseNameUtilNotExist(templates: LgTemplate[], name: string): string {
diff --git a/Composer/packages/lib/indexers/src/utils/luUtil.ts b/Composer/packages/lib/indexers/src/utils/luUtil.ts
index 1cd6539b0e..d9db779482 100644
--- a/Composer/packages/lib/indexers/src/utils/luUtil.ts
+++ b/Composer/packages/lib/indexers/src/utils/luUtil.ts
@@ -129,6 +129,7 @@ export function convertLuParseResultToLuFile(
diagnostics,
imports,
resource: { Sections, Errors, Content },
+ isContentUnparsed: false,
};
}
diff --git a/Composer/packages/lib/indexers/src/utils/qnaUtil.ts b/Composer/packages/lib/indexers/src/utils/qnaUtil.ts
index 77043decec..47043bdf7b 100644
--- a/Composer/packages/lib/indexers/src/utils/qnaUtil.ts
+++ b/Composer/packages/lib/indexers/src/utils/qnaUtil.ts
@@ -148,6 +148,7 @@ export function convertQnAParseResultToQnAFile(id = '', resource: LuParseResourc
resource: { Sections, Errors, Content },
imports,
options,
+ isContentUnparsed: false,
};
}
diff --git a/Composer/packages/tools/language-servers/language-generation/__tests__/LGUtil.test.ts b/Composer/packages/tools/language-servers/language-generation/__tests__/LGUtil.test.ts
index b33ff26483..164d6d62fc 100644
--- a/Composer/packages/tools/language-servers/language-generation/__tests__/LGUtil.test.ts
+++ b/Composer/packages/tools/language-servers/language-generation/__tests__/LGUtil.test.ts
@@ -13,7 +13,6 @@ import {
extractLUISContent,
getSuggestionEntities,
suggestionAllEntityTypes,
- getLineByIndex,
generateDiagnostic,
convertDiagnostics,
convertSeverity,
@@ -90,11 +89,6 @@ describe('LG LSP Server Function Unit Tests', () => {
expect(result).toEqual(['name', 'zipcode']);
});
- it('Test getLineByIndex function', () => {
- const result = getLineByIndex(textDoc2, 2);
- expect(result).toEqual('line2');
- });
-
it('Test generateDiagnostic function', () => {
const result = generateDiagnostic('No Template Found', DiagnosticSeverity.Error, textDoc2);
expect(result.message).toEqual('No Template Found');
diff --git a/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts b/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts
index e4c79c6523..597ae16e63 100644
--- a/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts
+++ b/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts
@@ -43,7 +43,7 @@ import {
cardTypes,
cardPropDict,
cardPropPossibleValueType,
- getLineByIndex,
+ createFoldingRanges,
} from './utils';
// define init methods call from client
@@ -183,54 +183,12 @@ export class LGServer {
protected foldingRangeHandler(params: FoldingRangeParams): FoldingRange[] {
const document = this.documents.get(params.textDocument.uri);
- const items: FoldingRange[] = [];
if (!document) {
- return items;
+ return [];
}
- const lineCount = document.lineCount;
- let i = 0;
- while (i < lineCount) {
- const currLine = getLineByIndex(document, i);
- if (currLine?.startsWith('>>')) {
- for (let j = i + 1; j < lineCount; j++) {
- if (getLineByIndex(document, j)?.startsWith('>>')) {
- items.push(FoldingRange.create(i, j - 1));
- i = j - 1;
- break;
- }
-
- if (j === lineCount - 1) {
- items.push(FoldingRange.create(i, j));
- i = j;
- }
- }
- }
-
- i = i + 1;
- }
-
- for (let i = 0; i < lineCount; i++) {
- const currLine = getLineByIndex(document, i);
- if (currLine?.startsWith('#')) {
- let j = i + 1;
- for (j = i + 1; j < lineCount; j++) {
- const secLine = getLineByIndex(document, j);
- if (secLine?.startsWith('>>') || secLine?.startsWith('#')) {
- items.push(FoldingRange.create(i, j - 1));
- i = j - 1;
- break;
- }
- }
-
- if (i !== j - 1) {
- items.push(FoldingRange.create(i, j - 1));
- i == j - 2;
- }
- }
- }
-
- return items;
+ const lines = document.getText().split(/\r?\n/g);
+ return [...createFoldingRanges(lines, '>>'), ...createFoldingRanges(lines, '#')];
}
protected updateObject(propertyList: string[], varaibles: Record): void {
diff --git a/Composer/packages/tools/language-servers/language-generation/src/utils.ts b/Composer/packages/tools/language-servers/language-generation/src/utils.ts
index 73b7f95f6e..989c9969fc 100644
--- a/Composer/packages/tools/language-servers/language-generation/src/utils.ts
+++ b/Composer/packages/tools/language-servers/language-generation/src/utils.ts
@@ -7,6 +7,7 @@ import { DiagnosticSeverity as LGDiagnosticSeverity } from 'botbuilder-lg';
import { Diagnostic as BFDiagnostic, LgFile } from '@bfc/shared';
import { parser } from '@microsoft/bf-lu/lib/parser';
import { offsetRange } from '@bfc/indexers';
+import { FoldingRange } from 'vscode-languageserver';
import { LGResource, Templates } from 'botbuilder-lg';
import { Expression } from 'adaptive-expressions';
import uniq from 'lodash/uniq';
@@ -221,11 +222,31 @@ export const suggestionAllEntityTypes = [
'composites',
];
-export function getLineByIndex(document: TextDocument, line: number) {
- const lineCount = document.lineCount;
- if (line >= lineCount || line < 0) return null;
+export function createFoldingRanges(lines: string[], prefix: string) {
+ const items: FoldingRange[] = [];
- return document.getText().split(/\r?\n/g)[line];
+ if (!lines || lines.length === 0) {
+ return items;
+ }
+
+ const lineCount = lines.length;
+ let startIdx = -1;
+
+ for (let i = 0; i < lineCount; i++) {
+ if (lines[i].trim().startsWith(prefix)) {
+ if (startIdx !== -1) {
+ items.push(FoldingRange.create(startIdx, i - 1));
+ }
+
+ startIdx = i;
+ }
+ }
+
+ if (startIdx !== -1) {
+ items.push(FoldingRange.create(startIdx, lineCount - 1));
+ }
+
+ return items;
}
function findExpr(pst: any, result: string[]): void {
diff --git a/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts b/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts
index c4e1ec4556..3c88ed234b 100644
--- a/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts
+++ b/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts
@@ -19,33 +19,23 @@ import {
DocumentOnTypeFormattingParams,
FoldingRange,
} from 'vscode-languageserver-protocol';
-import { updateIntent, isValid, checkSection, PlaceHolderSectionName } from '@bfc/indexers/lib/utils/luUtil';
-import { luIndexer } from '@bfc/indexers';
-import { parser } from '@microsoft/bf-lu/lib/parser';
+import { isValid, PlaceHolderSectionName } from '@bfc/indexers/lib/utils/luUtil';
+import { LuParser } from './luParser';
import { EntityTypesObj, LineState } from './entityEnum';
import * as util from './matchingPattern';
-import {
- LUImportResolverDelegate,
- LUOption,
- LUDocument,
- generateDiagnostic,
- convertDiagnostics,
- getLineByIndex,
-} from './utils';
+import { LUOption, LUDocument, generateDiagnostic, convertDiagnostics, createFoldingRanges } from './utils';
// define init methods call from client
const LABELEXPERIENCEREQUEST = 'labelingExperienceRequest';
const InitializeDocumentsMethodName = 'initializeDocuments';
-const { parse } = luIndexer;
-const { parseFile } = parser;
-
export class LUServer {
protected workspaceRoot: URI | undefined;
protected readonly documents = new TextDocuments();
protected readonly pendingValidationRequests = new Map();
protected LUDocuments: LUDocument[] = [];
+ private luParser = new LuParser();
constructor(
protected readonly connection: IConnection,
@@ -113,50 +103,16 @@ export class LUServer {
protected async foldingRangeHandler(params: FoldingRangeParams): Promise {
const document = this.documents.get(params.textDocument.uri);
- const items: FoldingRange[] = [];
if (!document) {
- return items;
- }
-
- const lineCount = document.lineCount;
- let i = 0;
- while (i < lineCount) {
- const currLine = getLineByIndex(document, i);
- if (currLine?.startsWith('>>')) {
- for (let j = i + 1; j < lineCount; j++) {
- if (getLineByIndex(document, j)?.startsWith('>>')) {
- items.push(FoldingRange.create(i, j - 1));
- i = j - 1;
- break;
- }
-
- if (j === lineCount - 1) {
- items.push(FoldingRange.create(i, j));
- i = j;
- }
- }
- }
-
- i = i + 1;
+ return [];
}
- const luResource = parse(document.getText(), undefined, {}).resource;
- const sections = luResource.Sections;
- for (const section in luResource.Sections) {
- const start = sections[section].Range.Start.Line - 1;
- let end = sections[section].Range.End.Line - 1;
- const sectionLastLine = getLineByIndex(document, end);
- if (sectionLastLine?.startsWith('>>')) {
- end = end - 1;
- }
-
- items.push(FoldingRange.create(start, end));
- }
+ const lines = document.getText().split(/\r?\n/g);
- return items;
+ return [...createFoldingRanges(lines, '>>'), ...createFoldingRanges(lines, '#')];
}
- protected validateLuOption(document: TextDocument, luOption?: LUOption) {
+ protected async validateLuOption(document: TextDocument, luOption?: LUOption) {
if (!luOption) return;
const diagnostics: string[] = [];
@@ -165,7 +121,7 @@ export class LUServer {
diagnostics.push('[Error luOption] importResolver is required but not exist.');
} else {
const { fileId, sectionId } = luOption;
- const luFile = this.getLUDocument(document)?.index();
+ const luFile = await this.getLUDocument(document)?.index();
if (!luFile) {
diagnostics.push(`[Error luOption] File ${fileId}.lu do not exist`);
} else if (sectionId && sectionId !== PlaceHolderSectionName) {
@@ -181,9 +137,9 @@ export class LUServer {
);
}
- protected getImportResolver(document: TextDocument) {
+ protected async getImportResolver(document: TextDocument) {
const editorContent = document.getText();
- const internalImportResolver = () => {
+ const internalImportResolver = async () => {
return {
id: document.uri,
content: editorContent,
@@ -193,14 +149,14 @@ export class LUServer {
if (this.importResolver && fileId && projectId) {
const resolver = this.importResolver;
- return (source: string, id: string) => {
+ return async (source: string, id: string) => {
const plainLuFile = resolver(source, id, projectId);
if (!plainLuFile) {
this.sendDiagnostics(document, [
generateDiagnostic(`lu file: ${fileId}.lu not exist on server`, DiagnosticSeverity.Error, document),
]);
}
- const luFile = luIndexer.parse(plainLuFile.content, plainLuFile.id, luFeatures);
+ const luFile = await this.luParser.parse(plainLuFile.content, plainLuFile.id, luFeatures);
let { content } = luFile;
/**
* source is . means use as file resolver, not import resolver
@@ -208,7 +164,9 @@ export class LUServer {
* so here build the full content from server file content and editor content
*/
if (source === '.' && sectionId) {
- content = updateIntent(luFile, sectionId, { Name: sectionId, Body: editorContent }, luFeatures).content;
+ content = (
+ await this.luParser.updateIntent(luFile, sectionId, { Name: sectionId, Body: editorContent }, luFeatures)
+ ).content;
}
return { id, content };
};
@@ -217,23 +175,23 @@ export class LUServer {
return internalImportResolver;
}
- protected addLUDocument(document: TextDocument, luOption?: LUOption) {
+ protected async addLUDocument(document: TextDocument, luOption?: LUOption) {
const { uri } = document;
const { fileId, sectionId, projectId, luFeatures = {} } = luOption || {};
- const index = () => {
- const importResolver: LUImportResolverDelegate = this.getImportResolver(document);
+ const index = async () => {
+ const importResolver = await this.getImportResolver(document);
let content: string = document.getText();
// if inline mode, composite local with server resolved file.
if (this.importResolver && fileId && sectionId) {
try {
- content = importResolver('.', `${fileId}.lu`).content;
+ content = (await importResolver('.', `${fileId}.lu`)).content;
} catch (error) {
// ignore if file not exist
}
}
const id = fileId || uri;
- const { intents: sections, diagnostics } = parse(content, id, luFeatures);
+ const { intents: sections, diagnostics } = await this.luParser.parse(content, id, luFeatures);
return { sections, diagnostics, content };
};
@@ -284,7 +242,7 @@ export class LUServer {
const edits: TextEdit[] = [];
const curLineNumber = params.position.line;
const luDoc = this.getLUDocument(document);
- const text = luDoc?.index().content || document.getText();
+ const text = (await (luDoc?.index()).content) || document.getText();
const lines = text.split('\n');
const position = params.position;
const textBeforeCurLine = lines.slice(0, curLineNumber).join('\n');
@@ -417,7 +375,7 @@ export class LUServer {
const log = false;
const locale = 'en-us';
try {
- parsedContent = await parseFile(text, log, locale);
+ parsedContent = await this.luParser.parseFile(text, log, locale);
} catch (e) {
// nothing to do in catch block
}
@@ -439,7 +397,7 @@ export class LUServer {
const range = Range.create(position.line, 0, position.line, position.character);
const curLineContent = document.getText(range);
const luDoc = this.getLUDocument(document);
- const text = luDoc?.index().content || document.getText();
+ const text = (await luDoc?.index()).content || document.getText();
const lines = text.split('\n');
const curLineNumber = params.position.line;
//const textBeforeCurLine = lines.slice(0, curLineNumber).join('\n');
@@ -682,14 +640,14 @@ export class LUServer {
}
}
- protected doValidate(document: TextDocument): void {
+ protected async doValidate(document: TextDocument): Promise {
const text = document.getText();
const luDoc = this.getLUDocument(document);
if (!luDoc) {
return;
}
const { fileId, sectionId } = luDoc;
- const luFile = luDoc.index();
+ const luFile = await luDoc.index();
if (!luFile) {
return;
}
@@ -703,7 +661,7 @@ export class LUServer {
// if inline editor, concat new content for validate
if (fileId && sectionId) {
- const sectionDiags = checkSection(
+ const sectionDiags = await this.luParser.checkSection(
{
Name: sectionId,
Body: text,
diff --git a/Composer/packages/tools/language-servers/language-understanding/src/luParser.ts b/Composer/packages/tools/language-servers/language-understanding/src/luParser.ts
new file mode 100644
index 0000000000..9ff75c09f6
--- /dev/null
+++ b/Composer/packages/tools/language-servers/language-understanding/src/luParser.ts
@@ -0,0 +1,125 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { fork, ChildProcess } from 'child_process';
+import path from 'path';
+
+import { parser } from '@microsoft/bf-lu/lib/parser';
+import { updateIntent, checkSection } from '@bfc/indexers/lib/utils/luUtil';
+import { luIndexer } from '@bfc/indexers';
+import { LuFile, ILUFeaturesConfig, LuIntentSection, Diagnostic } from '@bfc/shared';
+import uniqueId from 'lodash/uniqueId';
+
+export const isTest = process.env?.NODE_ENV === 'test';
+export interface WorkerMsg {
+ id: string;
+ type: 'parse' | 'updateIntent' | 'parseFile' | 'checkSection' | 'getFoldingRanges';
+ error?: any;
+ payload?: any;
+}
+
+export class LuParserWithoutWorker {
+ public async parse(content: string, id = '', config: ILUFeaturesConfig): Promise {
+ return luIndexer.parse(content, id, config);
+ }
+ public async updateIntent(
+ luFile: LuFile,
+ intentName: string,
+ intent: { Name?: string; Body?: string } | null,
+ luFeatures: ILUFeaturesConfig
+ ): Promise {
+ return updateIntent(luFile, intentName, intent, luFeatures);
+ }
+ public async parseFile(text, log, locale): Promise {
+ return await parser.parseFile(text, log, locale);
+ }
+ public async checkSection(intent: LuIntentSection, enableSections = true): Promise {
+ return checkSection(intent, enableSections);
+ }
+}
+
+class LuParserWithWorker {
+ private static _worker: ChildProcess;
+ private resolves = {};
+ private rejects = {};
+
+ constructor() {
+ LuParserWithWorker.worker.on('message', this.handleMsg.bind(this));
+ }
+
+ public async parse(content: string, id = '', config: ILUFeaturesConfig): Promise {
+ const msgId = uniqueId();
+ const msg = { id: msgId, type: 'parse', payload: { id, content, config } };
+ return new Promise((resolve, reject) => {
+ this.resolves[msgId] = resolve;
+ this.rejects[msgId] = reject;
+ LuParserWithWorker.worker.send(msg);
+ });
+ }
+ public async updateIntent(
+ luFile: LuFile,
+ intentName: string,
+ intent: { Name?: string; Body?: string } | null,
+ luFeatures: ILUFeaturesConfig
+ ): Promise {
+ const msgId = uniqueId();
+ const msg = { id: msgId, type: 'updateTemplate', payload: { luFile, intentName, intent, luFeatures } };
+ return new Promise((resolve, reject) => {
+ this.resolves[msgId] = resolve;
+ this.rejects[msgId] = reject;
+ LuParserWithWorker.worker.send(msg);
+ });
+ }
+
+ public async parseFile(text: string, log, locale: string): Promise {
+ const msgId = uniqueId();
+ const msg = { id: msgId, type: 'updateTemplate', payload: { text, log, locale } };
+ return new Promise((resolve, reject) => {
+ this.resolves[msgId] = resolve;
+ this.rejects[msgId] = reject;
+ LuParserWithWorker.worker.send(msg);
+ });
+ }
+
+ public async checkSection(intent: LuIntentSection, enableSections = true): Promise {
+ const msgId = uniqueId();
+ const msg = { id: msgId, type: 'checkSection', payload: { intent, enableSections } };
+ return new Promise((resolve, reject) => {
+ this.resolves[msgId] = resolve;
+ this.rejects[msgId] = reject;
+ LuParserWithWorker.worker.send(msg);
+ });
+ }
+
+ // Handle incoming calculation result
+ public handleMsg(msg: WorkerMsg) {
+ const { id, error, payload } = msg;
+ if (error) {
+ const reject = this.rejects[id];
+ if (reject) reject(error);
+ } else {
+ const resolve = this.resolves[id];
+ if (resolve) resolve(payload);
+ }
+
+ // purge used callbacks
+ delete this.resolves[id];
+ delete this.rejects[id];
+ }
+
+ static get worker() {
+ if (this._worker && !this._worker.killed) {
+ return this._worker;
+ }
+
+ const workerScriptPath = path.join(__dirname, 'luWorker.js');
+ // set exec arguments to empty, avoid fork nodemon `--inspect` error
+ this._worker = fork(workerScriptPath, [], { execArgv: [] });
+ return this._worker;
+ }
+}
+
+// Do not use worker when running test.
+const LuParser = LuParserWithWorker;
+
+export { LuParser };
diff --git a/Composer/packages/tools/language-servers/language-understanding/src/luWorker.ts b/Composer/packages/tools/language-servers/language-understanding/src/luWorker.ts
new file mode 100644
index 0000000000..3c3753a0e1
--- /dev/null
+++ b/Composer/packages/tools/language-servers/language-understanding/src/luWorker.ts
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { luIndexer } from '@bfc/indexers';
+import { updateIntent, checkSection } from '@bfc/indexers/lib/utils/luUtil';
+import { parser } from '@microsoft/bf-lu/lib/parser';
+
+import { WorkerMsg } from './luParser';
+process.on('message', async (msg: WorkerMsg) => {
+ try {
+ switch (msg.type) {
+ case 'parse': {
+ const { id, content, config } = msg.payload;
+ process.send?.({ id: msg.id, payload: luIndexer.parse(content, id, config) });
+ break;
+ }
+
+ case 'updateIntent': {
+ const { luFile, intentName, intent, luFeatures } = msg.payload;
+ process.send?.({ id: msg.id, payload: updateIntent(luFile, intentName, intent, luFeatures) });
+ break;
+ }
+
+ case 'parseFile': {
+ const { text, log, locale } = msg.payload;
+ process.send?.({ id: msg.id, payload: await parser.parseFile(text, log, locale) });
+ break;
+ }
+
+ case 'checkSection': {
+ const { intent, enableSections } = msg.payload;
+ process.send?.({ id: msg.id, payload: checkSection(intent, enableSections) });
+ break;
+ }
+ }
+ } catch (error) {
+ process.send?.({ id: msg.id, error });
+ }
+});
diff --git a/Composer/packages/tools/language-servers/language-understanding/src/utils.ts b/Composer/packages/tools/language-servers/language-understanding/src/utils.ts
index 9c0064b769..d9be289998 100644
--- a/Composer/packages/tools/language-servers/language-understanding/src/utils.ts
+++ b/Composer/packages/tools/language-servers/language-understanding/src/utils.ts
@@ -5,6 +5,7 @@ import { Range, Position, DiagnosticSeverity, Diagnostic } from 'vscode-language
import { TextDocument } from 'vscode-languageserver-textdocument';
import { offsetRange } from '@bfc/indexers';
import { DiagnosticSeverity as BFDiagnosticSeverity, Diagnostic as BFDiagnostic } from '@bfc/shared';
+import { FoldingRange } from 'vscode-languageserver';
export interface LUOption {
projectId: string;
@@ -72,9 +73,29 @@ export function convertDiagnostics(lgDiags: BFDiagnostic[] = [], document: TextD
return diagnostics;
}
-export function getLineByIndex(document: TextDocument, line: number) {
- const lineCount = document.lineCount;
- if (line >= lineCount || line < 0) return null;
+export function createFoldingRanges(lines: string[], prefix: string) {
+ const items: FoldingRange[] = [];
- return document.getText().split(/\r?\n/g)[line];
+ if (!lines?.length) {
+ return items;
+ }
+
+ const lineCount = lines.length;
+ let startIdx = -1;
+
+ for (let i = 0; i < lineCount; i++) {
+ if (lines[i].trim().startsWith(prefix)) {
+ if (startIdx !== -1) {
+ items.push(FoldingRange.create(startIdx, i - 1));
+ }
+
+ startIdx = i;
+ }
+ }
+
+ if (startIdx !== -1) {
+ items.push(FoldingRange.create(startIdx, lineCount - 1));
+ }
+
+ return items;
}
diff --git a/Composer/packages/types/src/indexers.ts b/Composer/packages/types/src/indexers.ts
index 21c106694d..9a4f16948b 100644
--- a/Composer/packages/types/src/indexers.ts
+++ b/Composer/packages/types/src/indexers.ts
@@ -117,6 +117,7 @@ export type LuFile = {
resource: LuParseResource;
imports: { id: string; path: string; description: string }[];
published?: boolean;
+ isContentUnparsed: boolean;
};
export type LuParseResourceSection = {
@@ -155,6 +156,7 @@ export type QnAFile = {
options: { id: string; name: string; value: string }[];
empty: boolean;
resource: LuParseResource;
+ isContentUnparsed: boolean;
};
export type LgTemplate = {
@@ -196,6 +198,7 @@ export type LgFile = {
imports: { id: string; path: string; description: string }[];
options?: string[];
parseResult?: any;
+ isContentUnparsed: boolean;
};
export type Manifest = {