diff --git a/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx b/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx
index cff71f2607..0d4e8641d8 100644
--- a/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx
+++ b/Composer/packages/client/__tests__/components/CreationFlow/index.test.tsx
@@ -76,7 +76,8 @@ describe('', () => {
'',
expect.stringMatching(/(\/|\\)test-folder(\/|\\)Desktop/),
'',
- 'en-US'
+ 'en-US',
+ undefined
);
});
});
diff --git a/Composer/packages/client/__tests__/components/TestController/publish-luis-modal.test.tsx b/Composer/packages/client/__tests__/components/TestController/publish-luis-modal.test.tsx
index fbd5cd945c..3bb6f18748 100644
--- a/Composer/packages/client/__tests__/components/TestController/publish-luis-modal.test.tsx
+++ b/Composer/packages/client/__tests__/components/TestController/publish-luis-modal.test.tsx
@@ -4,10 +4,11 @@ import * as React from 'react';
import { fireEvent } from '@bfc/test-utils';
import { PublishDialog } from '../../../src/components/TestController/publishDialog';
-import { projectIdState, botNameState, settingsState, dispatcherState } from '../../../src/recoilModel';
+import { botNameState, settingsState, dispatcherState, currentProjectIdState } from '../../../src/recoilModel';
import { renderWithRecoil } from '../../testUtils';
jest.useFakeTimers();
+const projectId = '12abvc.as324';
const luisConfig = {
name: '',
authoringKey: '12345',
@@ -29,15 +30,22 @@ describe('', () => {
set(dispatcherState, {
setSettings: setSettingsMock,
});
- set(projectIdState, '12345');
- set(botNameState, 'sampleBot0');
- set(settingsState, {
+ set(currentProjectIdState, projectId);
+ set(botNameState(projectId), 'sampleBot0');
+ set(settingsState(projectId), {
luis: luisConfig,
qna: qnaConfig,
});
};
const { getByText } = renderWithRecoil(
- ,
+ ,
recoilInitState
);
diff --git a/Composer/packages/client/__tests__/components/createDialogModal.test.tsx b/Composer/packages/client/__tests__/components/createDialogModal.test.tsx
index 97182b3aa2..be19a52005 100644
--- a/Composer/packages/client/__tests__/components/createDialogModal.test.tsx
+++ b/Composer/packages/client/__tests__/components/createDialogModal.test.tsx
@@ -11,12 +11,13 @@ import { showCreateDialogModalState } from '../../src/recoilModel';
describe('', () => {
const onSubmitMock = jest.fn();
const onDismissMock = jest.fn();
+ const projectId = 'test-create-dialog';
function renderComponent() {
return renderWithRecoil(
- ,
+ ,
({ set }) => {
- set(showCreateDialogModalState, true);
+ set(showCreateDialogModalState(projectId), true);
}
);
}
diff --git a/Composer/packages/client/__tests__/components/design.test.tsx b/Composer/packages/client/__tests__/components/design.test.tsx
index b702e4b4b6..3231e33d7a 100644
--- a/Composer/packages/client/__tests__/components/design.test.tsx
+++ b/Composer/packages/client/__tests__/components/design.test.tsx
@@ -16,6 +16,7 @@ jest.mock('@bfc/code-editor', () => {
LuEditor: () =>
,
};
});
+const projectId = '1234a-324234';
describe('', () => {
it('should render the ProjectTree', async () => {
@@ -46,7 +47,7 @@ describe('', () => {
});
const handleSubmit = jest.fn(() => {});
const { getByText } = renderWithRecoil(
-
+
);
const cancelButton = getByText('Cancel');
fireEvent.click(cancelButton);
@@ -60,7 +61,13 @@ describe('', () => {
});
const handleSubmit = jest.fn(() => {});
const { getByText } = renderWithRecoil(
-
+
);
const cancelButton = getByText('Cancel');
fireEvent.click(cancelButton);
diff --git a/Composer/packages/client/__tests__/components/skill.test.tsx b/Composer/packages/client/__tests__/components/skill.test.tsx
index 200a2e5fbd..1fe175c270 100644
--- a/Composer/packages/client/__tests__/components/skill.test.tsx
+++ b/Composer/packages/client/__tests__/components/skill.test.tsx
@@ -13,7 +13,7 @@ import CreateSkillModal, {
validateManifestUrl,
validateName,
} from '../../src/components/CreateSkillModal';
-import { settingsState, projectIdState, skillsState } from '../../src/recoilModel';
+import { currentProjectIdState, settingsState, skillsState } from '../../src/recoilModel';
import Skills from '../../src/pages/skills';
jest.mock('../../src//utils/httpUtil');
@@ -44,13 +44,14 @@ const skills: Skill[] = [
];
let recoilInitState;
+const projectId = '123a.234';
describe('Skill page', () => {
beforeEach(() => {
recoilInitState = ({ set }) => {
- set(projectIdState, '243245');
- set(skillsState, skills),
- set(settingsState, {
+ set(currentProjectIdState, projectId);
+ set(skillsState(projectId), skills),
+ set(settingsState(projectId), {
luis: {
name: '',
authoringKey: '12345',
@@ -90,7 +91,7 @@ describe('Skill page', () => {
describe('', () => {
it('should render the SkillList', () => {
- const { container } = renderWithRecoil(, recoilInitState);
+ const { container } = renderWithRecoil(, recoilInitState);
expect(container).toHaveTextContent('Email-Skill');
expect(container).toHaveTextContent('Point Of Interest Skill');
});
@@ -105,7 +106,7 @@ describe('', () => {
const onDismiss = jest.fn();
const onSubmit = jest.fn();
const { getByLabelText, getByText } = renderWithRecoil(
- ,
+ ,
recoilInitState
);
@@ -120,12 +121,10 @@ describe('', () => {
act(() => {
fireEvent.click(submitButton);
});
-
expect(onSubmit).not.toBeCalled();
});
let formDataErrors;
- let projectId;
let validationState;
let setFormDataErrors;
let setSkillManifest;
@@ -133,7 +132,6 @@ describe('', () => {
beforeEach(() => {
formDataErrors = {};
- projectId = '123';
validationState = {};
setFormDataErrors = jest.fn();
setSkillManifest = jest.fn();
@@ -225,7 +223,7 @@ describe('', () => {
manifestUrl: 'Validating',
})
);
- expect(httpClient.post).toBeCalledWith('/projects/123/skill/check', { url: formData.manifestUrl });
+ expect(httpClient.post).toBeCalledWith(`/projects/${projectId}/skill/check`, { url: formData.manifestUrl });
expect(setSkillManifest).toBeCalledWith('skill manifest');
expect(setValidationState).toBeCalledWith(
expect.objectContaining({
@@ -259,7 +257,7 @@ describe('', () => {
manifestUrl: 'Validating',
})
);
- expect(httpClient.post).toBeCalledWith('/projects/123/skill/check', { url: formData.manifestUrl });
+ expect(httpClient.post).toBeCalledWith(`/projects/${projectId}/skill/check`, { url: formData.manifestUrl });
expect(setSkillManifest).not.toBeCalled();
expect(setValidationState).toBeCalledWith(
expect.objectContaining({
diff --git a/Composer/packages/client/__tests__/components/triggerCreationModal.test.tsx b/Composer/packages/client/__tests__/components/triggerCreationModal.test.tsx
index aa6a82e8c1..f42c05e37b 100644
--- a/Composer/packages/client/__tests__/components/triggerCreationModal.test.tsx
+++ b/Composer/packages/client/__tests__/components/triggerCreationModal.test.tsx
@@ -7,13 +7,21 @@ import { fireEvent, waitFor } from '@bfc/test-utils';
import { TriggerCreationModal } from '../../src/components/ProjectTree/TriggerCreationModal';
import { renderWithRecoil } from '../testUtils';
+const projectId = '123a-bv3c4';
+
describe('', () => {
const onSubmitMock = jest.fn();
const onDismissMock = jest.fn();
function renderComponent() {
return renderWithRecoil(
-
+
);
}
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 019f174525..fcb4d7d0b2 100644
--- a/Composer/packages/client/__tests__/pages/knowledge-base/QnAPage.test.tsx
+++ b/Composer/packages/client/__tests__/pages/knowledge-base/QnAPage.test.tsx
@@ -7,13 +7,13 @@ import TableView from '../../../src/pages/knowledge-base/table-view';
import CodeEditor from '../../../src/pages/knowledge-base/code-editor';
import { renderWithRecoil } from '../../testUtils';
import {
- projectIdState,
localeState,
dialogsState,
qnaFilesState,
settingsState,
schemasState,
dispatcherState,
+ currentProjectIdState,
} from '../../../src/recoilModel';
import mockProjectResponse from '../../../src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json';
@@ -50,12 +50,12 @@ const state = {
const updateQnAFileMock = jest.fn();
const initRecoilState = ({ set }) => {
- set(projectIdState, state.projectId);
- set(localeState, state.locale);
- set(dialogsState, state.dialogs);
- set(qnaFilesState, state.qnaFiles);
- set(settingsState, state.settings);
- set(schemasState, mockProjectResponse.schemas);
+ set(currentProjectIdState, state.projectId);
+ set(localeState(state.projectId), state.locale);
+ set(dialogsState(state.projectId), state.dialogs);
+ set(qnaFilesState(state.projectId), state.qnaFiles);
+ set(settingsState(state.projectId), state.settings);
+ set(schemasState(state.projectId), mockProjectResponse.schemas);
set(dispatcherState, {
updateQnAFile: updateQnAFileMock,
});
@@ -63,13 +63,16 @@ const initRecoilState = ({ set }) => {
describe('QnA page all up view', () => {
it('should render QnA page table view', () => {
- const { getByText, getByTestId } = renderWithRecoil(, initRecoilState);
+ const { getByText, getByTestId } = renderWithRecoil(
+ ,
+ initRecoilState
+ );
getByTestId('table-view');
getByText('question (1)');
getByText('answer');
});
it('should render QnA page code editor', () => {
- renderWithRecoil(, initRecoilState);
+ renderWithRecoil(, initRecoilState);
});
});
diff --git a/Composer/packages/client/__tests__/pages/language-generation/LGPage.test.tsx b/Composer/packages/client/__tests__/pages/language-generation/LGPage.test.tsx
index b983df4960..7ee7ac4363 100644
--- a/Composer/packages/client/__tests__/pages/language-generation/LGPage.test.tsx
+++ b/Composer/packages/client/__tests__/pages/language-generation/LGPage.test.tsx
@@ -7,13 +7,13 @@ import { renderWithRecoil } from '../../testUtils';
import TableView from '../../../src/pages/language-generation/table-view';
import CodeEditor from '../../../src/pages/language-generation/code-editor';
import {
- projectIdState,
localeState,
luFilesState,
lgFilesState,
settingsState,
schemasState,
dialogsState,
+ currentProjectIdState,
} from '../../../src/recoilModel';
import mockProjectResponse from '../../../src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json';
@@ -48,23 +48,26 @@ const state = {
};
const initRecoilState = ({ set }) => {
- set(projectIdState, state.projectId);
- set(localeState, state.locale);
- set(dialogsState, state.dialogs);
- set(luFilesState, state.luFiles);
- set(lgFilesState, state.lgFiles);
- set(settingsState, state.settings);
- set(schemasState, mockProjectResponse.schemas);
+ set(currentProjectIdState, state.projectId);
+ set(localeState(state.projectId), state.locale);
+ set(dialogsState(state.projectId), state.dialogs);
+ set(luFilesState(state.projectId), state.luFiles);
+ set(lgFilesState(state.projectId), state.lgFiles);
+ set(settingsState(state.projectId), state.settings);
+ set(schemasState(state.projectId), mockProjectResponse.schemas);
};
describe('LG page all up view', () => {
it('should render lg page table view', () => {
- const { getByText, getByTestId } = renderWithRecoil(, initRecoilState);
+ const { getByText, getByTestId } = renderWithRecoil(
+ ,
+ initRecoilState
+ );
getByTestId('table-view');
getByText('Name');
});
it('should render lg page code editor', () => {
- renderWithRecoil(, initRecoilState);
+ renderWithRecoil(, initRecoilState);
});
});
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 b5ff82f14a..bd0b29be46 100644
--- a/Composer/packages/client/__tests__/pages/language-understanding/LUPage.test.tsx
+++ b/Composer/packages/client/__tests__/pages/language-understanding/LUPage.test.tsx
@@ -7,13 +7,13 @@ import TableView from '../../../src/pages/language-understanding/table-view';
import CodeEditor from '../../../src/pages/language-understanding/code-editor';
import { renderWithRecoil } from '../../testUtils';
import {
- projectIdState,
localeState,
dialogsState,
luFilesState,
lgFilesState,
settingsState,
schemasState,
+ currentProjectIdState,
} from '../../../src/recoilModel';
import mockProjectResponse from '../../../src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json';
@@ -41,23 +41,26 @@ const state = {
};
const initRecoilState = ({ set }) => {
- set(projectIdState, state.projectId);
- set(localeState, state.locale);
- set(dialogsState, state.dialogs);
- set(luFilesState, state.luFiles);
- set(lgFilesState, state.lgFiles);
- set(settingsState, state.settings);
- set(schemasState, mockProjectResponse.schemas);
+ set(currentProjectIdState, state.projectId);
+ set(localeState(state.projectId), state.locale);
+ set(dialogsState(state.projectId), state.dialogs);
+ set(luFilesState(state.projectId), state.luFiles);
+ set(lgFilesState(state.projectId), state.lgFiles);
+ set(settingsState(state.projectId), state.settings);
+ set(schemasState(state.projectId), mockProjectResponse.schemas);
};
describe('LU page all up view', () => {
it('should render lu page table view', () => {
- const { getByText, getByTestId } = renderWithRecoil(, initRecoilState);
+ const { getByText, getByTestId } = renderWithRecoil(
+ ,
+ initRecoilState
+ );
getByTestId('table-view');
getByText('Intent');
});
it('should render lu page code editor', () => {
- renderWithRecoil(, initRecoilState);
+ renderWithRecoil(, initRecoilState);
});
});
diff --git a/Composer/packages/client/__tests__/pages/notifications/useNotifications.test.tsx b/Composer/packages/client/__tests__/pages/notifications/useNotifications.test.tsx
index c0914b37a9..326a4d7d06 100644
--- a/Composer/packages/client/__tests__/pages/notifications/useNotifications.test.tsx
+++ b/Composer/packages/client/__tests__/pages/notifications/useNotifications.test.tsx
@@ -8,13 +8,13 @@ import { Range, Position } from '@bfc/shared';
import useNotifications from '../../../src/pages/notifications/useNotifications';
import {
- projectIdState,
dialogsState,
luFilesState,
lgFilesState,
- BotDiagnosticsState,
settingsState,
schemasState,
+ currentProjectIdState,
+ botDiagnosticsState,
} from '../../../src/recoilModel';
import mockProjectResponse from '../../../src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json';
@@ -97,13 +97,13 @@ const state = {
};
const initRecoilState = ({ set }) => {
- set(projectIdState, state.projectId);
- set(dialogsState, state.dialogs);
- set(luFilesState, state.luFiles);
- set(lgFilesState, state.lgFiles);
- set(BotDiagnosticsState, state.diagnostics);
- set(settingsState, state.settings);
- set(schemasState, mockProjectResponse.schemas);
+ set(currentProjectIdState, state.projectId);
+ set(dialogsState(state.projectId), state.dialogs);
+ set(luFilesState(state.projectId), state.luFiles);
+ set(lgFilesState(state.projectId), state.lgFiles);
+ set(botDiagnosticsState(state.projectId), state.diagnostics);
+ set(settingsState(state.projectId), state.settings);
+ set(schemasState(state.projectId), mockProjectResponse.schemas);
};
describe('useNotification hooks', () => {
@@ -114,7 +114,7 @@ describe('useNotification hooks', () => {
return {children};
};
- const { result } = renderHook(() => useNotifications(), {
+ const { result } = renderHook(() => useNotifications(state.projectId), {
wrapper,
});
renderedResult = result;
@@ -130,7 +130,7 @@ describe('useNotification hooks', () => {
return {children};
};
- const { result } = renderHook(() => useNotifications('Error'), {
+ const { result } = renderHook(() => useNotifications(state.projectId, 'Error'), {
wrapper,
});
diff --git a/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx b/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx
index c838890b05..b9aa12ddfe 100644
--- a/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx
+++ b/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx
@@ -7,8 +7,8 @@ import {
settingsState,
botNameState,
publishTypesState,
- projectIdState,
publishHistoryState,
+ currentProjectIdState,
} from '../../../src/recoilModel';
import { CreatePublishTarget } from '../../../src/pages/publish/createPublishTarget';
import { PublishStatusList } from '../../../src/pages/publish/publishStatusList';
@@ -52,11 +52,11 @@ const state = {
};
const initRecoilState = ({ set }) => {
- set(projectIdState, state.projectId);
- set(botNameState, state.botName);
- set(publishTypesState, state.publishTypes);
- set(publishHistoryState, state.publishHistory);
- set(settingsState, state.settings);
+ set(currentProjectIdState, state.projectId);
+ set(botNameState(state.projectId), state.botName);
+ set(publishTypesState(state.projectId), state.publishTypes);
+ set(publishHistoryState(state.projectId), state.publishHistory);
+ set(settingsState(state.projectId), state.settings);
};
describe('publish page', () => {
diff --git a/Composer/packages/client/__tests__/shell/lgApi.test.tsx b/Composer/packages/client/__tests__/shell/lgApi.test.tsx
index 362db33e52..b35ed50a06 100644
--- a/Composer/packages/client/__tests__/shell/lgApi.test.tsx
+++ b/Composer/packages/client/__tests__/shell/lgApi.test.tsx
@@ -6,7 +6,7 @@ import * as React from 'react';
import { RecoilRoot } from 'recoil';
import { useLgApi } from '../../src/shell/lgApi';
-import { lgFilesState, localeState, projectIdState, dispatcherState } from '../../src/recoilModel';
+import { lgFilesState, localeState, dispatcherState, currentProjectIdState } from '../../src/recoilModel';
import { Dispatcher } from '../../src/recoilModel/dispatchers';
const state = {
@@ -39,9 +39,9 @@ describe('use lgApi hooks', () => {
removeLgTemplateMock = jest.fn();
initRecoilState = ({ set }) => {
- set(projectIdState, state.projectId);
- set(localeState, 'en-us');
- set(lgFilesState, state.lgFiles);
+ set(currentProjectIdState, state.projectId);
+ set(localeState(state.projectId), 'en-us');
+ set(lgFilesState(state.projectId), state.lgFiles);
set(dispatcherState, (current: Dispatcher) => ({
...current,
updateLgTemplate: updateLgTemplateMock,
@@ -55,7 +55,7 @@ describe('use lgApi hooks', () => {
const { children } = props;
return {children};
};
- const rendered = renderHook(() => useLgApi(), {
+ const rendered = renderHook(() => useLgApi(state.projectId), {
wrapper,
});
result = rendered.result;
@@ -76,6 +76,7 @@ describe('use lgApi hooks', () => {
expect(updateLgTemplateMock).toBeCalledTimes(1);
const arg = {
id: 'test.en-us',
+ projectId: state.projectId,
template: {
body: 'update',
name: 'bar',
@@ -94,6 +95,7 @@ describe('use lgApi hooks', () => {
id: 'test.en-us',
fromTemplateName: 'from',
toTemplateName: 'to',
+ projectId: state.projectId,
};
expect(copyLgTemplateMock).toBeCalledWith(arg);
});
@@ -106,6 +108,7 @@ describe('use lgApi hooks', () => {
const arg = {
id: 'test.en-us',
templateName: 'bar',
+ projectId: state.projectId,
};
expect(removeLgTemplateMock).toBeCalledWith(arg);
});
@@ -117,6 +120,7 @@ describe('use lgApi hooks', () => {
const arg = {
id: 'test.en-us',
templateNames: ['bar'],
+ projectId: state.projectId,
};
expect(removeLgTemplatesMock).toBeCalledWith(arg);
});
diff --git a/Composer/packages/client/__tests__/shell/luApi.test.tsx b/Composer/packages/client/__tests__/shell/luApi.test.tsx
index 29d24a85cb..215c2cd7d1 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 { projectIdState, localeState, luFilesState, dispatcherState } from '../../src/recoilModel';
+import { localeState, luFilesState, dispatcherState, currentProjectIdState } from '../../src/recoilModel';
import { Dispatcher } from '../../src/recoilModel/dispatchers';
jest.mock('../../src/recoilModel/parsers/luWorker', () => {
@@ -32,9 +32,9 @@ describe('use luApi hooks', () => {
updateLuFileMockMock = jest.fn();
const initRecoilState = ({ set }) => {
- set(projectIdState, state.projectId);
- set(localeState, 'en-us');
- set(luFilesState, state.luFiles);
+ set(currentProjectIdState, state.projectId);
+ set(localeState(state.projectId), 'en-us');
+ set(luFilesState(state.projectId), state.luFiles);
set(dispatcherState, (current: Dispatcher) => ({
...current,
updateLuFile: updateLuFileMockMock,
@@ -48,7 +48,7 @@ describe('use luApi hooks', () => {
const { children } = props;
return {children};
};
- const rendered = renderHook(() => useLuApi(), {
+ const rendered = renderHook(() => useLuApi(state.projectId), {
wrapper,
});
result = rendered.result;
diff --git a/Composer/packages/client/__tests__/shell/triggerApi.test.tsx b/Composer/packages/client/__tests__/shell/triggerApi.test.tsx
index d1739b945f..50931022b8 100644
--- a/Composer/packages/client/__tests__/shell/triggerApi.test.tsx
+++ b/Composer/packages/client/__tests__/shell/triggerApi.test.tsx
@@ -7,13 +7,13 @@ import { RecoilRoot } from 'recoil';
import { useTriggerApi } from '../../src/shell/triggerApi';
import {
- projectIdState,
localeState,
luFilesState,
lgFilesState,
dialogsState,
schemasState,
dispatcherState,
+ currentProjectIdState,
} from '../../src/recoilModel';
import { Dispatcher } from '../../src/recoilModel/dispatchers';
@@ -53,12 +53,12 @@ describe('use triggerApi hooks', () => {
createLuIntentMock = jest.fn();
const initRecoilState = ({ set }) => {
- set(projectIdState, state.projectId);
- set(localeState, 'en-us');
- set(luFilesState, state.luFiles);
- set(lgFilesState, state.lgFiles);
- set(dialogsState, state.dialogs);
- set(schemasState, state.schemas);
+ set(currentProjectIdState, state.projectId);
+ set(localeState(state.projectId), 'en-us');
+ set(luFilesState(state.projectId), state.luFiles);
+ set(lgFilesState(state.projectId), state.lgFiles);
+ set(dialogsState(state.projectId), state.dialogs);
+ set(schemasState(state.projectId), state.schemas);
set(dispatcherState, (current: Dispatcher) => ({
...current,
selectTo: selectToMock,
@@ -72,7 +72,7 @@ describe('use triggerApi hooks', () => {
const { children } = props;
return {children};
};
- const rendered = renderHook(() => useTriggerApi(), {
+ const rendered = renderHook(() => useTriggerApi(state.projectId), {
wrapper,
});
result = rendered.result;
diff --git a/Composer/packages/client/__tests__/utils/navigation.test.ts b/Composer/packages/client/__tests__/utils/navigation.test.ts
index 0a3cee9921..a13499e194 100644
--- a/Composer/packages/client/__tests__/utils/navigation.test.ts
+++ b/Composer/packages/client/__tests__/utils/navigation.test.ts
@@ -13,6 +13,8 @@ import {
convertPathToUrl,
} from './../../src/utils/navigation';
+const projectId = '123a-sdf123';
+
describe('getFocusPath', () => {
it('return focus path', () => {
const result1 = getFocusPath('selected', 'focused');
@@ -71,17 +73,19 @@ describe('composer url util', () => {
});
it('check url', () => {
- const result1 = checkUrl(`/bot/1/dialogs/a?selected=triggers[0]&focused=triggers[0].actions[0]#botAsks`, {
- dialogId: 'a',
- projectId: '1',
- selected: 'triggers[0]',
- focused: 'triggers[0].actions[0]',
- promptTab: PromptTab.BOT_ASKS,
- });
+ const result1 = checkUrl(
+ `/bot/${projectId}/dialogs/a?selected=triggers[0]&focused=triggers[0].actions[0]#botAsks`,
+ projectId,
+ {
+ dialogId: 'a',
+ selected: 'triggers[0]',
+ focused: 'triggers[0].actions[0]',
+ promptTab: PromptTab.BOT_ASKS,
+ }
+ );
expect(result1).toEqual(true);
- const result2 = checkUrl(`test`, {
+ const result2 = checkUrl(`test`, projectId, {
dialogId: 'a',
- projectId: '1',
selected: 'triggers[0]',
focused: 'triggers[0].actions[0]',
promptTab: PromptTab.BOT_ASKS,
@@ -90,11 +94,13 @@ describe('composer url util', () => {
});
it('convert path to url', () => {
- const result1 = convertPathToUrl('1', 'main');
- expect(result1).toEqual('/bot/1/dialogs/main');
- const result2 = convertPathToUrl('1', 'main', 'main.triggers[0].actions[0]');
- expect(result2).toEqual('/bot/1/dialogs/main?selected=triggers[0]&focused=triggers[0].actions[0]');
- const result3 = convertPathToUrl('1', 'main', 'main.triggers[0].actions[0]#Microsoft.TextInput#prompt');
- expect(result3).toEqual('/bot/1/dialogs/main?selected=triggers[0]&focused=triggers[0].actions[0]#botAsks');
+ const result1 = convertPathToUrl(projectId, 'main');
+ expect(result1).toEqual(`/bot/${projectId}/dialogs/main`);
+ const result2 = convertPathToUrl(projectId, 'main', 'main.triggers[0].actions[0]');
+ expect(result2).toEqual(`/bot/${projectId}/dialogs/main?selected=triggers[0]&focused=triggers[0].actions[0]`);
+ const result3 = convertPathToUrl(projectId, 'main', 'main.triggers[0].actions[0]#Microsoft.TextInput#prompt');
+ expect(result3).toEqual(
+ `/bot/${projectId}/dialogs/main?selected=triggers[0]&focused=triggers[0].actions[0]#botAsks`
+ );
});
});
diff --git a/Composer/packages/client/src/Onboarding/Onboarding.tsx b/Composer/packages/client/src/Onboarding/Onboarding.tsx
index 1ac784288b..81b5c2b20b 100644
--- a/Composer/packages/client/src/Onboarding/Onboarding.tsx
+++ b/Composer/packages/client/src/Onboarding/Onboarding.tsx
@@ -9,8 +9,7 @@ import { useRecoilValue } from 'recoil';
import onboardingStorage from '../utils/onboardingStorage';
import { OpenConfirmModal } from '../components/Modal/ConfirmDialog';
import { useLocation } from '../utils/hooks';
-import { projectIdState, dispatcherState, onboardingState } from '../recoilModel';
-import { validatedDialogsSelector } from '../recoilModel/selectors/validatedDialogs';
+import { dispatcherState, onboardingState, botProjectsSpaceState, validateDialogSelectorFamily } from '../recoilModel';
import OnboardingContext from './OnboardingContext';
import TeachingBubbles from './TeachingBubbles/TeachingBubbles';
@@ -21,16 +20,17 @@ const getCurrentSet = (stepSets) => stepSets.findIndex(({ id }) => id === onboar
const Onboarding: React.FC = () => {
const didMount = useRef(false);
+ const botProjects = useRecoilValue(botProjectsSpaceState);
+ const rootBotProjectId = botProjects[0];
+ const dialogs = useRecoilValue(validateDialogSelectorFamily(rootBotProjectId));
const { onboardingSetComplete } = useRecoilValue(dispatcherState);
const onboarding = useRecoilValue(onboardingState);
const complete = onboarding.complete;
- const dialogs = useRecoilValue(validatedDialogsSelector);
- const projectId = useRecoilValue(projectIdState);
const rootDialogId = dialogs.find(({ isRoot }) => isRoot === true)?.id || 'Main';
const stepSets = useMemo(() => {
- return defaultStepSets(projectId, rootDialogId)
+ return defaultStepSets(rootBotProjectId, rootDialogId)
.map((stepSet) => ({
...stepSet,
steps: stepSet.steps.filter(({ targetId }) => {
@@ -43,7 +43,7 @@ const Onboarding: React.FC = () => {
}),
}))
.filter(({ steps }) => steps.length);
- }, [projectId, rootDialogId]);
+ }, [rootBotProjectId, rootDialogId]);
const [currentSet, setCurrentSet] = useState(getCurrentSet(stepSets));
const [currentStep, setCurrentStep] = useState(0);
@@ -68,7 +68,7 @@ const Onboarding: React.FC = () => {
const { steps } = stepSets[currentSet] || { steps: [] };
const coachMark = steps[currentStep] || {};
const { id, location, navigateTo, targetId } = coachMark;
- !complete && projectId && navigateTo && navigate(navigateTo);
+ !complete && rootBotProjectId && navigateTo && navigate(navigateTo);
setTeachingBubble({ currentStep, id, location, setLength: steps.length, targetId });
setMinimized(currentStep >= 0);
@@ -76,10 +76,10 @@ const Onboarding: React.FC = () => {
if (currentSet > -1 && currentSet < stepSets.length) {
onboardingStorage.setCurrentSet(stepSets[currentSet].id);
}
- }, [currentSet, currentStep, setTeachingBubble, projectId]);
+ }, [currentSet, currentStep, setTeachingBubble, rootBotProjectId]);
useEffect(() => {
- setHideModal(pathname !== `/bot/${projectId}/dialogs/${rootDialogId}`);
+ setHideModal(pathname !== `/bot/${rootBotProjectId}/dialogs/${rootDialogId}`);
if (currentSet === 0) {
setCurrentStep(pathname === '/home' ? 0 : -1);
}
diff --git a/Composer/packages/client/src/components/AppComponents/RightPanel.tsx b/Composer/packages/client/src/components/AppComponents/RightPanel.tsx
index 7c12a68899..0d13f00346 100644
--- a/Composer/packages/client/src/components/AppComponents/RightPanel.tsx
+++ b/Composer/packages/client/src/components/AppComponents/RightPanel.tsx
@@ -10,7 +10,7 @@ import { RequireAuth } from '../RequireAuth';
import { ErrorBoundary } from '../ErrorBoundary';
import Routes from './../../router';
-import { applicationErrorState, dispatcherState, projectIdState } from './../../recoilModel';
+import { applicationErrorState, dispatcherState, currentProjectIdState } from './../../recoilModel';
// -------------------- Styles -------------------- //
@@ -36,7 +36,7 @@ const Content = forwardRef((props, ref) => {
const applicationError = useRecoilValue(applicationErrorState);
const { setApplicationLevelError, fetchProjectById } = useRecoilValue(dispatcherState);
- const projectId = useRecoilValue(projectIdState);
+ const projectId = useRecoilValue(currentProjectIdState);
return (
= ({ projectId, onSubmit, onDismiss }) => {
- const skills = useRecoilValue(skillsState);
+ const skills = useRecoilValue(skillsState(projectId));
const [formData, setFormData] = useState>({});
const [formDataErrors, setFormDataErrors] = useState({});
diff --git a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx
index 5e2493766b..bcfff92fc8 100644
--- a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx
+++ b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx
@@ -12,12 +12,11 @@ import { CreationFlowStatus } from '../../constants';
import {
dispatcherState,
creationFlowStatusState,
- projectIdState,
templateProjectsState,
storagesState,
focusedStorageFolderState,
+ currentProjectIdState,
userSettingsState,
- localeState,
} from '../../recoilModel';
import Home from '../../pages/home/Home';
import ImportQnAFromUrlModal from '../../pages/knowledge-base/ImportQnAFromUrlModal';
@@ -33,7 +32,7 @@ type CreationFlowProps = RouteComponentProps<{}>;
const CreationFlow: React.FC = () => {
const {
fetchTemplates,
- openBotProject,
+ openProject,
createProject,
saveProjectAs,
fetchStorages,
@@ -43,17 +42,15 @@ const CreationFlow: React.FC = () => {
updateCurrentPathForStorage,
updateFolder,
saveTemplateId,
- importQnAFromUrls,
fetchProjectById,
fetchRecentProjects,
} = useRecoilValue(dispatcherState);
const creationFlowStatus = useRecoilValue(creationFlowStatusState);
- const projectId = useRecoilValue(projectIdState);
+ const projectId = useRecoilValue(currentProjectIdState);
const templateProjects = useRecoilValue(templateProjectsState);
const storages = useRecoilValue(storagesState);
const focusedStorageFolder = useRecoilValue(focusedStorageFolderState);
const { appLocale } = useRecoilValue(userSettingsState);
- const locale = useRecoilValue(localeState);
const cachedProjectId = useProjectIdCache();
const currentStorageIndex = useRef(0);
const storage = storages[currentStorageIndex.current];
@@ -101,17 +98,18 @@ const CreationFlow: React.FC = () => {
const openBot = async (botFolder) => {
setCreationFlowStatus(CreationFlowStatus.CLOSE);
- openBotProject(botFolder);
+ openProject(botFolder);
};
- const handleCreateNew = async (formData, templateId: string) => {
- await createProject(
+ const handleCreateNew = async (formData, templateId: string, qnaKbUrls?: string[]) => {
+ createProject(
templateId || '',
formData.name,
formData.description,
formData.location,
formData.schemaUrl,
- appLocale
+ appLocale,
+ qnaKbUrls
);
};
@@ -122,11 +120,7 @@ const CreationFlow: React.FC = () => {
const handleCreateQnA = async (urls: string[]) => {
saveTemplateId(QnABotTemplateId);
handleDismiss();
- await handleCreateNew(formData, QnABotTemplateId);
- // import qna from urls
- if (urls.length > 0) {
- await importQnAFromUrls({ id: `${formData.name.toLocaleLowerCase()}.${locale}`, urls });
- }
+ handleCreateNew(formData, QnABotTemplateId, urls);
};
const handleSubmitOrImportQnA = async (formData, templateId: string) => {
diff --git a/Composer/packages/client/src/components/Header.tsx b/Composer/packages/client/src/components/Header.tsx
index 801cc3b9fe..98519ed5a6 100644
--- a/Composer/packages/client/src/components/Header.tsx
+++ b/Composer/packages/client/src/components/Header.tsx
@@ -10,7 +10,7 @@ import { useRecoilValue } from 'recoil';
import { SharedColors } from '@uifabric/fluent-theme';
import { FontWeights } from 'office-ui-fabric-react/lib/Styling';
-import { dispatcherState, appUpdateState, botNameState, localeState } from '../recoilModel';
+import { dispatcherState, appUpdateState, botNameState, localeState, currentProjectIdState } from '../recoilModel';
import composerIcon from '../images/composerIcon.svg';
import { AppUpdaterStatus } from '../constants';
@@ -74,8 +74,9 @@ const headerTextContainer = css`
export const Header = () => {
const { setAppUpdateShowing } = useRecoilValue(dispatcherState);
- const curBotName = useRecoilValue(botNameState);
- const locale = useRecoilValue(localeState);
+ const projectId = useRecoilValue(currentProjectIdState);
+ const projectName = useRecoilValue(botNameState(projectId));
+ const locale = useRecoilValue(localeState(projectId));
const appUpdate = useRecoilValue(appUpdateState);
const { showing, status } = appUpdate;
@@ -95,10 +96,10 @@ export const Header = () => {
/>
{formatMessage('Bot Framework Composer')}
- {curBotName && (
+ {projectName && (
- {`${curBotName} (${locale})`}
+ {`${projectName} (${locale})`}
)}
diff --git a/Composer/packages/client/src/components/Modal/DisplayManifestModal.tsx b/Composer/packages/client/src/components/Modal/DisplayManifestModal.tsx
index e0cf4e2847..0f29520e55 100644
--- a/Composer/packages/client/src/components/Modal/DisplayManifestModal.tsx
+++ b/Composer/packages/client/src/components/Modal/DisplayManifestModal.tsx
@@ -16,7 +16,7 @@ import { IDialogContentStyles } from 'office-ui-fabric-react/lib/Dialog';
import { IModalStyles } from 'office-ui-fabric-react/lib/Modal';
import { useRecoilValue } from 'recoil';
-import { skillsState, userSettingsState } from '../../recoilModel';
+import { userSettingsState, skillsState } from '../../recoilModel';
// -------------------- Styles -------------------- //
@@ -55,6 +55,7 @@ interface DisplayManifestModalProps {
isModeless?: boolean;
manifestId?: string | null;
onDismiss: () => void;
+ projectId: string;
}
export const DisplayManifestModal: React.FC = ({
@@ -62,8 +63,9 @@ export const DisplayManifestModal: React.FC = ({
isModeless = true,
manifestId,
onDismiss,
+ projectId,
}) => {
- const skills = useRecoilValue(skillsState);
+ const skills = useRecoilValue(skillsState(projectId));
const userSettings = useRecoilValue(userSettingsState);
useEffect(() => onDismiss, []);
diff --git a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx
index b42bf98421..fe1e341d72 100644
--- a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx
+++ b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx
@@ -35,11 +35,10 @@ import {
qnaMatcherKey,
onChooseIntentKey,
} from '../../utils/dialogUtil';
-import { projectIdState } from '../../recoilModel/atoms/botState';
-import { userSettingsState } from '../../recoilModel';
+import { userSettingsState } from '../../recoilModel/atoms';
import { nameRegex } from '../../constants';
-import { validatedDialogsSelector } from '../../recoilModel/selectors/validatedDialogs';
import { isRegExRecognizerType, isLUISnQnARecognizerType } from '../../utils/dialogValidator';
+import { validateDialogSelectorFamily } from '../../recoilModel';
// -------------------- Styles -------------------- //
const styles = {
@@ -201,6 +200,7 @@ const validateForm = (
// -------------------- TriggerCreationModal -------------------- //
interface TriggerCreationModalProps {
+ projectId: string;
dialogId: string;
isOpen: boolean;
onDismiss: () => void;
@@ -208,10 +208,8 @@ interface TriggerCreationModalProps {
}
export const TriggerCreationModal: React.FC = (props) => {
- const { isOpen, onDismiss, onSubmit, dialogId } = props;
- const dialogs = useRecoilValue(validatedDialogsSelector);
-
- const projectId = useRecoilValue(projectIdState);
+ const { isOpen, onDismiss, onSubmit, dialogId, projectId } = props;
+ const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId));
const userSettings = useRecoilValue(userSettingsState);
const dialogFile = dialogs.find((dialog) => dialog.id === dialogId);
const isRegEx = isRegExRecognizerType(dialogFile);
diff --git a/Composer/packages/client/src/components/TestController/TestController.tsx b/Composer/packages/client/src/components/TestController/TestController.tsx
index 79022a265d..0c7f4ed829 100644
--- a/Composer/packages/client/src/components/TestController/TestController.tsx
+++ b/Composer/packages/client/src/components/TestController/TestController.tsx
@@ -11,15 +11,15 @@ import { useRecoilValue } from 'recoil';
import { IConfig, IPublishConfig, defaultPublishConfig } from '@bfc/shared';
import {
- botNameState,
+ botEndpointsState,
+ dispatcherState,
+ validateDialogSelectorFamily,
botStatusState,
+ botNameState,
luFilesState,
qnaFilesState,
settingsState,
- projectIdState,
botLoadErrorState,
- botEndpointsState,
- dispatcherState,
} from '../../recoilModel';
import settingsStorage from '../../utils/dialogSettingStorage';
import { QnaConfig, BotStatus, LuisConfig } from '../../constants';
@@ -27,7 +27,6 @@ import { isAbsHosted } from '../../utils/envUtil';
import useNotifications from '../../pages/notifications/useNotifications';
import { navigateTo, openInEmulator } from '../../utils/navigation';
import { getReferredQnaFiles } from '../../utils/qnaUtil';
-import { validatedDialogsSelector } from '../../recoilModel/selectors/validatedDialogs';
import { getReferredLuFiles } from './../../utils/luUtil';
import { PublishDialog } from './publishDialog';
@@ -54,20 +53,22 @@ let botStatusInterval: NodeJS.Timeout | undefined = undefined;
// -------------------- TestController -------------------- //
const POLLING_INTERVAL = 2500;
-export const TestController: React.FC = () => {
+export const TestController: React.FC<{ projectId: string }> = (props) => {
+ const { projectId = '' } = props;
const [modalOpen, setModalOpen] = useState(false);
const [calloutVisible, setCalloutVisible] = useState(false);
const botActionRef = useRef(null);
- const notifications = useNotifications();
- const botName = useRecoilValue(botNameState);
- const botStatus = useRecoilValue(botStatusState);
- const dialogs = useRecoilValue(validatedDialogsSelector);
- const luFiles = useRecoilValue(luFilesState);
- const qnaFiles = useRecoilValue(qnaFilesState);
- const settings = useRecoilValue(settingsState);
- const projectId = useRecoilValue(projectIdState);
- const botLoadErrorMsg = useRecoilValue(botLoadErrorState);
+ const notifications = useNotifications(projectId);
+
+ const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId));
+ const botStatus = useRecoilValue(botStatusState(projectId));
+ const botName = useRecoilValue(botNameState(projectId));
+ const luFiles = useRecoilValue(luFilesState(projectId));
+ const settings = useRecoilValue(settingsState(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesState(projectId));
+ const botLoadErrorMsg = useRecoilValue(botLoadErrorState(projectId));
+
const botEndpoints = useRecoilValue(botEndpointsState);
const {
publishToTarget,
@@ -99,7 +100,7 @@ export const TestController: React.FC = () => {
case BotStatus.failed:
openCallout();
stopPollingRuntime();
- setBotStatus(BotStatus.pending);
+ setBotStatus(BotStatus.pending, projectId);
break;
case BotStatus.published:
stopPollingRuntime();
@@ -153,7 +154,7 @@ export const TestController: React.FC = () => {
}
async function handlePublish(config: IPublishConfig) {
- setBotStatus(BotStatus.publishing);
+ setBotStatus(BotStatus.publishing, projectId);
dismissDialog();
const { luis, qna } = config;
const endpointKey = settings.qna?.endpointKey;
@@ -166,7 +167,7 @@ export const TestController: React.FC = () => {
}
async function handleLoadBot() {
- setBotStatus(BotStatus.reloading);
+ setBotStatus(BotStatus.reloading, projectId);
if (settings.qna && settings.qna.subscriptionKey) {
await setQnASettings(projectId, settings.qna.subscriptionKey);
}
@@ -275,6 +276,7 @@ export const TestController: React.FC = () => {
botName={botName}
config={publishDialogConfig}
isOpen={modalOpen}
+ projectId={projectId}
onDismiss={dismissDialog}
onPublish={handlePublish}
/>
diff --git a/Composer/packages/client/src/components/TestController/publishDialog.tsx b/Composer/packages/client/src/components/TestController/publishDialog.tsx
index c991d8dffa..b4464b3898 100644
--- a/Composer/packages/client/src/components/TestController/publishDialog.tsx
+++ b/Composer/packages/client/src/components/TestController/publishDialog.tsx
@@ -20,9 +20,9 @@ import { Dropdown, ResponsiveMode, IDropdownOption } from 'office-ui-fabric-reac
import { Text, Tips, Links, nameRegex } from '../../constants';
import { FieldConfig, useForm } from '../../hooks/useForm';
-import { dialogsState, luFilesState, qnaFilesState } from '../../recoilModel/atoms/botState';
import { getReferredQnaFiles } from '../../utils/qnaUtil';
import { getReferredLuFiles } from '../../utils/luUtil';
+import { dialogsState, luFilesState, qnaFilesState } from '../../recoilModel';
// -------------------- Styles -------------------- //
const textFieldLabel = css`
@@ -100,13 +100,14 @@ interface IPublishDialogProps {
config: IConfig;
onDismiss: () => void;
onPublish: (data: IPublishConfig) => void;
+ projectId: string;
}
export const PublishDialog: React.FC = (props) => {
- const { isOpen, onDismiss, onPublish, botName, config } = props;
- const dialogs = useRecoilValue(dialogsState);
- const luFiles = useRecoilValue(luFilesState);
- const qnaFiles = useRecoilValue(qnaFilesState);
+ const { isOpen, onDismiss, onPublish, botName, config, projectId } = props;
+ const dialogs = useRecoilValue(dialogsState(projectId));
+ const luFiles = useRecoilValue(luFilesState(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesState(projectId));
const qnaConfigShow = getReferredQnaFiles(qnaFiles, dialogs).length > 0;
const luConfigShow = getReferredLuFiles(luFiles, dialogs).length > 0;
diff --git a/Composer/packages/client/src/hooks/useResolver.ts b/Composer/packages/client/src/hooks/useResolver.ts
index 0e50990d6f..48d4dd35cb 100644
--- a/Composer/packages/client/src/hooks/useResolver.ts
+++ b/Composer/packages/client/src/hooks/useResolver.ts
@@ -4,14 +4,14 @@ import { useRef } from 'react';
import { importResolverGenerator } from '@bfc/shared';
import { useRecoilValue } from 'recoil';
-import { localeState, lgFilesState, luFilesState, qnaFilesState, dialogsState } from '../recoilModel';
-
-export const useResolvers = () => {
- const lgFiles = useRecoilValue(lgFilesState);
- const locale = useRecoilValue(localeState);
- const luFiles = useRecoilValue(luFilesState);
- const qnaFiles = useRecoilValue(qnaFilesState);
- const dialogs = useRecoilValue(dialogsState);
+import { dialogsState, luFilesState, lgFilesState, localeState, qnaFilesState } from '../recoilModel';
+
+export const useResolvers = (projectId: string) => {
+ const dialogs = useRecoilValue(dialogsState(projectId));
+ const luFiles = useRecoilValue(luFilesState(projectId));
+ const lgFiles = useRecoilValue(lgFilesState(projectId));
+ const locale = useRecoilValue(localeState(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesState(projectId));
const lgFilesRef = useRef(lgFiles);
lgFilesRef.current = lgFiles;
diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx
index f1c0b12362..29c51623ae 100644
--- a/Composer/packages/client/src/pages/design/DesignPage.tsx
+++ b/Composer/packages/client/src/pages/design/DesignPage.tsx
@@ -33,28 +33,27 @@ import { Toolbar, IToolbarItem } from '../../components/Toolbar';
import { clearBreadcrumb, getFocusPath } from '../../utils/navigation';
import { navigateTo } from '../../utils/navigation';
import { useShell } from '../../shell';
-import { undoFunctionState, undoVersionState } from '../../recoilModel/undo/history';
+import plugins, { mergePluginConfigs } from '../../plugins';
+import { useElectronFeatures } from '../../hooks/useElectronFeatures';
import {
- projectIdState,
- schemasState,
- showCreateDialogModalState,
+ visualEditorSelectionState,
+ userSettingsState,
dispatcherState,
+ schemasState,
displaySkillManifestState,
+ validateDialogSelectorFamily,
breadcrumbState,
- visualEditorSelectionState,
focusPathState,
+ showCreateDialogModalState,
showAddSkillDialogModalState,
actionsSeedState,
- userSettingsState,
localeState,
qnaFilesState,
} from '../../recoilModel';
import { getBaseName } from '../../utils/fileUtil';
-import { validatedDialogsSelector } from '../../recoilModel/selectors/validatedDialogs';
-import plugins, { mergePluginConfigs } from '../../plugins';
-import { useElectronFeatures } from '../../hooks/useElectronFeatures';
import ImportQnAFromUrlModal from '../knowledge-base/ImportQnAFromUrlModal';
import { triggerNotSupported } from '../../utils/dialogValidator';
+import { undoFunctionState, undoVersionState } from '../../recoilModel/undo/history';
import { WarningMessage } from './WarningMessage';
import {
@@ -110,23 +109,25 @@ const getTabFromFragment = () => {
};
const DesignPage: React.FC> = (props) => {
- const dialogs = useRecoilValue(validatedDialogsSelector);
- const projectId = useRecoilValue(projectIdState);
- const schemas = useRecoilValue(schemasState);
- const displaySkillManifest = useRecoilValue(displaySkillManifestState);
- const breadcrumb = useRecoilValue(breadcrumbState);
- const visualEditorSelection = useRecoilValue(visualEditorSelectionState);
- const focusPath = useRecoilValue(focusPathState);
- const showCreateDialogModal = useRecoilValue(showCreateDialogModalState);
- const showAddSkillDialogModal = useRecoilValue(showAddSkillDialogModalState);
- const { undo, redo, canRedo, canUndo, commitChanges, clearUndo } = useRecoilValue(undoFunctionState);
- const actionsSeed = useRecoilValue(actionsSeedState);
+ const { location, dialogId, projectId = '' } = props;
const userSettings = useRecoilValue(userSettingsState);
- const qnaFiles = useRecoilValue(qnaFilesState);
- const locale = useRecoilValue(localeState);
- const undoVersion = useRecoilValue(undoVersionState);
+
+ const schemas = useRecoilValue(schemasState(projectId));
+ const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId));
+ const displaySkillManifest = useRecoilValue(displaySkillManifestState(projectId));
+ const breadcrumb = useRecoilValue(breadcrumbState(projectId));
+ const focusPath = useRecoilValue(focusPathState(projectId));
+ const showCreateDialogModal = useRecoilValue(showCreateDialogModalState(projectId));
+ const showAddSkillDialogModal = useRecoilValue(showAddSkillDialogModalState(projectId));
+ const actionsSeed = useRecoilValue(actionsSeedState(projectId));
+ const locale = useRecoilValue(localeState(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesState(projectId));
+ const undoFunction = useRecoilValue(undoFunctionState(projectId));
+ const undoVersion = useRecoilValue(undoVersionState(projectId));
+
+ const { undo, redo, canRedo, canUndo, commitChanges, clearUndo } = undoFunction;
+ const visualEditorSelection = useRecoilValue(visualEditorSelectionState);
const {
- addSkill,
removeDialog,
updateDialog,
createDialogCancel,
@@ -142,9 +143,9 @@ const DesignPage: React.FC(dialogs[0]);
const [exportSkillModalVisible, setExportSkillModalVisible] = useState(false);
const [warningIsVisible, setWarningIsVisible] = useState(true);
- const shell = useShell('DesignPage');
- const shellForFlowEditor = useShell('FlowEditor');
- const shellForPropertyEditor = useShell('PropertyEditor');
+ const shell = useShell('DesignPage', projectId);
+ const shellForFlowEditor = useShell('FlowEditor', projectId);
+ const shellForPropertyEditor = useShell('PropertyEditor', projectId);
const triggerApi = useTriggerApi(shell.api);
const { createTrigger } = shell.api;
@@ -180,7 +181,7 @@ const DesignPage: React.FC {
dialogs.forEach(async (dialog) => {
if (!qnaFiles || qnaFiles.length === 0 || !qnaFiles.find((qnaFile) => getBaseName(qnaFile.id) === dialog.id)) {
- await createQnAFile({ id: dialog.id, content: '' });
+ await createQnAFile({ id: dialog.id, content: '', projectId });
}
});
}, [dialogs]);
@@ -212,14 +213,13 @@ const DesignPage: React.FC {
if (newDialog) {
- navTo(newDialog, []);
+ navTo(projectId, newDialog, []);
}
};
@@ -302,7 +302,7 @@ const DesignPage: React.FC {
- createDialogBegin([], onCreateDialogComplete);
+ createDialogBegin([], onCreateDialogComplete, projectId);
},
},
{
@@ -427,7 +427,7 @@ const DesignPage: React.FC {
- exportToZip({ projectId });
+ exportToZip(projectId);
},
},
{
@@ -442,7 +442,7 @@ const DesignPage: React.FC,
+ element: ,
align: 'right',
},
];
@@ -450,7 +450,7 @@ const DesignPage: React.FC triggerApi.deleteTrigger(id, trigger));
if (content) {
- updateDialog({ id, content });
+ updateDialog({ id, content, projectId });
const match = /\[(\d+)\]/g.exec(selected);
const current = match && match[1];
if (!current) return;
@@ -548,14 +548,14 @@ const DesignPage: React.FC= 0) {
//if the deleted node is selected and the selected one is not the first one, navTo the previous trigger;
- selectTo(createSelectedPath(currentIdx - 1));
+ selectTo(projectId, createSelectedPath(currentIdx - 1));
} else {
//if the deleted node is selected and the selected one is the first one, navTo the first trigger;
- navTo(id, []);
+ navTo(projectId, id, []);
}
} else if (index < currentIdx) {
//if the deleted node is at the front, navTo the current one;
- selectTo(createSelectedPath(currentIdx - 1));
+ selectTo(projectId, createSelectedPath(currentIdx - 1));
}
}
}
@@ -582,7 +582,7 @@ const DesignPage: React.FC 0) {
- await importQnAFromUrls({ id: `${dialogId}.${locale}`, urls });
+ await importQnAFromUrls({ id: `${dialogId}.${locale}`, urls, projectId });
}
}
};
@@ -597,7 +597,7 @@ const DesignPage: React.FC;
}
- const selectedTrigger = currentDialog.triggers.find((t) => t.id === selected);
+ const selectedTrigger = currentDialog?.triggers.find((t) => t.id === selected);
const withWarning = triggerNotSupported(currentDialog, selectedTrigger);
return (
@@ -609,10 +609,10 @@ const DesignPage: React.FC handleSelect(projectId, ...props)}
/>
-
+
{
- updateDialog({ id: currentDialog.id, content: data });
+ updateDialog({ id: currentDialog.id, content: data, projectId });
}}
/>
) : withWarning ? (
@@ -642,11 +642,11 @@ const DesignPage: React.FC {
setWarningIsVisible(false);
}}
- onOk={() => navTo(`/bot/${projectId}/knowledge-base/all`)}
+ onOk={() => navigateTo(`/bot/${projectId}/knowledge-base/all`)}
/>
)
) : (
-
+
setFlowEditorFocused(false)}
@@ -655,7 +655,7 @@ const DesignPage: React.FC
)}
-
+
@@ -666,20 +666,22 @@ const DesignPage: React.FC
createDialogCancel(projectId)}
onSubmit={handleCreateDialogSubmit}
/>
)}
{showAddSkillDialogModal && (
addSkillDialogCancel(projectId)}
onSubmit={(skill) => addSkill(projectId, skill)}
/>
)}
{exportSkillModalVisible && (
setExportSkillModalVisible(false)}
onSubmit={() => setExportSkillModalVisible(false)}
/>
@@ -688,6 +690,7 @@ const DesignPage: React.FC
@@ -696,7 +699,11 @@ const DesignPage: React.FC
)}
{displaySkillManifest && (
-
+ dismissManifestModal(projectId)}
+ />
)}
diff --git a/Composer/packages/client/src/pages/design/PropertyEditor.tsx b/Composer/packages/client/src/pages/design/PropertyEditor.tsx
index 2c203b8c30..e99598f147 100644
--- a/Composer/packages/client/src/pages/design/PropertyEditor.tsx
+++ b/Composer/packages/client/src/pages/design/PropertyEditor.tsx
@@ -5,15 +5,13 @@
import { jsx } from '@emotion/core';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import AdaptiveForm, { resolveRef, getUIOptions } from '@bfc/adaptive-form';
-import { FormErrors, JSONSchema7, useFormConfig } from '@bfc/extension-client';
+import { FormErrors, JSONSchema7, useFormConfig, useShellApi } from '@bfc/extension-client';
import formatMessage from 'format-message';
import isEqual from 'lodash/isEqual';
import debounce from 'lodash/debounce';
import { Resizable, ResizeCallback } from 're-resizable';
import { MicrosoftAdaptiveDialog } from '@bfc/shared';
-import { useShell } from '../../shell';
-
import { formEditor } from './styles';
function resolveBaseSchema(schema: JSONSchema7, $kind: string): JSONSchema7 | undefined {
@@ -27,7 +25,7 @@ function resolveBaseSchema(schema: JSONSchema7, $kind: string): JSONSchema7 | un
}
const PropertyEditor: React.FC = () => {
- const { api: shellApi, data: shellData } = useShell('PropertyEditor');
+ const { shellApi, ...shellData } = useShellApi();
const { currentDialog, data: formData = {}, focusPath, focusedSteps, schemas } = shellData;
const currentWidth = shellData?.userSettings?.propertyEditorWidth || 400;
diff --git a/Composer/packages/client/src/pages/design/VisualEditor.tsx b/Composer/packages/client/src/pages/design/VisualEditor.tsx
index 6d4d88f53a..0d2e012ec0 100644
--- a/Composer/packages/client/src/pages/design/VisualEditor.tsx
+++ b/Composer/packages/client/src/pages/design/VisualEditor.tsx
@@ -9,10 +9,15 @@ import { ActionButton } from 'office-ui-fabric-react/lib/Button';
import get from 'lodash/get';
import VisualDesigner from '@bfc/adaptive-flow';
import { useRecoilValue } from 'recoil';
+import { useShellApi } from '@bfc/extension-client';
import grayComposerIcon from '../../images/grayComposerIcon.svg';
-import { schemasState, designPageLocationState, dispatcherState } from '../../recoilModel';
-import { validatedDialogsSelector } from '../../recoilModel/selectors/validatedDialogs';
+import {
+ dispatcherState,
+ validateDialogSelectorFamily,
+ schemasState,
+ designPageLocationState,
+} from '../../recoilModel';
import { middleTriggerContainer, middleTriggerElements, triggerButton, visualEditor } from './styles';
@@ -55,12 +60,14 @@ interface VisualEditorProps {
}
const VisualEditor: React.FC = (props) => {
+ const { ...shellData } = useShellApi();
+ const { projectId } = shellData;
const { openNewTriggerModal, onFocus, onBlur } = props;
const [triggerButtonVisible, setTriggerButtonVisibility] = useState(false);
- const designPageLocation = useRecoilValue(designPageLocationState);
const { onboardingAddCoachMarkRef } = useRecoilValue(dispatcherState);
- const dialogs = useRecoilValue(validatedDialogsSelector);
- const schemas = useRecoilValue(schemasState);
+ const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId));
+ const schemas = useRecoilValue(schemasState(projectId));
+ const designPageLocation = useRecoilValue(designPageLocationState(projectId));
const { dialogId, selected } = designPageLocation;
const addRef = useCallback((visualEditor) => onboardingAddCoachMarkRef({ visualEditor }), []);
diff --git a/Composer/packages/client/src/pages/design/createDialogModal.tsx b/Composer/packages/client/src/pages/design/createDialogModal.tsx
index 9ea1037a43..c691d1f75f 100644
--- a/Composer/packages/client/src/pages/design/createDialogModal.tsx
+++ b/Composer/packages/client/src/pages/design/createDialogModal.tsx
@@ -12,7 +12,7 @@ import { DialogCreationCopy, nameRegex } from '../../constants';
import { StorageFolder } from '../../recoilModel/types';
import { DialogWrapper, DialogTypes } from '../../components/DialogWrapper';
import { FieldConfig, useForm } from '../../hooks/useForm';
-import { validatedDialogsSelector } from '../../recoilModel/selectors/validatedDialogs';
+import { validateDialogSelectorFamily } from '../../recoilModel';
import { name, description, styles as wizardStyles } from './styles';
@@ -27,11 +27,12 @@ interface CreateDialogModalProps {
onCurrentPathUpdate?: (newPath?: string, storageId?: string) => void;
focusedStorageFolder?: StorageFolder;
isOpen: boolean;
+ projectId: string;
}
export const CreateDialogModal: React.FC = (props) => {
- const dialogs = useRecoilValue(validatedDialogsSelector);
- const { onSubmit, onDismiss, isOpen } = props;
+ const { onSubmit, onDismiss, isOpen, projectId } = props;
+ const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId));
const formConfig: FieldConfig = {
name: {
required: true,
diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx
index 6ee8b11aae..c99ddd7e7e 100644
--- a/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx
+++ b/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx
@@ -98,6 +98,7 @@ export interface ContentProps {
skillManifests: SkillManifest[];
value: { [key: string]: any };
onChange: (_: any) => void;
+ projectId: string;
}
interface Button {
diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/Description.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/Description.tsx
index 4ffc85ea1f..269a8fea45 100644
--- a/Composer/packages/client/src/pages/design/exportSkillModal/content/Description.tsx
+++ b/Composer/packages/client/src/pages/design/exportSkillModal/content/Description.tsx
@@ -11,7 +11,7 @@ import { useRecoilValue } from 'recoil';
import { v4 as uuid } from 'uuid';
import { ContentProps } from '../constants';
-import { botNameState } from '../../../../recoilModel/atoms/botState';
+import { botNameState } from '../../../../recoilModel';
const styles = {
row: css`
@@ -50,8 +50,8 @@ const InlineLabelField: React.FC = (props) => {
);
};
-export const Description: React.FC = ({ errors, value, schema, onChange }) => {
- const botName = useRecoilValue(botNameState);
+export const Description: React.FC = ({ errors, value, schema, onChange, projectId }) => {
+ const botName = useRecoilValue(botNameState(projectId));
const { $schema, ...rest } = value;
const { hidden, properties } = useMemo(
diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/SaveManifest.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/SaveManifest.tsx
index 030275f44f..38578069a7 100644
--- a/Composer/packages/client/src/pages/design/exportSkillModal/content/SaveManifest.tsx
+++ b/Composer/packages/client/src/pages/design/exportSkillModal/content/SaveManifest.tsx
@@ -41,9 +41,9 @@ export const getManifestId = (
return fileId;
};
-export const SaveManifest: React.FC = ({ errors, manifest, setSkillManifest }) => {
- const botName = useRecoilValue(botNameState);
- const skillManifests = useRecoilValue(skillManifestsState);
+export const SaveManifest: React.FC = ({ errors, manifest, setSkillManifest, projectId }) => {
+ const botName = useRecoilValue(botNameState(projectId));
+ const skillManifests = useRecoilValue(skillManifestsState(projectId));
const { id } = manifest;
diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectDialogs.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectDialogs.tsx
index c7d13500d0..9f83cdce31 100644
--- a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectDialogs.tsx
+++ b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectDialogs.tsx
@@ -11,8 +11,7 @@ import debounce from 'lodash/debounce';
import formatMessage from 'format-message';
import { ContentProps } from '../constants';
-import { dispatcherState } from '../../../../recoilModel';
-import { validatedDialogsSelector } from '../../../../recoilModel/selectors/validatedDialogs';
+import { dispatcherState, validateDialogSelectorFamily } from '../../../../recoilModel';
import { SelectItems } from './SelectItems';
@@ -30,8 +29,13 @@ const textFieldStyles = (focused: boolean) => ({
},
});
-const DescriptionColumn: React.FC = ({ id, displayName }: DialogInfo) => {
- const items = useRecoilValue(validatedDialogsSelector);
+interface DescriptionColumnProps extends DialogInfo {
+ projectId: string;
+}
+
+const DescriptionColumn: React.FC = (props) => {
+ const { id, displayName, projectId } = props;
+ const items = useRecoilValue(validateDialogSelectorFamily(projectId));
const { content } = items.find(({ id: dialogId }) => dialogId === id) || {};
const [value, setValue] = useState(content?.$designer?.description);
@@ -89,9 +93,11 @@ const DescriptionColumn: React.FC = ({ id, displayName }: DialogInfo
);
};
-export const SelectDialogs: React.FC = ({ setSelectedDialogs }) => {
- const dialogs = useRecoilValue(validatedDialogsSelector);
- const items = useMemo(() => dialogs.map(({ id, content, displayName }) => ({ id, content, displayName })), []);
+export const SelectDialogs: React.FC = ({ setSelectedDialogs, projectId }) => {
+ const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId));
+ const items = useMemo(() => dialogs.map(({ id, content, displayName }) => ({ id, content, displayName })), [
+ projectId,
+ ]);
// for detail file list in open panel
const tableColumns = useMemo(
@@ -123,11 +129,13 @@ export const SelectDialogs: React.FC = ({ setSelectedDialogs }) =>
isResizable: true,
isSortedDescending: false,
data: 'string',
- onRender: DescriptionColumn,
+ onRender: (item: DialogInfo) => {
+ return ;
+ },
isPadded: true,
},
],
- []
+ [projectId]
);
const selection = useMemo(
diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectTriggers.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectTriggers.tsx
index e86db6fe2e..2f0f8b220f 100644
--- a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectTriggers.tsx
+++ b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectTriggers.tsx
@@ -10,9 +10,9 @@ import { useRecoilValue } from 'recoil';
import formatMessage from 'format-message';
import { ContentProps } from '../constants';
-import { dialogsState, schemasState } from '../../../../recoilModel';
import { getFriendlyName } from '../../../../utils/dialogUtil';
import { isSupportedTrigger } from '../generateSkillManifest';
+import { dialogsState, schemasState } from '../../../../recoilModel';
import { SelectItems } from './SelectItems';
@@ -21,9 +21,9 @@ const getLabel = (kind: SDKKinds, uiSchema) => {
return label || kind.replace('Microsoft.', '');
};
-export const SelectTriggers: React.FC = ({ setSelectedTriggers }) => {
- const dialogs = useRecoilValue(dialogsState);
- const schemas = useRecoilValue(schemasState);
+export const SelectTriggers: React.FC = ({ setSelectedTriggers, projectId }) => {
+ const dialogs = useRecoilValue(dialogsState(projectId));
+ const schemas = useRecoilValue(schemasState(projectId));
const items = useMemo(() => {
const { triggers = [] } = dialogs.find(({ isRoot }) => isRoot) || ({} as DialogInfo);
diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx
index df49f376c0..55a418fd90 100644
--- a/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx
+++ b/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx
@@ -13,12 +13,12 @@ import { useRecoilValue } from 'recoil';
import { SkillManifest } from '@bfc/shared';
import {
- dialogSchemasState,
- dialogsState,
dispatcherState,
- luFilesState,
skillManifestsState,
qnaFilesState,
+ dialogsState,
+ dialogSchemasState,
+ luFilesState,
} from '../../../recoilModel';
import { editorSteps, ManifestEditorSteps, order } from './constants';
@@ -29,14 +29,15 @@ interface ExportSkillModalProps {
isOpen: boolean;
onDismiss: () => void;
onSubmit: () => void;
+ projectId: string;
}
-const ExportSkillModal: React.FC = ({ onSubmit, onDismiss: handleDismiss }) => {
- const dialogs = useRecoilValue(dialogsState);
- const dialogSchemas = useRecoilValue(dialogSchemasState);
- const luFiles = useRecoilValue(luFilesState);
- const qnaFiles = useRecoilValue(qnaFilesState);
- const skillManifests = useRecoilValue(skillManifestsState);
+const ExportSkillModal: React.FC = ({ onSubmit, onDismiss: handleDismiss, projectId }) => {
+ const dialogs = useRecoilValue(dialogsState(projectId));
+ const dialogSchemas = useRecoilValue(dialogSchemasState(projectId));
+ const luFiles = useRecoilValue(luFilesState(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesState(projectId));
+ const skillManifests = useRecoilValue(skillManifestsState(projectId));
const { updateSkillManifest } = useRecoilValue(dispatcherState);
const [editingId, setEditingId] = useState();
@@ -78,7 +79,7 @@ const ExportSkillModal: React.FC = ({ onSubmit, onDismiss
const handleSave = () => {
if (skillManifest.content && skillManifest.id) {
- updateSkillManifest(skillManifest as SkillManifest);
+ updateSkillManifest(skillManifest as SkillManifest, projectId);
}
};
@@ -129,6 +130,7 @@ const ExportSkillModal: React.FC = ({ onSubmit, onDismiss
editJson={handleEditJson}
errors={errors}
manifest={skillManifest}
+ projectId={projectId}
schema={schema}
setErrors={setErrors}
setSchema={setSchema}
diff --git a/Composer/packages/client/src/pages/home/Home.tsx b/Composer/packages/client/src/pages/home/Home.tsx
index 477d5f8541..334bf548cc 100644
--- a/Composer/packages/client/src/pages/home/Home.tsx
+++ b/Composer/packages/client/src/pages/home/Home.tsx
@@ -12,9 +12,13 @@ import { navigate } from '@reach/router';
import { useRecoilValue } from 'recoil';
import { CreationFlowStatus } from '../../constants';
-import { dispatcherState } from '../../recoilModel';
-import { botNameState, projectIdState } from '../../recoilModel/atoms/botState';
-import { recentProjectsState, templateProjectsState, templateIdState } from '../../recoilModel/atoms/appState';
+import { dispatcherState, botNameState } from '../../recoilModel';
+import {
+ recentProjectsState,
+ templateProjectsState,
+ templateIdState,
+ currentProjectIdState,
+} from '../../recoilModel/atoms/appState';
import { Toolbar, IToolbarItem } from '../../components/Toolbar';
import * as home from './styles';
@@ -58,17 +62,17 @@ const tutorials = [
const Home: React.FC = () => {
const templateProjects = useRecoilValue(templateProjectsState);
- const botName = useRecoilValue(botNameState);
+ const projectId = useRecoilValue(currentProjectIdState);
+ const botName = useRecoilValue(botNameState(projectId));
const recentProjects = useRecoilValue(recentProjectsState);
- const projectId = useRecoilValue(projectIdState);
const templateId = useRecoilValue(templateIdState);
- const { openBotProject, setCreationFlowStatus, onboardingAddCoachMarkRef, saveTemplateId } = useRecoilValue(
+ const { openProject, setCreationFlowStatus, onboardingAddCoachMarkRef, saveTemplateId } = useRecoilValue(
dispatcherState
);
const onItemChosen = async (item) => {
if (item && item.path) {
- openBotProject(item.path);
+ openProject(item.path);
}
};
@@ -164,7 +168,7 @@ const Home: React.FC = () => {
styles={home.latestBotItem}
title={''}
onClick={async () => {
- openBotProject(recentProjects[0].path);
+ openProject(recentProjects[0].path);
}}
/>
) : (
diff --git a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx
index 586e8bf833..c4b90d6390 100644
--- a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx
+++ b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx
@@ -15,7 +15,7 @@ import { navigateTo } from '../../utils/navigation';
import { TestController } from '../../components/TestController/TestController';
import { INavTreeItem } from '../../components/NavTree';
import { Page } from '../../components/Page';
-import { dialogsState, projectIdState, qnaAllUpViewStatusState } from '../../recoilModel/atoms/botState';
+import { botNameState, dialogsState, qnaAllUpViewStatusState } from '../../recoilModel/atoms/botState';
import { dispatcherState } from '../../recoilModel';
import { QnAAllUpViewStatus } from '../../recoilModel/types';
@@ -25,21 +25,23 @@ import { ImportQnAFromUrlModal } from './ImportQnAFromUrlModal';
const CodeEditor = React.lazy(() => import('./code-editor'));
interface QnAPageProps extends RouteComponentProps<{}> {
+ projectId?: string;
dialogId?: string;
}
const QnAPage: React.FC = (props) => {
+ const { dialogId = '', projectId = '' } = props;
const actions = useRecoilValue(dispatcherState);
- const dialogs = useRecoilValue(dialogsState);
- const projectId = useRecoilValue(projectIdState);
+ const dialogs = useRecoilValue(dialogsState(projectId));
+ const botName = useRecoilValue(botNameState(projectId));
//To do: support other languages
const locale = 'en-us';
//const locale = useRecoilValue(localeState);
- const qnaAllUpViewStatus = useRecoilValue(qnaAllUpViewStatusState);
+ const qnaAllUpViewStatus = useRecoilValue(qnaAllUpViewStatusState(projectId));
const [importQnAFromUrlModalVisiability, setImportQnAFromUrlModalVisiability] = useState(false);
const path = props.location?.pathname ?? '';
- const { dialogId = '' } = props;
+
const edit = /\/edit(\/)?$/.test(path);
const isRoot = dialogId === 'all';
const navLinks: INavTreeItem[] = useMemo(() => {
@@ -66,6 +68,13 @@ const QnAPage: React.FC = (props) => {
return newDialogLinks;
}, [dialogs]);
+ useEffect(() => {
+ const qnaKbUrls: string[] | undefined = props.location?.state?.qnaKbUrls;
+ if (qnaKbUrls && qnaKbUrls.length > 0) {
+ actions.importQnAFromUrls({ id: `${botName.toLocaleLowerCase()}.${locale}`, urls: qnaKbUrls, projectId });
+ }
+ }, []);
+
useEffect(() => {
const activeDialog = dialogs.find(({ id }) => id === dialogId);
if (!activeDialog && dialogs.length && dialogId !== 'all') {
@@ -106,7 +115,7 @@ const QnAPage: React.FC = (props) => {
},
{
type: 'element',
- element: ,
+ element: ,
align: 'right',
},
];
@@ -134,7 +143,7 @@ const QnAPage: React.FC = (props) => {
const onSubmit = async (urls: string[]) => {
onDismiss();
- await actions.importQnAFromUrls({ id: `${dialogId}.${locale}`, urls });
+ await actions.importQnAFromUrls({ id: `${dialogId}.${locale}`, urls, projectId });
};
return (
@@ -149,8 +158,10 @@ const QnAPage: React.FC = (props) => {
>
}>
-
- {qnaAllUpViewStatus !== QnAAllUpViewStatus.Loading && }
+
+ {qnaAllUpViewStatus !== QnAAllUpViewStatus.Loading && (
+
+ )}
{qnaAllUpViewStatus === QnAAllUpViewStatus.Loading && (
diff --git a/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx b/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx
index 685ab34573..bd246f8397 100644
--- a/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx
+++ b/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx
@@ -13,23 +13,24 @@ import get from 'lodash/get';
import { CodeEditorSettings } from '@bfc/shared';
import { QnAEditor } from '@bfc/code-editor';
-import { qnaFilesState, projectIdState } from '../../recoilModel/atoms/botState';
+import { qnaFilesState } from '../../recoilModel/atoms/botState';
import { dispatcherState } from '../../recoilModel';
import { userSettingsState } from '../../recoilModel';
interface CodeEditorProps extends RouteComponentProps<{}> {
dialogId: string;
+ projectId: string;
}
const lspServerPath = '/lu-language-server';
const CodeEditor: React.FC = (props) => {
+ const { projectId = '', dialogId = '' } = props;
const actions = useRecoilValue(dispatcherState);
- const qnaFiles = useRecoilValue(qnaFilesState);
+ const qnaFiles = useRecoilValue(qnaFilesState(projectId));
//To do: support other languages
const locale = 'en-us';
//const locale = useRecoilValue(localeState);
- const projectId = useRecoilValue(projectIdState);
const userSettings = useRecoilValue(userSettingsState);
- const { dialogId } = props;
+
const file = qnaFiles.find(({ id }) => id === `${dialogId}.${locale}`);
const hash = props.location?.hash ?? '';
const hashLine = querystring.parse(hash).L;
@@ -65,7 +66,7 @@ const CodeEditor: React.FC = (props) => {
const onChangeContent = useMemo(
() =>
debounce((newContent: string) => {
- actions.updateQnAFile({ id: `${dialogId}.${locale}`, content: newContent });
+ actions.updateQnAFile({ id: `${dialogId}.${locale}`, content: newContent, projectId });
}, 500),
[projectId]
);
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 1ec64c37be..33776ce19f 100644
--- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx
+++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx
@@ -29,7 +29,7 @@ import {
insertSection,
removeSection,
} from '../../utils/qnaUtil';
-import { dialogsState, qnaFilesState, projectIdState } from '../../recoilModel/atoms/botState';
+import { dialogsState, qnaFilesState } from '../../recoilModel/atoms/botState';
import { dispatcherState } from '../../recoilModel';
import {
@@ -50,6 +50,7 @@ import {
interface TableViewProps extends RouteComponentProps<{}> {
dialogId: string;
+ projectId: string;
}
enum EditMode {
@@ -59,14 +60,13 @@ enum EditMode {
}
const TableView: React.FC = (props) => {
+ const { dialogId = '', projectId = '' } = props;
const actions = useRecoilValue(dispatcherState);
- const dialogs = useRecoilValue(dialogsState);
- const qnaFiles = useRecoilValue(qnaFilesState);
- const projectId = useRecoilValue(projectIdState);
+ const dialogs = useRecoilValue(dialogsState(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesState(projectId));
//To do: support other languages
const locale = 'en-us';
- //const locale = useRecoilValue(localeState);
- const { dialogId } = props;
+
const file = qnaFiles.find(({ id }) => id === `${dialogId}.${locale}`);
const fileRef = useRef(file);
fileRef.current = file;
@@ -111,11 +111,19 @@ const TableView: React.FC = (props) => {
const createOrUpdateQuestion = () => {
if (editMode === EditMode.Creating && question) {
const updatedQnAFileContent = addQuestion(question, qnaSections, qnaSectionIndex);
- actions.updateQnAFile({ id: `${dialogIdRef.current}.${localeRef.current}`, content: updatedQnAFileContent });
+ actions.updateQnAFile({
+ id: `${dialogIdRef.current}.${localeRef.current}`,
+ content: updatedQnAFileContent,
+ projectId,
+ });
}
if (editMode === EditMode.Updating && qnaSections[qnaSectionIndex].Questions[questionIndex].content !== question) {
const updatedQnAFileContent = updateQuestion(question, questionIndex, qnaSections, qnaSectionIndex);
- actions.updateQnAFile({ id: `${dialogIdRef.current}.${localeRef.current}`, content: updatedQnAFileContent });
+ actions.updateQnAFile({
+ id: `${dialogIdRef.current}.${localeRef.current}`,
+ content: updatedQnAFileContent,
+ projectId,
+ });
}
cancelQuestionEditOperation();
};
@@ -123,7 +131,11 @@ const TableView: React.FC = (props) => {
const updateAnswer = () => {
if (editMode === EditMode.Updating && qnaSections[qnaSectionIndex].Answer !== answer) {
const updatedQnAFileContent = updateAnswerUtil(answer, qnaSections, qnaSectionIndex);
- actions.updateQnAFile({ id: `${dialogIdRef.current}.${localeRef.current}`, content: updatedQnAFileContent });
+ actions.updateQnAFile({
+ id: `${dialogIdRef.current}.${localeRef.current}`,
+ content: updatedQnAFileContent,
+ projectId,
+ });
}
cancelAnswerEditOperation();
};
@@ -248,6 +260,7 @@ const TableView: React.FC = (props) => {
actions.updateQnAFile({
id: `${dialogIdRef.current}.${localeRef.current}`,
content: updatedQnAFileContent,
+ projectId,
});
}
const newArray = [...showQnAPairDetails];
@@ -526,7 +539,7 @@ const TableView: React.FC = (props) => {
const newQnAPair = generateQnAPair();
const content = get(fileRef.current, 'content', '');
const newContent = insertSection(0, content, newQnAPair);
- actions.updateQnAFile({ id: `${dialogIdRef.current}.${localeRef.current}`, content: newContent });
+ actions.updateQnAFile({ id: `${dialogIdRef.current}.${localeRef.current}`, content: newContent, projectId });
const newArray = [false, ...showQnAPairDetails];
setShowQnAPairDetails(newArray);
};
diff --git a/Composer/packages/client/src/pages/language-generation/LGPage.tsx b/Composer/packages/client/src/pages/language-generation/LGPage.tsx
index 93b5279f6c..d786f33b01 100644
--- a/Composer/packages/client/src/pages/language-generation/LGPage.tsx
+++ b/Composer/packages/client/src/pages/language-generation/LGPage.tsx
@@ -9,28 +9,28 @@ import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
import { RouteComponentProps, Router } from '@reach/router';
import { useRecoilValue } from 'recoil';
-import { projectIdState } from '../../recoilModel/atoms/botState';
import { LoadingSpinner } from '../../components/LoadingSpinner';
import { actionButton } from '../language-understanding/styles';
import { navigateTo } from '../../utils/navigation';
import { TestController } from '../../components/TestController/TestController';
import { INavTreeItem } from '../../components/NavTree';
import { Page } from '../../components/Page';
-import { validatedDialogsSelector } from '../../recoilModel/selectors/validatedDialogs';
+import { validateDialogSelectorFamily } from '../../recoilModel';
import TableView from './table-view';
const CodeEditor = React.lazy(() => import('./code-editor'));
-interface LGPageProps extends RouteComponentProps<{}> {
- dialogId?: string;
+interface LGPageProps {
+ dialogId: string;
+ projectId: string;
}
-const LGPage: React.FC = (props) => {
- const dialogs = useRecoilValue(validatedDialogsSelector);
- const projectId = useRecoilValue(projectIdState);
+const LGPage: React.FC> = (props: RouteComponentProps) => {
+ const { dialogId = '', projectId = '' } = props;
+ const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId));
const path = props.location?.pathname ?? '';
- const { dialogId = '' } = props;
+
const edit = /\/edit(\/)?$/.test(path);
const navLinks: INavTreeItem[] = useMemo(() => {
@@ -85,7 +85,7 @@ const LGPage: React.FC = (props) => {
const toolbarItems = [
{
type: 'element',
- element: ,
+ element: ,
align: 'right',
},
];
@@ -115,8 +115,8 @@ const LGPage: React.FC = (props) => {
>
}>
-
-
+
+
diff --git a/Composer/packages/client/src/pages/language-generation/code-editor.tsx b/Composer/packages/client/src/pages/language-generation/code-editor.tsx
index 7ade29a10a..30e5ec6aba 100644
--- a/Composer/packages/client/src/pages/language-generation/code-editor.tsx
+++ b/Composer/packages/client/src/pages/language-generation/code-editor.tsx
@@ -15,7 +15,7 @@ import { CodeEditorSettings } from '@bfc/shared';
import { useRecoilValue } from 'recoil';
import { LgFile } from '@bfc/shared/src/types/indexers';
-import { localeState, lgFilesState, projectIdState, settingsState } from '../../recoilModel/atoms/botState';
+import { localeState, lgFilesState, settingsState } from '../../recoilModel/atoms/botState';
import { userSettingsState, dispatcherState } from '../../recoilModel';
import { DiffCodeEditor } from '../language-understanding/diff-editor';
@@ -23,14 +23,15 @@ const lspServerPath = '/lg-language-server';
interface CodeEditorProps extends RouteComponentProps<{}> {
dialogId: string;
+ projectId: string;
}
const CodeEditor: React.FC = (props) => {
+ const { dialogId, projectId } = props;
const userSettings = useRecoilValue(userSettingsState);
- const projectId = useRecoilValue(projectIdState);
- const locale = useRecoilValue(localeState);
- const lgFiles = useRecoilValue(lgFilesState);
- const settings = useRecoilValue(settingsState);
+ const locale = useRecoilValue(localeState(projectId));
+ const lgFiles = useRecoilValue(lgFilesState(projectId));
+ const settings = useRecoilValue(settingsState(projectId));
const { languages, defaultLanguage } = settings;
@@ -40,7 +41,7 @@ const CodeEditor: React.FC = (props) => {
updateUserSettings,
setLocale,
} = useRecoilValue(dispatcherState);
- const { dialogId } = props;
+
const file: LgFile | undefined = lgFiles.find(({ id }) => id === `${dialogId}.${locale}`);
const defaultLangFile = lgFiles.find(({ id }) => id === `${dialogId}.${defaultLanguage}`);
@@ -192,7 +193,7 @@ const CodeEditor: React.FC = (props) => {
left={currentLanguageFileEditor}
locale={locale}
right={defaultLanguageFileEditor}
- onLanguageChange={setLocale}
+ onLanguageChange={(locale) => setLocale(locale, projectId)}
>
)}
diff --git a/Composer/packages/client/src/pages/language-generation/table-view.tsx b/Composer/packages/client/src/pages/language-generation/table-view.tsx
index d7d37fae95..11dccf3da1 100644
--- a/Composer/packages/client/src/pages/language-generation/table-view.tsx
+++ b/Composer/packages/client/src/pages/language-generation/table-view.tsx
@@ -22,27 +22,33 @@ import { lgUtil } from '@bfc/indexers';
import { EditableField } from '../../components/EditableField';
import { navigateTo } from '../../utils/navigation';
import { actionButton, formCell } from '../language-understanding/styles';
-import { dispatcherState, lgFilesState, projectIdState, localeState, settingsState } from '../../recoilModel';
+import {
+ dispatcherState,
+ localeState,
+ lgFilesState,
+ settingsState,
+ validateDialogSelectorFamily,
+} from '../../recoilModel';
import { languageListTemplates } from '../../components/MultiLanguage';
-import { validatedDialogsSelector } from '../../recoilModel/selectors/validatedDialogs';
-interface TableViewProps extends RouteComponentProps<{}> {
+interface TableViewProps extends RouteComponentProps<{ dialogId: string; projectId: string }> {
dialogId: string;
+ projectId: string;
}
const TableView: React.FC = (props) => {
- const dialogs = useRecoilValue(validatedDialogsSelector);
- const lgFiles = useRecoilValue(lgFilesState);
- const projectId = useRecoilValue(projectIdState);
- const locale = useRecoilValue(localeState);
- const settings = useRecoilValue(settingsState);
- const { createLgTemplate, copyLgTemplate, removeLgTemplate, updateLgTemplate, setMessage } = useRecoilValue(
+ const { dialogId, projectId } = props;
+
+ const lgFiles = useRecoilValue(lgFilesState(projectId));
+ const locale = useRecoilValue(localeState(projectId));
+ const settings = useRecoilValue(settingsState(projectId));
+ const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId));
+ const { createLgTemplate, copyLgTemplate, removeLgTemplate, setMessage, updateLgTemplate } = useRecoilValue(
dispatcherState
);
const { languages, defaultLanguage } = settings;
- const { dialogId } = props;
const file = lgFiles.find(({ id }) => id === `${dialogId}.${locale}`);
const defaultLangFile = lgFiles.find(({ id }) => id === `${dialogId}.${defaultLanguage}`);
@@ -72,6 +78,7 @@ const TableView: React.FC = (props) => {
if (file) {
const newName = lgUtil.increaseNameUtilNotExist(file.templates, 'TemplateName');
const payload = {
+ projectId,
id: file.id,
template: {
name: newName,
@@ -89,6 +96,7 @@ const TableView: React.FC = (props) => {
const payload = {
id: file.id,
templateName: name,
+ projectId,
};
removeLgTemplate(payload);
setFocusedIndex(file.templates.findIndex((item) => item.name === name));
@@ -105,6 +113,7 @@ const TableView: React.FC = (props) => {
id: file.id,
fromTemplateName: name,
toTemplateName: resolvedName,
+ projectId,
};
copyLgTemplate(payload);
setFocusedIndex(file.templates.length);
@@ -120,6 +129,7 @@ const TableView: React.FC = (props) => {
id: file.id,
templateName,
template,
+ projectId,
};
updateLgTemplate(payload);
}
@@ -134,6 +144,7 @@ const TableView: React.FC = (props) => {
id: defaultLangFile.id,
templateName,
template,
+ projectId,
};
updateLgTemplate(payload);
}
diff --git a/Composer/packages/client/src/pages/language-understanding/LUPage.tsx b/Composer/packages/client/src/pages/language-understanding/LUPage.tsx
index 3f35a24024..4cad612016 100644
--- a/Composer/packages/client/src/pages/language-understanding/LUPage.tsx
+++ b/Composer/packages/client/src/pages/language-understanding/LUPage.tsx
@@ -11,26 +11,22 @@ import { useRecoilValue } from 'recoil';
import { navigateTo } from '../../utils/navigation';
import { LoadingSpinner } from '../../components/LoadingSpinner';
import { TestController } from '../../components/TestController/TestController';
-import { projectIdState } from '../../recoilModel/atoms/botState';
import { INavTreeItem } from '../../components/NavTree';
import { Page } from '../../components/Page';
-import { validatedDialogsSelector } from '../../recoilModel/selectors/validatedDialogs';
+import { validateDialogSelectorFamily } from '../../recoilModel';
import TableView from './table-view';
import { actionButton } from './styles';
const CodeEditor = React.lazy(() => import('./code-editor'));
-interface LUPageProps extends RouteComponentProps<{}> {
+const LUPage: React.FC = (props) => {
- const dialogs = useRecoilValue(validatedDialogsSelector);
- const projectId = useRecoilValue(projectIdState);
+ projectId: string;
+}>> = (props) => {
+ const { dialogId = '', projectId = '' } = props;
+ const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId));
const path = props.location?.pathname ?? '';
- const { dialogId = '' } = props;
const edit = /\/edit(\/)?$/.test(path);
const isRoot = dialogId === 'all';
@@ -81,7 +77,7 @@ const LUPage: React.FC = (props) => {
const toolbarItems = [
{
type: 'element',
- element: ,
+ element: ,
align: 'right',
},
];
@@ -115,8 +111,8 @@ const LUPage: React.FC = (props) => {
>
}>
-
-
+
+
diff --git a/Composer/packages/client/src/pages/language-understanding/code-editor.tsx b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx
index 0af29261ff..739dbd022d 100644
--- a/Composer/packages/client/src/pages/language-understanding/code-editor.tsx
+++ b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx
@@ -12,7 +12,7 @@ import querystring from 'query-string';
import { CodeEditorSettings } from '@bfc/shared';
import { useRecoilValue } from 'recoil';
-import { luFilesState, projectIdState, localeState, settingsState } from '../../recoilModel/atoms/botState';
+import { luFilesState, localeState, settingsState } from '../../recoilModel/atoms';
import { userSettingsState, dispatcherState } from '../../recoilModel';
import { DiffCodeEditor } from './diff-editor';
@@ -21,6 +21,7 @@ const lspServerPath = '/lu-language-server';
interface CodeEditorProps extends RouteComponentProps<{}> {
dialogId: string;
+ projectId: string;
}
const CodeEditor: React.FC = (props) => {
@@ -31,14 +32,13 @@ const CodeEditor: React.FC = (props) => {
updateUserSettings,
setLocale,
} = useRecoilValue(dispatcherState);
- const luFiles = useRecoilValue(luFilesState);
- const projectId = useRecoilValue(projectIdState);
- const locale = useRecoilValue(localeState);
- const settings = useRecoilValue(settingsState);
+ const { dialogId, projectId } = props;
+ const luFiles = useRecoilValue(luFilesState(projectId));
+ const locale = useRecoilValue(localeState(projectId));
+ const settings = useRecoilValue(settingsState(projectId));
const { languages, defaultLanguage } = settings;
- const { dialogId } = props;
const file = luFiles.find(({ id }) => id === `${dialogId}.${locale}`);
const defaultLangFile = luFiles.find(({ id }) => id === `${dialogId}.${defaultLanguage}`);
@@ -179,7 +179,9 @@ const CodeEditor: React.FC = (props) => {
left={currentLanguageFileEditor}
locale={locale}
right={defaultLanguageFileEditor}
- onLanguageChange={setLocale}
+ onLanguageChange={(locale) => {
+ setLocale(locale, projectId);
+ }}
>
)}
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 dbb453cee4..bdbf22bdfb 100644
--- a/Composer/packages/client/src/pages/language-understanding/table-view.tsx
+++ b/Composer/packages/client/src/pages/language-understanding/table-view.tsx
@@ -23,13 +23,19 @@ import { LuFile, LuIntentSection } from '@bfc/shared';
import { EditableField } from '../../components/EditableField';
import { getExtension } from '../../utils/fileUtil';
import { languageListTemplates } from '../../components/MultiLanguage';
-import { dispatcherState, luFilesState, projectIdState, localeState, settingsState } from '../../recoilModel';
import { navigateTo } from '../../utils/navigation';
-import { validatedDialogsSelector } from '../../recoilModel/selectors/validatedDialogs';
+import {
+ dispatcherState,
+ luFilesState,
+ localeState,
+ settingsState,
+ validateDialogSelectorFamily,
+} from '../../recoilModel';
import { formCell, luPhraseCell, tableCell } from './styles';
-interface TableViewProps extends RouteComponentProps<{}> {
+interface TableViewProps extends RouteComponentProps<{ dialogId: string; projectId: string }> {
dialogId: string;
+ projectId: string;
}
interface Intent {
@@ -42,15 +48,16 @@ interface Intent {
}
const TableView: React.FC = (props) => {
- const dialogs = useRecoilValue(validatedDialogsSelector);
- const luFiles = useRecoilValue(luFilesState);
- const projectId = useRecoilValue(projectIdState);
- const locale = useRecoilValue(localeState);
- const settings = useRecoilValue(settingsState);
+ const { dialogId, projectId } = props;
const { updateLuIntent } = useRecoilValue(dispatcherState);
+ const luFiles = useRecoilValue(luFilesState(projectId));
+ const locale = useRecoilValue(localeState(projectId));
+ const settings = useRecoilValue(settingsState(projectId));
+ const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId));
+
const { languages, defaultLanguage } = settings;
- const { dialogId } = props;
+
const activeDialog = dialogs.find(({ id }) => id === dialogId);
const file = luFiles.find(({ id }) => id === `${dialogId}.${locale}`);
@@ -109,6 +116,7 @@ const TableView: React.FC = (props) => {
id: fileId,
intentName,
intent,
+ projectId,
};
updateLuIntent(payload);
},
@@ -122,6 +130,7 @@ const TableView: React.FC = (props) => {
id: defaultLangFile.id,
intentName,
intent,
+ projectId,
};
updateLuIntent(payload);
}
diff --git a/Composer/packages/client/src/pages/notifications/Notifications.tsx b/Composer/packages/client/src/pages/notifications/Notifications.tsx
index 639adff70d..d449358bb5 100644
--- a/Composer/packages/client/src/pages/notifications/Notifications.tsx
+++ b/Composer/packages/client/src/pages/notifications/Notifications.tsx
@@ -16,9 +16,10 @@ import { NotificationHeader } from './NotificationHeader';
import { root } from './styles';
import { INotification, NotificationType } from './types';
-const Notifications: React.FC = () => {
+const Notifications: React.FC> = (props) => {
+ const { projectId = '' } = props;
const [filter, setFilter] = useState('');
- const notifications = useNotifications(filter);
+ const notifications = useNotifications(projectId, filter);
const navigations = {
[NotificationType.LG]: (item: INotification) => {
const { projectId, resourceId, diagnostic, dialogPath } = item;
diff --git a/Composer/packages/client/src/pages/notifications/useNotifications.tsx b/Composer/packages/client/src/pages/notifications/useNotifications.tsx
index 77d5383b26..bbe66112bd 100644
--- a/Composer/packages/client/src/pages/notifications/useNotifications.tsx
+++ b/Composer/packages/client/src/pages/notifications/useNotifications.tsx
@@ -7,16 +7,15 @@ import get from 'lodash/get';
import { BotIndexer } from '@bfc/indexers';
import {
+ validateDialogSelectorFamily,
luFilesState,
- qnaFilesState,
lgFilesState,
- projectIdState,
- BotDiagnosticsState,
+ botDiagnosticsState,
settingsState,
skillManifestsState,
dialogSchemasState,
-} from '../../recoilModel/atoms/botState';
-import { validatedDialogsSelector } from '../../recoilModel/selectors/validatedDialogs';
+ qnaFilesState,
+} from '../../recoilModel';
import {
Notification,
@@ -30,16 +29,16 @@ import {
} from './types';
import { getReferredLuFiles } from './../../utils/luUtil';
-export default function useNotifications(filter?: string) {
- const dialogs = useRecoilValue(validatedDialogsSelector);
- const luFiles = useRecoilValue(luFilesState);
- const qnaFiles = useRecoilValue(qnaFilesState);
- const projectId = useRecoilValue(projectIdState);
- const lgFiles = useRecoilValue(lgFilesState);
- const diagnostics = useRecoilValue(BotDiagnosticsState);
- const setting = useRecoilValue(settingsState);
- const skillManifests = useRecoilValue(skillManifestsState);
- const dialogSchemas = useRecoilValue(dialogSchemasState);
+export default function useNotifications(projectId: string, filter?: string) {
+ const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId));
+ const luFiles = useRecoilValue(luFilesState(projectId));
+ const lgFiles = useRecoilValue(lgFilesState(projectId));
+ const diagnostics = useRecoilValue(botDiagnosticsState(projectId));
+ const setting = useRecoilValue(settingsState(projectId));
+ const skillManifests = useRecoilValue(skillManifestsState(projectId));
+ const dialogSchemas = useRecoilValue(dialogSchemasState(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesState(projectId));
+
const botAssets = {
projectId,
dialogs,
@@ -50,6 +49,7 @@ export default function useNotifications(filter?: string) {
setting,
dialogSchemas,
};
+
const memoized = useMemo(() => {
const notifications: Notification[] = [];
diagnostics.forEach((d) => {
diff --git a/Composer/packages/client/src/pages/publish/Publish.tsx b/Composer/packages/client/src/pages/publish/Publish.tsx
index 1bebfa04a3..5edbfac6ed 100644
--- a/Composer/packages/client/src/pages/publish/Publish.tsx
+++ b/Composer/packages/client/src/pages/publish/Publish.tsx
@@ -14,12 +14,11 @@ import { useRecoilValue } from 'recoil';
import settingsStorage from '../../utils/dialogSettingStorage';
import { projectContainer } from '../design/styles';
import {
+ dispatcherState,
settingsState,
botNameState,
publishTypesState,
- projectIdState,
publishHistoryState,
- dispatcherState,
} from '../../recoilModel';
import { navigateTo } from '../../utils/navigation';
import { Toolbar, IToolbarItem } from '../../components/Toolbar';
@@ -31,18 +30,15 @@ import { ContentHeaderStyle, HeaderText, ContentStyle, contentEditor, overflowSe
import { CreatePublishTarget } from './createPublishTarget';
import { PublishStatusList, IStatus } from './publishStatusList';
-interface PublishPageProps extends RouteComponentProps<{}> {
- targetName?: string;
-}
-
-const Publish: React.FC = (props) => {
+const Publish: React.FC> = (props) => {
const selectedTargetName = props.targetName;
+ const { projectId = '' } = props;
const [selectedTarget, setSelectedTarget] = useState();
- const settings = useRecoilValue(settingsState);
- const botName = useRecoilValue(botNameState);
- const publishTypes = useRecoilValue(publishTypesState);
- const projectId = useRecoilValue(projectIdState);
- const publishHistory = useRecoilValue(publishHistoryState);
+ const settings = useRecoilValue(settingsState(projectId));
+ const botName = useRecoilValue(botNameState(projectId));
+ const publishTypes = useRecoilValue(publishTypesState(projectId));
+ const publishHistory = useRecoilValue(publishHistoryState(projectId));
+
const {
getPublishStatus,
getPublishTargetTypes,
@@ -171,7 +167,7 @@ const Publish: React.FC = (props) => {
useEffect(() => {
if (projectId) {
- getPublishTargetTypes();
+ getPublishTargetTypes(projectId);
// init selected status
setSelectedVersion(null);
}
@@ -248,7 +244,7 @@ const Publish: React.FC = (props) => {
configuration,
},
]);
- await setPublishTargets(targets);
+ await setPublishTargets(targets, projectId);
onSelectTarget(name);
},
[settings.publishTargets, projectId, botName]
@@ -268,7 +264,7 @@ const Publish: React.FC = (props) => {
configuration,
};
- await setPublishTargets(targets);
+ await setPublishTargets(targets, projectId);
onSelectTarget(name);
},
@@ -337,7 +333,7 @@ const Publish: React.FC = (props) => {
}
});
- await setPublishTargets(updatedPublishTargets);
+ await setPublishTargets(updatedPublishTargets, projectId);
}
},
[projectId, selectedTarget, settings.publishTargets]
@@ -363,7 +359,7 @@ const Publish: React.FC = (props) => {
if (result) {
if (settings.publishTargets && settings.publishTargets.length > index) {
const targets = settings.publishTargets.slice(0, index).concat(settings.publishTargets.slice(index + 1));
- await setPublishTargets(targets);
+ await setPublishTargets(targets, projectId);
// redirect to all profiles
setSelectedTarget(undefined);
onSelectTarget('all');
@@ -394,7 +390,12 @@ const Publish: React.FC = (props) => {
{editDialogProps.children}
{!publishDialogHidden && (
- setPublishDialogHidden(true)} onSubmit={publish} />
+ setPublishDialogHidden(true)}
+ onSubmit={publish}
+ />
)}
{showLog && setShowLog(false)} />}
diff --git a/Composer/packages/client/src/pages/setting/SettingsPage.tsx b/Composer/packages/client/src/pages/setting/SettingsPage.tsx
index 1a99fd822c..0ceee729f0 100644
--- a/Composer/packages/client/src/pages/setting/SettingsPage.tsx
+++ b/Composer/packages/client/src/pages/setting/SettingsPage.tsx
@@ -11,13 +11,13 @@ import { Text } from 'office-ui-fabric-react/lib/Text';
import { useRecoilValue } from 'recoil';
import {
- projectIdState,
+ dispatcherState,
localeState,
- showAddLanguageModalState,
showDelLanguageModalState,
+ showAddLanguageModalState,
settingsState,
-} from '../../recoilModel/atoms/botState';
-import { dispatcherState } from '../../recoilModel';
+ currentProjectIdState,
+} from '../../recoilModel';
import { TestController } from '../../components/TestController/TestController';
import { OpenConfirmModal } from '../../components/Modal/ConfirmDialog';
import { navigateTo } from '../../utils/navigation';
@@ -34,7 +34,8 @@ const getProjectLink = (path: string, id?: string) => {
return id ? `/settings/bot/${id}/${path}` : `/settings/${path}`;
};
-const SettingPage: React.FC> = () => {
+const SettingPage: React.FC = () => {
+ const projectId = useRecoilValue(currentProjectIdState);
const {
deleteBotProject,
addLanguageDialogBegin,
@@ -45,11 +46,12 @@ const SettingPage: React.FC> = () => {
deleteLanguages,
fetchProjectById,
} = useRecoilValue(dispatcherState);
- const projectId = useRecoilValue(projectIdState);
- const locale = useRecoilValue(localeState);
- const showAddLanguageModal = useRecoilValue(showAddLanguageModalState);
- const showDelLanguageModal = useRecoilValue(showDelLanguageModalState);
- const { defaultLanguage, languages } = useRecoilValue(settingsState);
+ const locale = useRecoilValue(localeState(projectId));
+ const showDelLanguageModal = useRecoilValue(showDelLanguageModalState(projectId));
+ const showAddLanguageModal = useRecoilValue(showAddLanguageModalState(projectId));
+ const settings = useRecoilValue(settingsState(projectId));
+ const { defaultLanguage, languages } = settings;
+
const { navigate } = useLocation();
// when fresh page, projectId in store are empty, no project are opened at client
@@ -61,14 +63,6 @@ const SettingPage: React.FC> = () => {
}
}, []);
- // If no project is open and user tries to access a bot-scoped settings (e.g., browser history, deep link)
- // Redirect them to the default settings route that is not bot-scoped
- useEffect(() => {
- if (!projectId && location.pathname.indexOf('/settings/bot/') !== -1) {
- navigate('/settings/application');
- }
- }, [projectId]);
-
const settingLabels = {
botSettings: formatMessage('Bot Settings'),
appSettings: formatMessage('Application Settings'),
@@ -90,6 +84,16 @@ const SettingPage: React.FC> = () => {
{ id: 'about', name: settingLabels.about, url: getProjectLink('about') },
];
+ // If no project is open and user tries to access a bot-scoped settings (e.g., browser history, deep link)
+ // Redirect them to the default settings route that is not bot-scoped
+ useEffect(() => {
+ if (!projectId && location.pathname.indexOf('/settings/bot/') !== -1) {
+ navigate('/settings/application');
+ } else {
+ navigate(links[0].url);
+ }
+ }, [projectId]);
+
const openDeleteBotModal = async () => {
const boldWarningText = formatMessage(
'Warning: the action you are about to take cannot be undone. Going further will delete this bot and any related files in the bot project folder.'
@@ -191,7 +195,7 @@ const SettingPage: React.FC> = () => {
key: 'edit.deleteLanguage',
text: formatMessage('Delete language'),
onClick: () => {
- delLanguageDialogBegin(() => {});
+ delLanguageDialogBegin(projectId, () => {});
},
},
],
@@ -206,7 +210,7 @@ const SettingPage: React.FC> = () => {
iconName: 'CirclePlus',
},
onClick: () => {
- addLanguageDialogBegin(() => {});
+ addLanguageDialogBegin(projectId, () => {});
},
},
align: 'left',
@@ -216,7 +220,7 @@ const SettingPage: React.FC> = () => {
{
type: 'element',
- element: ,
+ element: ,
align: 'right',
},
];
@@ -243,7 +247,7 @@ const SettingPage: React.FC> = () => {
isOpen={showAddLanguageModal}
languages={languages}
locale={locale}
- onDismiss={addLanguageDialogCancel}
+ onDismiss={() => addLanguageDialogCancel(projectId)}
onSubmit={onAddLangModalSubmit}
>
> = () => {
isOpen={showDelLanguageModal}
languages={languages}
locale={locale}
- onDismiss={delLanguageDialogCancel}
+ onDismiss={() => delLanguageDialogCancel(projectId)}
onSubmit={onDeleteLangModalSubmit}
>
diff --git a/Composer/packages/client/src/pages/setting/dialog-settings/DialogSettings.tsx b/Composer/packages/client/src/pages/setting/dialog-settings/DialogSettings.tsx
index d6b013f7ec..d4c781d095 100644
--- a/Composer/packages/client/src/pages/setting/dialog-settings/DialogSettings.tsx
+++ b/Composer/packages/client/src/pages/setting/dialog-settings/DialogSettings.tsx
@@ -14,24 +14,17 @@ import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
import cloneDeep from 'lodash/cloneDeep';
import { Label } from 'office-ui-fabric-react/lib/Label';
-import {
- botNameState,
- settingsState,
- projectIdState,
- dispatcherState,
- userSettingsState,
- localeState,
-} from '../../../recoilModel';
+import { dispatcherState, userSettingsState, botNameState, localeState, settingsState } from '../../../recoilModel';
import { languageListTemplates } from '../../../components/MultiLanguage';
import { settingsEditor, toolbar } from './style';
import { BotSettings } from './constants';
-export const DialogSettings: React.FC = () => {
- const botName = useRecoilValue(botNameState);
- const locale = useRecoilValue(localeState);
- const settings = useRecoilValue(settingsState);
- const projectId = useRecoilValue(projectIdState);
+export const DialogSettings: React.FC> = (props) => {
+ const { projectId = '' } = props;
+ const botName = useRecoilValue(botNameState(projectId));
+ const locale = useRecoilValue(localeState(projectId));
+ const settings = useRecoilValue(settingsState(projectId));
const userSettings = useRecoilValue(userSettingsState);
const { setSettings, setLocale, addLanguageDialogBegin } = useRecoilValue(dispatcherState);
@@ -60,7 +53,7 @@ export const DialogSettings: React.FC = () => {
) => {
const selectedLang = option?.key as string;
if (selectedLang && selectedLang !== defaultLanguage) {
- setLocale(selectedLang);
+ setLocale(selectedLang, projectId);
const updatedSetting = { ...cloneDeep(settings), defaultLanguage: selectedLang };
if (updatedSetting?.luis?.defaultLanguage) {
updatedSetting.luis.defaultLanguage = selectedLang;
@@ -72,7 +65,7 @@ export const DialogSettings: React.FC = () => {
const onLanguageChange = (_event: React.FormEvent, option?: IDropdownOption, _index?: number) => {
const selectedLang = option?.key as string;
if (selectedLang && selectedLang !== locale) {
- setLocale(selectedLang);
+ setLocale(selectedLang, projectId);
}
};
@@ -118,13 +111,7 @@ export const DialogSettings: React.FC = () => {
onChange={onLanguageChange}
/>
- {
- addLanguageDialogBegin(() => {});
- }}
- >
- {BotSettings.languageAddLanauge}
-
+ addLanguageDialogBegin(projectId, () => {})}>{BotSettings.languageAddLanauge}
= () => {
- const botName = useRecoilValue(botNameState);
- const settings = useRecoilValue(settingsState);
- const projectId = useRecoilValue(projectIdState);
+export const RuntimeSettings: React.FC> = (props) => {
+ const { projectId = '' } = props;
+ const botName = useRecoilValue(botNameState(projectId));
+ const settings = useRecoilValue(settingsState(projectId));
+ const ejectedRuntimeExists = useRecoilValue(isEjectRuntimeExistState(projectId));
+
const boilerplateVersion = useRecoilValue(boilerplateVersionState);
- const isEjectRuntimeExist = useRecoilValue(isEjectRuntimeExistState);
const {
setCustomRuntime,
setRuntimeField,
@@ -61,11 +61,11 @@ export const RuntimeSettings: React.FC = () => {
}, [boilerplateVersion.updateRequired]);
useEffect(() => {
- if (isEjectRuntimeExist && templateKey) {
+ if (ejectedRuntimeExists && templateKey) {
confirmReplaceEject(templateKey);
setTemplateKey('');
}
- }, [isEjectRuntimeExist, templateKey]);
+ }, [ejectedRuntimeExists, templateKey]);
const handleChangeToggle = (_, isOn = false) => {
setCustomRuntime(projectId, isOn);
diff --git a/Composer/packages/client/src/pages/skills/index.tsx b/Composer/packages/client/src/pages/skills/index.tsx
index 15d21378c0..901c0ac06e 100644
--- a/Composer/packages/client/src/pages/skills/index.tsx
+++ b/Composer/packages/client/src/pages/skills/index.tsx
@@ -9,7 +9,7 @@ import formatMessage from 'format-message';
import { useRecoilValue } from 'recoil';
import { Skill } from '@bfc/shared';
-import { botNameState, settingsState, projectIdState, dispatcherState } from '../../recoilModel';
+import { dispatcherState, settingsState, botNameState } from '../../recoilModel';
import { Toolbar, IToolbarItem } from '../../components/Toolbar';
import { TestController } from '../../components/TestController/TestController';
import { CreateSkillModal } from '../../components/CreateSkillModal';
@@ -18,12 +18,12 @@ import { ContainerStyle, ContentHeaderStyle, HeaderText } from './styles';
import SkillSettings from './skill-settings';
import SkillList from './skill-list';
-const Skills: React.FC = () => {
+const Skills: React.FC> = (props) => {
+ const { projectId = '' } = props;
const [showAddSkillDialogModal, setShowAddSkillDialogModal] = useState(false);
- const botName = useRecoilValue(botNameState);
- const settings = useRecoilValue(settingsState);
- const projectId = useRecoilValue(projectIdState);
+ const botName = useRecoilValue(botNameState(projectId));
+ const settings = useRecoilValue(settingsState(projectId));
const { addSkill, setSettings } = useRecoilValue(dispatcherState);
const toolbarItems: IToolbarItem[] = [
@@ -42,7 +42,7 @@ const Skills: React.FC = () => {
},
{
type: 'element',
- element: ,
+ element: ,
align: 'right',
},
];
diff --git a/Composer/packages/client/src/pages/skills/skill-list.tsx b/Composer/packages/client/src/pages/skills/skill-list.tsx
index 390b009efe..7d19e28522 100644
--- a/Composer/packages/client/src/pages/skills/skill-list.tsx
+++ b/Composer/packages/client/src/pages/skills/skill-list.tsx
@@ -121,7 +121,7 @@ interface SkillListProps {
const SkillList: React.FC = ({ projectId }) => {
const { removeSkill, updateSkill } = useRecoilValue(dispatcherState);
- const skills = useRecoilValue(skillsState);
+ const skills = useRecoilValue(skillsState(projectId));
const [selectedSkillUrl, setSelectedSkillUrl] = useState(null);
@@ -183,6 +183,7 @@ const SkillList: React.FC = ({ projectId }) => {
isDraggable={false}
isModeless={false}
manifestId={selectedSkillUrl}
+ projectId={projectId}
onDismiss={onDismissManifest}
/>
diff --git a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx
index d99fb0ec5d..055ec170e4 100644
--- a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx
+++ b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx
@@ -7,42 +7,43 @@ import { atom, useRecoilTransactionObserver_UNSTABLE, Snapshot, useRecoilState }
import once from 'lodash/once';
import React from 'react';
import { BotAssets } from '@bfc/shared';
+import { useRecoilValue } from 'recoil';
+import isEmpty from 'lodash/isEmpty';
+import { UndoRoot } from './undo/history';
import { prepareAxios } from './../utils/auth';
-import filePersistence from './persistence/FilePersistence';
import createDispatchers, { Dispatcher } from './dispatchers';
import {
+ botProjectsSpaceState,
dialogsState,
- dialogSchemasState,
- projectIdState,
luFilesState,
qnaFilesState,
+ lgFilesState,
skillManifestsState,
+ dialogSchemasState,
settingsState,
- lgFilesState,
+ filePersistenceState,
} from './atoms';
-import { UndoRoot } from './undo/history';
-const getBotAssets = async (snapshot: Snapshot): Promise => {
+const getBotAssets = async (projectId, snapshot: Snapshot): Promise => {
const result = await Promise.all([
- snapshot.getPromise(projectIdState),
- snapshot.getPromise(dialogsState),
- snapshot.getPromise(luFilesState),
- snapshot.getPromise(qnaFilesState),
- snapshot.getPromise(lgFilesState),
- snapshot.getPromise(skillManifestsState),
- snapshot.getPromise(settingsState),
- snapshot.getPromise(dialogSchemasState),
+ snapshot.getPromise(dialogsState(projectId)),
+ snapshot.getPromise(luFilesState(projectId)),
+ snapshot.getPromise(qnaFilesState(projectId)),
+ snapshot.getPromise(lgFilesState(projectId)),
+ snapshot.getPromise(skillManifestsState(projectId)),
+ snapshot.getPromise(settingsState(projectId)),
+ snapshot.getPromise(dialogSchemasState(projectId)),
]);
return {
- projectId: result[0],
- dialogs: result[1],
- luFiles: result[2],
- qnaFiles: result[3],
- lgFiles: result[4],
- skillManifests: result[5],
- setting: result[6],
- dialogSchemas: result[7],
+ projectId,
+ dialogs: result[0],
+ luFiles: result[1],
+ qnaFiles: result[2],
+ lgFiles: result[3],
+ skillManifests: result[4],
+ setting: result[5],
+ dialogSchemas: result[6],
};
};
@@ -82,16 +83,24 @@ const InitDispatcher = ({ onLoad }) => {
export const DispatcherWrapper = ({ children }) => {
const [loaded, setLoaded] = useState(false);
+ const botProjects = useRecoilValue(botProjectsSpaceState);
useRecoilTransactionObserver_UNSTABLE(async ({ snapshot, previousSnapshot }) => {
- const assets = await getBotAssets(snapshot);
- const previousAssets = await getBotAssets(previousSnapshot);
- filePersistence.notify(assets, previousAssets);
+ for (const projectId of botProjects) {
+ const assets = await getBotAssets(projectId, snapshot);
+ const previousAssets = await getBotAssets(projectId, previousSnapshot);
+ const filePersistence = await snapshot.getPromise(filePersistenceState(projectId));
+ if (!isEmpty(filePersistence)) {
+ filePersistence.notify(assets, previousAssets);
+ }
+ }
});
return (
-
+ {botProjects.map((projectId) => (
+
+ ))}
{loaded ? children : null}
diff --git a/Composer/packages/client/src/recoilModel/atoms/appState.ts b/Composer/packages/client/src/recoilModel/atoms/appState.ts
index fa09f6a144..3acebf35ef 100644
--- a/Composer/packages/client/src/recoilModel/atoms/appState.ts
+++ b/Composer/packages/client/src/recoilModel/atoms/appState.ts
@@ -156,3 +156,18 @@ export const extensionsState = atom({
key: getFullyQualifiedKey('extensions'),
default: [],
});
+
+export const botOpeningState = atom({
+ key: getFullyQualifiedKey('botOpening'),
+ default: false,
+});
+
+export const botProjectsSpaceState = atom({
+ key: getFullyQualifiedKey('botProjectsSpace'),
+ default: [],
+});
+
+export const currentProjectIdState = atom({
+ key: getFullyQualifiedKey('currentProjectId'),
+ default: '',
+});
diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts
index cd35d8c701..5880e99a3d 100644
--- a/Composer/packages/client/src/recoilModel/atoms/botState.ts
+++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-import { atom } from 'recoil';
+import { atomFamily } from 'recoil';
import {
DialogInfo,
DialogSchemaFile,
@@ -15,6 +15,7 @@ import {
} from '@bfc/shared';
import { BotLoadError, DesignPageLocation, QnAAllUpViewStatus } from '../../recoilModel/types';
+import FilePersistence from '../persistence/FilePersistence';
import { PublishType, BreadcrumbItem } from './../../recoilModel/types';
import { BotStatus } from './../../constants';
@@ -22,200 +23,234 @@ const getFullyQualifiedKey = (value: string) => {
return `Bot_${value}_State`;
};
-export const dialogsState = atom({
+export const dialogsState = atomFamily({
key: getFullyQualifiedKey('dialogs'),
- default: [],
+ default: (id) => {
+ return [];
+ },
});
-export const dialogSchemasState = atom({
- key: getFullyQualifiedKey('dialogSchema'),
- default: [],
+export const schemasState = atomFamily({
+ key: getFullyQualifiedKey('schemas'),
+ default: (id) => {
+ return {};
+ },
});
-export const projectIdState = atom({
- key: getFullyQualifiedKey('projectId'),
- default: '',
+export const dialogSchemasState = atomFamily({
+ key: getFullyQualifiedKey('dialogSchema'),
+ default: [],
});
-export const botNameState = atom({
+export const botNameState = atomFamily({
key: getFullyQualifiedKey('botName'),
- default: '',
+ default: (id) => {
+ return '';
+ },
});
-export const locationState = atom({
+export const locationState = atomFamily({
key: getFullyQualifiedKey('location'),
- default: '',
+ default: (id) => {
+ return '';
+ },
});
-export const botEnvironmentState = atom({
+export const botEnvironmentState = atomFamily({
key: getFullyQualifiedKey('botEnvironment'),
- default: 'production',
+ default: (id) => {
+ return 'production';
+ },
});
// current bot authoring language
-export const localeState = atom({
+export const localeState = atomFamily({
key: getFullyQualifiedKey('locale'),
- default: 'en-us',
+ default: (id) => {
+ return 'en-us';
+ },
});
-export const BotDiagnosticsState = atom({
- key: getFullyQualifiedKey('botDiagnostics'),
- default: [],
+export const botStatusState = atomFamily({
+ key: getFullyQualifiedKey('botStatus'),
+ default: (id) => {
+ return BotStatus.unConnected;
+ },
});
-export const botStatusState = atom({
- key: getFullyQualifiedKey('botStatus'),
- default: BotStatus.unConnected,
+export const botDiagnosticsState = atomFamily({
+ key: getFullyQualifiedKey('botDiagnostics'),
+ default: (id) => {
+ return [];
+ },
});
-export const botLoadErrorState = atom({
+export const botLoadErrorState = atomFamily({
key: getFullyQualifiedKey('botLoadErrorMsg'),
- default: { title: '', message: '' },
+ default: (id) => {
+ return { title: '', message: '' };
+ },
});
-export const lgFilesState = atom({
+export const lgFilesState = atomFamily({
key: getFullyQualifiedKey('lgFiles'),
- default: [],
+ default: (id) => {
+ return [];
+ },
});
-export const luFilesState = atom({
+export const luFilesState = atomFamily({
key: getFullyQualifiedKey('luFiles'),
- default: [],
-});
-
-export const qnaFilesState = atom({
- key: getFullyQualifiedKey('qnaFiles'),
- default: [],
-});
-
-export const schemasState = atom({
- key: getFullyQualifiedKey('schemas'),
- default: {},
+ default: (id) => {
+ return [];
+ },
});
-export const skillsState = atom({
+export const skillsState = atomFamily({
key: getFullyQualifiedKey('skills'),
- default: [],
+ default: (id) => {
+ return [];
+ },
});
-export const actionsSeedState = atom({
+export const actionsSeedState = atomFamily({
key: getFullyQualifiedKey('actionsSeed'),
- default: [],
+ default: (id) => {
+ return [];
+ },
});
-export const skillManifestsState = atom({
+export const skillManifestsState = atomFamily({
key: getFullyQualifiedKey('skillManifests'),
- default: [],
-});
-
-export const designPageLocationState = atom({
- key: getFullyQualifiedKey('designPageLocation'),
- default: {
- projectId: '',
- dialogId: '',
- focused: '',
- selected: '',
+ default: (id) => {
+ return [];
},
});
-export const breadcrumbState = atom({
+export const breadcrumbState = atomFamily({
key: getFullyQualifiedKey('breadcrumb'),
- default: [],
+ default: (id) => {
+ return [];
+ },
});
-export const showCreateDialogModalState = atom({
+export const showCreateDialogModalState = atomFamily({
key: getFullyQualifiedKey('showCreateDialogModal'),
- default: false,
+ default: (id) => {
+ return false;
+ },
});
-export const showAddSkillDialogModalState = atom({
+export const showAddSkillDialogModalState = atomFamily({
key: getFullyQualifiedKey('showAddSkillDialogModal'),
default: false,
});
-export const settingsState = atom({
+export const settingsState = atomFamily({
key: getFullyQualifiedKey('settings'),
default: { defaultLanguage: 'en-us', languages: ['en-us'], luis: {}, qna: {} } as DialogSetting,
});
-export const publishVersionsState = atom({
+export const publishVersionsState = atomFamily({
key: getFullyQualifiedKey('publishVersions'),
default: {},
});
-export const publishStatusState = atom({
+export const publishStatusState = atomFamily({
key: getFullyQualifiedKey('publishStatus'),
default: 'inactive',
});
-export const lastPublishChangeState = atom({
+export const lastPublishChangeState = atomFamily({
key: getFullyQualifiedKey('lastPublishChange'),
default: null,
});
-export const publishTypesState = atom({
+export const publishTypesState = atomFamily({
key: getFullyQualifiedKey('publishTypes'),
default: [],
});
-export const botOpeningState = atom({
- key: getFullyQualifiedKey('botOpening'),
- default: false,
-});
-
-export const publishHistoryState = atom({
+export const publishHistoryState = atomFamily({
key: getFullyQualifiedKey('publishHistory'),
default: {},
});
-export const onCreateDialogCompleteState = atom({
+export const onCreateDialogCompleteState = atomFamily({
key: getFullyQualifiedKey('onCreateDialogComplete'),
default: {
func: undefined,
},
});
-export const focusPathState = atom({
+export const focusPathState = atomFamily({
key: getFullyQualifiedKey('focusPath'),
default: '',
});
-export const onAddSkillDialogCompleteState = atom({
+export const onAddSkillDialogCompleteState = atomFamily({
key: getFullyQualifiedKey('onAddSkillDialogComplete'),
default: { func: undefined },
});
-export const displaySkillManifestState = atom({
+export const displaySkillManifestState = atomFamily({
key: getFullyQualifiedKey('displaySkillManifest'),
default: undefined,
});
-export const showAddLanguageModalState = atom({
+export const showAddLanguageModalState = atomFamily({
key: getFullyQualifiedKey('showAddLanguageModal'),
default: false,
});
-export const showDelLanguageModalState = atom({
+export const showDelLanguageModalState = atomFamily({
key: getFullyQualifiedKey('showDelLanguageModal'),
default: false,
});
-export const onAddLanguageDialogCompleteState = atom({
+export const onAddLanguageDialogCompleteState = atomFamily({
key: getFullyQualifiedKey('onAddLanguageDialogComplete'),
default: { func: undefined },
});
-export const onDelLanguageDialogCompleteState = atom({
+export const onDelLanguageDialogCompleteState = atomFamily({
key: getFullyQualifiedKey('onDelLanguageDialogComplete'),
default: { func: undefined },
});
-export const qnaAllUpViewStatusState = atom({
+export const projectMetaDataState = atomFamily({
+ key: getFullyQualifiedKey('projectsMetaDataState'),
+ default: (id) => {
+ return {};
+ },
+});
+
+export const designPageLocationState = atomFamily({
+ key: getFullyQualifiedKey('designPageLocation'),
+ default: {
+ dialogId: '',
+ focused: '',
+ selected: '',
+ },
+});
+
+export const qnaAllUpViewStatusState = atomFamily({
key: getFullyQualifiedKey('qnaAllUpViewStatusState'),
default: QnAAllUpViewStatus.Success,
});
-export const isEjectRuntimeExistState = atom({
+export const isEjectRuntimeExistState = atomFamily({
key: getFullyQualifiedKey('isEjectRuntimeExist'),
default: false,
});
+
+export const qnaFilesState = atomFamily({
+ key: getFullyQualifiedKey('qnaFiles'),
+ default: [],
+});
+
+export const filePersistenceState = atomFamily({
+ key: getFullyQualifiedKey('filePersistence'),
+ default: {} as FilePersistence,
+ dangerouslyAllowMutability: true,
+});
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 55afe574fa..6e07c5aca1 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialog.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialog.test.tsx
@@ -12,13 +12,16 @@ import {
lgFilesState,
luFilesState,
schemasState,
+ dialogSchemasState,
actionsSeedState,
onCreateDialogCompleteState,
showCreateDialogModalState,
- dialogSchemasState,
qnaFilesState,
} from '../../atoms';
import { dispatcherState } from '../../../recoilModel/DispatcherWrapper';
+import { Dispatcher } from '..';
+
+const projectId = '42345.23432';
jest.mock('@bfc/indexers', () => {
return {
@@ -87,18 +90,19 @@ jest.mock('../../parsers/qnaWorker', () => {
});
describe('dialog dispatcher', () => {
- let renderedComponent, dispatcher;
+ let renderedComponent, dispatcher: Dispatcher;
beforeEach(() => {
const useRecoilTestHook = () => {
- const dialogs = useRecoilValue(dialogsState);
- const dialogSchemas = useRecoilValue(dialogSchemasState);
- const luFiles = useRecoilValue(luFilesState);
- const lgFiles = useRecoilValue(lgFilesState);
+ const dialogs = useRecoilValue(dialogsState(projectId));
+ const dialogSchemas = useRecoilValue(dialogSchemasState(projectId));
+ const luFiles = useRecoilValue(luFilesState(projectId));
+ const lgFiles = useRecoilValue(lgFilesState(projectId));
+ const actionsSeed = useRecoilValue(actionsSeedState(projectId));
+ const onCreateDialogComplete = useRecoilValue(onCreateDialogCompleteState(projectId));
+ const showCreateDialogModal = useRecoilValue(showCreateDialogModalState(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesState(projectId));
const currentDispatcher = useRecoilValue(dispatcherState);
- const actionsSeed = useRecoilValue(actionsSeedState);
- const onCreateDialogComplete = useRecoilValue(onCreateDialogCompleteState);
- const showCreateDialogModal = useRecoilValue(showCreateDialogModalState);
- const qnaFiles = useRecoilValue(qnaFilesState);
+
return {
dialogs,
dialogSchemas,
@@ -114,11 +118,11 @@ describe('dialog dispatcher', () => {
const { result } = renderRecoilHook(useRecoilTestHook, {
states: [
- { recoilState: dialogsState, initialValue: [{ id: '1' }, { id: '2' }] },
- { recoilState: dialogSchemasState, initialValue: [{ id: '1' }, { id: '2' }] },
- { recoilState: lgFilesState, initialValue: [{ id: '1.lg' }, { id: '2' }] },
- { recoilState: luFilesState, initialValue: [{ id: '1.lu' }, { id: '2' }] },
- { recoilState: schemasState, initialValue: { sdk: { content: '' } } },
+ { recoilState: dialogsState(projectId), initialValue: [{ id: '1' }, { id: '2' }] },
+ { recoilState: dialogSchemasState(projectId), initialValue: [{ id: '1' }, { id: '2' }] },
+ { recoilState: lgFilesState(projectId), initialValue: [{ id: '1.lg' }, { id: '2' }] },
+ { recoilState: luFilesState(projectId), initialValue: [{ id: '1.lu' }, { id: '2' }] },
+ { recoilState: schemasState(projectId), initialValue: { sdk: { content: '' } } },
],
dispatcher: {
recoilState: dispatcherState,
@@ -133,8 +137,12 @@ describe('dialog dispatcher', () => {
it('removes a dialog file', async () => {
await act(async () => {
- await dispatcher.removeDialog('1');
+ await dispatcher.createDialog({ id: '1', content: 'abcde', projectId });
});
+ await act(async () => {
+ await dispatcher.removeDialog('1', projectId);
+ });
+
expect(renderedComponent.current.dialogs).toEqual([{ id: '2' }]);
expect(renderedComponent.current.dialogSchemas).toEqual([{ id: '2' }]);
expect(renderedComponent.current.lgFiles).toEqual([{ id: '2' }]);
@@ -144,14 +152,14 @@ describe('dialog dispatcher', () => {
it('updates a dialog file', async () => {
test.validateDialog = jest.fn().mockReturnValue([]);
await act(async () => {
- await dispatcher.updateDialog({ id: '1', content: 'new' });
+ dispatcher.updateDialog({ id: '1', content: 'new', projectId });
});
expect(renderedComponent.current.dialogs.find((dialog) => dialog.id === '1').content).toEqual('new');
});
it('creates a dialog file', async () => {
await act(async () => {
- await dispatcher.createDialog({ id: '100', content: 'abcde' });
+ await dispatcher.createDialog({ id: '100', content: 'abcde', projectId });
});
expect(renderedComponent.current.luFiles.find((dialog) => dialog.id === '100.en-us')).not.toBeNull();
expect(renderedComponent.current.lgFiles.find((dialog) => dialog.id === '100.en-us')).not.toBeNull();
@@ -164,8 +172,9 @@ describe('dialog dispatcher', () => {
const ON_COMPLETE = { action: 'moreStuff' };
await act(async () => {
- await dispatcher.createDialogBegin({ actions: ACTIONS }, ON_COMPLETE);
+ dispatcher.createDialogBegin({ actions: ACTIONS }, ON_COMPLETE, projectId);
});
+
expect(renderedComponent.current.actionsSeed).toEqual({ actions: ACTIONS });
expect(renderedComponent.current.onCreateDialogComplete).toEqual({ func: ON_COMPLETE });
expect(renderedComponent.current.showCreateDialogModal).toBe(true);
@@ -173,7 +182,7 @@ describe('dialog dispatcher', () => {
it('cancels creating a dialog', async () => {
await act(async () => {
- await dispatcher.createDialogCancel();
+ await dispatcher.createDialogCancel(projectId);
});
expect(renderedComponent.current.actionsSeed).toEqual([]);
expect(renderedComponent.current.onCreateDialogComplete).toEqual({ func: undefined });
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialogSchema.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialogSchema.test.tsx
index a5e8f5d3d3..6a5070e9dd 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialogSchema.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialogSchema.test.tsx
@@ -6,14 +6,17 @@ import { act } from '@bfc/test-utils/lib/hooks';
import { dialogSchemaDispatcher } from '../dialogSchema';
import { renderRecoilHook } from '../../../../__tests__/testUtils';
-import { dialogSchemasState } from '../../atoms';
+import { dialogSchemasState, currentProjectIdState } from '../../atoms';
import { dispatcherState } from '../../../recoilModel/DispatcherWrapper';
+import { Dispatcher } from '..';
+
+const projectId = '42345.23432';
describe('dialog schema dispatcher', () => {
- let renderedComponent, dispatcher;
+ let renderedComponent, dispatcher: Dispatcher;
beforeEach(() => {
const useRecoilTestHook = () => {
- const dialogSchemas = useRecoilValue(dialogSchemasState);
+ const dialogSchemas = useRecoilValue(dialogSchemasState(projectId));
const currentDispatcher = useRecoilValue(dispatcherState);
return {
@@ -23,7 +26,10 @@ describe('dialog schema dispatcher', () => {
};
const { result } = renderRecoilHook(useRecoilTestHook, {
- states: [{ recoilState: dialogSchemasState, initialValue: [{ id: '1' }, { id: '2' }] }],
+ states: [
+ { recoilState: dialogSchemasState(projectId), initialValue: [{ id: '1' }, { id: '2' }] },
+ { recoilState: currentProjectIdState, initialValue: projectId },
+ ],
dispatcher: {
recoilState: dispatcherState,
initialValue: {
@@ -37,14 +43,14 @@ describe('dialog schema dispatcher', () => {
it('updates a dialog schema file', async () => {
await act(async () => {
- await dispatcher.updateDialogSchema({ id: '1', content: 'new' });
+ await dispatcher.updateDialogSchema({ id: '1', content: 'new' }, projectId);
});
expect(renderedComponent.current.dialogSchemas.find((dialog) => dialog.id === '1').content).toEqual('new');
});
it('creates a dialog schema file', async () => {
await act(async () => {
- await dispatcher.updateDialogSchema({ id: '100', content: 'abcde' });
+ await dispatcher.updateDialogSchema({ id: '100', content: 'abcde' }, projectId);
});
expect(renderedComponent.current.dialogSchemas.find((dialogSchema) => dialogSchema.id === '100').content).toEqual(
'abcde'
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/export.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/export.test.tsx
index e539e333fc..c02ff75ea7 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/export.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/export.test.tsx
@@ -7,11 +7,12 @@ import { act } from '@bfc/test-utils/lib/hooks';
import httpClient from '../../../utils/httpUtil';
import { exportDispatcher } from '../export';
import { renderRecoilHook } from '../../../../__tests__/testUtils';
-import { botNameState } from '../../atoms';
+import { botNameState, currentProjectIdState } from '../../atoms';
import { dispatcherState } from '../../../recoilModel/DispatcherWrapper';
-import { Dispatcher } from '..';
+import { Dispatcher } from '../../../recoilModel/dispatchers';
jest.mock('../../../utils/httpUtil');
+const projectId = '2345.32324';
describe('Export dispatcher', () => {
let renderedComponent, dispatcher: Dispatcher, prevDocumentCreateElement, prevCreateObjectURL, prevAppendChild;
@@ -21,7 +22,7 @@ describe('Export dispatcher', () => {
prevAppendChild = document.body.appendChild;
const useRecoilTestHook = () => {
- const botName = useRecoilValue(botNameState);
+ const botName = useRecoilValue(botNameState(projectId));
const currentDispatcher = useRecoilValue(dispatcherState);
return {
botName,
@@ -30,7 +31,10 @@ describe('Export dispatcher', () => {
};
const { result } = renderRecoilHook(useRecoilTestHook, {
- states: [{ recoilState: botNameState, initialValue: 'emptybot-1' }],
+ states: [
+ { recoilState: currentProjectIdState, initialValue: projectId },
+ { recoilState: botNameState(projectId), initialValue: 'emptybot-1' },
+ ],
dispatcher: {
recoilState: dispatcherState,
initialValue: {
@@ -62,7 +66,7 @@ describe('Export dispatcher', () => {
return '';
});
- const createElement = (element) => {
+ const createElement = () => {
return {
click: elementClick,
setAttribute: setAttributeMock,
@@ -75,7 +79,7 @@ describe('Export dispatcher', () => {
});
act(() => {
- dispatcher.exportToZip({ projectId: '1234-232' });
+ dispatcher.exportToZip(projectId);
});
});
});
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 54e9d6a39a..8d2f221ae7 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lg.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lg.test.tsx
@@ -8,10 +8,12 @@ import { act } from '@bfc/test-utils/lib/hooks';
import { lgDispatcher } from '../lg';
import { renderRecoilHook } from '../../../../__tests__/testUtils';
-import { lgFilesState, projectIdState } from '../../atoms';
+import { lgFilesState, currentProjectIdState } from '../../atoms';
import { dispatcherState } from '../../../recoilModel/DispatcherWrapper';
import { Dispatcher } from '..';
+const projectId = '123asad.123sad';
+
jest.mock('../../parsers/lgWorker', () => {
const filterParseResult = (lgFile: LgFile) => {
const cloned = { ...lgFile };
@@ -53,7 +55,7 @@ describe('Lg dispatcher', () => {
let renderedComponent, dispatcher: Dispatcher;
beforeEach(() => {
const useRecoilTestHook = () => {
- const [lgFiles, setLgFiles] = useRecoilState(lgFilesState);
+ const [lgFiles, setLgFiles] = useRecoilState(lgFilesState(projectId));
const currentDispatcher = useRecoilValue(dispatcherState);
return {
@@ -65,8 +67,8 @@ describe('Lg dispatcher', () => {
const { result } = renderRecoilHook(useRecoilTestHook, {
states: [
- { recoilState: lgFilesState, initialValue: lgFiles },
- { recoilState: projectIdState, initialValue: 'test' },
+ { recoilState: lgFilesState(projectId), initialValue: lgFiles },
+ { recoilState: currentProjectIdState, initialValue: projectId },
],
dispatcher: {
recoilState: dispatcherState,
@@ -84,6 +86,7 @@ describe('Lg dispatcher', () => {
await dispatcher.createLgTemplate({
id: 'common.en-us',
template: getLgTemplate('Test', '-add'),
+ projectId,
});
});
@@ -92,7 +95,7 @@ describe('Lg dispatcher', () => {
it('should update a lg file', async () => {
await act(async () => {
- await dispatcher.updateLgFile({ id: 'common.en-us', content: `test` });
+ await dispatcher.updateLgFile({ id: 'common.en-us', content: `test`, projectId });
});
expect(renderedComponent.current.lgFiles[0].content).toBe(`test`);
@@ -104,6 +107,7 @@ describe('Lg dispatcher', () => {
id: 'common.en-us',
templateName: 'Hello',
template: getLgTemplate('Hello', '-TemplateValue'),
+ projectId,
});
});
@@ -115,6 +119,7 @@ describe('Lg dispatcher', () => {
await dispatcher.removeLgTemplate({
id: 'common.en-us',
templateName: 'Hello',
+ projectId,
});
});
@@ -126,6 +131,7 @@ describe('Lg dispatcher', () => {
await dispatcher.removeLgTemplates({
id: 'common.en-us',
templateNames: ['Hello'],
+ projectId,
});
});
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 319e8430a8..dd7f2df8f8 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lu.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/lu.test.tsx
@@ -8,7 +8,7 @@ import { act } from '@bfc/test-utils/lib/hooks';
import { luUtil } from '@bfc/indexers';
import { renderRecoilHook } from '../../../../__tests__/testUtils';
-import { luFilesState } from '../../atoms';
+import { luFilesState, currentProjectIdState } from '../../atoms';
import { dispatcherState } from '../../../recoilModel/DispatcherWrapper';
import { Dispatcher } from '..';
import { luDispatcher } from '../lu';
@@ -23,7 +23,7 @@ jest.mock('../../parsers/luWorker', () => {
removeIntents: require('@bfc/indexers/lib/utils/luUtil').removeIntents,
};
});
-
+const projectId = '123ansd.23432';
const file1 = {
id: 'common.en-us',
content: `\r\n# Hello\r\n-hi`,
@@ -41,7 +41,7 @@ describe('Lu dispatcher', () => {
let renderedComponent, dispatcher: Dispatcher;
beforeEach(() => {
const useRecoilTestHook = () => {
- const [luFiles, setLuFiles] = useRecoilState(luFilesState);
+ const [luFiles, setLuFiles] = useRecoilState(luFilesState(projectId));
const currentDispatcher = useRecoilValue(dispatcherState);
return {
@@ -52,7 +52,10 @@ describe('Lu dispatcher', () => {
};
const { result } = renderRecoilHook(useRecoilTestHook, {
- states: [{ recoilState: luFilesState, initialValue: luFiles }],
+ states: [
+ { recoilState: luFilesState(projectId), initialValue: luFiles },
+ { recoilState: currentProjectIdState, initialValue: projectId },
+ ],
dispatcher: {
recoilState: dispatcherState,
initialValue: {
@@ -68,7 +71,7 @@ describe('Lu dispatcher', () => {
await act(async () => {
await dispatcher.updateLuFile({
id: 'common.en-us',
- projectId: 'test',
+ projectId,
content: `\r\n# New\r\n-new`,
});
});
@@ -82,6 +85,7 @@ describe('Lu dispatcher', () => {
id: luFiles[0].id,
intentName: 'Hello',
intent: getLuIntent('Hello', '-IntentValue'),
+ projectId,
});
});
@@ -93,7 +97,7 @@ describe('Lu dispatcher', () => {
await dispatcher.createLuIntent({
id: luFiles[0].id,
intent: getLuIntent('New', '-IntentValue'),
- projectId: '',
+ projectId,
});
});
expect(renderedComponent.current.luFiles[0].content).toMatch(/-IntentValue/);
@@ -104,7 +108,7 @@ describe('Lu dispatcher', () => {
await dispatcher.removeLuIntent({
id: luFiles[0].id,
intentName: 'Hello',
- projectId: '',
+ projectId,
});
});
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 4a52915537..2383f43e87 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/multilang.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/multilang.test.tsx
@@ -11,9 +11,10 @@ import {
settingsState,
dialogsState,
localeState,
+ actionsSeedState,
onAddLanguageDialogCompleteState,
onDelLanguageDialogCompleteState,
- actionsSeedState,
+ currentProjectIdState,
} from '../../atoms';
import { dispatcherState } from '../../../recoilModel/DispatcherWrapper';
import { Dispatcher } from '..';
@@ -34,21 +35,23 @@ const state = {
defaultLanguage: 'en-us',
languages: ['en-us', 'fr-fr'],
},
+ projectId: '1234-abcd',
};
describe('Multilang dispatcher', () => {
let renderedComponent, dispatcher: Dispatcher;
beforeEach(() => {
const useRecoilTestHook = () => {
- const dialogs = useRecoilValue(dialogsState);
- const locale = useRecoilValue(localeState);
- const settings = useRecoilValue(settingsState);
- const luFiles = useRecoilValue(luFilesState);
- const lgFiles = useRecoilValue(lgFilesState);
+ const actionsSeed = useRecoilValue(actionsSeedState(state.projectId));
+ const dialogs = useRecoilValue(dialogsState(state.projectId));
+ const locale = useRecoilValue(localeState(state.projectId));
+ const settings = useRecoilValue(settingsState(state.projectId));
+ const luFiles = useRecoilValue(luFilesState(state.projectId));
+ const lgFiles = useRecoilValue(lgFilesState(state.projectId));
+ const onAddLanguageDialogComplete = useRecoilValue(onAddLanguageDialogCompleteState(state.projectId));
+ const onDelLanguageDialogComplete = useRecoilValue(onDelLanguageDialogCompleteState(state.projectId));
+
const currentDispatcher = useRecoilValue(dispatcherState);
- const actionsSeed = useRecoilValue(actionsSeedState);
- const onAddLanguageDialogComplete = useRecoilValue(onAddLanguageDialogCompleteState);
- const onDelLanguageDialogComplete = useRecoilValue(onDelLanguageDialogCompleteState);
return {
dialogs,
@@ -65,11 +68,12 @@ describe('Multilang dispatcher', () => {
const { result } = renderRecoilHook(useRecoilTestHook, {
states: [
- { recoilState: dialogsState, initialValue: state.dialogs },
- { recoilState: localeState, initialValue: state.locale },
- { recoilState: lgFilesState, initialValue: state.lgFiles },
- { recoilState: luFilesState, initialValue: state.luFiles },
- { recoilState: settingsState, initialValue: state.settings },
+ { recoilState: currentProjectIdState, initialValue: state.projectId },
+ { recoilState: dialogsState(state.projectId), initialValue: state.dialogs },
+ { recoilState: localeState(state.projectId), initialValue: state.locale },
+ { recoilState: lgFilesState(state.projectId), initialValue: state.lgFiles },
+ { recoilState: luFilesState(state.projectId), initialValue: state.luFiles },
+ { recoilState: settingsState(state.projectId), initialValue: state.settings },
],
dispatcher: {
recoilState: dispatcherState,
@@ -88,6 +92,7 @@ describe('Multilang dispatcher', () => {
languages: ['zh-cn'],
defaultLang: 'en-us',
switchTo: true,
+ projectId: state.projectId,
});
});
expect(renderedComponent.current.settings.languages).toEqual(['en-us', 'fr-fr', 'zh-cn']);
@@ -102,6 +107,7 @@ describe('Multilang dispatcher', () => {
await act(async () => {
await dispatcher.deleteLanguages({
languages: ['fr-fr'],
+ projectId: state.projectId,
});
});
expect(renderedComponent.current.settings.languages).toEqual(['en-us']);
@@ -111,7 +117,7 @@ describe('Multilang dispatcher', () => {
it('set locale', async () => {
await act(async () => {
- await dispatcher.setLocale('fr-fr');
+ await dispatcher.setLocale('fr-fr', state.projectId);
});
expect(renderedComponent.current.locale).toEqual('fr-fr');
});
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/navigation.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/navigation.test.tsx
index e9aba285c1..44385b6fc4 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/navigation.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/navigation.test.tsx
@@ -7,14 +7,9 @@ import { SDKKinds } from '@bfc/shared';
import { navigationDispatcher } from '../navigation';
import { renderRecoilHook } from '../../../../__tests__/testUtils';
-import {
- focusPathState,
- breadcrumbState,
- designPageLocationState,
- projectIdState,
- dialogsState,
-} from '../../atoms/botState';
+import { focusPathState, breadcrumbState, designPageLocationState, dialogsState } from '../../atoms/botState';
import { dispatcherState } from '../../../recoilModel/DispatcherWrapper';
+import { Dispatcher } from '../../../recoilModel/dispatchers';
import {
convertPathToUrl,
navigateTo,
@@ -25,6 +20,7 @@ import {
} from '../../../utils/navigation';
import { createSelectedPath, getSelected } from '../../../utils/dialogUtil';
import { BreadcrumbItem } from '../../../recoilModel/types';
+import { currentProjectIdState } from '../../atoms';
jest.mock('../../../utils/navigation');
jest.mock('../../../utils/dialogUtil');
@@ -37,14 +33,14 @@ const mockGetUrlSearch = getUrlSearch as jest.Mock;
const mockConvertPathToUrl = convertPathToUrl as jest.Mock;
const mockCreateSelectedPath = createSelectedPath as jest.Mock;
-const PROJECT_ID = '12345.678';
+const projectId = '12345.678';
function expectNavTo(location: string, state: {} | null = null) {
expect(mockNavigateTo).toHaveBeenLastCalledWith(location, state == null ? expect.anything() : state);
}
describe('navigation dispatcher', () => {
- let renderedComponent, dispatcher;
+ let renderedComponent, dispatcher: Dispatcher;
beforeEach(() => {
mockCheckUrl.mockClear();
mockNavigateTo.mockClear();
@@ -55,12 +51,11 @@ describe('navigation dispatcher', () => {
mockCheckUrl.mockReturnValue(false);
const useRecoilTestHook = () => {
- const focusPath = useRecoilValue(focusPathState);
- const breadcrumb = useRecoilValue(breadcrumbState);
- const designPageLocation = useRecoilValue(designPageLocationState);
- const projectId = useRecoilValue(projectIdState);
+ const focusPath = useRecoilValue(focusPathState(projectId));
+ const breadcrumb = useRecoilValue(breadcrumbState(projectId));
+ const designPageLocation = useRecoilValue(designPageLocationState(projectId));
+ const dialogs = useRecoilValue(dialogsState(projectId));
const currentDispatcher = useRecoilValue(dispatcherState);
- const dialogs = useRecoilValue(dialogsState);
return {
dialogs,
@@ -74,20 +69,19 @@ describe('navigation dispatcher', () => {
const { result } = renderRecoilHook(useRecoilTestHook, {
states: [
- { recoilState: focusPathState, initialValue: 'path' },
- { recoilState: breadcrumbState, initialValue: [{ dialogId: '100', selected: 'a', focused: 'b' }] },
+ { recoilState: focusPathState(projectId), initialValue: 'path' },
+ { recoilState: breadcrumbState(projectId), initialValue: [{ dialogId: '100', selected: 'a', focused: 'b' }] },
{
- recoilState: designPageLocationState,
+ recoilState: designPageLocationState(projectId),
initialValue: {
- projectId: PROJECT_ID,
dialogId: 'dialogId',
selected: 'a',
focused: 'b',
},
},
- { recoilState: projectIdState, initialValue: PROJECT_ID },
+ { recoilState: currentProjectIdState, initialValue: projectId },
{
- recoilState: dialogsState,
+ recoilState: dialogsState(projectId),
initialValue: [{ id: 'newDialogId', triggers: [{ type: SDKKinds.OnBeginDialog }] }],
},
],
@@ -106,10 +100,10 @@ describe('navigation dispatcher', () => {
describe('sets the design page location', () => {
it('with no focus or selection', async () => {
await act(async () => {
- await dispatcher.setDesignPageLocation({
- projectId: 'projectId',
+ await dispatcher.setDesignPageLocation(projectId, {
dialogId: 'dialogId',
breadcrumb: [],
+ promptTab: undefined,
});
});
expect(renderedComponent.current.focusPath).toEqual('dialogId#');
@@ -120,7 +114,6 @@ describe('navigation dispatcher', () => {
selected: '',
});
expect(renderedComponent.current.designPageLocation).toEqual({
- projectId: 'projectId',
dialogId: 'dialogId',
promptTab: undefined,
focused: '',
@@ -130,11 +123,11 @@ describe('navigation dispatcher', () => {
it('with selection', async () => {
await act(async () => {
- await dispatcher.setDesignPageLocation({
- projectId: 'projectId',
+ await dispatcher.setDesignPageLocation(projectId, {
dialogId: 'dialogId',
breadcrumb: [],
selected: 'select',
+ promptTab: undefined,
});
});
expect(renderedComponent.current.focusPath).toEqual('dialogId#.select');
@@ -145,7 +138,6 @@ describe('navigation dispatcher', () => {
selected: 'select',
});
expect(renderedComponent.current.designPageLocation).toEqual({
- projectId: 'projectId',
dialogId: 'dialogId',
promptTab: undefined,
focused: '',
@@ -155,12 +147,12 @@ describe('navigation dispatcher', () => {
it('with focus overriding selection', async () => {
await act(async () => {
- await dispatcher.setDesignPageLocation({
- projectId: 'projectId',
+ await dispatcher.setDesignPageLocation(projectId, {
dialogId: 'dialogId',
breadcrumb: [],
focused: 'focus',
selected: 'select',
+ promptTab: undefined,
});
});
expect(renderedComponent.current.focusPath).toEqual('dialogId#.focus');
@@ -171,7 +163,6 @@ describe('navigation dispatcher', () => {
selected: 'select',
});
expect(renderedComponent.current.designPageLocation).toEqual({
- projectId: 'projectId',
dialogId: 'dialogId',
promptTab: undefined,
focused: 'focus',
@@ -182,29 +173,29 @@ describe('navigation dispatcher', () => {
describe('navTo', () => {
it('navigates to a destination', async () => {
- mockConvertPathToUrl.mockReturnValue(`/bot/${PROJECT_ID}/dialogs/dialogId`);
+ mockConvertPathToUrl.mockReturnValue(`/bot/${projectId}/dialogs/dialogId`);
await act(async () => {
- await dispatcher.navTo('dialogId', []);
+ await dispatcher.navTo(projectId, 'dialogId', []);
});
- expectNavTo(`/bot/${PROJECT_ID}/dialogs/dialogId`);
- expect(mockConvertPathToUrl).toBeCalledWith(PROJECT_ID, 'dialogId', undefined);
+ expectNavTo(`/bot/${projectId}/dialogs/dialogId`);
+ expect(mockConvertPathToUrl).toBeCalledWith(projectId, 'dialogId', undefined);
});
it('redirects to the begin dialog trigger', async () => {
- mockConvertPathToUrl.mockReturnValue(`/bot/${PROJECT_ID}/dialogs/newDialogId?selection=triggers[0]`);
+ mockConvertPathToUrl.mockReturnValue(`/bot/${projectId}/dialogs/newDialogId?selection=triggers[0]`);
mockCreateSelectedPath.mockReturnValue('triggers[0]');
await act(async () => {
- await dispatcher.navTo('newDialogId', []);
+ await dispatcher.navTo(projectId, 'newDialogId', []);
});
- expectNavTo(`/bot/${PROJECT_ID}/dialogs/newDialogId?selection=triggers[0]`);
- expect(mockConvertPathToUrl).toBeCalledWith(PROJECT_ID, 'newDialogId', 'triggers[0]');
+ expectNavTo(`/bot/${projectId}/dialogs/newDialogId?selection=triggers[0]`);
+ expect(mockConvertPathToUrl).toBeCalledWith(projectId, 'newDialogId', 'triggers[0]');
expect(mockCreateSelectedPath).toBeCalledWith(0);
});
it("doesn't navigate to a destination where we already are", async () => {
mockCheckUrl.mockReturnValue(true);
await act(async () => {
- await dispatcher.navTo('dialogId', []);
+ await dispatcher.navTo(projectId, 'dialogId', []);
});
expect(mockNavigateTo).not.toBeCalled();
});
@@ -213,24 +204,24 @@ describe('navigation dispatcher', () => {
describe('selectTo', () => {
it("doesn't go anywhere without a selection", async () => {
await act(async () => {
- await dispatcher.selectTo('');
+ await dispatcher.selectTo(projectId, '');
});
expect(mockNavigateTo).not.toBeCalled();
});
it('navigates to a default URL with selected path', async () => {
- mockConvertPathToUrl.mockReturnValue(`/bot/${PROJECT_ID}/dialogs/dialogId?selected=selection`);
+ mockConvertPathToUrl.mockReturnValue(`/bot/${projectId}/dialogs/dialogId?selected=selection`);
await act(async () => {
- await dispatcher.selectTo('selection');
+ await dispatcher.selectTo(projectId, 'selection');
});
- expectNavTo(`/bot/${PROJECT_ID}/dialogs/dialogId?selected=selection`);
- expect(mockConvertPathToUrl).toBeCalledWith(PROJECT_ID, 'dialogId', 'selection');
+ expectNavTo(`/bot/${projectId}/dialogs/dialogId?selected=selection`);
+ expect(mockConvertPathToUrl).toBeCalledWith(projectId, 'dialogId', 'selection');
});
it("doesn't go anywhere if we're already there", async () => {
mockCheckUrl.mockReturnValue(true);
await act(async () => {
- await dispatcher.selectTo('selection');
+ await dispatcher.selectTo(projectId, 'selection');
});
expect(mockNavigateTo).not.toBeCalled();
});
@@ -239,17 +230,17 @@ describe('navigation dispatcher', () => {
describe('focusTo', () => {
it('goes to the same page with no arguments', async () => {
await act(async () => {
- await dispatcher.focusTo();
+ await dispatcher.focusTo(projectId, '', '');
});
- expectNavTo(`/bot/${PROJECT_ID}/dialogs/dialogId?selected=a`);
+ expectNavTo(`/bot/${projectId}/dialogs/dialogId?selected=a`);
});
it('goes to a focused page', async () => {
mockGetSelected.mockReturnValueOnce('select');
await act(async () => {
- await dispatcher.focusTo('focus');
+ await dispatcher.focusTo(projectId, 'focus', '');
});
- expectNavTo(`/bot/${PROJECT_ID}/dialogs/dialogId?selected=select&focused=focus`);
+ expectNavTo(`/bot/${projectId}/dialogs/dialogId?selected=select&focused=focus`);
expect(mockUpdateBreadcrumb).toHaveBeenCalledWith(expect.anything(), BreadcrumbUpdateType.Selected);
expect(mockUpdateBreadcrumb).toHaveBeenCalledWith(expect.anything(), BreadcrumbUpdateType.Focused);
});
@@ -257,9 +248,9 @@ describe('navigation dispatcher', () => {
it('goes to a focused page with fragment', async () => {
mockGetSelected.mockReturnValueOnce('select');
await act(async () => {
- await dispatcher.focusTo('focus', 'fragment');
+ await dispatcher.focusTo(projectId, 'focus', 'fragment');
});
- expectNavTo(`/bot/${PROJECT_ID}/dialogs/dialogId?selected=select&focused=focus#fragment`);
+ expectNavTo(`/bot/${projectId}/dialogs/dialogId?selected=select&focused=focus#fragment`);
expect(mockUpdateBreadcrumb).toHaveBeenCalledWith(expect.anything(), BreadcrumbUpdateType.Selected);
expect(mockUpdateBreadcrumb).toHaveBeenCalledWith(expect.anything(), BreadcrumbUpdateType.Focused);
});
@@ -268,7 +259,7 @@ describe('navigation dispatcher', () => {
mockCheckUrl.mockReturnValue(true);
mockGetSelected.mockReturnValueOnce('select');
await act(async () => {
- await dispatcher.focusTo('focus', 'fragment');
+ await dispatcher.focusTo(projectId, 'focus', 'fragment');
});
expect(mockNavigateTo).not.toBeCalled();
expect(mockUpdateBreadcrumb).toHaveBeenCalledWith(expect.anything(), BreadcrumbUpdateType.Selected);
@@ -280,15 +271,15 @@ describe('navigation dispatcher', () => {
it('sets selection and focus with a valud search', async () => {
mockGetUrlSearch.mockReturnValue('?foo=bar&baz=quux');
await act(async () => {
- await dispatcher.selectAndFocus('dialogId', 'select', 'focus');
+ await dispatcher.selectAndFocus(projectId, 'dialogId', 'select', 'focus');
});
- expectNavTo(`/bot/${PROJECT_ID}/dialogs/dialogId?foo=bar&baz=quux`);
+ expectNavTo(`/bot/${projectId}/dialogs/dialogId?foo=bar&baz=quux`);
});
it("doesn't go anywhere if we're already there", async () => {
mockCheckUrl.mockReturnValue(true);
await act(async () => {
- await dispatcher.selectAndFocus('dialogId', 'select', 'focus');
+ await dispatcher.selectAndFocus(projectId, 'dialogId', 'select', 'focus');
});
expect(mockNavigateTo).not.toBeCalled();
});
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 ba641fc691..b572b7f35d 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/project.test.tsx
@@ -9,31 +9,31 @@ import httpClient from '../../../utils/httpUtil';
import { projectDispatcher } from '../project';
import { renderRecoilHook } from '../../../../__tests__/testUtils';
import {
+ recentProjectsState,
+ applicationErrorState,
+ templateIdState,
+ announcementState,
+ boilerplateVersionState,
+ templateProjectsState,
+ runtimeTemplatesState,
+ currentProjectIdState,
skillManifestsState,
luFilesState,
lgFilesState,
settingsState,
dialogsState,
botEnvironmentState,
- botNameState,
- botStatusState,
+ botDiagnosticsState,
+ localeState,
+ schemasState,
locationState,
skillsState,
- schemasState,
- localeState,
- BotDiagnosticsState,
botOpeningState,
- projectIdState,
- recentProjectsState,
- applicationErrorState,
- templateIdState,
- announcementState,
- boilerplateVersionState,
- templateProjectsState,
- runtimeTemplatesState,
+ botStatusState,
+ botNameState,
} from '../../atoms';
import { dispatcherState } from '../../../recoilModel/DispatcherWrapper';
-import { Dispatcher } from '..';
+import { Dispatcher } from '../../dispatchers';
import { BotStatus } from '../../../constants';
import mockProjectResponse from './mocks/mockProjectResponse.json';
@@ -41,6 +41,8 @@ import mockProjectResponse from './mocks/mockProjectResponse.json';
// let httpMocks;
let navigateTo;
+const projectId = '30876.502871204648';
+
jest.mock('../../../utils/navigation', () => {
const navigateMock = jest.fn();
navigateTo = navigateMock;
@@ -65,28 +67,28 @@ jest.mock('../../parsers/luWorker', () => {
});
jest.mock('../../persistence/FilePersistence', () => {
- return {
- flush: () => new Promise((resolve) => resolve()),
- };
+ return jest.fn().mockImplementation(() => {
+ return { flush: () => new Promise((resolve) => resolve()) };
+ });
});
describe('Project dispatcher', () => {
const useRecoilTestHook = () => {
+ const schemas = useRecoilValue(schemasState(projectId));
+ const location = useRecoilValue(locationState(projectId));
+ const skills = useRecoilValue(skillsState(projectId));
+ const botName = useRecoilValue(botNameState(projectId));
+ const skillManifests = useRecoilValue(skillManifestsState(projectId));
+ const luFiles = useRecoilValue(luFilesState(projectId));
+ const lgFiles = useRecoilValue(lgFilesState(projectId));
+ const settings = useRecoilValue(settingsState(projectId));
+ const dialogs = useRecoilValue(dialogsState(projectId));
+ const botEnvironment = useRecoilValue(botEnvironmentState(projectId));
+ const diagnostics = useRecoilValue(botDiagnosticsState(projectId));
+ const locale = useRecoilValue(localeState(projectId));
+ const botStatus = useRecoilValue(botStatusState(projectId));
+
const botOpening = useRecoilValue(botOpeningState);
- const skillManifests = useRecoilValue(skillManifestsState);
- const luFiles = useRecoilValue(luFilesState);
- const lgFiles = useRecoilValue(lgFilesState);
- const settings = useRecoilValue(settingsState);
- const dialogs = useRecoilValue(dialogsState);
- const botEnvironment = useRecoilValue(botEnvironmentState);
- const botName = useRecoilValue(botNameState);
- const botStatus = useRecoilValue(botStatusState);
- const skills = useRecoilValue(skillsState);
- const location = useRecoilValue(locationState);
- const schemas = useRecoilValue(schemasState);
- const diagnostics = useRecoilValue(BotDiagnosticsState);
- const projectId = useRecoilValue(projectIdState);
- const locale = useRecoilValue(localeState);
const currentDispatcher = useRecoilValue(dispatcherState);
const [recentProjects, setRecentProjects] = useRecoilState(recentProjectsState);
const appError = useRecoilValue(applicationErrorState);
@@ -131,7 +133,7 @@ describe('Project dispatcher', () => {
const rendered: RenderHookResult> = renderRecoilHook(
useRecoilTestHook,
{
- states: [],
+ states: [{ recoilState: currentProjectIdState, initialValue: projectId }],
dispatcher: {
recoilState: dispatcherState,
initialValue: {
@@ -150,8 +152,9 @@ describe('Project dispatcher', () => {
data: mockProjectResponse,
});
await act(async () => {
- result = await dispatcher.openBotProject('../test/empty-bot', 'default');
+ result = await dispatcher.openProject('../test/empty-bot', 'default');
});
+
expect(renderedComponent.current.projectId).toBe(mockProjectResponse.id);
expect(renderedComponent.current.botName).toBe(mockProjectResponse.botName);
expect(renderedComponent.current.settings).toStrictEqual(mockProjectResponse.settings);
@@ -163,7 +166,7 @@ describe('Project dispatcher', () => {
expect(renderedComponent.current.schemas.sdk).toBeDefined();
expect(renderedComponent.current.schemas.default).toBeDefined();
expect(renderedComponent.current.schemas.diagnostics?.length).toBe(0);
- expect(navigateTo).toHaveBeenLastCalledWith('/bot/30876.502871204648/dialogs/');
+ expect(navigateTo).toHaveBeenLastCalledWith(`/bot/${projectId}/dialogs/`);
expect(result).toBe(renderedComponent.current.projectId);
});
@@ -179,7 +182,7 @@ describe('Project dispatcher', () => {
path: '../test/empty-bot',
},
]);
- await dispatcher.openBotProject('../test/empty-bot', 'default');
+ await dispatcher.openProject('../test/empty-bot', 'default');
});
expect(renderedComponent.current.botOpening).toBeFalsy();
expect(renderedComponent.current.appError).toEqual(errorObj);
@@ -233,13 +236,11 @@ describe('Project dispatcher', () => {
data: mockProjectResponse,
});
await act(async () => {
- await dispatcher.openBotProject('../test/empty-bot', 'default');
- await dispatcher.deleteBotProject('30876.502871204648');
+ await dispatcher.openProject('../test/empty-bot', 'default');
+ await dispatcher.deleteBotProject(projectId);
});
expect(renderedComponent.current.botName).toEqual('');
- expect(renderedComponent.current.projectId).toBe('');
- 7;
expect(renderedComponent.current.locale).toBe('en-us');
expect(renderedComponent.current.lgFiles.length).toBe(0);
expect(renderedComponent.current.luFiles.length).toBe(0);
@@ -253,7 +254,7 @@ describe('Project dispatcher', () => {
it('should set bot status', async () => {
await act(async () => {
- await dispatcher.setBotStatus(BotStatus.pending);
+ await dispatcher.setBotStatus(BotStatus.pending, projectId);
});
expect(renderedComponent.current.botStatus).toEqual(BotStatus.pending);
@@ -267,10 +268,10 @@ describe('Project dispatcher', () => {
expect(renderedComponent.current.templateId).toEqual('EchoBot');
});
- it('should update bolierplate', async () => {
+ it('should update boilerplate', async () => {
httpClient.get as jest.Mock;
await act(async () => {
- await dispatcher.updateBoilerplate('30876.502871204648');
+ await dispatcher.updateBoilerplate(projectId);
});
expect(renderedComponent.current.announcement).toEqual('Scripts successfully updated.');
@@ -282,7 +283,7 @@ describe('Project dispatcher', () => {
data: version,
});
await act(async () => {
- await dispatcher.getBoilerplateVersion('30876.502871204648');
+ await dispatcher.getBoilerplateVersion(projectId);
});
expect(renderedComponent.current.boilerplateVersion).toEqual(version);
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/setting.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/setting.test.tsx
index f74adb9a0d..c2f11a5151 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/setting.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/setting.test.tsx
@@ -5,11 +5,13 @@ import { useRecoilValue } from 'recoil';
import { act } from '@bfc/test-utils/lib/hooks';
import { renderRecoilHook } from '../../../../__tests__/testUtils';
-import { settingsState } from '../../atoms';
+import { settingsState, currentProjectIdState } from '../../atoms';
import { dispatcherState } from '../../../recoilModel/DispatcherWrapper';
import { Dispatcher } from '..';
import { settingsDispatcher } from '../setting';
+const projectId = '1235a.2341';
+
const settings = {
feature: {
UseShowTypingMiddleware: false,
@@ -69,7 +71,7 @@ describe('setting dispatcher', () => {
let renderedComponent, dispatcher: Dispatcher;
beforeEach(() => {
const useRecoilTestHook = () => {
- const settings = useRecoilValue(settingsState);
+ const settings = useRecoilValue(settingsState(projectId));
const currentDispatcher = useRecoilValue(dispatcherState);
return {
@@ -79,7 +81,10 @@ describe('setting dispatcher', () => {
};
const { result } = renderRecoilHook(useRecoilTestHook, {
- states: [{ recoilState: settingsState, initialValue: settings }],
+ states: [
+ { recoilState: settingsState(projectId), initialValue: settings },
+ { recoilState: currentProjectIdState, initialValue: projectId },
+ ],
dispatcher: {
recoilState: dispatcherState,
initialValue: {
@@ -93,7 +98,7 @@ describe('setting dispatcher', () => {
it('should update all settings', async () => {
await act(async () => {
- await dispatcher.setSettings('test', {
+ await dispatcher.setSettings(projectId, {
...settings,
MicrosoftAppPassword: 'test',
luis: { ...settings.luis, authoringKey: 'test', endpointKey: 'test' },
@@ -106,14 +111,17 @@ describe('setting dispatcher', () => {
it('should update PublishTargets', async () => {
await act(async () => {
- await dispatcher.setPublishTargets([
- {
- name: 'new',
- type: 'type',
- configuration: '',
- lastPublished: new Date(),
- },
- ]);
+ await dispatcher.setPublishTargets(
+ [
+ {
+ name: 'new',
+ type: 'type',
+ configuration: '',
+ lastPublished: new Date(),
+ },
+ ],
+ projectId
+ );
});
expect(renderedComponent.current.settings.publishTargets.length).toBe(1);
@@ -122,7 +130,7 @@ describe('setting dispatcher', () => {
it('should update RuntimeSettings', async () => {
await act(async () => {
- await dispatcher.setRuntimeSettings('', { path: 'path', command: 'command', key: 'key', name: 'name' });
+ await dispatcher.setRuntimeSettings(projectId, { path: 'path', command: 'command', key: 'key', name: 'name' });
});
expect(renderedComponent.current.settings.runtime.customRuntime).toBeTruthy();
@@ -134,12 +142,12 @@ describe('setting dispatcher', () => {
it('should update customRuntime', async () => {
await act(async () => {
- await dispatcher.setCustomRuntime('', false);
+ await dispatcher.setCustomRuntime(projectId, false);
});
expect(renderedComponent.current.settings.runtime.customRuntime).toBeFalsy();
await act(async () => {
- await dispatcher.setCustomRuntime('', true);
+ await dispatcher.setCustomRuntime(projectId, true);
});
expect(renderedComponent.current.settings.runtime.customRuntime).toBeTruthy();
});
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/skill.test.ts b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/skill.test.ts
index b029dfd340..39d6078de4 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/skill.test.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/skill.test.ts
@@ -14,9 +14,10 @@ import {
settingsState,
showAddSkillDialogModalState,
displaySkillManifestState,
- projectIdState,
} from '../../atoms/botState';
import { dispatcherState } from '../../DispatcherWrapper';
+import { currentProjectIdState } from '../../atoms';
+import { Dispatcher } from '..';
jest.mock('../../../utils/httpUtil', () => {
return {
@@ -31,6 +32,7 @@ jest.mock('../../../utils/httpUtil', () => {
});
const mockDialogComplete = jest.fn();
+const projectId = '42345.23432';
const makeTestSkill: (number) => Skill = (n) => ({
manifestUrl: 'url' + n,
@@ -44,18 +46,18 @@ const makeTestSkill: (number) => Skill = (n) => ({
});
describe('skill dispatcher', () => {
- let renderedComponent, dispatcher;
+ let renderedComponent, dispatcher: Dispatcher;
beforeEach(() => {
mockDialogComplete.mockClear();
const useRecoilTestHook = () => {
- const projectId = useRecoilValue(projectIdState);
- const skillManifests = useRecoilValue(skillManifestsState);
- const onAddSkillDialogComplete = useRecoilValue(onAddSkillDialogCompleteState);
- const skills: Skill[] = useRecoilValue(skillsState);
- const settings = useRecoilValue(settingsState);
- const showAddSkillDialogModal = useRecoilValue(showAddSkillDialogModalState);
- const displaySkillManifest = useRecoilValue(displaySkillManifestState);
+ const projectId = useRecoilValue(currentProjectIdState);
+ const skillManifests = useRecoilValue(skillManifestsState(projectId));
+ const onAddSkillDialogComplete = useRecoilValue(onAddSkillDialogCompleteState(projectId));
+ const skills: Skill[] = useRecoilValue(skillsState(projectId));
+ const settings = useRecoilValue(settingsState(projectId));
+ const showAddSkillDialogModal = useRecoilValue(showAddSkillDialogModalState(projectId));
+ const displaySkillManifest = useRecoilValue(displaySkillManifestState(projectId));
const currentDispatcher = useRecoilValue(dispatcherState);
@@ -74,21 +76,21 @@ describe('skill dispatcher', () => {
const { result } = renderRecoilHook(useRecoilTestHook, {
states: [
{
- recoilState: skillManifestsState,
+ recoilState: skillManifestsState(projectId),
initialValue: [
{ id: 'id1', content: 'content1' },
{ id: 'id2', content: 'content2' },
],
},
- { recoilState: onAddSkillDialogCompleteState, initialValue: { func: undefined } },
+ { recoilState: onAddSkillDialogCompleteState(projectId), initialValue: { func: undefined } },
{
- recoilState: skillsState,
+ recoilState: skillsState(projectId),
initialValue: [makeTestSkill(1), makeTestSkill(2)],
},
- { recoilState: settingsState, initialValue: {} },
- { recoilState: showAddSkillDialogModalState, initialValue: false },
- { recoilState: displaySkillManifestState, initialValue: undefined },
- { recoilState: projectIdState, initialValue: '123' },
+ { recoilState: settingsState(projectId), initialValue: {} },
+ { recoilState: showAddSkillDialogModalState(projectId), initialValue: false },
+ { recoilState: displaySkillManifestState(projectId), initialValue: undefined },
+ { recoilState: currentProjectIdState, initialValue: projectId },
],
dispatcher: {
recoilState: dispatcherState,
@@ -104,7 +106,7 @@ describe('skill dispatcher', () => {
it('createSkillManifest', async () => {
await act(async () => {
- dispatcher.createSkillManifest({ id: 'id3', content: 'content3' });
+ dispatcher.updateSkillManifest({ id: 'id3', content: 'content3' }, projectId);
});
expect(renderedComponent.current.skillManifests).toEqual([
{ id: 'id1', content: 'content1' },
@@ -115,14 +117,14 @@ describe('skill dispatcher', () => {
it('removeSkillManifest', async () => {
await act(async () => {
- dispatcher.removeSkillManifest('id1');
+ dispatcher.removeSkillManifest('id1', projectId);
});
expect(renderedComponent.current.skillManifests).toEqual([{ id: 'id2', content: 'content2' }]);
});
it('updateSkillManifest', async () => {
await act(async () => {
- dispatcher.updateSkillManifest({ id: 'id1', content: 'newContent' });
+ dispatcher.updateSkillManifest({ id: 'id1', content: 'newContent' }, projectId);
});
expect(renderedComponent.current.skillManifests).toEqual([
{ id: 'id1', content: 'newContent' },
@@ -132,7 +134,7 @@ describe('skill dispatcher', () => {
it('addsSkill', async () => {
await act(async () => {
- dispatcher.addSkill('123', makeTestSkill(3));
+ dispatcher.addSkill(projectId, makeTestSkill(3));
});
expect(renderedComponent.current.showAddSkillDialogModal).toBe(false);
expect(renderedComponent.current.onAddSkillDialogComplete.func).toBeUndefined();
@@ -141,7 +143,7 @@ describe('skill dispatcher', () => {
it('updateSkill', async () => {
await act(async () => {
- dispatcher.updateSkill('123', {
+ dispatcher.updateSkill(projectId, {
targetId: 0,
skillData: makeTestSkill(100),
});
@@ -153,14 +155,14 @@ describe('skill dispatcher', () => {
it('removeSkill', async () => {
await act(async () => {
- dispatcher.removeSkill('123', makeTestSkill(1).manifestUrl);
+ dispatcher.removeSkill(projectId, makeTestSkill(1).manifestUrl);
});
expect(renderedComponent.current.skills).not.toContain(makeTestSkill(1));
});
it('addSkillDialogBegin', async () => {
await act(async () => {
- dispatcher.addSkillDialogBegin(mockDialogComplete);
+ dispatcher.addSkillDialogBegin(mockDialogComplete, projectId);
});
expect(renderedComponent.current.showAddSkillDialogModal).toBe(true);
expect(renderedComponent.current.onAddSkillDialogComplete.func).toBe(mockDialogComplete);
@@ -168,7 +170,7 @@ describe('skill dispatcher', () => {
it('addSkillDialogCancel', async () => {
await act(async () => {
- dispatcher.addSkillDialogCancel();
+ dispatcher.addSkillDialogCancel(projectId);
});
expect(renderedComponent.current.showAddSkillDialogModal).toBe(false);
expect(renderedComponent.current.onAddSkillDialogComplete.func).toBe(undefined);
@@ -177,10 +179,10 @@ describe('skill dispatcher', () => {
describe('addSkillDialogSuccess', () => {
it('with a function in onAddSkillDialogComplete', async () => {
await act(async () => {
- dispatcher.addSkillDialogBegin(mockDialogComplete);
+ dispatcher.addSkillDialogBegin(mockDialogComplete, projectId);
});
await act(async () => {
- dispatcher.addSkillDialogSuccess();
+ dispatcher.addSkillDialogSuccess(projectId);
});
expect(mockDialogComplete).toHaveBeenCalledWith(null);
expect(renderedComponent.current.showAddSkillDialogModal).toBe(false);
@@ -189,10 +191,10 @@ describe('skill dispatcher', () => {
it('with nothing in onAddSkillDialogComplete', async () => {
await act(async () => {
- dispatcher.addSkillDialogCancel();
+ dispatcher.addSkillDialogCancel(projectId);
});
await act(async () => {
- dispatcher.addSkillDialogSuccess();
+ dispatcher.addSkillDialogSuccess(projectId);
});
expect(mockDialogComplete).not.toHaveBeenCalled();
expect(renderedComponent.current.showAddSkillDialogModal).toBe(false);
@@ -202,14 +204,14 @@ describe('skill dispatcher', () => {
it('displayManifestModal', async () => {
await act(async () => {
- dispatcher.displayManifestModal('foo');
+ dispatcher.displayManifestModal('foo', projectId);
});
expect(renderedComponent.current.displaySkillManifest).toEqual('foo');
});
it('dismissManifestModal', async () => {
await act(async () => {
- dispatcher.dismissManifestModal();
+ dispatcher.dismissManifestModal(projectId);
});
expect(renderedComponent.current.displaySkillManifest).toBeUndefined();
});
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/builder.ts b/Composer/packages/client/src/recoilModel/dispatchers/builder.ts
index 3a83dcd4b6..4bf6302f6f 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/builder.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/builder.ts
@@ -23,9 +23,9 @@ export const builderDispatcher = () => {
qnaConfig: IQnAConfig,
projectId: string
) => {
- const dialogs = await snapshot.getPromise(dialogsState);
- const luFiles = await snapshot.getPromise(luFilesState);
- const qnaFiles = await snapshot.getPromise(qnaFilesState);
+ const dialogs = await snapshot.getPromise(dialogsState(projectId));
+ const luFiles = await snapshot.getPromise(luFilesState(projectId));
+ const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
const referredLuFiles = luUtil.checkLuisBuild(luFiles, dialogs);
const errorMsg = qnaFiles.reduce(
@@ -42,8 +42,8 @@ export const builderDispatcher = () => {
{ title: Text.LUISDEPLOYFAILURE, message: '' }
);
if (errorMsg.message) {
- set(botLoadErrorState, errorMsg);
- set(botStatusState, BotStatus.failed);
+ set(botLoadErrorState(projectId), errorMsg);
+ set(botStatusState(projectId), BotStatus.failed);
return;
}
try {
@@ -59,10 +59,13 @@ export const builderDispatcher = () => {
});
luFileStatusStorage.publishAll(projectId);
qnaFileStatusStorage.publishAll(projectId);
- set(botStatusState, BotStatus.published);
+ set(botStatusState(projectId), BotStatus.published);
} catch (err) {
- set(botStatusState, BotStatus.failed);
- set(botLoadErrorState, { title: Text.LUISDEPLOYFAILURE, message: err.response?.data?.message || err.message });
+ set(botStatusState(projectId), BotStatus.failed);
+ set(botLoadErrorState(projectId), {
+ title: Text.LUISDEPLOYFAILURE,
+ message: err.response?.data?.message || err.message,
+ });
}
}
);
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/dialogSchema.ts b/Composer/packages/client/src/recoilModel/dispatchers/dialogSchema.ts
index a9a8e85ebe..717a6a06a4 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/dialogSchema.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/dialogSchema.ts
@@ -6,25 +6,28 @@ import { DialogSchemaFile } from '@bfc/shared';
import { dialogSchemasState } from '../atoms/botState';
-const createDialogSchema = ({ set }: CallbackInterface, dialogSchema: DialogSchemaFile) => {
- set(dialogSchemasState, (dialogSchemas) => [...dialogSchemas, dialogSchema]);
+const createDialogSchema = ({ set }: CallbackInterface, dialogSchema: DialogSchemaFile, projectId: string) => {
+ set(dialogSchemasState(projectId), (dialogSchemas) => [...dialogSchemas, dialogSchema]);
};
-export const removeDialogSchema = ({ set }: CallbackInterface, id: string) => {
- set(dialogSchemasState, (dialogSchemas) => dialogSchemas.filter((dialogSchema) => dialogSchema.id !== id));
+export const removeDialogSchema = (
+ { set }: CallbackInterface,
+ { id, projectId }: { id: string; projectId: string }
+) => {
+ set(dialogSchemasState(projectId), (dialogSchemas) => dialogSchemas.filter((dialogSchema) => dialogSchema.id !== id));
};
export const dialogSchemaDispatcher = () => {
const updateDialogSchema = useRecoilCallback(
- (callbackHelpers: CallbackInterface) => async (dialogSchema: DialogSchemaFile) => {
+ (callbackHelpers: CallbackInterface) => async (dialogSchema: DialogSchemaFile, projectId: string) => {
const { set, snapshot } = callbackHelpers;
- const dialogSchemas = await snapshot.getPromise(dialogSchemasState);
+ const dialogSchemas = await snapshot.getPromise(dialogSchemasState(projectId));
if (!dialogSchemas.some((dialog) => dialog.id === dialogSchema.id)) {
- return createDialogSchema(callbackHelpers, dialogSchema);
+ return createDialogSchema(callbackHelpers, dialogSchema, projectId);
}
- set(dialogSchemasState, (dialogSchemas) =>
+ set(dialogSchemasState(projectId), (dialogSchemas) =>
dialogSchemas.map((schema) => (schema.id === dialogSchema.id ? dialogSchema : schema))
);
}
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/dialogs.ts b/Composer/packages/client/src/recoilModel/dispatchers/dialogs.ts
index 9ffcb85273..ed873d7037 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/dialogs.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/dialogs.ts
@@ -20,24 +20,26 @@ import { createQnAFileState, removeQnAFileState } from './qna';
import { removeDialogSchema } from './dialogSchema';
export const dialogsDispatcher = () => {
- const removeDialog = useRecoilCallback((callbackHelpers: CallbackInterface) => async (id: string) => {
- const { set, snapshot } = callbackHelpers;
- let dialogs = await snapshot.getPromise(dialogsState);
- dialogs = dialogs.filter((dialog) => dialog.id !== id);
- set(dialogsState, dialogs);
- //remove dialog should remove all locales lu and lg files and the dialog schema file
- await removeLgFileState(callbackHelpers, { id });
- await removeLuFileState(callbackHelpers, { id });
- await removeQnAFileState(callbackHelpers, { id });
- await removeDialogSchema(callbackHelpers, id);
- });
+ const removeDialog = useRecoilCallback(
+ (callbackHelpers: CallbackInterface) => async (id: string, projectId: string) => {
+ const { set, snapshot } = callbackHelpers;
+ let dialogs = await snapshot.getPromise(dialogsState(projectId));
+ dialogs = dialogs.filter((dialog) => dialog.id !== id);
+ set(dialogsState(projectId), dialogs);
+ //remove dialog should remove all locales lu and lg files and the dialog schema file
+ await removeLgFileState(callbackHelpers, { id, projectId });
+ await removeLuFileState(callbackHelpers, { id, projectId });
+ await removeQnAFileState(callbackHelpers, { id, projectId });
+ removeDialogSchema(callbackHelpers, { id, projectId });
+ }
+ );
- const updateDialog = useRecoilCallback(({ set }: CallbackInterface) => ({ id, content }) => {
+ const updateDialog = useRecoilCallback(({ set }: CallbackInterface) => ({ id, content, projectId }) => {
// migration: add id for dialog
if (typeof content === 'object' && !content.id) {
content.id = id;
}
- set(dialogsState, (dialogs) => {
+ set(dialogsState(projectId), (dialogs) => {
return dialogs.map((dialog) => {
if (dialog.id === id) {
const fixedContent = JSON.parse(autofixReferInDialog(id, JSON.stringify(content)));
@@ -48,43 +50,45 @@ export const dialogsDispatcher = () => {
});
});
- const createDialogBegin = useRecoilCallback((callbackHelpers: CallbackInterface) => (actions, onComplete) => {
- const { set } = callbackHelpers;
- set(actionsSeedState, actions);
- set(onCreateDialogCompleteState, { func: onComplete });
- set(showCreateDialogModalState, true);
- });
+ const createDialogBegin = useRecoilCallback(
+ (callbackHelpers: CallbackInterface) => (actions, onComplete, projectId: string) => {
+ const { set } = callbackHelpers;
+ set(actionsSeedState(projectId), actions);
+ set(onCreateDialogCompleteState(projectId), { func: onComplete });
+ set(showCreateDialogModalState(projectId), true);
+ }
+ );
- const createDialogCancel = useRecoilCallback((callbackHelpers: CallbackInterface) => () => {
+ const createDialogCancel = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => {
const { set } = callbackHelpers;
- set(actionsSeedState, []);
- set(onCreateDialogCompleteState, { func: undefined });
- set(showCreateDialogModalState, false);
+ set(actionsSeedState(projectId), []);
+ set(onCreateDialogCompleteState(projectId), { func: undefined });
+ set(showCreateDialogModalState(projectId), false);
});
- const createDialog = useRecoilCallback((callbackHelpers: CallbackInterface) => async ({ id, content }) => {
+ const createDialog = useRecoilCallback((callbackHelpers: CallbackInterface) => async ({ id, content, projectId }) => {
const { set, snapshot } = callbackHelpers;
const fixedContent = JSON.parse(autofixReferInDialog(id, JSON.stringify(content)));
- const schemas = await snapshot.getPromise(schemasState);
- const lgFiles = await snapshot.getPromise(lgFilesState);
- const luFiles = await snapshot.getPromise(luFilesState);
+ const schemas = await snapshot.getPromise(schemasState(projectId));
+ const lgFiles = await snapshot.getPromise(lgFilesState(projectId));
+ const luFiles = await snapshot.getPromise(luFilesState(projectId));
const dialog = { isRoot: false, displayName: id, ...dialogIndexer.parse(id, fixedContent) };
dialog.diagnostics = validateDialog(dialog, schemas.sdk.content, lgFiles, luFiles);
if (typeof dialog.content === 'object') {
dialog.content.id = id;
}
- await createLgFileState(callbackHelpers, { id, content: '' });
- await createLuFileState(callbackHelpers, { id, content: '' });
- await createQnAFileState(callbackHelpers, { id, content: '' });
+ await createLgFileState(callbackHelpers, { id, content: '', projectId });
+ await createLuFileState(callbackHelpers, { id, content: '', projectId });
+ await createQnAFileState(callbackHelpers, { id, content: '', projectId });
- set(dialogsState, (dialogs) => [...dialogs, dialog]);
- set(actionsSeedState, []);
- set(showCreateDialogModalState, false);
- const onComplete = (await snapshot.getPromise(onCreateDialogCompleteState)).func;
+ set(dialogsState(projectId), (dialogs) => [...dialogs, dialog]);
+ set(actionsSeedState(projectId), []);
+ set(showCreateDialogModalState(projectId), false);
+ const onComplete = (await snapshot.getPromise(onCreateDialogCompleteState(projectId))).func;
if (typeof onComplete === 'function') {
setTimeout(() => onComplete(id));
}
- set(onCreateDialogCompleteState, { func: undefined });
+ set(onCreateDialogCompleteState(projectId), { func: undefined });
});
return {
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/export.ts b/Composer/packages/client/src/recoilModel/dispatchers/export.ts
index 73fd255c32..3a00d0f35a 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/export.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/export.ts
@@ -5,19 +5,18 @@
import { CallbackInterface, useRecoilCallback } from 'recoil';
import httpClient from '../../utils/httpUtil';
+import { botNameState } from '../atoms';
-import { botNameState } from './../atoms/botState';
import { logMessage } from './shared';
export const exportDispatcher = () => {
- const exportToZip = useRecoilCallback((callbackHelpers: CallbackInterface) => async ({ projectId }) => {
- const botName = await callbackHelpers.snapshot.getPromise(botNameState);
+ const exportToZip = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => {
+ const botName = await callbackHelpers.snapshot.getPromise(botNameState(projectId));
try {
const response = await httpClient.get(`/projects/${projectId}/export/`, { responseType: 'blob' });
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
-
link.setAttribute('download', `${botName}_export.zip`);
document.body.appendChild(link);
link.click();
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts
index 93afaaed1c..cc77bf45a5 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts
@@ -10,7 +10,7 @@ import { getBaseName, getExtension } from '../../utils/fileUtil';
import { setError } from './shared';
import LgWorker from './../parsers/lgWorker';
-import { lgFilesState, localeState, settingsState, projectIdState } from './../atoms/botState';
+import { lgFilesState, localeState, settingsState } from './../atoms/botState';
const templateIsNotEmpty = ({ name, body }) => {
return !!name && !!body;
@@ -73,73 +73,96 @@ export const updateLgFileState = async (projectId: string, lgFiles: LgFile[], up
// when do create, passed id do not carried with locale
export const createLgFileState = async (
callbackHelpers: CallbackInterface,
- { id, content }: { id: string; content: string }
+ { id, content, projectId }: { id: string; content: string; projectId: string }
) => {
- const { set, snapshot } = callbackHelpers;
- const lgFiles = await snapshot.getPromise(lgFilesState);
- const locale = await snapshot.getPromise(localeState);
- const projectId = await snapshot.getPromise(projectIdState);
- const { languages } = await snapshot.getPromise(settingsState);
- const createdLgId = `${id}.${locale}`;
- if (lgFiles.find((lg) => lg.id === createdLgId)) {
- throw new Error(formatMessage('lg file already exist'));
- }
- // slot with common.lg import
- let lgInitialContent = '';
- const lgCommonFile = lgFiles.find(({ id }) => id === `common.${locale}`);
- if (lgCommonFile) {
- lgInitialContent = `[import](common.lg)`;
- }
- content = [lgInitialContent, content].join('\n');
- const createdLgFile = (await LgWorker.parse(projectId, createdLgId, content, lgFiles)) as LgFile;
- const changes: LgFile[] = [];
+ try {
+ const { set, snapshot } = callbackHelpers;
+ const lgFiles = await snapshot.getPromise(lgFilesState(projectId));
+ const locale = await snapshot.getPromise(localeState(projectId));
+ const { languages } = await snapshot.getPromise(settingsState(projectId));
+ const createdLgId = `${id}.${locale}`;
+ if (lgFiles.find((lg) => lg.id === createdLgId)) {
+ throw new Error(formatMessage('lg file already exist'));
+ }
+ // slot with common.lg import
+ let lgInitialContent = '';
+ const lgCommonFile = lgFiles.find(({ id }) => id === `common.${locale}`);
+ if (lgCommonFile) {
+ lgInitialContent = `[import](common.lg)`;
+ }
+ content = [lgInitialContent, content].join('\n');
+ const createdLgFile = (await LgWorker.parse(projectId, createdLgId, content, lgFiles)) as LgFile;
+ const changes: LgFile[] = [];
- // copy to other locales
- languages.forEach((lang) => {
- changes.push({
- ...createdLgFile,
- id: `${id}.${lang}`,
+ // copy to other locales
+ languages.forEach((lang) => {
+ changes.push({
+ ...createdLgFile,
+ id: `${id}.${lang}`,
+ });
});
- });
- set(lgFilesState, [...lgFiles, ...changes]);
+ set(lgFilesState(projectId), [...lgFiles, ...changes]);
+ } catch (error) {
+ setError(callbackHelpers, error);
+ }
};
-export const removeLgFileState = async (callbackHelpers: CallbackInterface, { id }: { id: string }) => {
- const { set, snapshot } = callbackHelpers;
- let lgFiles = await snapshot.getPromise(lgFilesState);
- lgFiles = lgFiles.filter((file) => getBaseName(file.id) !== id);
- set(lgFilesState, lgFiles);
+export const removeLgFileState = async (
+ callbackHelpers: CallbackInterface,
+ { id, projectId }: { id: string; projectId: string }
+) => {
+ try {
+ const { set, snapshot } = callbackHelpers;
+ let lgFiles = await snapshot.getPromise(lgFilesState(projectId));
+ lgFiles = lgFiles.filter((file) => getBaseName(file.id) !== id);
+ set(lgFilesState(projectId), lgFiles);
+ } catch (error) {
+ setError(callbackHelpers, error);
+ }
};
export const lgDispatcher = () => {
const createLgFile = useRecoilCallback(
- (callbackHelpers: CallbackInterface) => async ({ id, content }: { id: string; content: string }) => {
+ (callbackHelpers: CallbackInterface) => async ({
+ id,
+ content,
+ projectId,
+ }: {
+ id: string;
+ content: string;
+ projectId: string;
+ }) => {
try {
- await createLgFileState(callbackHelpers, { id, content });
+ await createLgFileState(callbackHelpers, { id, content, projectId });
} catch (error) {
setError(callbackHelpers, error);
}
}
);
- const removeLgFile = useRecoilCallback((callbackHelpers: CallbackInterface) => async ({ id }: { id: string }) => {
- try {
- await removeLgFileState(callbackHelpers, { id });
- } catch (error) {
- setError(callbackHelpers, error);
+ const removeLgFile = useRecoilCallback(
+ (callbackHelpers: CallbackInterface) => async ({ id, projectId }: { id: string; projectId: string }) => {
+ await removeLgFileState(callbackHelpers, { id, projectId });
}
- });
+ );
const updateLgFile = useRecoilCallback(
- (callbackHelpers: CallbackInterface) => async ({ id, content }: { id: string; content: string }) => {
- const { set, snapshot } = callbackHelpers;
- const lgFiles = await snapshot.getPromise(lgFilesState);
- const projectId = await snapshot.getPromise(projectIdState);
+ (callbackHelpers: CallbackInterface) => async ({
+ id,
+ content,
+ projectId,
+ }: {
+ id: string;
+ content: string;
+ projectId: string;
+ }) => {
try {
+ const { set, snapshot } = callbackHelpers;
+ const lgFiles = await snapshot.getPromise(lgFilesState(projectId));
const updatedFile = (await LgWorker.parse(projectId, id, content, lgFiles)) as LgFile;
const updatedFiles = await updateLgFileState(projectId, lgFiles, updatedFile);
- set(lgFilesState, updatedFiles);
+ set(lgFilesState(projectId), updatedFiles);
} catch (error) {
setError(callbackHelpers, error);
}
@@ -151,14 +174,15 @@ export const lgDispatcher = () => {
id,
templateName,
template,
+ projectId,
}: {
id: string;
templateName: string;
template: LgTemplate;
+ projectId: string;
}) => {
const { set, snapshot } = callbackHelpers;
- const lgFiles = await snapshot.getPromise(lgFilesState);
- const projectId = await snapshot.getPromise(projectIdState);
+ const lgFiles = await snapshot.getPromise(lgFilesState(projectId));
const lgFile = lgFiles.find((file) => file.id === id);
if (!lgFile) return lgFiles;
const sameIdOtherLocaleFiles = lgFiles.filter((file) => getBaseName(file.id) === getBaseName(id));
@@ -179,7 +203,7 @@ export const lgDispatcher = () => {
changes.push(updatedFile);
}
- set(lgFilesState, (lgFiles) => {
+ set(lgFilesState(projectId), (lgFiles) => {
return lgFiles.map((file) => {
const changedFile = changes.find(({ id }) => id === file.id);
return changedFile ? changedFile : file;
@@ -195,7 +219,7 @@ export const lgDispatcher = () => {
lgFiles
)) as LgFile;
- set(lgFilesState, (lgFiles) => {
+ set(lgFilesState(projectId), (lgFiles) => {
return lgFiles.map((file) => {
return file.id === id ? updatedFile : file;
});
@@ -208,33 +232,42 @@ export const lgDispatcher = () => {
);
const createLgTemplate = useRecoilCallback(
- (callbackHelpers: CallbackInterface) => async ({ id, template }: { id: string; template: LgTemplate }) => {
- const { set, snapshot } = callbackHelpers;
- const lgFiles = await snapshot.getPromise(lgFilesState);
- const projectId = await snapshot.getPromise(projectIdState);
+ ({ set, snapshot }: CallbackInterface) => async ({
+ id,
+ template,
+ projectId,
+ }: {
+ id: string;
+ template: LgTemplate;
+ projectId: string;
+ }) => {
+ const lgFiles = await snapshot.getPromise(lgFilesState(projectId));
const lgFile = lgFiles.find((file) => file.id === id);
if (!lgFile) return lgFiles;
- try {
- const updatedFile = (await LgWorker.addTemplate(projectId, lgFile, template, lgFiles)) as LgFile;
- const updatedFiles = await updateLgFileState(projectId, lgFiles, updatedFile);
- set(lgFilesState, updatedFiles);
- } catch (error) {
- setError(callbackHelpers, error);
- }
+ const updatedFile = (await LgWorker.addTemplate(projectId, lgFile, template, lgFiles)) as LgFile;
+ const updatedFiles = await updateLgFileState(projectId, lgFiles, updatedFile);
+ set(lgFilesState(projectId), updatedFiles);
}
);
const createLgTemplates = useRecoilCallback(
- (callbackHelpers: CallbackInterface) => async ({ id, templates }: { id: string; templates: LgTemplate[] }) => {
- const { set, snapshot } = callbackHelpers;
- const lgFiles = await snapshot.getPromise(lgFilesState);
- const projectId = await snapshot.getPromise(projectIdState);
- const lgFile = lgFiles.find((file) => file.id === id);
- if (!lgFile) return lgFiles;
+ (callbackHelpers: CallbackInterface) => async ({
+ id,
+ templates,
+ projectId,
+ }: {
+ id: string;
+ templates: LgTemplate[];
+ projectId: string;
+ }) => {
try {
+ const { set, snapshot } = callbackHelpers;
+ const lgFiles = await snapshot.getPromise(lgFilesState(projectId));
+ const lgFile = lgFiles.find((file) => file.id === id);
+ if (!lgFile) return lgFiles;
const updatedFile = (await LgWorker.addTemplates(projectId, lgFile, templates, lgFiles)) as LgFile;
const updatedFiles = await updateLgFileState(projectId, lgFiles, updatedFile);
- set(lgFilesState, updatedFiles);
+ set(lgFilesState(projectId), updatedFiles);
} catch (error) {
setError(callbackHelpers, error);
}
@@ -242,17 +275,24 @@ export const lgDispatcher = () => {
);
const removeLgTemplate = useRecoilCallback(
- (callbackHelpers: CallbackInterface) => async ({ id, templateName }: { id: string; templateName: string }) => {
+ (callbackHelpers: CallbackInterface) => async ({
+ id,
+ templateName,
+ projectId,
+ }: {
+ id: string;
+ templateName: string;
+ projectId: string;
+ }) => {
const { set, snapshot } = callbackHelpers;
- const lgFiles = await snapshot.getPromise(lgFilesState);
- const projectId = await snapshot.getPromise(projectIdState);
+ const lgFiles = await snapshot.getPromise(lgFilesState(projectId));
const lgFile = lgFiles.find((file) => file.id === id);
if (!lgFile) return lgFiles;
try {
const updatedFile = (await LgWorker.removeTemplate(projectId, lgFile, templateName, lgFiles)) as LgFile;
const updatedFiles = await updateLgFileState(projectId, lgFiles, updatedFile);
- set(lgFilesState, updatedFiles);
+ set(lgFilesState(projectId), updatedFiles);
} catch (error) {
setError(callbackHelpers, error);
}
@@ -260,17 +300,25 @@ export const lgDispatcher = () => {
);
const removeLgTemplates = useRecoilCallback(
- (callbackHelpers: CallbackInterface) => async ({ id, templateNames }: { id: string; templateNames: string[] }) => {
- const { set, snapshot } = callbackHelpers;
- const lgFiles = await snapshot.getPromise(lgFilesState);
- const projectId = await snapshot.getPromise(projectIdState);
- const lgFile = lgFiles.find((file) => file.id === id);
- if (!lgFile) return lgFiles;
+ (callbackHelpers: CallbackInterface) => async ({
+ id,
+ templateNames,
+ projectId,
+ }: {
+ id: string;
+ templateNames: string[];
+ projectId: string;
+ }) => {
try {
+ const { set, snapshot } = callbackHelpers;
+ const lgFiles = await snapshot.getPromise(lgFilesState(projectId));
+ const lgFile = lgFiles.find((file) => file.id === id);
+ if (!lgFile) return lgFiles;
+
const updatedFile = (await LgWorker.removeTemplates(projectId, lgFile, templateNames, lgFiles)) as LgFile;
const updatedFiles = await updateLgFileState(projectId, lgFiles, updatedFile);
- set(lgFilesState, updatedFiles);
+ set(lgFilesState(projectId), updatedFiles);
} catch (error) {
setError(callbackHelpers, error);
}
@@ -282,17 +330,18 @@ export const lgDispatcher = () => {
id,
fromTemplateName,
toTemplateName,
+ projectId,
}: {
id: string;
fromTemplateName: string;
toTemplateName: string;
+ projectId: string;
}) => {
- const { set, snapshot } = callbackHelpers;
- const lgFiles = await snapshot.getPromise(lgFilesState);
- const projectId = await snapshot.getPromise(projectIdState);
- const lgFile = lgFiles.find((file) => file.id === id);
- if (!lgFile) return lgFiles;
try {
+ const { set, snapshot } = callbackHelpers;
+ const lgFiles = await snapshot.getPromise(lgFilesState(projectId));
+ const lgFile = lgFiles.find((file) => file.id === id);
+ if (!lgFile) return lgFiles;
const updatedFile = (await LgWorker.copyTemplate(
projectId,
lgFile,
@@ -301,7 +350,7 @@ export const lgDispatcher = () => {
lgFiles
)) as LgFile;
const updatedFiles = await updateLgFileState(projectId, lgFiles, updatedFile);
- set(lgFilesState, updatedFiles);
+ set(lgFilesState(projectId), updatedFiles);
} catch (error) {
setError(callbackHelpers, error);
}
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lu.ts b/Composer/packages/client/src/recoilModel/dispatchers/lu.ts
index f85715e53f..665bcb97e1 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/lu.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/lu.ts
@@ -9,7 +9,7 @@ import formatMessage from 'format-message';
import luWorker from '../parsers/luWorker';
import { getBaseName, getExtension } from '../../utils/fileUtil';
import luFileStatusStorage from '../../utils/luFileStatusStorage';
-import { luFilesState, projectIdState, localeState, settingsState } from '../atoms/botState';
+import { luFilesState, localeState, settingsState } from '../atoms/botState';
import { setError } from './shared';
@@ -71,13 +71,12 @@ export const updateLuFileState = async (luFiles: LuFile[], updatedLuFile: LuFile
export const createLuFileState = async (
callbackHelpers: CallbackInterface,
- { id, content }: { id: string; content: string }
+ { id, content, projectId }: { id: string; content: string; projectId: string }
) => {
const { set, snapshot } = callbackHelpers;
- const luFiles = await snapshot.getPromise(luFilesState);
- const projectId = await snapshot.getPromise(projectIdState);
- const locale = await snapshot.getPromise(localeState);
- const { languages } = await snapshot.getPromise(settingsState);
+ const luFiles = await snapshot.getPromise(luFilesState(projectId));
+ const locale = await snapshot.getPromise(localeState(projectId));
+ const { languages } = await snapshot.getPromise(settingsState(projectId));
const createdLuId = `${id}.${locale}`;
const createdLuFile = (await luWorker.parse(id, content)) as LuFile;
if (luFiles.find((lu) => lu.id === createdLuId)) {
@@ -95,13 +94,15 @@ export const createLuFileState = async (
});
});
- set(luFilesState, [...luFiles, ...changes]);
+ set(luFilesState(projectId), [...luFiles, ...changes]);
};
-export const removeLuFileState = async (callbackHelpers: CallbackInterface, { id }: { id: string }) => {
+export const removeLuFileState = async (
+ callbackHelpers: CallbackInterface,
+ { id, projectId }: { id: string; projectId: string }
+) => {
const { set, snapshot } = callbackHelpers;
- let luFiles = await snapshot.getPromise(luFilesState);
- const projectId = await snapshot.getPromise(projectIdState);
+ let luFiles = await snapshot.getPromise(luFilesState(projectId));
luFiles.forEach((file) => {
if (getBaseName(file.id) === getBaseName(id)) {
@@ -110,7 +111,7 @@ export const removeLuFileState = async (callbackHelpers: CallbackInterface, { id
});
luFiles = luFiles.filter((file) => getBaseName(file.id) !== id);
- set(luFilesState, luFiles);
+ set(luFilesState(projectId), luFiles);
};
export const luDispatcher = () => {
@@ -125,11 +126,11 @@ export const luDispatcher = () => {
projectId: string;
}) => {
const { set, snapshot } = callbackHelpers;
- const luFiles = await snapshot.getPromise(luFilesState);
+ const luFiles = await snapshot.getPromise(luFilesState(projectId));
try {
const updatedFile = (await luWorker.parse(id, content)) as LuFile;
const result = await updateLuFileState(luFiles, updatedFile, projectId);
- set(luFilesState, result);
+ set(luFilesState(projectId), result);
} catch (error) {
setError(callbackHelpers, error);
}
@@ -141,13 +142,15 @@ export const luDispatcher = () => {
id,
intentName,
intent,
+ projectId,
}: {
id: string;
intentName: string;
intent: LuIntentSection;
+ projectId: string;
}) => {
const { set, snapshot } = callbackHelpers;
- const luFiles = await snapshot.getPromise(luFilesState);
+ const luFiles = await snapshot.getPromise(luFilesState(projectId));
const luFile = luFiles.find((temp) => temp.id === id);
if (!luFile) return luFiles;
try {
@@ -161,7 +164,7 @@ export const luDispatcher = () => {
changes.push(updatedFile);
}
- set(luFilesState, (luFiles) => {
+ set(luFilesState(projectId), (luFiles) => {
return luFiles.map((file) => {
const changedFile = changes.find(({ id }) => id === file.id);
return changedFile ? changedFile : file;
@@ -170,7 +173,7 @@ export const luDispatcher = () => {
// body change, only update current locale file
} else {
const updatedFile = (await luWorker.updateIntent(luFile, intentName, { Body: intent.Body })) as LuFile;
- set(luFilesState, (luFiles) => {
+ set(luFilesState(projectId), (luFiles) => {
return luFiles.map((file) => {
return file.id === id ? updatedFile : file;
});
@@ -193,13 +196,13 @@ export const luDispatcher = () => {
projectId: string;
}) => {
const { set, snapshot } = callbackHelpers;
- const luFiles = await snapshot.getPromise(luFilesState);
+ const luFiles = await snapshot.getPromise(luFilesState(projectId));
const file = luFiles.find((temp) => temp.id === id);
if (!file) return luFiles;
try {
const updatedFile = (await luWorker.addIntent(file, intent)) as LuFile;
const result = await updateLuFileState(luFiles, updatedFile, projectId);
- set(luFilesState, result);
+ set(luFilesState(projectId), result);
} catch (error) {
setError(callbackHelpers, error);
}
@@ -217,13 +220,13 @@ export const luDispatcher = () => {
projectId: string;
}) => {
const { set, snapshot } = callbackHelpers;
- const luFiles = await snapshot.getPromise(luFilesState);
+ const luFiles = await snapshot.getPromise(luFilesState(projectId));
const file = luFiles.find((temp) => temp.id === id);
if (!file) return luFiles;
try {
const updatedFile = (await luWorker.removeIntent(file, intentName)) as LuFile;
const result = await updateLuFileState(luFiles, updatedFile, projectId);
- set(luFilesState, result);
+ set(luFilesState(projectId), result);
} 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 7409e61eea..395748390a 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/multilang.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/multilang.ts
@@ -56,21 +56,23 @@ const deleteLanguageResources = (
};
export const multilangDispatcher = () => {
- const setLocale = useRecoilCallback(({ set, snapshot }: CallbackInterface) => async (locale: string) => {
- const botName = await snapshot.getPromise(botNameState);
+ const setLocale = useRecoilCallback(
+ ({ set, snapshot }: CallbackInterface) => async (locale: string, projectId: string) => {
+ const botName = await snapshot.getPromise(botNameState(projectId));
- set(localeState, locale);
- languageStorage.setLocale(botName, locale);
- });
+ set(localeState(projectId), locale);
+ languageStorage.setLocale(botName, locale);
+ }
+ );
const addLanguages = useRecoilCallback(
- (callbackHelpers: CallbackInterface) => async ({ languages, defaultLang, switchTo = false }) => {
+ (callbackHelpers: CallbackInterface) => async ({ languages, defaultLang, switchTo = false, projectId }) => {
const { set, snapshot } = callbackHelpers;
- const botName = await snapshot.getPromise(botNameState);
- const prevlgFiles = await snapshot.getPromise(lgFilesState);
- const prevluFiles = await snapshot.getPromise(luFilesState);
- const prevSettings = await snapshot.getPromise(settingsState);
- const onAddLanguageDialogComplete = (await snapshot.getPromise(onAddLanguageDialogCompleteState)).func;
+ const botName = await snapshot.getPromise(botNameState(projectId));
+ const prevlgFiles = await snapshot.getPromise(lgFilesState(projectId));
+ const prevluFiles = await snapshot.getPromise(luFilesState(projectId));
+ const prevSettings = await snapshot.getPromise(settingsState(projectId));
+ const onAddLanguageDialogComplete = (await snapshot.getPromise(onAddLanguageDialogCompleteState(projectId))).func;
// copy files from default language
const lgFiles = copyLanguageResources(prevlgFiles, defaultLang, languages);
@@ -85,69 +87,75 @@ export const multilangDispatcher = () => {
if (switchTo) {
const switchToLocale = languages[0];
- set(localeState, switchToLocale);
+ set(localeState(projectId), switchToLocale);
languageStorage.setLocale(botName, switchToLocale);
}
- set(lgFilesState, [...prevlgFiles, ...lgFiles]);
- set(luFilesState, [...prevluFiles, ...luFiles]);
- set(settingsState, settings);
+ set(lgFilesState(projectId), [...prevlgFiles, ...lgFiles]);
+ set(luFilesState(projectId), [...prevluFiles, ...luFiles]);
+ set(settingsState(projectId), settings);
if (typeof onAddLanguageDialogComplete === 'function') {
onAddLanguageDialogComplete(languages);
}
- set(showAddLanguageModalState, false);
- set(onAddLanguageDialogCompleteState, { func: undefined });
+ set(showAddLanguageModalState(projectId), false);
+ set(onAddLanguageDialogCompleteState(projectId), { func: undefined });
}
);
- const deleteLanguages = useRecoilCallback((callbackHelpers: CallbackInterface) => async ({ languages }) => {
- const { set, snapshot } = callbackHelpers;
- const prevlgFiles = await snapshot.getPromise(lgFilesState);
- const prevluFiles = await snapshot.getPromise(luFilesState);
- const prevSettings = await snapshot.getPromise(settingsState);
- const onDelLanguageDialogComplete = (await snapshot.getPromise(onDelLanguageDialogCompleteState)).func;
+ const deleteLanguages = useRecoilCallback(
+ (callbackHelpers: CallbackInterface) => async ({ languages, projectId }) => {
+ const { set, snapshot } = callbackHelpers;
+ const prevlgFiles = await snapshot.getPromise(lgFilesState(projectId));
+ const prevluFiles = await snapshot.getPromise(luFilesState(projectId));
+ const prevSettings = await snapshot.getPromise(settingsState(projectId));
+ const onDelLanguageDialogComplete = (await snapshot.getPromise(onDelLanguageDialogCompleteState(projectId))).func;
- // copy files from default language
- const { left: leftLgFiles } = deleteLanguageResources(prevlgFiles, languages);
- const { left: leftLuFiles } = deleteLanguageResources(prevluFiles, languages);
+ // copy files from default language
+ const { left: leftLgFiles } = deleteLanguageResources(prevlgFiles, languages);
+ const { left: leftLuFiles } = deleteLanguageResources(prevluFiles, languages);
- const settings: any = cloneDeep(prevSettings);
+ const settings: any = cloneDeep(prevSettings);
- const leftLanguages = difference(settings.languages, languages);
- settings.languages = leftLanguages;
+ const leftLanguages = difference(settings.languages, languages);
+ settings.languages = leftLanguages;
- set(lgFilesState, leftLgFiles);
- set(luFilesState, leftLuFiles);
- set(settingsState, settings);
+ set(lgFilesState(projectId), leftLgFiles);
+ set(luFilesState(projectId), leftLuFiles);
+ set(settingsState(projectId), settings);
- if (typeof onDelLanguageDialogComplete === 'function') {
- onDelLanguageDialogComplete(leftLanguages);
- }
+ if (typeof onDelLanguageDialogComplete === 'function') {
+ onDelLanguageDialogComplete(leftLanguages);
+ }
- set(showDelLanguageModalState, false);
- set(onDelLanguageDialogCompleteState, { func: undefined });
- });
+ set(showDelLanguageModalState(projectId), false);
+ set(onDelLanguageDialogCompleteState(projectId), { func: undefined });
+ }
+ );
- const addLanguageDialogBegin = useRecoilCallback(({ set }: CallbackInterface) => async (onComplete) => {
- set(showAddLanguageModalState, true);
- set(onAddLanguageDialogCompleteState, { func: onComplete });
- });
+ const addLanguageDialogBegin = useRecoilCallback(
+ ({ set }: CallbackInterface) => async (projectId: string, onComplete) => {
+ set(showAddLanguageModalState(projectId), true);
+ set(onAddLanguageDialogCompleteState(projectId), { func: onComplete });
+ }
+ );
- const addLanguageDialogCancel = useRecoilCallback(({ set }: CallbackInterface) => async () => {
- set(showAddLanguageModalState, false);
- set(onAddLanguageDialogCompleteState, { func: undefined });
+ const addLanguageDialogCancel = useRecoilCallback(({ set }: CallbackInterface) => async (projectId: string) => {
+ set(showAddLanguageModalState(projectId), false);
+ set(onAddLanguageDialogCompleteState(projectId), { func: undefined });
});
- const delLanguageDialogBegin = useRecoilCallback(({ set }: CallbackInterface) => async (onComplete) => {
- set(showDelLanguageModalState, true);
- set(onDelLanguageDialogCompleteState, { func: onComplete });
- });
+ const delLanguageDialogBegin = useRecoilCallback(
+ ({ set }: CallbackInterface) => async (projectId: string, onComplete) => {
+ set(showDelLanguageModalState(projectId), true);
+ set(onDelLanguageDialogCompleteState(projectId), { func: onComplete });
+ }
+ );
- const delLanguageDialogCancel = useRecoilCallback(({ set }: CallbackInterface) => async () => {
- set(showDelLanguageModalState, false);
- set(onDelLanguageDialogCompleteState, { func: undefined });
+ const delLanguageDialogCancel = useRecoilCallback(({ set }: CallbackInterface) => async (projectId: string) => {
+ set(showDelLanguageModalState(projectId), false);
+ set(onDelLanguageDialogCompleteState(projectId), { func: undefined });
});
return {
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/navigation.ts b/Composer/packages/client/src/recoilModel/dispatchers/navigation.ts
index dc94abfedd..03dd9c0def 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/navigation.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/navigation.ts
@@ -7,15 +7,11 @@ import { useRecoilCallback, CallbackInterface } from 'recoil';
import { PromptTab, SDKKinds } from '@bfc/shared';
import cloneDeep from 'lodash/cloneDeep';
+import { currentProjectIdState } from '../atoms';
+
import { createSelectedPath, getSelected } from './../../utils/dialogUtil';
import { BreadcrumbItem } from './../../recoilModel/types';
-import {
- breadcrumbState,
- designPageLocationState,
- dialogsState,
- focusPathState,
- projectIdState,
-} from './../atoms/botState';
+import { breadcrumbState, designPageLocationState, focusPathState, dialogsState } from './../atoms/botState';
import {
BreadcrumbUpdateType,
checkUrl,
@@ -27,28 +23,22 @@ import {
export const navigationDispatcher = () => {
const setDesignPageLocation = useRecoilCallback(
- ({ set }: CallbackInterface) => async ({
- projectId = '',
- dialogId = '',
- selected = '',
- focused = '',
- breadcrumb = [],
- promptTab,
- }) => {
- //generate focusedPath. This will remove when all focusPath related is removed
+ ({ set }: CallbackInterface) => async (
+ projectId: string,
+ { dialogId = '', selected = '', focused = '', breadcrumb = [], promptTab }
+ ) => {
let focusPath = dialogId + '#';
if (focused) {
focusPath = dialogId + '#.' + focused;
} else if (selected) {
focusPath = dialogId + '#.' + selected;
}
-
- set(focusPathState, focusPath);
+ set(currentProjectIdState, projectId);
+ set(focusPathState(projectId), focusPath);
//add current path to the breadcrumb
- set(breadcrumbState, [...breadcrumb, { dialogId, selected, focused }]);
- set(designPageLocationState, {
+ set(breadcrumbState(projectId), [...breadcrumb, { dialogId, selected, focused }]);
+ set(designPageLocationState(projectId), {
dialogId,
- projectId,
selected,
focused,
promptTab: Object.values(PromptTab).find((value) => promptTab === value),
@@ -57,15 +47,18 @@ export const navigationDispatcher = () => {
);
const navTo = useRecoilCallback(
- ({ snapshot }: CallbackInterface) => async (dialogId: string, breadcrumb: BreadcrumbItem[] = []) => {
- const projectId = await snapshot.getPromise(projectIdState);
- const designPageLocation = await snapshot.getPromise(designPageLocationState);
+ ({ snapshot, set }: CallbackInterface) => async (
+ projectId: string,
+ dialogId: string,
+ breadcrumb: BreadcrumbItem[] = []
+ ) => {
+ const dialogs = await snapshot.getPromise(dialogsState(projectId));
+ const designPageLocation = await snapshot.getPromise(designPageLocationState(projectId));
const updatedBreadcrumb = cloneDeep(breadcrumb);
+ set(currentProjectIdState, projectId);
let path;
if (dialogId !== designPageLocation.dialogId) {
- // Redirect to Microsoft.OnBeginDialog trigger if it exists on the dialog
- const dialogs = await snapshot.getPromise(dialogsState);
const currentDialog = dialogs.find(({ id }) => id === dialogId);
const beginDialogIndex = currentDialog?.triggers.findIndex(({ type }) => type === SDKKinds.OnBeginDialog);
@@ -77,76 +70,80 @@ export const navigationDispatcher = () => {
const currentUri = convertPathToUrl(projectId, dialogId, path);
- if (checkUrl(currentUri, designPageLocation)) return;
- //if dialog change we should flush some debounced functions
+ if (checkUrl(currentUri, projectId, designPageLocation)) return;
navigateTo(currentUri, { state: { breadcrumb: updatedBreadcrumb } });
}
);
- const selectTo = useRecoilCallback(({ snapshot }: CallbackInterface) => async (selectPath: string) => {
- if (!selectPath) return;
- const designPageLocation = await snapshot.getPromise(designPageLocationState);
- const breadcrumb = await snapshot.getPromise(breadcrumbState);
- const currentProjectId = await snapshot.getPromise(projectIdState);
- // initial dialogId, projectId maybe empty string ""
- let { dialogId, projectId } = designPageLocation;
+ const selectTo = useRecoilCallback(
+ ({ snapshot, set }: CallbackInterface) => async (projectId: string, selectPath: string) => {
+ if (!selectPath) return;
+ set(currentProjectIdState, projectId);
+ const designPageLocation = await snapshot.getPromise(designPageLocationState(projectId));
+ const breadcrumb = await snapshot.getPromise(breadcrumbState(projectId));
- if (!dialogId) dialogId = 'Main';
- if (!projectId) projectId = currentProjectId;
+ // initial dialogId, projectId maybe empty string ""
+ let { dialogId } = designPageLocation;
- const currentUri = convertPathToUrl(projectId, dialogId, selectPath);
+ if (!dialogId) dialogId = 'Main';
- if (checkUrl(currentUri, designPageLocation)) return;
- navigateTo(currentUri, { state: { breadcrumb: updateBreadcrumb(breadcrumb, BreadcrumbUpdateType.Selected) } });
- });
+ const currentUri = convertPathToUrl(projectId, dialogId, selectPath);
+
+ if (checkUrl(currentUri, projectId, designPageLocation)) return;
+ navigateTo(currentUri, { state: { breadcrumb: updateBreadcrumb(breadcrumb, BreadcrumbUpdateType.Selected) } });
+ }
+ );
const focusTo = useRecoilCallback(
- ({ snapshot }: CallbackInterface) => async (focusPath: string, fragment: string) => {
- const designPageLocation = await snapshot.getPromise(designPageLocationState);
- let breadcrumb = await snapshot.getPromise(breadcrumbState);
- const { dialogId, projectId, selected } = designPageLocation;
+ ({ snapshot, set }: CallbackInterface) => async (projectId: string, focusPath: string, fragment: string) => {
+ set(currentProjectIdState, projectId);
+ const designPageLocation = await snapshot.getPromise(designPageLocationState(projectId));
+ const breadcrumb = await snapshot.getPromise(breadcrumbState(projectId));
+ let updatedBreadcrumb = [...breadcrumb];
+ const { dialogId, selected } = designPageLocation;
let currentUri = `/bot/${projectId}/dialogs/${dialogId}`;
if (focusPath) {
const targetSelected = getSelected(focusPath);
if (targetSelected !== selected) {
- breadcrumb = updateBreadcrumb(breadcrumb, BreadcrumbUpdateType.Selected);
- breadcrumb.push({ dialogId, selected: targetSelected, focused: '' });
+ updatedBreadcrumb = updateBreadcrumb(breadcrumb, BreadcrumbUpdateType.Selected);
+ updatedBreadcrumb.push({ dialogId, selected: targetSelected, focused: '' });
}
currentUri = `${currentUri}?selected=${targetSelected}&focused=${focusPath}`;
- breadcrumb = updateBreadcrumb(breadcrumb, BreadcrumbUpdateType.Focused);
+ updatedBreadcrumb = updateBreadcrumb(breadcrumb, BreadcrumbUpdateType.Focused);
} else {
currentUri = `${currentUri}?selected=${selected}`;
- breadcrumb = updateBreadcrumb(breadcrumb, BreadcrumbUpdateType.Selected);
+ updatedBreadcrumb = updateBreadcrumb(breadcrumb, BreadcrumbUpdateType.Selected);
}
if (fragment && typeof fragment === 'string') {
currentUri += `#${fragment}`;
}
- if (checkUrl(currentUri, designPageLocation)) return;
- navigateTo(currentUri, { state: { breadcrumb } });
+ if (checkUrl(currentUri, projectId, designPageLocation)) return;
+ navigateTo(currentUri, { state: { breadcrumb: updatedBreadcrumb } });
}
);
const selectAndFocus = useRecoilCallback(
- ({ snapshot }: CallbackInterface) => async (
+ ({ snapshot, set }: CallbackInterface) => async (
+ projectId: string,
dialogId: string,
selectPath: string,
focusPath: string,
breadcrumb: BreadcrumbItem[] = []
) => {
+ set(currentProjectIdState, projectId);
const search = getUrlSearch(selectPath, focusPath);
- const designPageLocation = await snapshot.getPromise(designPageLocationState);
+ const designPageLocation = await snapshot.getPromise(designPageLocationState(projectId));
if (search) {
- const projectId = await snapshot.getPromise(projectIdState);
const currentUri = `/bot/${projectId}/dialogs/${dialogId}${getUrlSearch(selectPath, focusPath)}`;
- if (checkUrl(currentUri, designPageLocation)) return;
+ if (checkUrl(currentUri, projectId, designPageLocation)) return;
navigateTo(currentUri, { state: { breadcrumb } });
} else {
- navTo(dialogId, breadcrumb);
+ navTo(projectId, dialogId, breadcrumb);
}
}
);
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts
index 5211cc383f..c5136d015a 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts
@@ -30,12 +30,21 @@ import filePersistence from '../persistence/FilePersistence';
import { navigateTo } from '../../utils/navigation';
import languageStorage from '../../utils/languageStorage';
import { projectIdCache } from '../../utils/projectCache';
-import { designPageLocationState } from '../atoms/botState';
+import {
+ designPageLocationState,
+ botDiagnosticsState,
+ botProjectsSpaceState,
+ projectMetaDataState,
+ filePersistenceState,
+ currentProjectIdState,
+} from '../atoms';
import { QnABotTemplateId } from '../../constants';
+import FilePersistence from '../persistence/FilePersistence';
+import UndoHistory from '../undo/undoHistory';
+import { undoHistoryState } from '../undo/history';
import {
skillManifestsState,
- BotDiagnosticsState,
settingsState,
localeState,
luFilesState,
@@ -48,7 +57,6 @@ import {
botNameState,
botEnvironmentState,
dialogsState,
- projectIdState,
botOpeningState,
recentProjectsState,
templateProjectsState,
@@ -66,12 +74,6 @@ const handleProjectFailure = (callbackHelpers: CallbackInterface, ex) => {
setError(callbackHelpers, ex);
};
-const checkProjectUpdates = async () => {
- const workers = [filePersistence, lgWorker, luWorker, qnaWorker];
-
- return Promise.all(workers.map((w) => w.flush()));
-};
-
const processSchema = (projectId: string, schema: any) => ({
...schema,
definitions: dereferenceDefinitions(schema.definitions),
@@ -142,10 +144,16 @@ const initQnaFilesStatus = (projectId: string, qnaFiles: QnAFile[], dialogs: Dia
return updateQnaFilesStatus(projectId, qnaFiles);
};
export const projectDispatcher = () => {
- const initBotState = async (callbackHelpers: CallbackInterface, data: any, jump: boolean, templateId: string) => {
- const { snapshot, gotoSnapshot } = callbackHelpers;
- const curLocation = await snapshot.getPromise(locationState);
+ const initBotState = async (
+ callbackHelpers: CallbackInterface,
+ data: any,
+ jump: boolean,
+ templateId: string,
+ qnaKbUrls?: string[]
+ ) => {
+ const { snapshot, gotoSnapshot, set } = callbackHelpers;
const { files, botName, botEnvironment, location, schemas, settings, id: projectId, diagnostics, skills } = data;
+ const curLocation = await snapshot.getPromise(locationState(projectId));
const storedLocale = languageStorage.get(botName)?.locale;
const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage;
@@ -177,27 +185,27 @@ export const projectDispatcher = () => {
});
await lgWorker.addProject(projectId, lgFiles);
+ set(botProjectsSpaceState, []);
// Important: gotoSnapshot will wipe all states.
const newSnapshot = snapshot.map(({ set }) => {
- set(skillManifestsState, skillManifestFiles);
- set(luFilesState, initLuFilesStatus(botName, luFiles, dialogs));
- set(qnaFilesState, initQnaFilesStatus(botName, qnaFiles, dialogs));
- set(lgFilesState, lgFiles);
- set(dialogsState, verifiedDialogs);
- set(dialogSchemasState, dialogSchemas);
- set(botEnvironmentState, botEnvironment);
- set(botNameState, botName);
+ set(skillManifestsState(projectId), skillManifestFiles);
+ set(luFilesState(projectId), initLuFilesStatus(botName, luFiles, dialogs));
+ set(lgFilesState(projectId), lgFiles);
+ set(dialogsState(projectId), verifiedDialogs);
+ set(dialogSchemasState(projectId), dialogSchemas);
+ set(botEnvironmentState(projectId), botEnvironment);
+ set(botNameState(projectId), botName);
+ set(qnaFilesState(projectId), initQnaFilesStatus(botName, qnaFiles, dialogs));
if (location !== curLocation) {
- set(botStatusState, BotStatus.unConnected);
- set(locationState, location);
+ set(botStatusState(projectId), BotStatus.unConnected);
+ set(locationState(projectId), location);
}
- set(skillsState, skills);
- set(schemasState, schemas);
- set(localeState, locale);
- set(BotDiagnosticsState, diagnostics);
- set(botOpeningState, false);
- set(projectIdState, projectId);
+ set(skillsState(projectId), skills);
+ set(schemasState(projectId), schemas);
+ set(localeState(projectId), locale);
+ set(botDiagnosticsState(projectId), diagnostics);
+
refreshLocalStorage(projectId, settings);
const mergedSettings = mergeLocalStorage(projectId, settings);
if (Array.isArray(mergedSettings.skill)) {
@@ -208,13 +216,27 @@ export const projectDispatcher = () => {
});
mergedSettings.skill = convertSkillsToDictionary(skillsArr);
}
- set(settingsState, mergedSettings);
+ set(settingsState(projectId), mergedSettings);
+ set(filePersistenceState(projectId), new FilePersistence(projectId));
+ set(undoHistoryState(projectId), new UndoHistory(projectId));
+ //TODO: Botprojects space will be populated for now with just the rootbot. Once, BotProjects UI is hookedup this will be refactored to use addToBotProject
+ set(botProjectsSpaceState, (current) => [...current, projectId]);
+ set(projectMetaDataState(projectId), {
+ isRootBot: true,
+ });
+ set(botOpeningState, false);
});
+
gotoSnapshot(newSnapshot);
+
if (jump && projectId) {
+ // TODO: Refactor to set it always on init to the root bot
+ set(currentProjectIdState, projectId);
let url = `/bot/${projectId}/dialogs/${mainDialog}`;
if (templateId === QnABotTemplateId) {
url = `/bot/${projectId}/knowledge-base/${mainDialog}`;
+ navigateTo(url, { state: { qnaKbUrls } });
+ return;
}
navigateTo(url);
}
@@ -240,18 +262,24 @@ export const projectDispatcher = () => {
};
const setBotOpeningStatus = async (callbackHelpers: CallbackInterface) => {
- const { set } = callbackHelpers;
+ const { set, snapshot } = callbackHelpers;
set(botOpeningState, true);
- await checkProjectUpdates();
+ const botProjectSpace = await snapshot.getPromise(botProjectsSpaceState);
+ const filePersistenceHandlers: filePersistence[] = [];
+ for (const projectId of botProjectSpace) {
+ const fp = await snapshot.getPromise(filePersistenceState(projectId));
+ filePersistenceHandlers.push(fp);
+ }
+ const workers = [lgWorker, luWorker, qnaWorker, ...filePersistenceHandlers];
+ return Promise.all(workers.map((w) => w.flush()));
};
- const openBotProject = useRecoilCallback(
+ const openProject = useRecoilCallback(
(callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default') => {
try {
await setBotOpeningStatus(callbackHelpers);
const response = await httpClient.put(`/projects/open`, { path, storageId });
await initBotState(callbackHelpers, response.data, true, '');
-
return response.data.id;
} catch (ex) {
removeRecentProject(callbackHelpers, path);
@@ -277,7 +305,8 @@ export const projectDispatcher = () => {
description: string,
location: string,
schemaUrl?: string,
- locale?: string
+ locale?: string,
+ qnaKbUrls?: string[]
) => {
try {
await setBotOpeningStatus(callbackHelpers);
@@ -294,7 +323,7 @@ export const projectDispatcher = () => {
if (settingStorage.get(projectId)) {
settingStorage.remove(projectId);
}
- await initBotState(callbackHelpers, response.data, true, templateId);
+ await initBotState(callbackHelpers, response.data, true, templateId, qnaKbUrls);
return projectId;
} catch (ex) {
handleProjectFailure(callbackHelpers, ex);
@@ -310,20 +339,23 @@ export const projectDispatcher = () => {
qnaFileStatusStorage.removeAllStatuses(projectId);
settingStorage.remove(projectId);
projectIdCache.clear();
- reset(projectIdState);
- reset(dialogsState);
- reset(botEnvironmentState);
- reset(botNameState);
- reset(botStatusState);
- reset(locationState);
- reset(lgFilesState);
- reset(skillsState);
- reset(schemasState);
- reset(luFilesState);
- reset(settingsState);
- reset(localeState);
- reset(skillManifestsState);
- reset(designPageLocationState);
+ reset(dialogsState(projectId));
+ reset(botEnvironmentState(projectId));
+ reset(botNameState(projectId));
+ reset(botStatusState(projectId));
+ reset(locationState(projectId));
+ reset(lgFilesState(projectId));
+ reset(skillsState(projectId));
+ reset(schemasState(projectId));
+ reset(luFilesState(projectId));
+ reset(settingsState(projectId));
+ reset(localeState(projectId));
+ reset(skillManifestsState(projectId));
+ reset(designPageLocationState(projectId));
+ reset(filePersistenceState(projectId));
+ reset(undoHistoryState(projectId));
+ reset(botProjectsSpaceState);
+ reset(currentProjectIdState);
} catch (e) {
logMessage(callbackHelpers, e.message);
}
@@ -389,9 +421,9 @@ export const projectDispatcher = () => {
}
});
- const setBotStatus = useRecoilCallback<[BotStatus], Promise>(
- ({ set }: CallbackInterface) => async (status: BotStatus) => {
- set(botStatusState, status);
+ const setBotStatus = useRecoilCallback<[BotStatus, string], void>(
+ ({ set }: CallbackInterface) => (status: BotStatus, projectId: string) => {
+ set(botStatusState(projectId), status);
}
);
@@ -449,7 +481,7 @@ export const projectDispatcher = () => {
});
return {
- openBotProject,
+ openProject,
createProject,
deleteBotProject,
saveProjectAs,
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts
index 56b1097892..c9eeac9e33 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts
@@ -12,8 +12,8 @@ import {
publishHistoryState,
botLoadErrorState,
isEjectRuntimeExistState,
+ filePersistenceState,
} from '../atoms/botState';
-import filePersistence from '../persistence/FilePersistence';
import { botEndpointsState } from '../atoms';
import { BotStatus, Text } from './../../constants';
@@ -25,14 +25,14 @@ const PUBLISH_PENDING = 202;
const PUBLISH_FAILED = 500;
export const publisherDispatcher = () => {
- const publishFailure = async ({ set }: CallbackInterface, title: string, error, target) => {
+ const publishFailure = async ({ set }: CallbackInterface, title: string, error, target, projectId: string) => {
if (target.name === defaultPublishConfig.name) {
- set(botStatusState, BotStatus.failed);
- set(botLoadErrorState, { ...error, title });
+ set(botStatusState(projectId), BotStatus.failed);
+ set(botLoadErrorState(projectId), { ...error, title });
}
// prepend the latest publish results to the history
- set(publishHistoryState, (publishHistory) => {
+ set(publishHistoryState(projectId), (publishHistory) => {
const targetHistory = publishHistory[target.name] ?? [];
return {
...publishHistory,
@@ -45,14 +45,14 @@ export const publisherDispatcher = () => {
const { endpointURL, status } = data;
if (target.name === defaultPublishConfig.name) {
if (status === PUBLISH_SUCCESS && endpointURL) {
- set(botStatusState, BotStatus.connected);
+ set(botStatusState(projectId), BotStatus.connected);
set(botEndpointsState, (botEndpoints) => ({ ...botEndpoints, [projectId]: `${endpointURL}/api/messages` }));
} else {
- set(botStatusState, BotStatus.reloading);
+ set(botStatusState(projectId), BotStatus.reloading);
}
}
- set(publishHistoryState, (publishHistory) => {
+ set(publishHistoryState(projectId), (publishHistory) => {
const targetHistory = publishHistory[target.name] ?? [];
return {
...publishHistory,
@@ -73,21 +73,21 @@ export const publisherDispatcher = () => {
// a check should be added to this that ensures this ONLY applies to the "default" profile.
if (target.name === defaultPublishConfig.name) {
if (status === PUBLISH_SUCCESS && endpointURL) {
- set(botStatusState, BotStatus.connected);
+ set(botStatusState(projectId), BotStatus.connected);
set(botEndpointsState, (botEndpoints) => ({
...botEndpoints,
[projectId]: `${endpointURL}/api/messages`,
}));
} else if (status === PUBLISH_PENDING) {
- set(botStatusState, BotStatus.reloading);
+ set(botStatusState(projectId), BotStatus.reloading);
} else if (status === PUBLISH_FAILED) {
- set(botStatusState, BotStatus.failed);
- set(botLoadErrorState, { ...data, title: formatMessage('Start bot failed') });
+ set(botStatusState(projectId), BotStatus.failed);
+ set(botLoadErrorState(projectId), { ...data, title: formatMessage('Start bot failed') });
}
}
if (status !== 404) {
- set(publishHistoryState, (publishHistory) => {
+ set(publishHistoryState(projectId), (publishHistory) => {
const currentHistory = { ...data, target: target };
let targetHistories = publishHistory[target.name] ? [...publishHistory[target.name]] : [];
// if no history exists, create one with the latest status
@@ -108,11 +108,11 @@ export const publisherDispatcher = () => {
}
};
- const getPublishTargetTypes = useRecoilCallback((callbackHelpers: CallbackInterface) => async () => {
+ const getPublishTargetTypes = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => {
const { set } = callbackHelpers;
try {
const response = await httpClient.get(`/publish/types`);
- set(publishTypesState, response.data);
+ set(publishTypesState(projectId), response.data);
} catch (err) {
//TODO: error
logMessage(callbackHelpers, err.message);
@@ -146,9 +146,9 @@ export const publisherDispatcher = () => {
},
};
- await publishFailure(callbackHelpers, Text.DOTNETFAILURE, error, target);
+ await publishFailure(callbackHelpers, Text.DOTNETFAILURE, error, target, projectId);
} else {
- await publishFailure(callbackHelpers, Text.CONNECTBOTFAILURE, err.response?.data, target);
+ await publishFailure(callbackHelpers, Text.CONNECTBOTFAILURE, err.response?.data, target, projectId);
}
}
}
@@ -163,7 +163,7 @@ export const publisherDispatcher = () => {
});
await publishSuccess(callbackHelpers, projectId, response.data, target);
} catch (err) {
- await publishFailure(callbackHelpers, Text.CONNECTBOTFAILURE, err.response.data, target);
+ await publishFailure(callbackHelpers, Text.CONNECTBOTFAILURE, err.response.data, target, projectId);
}
}
);
@@ -182,11 +182,12 @@ export const publisherDispatcher = () => {
const getPublishHistory = useRecoilCallback(
(callbackHelpers: CallbackInterface) => async (projectId: string, target: any) => {
- const { set } = callbackHelpers;
+ const { set, snapshot } = callbackHelpers;
try {
- await filePersistence.flush();
+ const filePersistence = await snapshot.getPromise(filePersistenceState(projectId));
+ filePersistence.flush();
const response = await httpClient.get(`/publish/${projectId}/history/${target.name}`);
- set(publishHistoryState, (publishHistory) => ({
+ set(publishHistoryState(projectId), (publishHistory) => ({
...publishHistory,
[target.name]: response.data,
}));
@@ -197,9 +198,11 @@ export const publisherDispatcher = () => {
}
);
- const setEjectRuntimeExist = useRecoilCallback(({ set }: CallbackInterface) => async (isExist: boolean) => {
- set(isEjectRuntimeExistState, isExist);
- });
+ const setEjectRuntimeExist = useRecoilCallback(
+ ({ set }: CallbackInterface) => async (isExist: boolean, projectId: string) => {
+ set(isEjectRuntimeExistState(projectId), isExist);
+ }
+ );
// only support local publish
const stopPublishBot = useRecoilCallback(
@@ -207,7 +210,7 @@ export const publisherDispatcher = () => {
const { set } = callbackHelpers;
try {
await httpClient.post(`/publish/${projectId}/stopPublish/${target.name}`);
- set(botStatusState, BotStatus.unConnected);
+ set(botStatusState(projectId), BotStatus.unConnected);
} catch (err) {
setError(callbackHelpers, err);
logMessage(callbackHelpers, err.message);
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts
index 62dc9bbfc5..aac98d04f8 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts
@@ -5,7 +5,7 @@ import { QnAFile } from '@bfc/shared';
import { useRecoilCallback, CallbackInterface } from 'recoil';
import qnaWorker from '../parsers/qnaWorker';
-import { qnaFilesState, qnaAllUpViewStatusState, projectIdState, localeState, settingsState } from '../atoms/botState';
+import { qnaFilesState, qnaAllUpViewStatusState, localeState, settingsState } from '../atoms/botState';
import { QnAAllUpViewStatus } from '../types';
import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage';
import { getBaseName } from '../../utils/fileUtil';
@@ -15,10 +15,10 @@ import { setError } from './shared';
export const updateQnAFileState = async (
callbackHelpers: CallbackInterface,
- { id, content }: { id: string; content: string }
+ { id, content, projectId }: { id: string; content: string; projectId: string }
) => {
const { set, snapshot } = callbackHelpers;
- const qnaFiles = await snapshot.getPromise(qnaFilesState);
+ const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
const updatedQnAFile = (await qnaWorker.parse(id, content)) as QnAFile;
const newQnAFiles = qnaFiles.map((file) => {
if (file.id === id) {
@@ -27,18 +27,17 @@ export const updateQnAFileState = async (
return file;
});
- set(qnaFilesState, newQnAFiles);
+ set(qnaFilesState(projectId), newQnAFiles);
};
export const createQnAFileState = async (
callbackHelpers: CallbackInterface,
- { id, content }: { id: string; content: string }
+ { id, content, projectId }: { id: string; content: string; projectId: string }
) => {
const { set, snapshot } = callbackHelpers;
- const qnaFiles = await snapshot.getPromise(qnaFilesState);
- const projectId = await snapshot.getPromise(projectIdState);
- const locale = await snapshot.getPromise(localeState);
- const { languages } = await snapshot.getPromise(settingsState);
+ const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const locale = await snapshot.getPromise(localeState(projectId));
+ const { languages } = await snapshot.getPromise(settingsState(projectId));
const createdQnaId = `${id}.${locale}`;
const createdQnaFile = (await qnaWorker.parse(id, content)) as QnAFile;
if (qnaFiles.find((qna) => qna.id === createdQnaId)) {
@@ -56,13 +55,15 @@ export const createQnAFileState = async (
});
});
- set(qnaFilesState, [...qnaFiles, ...changes]);
+ set(qnaFilesState(projectId), [...qnaFiles, ...changes]);
};
-export const removeQnAFileState = async (callbackHelpers: CallbackInterface, { id }: { id: string }) => {
+export const removeQnAFileState = async (
+ callbackHelpers: CallbackInterface,
+ { id, projectId }: { id: string; projectId: string }
+) => {
const { set, snapshot } = callbackHelpers;
- let qnaFiles = await snapshot.getPromise(qnaFilesState);
- const projectId = await snapshot.getPromise(projectIdState);
+ let qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
qnaFiles.forEach((file) => {
if (getBaseName(file.id) === getBaseName(id)) {
@@ -71,40 +72,64 @@ export const removeQnAFileState = async (callbackHelpers: CallbackInterface, { i
});
qnaFiles = qnaFiles.filter((file) => getBaseName(file.id) !== id);
- set(qnaFilesState, qnaFiles);
+ set(qnaFilesState(projectId), qnaFiles);
};
export const qnaDispatcher = () => {
const updateQnAFile = useRecoilCallback(
- (callbackHelpers: CallbackInterface) => async ({ id, content }: { id: string; content: string }) => {
- await updateQnAFileState(callbackHelpers, { id, content });
+ (callbackHelpers: CallbackInterface) => async ({
+ id,
+ content,
+ projectId,
+ }: {
+ id: string;
+ content: string;
+ projectId: string;
+ }) => {
+ await updateQnAFileState(callbackHelpers, { id, content, projectId });
}
);
const createQnAFile = useRecoilCallback(
- (callbackHelpers: CallbackInterface) => async ({ id, content }: { id: string; content: string }) => {
- await createQnAFileState(callbackHelpers, { id, content });
+ (callbackHelpers: CallbackInterface) => async ({
+ id,
+ content,
+ projectId,
+ }: {
+ id: string;
+ content: string;
+ projectId: string;
+ }) => {
+ await createQnAFileState(callbackHelpers, { id, content, projectId });
}
);
const importQnAFromUrls = useRecoilCallback(
- (callbackHelpers: CallbackInterface) => async ({ id, urls }: { id: string; urls: string[] }) => {
+ (callbackHelpers: CallbackInterface) => async ({
+ id,
+ urls,
+ projectId,
+ }: {
+ id: string;
+ urls: string[];
+ projectId: string;
+ }) => {
const { set, snapshot } = callbackHelpers;
- const qnaFiles = await snapshot.getPromise(qnaFilesState);
+ const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
const qnaFile = qnaFiles.find((f) => f.id === id);
- set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Loading);
+ set(qnaAllUpViewStatusState(projectId), QnAAllUpViewStatus.Loading);
try {
const response = await httpClient.get(`/utilities/qna/parse`, {
params: { urls: encodeURIComponent(urls.join(',')) },
});
const content = qnaFile ? qnaFile.content + '\n' + response.data : response.data;
- await updateQnAFileState(callbackHelpers, { id, content });
- set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Success);
+ await updateQnAFileState(callbackHelpers, { id, content, projectId });
+ set(qnaAllUpViewStatusState(projectId), QnAAllUpViewStatus.Success);
} catch (err) {
setError(callbackHelpers, err);
} finally {
- set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Success);
+ set(qnaAllUpViewStatusState(projectId), QnAAllUpViewStatus.Success);
}
}
);
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/setting.ts b/Composer/packages/client/src/recoilModel/dispatchers/setting.ts
index aa2e75964d..b8fc5f344c 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/setting.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/setting.ts
@@ -23,13 +23,13 @@ export const settingsDispatcher = () => {
settingStorage.setField(projectId, property, propertyValue);
}
}
- set(settingsState, settings);
+ set(settingsState(projectId), settings);
}
);
const setPublishTargets = useRecoilCallback(
- ({ set }: CallbackInterface) => async (publishTargets: PublishTarget[]) => {
- set(settingsState, (settings) => ({
+ ({ set }: CallbackInterface) => async (publishTargets: PublishTarget[], projectId: string) => {
+ set(settingsState(projectId), (settings) => ({
...settings,
publishTargets,
}));
@@ -38,10 +38,10 @@ export const settingsDispatcher = () => {
const setRuntimeSettings = useRecoilCallback(
({ set }: CallbackInterface) => async (
- _,
+ projectId: string,
runtime: { path: string; command: string; key: string; name: string }
) => {
- set(settingsState, (currentSettingsState) => ({
+ set(settingsState(projectId), (currentSettingsState) => ({
...currentSettingsState,
runtime: {
...runtime,
@@ -52,8 +52,8 @@ export const settingsDispatcher = () => {
);
const setRuntimeField = useRecoilCallback(
- ({ set }: CallbackInterface) => async (_, field: string, newValue: boolean) => {
- set(settingsState, (currentValue) => ({
+ ({ set }: CallbackInterface) => async (projectId: string, field: string, newValue: boolean) => {
+ set(settingsState(projectId), (currentValue) => ({
...currentValue,
runtime: {
...currentValue.runtime,
@@ -63,8 +63,8 @@ export const settingsDispatcher = () => {
}
);
- const setCustomRuntime = useRecoilCallback(() => async (_, isOn: boolean) => {
- setRuntimeField('', 'customRuntime', isOn);
+ const setCustomRuntime = useRecoilCallback(() => async (projectId: string, isOn: boolean) => {
+ setRuntimeField(projectId, 'customRuntime', isOn);
});
const setQnASettings = useRecoilCallback(
@@ -76,7 +76,7 @@ export const settingsDispatcher = () => {
subscriptionKey,
});
settingStorage.setField(projectId, 'qna.endpointKey', response.data);
- set(settingsState, (currentValue) => ({
+ set(settingsState(projectId), (currentValue) => ({
...currentValue,
qna: {
...currentValue.qna,
@@ -90,11 +90,15 @@ export const settingsDispatcher = () => {
);
const updateSkillsInSetting = useRecoilCallback(
- ({ set, snapshot }: CallbackInterface) => async (skillName: string, skillInfo: Partial) => {
- const currentSettings: DialogSetting = await snapshot.getPromise(settingsState);
+ ({ set, snapshot }: CallbackInterface) => async (
+ skillName: string,
+ skillInfo: Partial,
+ projectId: string
+ ) => {
+ const currentSettings: DialogSetting = await snapshot.getPromise(settingsState(projectId));
const matchedSkill = get(currentSettings, `skill[${skillName}]`, undefined);
if (matchedSkill) {
- set(settingsState, {
+ set(settingsState(projectId), {
...currentSettings,
skill: {
...currentSettings.skill,
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/skill.ts b/Composer/packages/client/src/recoilModel/dispatchers/skill.ts
index 249c116075..d728fa46a6 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/skill.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/skill.ts
@@ -18,22 +18,24 @@ import {
import { logMessage } from './shared';
export const skillDispatcher = () => {
- const createSkillManifest = useRecoilCallback(({ set }: CallbackInterface) => async ({ id, content }) => {
- set(skillManifestsState, (skillManifests) => [...skillManifests, { content, id }]);
- });
+ const createSkillManifest = ({ set }, { id, content, projectId }) => {
+ set(skillManifestsState(projectId), (skillManifests) => [...skillManifests, { content, id }]);
+ };
- const removeSkillManifest = useRecoilCallback(({ set }: CallbackInterface) => async (id: string) => {
- set(skillManifestsState, (skillManifests) => skillManifests.filter((manifest) => manifest.id !== id));
- });
+ const removeSkillManifest = useRecoilCallback(
+ ({ set }: CallbackInterface) => async (id: string, projectId: string) => {
+ set(skillManifestsState(projectId), (skillManifests) => skillManifests.filter((manifest) => manifest.id !== id));
+ }
+ );
const updateSkillManifest = useRecoilCallback(
- ({ set, snapshot }: CallbackInterface) => async ({ id, content }: SkillManifest) => {
- const manifests = await snapshot.getPromise(skillManifestsState);
+ ({ set, snapshot }: CallbackInterface) => async ({ id, content }: SkillManifest, projectId: string) => {
+ const manifests = await snapshot.getPromise(skillManifestsState(projectId));
if (!manifests.some((manifest) => manifest.id === id)) {
- createSkillManifest({ id, content });
+ createSkillManifest({ set }, { id, content, projectId });
}
- set(skillManifestsState, (skillManifests) =>
+ set(skillManifestsState(projectId), (skillManifests) =>
skillManifests.map((manifest) => (manifest.id === id ? { id, content } : manifest))
);
}
@@ -49,11 +51,11 @@ export const skillDispatcher = () => {
const { data: skills } = await httpClient.post(`/projects/${projectId}/skills/`, { skills: updatedSkills });
- set(settingsState, (settings) => ({
+ set(settingsState(projectId), (settings) => ({
...settings,
skill: convertSkillsToDictionary(skills),
}));
- set(skillsState, skills);
+ set(skillsState(projectId), skills);
return skills;
} catch (error) {
@@ -64,9 +66,9 @@ export const skillDispatcher = () => {
const addSkill = useRecoilCallback(
(callbackHelpers: CallbackInterface) => async (projectId: string, skillData: Skill) => {
const { set, snapshot } = callbackHelpers;
- const { func: onAddSkillDialogComplete } = await snapshot.getPromise(onAddSkillDialogCompleteState);
+ const { func: onAddSkillDialogComplete } = await snapshot.getPromise(onAddSkillDialogCompleteState(projectId));
const skills = await updateSkillState(callbackHelpers, projectId, [
- ...(await snapshot.getPromise(skillsState)),
+ ...(await snapshot.getPromise(skillsState(projectId))),
skillData,
]);
@@ -76,15 +78,16 @@ export const skillDispatcher = () => {
onAddSkillDialogComplete(skill || null);
}
- set(showAddSkillDialogModalState, false);
- set(onAddSkillDialogCompleteState, {});
+ set(showAddSkillDialogModalState(projectId), false);
+ set(onAddSkillDialogCompleteState(projectId), {});
}
);
const removeSkill = useRecoilCallback(
(callbackHelpers: CallbackInterface) => async (projectId: string, manifestUrl?: string) => {
const { snapshot } = callbackHelpers;
- const skills = [...(await snapshot.getPromise(skillsState))].filter((skill) => skill.manifestUrl !== manifestUrl);
+ const currentSkills = await snapshot.getPromise(skillsState(projectId));
+ const skills = currentSkills.filter((skill) => skill.manifestUrl !== manifestUrl);
await updateSkillState(callbackHelpers, projectId, skills);
}
);
@@ -95,7 +98,7 @@ export const skillDispatcher = () => {
{ targetId, skillData }: { targetId: number; skillData?: any }
) => {
const { snapshot } = callbackHelpers;
- const originSkills = [...(await snapshot.getPromise(skillsState))];
+ const originSkills = [...(await snapshot.getPromise(skillsState(projectId)))];
if (targetId >= 0 && targetId < originSkills.length && skillData) {
originSkills.splice(targetId, 1, skillData);
@@ -107,32 +110,34 @@ export const skillDispatcher = () => {
}
);
- const addSkillDialogBegin = useRecoilCallback(({ set }: CallbackInterface) => (onComplete) => {
- set(showAddSkillDialogModalState, true);
- set(onAddSkillDialogCompleteState, { func: onComplete });
+ const addSkillDialogBegin = useRecoilCallback(({ set }: CallbackInterface) => (onComplete, projectId: string) => {
+ set(showAddSkillDialogModalState(projectId), true);
+ set(onAddSkillDialogCompleteState(projectId), { func: onComplete });
});
- const addSkillDialogCancel = useRecoilCallback(({ set }: CallbackInterface) => () => {
- set(showAddSkillDialogModalState, false);
- set(onAddSkillDialogCompleteState, { func: undefined });
+ const addSkillDialogCancel = useRecoilCallback(({ set }: CallbackInterface) => (projectId: string) => {
+ set(showAddSkillDialogModalState(projectId), false);
+ set(onAddSkillDialogCompleteState(projectId), { func: undefined });
});
- const addSkillDialogSuccess = useRecoilCallback(({ set, snapshot }: CallbackInterface) => async () => {
- const onAddSkillDialogComplete = (await snapshot.getPromise(onAddSkillDialogCompleteState)).func;
- if (typeof onAddSkillDialogComplete === 'function') {
- onAddSkillDialogComplete(null);
- }
+ const addSkillDialogSuccess = useRecoilCallback(
+ ({ set, snapshot }: CallbackInterface) => async (projectId: string) => {
+ const onAddSkillDialogComplete = (await snapshot.getPromise(onAddSkillDialogCompleteState(projectId))).func;
+ if (typeof onAddSkillDialogComplete === 'function') {
+ onAddSkillDialogComplete(null);
+ }
- set(showAddSkillDialogModalState, false);
- set(onAddSkillDialogCompleteState, { func: undefined });
- });
+ set(showAddSkillDialogModalState(projectId), false);
+ set(onAddSkillDialogCompleteState(projectId), { func: undefined });
+ }
+ );
- const displayManifestModal = useRecoilCallback(({ set }: CallbackInterface) => (id: string) => {
- set(displaySkillManifestState, id);
+ const displayManifestModal = useRecoilCallback(({ set }: CallbackInterface) => (id: string, projectId: string) => {
+ set(displaySkillManifestState(projectId), id);
});
- const dismissManifestModal = useRecoilCallback(({ set }: CallbackInterface) => () => {
- set(displaySkillManifestState, undefined);
+ const dismissManifestModal = useRecoilCallback(({ set }: CallbackInterface) => (projectId: string) => {
+ set(displaySkillManifestState(projectId), undefined);
});
return {
diff --git a/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts b/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts
index f9d871abc7..16a098ea40 100644
--- a/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts
+++ b/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts
@@ -21,6 +21,10 @@ class FilePersistence {
[ChangeType.DELETE]: this.delete,
};
+ constructor(projectId: string) {
+ this._projectId = projectId;
+ }
+
public get projectId(): string {
return this._projectId;
}
@@ -30,12 +34,6 @@ class FilePersistence {
}
public async notify(currentAssets: BotAssets, previousAssets: BotAssets) {
- if (!currentAssets.projectId) return;
- if (currentAssets.projectId !== previousAssets.projectId) {
- this.init(currentAssets.projectId);
- return;
- }
-
const fileChanges: IFileChange[] = this.getAssetsChanges(currentAssets, previousAssets);
for (const change of fileChanges) {
@@ -48,25 +46,6 @@ class FilePersistence {
await this.flush();
}
- // public registerHandleError(store: Store) {
- // const curStore = store;
- // this._handleError = (name) => (err) => {
- // //TODO: error handling now if sync file error, do a full refresh.
- // const fileName = name;
- // setError(curStore, {
- // message: err.response && err.response.data.message ? err.response.data.message : err,
- // summary: `HANDLE ${fileName} ERROR`,
- // });
- // fetchProject(curStore);
- // };
- // }
-
- private init(projectId: string) {
- if (projectId) {
- this._projectId = projectId;
- }
- }
-
public async flush(): Promise {
try {
if (this._isFlushing) {
@@ -243,6 +222,4 @@ class FilePersistence {
}
}
-const filePersistence = new FilePersistence();
-
-export default filePersistence;
+export default FilePersistence;
diff --git a/Composer/packages/client/src/recoilModel/persistence/__test__/FilePersistence.test.ts b/Composer/packages/client/src/recoilModel/persistence/__test__/FilePersistence.test.ts
index 55a2d6b271..64b27a9cfb 100644
--- a/Composer/packages/client/src/recoilModel/persistence/__test__/FilePersistence.test.ts
+++ b/Composer/packages/client/src/recoilModel/persistence/__test__/FilePersistence.test.ts
@@ -2,7 +2,8 @@
// Licensed under the MIT License.
import { DialogInfo, DialogSchemaFile, LgFile, LuFile, BotAssets } from '@bfc/shared';
-import filePersistence from '../FilePersistence';
+import FilePersistence from '../FilePersistence';
+const projectId = '2123.2234as';
jest.mock('axios', () => {
return {
@@ -17,15 +18,9 @@ jest.mock('axios', () => {
});
describe('test persistence layer', () => {
- it('test init persistence', async () => {
- expect(filePersistence.projectId).toBe('');
- const current = { projectId: '' } as BotAssets;
- const previous = { projectId: '' } as BotAssets;
- await filePersistence.notify(current, previous);
- expect(filePersistence.projectId).toBe('');
- current.projectId = 'test';
- await filePersistence.notify(current, previous);
- expect(filePersistence.projectId).toBe('test');
+ let filePersistence: FilePersistence;
+ beforeEach(() => {
+ filePersistence = new FilePersistence(projectId);
});
it('test notify update', async () => {
diff --git a/Composer/packages/client/src/recoilModel/selectors/design.ts b/Composer/packages/client/src/recoilModel/selectors/design.ts
new file mode 100644
index 0000000000..7814bdfd45
--- /dev/null
+++ b/Composer/packages/client/src/recoilModel/selectors/design.ts
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { selector } from 'recoil';
+
+import { botNameState, botProjectsSpaceState } from '../atoms';
+
+//TODO: This selector will be used when BotProjects is implemented
+export const botProjectSpaceSelector = selector({
+ key: 'botProjectSpaceSelector',
+ get: ({ get }) => {
+ const botProjects = get(botProjectsSpaceState);
+ const result = botProjects.map((botProjectId: string) => {
+ const name = get(botNameState(botProjectId));
+ return { projectId: botProjectId, name };
+ });
+ return result;
+ },
+});
diff --git a/Composer/packages/client/src/recoilModel/selectors/eject.ts b/Composer/packages/client/src/recoilModel/selectors/eject.ts
index cb0ba689cf..c731dfcb03 100644
--- a/Composer/packages/client/src/recoilModel/selectors/eject.ts
+++ b/Composer/packages/client/src/recoilModel/selectors/eject.ts
@@ -15,7 +15,7 @@ const ejectRuntimeAction = (dispatcher: Dispatcher) => {
return {
onAction: async (projectId: string, name: string, replace = false) => {
try {
- dispatcher.setEjectRuntimeExist(false);
+ dispatcher.setEjectRuntimeExist(false, projectId);
const response = await httpClient.post(`/runtime/eject/${projectId}/${name}`, { isReplace: replace });
if (!lodashGet(response, 'data.settings.path', '') || !lodashGet(response, 'data.settings.startCommand', '')) {
throw new Error('Runtime cannot be ejected');
@@ -30,7 +30,7 @@ const ejectRuntimeAction = (dispatcher: Dispatcher) => {
typeof ex.response.data.message === 'string' &&
ex.response.data.message.includes('Runtime already exists')
) {
- dispatcher.setEjectRuntimeExist(true);
+ dispatcher.setEjectRuntimeExist(true, projectId);
} else {
const errorToShow: StateError = {
message: ex.response?.data?.message || ex.response?.data || ex.message,
diff --git a/Composer/packages/client/src/recoilModel/selectors/index.ts b/Composer/packages/client/src/recoilModel/selectors/index.ts
index d149c57ac0..2679b02214 100644
--- a/Composer/packages/client/src/recoilModel/selectors/index.ts
+++ b/Composer/packages/client/src/recoilModel/selectors/index.ts
@@ -2,3 +2,5 @@
// Licensed under the MIT License.
export * from '../selectors/eject';
+export * from '../selectors/design';
+export * from '../selectors/validatedDialogs';
diff --git a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts
index 649b82fed0..cf92edf094 100644
--- a/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts
+++ b/Composer/packages/client/src/recoilModel/selectors/validatedDialogs.ts
@@ -1,18 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-import { selector } from 'recoil';
+import { selectorFamily } from 'recoil';
import { validateDialog } from '@bfc/indexers';
import { dialogsState, schemasState, lgFilesState, luFilesState } from '../atoms/botState';
-export const validatedDialogsSelector = selector({
- key: 'validatedDialogsSelector',
- get: ({ get }) => {
- const dialogs = get(dialogsState);
- const schemas = get(schemasState);
- const lgFiles = get(lgFilesState);
- const luFiles = get(luFilesState);
+export const validateDialogSelectorFamily = selectorFamily({
+ key: 'validateDialogSelectorFamily',
+ get: (projectId: string) => ({ get }) => {
+ const dialogs = get(dialogsState(projectId));
+ const schemas = get(schemasState(projectId));
+ const lgFiles = get(lgFilesState(projectId));
+ const luFiles = get(luFilesState(projectId));
return dialogs.map((dialog) => {
return { ...dialog, diagnostics: validateDialog(dialog, schemas.sdk.content, lgFiles, luFiles) };
});
diff --git a/Composer/packages/client/src/recoilModel/types.ts b/Composer/packages/client/src/recoilModel/types.ts
index 99949fe6c7..38c3748f73 100644
--- a/Composer/packages/client/src/recoilModel/types.ts
+++ b/Composer/packages/client/src/recoilModel/types.ts
@@ -72,7 +72,6 @@ export interface BotLoadError {
}
export interface DesignPageLocation {
- projectId: string;
dialogId: string;
selected: string;
focused: string;
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 ba0df829de..e64a06987e 100644
--- a/Composer/packages/client/src/recoilModel/undo/__test__/history.test.tsx
+++ b/Composer/packages/client/src/recoilModel/undo/__test__/history.test.tsx
@@ -1,23 +1,39 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-import React, { useRef } from 'react';
+/** @jsx jsx */
+import { jsx } from '@emotion/core';
import { act } from 'react-test-renderer';
import { useRecoilValue, useSetRecoilState, useRecoilState } from 'recoil';
-import { UndoRoot, undoFunctionState } from '../history';
-import { dialogsState, lgFilesState, luFilesState, projectIdState } from '../../atoms';
+import { UndoRoot, undoFunctionState, undoHistoryState } from '../history';
+import {
+ dialogsState,
+ lgFilesState,
+ luFilesState,
+ projectMetaDataState,
+ currentProjectIdState,
+ botProjectsSpaceState,
+} from '../../atoms';
import { renderRecoilHook } from '../../../../__tests__/testUtils/react-recoil-hooks-testing-library';
-import undoHistory from '../undoHistory';
+import UndoHistory from '../undoHistory';
+const projectId = '123-asd';
+
+export const UndoRedoWrapper = () => {
+ const botProjects = useRecoilValue(botProjectsSpaceState);
+
+ return botProjects.length > 0 ? : null;
+};
describe('', () => {
let renderedComponent;
+
beforeEach(() => {
const useRecoilTestHook = () => {
- const { undo, redo, canRedo, canUndo, commitChanges, clearUndo } = useRecoilValue(undoFunctionState);
- const [dialogs, setDialogs] = useRecoilState(dialogsState);
- const setProjectIdState = useSetRecoilState(projectIdState);
- const history = useRef(undoHistory).current;
+ const { undo, redo, canRedo, canUndo, commitChanges, clearUndo } = useRecoilValue(undoFunctionState(projectId));
+ const [dialogs, setDialogs] = useRecoilState(dialogsState(projectId));
+ const setProjectIdState = useSetRecoilState(currentProjectIdState);
+ const history = useRecoilValue(undoHistoryState(projectId));
return {
undo,
@@ -28,25 +44,29 @@ describe('', () => {
clearUndo,
setProjectIdState,
setDialogs,
- history,
dialogs,
+ history,
};
};
const { result } = renderRecoilHook(useRecoilTestHook, {
- wrapper: ({ children }) => (
-
-
- {children}
-
- ),
+ wrapper: ({ children }) => {
+ return (
+
+
+ {children}
+
+ );
+ },
states: [
- { recoilState: dialogsState, initialValue: [{ id: '1' }] },
- { recoilState: lgFilesState, initialValue: [{ id: '1.lg' }, { id: '2' }] },
- { recoilState: luFilesState, initialValue: [{ id: '1.lu' }, { id: '2' }] },
- { recoilState: projectIdState, initialValue: '' },
+ { recoilState: botProjectsSpaceState, initialValue: [projectId] },
+ { recoilState: dialogsState(projectId), initialValue: [{ id: '1' }] },
+ { recoilState: lgFilesState(projectId), initialValue: [{ id: '1.lg' }, { id: '2' }] },
+ { recoilState: luFilesState(projectId), initialValue: [{ id: '1.lu' }, { id: '2' }] },
+ { recoilState: currentProjectIdState, initialValue: projectId },
+ { recoilState: undoHistoryState(projectId), initialValue: new UndoHistory(projectId) },
{
- recoilState: undoFunctionState,
+ recoilState: undoFunctionState(projectId),
initialValue: {
undo: jest.fn(),
redo: jest.fn(),
@@ -56,19 +76,16 @@ describe('', () => {
clearUndo: jest.fn(),
},
},
+ { recoilState: projectMetaDataState(projectId), initialValue: { isRootBot: true } },
],
});
renderedComponent = result;
});
- it('should add first snapshot', () => {
- act(() => {
- renderedComponent.current.setProjectIdState('test');
- });
-
- expect(renderedComponent.current.history.stack.length).toBe(1);
- expect(renderedComponent.current.dialogs).toStrictEqual([{ id: '1' }]);
- });
+ // it('should add first snapshot', () => {
+ // expect(renderedComponent.current.history.stack.length).toBe(1);
+ // expect(renderedComponent.current.dialogs).toStrictEqual([{ id: '1' }]);
+ // });
it('should commit one change', () => {
act(() => {
@@ -83,11 +100,18 @@ describe('', () => {
});
it('should undo', () => {
+ act(() => {
+ renderedComponent.current.setDialogs([]);
+ });
+ act(() => {
+ renderedComponent.current.commitChanges();
+ });
+
expect(renderedComponent.current.canUndo()).toBeTruthy();
+
act(() => {
renderedComponent.current.undo();
});
-
expect(renderedComponent.current.history.stack.length).toBe(2);
expect(renderedComponent.current.dialogs).toStrictEqual([{ id: '1' }]);
expect(renderedComponent.current.canRedo()).toBeTruthy();
@@ -97,24 +121,33 @@ describe('', () => {
act(() => {
renderedComponent.current.setDialogs([{ id: '2' }]);
});
+
act(() => {
renderedComponent.current.commitChanges();
});
+
expect(renderedComponent.current.history.stack.length).toBe(2);
expect(renderedComponent.current.dialogs).toStrictEqual([{ id: '2' }]);
});
it('should redo', () => {
+ act(() => {
+ renderedComponent.current.setDialogs([{ id: '2' }]);
+ });
+ act(() => {
+ renderedComponent.current.commitChanges();
+ });
expect(renderedComponent.current.canRedo()).toBeFalsy();
+
act(() => {
renderedComponent.current.undo();
});
expect(renderedComponent.current.dialogs).toStrictEqual([{ id: '1' }]);
expect(renderedComponent.current.canRedo()).toBeTruthy();
+
act(() => {
renderedComponent.current.redo();
});
-
expect(renderedComponent.current.history.stack.length).toBe(2);
expect(renderedComponent.current.dialogs).toStrictEqual([{ id: '2' }]);
});
diff --git a/Composer/packages/client/src/recoilModel/undo/__test__/undoHistory.test.ts b/Composer/packages/client/src/recoilModel/undo/__test__/undoHistory.test.ts
index 30e14ad712..1da24eeba7 100644
--- a/Composer/packages/client/src/recoilModel/undo/__test__/undoHistory.test.ts
+++ b/Composer/packages/client/src/recoilModel/undo/__test__/undoHistory.test.ts
@@ -1,29 +1,32 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-import undoHistory from '../undoHistory';
+import undoHistoryImpl from '../undoHistory';
import { dialogsState } from './../../atoms/botState';
+const projectId = '12a-sdaas';
+const undoHistory = new undoHistoryImpl(projectId);
+
describe('undoHistory class', () => {
it('should add value to stack', () => {
- undoHistory.add(new Map().set(dialogsState, 'stack 1'));
+ undoHistory.add(new Map().set(dialogsState(projectId), 'stack 1'));
expect(undoHistory.canUndo()).toBeFalsy();
- undoHistory.add(new Map().set(dialogsState, 'stack 2'));
+ undoHistory.add(new Map().set(dialogsState(projectId), 'stack 2'));
expect(undoHistory.canUndo()).toBeTruthy();
- expect(undoHistory.getPresentAssets()?.get(dialogsState)).toBe('stack 2');
+ expect(undoHistory.getPresentAssets()?.get(dialogsState(projectId))).toBe('stack 2');
});
it('should do undo', () => {
expect(undoHistory.canUndo()).toBeTruthy();
const result = undoHistory.undo();
- expect(result.get(dialogsState)).toBe('stack 1');
+ expect(result.get(dialogsState(projectId))).toBe('stack 1');
expect(undoHistory.stack.length).toBe(2);
});
it('should remove the tail stack value when add a new one after undo ', () => {
- undoHistory.add(new Map().set(dialogsState, 'stack 3'));
- expect(undoHistory.getPresentAssets()?.get(dialogsState)).toBe('stack 3');
+ undoHistory.add(new Map().set(dialogsState(projectId), 'stack 3'));
+ expect(undoHistory.getPresentAssets()?.get(dialogsState(projectId))).toBe('stack 3');
expect(undoHistory.stack.length).toBe(2);
});
@@ -32,13 +35,13 @@ describe('undoHistory class', () => {
undoHistory.undo();
expect(undoHistory.canRedo()).toBeTruthy();
const result = undoHistory.redo();
- expect(result.get(dialogsState)).toBe('stack 3');
+ expect(result.get(dialogsState(projectId))).toBe('stack 3');
expect(undoHistory.stack.length).toBe(2);
});
it('should replace the last stack value', () => {
- undoHistory.replace(new Map().set(dialogsState, 'stack 4'));
- expect(undoHistory.getPresentAssets()?.get(dialogsState)).toBe('stack 4');
+ undoHistory.replace(new Map().set(dialogsState(projectId), 'stack 4'));
+ expect(undoHistory.getPresentAssets()?.get(dialogsState(projectId))).toBe('stack 4');
expect(undoHistory.stack.length).toBe(2);
});
@@ -47,12 +50,12 @@ describe('undoHistory class', () => {
expect(undoHistory.stack.length).toBe(0);
});
- it('should only support 30 history', () => {
+ it('should only support 30 actions in history', () => {
for (let i = 0; i < 40; i++) {
- undoHistory.add(new Map().set(dialogsState, `${i}`));
+ undoHistory.add(new Map().set(dialogsState(projectId), `${i}`));
}
expect(undoHistory.stack.length).toBe(30);
- expect(undoHistory.getPresentAssets()?.get(dialogsState)).toBe('39');
- expect(undoHistory.stack[0].get(dialogsState)).toBe('10');
+ expect(undoHistory.getPresentAssets()?.get(dialogsState(projectId))).toBe('39');
+ expect(undoHistory.stack[0].get(dialogsState(projectId))).toBe('10');
});
});
diff --git a/Composer/packages/client/src/recoilModel/undo/history.ts b/Composer/packages/client/src/recoilModel/undo/history.ts
index 51ad2d7c4c..b36e4f16e4 100644
--- a/Composer/packages/client/src/recoilModel/undo/history.ts
+++ b/Composer/packages/client/src/recoilModel/undo/history.ts
@@ -2,53 +2,65 @@
// Licensed under the MIT License.
import React, { useEffect, useRef, useCallback, useState } from 'react';
-import { useRecoilTransactionObserver_UNSTABLE as useRecoilTransactionObserver, RecoilState } from 'recoil';
-import { atom, Snapshot, useRecoilCallback, CallbackInterface, useSetRecoilState } from 'recoil';
+import {
+ useRecoilTransactionObserver_UNSTABLE as useRecoilTransactionObserver,
+ RecoilState,
+ useRecoilValue,
+} from 'recoil';
+import { atomFamily, Snapshot, useRecoilCallback, CallbackInterface, useSetRecoilState } from 'recoil';
import uniqueId from 'lodash/uniqueId';
-import { projectIdState } from '../atoms';
import { navigateTo, getUrlSearch } from '../../utils/navigation';
import { breadcrumbState } from './../atoms/botState';
-import { designPageLocationState } from './../atoms/botState';
-import undoHistory, { UndoHistory } from './undoHistory';
+import { designPageLocationState } from './../atoms';
import { trackedAtoms, AtomAssetsMap } from './trackedAtoms';
+import UndoHistory from './undoHistory';
+
+type IUndoRedo = {
+ undo: () => void;
+ redo: () => void;
+ canUndo: () => boolean;
+ canRedo: () => boolean;
+ commitChanges: () => void;
+ clearUndo: () => void;
+};
-export const undoFunctionState = atom({
+export const undoFunctionState = atomFamily({
key: 'undoFunction',
- default: {
- undo: () => {},
- redo: () => {},
- canUndo: (): boolean => false,
- canRedo: (): boolean => false,
- commitChanges: () => {},
- clearUndo: () => {},
- },
+ default: {} as IUndoRedo,
+ dangerouslyAllowMutability: true,
+});
+
+export const undoHistoryState = atomFamily({
+ key: 'undoHistory',
+ default: {} as UndoHistory,
+ dangerouslyAllowMutability: true,
});
-export const undoVersionState = atom({
+export const undoVersionState = atomFamily({
key: 'version',
default: '',
+ dangerouslyAllowMutability: true,
});
-const getAtomAssetsMap = (snap: Snapshot): AtomAssetsMap => {
+const getAtomAssetsMap = (snap: Snapshot, projectId: string): AtomAssetsMap => {
const atomMap = new Map, any>();
- trackedAtoms.forEach((atom) => {
+ const atomsToBeTracked = trackedAtoms(projectId);
+ atomsToBeTracked.forEach((atom) => {
const loadable = snap.getLoadable(atom);
atomMap.set(atom, loadable.state === 'hasValue' ? loadable.contents : null);
});
//should record the location state
- atomMap.set(designPageLocationState, snap.getLoadable(designPageLocationState).contents);
- atomMap.set(projectIdState, snap.getLoadable(projectIdState).contents);
- atomMap.set(breadcrumbState, snap.getLoadable(breadcrumbState).contents);
+ atomMap.set(designPageLocationState(projectId), snap.getLoadable(designPageLocationState(projectId)).contents);
+ atomMap.set(breadcrumbState(projectId), snap.getLoadable(breadcrumbState(projectId)).contents);
return atomMap;
};
const checkAtomChanged = (current: AtomAssetsMap, previous: AtomAssetsMap, atom: RecoilState) => {
const currVal = current.get(atom);
const prevVal = previous.get(atom);
-
if (prevVal !== currVal) {
return true;
}
@@ -60,11 +72,11 @@ const checkAtomsChanged = (current: AtomAssetsMap, previous: AtomAssetsMap, atom
return atoms.some((atom) => checkAtomChanged(current, previous, atom));
};
-function navigate(next: AtomAssetsMap) {
- const location = next.get(designPageLocationState);
- const breadcrumb = [...next.get(breadcrumbState)];
+function navigate(next: AtomAssetsMap, projectId: string) {
+ const location = next.get(designPageLocationState(projectId));
+ const breadcrumb = [...next.get(breadcrumbState(projectId))];
if (location) {
- const { dialogId, selected, focused, projectId, promptTab } = location;
+ const { dialogId, selected, focused, promptTab } = location;
let currentUri = `/bot/${projectId}/dialogs/${dialogId}${getUrlSearch(selected, focused)}`;
if (promptTab) {
currentUri += `#${promptTab}`;
@@ -77,9 +89,10 @@ function navigate(next: AtomAssetsMap) {
function mapTrackedAtomsOntoSnapshot(
target: Snapshot,
currentAssets: AtomAssetsMap,
- nextAssets: AtomAssetsMap
+ nextAssets: AtomAssetsMap,
+ projectId: string
): Snapshot {
- trackedAtoms.forEach((atom) => {
+ trackedAtoms(projectId).forEach((atom) => {
const current = currentAssets.get(atom);
const next = nextAssets.get(atom);
if (current !== next) {
@@ -89,56 +102,71 @@ function mapTrackedAtomsOntoSnapshot(
return target;
}
-function setInitialLocation(snapshot: Snapshot, undoHistory: UndoHistory) {
- const location = snapshot.getLoadable(designPageLocationState);
- const breadcrumb = snapshot.getLoadable(breadcrumbState);
+function setInitialLocation(snapshot: Snapshot, projectId: string, undoHistory: UndoHistory) {
+ const location = snapshot.getLoadable(designPageLocationState(projectId));
+ const breadcrumb = snapshot.getLoadable(breadcrumbState(projectId));
if (location.state === 'hasValue') {
- undoHistory.setInitialValue(designPageLocationState, location.contents);
- undoHistory.setInitialValue(breadcrumbState, breadcrumb.contents);
+ undoHistory.setInitialValue(designPageLocationState(projectId), location.contents);
+ undoHistory.setInitialValue(breadcrumbState(projectId), breadcrumb.contents);
}
}
+interface UndoRootProps {
+ projectId: string;
+}
+
+export const UndoRoot = React.memo((props: UndoRootProps) => {
+ const { projectId } = props;
+ const undoHistory = useRecoilValue(undoHistoryState(projectId));
+ const history: UndoHistory = useRef(undoHistory).current;
+ const [initialStateLoaded, setInitialStateLoaded] = useState(false);
-export const UndoRoot = React.memo(() => {
- const history = useRef(undoHistory).current;
- const setUndoFunction = useSetRecoilState(undoFunctionState);
+ const setUndoFunction = useSetRecoilState(undoFunctionState(projectId));
const [, forceUpdate] = useState([]);
- const setVersion = useSetRecoilState(undoVersionState);
+ const setVersion = useSetRecoilState(undoVersionState(projectId));
//use to record the first time change, this will help to get the init location
//init location is used to undo navigate
const assetsChanged = useRef(false);
useRecoilTransactionObserver(({ snapshot, previousSnapshot }) => {
- const currentAssets = getAtomAssetsMap(snapshot);
- const previousAssets = getAtomAssetsMap(previousSnapshot);
- if (checkAtomChanged(currentAssets, previousAssets, projectIdState)) {
- //switch project should clean the undo history
- undoHistory.clear();
- undoHistory.add(getAtomAssetsMap(snapshot));
- } else if (!assetsChanged.current) {
- if (checkAtomsChanged(currentAssets, previousAssets, trackedAtoms)) {
+ if (initialStateLoaded && !assetsChanged.current) {
+ const currentAssets = getAtomAssetsMap(snapshot, projectId);
+ const previousAssets = getAtomAssetsMap(previousSnapshot, projectId);
+ if (checkAtomsChanged(currentAssets, previousAssets, trackedAtoms(projectId))) {
assetsChanged.current = true;
}
- setInitialLocation(snapshot, history);
+ setInitialLocation(snapshot, projectId, history);
}
});
+ const setInitialProjectState = useRecoilCallback(({ snapshot }: CallbackInterface) => () => {
+ undoHistory.clear();
+ const assetMap = getAtomAssetsMap(snapshot, projectId);
+ undoHistory.add(assetMap);
+ setInitialStateLoaded(true);
+ });
+
+ useEffect(() => {
+ setInitialProjectState();
+ }, []);
+
const undoAssets = (
target: Snapshot,
current: AtomAssetsMap,
next: AtomAssetsMap,
- gotoSnapshot: (snapshot: Snapshot) => void
+ gotoSnapshot: (snapshot: Snapshot) => void,
+ projectId: string
) => {
- target = mapTrackedAtomsOntoSnapshot(target, current, next);
+ target = mapTrackedAtomsOntoSnapshot(target, current, next, projectId);
gotoSnapshot(target);
- navigate(next);
+ navigate(next, projectId);
};
const undo = useRecoilCallback(({ snapshot, gotoSnapshot }: CallbackInterface) => () => {
if (history.canUndo()) {
const present = history.getPresentAssets();
const next = history.undo();
- if (present) undoAssets(snapshot, present, next, gotoSnapshot);
+ if (present) undoAssets(snapshot, present, next, gotoSnapshot, projectId);
setVersion(uniqueId());
}
});
@@ -147,7 +175,7 @@ export const UndoRoot = React.memo(() => {
if (history.canRedo()) {
const present = history.getPresentAssets();
const next = history.redo();
- if (present) undoAssets(snapshot, present, next, gotoSnapshot);
+ if (present) undoAssets(snapshot, present, next, gotoSnapshot, projectId);
setVersion(uniqueId());
}
});
@@ -161,11 +189,12 @@ export const UndoRoot = React.memo(() => {
};
const commit = useRecoilCallback(({ snapshot }) => () => {
- const currentAssets = getAtomAssetsMap(snapshot);
+ const currentAssets = getAtomAssetsMap(snapshot, projectId);
const previousAssets = history.getPresentAssets();
//filter some invalid changes
- if (previousAssets && checkAtomsChanged(currentAssets, previousAssets, trackedAtoms)) {
- history.add(getAtomAssetsMap(snapshot));
+
+ if (previousAssets && checkAtomsChanged(currentAssets, previousAssets, trackedAtoms(projectId))) {
+ history.add(getAtomAssetsMap(snapshot, projectId));
}
});
@@ -177,13 +206,13 @@ export const UndoRoot = React.memo(() => {
const clearUndo = useRecoilCallback(({ snapshot }) => () => {
history.clear();
- history.add(getAtomAssetsMap(snapshot));
+ history.add(getAtomAssetsMap(snapshot, projectId));
assetsChanged.current = false;
});
useEffect(() => {
setUndoFunction({ undo, redo, canRedo, canUndo, commitChanges, clearUndo });
- });
+ }, []);
return null;
});
diff --git a/Composer/packages/client/src/recoilModel/undo/trackedAtoms.ts b/Composer/packages/client/src/recoilModel/undo/trackedAtoms.ts
index 3d408c6507..3399bb247f 100644
--- a/Composer/packages/client/src/recoilModel/undo/trackedAtoms.ts
+++ b/Composer/packages/client/src/recoilModel/undo/trackedAtoms.ts
@@ -3,8 +3,10 @@
import { RecoilState } from 'recoil';
-import { dialogsState, luFilesState, lgFilesState } from './../atoms/botState';
+import { dialogsState, luFilesState, lgFilesState } from '../atoms';
export type AtomAssetsMap = Map, any>;
-export const trackedAtoms: RecoilState[] = [dialogsState, luFilesState, lgFilesState];
+export const trackedAtoms = (projectId: string): RecoilState[] => {
+ return [dialogsState(projectId), luFilesState(projectId), lgFilesState(projectId)];
+};
diff --git a/Composer/packages/client/src/recoilModel/undo/undoHistory.ts b/Composer/packages/client/src/recoilModel/undo/undoHistory.ts
index 49882ecb4f..8b36c7ebec 100644
--- a/Composer/packages/client/src/recoilModel/undo/undoHistory.ts
+++ b/Composer/packages/client/src/recoilModel/undo/undoHistory.ts
@@ -8,19 +8,27 @@ import { AtomAssetsMap } from './trackedAtoms';
// use number to limit the stack size first
const MAX_STACK_LENGTH = 30;
-export class UndoHistory {
+export default class {
+ private _projectId = '';
+ /**
+ *
+ */
+ constructor(projectId) {
+ this._projectId = projectId;
+ }
+
public stack: AtomAssetsMap[] = [];
public present = -1;
public undo() {
- if (!this.canUndo()) throw new Error(formatMessage('Undo is not support'));
+ if (!this.canUndo()) throw new Error(formatMessage('Undo is not supported'));
this.present = this.present - 1;
return this.stack[this.present];
}
public redo() {
- if (!this.canRedo()) throw new Error(formatMessage('Redo is not support'));
+ if (!this.canRedo()) throw new Error(formatMessage('Redo is not supported'));
this.present = this.present + 1;
return this.stack[this.present];
@@ -37,6 +45,7 @@ export class UndoHistory {
}
this.stack.push(assets);
+
this.present++;
}
@@ -58,11 +67,14 @@ export class UndoHistory {
}
}
- public canUndo = () => this.stack.length > 0 && this.present > 0;
+ public canUndo = () => {
+ return this.stack.length > 0 && this.present > 0;
+ };
public canRedo = () => this.stack.length > 0 && this.present < this.stack.length - 1;
public isEmpty = () => this.stack.length === 0;
public getPresentAssets = () => (this.present > -1 ? this.stack[this.present] : null);
-}
-const undoHistory = new UndoHistory();
-export default undoHistory;
+ public get projectId() {
+ return this._projectId;
+ }
+}
diff --git a/Composer/packages/client/src/router.tsx b/Composer/packages/client/src/router.tsx
index 42ff6b4b40..f24f4013a8 100644
--- a/Composer/packages/client/src/router.tsx
+++ b/Composer/packages/client/src/router.tsx
@@ -12,7 +12,7 @@ import { resolveToBasePath } from './utils/fileUtil';
import { data } from './styles';
import { NotFound } from './components/NotFound';
import { BASEPATH } from './constants';
-import { botOpeningState, projectIdState, dispatcherState, schemasState } from './recoilModel';
+import { dispatcherState, schemasState, botProjectsSpaceState, botOpeningState } from './recoilModel';
import { openAlertModal } from './components/Modal/AlertDialog';
import { dialogStyle } from './components/Modal/dialogStyle';
import { LoadingSpinner } from './components/LoadingSpinner';
@@ -86,13 +86,14 @@ const projectStyle = css`
`;
const ProjectRouter: React.FC> = (props) => {
- const botOpening = useRecoilValue(botOpeningState);
- const projectId = useRecoilValue(projectIdState);
- const schemas = useRecoilValue(schemasState);
+ const { projectId = '' } = props;
+ const schemas = useRecoilValue(schemasState(projectId));
const { fetchProjectById } = useRecoilValue(dispatcherState);
+ const botProjects = useRecoilValue(botProjectsSpaceState);
+ const botOpening = useRecoilValue(botOpeningState);
useEffect(() => {
- if (projectId !== props.projectId && props.projectId) {
+ if (props.projectId && !botProjects.includes(props.projectId)) {
fetchProjectById(props.projectId);
}
}, [props.projectId]);
@@ -106,11 +107,10 @@ const ProjectRouter: React.FC> = (pro
}
}, [schemas, projectId]);
- if (botOpening || props.projectId !== projectId) {
- return ;
+ if (props.projectId && !botOpening && botProjects.includes(props.projectId)) {
+ return {props.children}
;
}
-
- return {props.children}
;
+ return ;
};
export default Routes;
diff --git a/Composer/packages/client/src/shell/lgApi.ts b/Composer/packages/client/src/shell/lgApi.ts
index 8c78fd148b..76168af704 100644
--- a/Composer/packages/client/src/shell/lgApi.ts
+++ b/Composer/packages/client/src/shell/lgApi.ts
@@ -4,25 +4,26 @@
import { useEffect, useState } from 'react';
import { LgFile } from '@bfc/shared';
import { useRecoilValue } from 'recoil';
-import formatMessage from 'format-message';
import debounce from 'lodash/debounce';
+import formatMessage from 'format-message';
import { useResolvers } from '../hooks/useResolver';
+import { Dispatcher } from '../recoilModel/dispatchers';
-import { projectIdState, focusPathState } from './../recoilModel';
+import { focusPathState } from './../recoilModel';
import { dispatcherState } from './../recoilModel/DispatcherWrapper';
const fileNotFound = (id: string) => formatMessage('LG file {id} not found', { id });
const TEMPLATE_ERROR = formatMessage('templateName is missing or empty');
function createLgApi(
- focusPath: string,
- actions: any, //TODO
+ state: { focusPath: string; projectId: string },
+ actions: Dispatcher,
lgFileResolver: (id: string) => LgFile | undefined
) {
const getLgTemplates = (id) => {
if (id === undefined) throw new Error('must have a file id');
- const focusedDialogId = focusPath.split('#').shift() || id;
+ const focusedDialogId = state.focusPath.split('#').shift() || id;
const file = lgFileResolver(focusedDialogId);
if (!file) throw new Error(fileNotFound(id));
return file.templates;
@@ -38,18 +39,20 @@ function createLgApi(
id: file.id,
templateName,
template,
+ projectId: state.projectId,
});
};
const copyLgTemplate = async (id, fromTemplateName, toTemplateName) => {
const file = lgFileResolver(id);
if (!file) throw new Error(fileNotFound(id));
- if (!fromTemplateName || !toTemplateName) throw new Error(TEMPLATE_ERROR);
+ if (!fromTemplateName || !toTemplateName) throw new Error(`templateName is missing or empty`);
return await actions.copyLgTemplate({
id: file.id,
fromTemplateName,
toTemplateName,
+ projectId: state.projectId,
});
};
@@ -61,6 +64,7 @@ function createLgApi(
return await actions.removeLgTemplate({
id: file.id,
templateName,
+ projectId: state.projectId,
});
};
@@ -72,6 +76,7 @@ function createLgApi(
return await actions.removeLgTemplates({
id: file.id,
templateNames,
+ projectId: state.projectId,
});
};
@@ -86,15 +91,14 @@ function createLgApi(
};
}
-export function useLgApi() {
- const focusPath = useRecoilValue(focusPathState);
- const projectId = useRecoilValue(projectIdState);
- const actions = useRecoilValue(dispatcherState);
- const { lgFileResolver } = useResolvers();
- const [api, setApi] = useState(createLgApi(focusPath, actions, lgFileResolver));
+export function useLgApi(projectId: string) {
+ const focusPath = useRecoilValue(focusPathState(projectId));
+ const actions: Dispatcher = useRecoilValue(dispatcherState);
+ const { lgFileResolver } = useResolvers(projectId);
+ const [api, setApi] = useState(createLgApi({ focusPath, projectId }, actions, lgFileResolver));
useEffect(() => {
- const newApi = createLgApi(focusPath, actions, lgFileResolver);
+ const newApi = createLgApi({ focusPath, projectId }, actions, lgFileResolver);
setApi(newApi);
return () => {
diff --git a/Composer/packages/client/src/shell/luApi.ts b/Composer/packages/client/src/shell/luApi.ts
index 68aae7215f..7581c402ba 100644
--- a/Composer/packages/client/src/shell/luApi.ts
+++ b/Composer/packages/client/src/shell/luApi.ts
@@ -7,18 +7,18 @@ import { useRecoilValue } from 'recoil';
import formatMessage from 'format-message';
import debounce from 'lodash/debounce';
-import { projectIdState } from '../recoilModel/atoms/botState';
import { useResolvers } from '../hooks/useResolver';
+import { focusPathState } from '../recoilModel';
+import { Dispatcher } from '../recoilModel/dispatchers';
import { dispatcherState } from './../recoilModel/DispatcherWrapper';
-import { focusPathState } from './../recoilModel/atoms/botState';
const fileNotFound = (id: string) => formatMessage(`LU file {id} not found`, { id });
const INTENT_ERROR = formatMessage('intentName is missing or empty');
function createLuApi(
state: { focusPath: string; projectId: string },
- dispatchers: any, //TODO
+ dispatchers: Dispatcher,
luFileResolver: (id: string) => LuFile | undefined
) {
const addLuIntent = async (id: string, intentName: string, intent: LuIntentSection) => {
@@ -47,7 +47,7 @@ function createLuApi(
const newIntent = { ...oldIntent, Name: newIntentName };
- return await dispatchers.updateLuIntent({ id: file.id, intentName, intent: newIntent });
+ return await dispatchers.updateLuIntent({ id: file.id, intentName, intent: newIntent, projectId: state.projectId });
};
const removeLuIntent = async (id: string, intentName: string) => {
@@ -83,11 +83,10 @@ function createLuApi(
};
}
-export function useLuApi() {
- const focusPath = useRecoilValue(focusPathState);
- const projectId = useRecoilValue(projectIdState);
+export function useLuApi(projectId: string) {
+ const focusPath = useRecoilValue(focusPathState(projectId));
const dispatchers = useRecoilValue(dispatcherState);
- const { luFileResolver } = useResolvers();
+ const { luFileResolver } = useResolvers(projectId);
const [api, setApi] = useState(createLuApi({ focusPath, projectId }, dispatchers, luFileResolver));
useEffect(() => {
diff --git a/Composer/packages/client/src/shell/qnaApi.ts b/Composer/packages/client/src/shell/qnaApi.ts
index c726a3febb..13fdb88bfa 100644
--- a/Composer/packages/client/src/shell/qnaApi.ts
+++ b/Composer/packages/client/src/shell/qnaApi.ts
@@ -6,7 +6,6 @@ import { QnAFile } from '@bfc/shared';
import { useRecoilValue } from 'recoil';
import { useResolvers } from '../hooks/useResolver';
-import { projectIdState } from '../recoilModel/atoms';
import { dispatcherState } from './../recoilModel/DispatcherWrapper';
@@ -23,10 +22,9 @@ function createQnaApi(state: { projectId }, dispatchers: any, qnaFileResolver: (
};
}
-export function useQnaApi() {
- const projectId = useRecoilValue(projectIdState);
+export function useQnaApi(projectId) {
const dispatchers = useRecoilValue(dispatcherState);
- const { qnaFileResolver } = useResolvers();
+ const { qnaFileResolver } = useResolvers(projectId);
const [api, setApi] = useState(createQnaApi({ projectId }, dispatchers, qnaFileResolver));
useEffect(() => {
diff --git a/Composer/packages/client/src/shell/triggerApi.ts b/Composer/packages/client/src/shell/triggerApi.ts
index f3d1d9d2cc..79cc9e64f2 100644
--- a/Composer/packages/client/src/shell/triggerApi.ts
+++ b/Composer/packages/client/src/shell/triggerApi.ts
@@ -8,15 +8,16 @@ import { LgTemplate } from '@bfc/shared';
import get from 'lodash/get';
import { useResolvers } from '../hooks/useResolver';
-import { projectIdState, schemasState, dialogsState, localeState, lgFilesState } from '../recoilModel/atoms';
import { onChooseIntentKey, generateNewDialog, intentTypeKey, qnaMatcherKey } from '../utils/dialogUtil';
import { navigateTo } from '../utils/navigation';
+import { schemasState, lgFilesState, dialogsState, localeState } from '../recoilModel';
+import { Dispatcher } from '../recoilModel/dispatchers';
import { dispatcherState } from './../recoilModel/DispatcherWrapper';
function createTriggerApi(
state: { projectId; schemas; dialogs; locale; lgFiles },
- dispatchers: any, //TODO
+ dispatchers: Dispatcher, //TODO
luFileResolver: (id: string) => LuFile | undefined,
lgFileResolver: (id: string) => LgFile | undefined,
dialogResolver: (id: string) => DialogInfo | undefined
@@ -55,7 +56,7 @@ function createTriggerApi(
LgTemplateSamples.TextInputPromptForQnAMatcher(designerId1) as LgTemplate,
LgTemplateSamples.SendActivityForQnAMatcher(designerId2) as LgTemplate,
];
- await createLgTemplates({ id: lgFile.id, templates: lgTemplates });
+ await createLgTemplates({ id: lgFile.id, templates: lgTemplates, projectId });
} else if (formData.$kind === onChooseIntentKey) {
const designerId1 = getDesignerIdFromDialogPath(newDialog, `content.triggers[${index}].actions[4].prompt`);
const designerId2 = getDesignerIdFromDialogPath(
@@ -80,8 +81,8 @@ function createTriggerApi(
(t) => commonlgFile?.templates.findIndex((clft) => clft.name === t.name) === -1
);
- await createLgTemplates({ id: `common.${locale}`, templates: lgTemplates2 });
- await createLgTemplates({ id: lgFile.id, templates: lgTemplates1 });
+ await createLgTemplates({ id: `common.${locale}`, templates: lgTemplates2, projectId });
+ await createLgTemplates({ id: lgFile.id, templates: lgTemplates1, projectId });
}
const dialogPayload = {
id: newDialog.id,
@@ -92,7 +93,7 @@ function createTriggerApi(
if (url) {
navigateTo(url);
} else {
- selectTo(`triggers[${index}]`);
+ selectTo(projectId, `triggers[${index}]`);
}
};
return {
@@ -100,14 +101,14 @@ function createTriggerApi(
};
}
-export function useTriggerApi() {
- const projectId = useRecoilValue(projectIdState);
- const schemas = useRecoilValue(schemasState);
- const dialogs = useRecoilValue(dialogsState);
- const locale = useRecoilValue(localeState);
- const lgFiles = useRecoilValue(lgFilesState);
+export function useTriggerApi(projectId: string) {
+ const schemas = useRecoilValue(schemasState(projectId));
+ const lgFiles = useRecoilValue(lgFilesState(projectId));
+ const dialogs = useRecoilValue(dialogsState(projectId));
+ const locale = useRecoilValue(localeState(projectId));
+
const dispatchers = useRecoilValue(dispatcherState);
- const { luFileResolver, lgFileResolver, dialogResolver } = useResolvers();
+ const { luFileResolver, lgFileResolver, dialogResolver } = useResolvers(projectId);
const [api, setApi] = useState(
createTriggerApi(
{ projectId, schemas, dialogs, locale, lgFiles },
diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts
index d65310c2d0..f80bacaafc 100644
--- a/Composer/packages/client/src/shell/useShell.ts
+++ b/Composer/packages/client/src/shell/useShell.ts
@@ -2,33 +2,32 @@
// Licensed under the MIT License.
import { useMemo, useRef } from 'react';
-import { ShellApi, ShellData, Shell, fetchFromSettings } from '@bfc/shared';
+import { ShellApi, ShellData, Shell, fetchFromSettings, DialogSchemaFile, Skill } from '@bfc/shared';
import { useRecoilValue } from 'recoil';
import formatMessage from 'format-message';
import { updateRegExIntent, renameRegExIntent, updateIntentTrigger } from '../utils/dialogUtil';
import { getDialogData, setDialogData } from '../utils/dialogUtil';
import { isAbsHosted } from '../utils/envUtil';
-import { undoFunctionState } from '../recoilModel/undo/history';
import {
- botNameState,
+ dispatcherState,
+ userSettingsState,
+ settingsState,
+ clipboardActionsState,
schemasState,
+ validateDialogSelectorFamily,
+ breadcrumbState,
+ focusPathState,
skillsState,
- lgFilesState,
- dialogSchemasState,
- projectIdState,
localeState,
- luFilesState,
qnaFilesState,
- dispatcherState,
- breadcrumbState,
designPageLocationState,
- focusPathState,
- userSettingsState,
- clipboardActionsState,
- settingsState,
+ botNameState,
+ dialogSchemasState,
+ lgFilesState,
+ luFilesState,
} from '../recoilModel';
-import { validatedDialogsSelector } from '../recoilModel/selectors/validatedDialogs';
+import { undoFunctionState } from '../recoilModel/undo/history';
import { useLgApi } from './lgApi';
import { useLuApi } from './luApi';
@@ -39,25 +38,27 @@ const FORM_EDITOR = 'PropertyEditor';
type EventSource = 'FlowEditor' | 'PropertyEditor' | 'DesignPage';
-export function useShell(source: EventSource): Shell {
+export function useShell(source: EventSource, projectId: string): Shell {
const dialogMapRef = useRef({});
- const botName = useRecoilValue(botNameState);
- const dialogs = useRecoilValue(validatedDialogsSelector);
- const dialogSchemas = useRecoilValue(dialogSchemasState);
- const luFiles = useRecoilValue(luFilesState);
- const qnaFiles = useRecoilValue(qnaFilesState);
- const projectId = useRecoilValue(projectIdState);
- const locale = useRecoilValue(localeState);
- const lgFiles = useRecoilValue(lgFilesState);
- const skills = useRecoilValue(skillsState);
- const schemas = useRecoilValue(schemasState);
- const designPageLocation = useRecoilValue(designPageLocationState);
- const breadcrumb = useRecoilValue(breadcrumbState);
- const focusPath = useRecoilValue(focusPathState);
+
+ const schemas = useRecoilValue(schemasState(projectId));
+ const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId));
+ const breadcrumb = useRecoilValue(breadcrumbState(projectId));
+ const focusPath = useRecoilValue(focusPathState(projectId));
+ const skills = useRecoilValue(skillsState(projectId));
+ const locale = useRecoilValue(localeState(projectId));
+ const qnaFiles = useRecoilValue(qnaFilesState(projectId));
+ const undoFunction = useRecoilValue(undoFunctionState(projectId));
+ const designPageLocation = useRecoilValue(designPageLocationState(projectId));
+ const { undo, redo, commitChanges } = undoFunction;
+ const luFiles = useRecoilValue(luFilesState(projectId));
+ const lgFiles = useRecoilValue(lgFilesState(projectId));
+ const dialogSchemas = useRecoilValue(dialogSchemasState(projectId));
+ const botName = useRecoilValue(botNameState(projectId));
+ const settings = useRecoilValue(settingsState(projectId));
+
const userSettings = useRecoilValue(userSettingsState);
const clipboardActions = useRecoilValue(clipboardActionsState);
- const { undo, redo, commitChanges } = useRecoilValue(undoFunctionState);
- const settings = useRecoilValue(settingsState);
const {
updateDialog,
updateDialogSchema,
@@ -74,10 +75,11 @@ export function useShell(source: EventSource): Shell {
displayManifestModal,
updateSkillsInSetting,
} = useRecoilValue(dispatcherState);
- const lgApi = useLgApi();
- const luApi = useLuApi();
- const qnaApi = useQnaApi();
- const triggerApi = useTriggerApi();
+
+ const lgApi = useLgApi(projectId);
+ const luApi = useLuApi(projectId);
+ const qnaApi = useQnaApi(projectId);
+ const triggerApi = useTriggerApi(projectId);
const { dialogId, selected, focused, promptTab } = designPageLocation;
const dialogsMap = useMemo(() => {
@@ -91,29 +93,29 @@ export function useShell(source: EventSource): Shell {
const dialog = dialogs.find((dialog) => dialog.id === id);
if (!dialog) throw new Error(formatMessage(`dialog {dialogId} not found`, { dialogId }));
const newDialog = updateRegExIntent(dialog, intentName, pattern);
- return updateDialog({ id, content: newDialog.content });
+ updateDialog({ id, content: newDialog.content, projectId });
}
function renameRegExIntentHandler(id: string, intentName: string, newIntentName: string) {
const dialog = dialogs.find((dialog) => dialog.id === id);
if (!dialog) throw new Error(`dialog ${dialogId} not found`);
const newDialog = renameRegExIntent(dialog, intentName, newIntentName);
- updateDialog({ id, content: newDialog.content });
+ updateDialog({ id, content: newDialog.content, projectId });
}
function updateIntentTriggerHandler(id: string, intentName: string, newIntentName: string) {
const dialog = dialogs.find((dialog) => dialog.id === id);
if (!dialog) throw new Error(`dialog ${dialogId} not found`);
const newDialog = updateIntentTrigger(dialog, intentName, newIntentName);
- updateDialog({ id, content: newDialog.content });
+ updateDialog({ id, content: newDialog.content, projectId });
}
function navigationTo(path) {
- navTo(path, breadcrumb);
+ navTo(projectId, path, breadcrumb);
}
function focusEvent(subPath) {
- selectTo(subPath);
+ selectTo(projectId, subPath);
}
function focusSteps(subPaths: string[] = [], fragment?: string) {
@@ -128,7 +130,7 @@ export function useShell(source: EventSource): Shell {
}
}
- focusTo(dataPath, fragment ?? '');
+ focusTo(projectId, dataPath, fragment ?? '');
}
dialogMapRef.current = dialogsMap;
@@ -142,6 +144,7 @@ export function useShell(source: EventSource): Shell {
updateDialog({
id: dialogId,
content: newDialogData,
+ projectId,
});
},
saveData: (newData, updatePath) => {
@@ -154,6 +157,7 @@ export function useShell(source: EventSource): Shell {
const payload = {
id: dialogId,
content: updatedDialog,
+ projectId,
};
dialogMapRef.current[dialogId] = updatedDialog;
updateDialog(payload);
@@ -173,16 +177,20 @@ export function useShell(source: EventSource): Shell {
onCopy: setVisualEditorClipboard,
createDialog: (actionsSeed) => {
return new Promise((resolve) => {
- createDialogBegin(actionsSeed, (newDialog: string | null) => {
- resolve(newDialog);
- });
+ createDialogBegin(
+ actionsSeed,
+ (newDialog: string | null) => {
+ resolve(newDialog);
+ },
+ projectId
+ );
});
},
addSkillDialog: () => {
return new Promise((resolve) => {
addSkillDialogBegin((newSkill: { manifestUrl: string; name: string } | null) => {
resolve(newSkill);
- });
+ }, projectId);
});
},
undo,
@@ -191,11 +199,13 @@ export function useShell(source: EventSource): Shell {
addCoachMarkRef: onboardingAddCoachMarkRef,
updateUserSettings: updateUserSettings,
announce: setMessage,
- displayManifestModal: displayManifestModal,
- updateDialogSchema,
+ displayManifestModal: (skillId) => displayManifestModal(skillId, projectId),
+ updateDialogSchema: async (dialogSchema: DialogSchemaFile) => {
+ updateDialogSchema(dialogSchema, projectId);
+ },
skillsInSettings: {
get: (path: string) => fetchFromSettings(path, settings),
- set: updateSkillsInSetting,
+ set: (skillName: string, skillInfo: Partial) => updateSkillsInSetting(skillName, skillInfo, projectId),
},
};
diff --git a/Composer/packages/client/src/utils/hooks.ts b/Composer/packages/client/src/utils/hooks.ts
index 51533cca93..8df4ab3cd3 100644
--- a/Composer/packages/client/src/utils/hooks.ts
+++ b/Composer/packages/client/src/utils/hooks.ts
@@ -7,7 +7,7 @@ import replace from 'lodash/replace';
import find from 'lodash/find';
import { useRecoilValue } from 'recoil';
-import { projectIdState, designPageLocationState, extensionsState } from './../recoilModel';
+import { designPageLocationState, extensionsState, currentProjectIdState } from './../recoilModel';
import { bottomLinks, topLinks } from './pageLinks';
import routerCache from './routerCache';
import { projectIdCache } from './projectCache';
@@ -22,8 +22,8 @@ export const useLocation = () => {
};
export const useLinks = () => {
- const projectId = useRecoilValue(projectIdState);
- const designPageLocation = useRecoilValue(designPageLocationState);
+ const projectId = useRecoilValue(currentProjectIdState);
+ const designPageLocation = useRecoilValue(designPageLocationState(projectId));
const extensions = useRecoilValue(extensionsState);
const openedDialogId = designPageLocation.dialogId || 'Main';
diff --git a/Composer/packages/client/src/utils/luFileStatusStorage.ts b/Composer/packages/client/src/utils/luFileStatusStorage.ts
index 8cdd12c795..8c2debaa2e 100644
--- a/Composer/packages/client/src/utils/luFileStatusStorage.ts
+++ b/Composer/packages/client/src/utils/luFileStatusStorage.ts
@@ -36,7 +36,7 @@ class LuFileStatusStorage {
}
public removeFileStatus(projectId: string, fileId: string) {
- if (!projectId) return;
+ if (!projectId || !this._all[projectId]) return;
if (typeof this._all[projectId][fileId] !== 'undefined') {
delete this._all[projectId][fileId];
this.storage.set(KEY, this._all);
diff --git a/Composer/packages/client/src/utils/navigation.ts b/Composer/packages/client/src/utils/navigation.ts
index 5e113e5272..e988b801c9 100644
--- a/Composer/packages/client/src/utils/navigation.ts
+++ b/Composer/packages/client/src/utils/navigation.ts
@@ -68,7 +68,8 @@ export function getUrlSearch(selected: string, focused: string): string {
export function checkUrl(
currentUri: string,
- { dialogId, projectId, selected, focused, promptTab }: DesignPageLocation
+ projectId: string,
+ { dialogId, selected, focused, promptTab }: DesignPageLocation
) {
let lastUri = `/bot/${projectId}/dialogs/${dialogId}${getUrlSearch(selected, focused)}`;
if (promptTab) {
@@ -77,8 +78,9 @@ export function checkUrl(
return lastUri === currentUri;
}
-interface NavigationState {
- breadcrumb: BreadcrumbItem[];
+export interface NavigationState {
+ breadcrumb?: BreadcrumbItem[];
+ qnaKbUrls?: string[];
}
export function convertPathToUrl(projectId: string, dialogId: string, path?: string): string {
diff --git a/Composer/packages/extension-client/src/components/EditorExtension.tsx b/Composer/packages/extension-client/src/components/EditorExtension.tsx
index de1056c764..a46a72f200 100644
--- a/Composer/packages/extension-client/src/components/EditorExtension.tsx
+++ b/Composer/packages/extension-client/src/components/EditorExtension.tsx
@@ -10,11 +10,12 @@ import { PluginConfig } from '../types';
interface EditorExtensionProps {
shell: Shell;
plugins: PluginConfig;
+ projectId: string;
}
-export const EditorExtension: React.FC = ({ shell, plugins, children }) => {
+export const EditorExtension: React.FC = ({ shell, plugins, children, projectId }) => {
const context = useMemo(() => {
- return { shellApi: shell.api, shellData: shell.data, plugins };
+ return { shellApi: shell.api, shellData: shell.data, plugins, projectId };
}, [shell.api, shell.data, plugins]);
return {children};
diff --git a/Composer/packages/extension-client/src/hooks/useTriggerApi.ts b/Composer/packages/extension-client/src/hooks/useTriggerApi.ts
index 2d17399ab6..4a6f35fc57 100644
--- a/Composer/packages/extension-client/src/hooks/useTriggerApi.ts
+++ b/Composer/packages/extension-client/src/hooks/useTriggerApi.ts
@@ -7,9 +7,9 @@ import get from 'lodash/get';
import { useActionApi } from './useActionApi';
import { useLuApi } from './useLuApi';
-export const useTriggerApi = (shellAPi: ShellApi) => {
- const { deleteActions } = useActionApi(shellAPi);
- const { deleteLuIntent } = useLuApi(shellAPi);
+export const useTriggerApi = (shellApi: ShellApi) => {
+ const { deleteActions } = useActionApi(shellApi);
+ const { deleteLuIntent } = useLuApi(shellApi);
const deleteTrigger = (dialogId: string, trigger: ITriggerCondition) => {
if (!trigger) return;
diff --git a/Composer/packages/lib/shared/src/types/shell.ts b/Composer/packages/lib/shared/src/types/shell.ts
index a077fc39c4..cca88829fa 100644
--- a/Composer/packages/lib/shared/src/types/shell.ts
+++ b/Composer/packages/lib/shared/src/types/shell.ts
@@ -57,7 +57,7 @@ export interface ShellData {
luFiles: LuFile[];
qnaFiles: QnAFile[];
userSettings: UserSettings;
- skills: Skill[];
+ skills: any[];
// TODO: remove
schemas: BotSchemas;
}
@@ -71,18 +71,18 @@ export interface ShellApi {
onFocusEvent: (eventId: string) => void;
onSelect: (ids: string[]) => void;
getLgTemplates: (id: string) => LgTemplate[];
- copyLgTemplate: (id: string, fromTemplateName: string, toTemplateName?: string) => Promise;
- addLgTemplate: (id: string, templateName: string, templateStr: string) => Promise;
- updateLgTemplate: (id: string, templateName: string, templateStr: string) => Promise;
- deboucedUpdateLgTemplate: (id: string, templateName: string, templateStr: string) => Promise;
- removeLgTemplate: (id: string, templateName: string) => Promise;
- removeLgTemplates: (id: string, templateNames: string[]) => Promise;
+ copyLgTemplate: (id: string, fromTemplateName: string, toTemplateName?: string) => Promise;
+ addLgTemplate: (id: string, templateName: string, templateStr: string) => Promise;
+ updateLgTemplate: (id: string, templateName: string, templateStr: string) => Promise;
+ deboucedUpdateLgTemplate: (id: string, templateName: string, templateStr: string) => Promise;
+ removeLgTemplate: (id: string, templateName: string) => Promise;
+ removeLgTemplates: (id: string, templateNames: string[]) => Promise;
getLuIntent: (id: string, intentName: string) => LuIntentSection | undefined;
getLuIntents: (id: string) => LuIntentSection[];
- addLuIntent: (id: string, intentName: string, intent: LuIntentSection) => Promise;
- updateLuIntent: (id: string, intentName: string, intent: LuIntentSection) => Promise;
- deboucedUpdateLuIntent: (id: string, intentName: string, intent: LuIntentSection) => Promise;
- renameLuIntent: (id: string, intentName: string, newIntentName: string) => Promise;
+ addLuIntent: (id: string, intentName: string, intent: LuIntentSection) => Promise;
+ updateLuIntent: (id: string, intentName: string, intent: LuIntentSection) => Promise;
+ deboucedUpdateLuIntent: (id: string, intentName: string, intent: LuIntentSection) => Promise;
+ renameLuIntent: (id: string, intentName: string, newIntentName: string) => Promise;
removeLuIntent: (id: string, intentName: string) => void;
updateQnaContent: (id: string, content: string) => void;
updateRegExIntent: (id: string, intentName: string, pattern: string) => void;
diff --git a/Composer/packages/server/src/locales/en-US.json b/Composer/packages/server/src/locales/en-US.json
index 051b58ea6a..cc71cbef03 100644
--- a/Composer/packages/server/src/locales/en-US.json
+++ b/Composer/packages/server/src/locales/en-US.json
@@ -2396,4 +2396,4 @@
"your_bot_is_using_luis_and_qna_for_natural_languag_53830684": {
"message": "Your bot is using LUIS and QNA for natural language understanding."
}
-}
\ No newline at end of file
+}