Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#1741] Restore focus after dialog has been closed #1901

Merged
merged 10 commits into from
Oct 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions packages/app/client/src/commands/uiCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,17 @@ export class UiCommands {
// Azure sign in
@Command(UI.SignInToAzure)
protected signIntoAzure(serviceType: ServiceTypes) {
store.dispatch(
beginAzureAuthWorkflow(
AzureLoginPromptDialogContainer,
{ serviceType },
AzureLoginSuccessDialogContainer,
AzureLoginFailedDialogContainer
)
);
return new Promise(resolve => {
store.dispatch(
beginAzureAuthWorkflow(
AzureLoginPromptDialogContainer,
{ serviceType },
AzureLoginSuccessDialogContainer,
AzureLoginFailedDialogContainer,
resolve
)
);
});
}

@Command(UI.ArmTokenReceivedOnStartup)
Expand Down
5 changes: 4 additions & 1 deletion packages/app/client/src/state/actions/azureAuthActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ export interface AzureAuthWorkflow {
promptDialogProps: { [propName: string]: any };
loginSuccessDialog: ComponentClass<any>;
loginFailedDialog: ComponentClass<any>;
resolver?: Function;
}

export function beginAzureAuthWorkflow(
promptDialog: ComponentClass<any>,
promptDialogProps: { [propName: string]: any },
loginSuccessDialog: ComponentClass<any>,
loginFailedDialog: ComponentClass<any>
loginFailedDialog: ComponentClass<any>,
resolver?: Function
): AzureAuthAction<AzureAuthWorkflow> {
return {
type: AZURE_BEGIN_AUTH_WORKFLOW,
Expand All @@ -68,6 +70,7 @@ export function beginAzureAuthWorkflow(
promptDialogProps,
loginSuccessDialog,
loginFailedDialog,
resolver,
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ describe('connected service actions', () => {
});

it('should create an openAddServiceContextMenu action', () => {
const payload: any = {};
const action = openAddServiceContextMenu(payload);
const payload: any = { resolver: jasmine.any(Function) };
const action = openAddServiceContextMenu(payload, jasmine.any(Function) as any);

expect(action.type).toBe(OPEN_ADD_CONNECTED_SERVICE_CONTEXT_MENU);
expect(action.payload).toEqual(payload);
Expand Down
14 changes: 11 additions & 3 deletions packages/app/client/src/state/actions/connectedServiceActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ export interface ConnectedServicePickerPayload extends ConnectedServicePayload {
progressIndicatorComponent?: ComponentClass<any>;
}

export interface OpenAddServiceContextMenuPayload extends ConnectedServicePickerPayload {
resolver: Function;
}

export function launchConnectedServicePicker(
payload: ConnectedServicePickerPayload
): ConnectedServiceAction<ConnectedServicePickerPayload> {
Expand Down Expand Up @@ -108,11 +112,15 @@ export function openContextMenuForConnectedService<T>(
}

export function openAddServiceContextMenu(
payload: ConnectedServicePickerPayload
): ConnectedServiceAction<ConnectedServicePickerPayload> {
payload: ConnectedServicePickerPayload,
resolver: Function
): ConnectedServiceAction<OpenAddServiceContextMenuPayload> {
return {
type: OPEN_ADD_CONNECTED_SERVICE_CONTEXT_MENU,
payload,
payload: {
...payload,
resolver,
},
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface EndpointServiceAction<T> extends Action {
export interface EndpointServicePayload {
endpointService: IEndpointService;
focusExistingChatIfAvailable?: boolean;
resolver?: Function;
}

export interface EndpointEditorPayload extends EndpointServicePayload {
Expand All @@ -54,11 +55,12 @@ export interface EndpointEditorPayload extends EndpointServicePayload {

export function launchEndpointEditor(
endpointEditorComponent: ComponentClass<any>,
endpointService?: IEndpointService
endpointService?: IEndpointService,
resolver?: Function
): EndpointServiceAction<EndpointEditorPayload> {
return {
type: LAUNCH_ENDPOINT_EDITOR,
payload: { endpointEditorComponent, endpointService },
payload: { endpointEditorComponent, endpointService, resolver },
};
}

Expand Down
7 changes: 5 additions & 2 deletions packages/app/client/src/state/sagas/azureAuthSaga.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,13 @@ describe('The azureAuthSaga', () => {

it('should contain a single step if the token in the store is valid', () => {
store.dispatch(azureArmTokenDataChanged('a valid access_token'));

const it = azureAuthSagas()
.next()
.value.FORK.args[1]();
let val = undefined;
.value.FORK.args[1]({
payload: 'blargh',
});
let val;
let ct = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
Expand Down
54 changes: 30 additions & 24 deletions packages/app/client/src/state/sagas/azureAuthSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,32 +51,38 @@ export class AzureAuthSaga {
private static commandService: CommandServiceImpl;

public static *getArmToken(action: AzureAuthAction<AzureAuthWorkflow>): IterableIterator<any> {
let azureAuth: AzureAuthState = yield select(getArmTokenFromState);
if (azureAuth.access_token) {
const { resolver } = action.payload;

try {
let azureAuth: AzureAuthState = yield select(getArmTokenFromState);
if (azureAuth.access_token) {
return azureAuth;
}
const result = yield DialogService.showDialog(action.payload.promptDialog, action.payload.promptDialogProps);
if (result !== 1) {
// Result must be 1 which is a confirmation to sign in to Azure
return result;
}
const { RetrieveArmToken, PersistAzureLoginChanged } = SharedConstants.Commands.Azure;
const { TrackEvent } = SharedConstants.Commands.Telemetry;
azureAuth = yield call([AzureAuthSaga.commandService, AzureAuthSaga.commandService.remoteCall], RetrieveArmToken);
if (azureAuth && !('error' in azureAuth)) {
const persistLogin = yield DialogService.showDialog(action.payload.loginSuccessDialog, azureAuth);
yield call(
AzureAuthSaga.commandService.remoteCall.bind(AzureAuthSaga.commandService),
PersistAzureLoginChanged,
persistLogin
);
AzureAuthSaga.commandService.remoteCall(TrackEvent, 'signIn_success').catch(_e => void 0);
} else {
yield DialogService.showDialog(action.payload.loginFailedDialog);
AzureAuthSaga.commandService.remoteCall(TrackEvent, 'signIn_failure').catch(_e => void 0);
}
yield put(azureArmTokenDataChanged(azureAuth.access_token));
return azureAuth;
} finally {
resolver && resolver();
}
const result = yield DialogService.showDialog(action.payload.promptDialog, action.payload.promptDialogProps);
if (result !== 1) {
// Result must be 1 which is a confirmation to sign in to Azure
return result;
}
const { RetrieveArmToken, PersistAzureLoginChanged } = SharedConstants.Commands.Azure;
const { TrackEvent } = SharedConstants.Commands.Telemetry;
azureAuth = yield call([AzureAuthSaga.commandService, AzureAuthSaga.commandService.remoteCall], RetrieveArmToken);
if (azureAuth && !('error' in azureAuth)) {
const persistLogin = yield DialogService.showDialog(action.payload.loginSuccessDialog, azureAuth);
yield call(
AzureAuthSaga.commandService.remoteCall.bind(AzureAuthSaga.commandService),
PersistAzureLoginChanged,
persistLogin
);
AzureAuthSaga.commandService.remoteCall(TrackEvent, 'signIn_success').catch(_e => void 0);
} else {
yield DialogService.showDialog(action.payload.loginFailedDialog);
AzureAuthSaga.commandService.remoteCall(TrackEvent, 'signIn_failure').catch(_e => void 0);
}
yield put(azureArmTokenDataChanged(azureAuth.access_token));
return azureAuth;
}
}

Expand Down
94 changes: 81 additions & 13 deletions packages/app/client/src/state/sagas/endpointSagas.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,31 @@ import { applyMiddleware, combineReducers, createStore } from 'redux';
import sagaMiddlewareFactory from 'redux-saga';
import { Component } from 'react';
import { SharedConstants } from '@bfemulator/app-shared';
import { takeEvery, takeLatest } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { IEndpointService } from 'botframework-config';

import { bot } from '../reducers/bot';
import { load, setActive } from '../actions/botActions';
import { launchEndpointEditor, openEndpointExplorerContextMenu } from '../actions/endpointServiceActions';
import {
launchEndpointEditor,
openEndpointExplorerContextMenu,
LAUNCH_ENDPOINT_EDITOR,
OPEN_ENDPOINT_CONTEXT_MENU,
OPEN_ENDPOINT_IN_EMULATOR,
EndpointServicePayload,
EndpointServiceAction,
} from '../actions/endpointServiceActions';
import { DialogService } from '../../ui/dialogs/service';
import { OPEN_ENDPOINT_EXPLORER_CONTEXT_MENU } from '../actions/endpointActions';
import { executeCommand } from '../actions/commandActions';

import { endpointSagas } from './endpointSagas';
import { EndpointSagas, endpointSagas, getConnectedAbs } from './endpointSagas';

const sagaMiddleWare = sagaMiddlewareFactory();
const mockStore = createStore(combineReducers({ bot }), {}, applyMiddleware(sagaMiddleWare));
sagaMiddleWare.run(endpointSagas);
const mockComponentClass = class extends Component<{}, {}> {};
jest.mock('../store', () => ({
get store() {
return mockStore;
},
jest.mock('../../ui/dialogs', () => ({
DialogService: { showDialog: () => Promise.resolve(true) },
}));

const mockBot = JSON.parse(`{
"name": "TestBot",
"description": "",
Expand Down Expand Up @@ -99,20 +106,77 @@ jest.mock('electron', () => ({
}
),
}));
let mockRemoteCommandsCalled = [];

const endpointService: IEndpointService = {
appId: 'appId',
name: 'service',
appPassword: 'password123',
endpoint: 'http://localendpoint',
channelService: 'channel service',
};
const resolver = jest.fn(() => {});

const endpointPayload: EndpointServicePayload = {
endpointService,
resolver,
};
const endpointServiceAction: EndpointServiceAction<EndpointServicePayload> = {
type: OPEN_ENDPOINT_EXPLORER_CONTEXT_MENU,
payload: endpointPayload,
};

describe('The endpoint sagas', () => {
describe('The endpointSagas', () => {
let commandService: CommandServiceImpl;
let sagaMiddleware;
let mockStore;
let mockComponentClass;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();

commandService.remoteCall = async (commandName: string, ...args: any[]) => {
mockRemoteCommandsCalled.push({ commandName, args: args });

return Promise.resolve(true) as any;
};
});

beforeEach(() => {
sagaMiddleware = sagaMiddlewareFactory();
mockStore = createStore(combineReducers({ bot }), {}, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(endpointSagas);
mockComponentClass = class extends Component<{}, {}> {};
jest.mock('../store', () => ({
get store() {
return mockStore;
},
}));
mockRemoteCommandsCalled = [];
mockStore.dispatch(load([mockBot]));
mockStore.dispatch(setActive(mockBot));
});

it('should initialize the root saga', () => {
const gen = endpointSagas();

expect(gen.next().value).toEqual(takeLatest(LAUNCH_ENDPOINT_EDITOR, EndpointSagas.launchEndpointEditor));
expect(gen.next().value).toEqual(takeEvery(OPEN_ENDPOINT_CONTEXT_MENU, EndpointSagas.openEndpointContextMenu));
expect(gen.next().value).toEqual(takeEvery(OPEN_ENDPOINT_IN_EMULATOR, EndpointSagas.openEndpointInEmulator));

expect(gen.next().done).toBe(true);
});

it('should launch an endpoint editor', () => {
const gen = EndpointSagas.launchEndpointEditor(endpointServiceAction);
gen.next();
gen.next([endpointService]);
expect(gen.next().done).toBe(true);
expect(resolver).toHaveBeenCalledTimes(1);
expect(mockRemoteCommandsCalled.length).toEqual(1);
});

it('should launch the endpoint editor and execute a command to save the edited services', async () => {
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall');
const dialogServiceSpy = jest.spyOn(DialogService, 'showDialog').mockResolvedValue(mockBot.services);
Expand All @@ -134,15 +198,17 @@ describe('The endpoint sagas', () => {

const { DisplayContextMenu, ShowMessageBox } = SharedConstants.Commands.Electron;
const { NewLiveChat } = SharedConstants.Commands.Emulator;
it('should launch the endpoint editor when that menu option is chosen', () => {
it('should launch the endpoint editor when that menu option is chosen', async () => {
const commandServiceSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue({ id: 'edit' });
const dialogServiceSpy = jest.spyOn(DialogService, 'showDialog').mockResolvedValue(mockBot.services);
mockStore.dispatch(openEndpointExplorerContextMenu(mockComponentClass, mockBot.services[0]));
await mockStore.dispatch(openEndpointExplorerContextMenu(mockComponentClass, mockBot.services[0]));

expect(commandServiceSpy).toHaveBeenCalledWith(DisplayContextMenu, menuItems);
expect(dialogServiceSpy).toHaveBeenCalledWith(mockComponentClass, {
endpointService: mockBot.services[0],
});
commandServiceSpy.mockClear();
dialogServiceSpy.mockClear();
});

it('should open a deep link when that menu option is chosen', async () => {
Expand All @@ -152,6 +218,8 @@ describe('The endpoint sagas', () => {
await mockStore.dispatch(openEndpointExplorerContextMenu(mockComponentClass, mockBot.services[0]));
expect(commandServiceRemoteCallSpy).toHaveBeenCalledWith(DisplayContextMenu, menuItems);
expect(commandServiceCallSpy).toHaveBeenCalledWith(NewLiveChat, mockBot.services[0], false);
commandServiceRemoteCallSpy.mockClear();
commandServiceCallSpy.mockClear();
});

it('should forget the service when that menu item is chosen', async () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/app/client/src/state/sagas/endpointSagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class EndpointSagas {
private static commandService: CommandServiceImpl;

public static *launchEndpointEditor(action: EndpointServiceAction<EndpointEditorPayload>): IterableIterator<any> {
const { endpointEditorComponent, endpointService = {} } = action.payload;
const { endpointEditorComponent, endpointService = {}, resolver } = action.payload;
const servicesToUpdate = yield DialogService.showDialog<ComponentClass<any>, IEndpointService[]>(
endpointEditorComponent,
{ endpointService }
Expand All @@ -85,6 +85,7 @@ export class EndpointSagas {
);
}
}
resolver && resolver();
}

public static *openEndpointContextMenu(
Expand Down
Loading