diff --git a/Composer/package.json b/Composer/package.json index f5ba9e4357..035518e525 100644 --- a/Composer/package.json +++ b/Composer/package.json @@ -53,7 +53,7 @@ "start:server:dev": "yarn workspace @bfc/server start:dev", "runtime": "cd ../runtime/dotnet/azurewebapp && dotnet build && dotnet run", "test": "yarn typecheck && jest", - "test:watch": "jest --watch", + "test:watch": "yarn typecheck jest --watch", "test:coverage": "yarn test --coverage --no-cache --forceExit --reporters=default", "test:integration": "cypress run --browser edge", "test:integration:start-server": "node scripts/e2e.js", diff --git a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx index ac4d09c5c6..4b46370ba1 100644 --- a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx +++ b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx @@ -32,7 +32,6 @@ const CreationFlow: React.FC = () => { openBotProject, createProject, saveProjectAs, - saveTemplateId, fetchStorages, fetchFolderItemsByPath, setCreationFlowStatus, @@ -81,9 +80,12 @@ const CreationFlow: React.FC = () => { const openBot = async (botFolder) => { const projectId = await openBotProject(botFolder); - setCreationFlowStatus(CreationFlowStatus.CLOSE); - const mainUrl = `/bot/${projectId}/dialogs/Main`; - navigateTo(mainUrl); + if (projectId) { + setCreationFlowStatus(CreationFlowStatus.CLOSE); + const mainUrl = `/bot/${projectId}/dialogs/Main`; + + navigateTo(mainUrl); + } }; const handleCreateNew = async (formData, templateId: string) => { @@ -125,7 +127,6 @@ const CreationFlow: React.FC = () => { createFolder={createFolder} focusedStorageFolder={focusedStorageFolder} path="create/:templateId" - saveTemplateId={saveTemplateId} updateFolder={updateFolder} onCurrentPathUpdate={updateCurrentPath} onDismiss={handleDismiss} diff --git a/Composer/packages/client/src/components/CreationFlow/DefineConversation/DefineConversation.tsx b/Composer/packages/client/src/components/CreationFlow/DefineConversation/DefineConversation.tsx index 49e37c39d0..30b6bdcfcd 100644 --- a/Composer/packages/client/src/components/CreationFlow/DefineConversation/DefineConversation.tsx +++ b/Composer/packages/client/src/components/CreationFlow/DefineConversation/DefineConversation.tsx @@ -41,7 +41,6 @@ interface DefineConversationProps onDismiss: () => void; onCurrentPathUpdate: (newPath?: string, storageId?: string) => void; onGetErrorMessage?: (text: string) => void; - saveTemplateId?: (templateId: string) => void; focusedStorageFolder: StorageFolder; } diff --git a/Composer/packages/client/src/pages/home/index.tsx b/Composer/packages/client/src/pages/home/index.tsx index a1aa89d411..ec6032736b 100644 --- a/Composer/packages/client/src/pages/home/index.tsx +++ b/Composer/packages/client/src/pages/home/index.tsx @@ -15,7 +15,7 @@ import { CreationFlowStatus } from '../../constants'; import { dispatcherState } from '../../recoilModel'; import { navigateTo } from '../../utils'; import { botNameState, projectIdState } from '../../recoilModel/atoms/botState'; -import { recentProjectsState, templateProjectsState, templateIdState } from '../../recoilModel/atoms/appState'; +import { recentProjectsState, templateProjectsState } from '../../recoilModel/atoms/appState'; import { ToolBar, IToolBarItem } from '../../components/ToolBar/ToolBar'; import * as home from './styles'; @@ -58,7 +58,6 @@ const tutorials = [ ]; const Home: React.FC = () => { - const templateId = useRecoilValue(templateIdState); const templateProjects = useRecoilValue(templateProjectsState); const botName = useRecoilValue(botNameState); const recentProjects = useRecoilValue(recentProjectsState); @@ -69,8 +68,10 @@ const Home: React.FC = () => { const onClickRecentBotProject = async (path) => { const projectId = await openBotProject(path); - const mainUrl = `/bot/${projectId}/dialogs/Main`; - navigateTo(mainUrl); + if (projectId) { + const mainUrl = `/bot/${projectId}/dialogs/Main`; + navigateTo(mainUrl); + } }; const onItemChosen = async (item) => { @@ -130,7 +131,7 @@ const Home: React.FC = () => { }, onClick: () => { setCreationFlowStatus(CreationFlowStatus.SAVEAS); - navigate(`projects/${projectId}/${templateId}/save`); + navigate(`projects/${projectId}/save`); }, }, align: 'left', diff --git a/Composer/packages/client/src/recoilModel/atoms/appState.ts b/Composer/packages/client/src/recoilModel/atoms/appState.ts index bf05a4d2ab..00d324158c 100644 --- a/Composer/packages/client/src/recoilModel/atoms/appState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/appState.ts @@ -123,11 +123,6 @@ export const runtimeSettingsState = atom<{ }, }); -export const templateIdState = atom({ - key: getFullyQualifiedKey('templateId'), - default: '', -}); - export const botEndpointsState = atom({ key: 'botEndpoints', default: {}, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/application.ts b/Composer/packages/client/src/recoilModel/dispatchers/application.ts index d2d282e95a..ae7531238b 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/application.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/application.ts @@ -5,17 +5,13 @@ import { CallbackInterface, useRecoilCallback } from 'recoil'; import debounce from 'lodash/debounce'; -import { - appUpdateState, - announcementState, - onboardingState, - creationFlowStatusState, - applicationErrorState, -} from '../atoms/appState'; +import { appUpdateState, announcementState, onboardingState, creationFlowStatusState } from '../atoms/appState'; import { AppUpdaterStatus, CreationFlowStatus } from '../../constants'; import OnboardingState from '../../utils/onboardingStorage'; import { StateError, AppUpdateState } from '../../recoilModel/types'; +import { setError } from './shared'; + export const applicationDispatcher = () => { const setAppUpdateStatus = useRecoilCallback<[AppUpdaterStatus, string | undefined], Promise>( ({ set, snapshot: { getPromise } }: CallbackInterface) => async ( @@ -104,8 +100,8 @@ export const applicationDispatcher = () => { ); const setApplicationLevelError = useRecoilCallback<[StateError | undefined], void>( - ({ set }: CallbackInterface) => (errorObj: StateError | undefined) => { - set(applicationErrorState, errorObj); + (callbackHelpers: CallbackInterface) => (errorObj: StateError | undefined) => { + setError(callbackHelpers, errorObj); } ); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 2a4777e22f..9dec2684cf 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -18,6 +18,7 @@ import luFileStatusStorage from '../../utils/luFileStatusStorage'; import { DialogSetting } from '../../recoilModel/types'; import settingStorage from '../../utils/dialogSettingStorage'; import filePersistence from '../persistence/FilePersistence'; +import { navigateTo } from '../../utils'; import { skillManifestsState, @@ -38,10 +39,14 @@ import { recentProjectsState, templateProjectsState, runtimeTemplatesState, - templateIdState, applicationErrorState, } from './../atoms'; -import { logMessage } from './../dispatchers/shared'; +import { logMessage, setError } from './../dispatchers/shared'; + +const handleProjectFailure = (callbackHelpers: CallbackInterface, ex) => { + callbackHelpers.set(botOpeningState, false); + setError(callbackHelpers, ex); +}; const checkProjectUpdates = async () => { const workers = [filePersistence, lgWorker, luWorker]; @@ -143,12 +148,11 @@ export const projectDispatcher = () => { ...currentRecentProjects, }); } catch (ex) { - // TODO: Handle exceptions logMessage(callbackHelpers, `Error removing recent project: ${ex}`); } }; - const setOpenPendingStatusasync = async (callbackHelpers: CallbackInterface) => { + const setOpenPendingStatusAsync = async (callbackHelpers: CallbackInterface) => { const { set } = callbackHelpers; set(botOpeningState, true); await checkProjectUpdates(); @@ -157,21 +161,26 @@ export const projectDispatcher = () => { const openBotProject = useRecoilCallback<[string, string?], Promise>( (callbackHelpers: CallbackInterface) => async (path: string, storageId = 'default') => { try { - await setOpenPendingStatusasync(callbackHelpers); + await setOpenPendingStatusAsync(callbackHelpers); const response = await httpClient.put(`/projects/open`, { path, storageId }); await initBotState(callbackHelpers, response.data); return response.data.id; } catch (ex) { - callbackHelpers.set(botOpeningState, false); removeRecentProject(callbackHelpers, path); + handleProjectFailure(callbackHelpers, ex); } } ); const fetchProjectById = useRecoilCallback<[string], Promise>( (callbackHelpers: CallbackInterface) => async (projectId: string) => { - const response = await httpClient.get(`/projects/${projectId}`); - await initBotState(callbackHelpers, response.data); + try { + const response = await httpClient.get(`/projects/${projectId}`); + await initBotState(callbackHelpers, response.data); + } catch (ex) { + handleProjectFailure(callbackHelpers, ex); + navigateTo('/home'); + } } ); @@ -184,7 +193,7 @@ export const projectDispatcher = () => { schemaUrl?: string ) => { try { - await setOpenPendingStatusasync(callbackHelpers); + await setOpenPendingStatusAsync(callbackHelpers); const response = await httpClient.post(`/projects`, { storageId: 'default', templateId, @@ -199,9 +208,8 @@ export const projectDispatcher = () => { } await initBotState(callbackHelpers, response.data); return projectId; - } catch (error) { - callbackHelpers.set(botOpeningState, false); - logMessage(callbackHelpers, error.message); + } catch (ex) { + handleProjectFailure(callbackHelpers, ex); } } ); @@ -235,7 +243,7 @@ export const projectDispatcher = () => { const saveProjectAs = useRecoilCallback<[string, string, string, string], Promise>( (callbackHelpers: CallbackInterface) => async (projectId, name, description, location) => { try { - await setOpenPendingStatusasync(callbackHelpers); + await setOpenPendingStatusAsync(callbackHelpers); const response = await httpClient.post(`/projects/${projectId}/project/saveAs`, { storageId: 'default', name, @@ -244,10 +252,9 @@ export const projectDispatcher = () => { }); await initBotState(callbackHelpers, response.data); return response.data.id; - } catch (error) { - //TODO: error handling - callbackHelpers.set(botOpeningState, false); - logMessage(callbackHelpers, error.message); + } catch (ex) { + handleProjectFailure(callbackHelpers, ex); + logMessage(callbackHelpers, ex.message); } } ); @@ -258,7 +265,6 @@ export const projectDispatcher = () => { const response = await httpClient.get(`/projects/recent`); set(recentProjectsState, response.data); } catch (ex) { - // TODO: Handle exceptions set(recentProjectsState, []); logMessage(callbackHelpers, `Error in fetching recent projects: ${ex}`); } @@ -294,10 +300,6 @@ export const projectDispatcher = () => { } ); - const saveTemplateId = useRecoilCallback(({ set }) => async (templateId: string) => { - set(templateIdState, templateId); - }); - const fetchTemplates = useRecoilCallback<[], Promise>((callbackHelpers: CallbackInterface) => async () => { try { const response = await httpClient.get(`/assets/projectTemplates`); @@ -352,7 +354,6 @@ export const projectDispatcher = () => { createProject, deleteBotProject, saveProjectAs, - saveTemplateId, fetchTemplates, fetchProjectById, fetchRecentProjects, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/shared.ts b/Composer/packages/client/src/recoilModel/dispatchers/shared.ts index 15ccdaee13..b9922069e8 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/shared.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/shared.ts @@ -3,8 +3,9 @@ // Licensed under the MIT License. import { CallbackInterface } from 'recoil'; +import formatMessage from 'format-message'; -import { logEntryListState } from '../atoms/appState'; +import { logEntryListState, applicationErrorState } from '../atoms/appState'; export enum ConsoleMsgLevel { Error, @@ -29,3 +30,23 @@ export const logMessage = ({ set }: CallbackInterface, message: string, level = } set(logEntryListState, (logEntries) => [...logEntries, message]); }; + +export const setError = (callbackHelpers: CallbackInterface, payload) => { + // if the error originated at the server and the server included message, use it... + if (payload?.status === 409) { + callbackHelpers.set(applicationErrorState, { + status: 409, + message: formatMessage( + 'This version of the content is out of date, and your last change was rejected. The content will be automatically refreshed.' + ), + summary: formatMessage('Modification Rejected'), + }); + } else { + if (payload?.response?.data?.message) { + callbackHelpers.set(applicationErrorState, payload.response.data); + } else { + callbackHelpers.set(applicationErrorState, payload); + } + } + logMessage(callbackHelpers, `Error: ${JSON.stringify(payload)}`); +};