This commit is contained in:
Andy Brown 2019-01-17 14:10:03 -08:00 ΠΊΠΎΠΌΠΌΠΈΡ‚ ΠΏΡ€ΠΎΠΈΠ·Π²Ρ‘Π» GitHub
Π ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒ 877843dee4
ΠšΠΎΠΌΠΌΠΈΡ‚ d71c03c25a
НС Π½Π°ΠΉΠ΄Π΅Π½ ΠΊΠ»ΡŽΡ‡, ΡΠΎΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΉ Π΄Π°Π½Π½ΠΎΠΉ подписи
Π˜Π΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ ΠΊΠ»ΡŽΡ‡Π° GPG: 4AEE18F83AFDEB23
672 ΠΈΠ·ΠΌΠ΅Π½Ρ‘Π½Π½Ρ‹Ρ… Ρ„Π°ΠΉΠ»ΠΎΠ²: 57974 Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠΉ ΠΈ 71519 ΡƒΠ΄Π°Π»Π΅Π½ΠΈΠΉ

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -10,9 +10,7 @@
],
"@babel/preset-typescript"
],
"ignore": [
"**/*.d.ts"
],
"ignore": ["**/*.d.ts"],
"sourceMaps": "inline",
"plugins": [
"@babel/plugin-proposal-class-properties",

62
.eslintrc.js Normal file
ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -0,0 +1,62 @@
module.exports = {
extends: [
'eslint:recommended',
'plugin:typescript/recommended',
'plugin:prettier/recommended',
],
plugins: ['import', 'notice'],
env: {
browser: true,
es6: true,
node: true,
},
rules: {
// eslint rules
'no-dupe-class-members': 'off',
'no-undef': 'off', // ts compiler catches this
'prefer-const': 'error',
// plugin: import
'import/first': 'error',
'import/order': ['error', { 'newlines-between': 'always' }],
// plugin: notice
'notice/notice': [
'error',
{
mustMatch: 'Copyright \\(c\\) Microsoft',
templateFile: require.resolve('./copyright.js'),
messages: {
whenFailedToMatch: 'Missing copyright header.',
},
},
],
// plugin: typescript
'typescript/explicit-function-return-type': 'off',
'typescript/explicit-member-accessibility': 'off',
'typescript/indent': 'off',
'typescript/no-empty-interface': 'warn',
'typescript/no-object-literal-type-assertion': 'off',
'typescript/no-parameter-properties': 'off',
'typescript/no-use-before-define': [
'error',
{ functions: false, classes: false },
],
},
overrides: [
{
files: ['**/*.+(js|jsx)'],
parser: 'babel-eslint',
},
{
files: ['**/*.+(test|spec).+(js|jsx|ts|tsx)'],
env: {
jest: true,
},
rules: {
'typescript/class-name-casing': 'off',
},
},
],
};

26
.eslintrc.react.js Normal file
ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -0,0 +1,26 @@
module.exports = {
extends: ['./.eslintrc.js', 'plugin:react/recommended'],
settings: {
react: {
version: 'detect',
},
},
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
rules: {
'react/no-deprecated': 'warn',
},
overrides: [
{
files: ['**/*.+(test|spec).+(js|jsx|ts|tsx)'],
rules: {
'react/display-name': 'off',
},
},
],
};

5
.prettierrc Normal file
ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -0,0 +1,5 @@
{
"parser": "typescript",
"trailingComma": "es5",
"singleQuote": true
}

32
copyright.js Normal file
ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -0,0 +1,32 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

2125
package-lock.json сгСнСрированный

Π Π°Π·Π½ΠΈΡ†Π° ΠΌΠ΅ΠΆΠ΄Ρƒ Ρ„Π°ΠΉΠ»Π°ΠΌΠΈ Π½Π΅ ΠΏΠΎΠΊΠ°Π·Π°Π½Π° ΠΈΠ·-Π·Π° своСго большого Ρ€Π°Π·ΠΌΠ΅Ρ€Π° Π—Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ Ρ€Π°Π·Π½ΠΈΡ†Ρƒ

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -2,6 +2,8 @@
"scripts": {
"bootstrap": "lerna bootstrap --hoist",
"build": "npm rebuild node-sass && lerna run build",
"lint": "lerna run lint --no-bail",
"lint:fix": "lerna run lint:fix --no-bail",
"start": "cd packages\\app\\client && npm run start",
"test": "jest --no-cache",
"test:coveralls": "jest --runInBand --bail --coverage --coverageReporters=text-lcov | coveralls"
@ -15,7 +17,10 @@
"@babel/preset-env": "^7.1.0",
"@babel/preset-typescript": "^7.1.0",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "23.6.0"
"babel-jest": "23.6.0",
"husky": "^1.3.1",
"lint-staged": "^8.1.0",
"prettier": "^1.15.3"
},
"jest": {
"setupTestFrameworkScriptFile": "./testSetup.js",
@ -35,6 +40,17 @@
"node"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"prettier --write",
"git add"
]
},
"dependencies": {
"lerna": "3.4.0"
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -0,0 +1 @@
**/*.scss.d.ts

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -0,0 +1,3 @@
module.exports = {
extends: '../../../.eslintrc.react.js',
};

802
packages/app/client/package-lock.json сгСнСрированный

Π Π°Π·Π½ΠΈΡ†Π° ΠΌΠ΅ΠΆΠ΄Ρƒ Ρ„Π°ΠΉΠ»Π°ΠΌΠΈ Π½Π΅ ΠΏΠΎΠΊΠ°Π·Π°Π½Π° ΠΈΠ·-Π·Π° своСго большого Ρ€Π°Π·ΠΌΠ΅Ρ€Π° Π—Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ Ρ€Π°Π·Π½ΠΈΡ†Ρƒ

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -12,7 +12,8 @@
"build:shared:dev": "webpack --mode development --progress --colors",
"build:app:dev": "webpack --mode development --progress --colors",
"build": "run-s lint build:vendors build:shared build:app",
"lint": "tslint --project tsconfig.json",
"lint": "eslint --color --quiet --ext .js,.jsx,.ts,.tsx ./src",
"lint:fix": "npm run lint -- --fix",
"start": "run-s build:vendors:dev build:shared:dev webpackdevServer:dev",
"webpackdevServer:dev": "webpack-dev-server --mode development --hot --inline --progress --colors --content-base ./public",
"test": "jest"
@ -50,6 +51,7 @@
"@types/react": "~16.3.2",
"@types/react-dom": "^16.0.4",
"@types/request": "^2.47.0",
"babel-eslint": "^10.0.1",
"babel-jest": "23.6.0",
"babel-loader": "^8.0.2",
"babel-preset-react-app": "^3.1.1",
@ -59,6 +61,12 @@
"css-loader": "^0.28.11",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"eslint": "^5.12.0",
"eslint-config-prettier": "^3.5.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-notice": "^0.7.7",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-typescript": "^1.0.0-rc.3",
"file-loader": "^1.1.11",
"hard-source-webpack-plugin": "^0.12.0",
"jest": "^23.0.0",
@ -75,8 +83,6 @@
"sass-loader": "^7.1.0",
"style-loader": "^0.21.0",
"ts-loader": "^4.4.2",
"tslint": "^5.10.0",
"tslint-loader": "^3.6.0",
"typescript": "3.1.1",
"typings-for-css-modules-loader": "^1.7.0",
"url-loader": "^1.0.1",

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,44 +1,81 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { SharedConstants } from '@bfemulator/app-shared';
import { BotConfigWithPathImpl, CommandRegistryImpl } from '@bfemulator/sdk-shared';
import {
BotConfigWithPathImpl,
CommandRegistryImpl,
} from '@bfemulator/sdk-shared';
import { combineReducers, createStore } from 'redux';
import * as BotActions from '../data/action/botActions';
import { bot } from '../data/reducer/bot';
import { resources } from '../data/reducer/resourcesReducer';
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
import { ActiveBotHelper } from '../ui/helpers/activeBotHelper';
import { registerCommands } from './botCommands';
import { combineReducers, createStore } from 'redux';
const mockBotInfo = {
path: 'some/path.bot',
displayName: 'MyBot',
secret: 'secret'
secret: 'secret',
};
const mockBot = BotConfigWithPathImpl.fromJSON({
'path': 'some/path',
'name': 'AuthBot',
'description': '',
'padlock': '',
'services': [
path: 'some/path',
name: 'AuthBot',
description: '',
padlock: '',
services: [
{
'appId': '4f8fde3f-48d3-4d8a-a954-393efe39809e',
'id': 'cded37c0-83f2-11e8-ac6d-b7172cd24b28',
'type': 'endpoint',
'appPassword': 'REDACTED',
'endpoint': 'http://localhost:55697/api/messages',
'name': 'authsample'
}
]
appId: '4f8fde3f-48d3-4d8a-a954-393efe39809e',
id: 'cded37c0-83f2-11e8-ac6d-b7172cd24b28',
type: 'endpoint',
appPassword: 'REDACTED',
endpoint: 'http://localhost:55697/api/messages',
name: 'authsample',
},
],
} as any);
let mockStore = createStore(combineReducers({ bot, resources }), {
bot: { botFiles: [mockBotInfo] }
const mockStore = createStore(combineReducers({ bot, resources }), {
bot: { botFiles: [mockBotInfo] },
});
jest.mock('../data/store', () => ({
get store() {
return mockStore;
}
},
}));
jest.mock('../ui/dialogs/', () => ({}));
@ -51,7 +88,9 @@ describe('The bot commands', () => {
it('should make the appropriate calls to switch bots', () => {
const spy = jest.spyOn(ActiveBotHelper, 'confirmAndSwitchBots');
const { handler } = registry.getCommand(SharedConstants.Commands.Bot.Switch);
const { handler } = registry.getCommand(
SharedConstants.Commands.Bot.Switch
);
handler({});
expect(spy).toHaveBeenCalledWith({});
});
@ -82,11 +121,15 @@ describe('The bot commands', () => {
it('should make the appropriate calls to sync the bot list', () => {
const dispatchSpy = jest.spyOn(mockStore, 'dispatch');
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall');
const { handler } = registry.getCommand(SharedConstants.Commands.Bot.SyncBotList);
const { handler } = registry.getCommand(
SharedConstants.Commands.Bot.SyncBotList
);
handler([{}]);
expect(dispatchSpy).toHaveBeenCalledWith(BotActions.load([{}]));
expect(remoteCallSpy).toHaveBeenCalledWith(SharedConstants.Commands.Electron.UpdateFileMenu);
expect(remoteCallSpy).toHaveBeenCalledWith(
SharedConstants.Commands.Electron.UpdateFileMenu
);
});
it('should make the appropriate call when setting the active bot', async () => {
@ -95,7 +138,9 @@ describe('The bot commands', () => {
remoteCallArgs.push(args);
return true;
};
const { handler } = registry.getCommand(SharedConstants.Commands.Bot.SetActive);
const { handler } = registry.getCommand(
SharedConstants.Commands.Bot.SetActive
);
await handler(mockBot, mockBotInfo.path);
const state: any = mockStore.getState();
expect(state.bot.activeBot).toEqual(mockBot);
@ -104,23 +149,32 @@ describe('The bot commands', () => {
});
it('should dispatch the appropriate actions when updating the list of transcript files on disc', () => {
const { handler: transcriptFilesUpdated } = registry.getCommand(SharedConstants.Commands.Bot.TranscriptFilesUpdated);
const { handler: transcriptPathUpdated } = registry.getCommand(SharedConstants.Commands.Bot.TranscriptsPathUpdated);
const { handler: transcriptFilesUpdated } = registry.getCommand(
SharedConstants.Commands.Bot.TranscriptFilesUpdated
);
const { handler: transcriptPathUpdated } = registry.getCommand(
SharedConstants.Commands.Bot.TranscriptsPathUpdated
);
transcriptFilesUpdated([{ path: 'transcript/path.transcript' }]);
transcriptPathUpdated('transcript/');
const state: any = mockStore.getState();
expect(state.resources.transcripts).toEqual([{ path: 'transcript/path.transcript' }]);
expect(state.resources.transcripts).toEqual([
{ path: 'transcript/path.transcript' },
]);
expect(state.resources.transcriptsPath).toBe('transcript/');
});
it('should dispatch the appropriate actions when updating the list of chat files on disc', () => {
const { handler: chatFilesUpdated } = registry.getCommand(SharedConstants.Commands.Bot.ChatFilesUpdated);
const { handler: chatPathUpdated } = registry.getCommand(SharedConstants.Commands.Bot.ChatsPathUpdated);
const { handler: chatFilesUpdated } = registry.getCommand(
SharedConstants.Commands.Bot.ChatFilesUpdated
);
const { handler: chatPathUpdated } = registry.getCommand(
SharedConstants.Commands.Bot.ChatsPathUpdated
);
chatFilesUpdated([{ path: 'chat/path.chat' }]);
chatPathUpdated('chat/');
const state: any = mockStore.getState();
expect(state.resources.chats).toEqual([{ path: 'chat/path.chat' }]);
expect(state.resources.chatsPath).toBe('chat/');
});
});

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,21 +31,26 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import {
BotInfo,
getBotDisplayName,
SharedConstants,
} from '@bfemulator/app-shared';
import { BotConfigWithPath, CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { ActiveBotHelper } from '../ui/helpers/activeBotHelper';
import { pathExistsInRecentBots } from '../data/botHelpers';
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
import { store } from '../data/store';
import { IFileService } from 'botframework-config/lib/schema';
import * as BotActions from '../data/action/botActions';
import * as FileActions from '../data/action/fileActions';
import { BotInfo, getBotDisplayName, SharedConstants } from '@bfemulator/app-shared';
import {
chatFilesUpdated,
chatsDirectoryUpdated,
transcriptDirectoryUpdated,
transcriptsUpdated
transcriptsUpdated,
} from '../data/action/resourcesAction';
import { IFileService } from 'botframework-config/lib/schema';
import { pathExistsInRecentBots } from '../data/botHelpers';
import { store } from '../data/store';
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
import { ActiveBotHelper } from '../ui/helpers/activeBotHelper';
/** Registers bot commands */
export function registerCommands(commandRegistry: CommandRegistryImpl) {
@ -53,58 +58,89 @@ export function registerCommands(commandRegistry: CommandRegistryImpl) {
// ---------------------------------------------------------------------------
// Switches the current active bot
commandRegistry.registerCommand(Commands.Bot.Switch,
(bot: BotConfigWithPath | string) => ActiveBotHelper.confirmAndSwitchBots(bot));
commandRegistry.registerCommand(
Commands.Bot.Switch,
(bot: BotConfigWithPath | string) =>
ActiveBotHelper.confirmAndSwitchBots(bot)
);
// ---------------------------------------------------------------------------
// Closes the current active bot
commandRegistry.registerCommand(Commands.Bot.Close, () => ActiveBotHelper.confirmAndCloseBot());
commandRegistry.registerCommand(Commands.Bot.Close, () =>
ActiveBotHelper.confirmAndCloseBot()
);
// ---------------------------------------------------------------------------
// Browse for a .bot file and open it
commandRegistry.registerCommand(Commands.Bot.OpenBrowse, () => ActiveBotHelper.confirmAndOpenBotFromFile());
commandRegistry.registerCommand(Commands.Bot.OpenBrowse, () =>
ActiveBotHelper.confirmAndOpenBotFromFile()
);
// ---------------------------------------------------------------------------
// Loads the bot on the client side using the activeBotHelper
commandRegistry.registerCommand(Commands.Bot.Load, (bot: BotConfigWithPath): Promise<any> => {
if (!pathExistsInRecentBots(bot.path)) {
// create and switch bots
return ActiveBotHelper.confirmAndCreateBot(bot, '');
commandRegistry.registerCommand(
Commands.Bot.Load,
(bot: BotConfigWithPath): Promise<any> => {
if (!pathExistsInRecentBots(bot.path)) {
// create and switch bots
return ActiveBotHelper.confirmAndCreateBot(bot, '');
}
return ActiveBotHelper.confirmAndSwitchBots(bot);
}
return ActiveBotHelper.confirmAndSwitchBots(bot);
});
);
// ---------------------------------------------------------------------------
// Syncs the client side list of bots with bots arg (usually called from server side)
commandRegistry.registerCommand(Commands.Bot.SyncBotList, async (bots: BotInfo[]): Promise<void> => {
store.dispatch(BotActions.load(bots));
await CommandServiceImpl.remoteCall(Commands.Electron.UpdateFileMenu);
});
commandRegistry.registerCommand(
Commands.Bot.SyncBotList,
async (bots: BotInfo[]): Promise<void> => {
store.dispatch(BotActions.load(bots));
await CommandServiceImpl.remoteCall(Commands.Electron.UpdateFileMenu);
}
);
// ---------------------------------------------------------------------------
// Sets a bot as active (called from server-side)
commandRegistry.registerCommand(Commands.Bot.SetActive, async (bot: BotConfigWithPath, botDirectory: string) => {
store.dispatch(BotActions.setActive(bot));
store.dispatch(FileActions.setRoot(botDirectory));
await Promise.all([
CommandServiceImpl.remoteCall(Commands.Electron.UpdateFileMenu),
CommandServiceImpl.remoteCall(Commands.Electron.SetTitleBar, getBotDisplayName(bot))
]);
});
commandRegistry.registerCommand(
Commands.Bot.SetActive,
async (bot: BotConfigWithPath, botDirectory: string) => {
store.dispatch(BotActions.setActive(bot));
store.dispatch(FileActions.setRoot(botDirectory));
await Promise.all([
CommandServiceImpl.remoteCall(Commands.Electron.UpdateFileMenu),
CommandServiceImpl.remoteCall(
Commands.Electron.SetTitleBar,
getBotDisplayName(bot)
),
]);
}
);
commandRegistry.registerCommand(Commands.Bot.TranscriptFilesUpdated, (transcripts: IFileService[]) => {
store.dispatch(transcriptsUpdated(transcripts));
});
commandRegistry.registerCommand(
Commands.Bot.TranscriptFilesUpdated,
(transcripts: IFileService[]) => {
store.dispatch(transcriptsUpdated(transcripts));
}
);
commandRegistry.registerCommand(Commands.Bot.ChatFilesUpdated, (chatFiles: IFileService[]) => {
store.dispatch(chatFilesUpdated(chatFiles));
});
commandRegistry.registerCommand(
Commands.Bot.ChatFilesUpdated,
(chatFiles: IFileService[]) => {
store.dispatch(chatFilesUpdated(chatFiles));
}
);
commandRegistry.registerCommand(Commands.Bot.TranscriptsPathUpdated, (path: string) => {
store.dispatch(transcriptDirectoryUpdated(path));
});
commandRegistry.registerCommand(
Commands.Bot.TranscriptsPathUpdated,
(path: string) => {
store.dispatch(transcriptDirectoryUpdated(path));
}
);
commandRegistry.registerCommand(Commands.Bot.ChatsPathUpdated, (path: string) => {
store.dispatch(chatsDirectoryUpdated(path));
});
commandRegistry.registerCommand(
Commands.Bot.ChatsPathUpdated,
(path: string) => {
store.dispatch(chatsDirectoryUpdated(path));
}
);
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,8 +31,8 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
/** Registers electron commands */
export function registerCommands(commandRegistry: CommandRegistryImpl) {
@ -45,15 +45,20 @@ export function registerCommands(commandRegistry: CommandRegistryImpl) {
// ---------------------------------------------------------------------------
// An update is ready to install
commandRegistry.registerCommand(Electron.UpdateAvailable, (...args: any[]) => {
// TODO: Show a notification
console.log('Update available', ...args);
});
commandRegistry.registerCommand(
Electron.UpdateAvailable,
(...args: any[]) => {
// TODO: Show a notification
// eslint-disable-next-line no-console
console.log('Update available', ...args);
}
);
// ---------------------------------------------------------------------------
// Application is up to date
commandRegistry.registerCommand(Electron.UpdateNotAvailable, () => {
// TODO: Show a notification
// eslint-disable-next-line no-console
console.log('Application is up to date');
});

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,25 +1,59 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { newNotification, SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { combineReducers, createStore } from 'redux';
import { clientAwareSettingsChanged } from '../data/action/clientAwareSettingsActions';
import { beginAdd } from '../data/action/notificationActions';
import { bot } from '../data/reducer/bot';
import { chat } from '../data/reducer/chat';
import { clientAwareSettings } from '../data/reducer/clientAwareSettingsReducer';
import { editor } from '../data/reducer/editor';
import { combineReducers, createStore } from 'redux';
import { RootState, store } from '../data/store';
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
import { registerCommands } from './emulatorCommands';
const mockEndpoint = {
endpoint: 'https://localhost:8080/api/messages'
endpoint: 'https://localhost:8080/api/messages',
};
let mockStore;
jest.mock('../data/store', () => ({
get store() {
return mockStore;
}
},
}));
jest.mock('../ui/dialogs/', () => ({}));
@ -31,17 +65,23 @@ describe('The emulator commands', () => {
});
beforeEach(() => {
mockStore = createStore(combineReducers({ bot, chat, clientAwareSettings, editor }));
mockStore.dispatch(clientAwareSettingsChanged({
users: { currentUserId: '1234' },
cwd: 'path',
locale: 'en-us',
serverUrl: 'https://localhost'
}));
mockStore = createStore(
combineReducers({ bot, chat, clientAwareSettings, editor })
);
mockStore.dispatch(
clientAwareSettingsChanged({
users: { currentUserId: '1234' },
cwd: 'path',
locale: 'en-us',
serverUrl: 'https://localhost',
})
);
});
it('Should open a new emulator tabbed document for an endpoint', () => {
const { handler } = registry.getCommand(SharedConstants.Commands.Emulator.NewLiveChat);
const { handler } = registry.getCommand(
SharedConstants.Commands.Emulator.NewLiveChat
);
const documentId = handler(mockEndpoint, false);
const state: RootState = mockStore.getState();
const documentIds = Object.keys(state.chat.chats);
@ -50,18 +90,28 @@ describe('The emulator commands', () => {
});
it('should set the active tab of an existing chat', () => {
const { handler } = registry.getCommand(SharedConstants.Commands.Emulator.NewLiveChat);
const { handler } = registry.getCommand(
SharedConstants.Commands.Emulator.NewLiveChat
);
const documentId = handler(mockEndpoint, false);
const secondDocumentId = handler({ endpoint: 'https://localhost:8181/api/messages' });
const secondDocumentId = handler({
endpoint: 'https://localhost:8181/api/messages',
});
// At this point we should have 2 open documents
// with the second on
expect(mockStore.getState().editor.editors.primary.activeDocumentId).toBe(secondDocumentId);
expect(mockStore.getState().editor.editors.primary.activeDocumentId).toBe(
secondDocumentId
);
handler(mockEndpoint, true); // re-open the original document
expect(mockStore.getState().editor.editors.primary.activeDocumentId).toBe(documentId);
expect(mockStore.getState().editor.editors.primary.activeDocumentId).toBe(
documentId
);
});
it('should open a transcript', () => {
const { handler } = registry.getCommand(SharedConstants.Commands.Emulator.OpenTranscript);
const { handler } = registry.getCommand(
SharedConstants.Commands.Emulator.OpenTranscript
);
const filePath = 'transcript.transcript';
handler(filePath, filePath);
@ -71,28 +121,44 @@ describe('The emulator commands', () => {
});
it('Should prompt to open a transcript', async () => {
const { handler } = registry.getCommand(SharedConstants.Commands.Emulator.PromptToOpenTranscript);
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue('transcript.transcript');
const { handler } = registry.getCommand(
SharedConstants.Commands.Emulator.PromptToOpenTranscript
);
const remoteCallSpy = jest
.spyOn(CommandServiceImpl, 'remoteCall')
.mockResolvedValue('transcript.transcript');
const callSpy = jest.spyOn(CommandServiceImpl, 'call');
await handler();
expect(remoteCallSpy).toHaveBeenCalledWith('shell:showExplorer-open-dialog', {
'buttonLabel': 'Choose file',
'filters': [{ 'extensions': ['transcript'], 'name': 'Transcript Files' }],
'properties': ['openFile'],
'title': 'Open transcript file'
});
expect(remoteCallSpy).toHaveBeenCalledWith(
'shell:showExplorer-open-dialog',
{
buttonLabel: 'Choose file',
filters: [{ extensions: ['transcript'], name: 'Transcript Files' }],
properties: ['openFile'],
title: 'Open transcript file',
}
);
expect(callSpy).toHaveBeenCalledWith('transcript:open', 'transcript.transcript');
expect(callSpy).toHaveBeenCalledWith(
'transcript:open',
'transcript.transcript'
);
});
it('should dispatch a notification when opening a transcript fails', async () => {
const { handler } = registry.getCommand(SharedConstants.Commands.Emulator.PromptToOpenTranscript);
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue('transcript.transcript');
const callSpy = jest.spyOn(CommandServiceImpl, 'call').mockImplementationOnce(() => {
throw new Error('Oh noes!');
});
const { handler } = registry.getCommand(
SharedConstants.Commands.Emulator.PromptToOpenTranscript
);
const remoteCallSpy = jest
.spyOn(CommandServiceImpl, 'remoteCall')
.mockResolvedValue('transcript.transcript');
const callSpy = jest
.spyOn(CommandServiceImpl, 'call')
.mockImplementationOnce(() => {
throw new Error('Oh noes!');
});
const dispatchSpy = jest.spyOn(mockStore, 'dispatch');
const errMsg = `Error while opening transcript file: Error: Oh noes!`;
const notification = newNotification(errMsg);
@ -101,18 +167,24 @@ describe('The emulator commands', () => {
action.payload.notification.id = jasmine.any(String) as any;
await handler();
expect(remoteCallSpy).toHaveBeenCalled();
expect(callSpy).toHaveBeenCalledWith('transcript:open', 'transcript.transcript');
expect(callSpy).toHaveBeenCalledWith(
'transcript:open',
'transcript.transcript'
);
expect(dispatchSpy).toHaveBeenCalledWith(action);
jest.restoreAllMocks();
});
it('should reload a transcript', async () => {
const { handler: openTranscriptHandler } =
registry.getCommand(SharedConstants.Commands.Emulator.OpenTranscript);
const { handler: openTranscriptHandler } = registry.getCommand(
SharedConstants.Commands.Emulator.OpenTranscript
);
await openTranscriptHandler('transcript.transcript');
let state = mockStore.getState();
expect(state.chat.changeKey).toBe(1);
const { handler } = registry.getCommand(SharedConstants.Commands.Emulator.ReloadTranscript);
const { handler } = registry.getCommand(
SharedConstants.Commands.Emulator.ReloadTranscript
);
await handler('transcript.transcript');
state = mockStore.getState();
expect(state.chat.changeKey).toBe(3);

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -32,8 +32,13 @@
//
import { newNotification, SharedConstants } from '@bfemulator/app-shared';
import { Activity, CommandRegistryImpl, uniqueId } from '@bfemulator/sdk-shared';
import {
Activity,
CommandRegistryImpl,
uniqueId,
} from '@bfemulator/sdk-shared';
import { IEndpointService } from 'botframework-config/lib/schema';
import * as Constants from '../constants';
import * as ChatActions from '../data/action/chatActions';
import * as EditorActions from '../data/action/editorActions';
@ -48,66 +53,71 @@ export function registerCommands(commandRegistry: CommandRegistryImpl) {
// ---------------------------------------------------------------------------
// Open a new emulator tabbed document
commandRegistry.registerCommand(Emulator.NewLiveChat,
commandRegistry.registerCommand(
Emulator.NewLiveChat,
(endpoint: IEndpointService, focusExistingChat: boolean = false) => {
const state = store.getState();
let documentId: string;
if (focusExistingChat && state.chat.chats) {
const { chats } = state.chat;
documentId = Object.keys(chats)
.find((docId) => chats[docId].endpointUrl === endpoint.endpoint);
documentId = Object.keys(chats).find(
docId => chats[docId].endpointUrl === endpoint.endpoint
);
}
if (!documentId) {
documentId = uniqueId();
const { currentUserId } = state.clientAwareSettings.users;
store.dispatch(ChatActions.newDocument(
documentId,
'livechat',
{
store.dispatch(
ChatActions.newDocument(documentId, 'livechat', {
botId: 'bot',
endpointId: endpoint.id,
endpointUrl: endpoint.endpoint,
userId: currentUserId
}
));
userId: currentUserId,
})
);
}
store.dispatch(EditorActions.open({
contentType: Constants.CONTENT_TYPE_LIVE_CHAT,
documentId,
isGlobal: false
}));
store.dispatch(
EditorActions.open({
contentType: Constants.CONTENT_TYPE_LIVE_CHAT,
documentId,
isGlobal: false,
})
);
return documentId;
});
}
);
// ---------------------------------------------------------------------------
// Open the transcript file in a tabbed document
commandRegistry.registerCommand(Emulator.OpenTranscript,
commandRegistry.registerCommand(
Emulator.OpenTranscript,
(filePath: string, fileName: string, additionalData?: object) => {
const tabGroup = getTabGroupForDocument(filePath);
const { currentUserId } = store.getState().clientAwareSettings.users;
if (!tabGroup) {
store.dispatch(ChatActions.newDocument(
filePath,
'transcript',
{
store.dispatch(
ChatActions.newDocument(filePath, 'transcript', {
...additionalData,
botId: 'bot',
userId: currentUserId
}
));
userId: currentUserId,
})
);
}
store.dispatch(EditorActions.open({
contentType: Constants.CONTENT_TYPE_TRANSCRIPT,
documentId: filePath,
fileName,
filePath,
isGlobal: false
}));
});
store.dispatch(
EditorActions.open({
contentType: Constants.CONTENT_TYPE_TRANSCRIPT,
documentId: filePath,
fileName,
filePath,
isGlobal: false,
})
);
}
);
// ---------------------------------------------------------------------------
// Prompt to open a transcript file, then open it
@ -119,13 +129,16 @@ export function registerCommands(commandRegistry: CommandRegistryImpl) {
filters: [
{
name: 'Transcript Files',
extensions: ['transcript']
}
extensions: ['transcript'],
},
],
};
try {
const { ShowOpenDialog } = SharedConstants.Commands.Electron;
const filename = await CommandServiceImpl.remoteCall(ShowOpenDialog, dialogOptions);
const filename = await CommandServiceImpl.remoteCall(
ShowOpenDialog,
dialogOptions
);
await CommandServiceImpl.call(Emulator.OpenTranscript, filename);
} catch (e) {
const errMsg = `Error while opening transcript file: ${e}`;
@ -136,48 +149,75 @@ export function registerCommands(commandRegistry: CommandRegistryImpl) {
// ---------------------------------------------------------------------------
// Same as open transcript, except that it closes the transcript first, before reopening it
commandRegistry.registerCommand(Emulator.ReloadTranscript,
commandRegistry.registerCommand(
Emulator.ReloadTranscript,
(filePath: string, fileName: string, additionalData?: object) => {
const tabGroup = getTabGroupForDocument(filePath);
const { currentUserId } = store.getState().clientAwareSettings.users;
if (tabGroup) {
store.dispatch(EditorActions.close(getTabGroupForDocument(filePath), filePath));
store.dispatch(
EditorActions.close(getTabGroupForDocument(filePath), filePath)
);
store.dispatch(ChatActions.closeDocument(filePath));
}
store.dispatch(ChatActions.newDocument(
filePath,
'transcript',
{
store.dispatch(
ChatActions.newDocument(filePath, 'transcript', {
...additionalData,
botId: 'bot',
userId: currentUserId
}
));
store.dispatch(EditorActions.open({
contentType: Constants.CONTENT_TYPE_TRANSCRIPT,
documentId: filePath,
filePath,
fileName,
isGlobal: false
}));
});
userId: currentUserId,
})
);
store.dispatch(
EditorActions.open({
contentType: Constants.CONTENT_TYPE_TRANSCRIPT,
documentId: filePath,
filePath,
fileName,
isGlobal: false,
})
);
}
);
// ---------------------------------------------------------------------------
// Open the chat file in a tabbed document as a transcript
commandRegistry.registerCommand(Emulator.OpenChatFile, async (filePath: string, reload?: boolean) => {
try {
// wait for the main side to use the chatdown library to parse the activities (transcript) out of the .chat file
const { activities, fileName }: { activities: Activity[], fileName: string }
= await CommandServiceImpl.remoteCall(Emulator.OpenChatFile, filePath);
commandRegistry.registerCommand(
Emulator.OpenChatFile,
async (filePath: string, reload?: boolean) => {
try {
// wait for the main side to use the chatdown library to parse the activities (transcript) out of the .chat file
const {
activities,
fileName,
}: {
activities: Activity[];
fileName: string;
} = await CommandServiceImpl.remoteCall(
Emulator.OpenChatFile,
filePath
);
// open or reload the transcript
if (reload) {
await CommandServiceImpl.call(Emulator.ReloadTranscript, filePath, fileName, { activities, inMemory: true });
} else {
await CommandServiceImpl.call(Emulator.OpenTranscript, filePath, fileName, { activities, inMemory: true });
// open or reload the transcript
if (reload) {
await CommandServiceImpl.call(
Emulator.ReloadTranscript,
filePath,
fileName,
{ activities, inMemory: true }
);
} else {
await CommandServiceImpl.call(
Emulator.OpenTranscript,
filePath,
fileName,
{ activities, inMemory: true }
);
}
} catch (err) {
throw new Error(
`Error while retrieving activities from main side: ${err}`
);
}
} catch (err) {
throw new Error(`Error while retrieving activities from main side: ${err}`);
}
});
);
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,24 +31,29 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { store } from '../data/store';
import * as FileActions from '../data/action/fileActions';
import * as EditorActions from '../data/action/editorActions';
import {
isChatFile,
isTranscriptFile,
SharedConstants,
} from '@bfemulator/app-shared';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { SharedConstants, isChatFile, isTranscriptFile } from '@bfemulator/app-shared';
import * as EditorActions from '../data/action/editorActions';
import * as FileActions from '../data/action/fileActions';
import { store } from '../data/store';
/** Registers file commands */
export function registerCommands(commandRegistry: CommandRegistryImpl) {
const {File} = SharedConstants.Commands;
const { File } = SharedConstants.Commands;
// ---------------------------------------------------------------------------
// Adds a file to the file store
commandRegistry.registerCommand(File.Add, (payload) => {
commandRegistry.registerCommand(File.Add, payload => {
store.dispatch(FileActions.addFile(payload));
});
// ---------------------------------------------------------------------------
// Removes a file from the file store
commandRegistry.registerCommand(File.Remove, (path) => {
commandRegistry.registerCommand(File.Remove, path => {
store.dispatch(FileActions.removeFile(path));
});

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,9 +31,10 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { store } from '../data/store';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { store } from '../data/store';
/** Registers miscellaneous commands */
export function registerCommands(commandRegistry: CommandRegistryImpl) {

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,12 +31,13 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { store } from '../data/store';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { SharedConstants } from '@bfemulator/app-shared';
import { Notification } from '@bfemulator/app-shared';
import { getGlobal } from '../utils';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import * as NotificationActions from '../data/action/notificationActions';
import { store } from '../data/store';
import { getGlobal } from '../utils';
/** Registers notification commands */
export function registerCommands(commandRegistry: CommandRegistryImpl) {
@ -44,7 +45,9 @@ export function registerCommands(commandRegistry: CommandRegistryImpl) {
// ---------------------------------------------------------------------------
// Adds a notification from the main side to the store / notification manager
commandRegistry.registerCommand(Commands.Add, () => {
const notification: Notification = getGlobal(SharedConstants.NOTIFICATION_FROM_MAIN);
const notification: Notification = getGlobal(
SharedConstants.NOTIFICATION_FROM_MAIN
);
store.dispatch(NotificationActions.beginAdd(notification));
});

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,18 +31,19 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { ExtensionManager } from '../extensions';
import * as LogService from '../platform/log/logService';
import { registerCommands as registerBotCommands } from './botCommands';
import { registerCommands as registerElectronCommands } from './electronCommands';
import { registerCommands as registerEmulatorCommands } from './emulatorCommands';
import { registerCommands as registerFileCommands } from './fileCommands';
import { registerCommands as registerMiscCommands } from './miscCommands';
import { registerCommands as registerNotificationCommands } from './notificationCommands';
import { registerCommands as registerUICommands } from './uiCommands';
import { registerCommands as registerSettingsCommand } from './settingsCommands';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { registerCommands as registerUICommands } from './uiCommands';
/** Registers all commands */
export function registerAllCommands(commandRegistry: CommandRegistryImpl) {

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,16 +1,51 @@
import { registerCommands } from './settingsCommands';
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { SharedConstants } from '@bfemulator/app-shared';
import { clientAwareSettings } from '../data/reducer/clientAwareSettingsReducer';
import { combineReducers, createStore } from 'redux';
import { clientAwareSettings } from '../data/reducer/clientAwareSettingsReducer';
import { store } from '../data/store';
import { clientAwareSettingsChanged } from '../data/action/clientAwareSettingsActions';
let mockStore = createStore(combineReducers({ clientAwareSettings }));
import { registerCommands } from './settingsCommands';
const mockStore = createStore(combineReducers({ clientAwareSettings }));
jest.mock('../data/store', () => ({
get store() {
return mockStore;
}
},
}));
describe('the settings commands', () => {
@ -21,9 +56,13 @@ describe('the settings commands', () => {
});
it('should dispatch to the store when settings are sent from the main side', () => {
const command = registry.getCommand(SharedConstants.Commands.Settings.ReceiveGlobalSettings).handler;
const command = registry.getCommand(
SharedConstants.Commands.Settings.ReceiveGlobalSettings
).handler;
const dispatchSpy = jest.spyOn(store, 'dispatch');
command({});
expect(dispatchSpy).toHaveBeenCalledWith(clientAwareSettingsChanged({} as any));
expect(dispatchSpy).toHaveBeenCalledWith(
clientAwareSettingsChanged({} as any)
);
});
});

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,16 +31,20 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { ClientAwareSettings, SharedConstants } from '@bfemulator/app-shared';
import { store } from '../data/store';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { clientAwareSettingsChanged } from '../data/action/clientAwareSettingsActions';
import { store } from '../data/store';
/** Registers settings commands */
export function registerCommands(commandRegistry: CommandRegistryImpl) {
const { Settings } = SharedConstants.Commands;
commandRegistry.registerCommand(Settings.ReceiveGlobalSettings, (settings: ClientAwareSettings) => {
store.dispatch(clientAwareSettingsChanged(settings));
});
commandRegistry.registerCommand(
Settings.ReceiveGlobalSettings,
(settings: ClientAwareSettings) => {
store.dispatch(clientAwareSettingsChanged(settings));
}
);
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,31 +1,71 @@
jest.mock('../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: class {
},
AzureLoginSuccessDialogContainer: class {
},
BotCreationDialog: class {
},
DialogService: { showDialog: () => Promise.resolve(true) },
SecretPromptDialog: class {
}
}
));
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { CONTENT_TYPE_APP_SETTINGS, DOCUMENT_ID_APP_SETTINGS } from '../constants';
import { AzureAuthAction, AzureAuthWorkflow, invalidateArmToken } from '../data/action/azureAuthActions';
import {
CONTENT_TYPE_APP_SETTINGS,
DOCUMENT_ID_APP_SETTINGS,
} from '../constants';
import {
AzureAuthAction,
AzureAuthWorkflow,
invalidateArmToken,
} from '../data/action/azureAuthActions';
import { EditorActions, OpenEditorAction } from '../data/action/editorActions';
import { NavBarActions, SelectNavBarAction } from '../data/action/navBarActions';
import {
NavBarActions,
SelectNavBarAction,
} from '../data/action/navBarActions';
import * as editorHelpers from '../data/editorHelpers';
import { store } from '../data/store';
import {
AzureLoginPromptDialogContainer,
AzureLoginSuccessDialogContainer,
BotCreationDialog,
DialogService, OpenBotDialogContainer,
SecretPromptDialogContainer
DialogService,
OpenBotDialogContainer,
SecretPromptDialogContainer,
} from '../ui/dialogs';
import { registerCommands } from './uiCommands';
jest.mock('../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: class {},
AzureLoginSuccessDialogContainer: class {},
BotCreationDialog: class {},
DialogService: { showDialog: () => Promise.resolve(true) },
SecretPromptDialog: class {},
}));
const Commands = SharedConstants.Commands.UI;
@ -44,37 +84,45 @@ describe('the uiCommands', () => {
it('should call DialogService.showDialog when the ShowBotCreationDialog command is dispatched', async () => {
const spy = jest.spyOn(DialogService, 'showDialog');
const result = await registry.getCommand(Commands.ShowBotCreationDialog).handler();
const result = await registry
.getCommand(Commands.ShowBotCreationDialog)
.handler();
expect(spy).toHaveBeenCalledWith(BotCreationDialog);
expect(result).toBe(true);
});
it('should call DialogService.showDialog when the ShowSecretPromptDialog command is dispatched', async () => {
const spy = jest.spyOn(DialogService, 'showDialog');
const result = await registry.getCommand(Commands.ShowSecretPromptDialog).handler();
const result = await registry
.getCommand(Commands.ShowSecretPromptDialog)
.handler();
expect(spy).toHaveBeenCalledWith(SecretPromptDialogContainer);
expect(result).toBe(true);
});
it('should call DialogService.showDialog when the ShowOpenBotDialog command is dispatched', async () => {
const spy = jest.spyOn(DialogService, 'showDialog');
const result = await registry.getCommand(Commands.ShowOpenBotDialog).handler();
const result = await registry
.getCommand(Commands.ShowOpenBotDialog)
.handler();
expect(spy).toHaveBeenCalledWith(OpenBotDialogContainer);
expect(result).toBe(true);
});
describe('should dispatch the appropriate action to the store', () => {
it('when the SwitchNavBarTab command is dispatched', () => {
// eslint-disable-next-line prefer-const
let arg: SelectNavBarAction = {} as SelectNavBarAction;
store.dispatch = action => (arg as any) = action;
store.dispatch = action => ((arg as any) = action);
registry.getCommand(Commands.SwitchNavBarTab).handler('Do it Nauuuw!');
expect(arg.type).toBe(NavBarActions.select);
expect(arg.payload.selection).toBe('Do it Nauuuw!');
});
it('when the ShowAppSettings command is dispatched', () => {
// eslint-disable-next-line prefer-const
let arg: OpenEditorAction = {} as OpenEditorAction;
store.dispatch = action => (arg as any) = action;
store.dispatch = action => ((arg as any) = action);
registry.getCommand(Commands.ShowAppSettings).handler();
expect(arg.type).toBe(EditorActions.open);
expect(arg.payload.contentType).toBe(CONTENT_TYPE_APP_SETTINGS);
@ -83,23 +131,29 @@ describe('the uiCommands', () => {
});
it('when the SignInToAzure command is dispatched', async () => {
let arg: AzureAuthAction<AzureAuthWorkflow> = {} as AzureAuthAction<AzureAuthWorkflow>;
store.dispatch = action => (arg as any) = action;
// eslint-disable-next-line prefer-const
let arg: AzureAuthAction<AzureAuthWorkflow> = {} as AzureAuthAction<
AzureAuthWorkflow
>;
store.dispatch = action => ((arg as any) = action);
registry.getCommand(Commands.SignInToAzure).handler();
expect(arg.payload.loginSuccessDialog).toBe(AzureLoginSuccessDialogContainer);
expect(arg.payload.loginSuccessDialog).toBe(
AzureLoginSuccessDialogContainer
);
expect(arg.payload.promptDialog).toBe(AzureLoginPromptDialogContainer);
});
it('when the InvalidateArmToken command is dispatched', async () => {
// eslint-disable-next-line prefer-const
let arg: AzureAuthAction<void> = {} as AzureAuthAction<void>;
store.dispatch = action => (arg as any) = action;
store.dispatch = action => ((arg as any) = action);
registry.getCommand(Commands.InvalidateAzureArmToken).handler();
expect(arg).toEqual(invalidateArmToken());
});
});
it('should set the proper href on the theme tag when the SwitchTheme command is dispatched', () => {
let link = document.createElement('link');
const link = document.createElement('link');
link.id = 'themeVars';
document.querySelector('head').appendChild(link);
registry.getCommand(Commands.SwitchTheme).handler('light', './light.css');

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -34,11 +34,19 @@
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistry } from '@bfemulator/sdk-shared';
import { ServiceTypes } from 'botframework-config/lib/schema';
import * as Constants from '../constants';
import { azureArmTokenDataChanged, beginAzureAuthWorkflow, invalidateArmToken } from '../data/action/azureAuthActions';
import {
azureArmTokenDataChanged,
beginAzureAuthWorkflow,
invalidateArmToken,
} from '../data/action/azureAuthActions';
import * as EditorActions from '../data/action/editorActions';
import * as NavBarActions from '../data/action/navBarActions';
import { ProgressIndicatorPayload, updateProgressIndicator } from '../data/action/progressIndicatorActions';
import {
ProgressIndicatorPayload,
updateProgressIndicator,
} from '../data/action/progressIndicatorActions';
import { switchTheme } from '../data/action/themeActions';
import { showWelcomePage } from '../data/editorHelpers';
import { AzureAuthState } from '../data/reducer/azureAuthReducer';
@ -54,7 +62,7 @@ import {
ProgressIndicatorContainer,
SecretPromptDialogContainer,
UpdateAvailableDialogContainer,
UpdateUnavailableDialogContainer
UpdateUnavailableDialogContainer,
} from '../ui/dialogs';
/** Register UI commands (toggling UI) */
@ -87,47 +95,72 @@ export function registerCommands(commandRegistry: CommandRegistry) {
// ---------------------------------------------------------------------------
// Switches navbar tab selection
commandRegistry.registerCommand(UI.SwitchNavBarTab, (tabName: string): void => {
store.dispatch(NavBarActions.select(tabName));
});
commandRegistry.registerCommand(
UI.SwitchNavBarTab,
(tabName: string): void => {
store.dispatch(NavBarActions.select(tabName));
}
);
// ---------------------------------------------------------------------------
// Open App Settings
commandRegistry.registerCommand(UI.ShowAppSettings, (): void => {
const { CONTENT_TYPE_APP_SETTINGS, DOCUMENT_ID_APP_SETTINGS } = Constants;
store.dispatch(EditorActions.open({
contentType: CONTENT_TYPE_APP_SETTINGS,
documentId: DOCUMENT_ID_APP_SETTINGS,
isGlobal: true,
meta: null
}));
});
commandRegistry.registerCommand(
UI.ShowAppSettings,
(): void => {
const { CONTENT_TYPE_APP_SETTINGS, DOCUMENT_ID_APP_SETTINGS } = Constants;
store.dispatch(
EditorActions.open({
contentType: CONTENT_TYPE_APP_SETTINGS,
documentId: DOCUMENT_ID_APP_SETTINGS,
isGlobal: true,
meta: null,
})
);
}
);
// ---------------------------------------------------------------------------
// Theme switching from main
commandRegistry.registerCommand(UI.SwitchTheme, (themeName: string, themeHref: string) => {
const linkTags = document.querySelectorAll<HTMLLinkElement>('[data-theme-component="true"]');
const themeTag = document.getElementById('themeVars') as HTMLLinkElement;
if (themeTag) {
themeTag.href = themeHref;
commandRegistry.registerCommand(
UI.SwitchTheme,
(themeName: string, themeHref: string) => {
const linkTags = document.querySelectorAll<HTMLLinkElement>(
'[data-theme-component="true"]'
);
const themeTag = document.getElementById('themeVars') as HTMLLinkElement;
if (themeTag) {
themeTag.href = themeHref;
}
const themeComponents = Array.prototype.map.call(
linkTags,
link => link.href
); // href is fully qualified
store.dispatch(switchTheme(themeName, themeComponents));
}
const themeComponents = Array.prototype.map.call(linkTags, link => link.href); // href is fully qualified
store.dispatch(switchTheme(themeName, themeComponents));
});
);
// ---------------------------------------------------------------------------
// Azure sign in
commandRegistry.registerCommand(UI.SignInToAzure, (serviceType: ServiceTypes) => {
store.dispatch(beginAzureAuthWorkflow(
AzureLoginPromptDialogContainer,
{ serviceType },
AzureLoginSuccessDialogContainer,
AzureLoginFailedDialogContainer));
});
commandRegistry.registerCommand(
UI.SignInToAzure,
(serviceType: ServiceTypes) => {
store.dispatch(
beginAzureAuthWorkflow(
AzureLoginPromptDialogContainer,
{ serviceType },
AzureLoginSuccessDialogContainer,
AzureLoginFailedDialogContainer
)
);
}
);
commandRegistry.registerCommand(UI.ArmTokenReceivedOnStartup, (azureAuth: AzureAuthState) => {
store.dispatch(azureArmTokenDataChanged(azureAuth.access_token));
});
commandRegistry.registerCommand(
UI.ArmTokenReceivedOnStartup,
(azureAuth: AzureAuthState) => {
store.dispatch(azureArmTokenDataChanged(azureAuth.access_token));
}
);
commandRegistry.registerCommand(UI.InvalidateAzureArmToken, () => {
store.dispatch(invalidateArmToken());
@ -141,25 +174,44 @@ export function registerCommands(commandRegistry: CommandRegistry) {
// ---------------------------------------------------------------------------
// Shows the progress indicator component
commandRegistry.registerCommand(UI.ShowProgressIndicator, async (props?: ProgressIndicatorPayload) => {
return await DialogService.showDialog(ProgressIndicatorContainer, props).catch(e => console.error(e));
});
commandRegistry.registerCommand(
UI.ShowProgressIndicator,
async (props?: ProgressIndicatorPayload) => {
return await DialogService.showDialog(
ProgressIndicatorContainer,
props
// eslint-disable-next-line no-console
).catch(e => console.error(e));
}
);
// ---------------------------------------------------------------------------
// Updates the progress of the progress indicator component
commandRegistry.registerCommand(UI.UpdateProgressIndicator, (value: ProgressIndicatorPayload) => {
store.dispatch(updateProgressIndicator(value));
});
commandRegistry.registerCommand(
UI.UpdateProgressIndicator,
(value: ProgressIndicatorPayload) => {
store.dispatch(updateProgressIndicator(value));
}
);
// ---------------------------------------------------------------------------
// Shows the dialog telling the user that an update is available
commandRegistry.registerCommand(UI.ShowUpdateAvailableDialog, async (version: string = '') => {
return await DialogService.showDialog(UpdateAvailableDialogContainer, { version }).catch(e => console.error(e));
});
commandRegistry.registerCommand(
UI.ShowUpdateAvailableDialog,
async (version: string = '') => {
return await DialogService.showDialog(UpdateAvailableDialogContainer, {
version,
// eslint-disable-next-line no-console
}).catch(e => console.error(e));
}
);
// ---------------------------------------------------------------------------
// Shows the dialog telling the user that an update is unavailable
commandRegistry.registerCommand(UI.ShowUpdateUnavailableDialog, async () => {
return await DialogService.showDialog(UpdateUnavailableDialogContainer).catch(e => console.error(e));
return await DialogService.showDialog(
UpdateUnavailableDialogContainer
// eslint-disable-next-line no-console
).catch(e => console.error(e));
});
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -33,10 +33,14 @@
import { SharedConstants } from '@bfemulator/app-shared';
export const CONTENT_TYPE_APP_SETTINGS = 'application/vnd.microsoft.bfemulator.document.appsettings';
export const CONTENT_TYPE_WELCOME_PAGE = 'application/vnd.microsoft.bfemulator.document.welcome';
export const CONTENT_TYPE_TRANSCRIPT = 'application/vnd.microsoft.bfemulator.document.transcript';
export const CONTENT_TYPE_LIVE_CHAT = SharedConstants.ContentTypes.CONTENT_TYPE_LIVE_CHAT;
export const CONTENT_TYPE_APP_SETTINGS =
'application/vnd.microsoft.bfemulator.document.appsettings';
export const CONTENT_TYPE_WELCOME_PAGE =
'application/vnd.microsoft.bfemulator.document.welcome';
export const CONTENT_TYPE_TRANSCRIPT =
'application/vnd.microsoft.bfemulator.document.transcript';
export const CONTENT_TYPE_LIVE_CHAT =
SharedConstants.ContentTypes.CONTENT_TYPE_LIVE_CHAT;
export const NAVBAR_BOT_EXPLORER = 'navbar.botExplorer';
export const NAVBAR_SETTINGS = 'navbar.settings';
@ -46,10 +50,7 @@ export const NAVBAR_RESOURCES = 'navbar:resources';
export const EDITOR_KEY_PRIMARY = 'primary';
export const EDITOR_KEY_SECONDARY = 'secondary';
export const EditorKeys = [
EDITOR_KEY_PRIMARY,
EDITOR_KEY_SECONDARY
];
export const EditorKeys = [EDITOR_KEY_PRIMARY, EDITOR_KEY_SECONDARY];
export const DOCUMENT_ID_APP_SETTINGS = 'app:settings';
export const DOCUMENT_ID_BOT_SETTINGS = 'bot:settings';

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,8 +31,8 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { Action } from 'redux';
import { ComponentClass } from 'react';
import { Action } from 'redux';
export const AZURE_ARM_TOKEN_DATA_CHANGED = 'AZURE_ARM_TOKEN_DATA_CHANGED';
export const AZURE_BEGIN_AUTH_WORKFLOW = 'AZURE_BEGIN_AUTH_WORKFLOW';
@ -61,19 +61,27 @@ export function beginAzureAuthWorkflow(
): AzureAuthAction<AzureAuthWorkflow> {
return {
type: AZURE_BEGIN_AUTH_WORKFLOW,
payload: { promptDialog, promptDialogProps, loginSuccessDialog, loginFailedDialog }
payload: {
promptDialog,
promptDialogProps,
loginSuccessDialog,
loginFailedDialog,
},
};
}
export function azureArmTokenDataChanged(armToken: string): AzureAuthAction<ArmTokenData> {
export function azureArmTokenDataChanged(
armToken: string
): AzureAuthAction<ArmTokenData> {
return {
type: AZURE_ARM_TOKEN_DATA_CHANGED,
payload: { access_token: armToken }
// eslint-disable-next-line typescript/camelcase
payload: { access_token: armToken },
};
}
export function invalidateArmToken(): AzureAuthAction<void> {
return {
type: AZURE_INVALIDATE_ARM_TOKEN
type: AZURE_INVALIDATE_ARM_TOKEN,
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -39,20 +39,20 @@ export enum BotActions {
setActive = 'BOT/SET_ACTIVE',
close = 'BOT/CLOSE',
browse = 'BOT/BROWSE',
hashGenerated = 'BOT/HASH_GENERATED'
hashGenerated = 'BOT/HASH_GENERATED',
}
export interface LoadBotAction {
type: BotActions.load;
payload: {
bots: BotInfo[]
bots: BotInfo[];
};
}
export interface SetActiveBotAction {
type: BotActions.setActive;
payload: {
bot: BotConfigWithPath
bot: BotConfigWithPath;
};
}
@ -72,11 +72,11 @@ export interface BotHashAction {
}
export type BotAction =
LoadBotAction |
SetActiveBotAction |
CloseBotAction |
BrowseBotAction |
BotHashAction;
| LoadBotAction
| SetActiveBotAction
| CloseBotAction
| BrowseBotAction
| BotHashAction;
export function load(bots: BotInfo[]): LoadBotAction {
// prune bad bots
@ -85,8 +85,8 @@ export function load(bots: BotInfo[]): LoadBotAction {
return {
type: BotActions.load,
payload: {
bots
}
bots,
},
};
}
@ -98,28 +98,28 @@ export function setActive(bot: BotConfigWithPath): SetActiveBotAction {
return {
type: BotActions.setActive,
payload: {
bot
}
bot,
},
};
}
export function close(): CloseBotAction {
return {
type: BotActions.close,
payload: {}
payload: {},
};
}
export function browse(): BrowseBotAction {
return {
type: BotActions.browse,
payload: {}
payload: {},
};
}
export function botHashGenerated(hash: string): BotHashAction {
return {
type: BotActions.hashGenerated,
payload: { hash }
payload: { hash },
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -46,7 +46,7 @@ export enum ChatActions {
addTranscript = 'CHAT/TRANSCRIPT/ADD',
clearTranscripts = 'CHAT/TRANSCRIPT/CLEAR',
removeTranscript = 'CHAT/TRANSCRIPT/REMOVE',
updateChat = 'CHAT/DOCUMENT/UPDATE'
updateChat = 'CHAT/DOCUMENT/UPDATE',
}
export interface ActiveInspectorChangedPayload {
@ -56,9 +56,9 @@ export interface ActiveInspectorChangedPayload {
export interface NewChatAction {
type: ChatActions.newChat;
payload: {
[propName: string]: any,
documentId: string,
mode: ChatMode
[propName: string]: any;
documentId: string;
mode: ChatMode;
};
}
@ -85,8 +85,7 @@ export interface SetInspectorObjectsPayload {
objs: any;
}
export interface AddTranscriptPayload extends RemoveTranscriptPayload {
}
export interface AddTranscriptPayload extends RemoveTranscriptPayload {}
export interface RemoveTranscriptPayload {
filename: string;
@ -103,39 +102,49 @@ export interface ChatAction<T = any> extends Action {
type ChatMode = 'livechat' | 'transcript';
export function inspectorChanged(inspectorWebView: HTMLWebViewElement): ChatAction<ActiveInspectorChangedPayload> {
export function inspectorChanged(
inspectorWebView: HTMLWebViewElement
): ChatAction<ActiveInspectorChangedPayload> {
return {
type: ChatActions.activeInspectorChanged,
payload: { inspectorWebView }
payload: { inspectorWebView },
};
}
export function addTranscript(filename: string): ChatAction<AddTranscriptPayload> {
export function addTranscript(
filename: string
): ChatAction<AddTranscriptPayload> {
return {
type: ChatActions.addTranscript,
payload: {
filename
}
filename,
},
};
}
export function clearTranscripts(): ChatAction<{}> {
return {
type: ChatActions.clearTranscripts,
payload: {}
payload: {},
};
}
export function removeTranscript(filename: string): ChatAction<RemoveTranscriptPayload> {
export function removeTranscript(
filename: string
): ChatAction<RemoveTranscriptPayload> {
return {
type: ChatActions.removeTranscript,
payload: {
filename
}
filename,
},
};
}
export function newDocument(documentId: string, mode: ChatMode, additionalData?: object): NewChatAction {
export function newDocument(
documentId: string,
mode: ChatMode,
additionalData?: object
): NewChatAction {
return {
type: ChatActions.newChat,
payload: {
@ -144,62 +153,70 @@ export function newDocument(documentId: string, mode: ChatMode, additionalData?:
conversationId: null,
directLine: null,
log: {
entries: []
entries: [],
},
inspectorObjects: [],
ui: {
horizontalSplitter: [
{
absolute: null,
percentage: 50
percentage: 50,
},
{
absolute: null,
percentage: 50
}
percentage: 50,
},
],
verticalSplitter: [
{
absolute: null,
percentage: 50
percentage: 50,
},
{
absolute: null,
percentage: 50
}
percentage: 50,
},
],
},
...additionalData
}
...additionalData,
},
};
}
export function closeDocument(documentId: string): ChatAction<CloseChatPayload> {
export function closeDocument(
documentId: string
): ChatAction<CloseChatPayload> {
return {
type: ChatActions.closeChat,
payload: {
documentId,
}
},
};
}
export function newConversation(documentId: string, options: any): ChatAction<NewConversationPayload> {
export function newConversation(
documentId: string,
options: any
): ChatAction<NewConversationPayload> {
return {
type: ChatActions.newConversation,
payload: {
documentId,
options
}
options,
},
};
}
export function appendToLog(documentId: string, entry: LogEntry): ChatAction<AppendLogPayload> {
export function appendToLog(
documentId: string,
entry: LogEntry
): ChatAction<AppendLogPayload> {
return {
type: ChatActions.appendLog,
payload: {
documentId,
entry
}
entry,
},
};
}
@ -208,27 +225,33 @@ export function clearLog(documentId: string): ChatAction<ClearLogPayload> {
type: ChatActions.clearLog,
payload: {
documentId,
}
},
};
}
export function setInspectorObjects(documentId: string, objs: any): ChatAction<SetInspectorObjectsPayload> {
export function setInspectorObjects(
documentId: string,
objs: any
): ChatAction<SetInspectorObjectsPayload> {
objs = Array.isArray(objs) ? objs : [objs];
return {
type: ChatActions.setInspectorObjects,
payload: {
documentId,
objs
}
objs,
},
};
}
export function updateChat(documentId: string, updatedValues: any): ChatAction<UpdateChatPayload> {
export function updateChat(
documentId: string,
updatedValues: any
): ChatAction<UpdateChatPayload> {
return {
type: ChatActions.updateChat,
payload: {
documentId,
updatedValues
}
updatedValues,
},
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,5 +1,37 @@
import { Action } from 'redux';
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { ClientAwareSettings } from '@bfemulator/app-shared';
import { Action } from 'redux';
export const CLIENT_AWARE_SETTINGS_CHANGED = 'CLIENT_AWARE_SETTINGS_CHANGED';
@ -10,9 +42,11 @@ export interface ClientAwareSettingsActions extends Action {
payload: ClientAwareSettings;
}
export function clientAwareSettingsChanged(settings: ClientAwareSettings): ClientAwareSettingsActions {
export function clientAwareSettingsChanged(
settings: ClientAwareSettings
): ClientAwareSettingsActions {
return {
type: CLIENT_AWARE_SETTINGS_CHANGED,
payload: settings
payload: settings,
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,17 +31,26 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { IConnectedService, ServiceTypes } from 'botframework-config/lib/schema';
import {
IConnectedService,
ServiceTypes,
} from 'botframework-config/lib/schema';
import { ComponentClass } from 'react';
import { Action } from 'redux';
import { CONNECTED_SERVICES_PANEL_ID } from './explorerActions';
export const OPEN_SERVICE_DEEP_LINK = 'OPEN_SERVICE_DEEP_LINK';
export const OPEN_CONTEXT_MENU_FOR_CONNECTED_SERVICE = 'OPEN_CONTEXT_MENU_FOR_CONNECTED_SERVICE';
export const OPEN_ADD_CONNECTED_SERVICE_CONTEXT_MENU = 'OPEN_ADD_CONNECTED_SERVICE_CONTEXT_MENU';
export const OPEN_CONNECTED_SERVICE_SORT_CONTEXT_MENU = 'OPEN_CONNECTED_SERVICE_SORT_CONTEXT_MENU';
export const LAUNCH_CONNECTED_SERVICE_EDITOR = 'LAUNCH_CONNECTED_SERVICE_EDITOR';
export const LAUNCH_CONNECTED_SERVICE_PICKER = 'LAUNCH_CONNECTED_SERVICE_PICKER';
export const OPEN_CONTEXT_MENU_FOR_CONNECTED_SERVICE =
'OPEN_CONTEXT_MENU_FOR_CONNECTED_SERVICE';
export const OPEN_ADD_CONNECTED_SERVICE_CONTEXT_MENU =
'OPEN_ADD_CONNECTED_SERVICE_CONTEXT_MENU';
export const OPEN_CONNECTED_SERVICE_SORT_CONTEXT_MENU =
'OPEN_CONNECTED_SERVICE_SORT_CONTEXT_MENU';
export const LAUNCH_CONNECTED_SERVICE_EDITOR =
'LAUNCH_CONNECTED_SERVICE_EDITOR';
export const LAUNCH_CONNECTED_SERVICE_PICKER =
'LAUNCH_CONNECTED_SERVICE_PICKER';
export interface ConnectedServiceAction<T> extends Action {
payload: T;
@ -57,18 +66,19 @@ export interface ConnectedServicePayload {
export function launchConnectedServiceEditor<T>(
editorComponent: ComponentClass<T>,
connectedService?: IConnectedService): ConnectedServiceAction<ConnectedServicePayload> {
connectedService?: IConnectedService
): ConnectedServiceAction<ConnectedServicePayload> {
return {
type: LAUNCH_CONNECTED_SERVICE_EDITOR,
payload: { editorComponent, connectedService }
payload: { editorComponent, connectedService },
};
}
export interface ConnectedServicePickerPayload extends ConnectedServicePayload {
azureAuthWorkflowComponents: {
promptDialog: ComponentClass<any>,
loginSuccessDialog: ComponentClass<any>,
loginFailedDialog: ComponentClass<any>
promptDialog: ComponentClass<any>;
loginSuccessDialog: ComponentClass<any>;
loginFailedDialog: ComponentClass<any>;
};
pickerComponent: ComponentClass<any>;
getStartedDialog: ComponentClass<any>;
@ -76,42 +86,48 @@ export interface ConnectedServicePickerPayload extends ConnectedServicePayload {
progressIndicatorComponent?: ComponentClass<any>;
}
export function launchConnectedServicePicker(payload: ConnectedServicePickerPayload)
: ConnectedServiceAction<ConnectedServicePickerPayload> {
export function launchConnectedServicePicker(
payload: ConnectedServicePickerPayload
): ConnectedServiceAction<ConnectedServicePickerPayload> {
return {
type: LAUNCH_CONNECTED_SERVICE_PICKER,
payload
payload,
};
}
export function openServiceDeepLink(connectedService: IConnectedService)
: ConnectedServiceAction<ConnectedServicePayload> {
export function openServiceDeepLink(
connectedService: IConnectedService
): ConnectedServiceAction<ConnectedServicePayload> {
return {
type: OPEN_SERVICE_DEEP_LINK,
payload: { connectedService }
payload: { connectedService },
};
}
export function openContextMenuForConnectedService<T>(
editorComponent: ComponentClass<T>,
connectedService?: IConnectedService): ConnectedServiceAction<ConnectedServicePayload> {
connectedService?: IConnectedService
): ConnectedServiceAction<ConnectedServicePayload> {
return {
type: OPEN_CONTEXT_MENU_FOR_CONNECTED_SERVICE,
payload: { editorComponent, connectedService }
payload: { editorComponent, connectedService },
};
}
export function openAddServiceContextMenu(payload: ConnectedServicePickerPayload)
: ConnectedServiceAction<ConnectedServicePickerPayload> {
export function openAddServiceContextMenu(
payload: ConnectedServicePickerPayload
): ConnectedServiceAction<ConnectedServicePickerPayload> {
return {
type: OPEN_ADD_CONNECTED_SERVICE_CONTEXT_MENU,
payload
payload,
};
}
export function openSortContextMenu(): ConnectedServiceAction<ConnectedServicePayload> {
export function openSortContextMenu(): ConnectedServiceAction<
ConnectedServicePayload
> {
return {
type: OPEN_CONNECTED_SERVICE_SORT_CONTEXT_MENU,
payload: { panelId: CONNECTED_SERVICES_PANEL_ID }
payload: { panelId: CONNECTED_SERVICES_PANEL_ID },
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -32,13 +32,13 @@
//
export enum DialogActions {
setShowing = 'DIALOG/SET_SHOWING'
setShowing = 'DIALOG/SET_SHOWING',
}
export interface SetShowingDialogAction {
type: DialogActions.setShowing;
payload: {
showing: boolean
showing: boolean;
};
}
@ -48,7 +48,7 @@ export function setShowing(showing: boolean = false): SetShowingDialogAction {
return {
type: DialogActions.setShowing,
payload: {
showing
}
showing,
},
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -35,7 +35,8 @@ import { IDispatchService } from 'botframework-config/lib/schema';
import { Action } from 'redux';
export const OPEN_DISPATCH_DEEP_LINK = 'OPEN_DISPATCH_DEEP_LINK';
export const OPEN_DISPATCH_EXPLORER_CONTEXT_MENU = 'OPEN_DISPATCH_EXPLORER_CONTEXT_MENU';
export const OPEN_DISPATCH_EXPLORER_CONTEXT_MENU =
'OPEN_DISPATCH_EXPLORER_CONTEXT_MENU';
export interface DispatchServiceAction<T> extends Action {
payload: T;
@ -45,17 +46,20 @@ export interface DispatchServicePayload {
dispatchService?: IDispatchService;
}
export function openDispatchDeepLink(dispatchService: IDispatchService): DispatchServiceAction<DispatchServicePayload> {
export function openDispatchDeepLink(
dispatchService: IDispatchService
): DispatchServiceAction<DispatchServicePayload> {
return {
type: OPEN_DISPATCH_DEEP_LINK,
payload: { dispatchService }
};
}
export function openDispatchExplorerContextMenu(dispatchService: IDispatchService)
: DispatchServiceAction<DispatchServicePayload> {
return {
type: OPEN_DISPATCH_EXPLORER_CONTEXT_MENU,
payload: { dispatchService }
payload: { dispatchService },
};
}
export function openDispatchExplorerContextMenu(
dispatchService: IDispatchService
): DispatchServiceAction<DispatchServicePayload> {
return {
type: OPEN_DISPATCH_EXPLORER_CONTEXT_MENU,
payload: { dispatchService },
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -47,38 +47,38 @@ export enum EditorActions {
splitTab = 'EDITOR/SPLIT_TAB',
swapTabs = 'EDITOR/SWAP_TABS',
toggleDraggingTab = 'EDITOR/TOGGLE_DRAGGING_TAB',
updateDocument = 'EDITOR/UPDATE_DOCUMENT'
updateDocument = 'EDITOR/UPDATE_DOCUMENT',
}
export interface AppendTabAction {
type: EditorActions.appendTab;
payload: {
srcEditorKey: string,
destEditorKey: string,
documentId: string
srcEditorKey: string;
destEditorKey: string;
documentId: string;
};
}
export interface CloseEditorAction {
type: EditorActions.close;
payload: {
editorKey: string,
documentId: string
editorKey: string;
documentId: string;
};
}
export interface CloseAllEditorAction {
type: EditorActions.closeAll;
payload: {
includeGlobal: boolean
includeGlobal: boolean;
};
}
export interface SetDirtyFlagAction {
type: EditorActions.setDirtyFlag;
payload: {
documentId: string
dirty: boolean
documentId: string;
dirty: boolean;
};
}
@ -95,41 +95,41 @@ export interface UpdateDocumentAction {
export interface SetActiveTabAction {
type: EditorActions.setActiveTab;
payload: {
documentId: string
documentId: string;
};
}
export interface SetActiveEditorAction {
type: EditorActions.setActiveEditor;
payload: {
editorKey: string
editorKey: string;
};
}
export interface SplitTabAction {
type: EditorActions.splitTab;
payload: {
contentType: string,
documentId: string,
srcEditorKey: string,
destEditorKey: string
contentType: string;
documentId: string;
srcEditorKey: string;
destEditorKey: string;
};
}
export interface SwapTabsAction {
type: EditorActions.swapTabs;
payload: {
srcEditorKey: string,
destEditorKey: string,
srcTabId: string,
destTabId: string
srcEditorKey: string;
destEditorKey: string;
srcTabId: string;
destTabId: string;
};
}
export interface ToggleDraggingTabAction {
type: EditorActions.toggleDraggingTab;
payload: {
draggingTab: boolean
draggingTab: boolean;
};
}
@ -150,62 +150,73 @@ export interface RemoveDocPendingChangeAction {
export interface DropTabOnLeftOverlayAction {
type: EditorActions.dropTabOnLeftOverlay;
payload: {
tabId: string
tabId: string;
};
}
export type EditorAction =
AppendTabAction |
CloseEditorAction |
CloseAllEditorAction |
SetDirtyFlagAction |
OpenEditorAction |
UpdateDocumentAction |
SetActiveTabAction |
SetActiveEditorAction |
SplitTabAction |
SwapTabsAction |
ToggleDraggingTabAction |
AddDocPendingChangeAction |
RemoveDocPendingChangeAction |
DropTabOnLeftOverlayAction;
| AppendTabAction
| CloseEditorAction
| CloseAllEditorAction
| SetDirtyFlagAction
| OpenEditorAction
| UpdateDocumentAction
| SetActiveTabAction
| SetActiveEditorAction
| SplitTabAction
| SwapTabsAction
| ToggleDraggingTabAction
| AddDocPendingChangeAction
| RemoveDocPendingChangeAction
| DropTabOnLeftOverlayAction;
export function appendTab(srcEditorKey: string, destEditorKey: string, documentId: string): AppendTabAction {
export function appendTab(
srcEditorKey: string,
destEditorKey: string,
documentId: string
): AppendTabAction {
return {
type: EditorActions.appendTab,
payload: {
srcEditorKey,
destEditorKey,
documentId
}
documentId,
},
};
}
export function addDocPendingChange(documentId: string): AddDocPendingChangeAction {
export function addDocPendingChange(
documentId: string
): AddDocPendingChangeAction {
return {
type: EditorActions.addDocPendingChange,
payload: {
documentId
}
documentId,
},
};
}
export function removeDocPendingChange(documentId: string): RemoveDocPendingChangeAction {
export function removeDocPendingChange(
documentId: string
): RemoveDocPendingChangeAction {
return {
type: EditorActions.removeDocPendingChange,
payload: {
documentId
}
documentId,
},
};
}
export function close(editorKey: string, documentId: string): CloseEditorAction {
export function close(
editorKey: string,
documentId: string
): CloseEditorAction {
return {
type: EditorActions.close,
payload: {
editorKey,
documentId
}
documentId,
},
};
}
@ -213,32 +224,38 @@ export function closeNonGlobalTabs(): CloseAllEditorAction {
return {
type: EditorActions.closeAll,
payload: {
includeGlobal: false
}
includeGlobal: false,
},
};
}
export function setDirtyFlag(documentId: string, dirty: boolean): SetDirtyFlagAction {
export function setDirtyFlag(
documentId: string,
dirty: boolean
): SetDirtyFlagAction {
return {
type: EditorActions.setDirtyFlag,
payload: {
documentId,
dirty
}
dirty,
},
};
}
export function open(document: Document): OpenEditorAction {
return {
type: EditorActions.open,
payload: document
payload: document,
};
}
export function updateDocument(documentId: string, updatedDocument: Partial<Document>): UpdateDocumentAction {
export function updateDocument(
documentId: string,
updatedDocument: Partial<Document>
): UpdateDocumentAction {
return {
type: EditorActions.updateDocument,
payload: { documentId, ...updatedDocument }
payload: { documentId, ...updatedDocument },
};
}
@ -246,8 +263,8 @@ export function setActiveTab(documentId: string): SetActiveTabAction {
return {
type: EditorActions.setActiveTab,
payload: {
documentId
}
documentId,
},
};
}
@ -255,51 +272,63 @@ export function setActiveEditor(editorKey: string): SetActiveEditorAction {
return {
type: EditorActions.setActiveEditor,
payload: {
editorKey
}
editorKey,
},
};
}
export function splitTab(contentType: string, documentId: string, srcEditorKey: string, destEditorKey: string)
: SplitTabAction {
export function splitTab(
contentType: string,
documentId: string,
srcEditorKey: string,
destEditorKey: string
): SplitTabAction {
return {
type: EditorActions.splitTab,
payload: {
contentType,
documentId,
srcEditorKey,
destEditorKey
}
destEditorKey,
},
};
}
export function swapTabs(srcEditorKey: string, destEditorKey: string, srcTabId: string, destTabId: string)
: SwapTabsAction {
export function swapTabs(
srcEditorKey: string,
destEditorKey: string,
srcTabId: string,
destTabId: string
): SwapTabsAction {
return {
type: EditorActions.swapTabs,
payload: {
srcEditorKey,
destEditorKey,
srcTabId,
destTabId
}
destTabId,
},
};
}
export function toggleDraggingTab(draggingTab: boolean): ToggleDraggingTabAction {
export function toggleDraggingTab(
draggingTab: boolean
): ToggleDraggingTabAction {
return {
type: EditorActions.toggleDraggingTab,
payload: {
draggingTab
}
draggingTab,
},
};
}
export function dropTabOnLeftOverlay(tabId: string): DropTabOnLeftOverlayAction {
export function dropTabOnLeftOverlay(
tabId: string
): DropTabOnLeftOverlayAction {
return {
type: EditorActions.dropTabOnLeftOverlay,
payload: {
tabId
}
tabId,
},
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -34,7 +34,8 @@
import { IEndpointService } from 'botframework-config/lib/schema';
import { Action } from 'redux';
export const OPEN_ENDPOINT_EXPLORER_CONTEXT_MENU = 'OPEN_ENDPOINT_EXPLORER_CONTEXT_MENU';
export const OPEN_ENDPOINT_EXPLORER_CONTEXT_MENU =
'OPEN_ENDPOINT_EXPLORER_CONTEXT_MENU';
export interface EndpointServiceAction<T> extends Action {
payload: T;
@ -44,10 +45,11 @@ export interface EndpointServicePayload {
endpointService?: IEndpointService;
}
export function openEndpointExplorerContextMenu(endpointService: IEndpointService)
: EndpointServiceAction<EndpointServicePayload> {
export function openEndpointExplorerContextMenu(
endpointService: IEndpointService
): EndpointServiceAction<EndpointServicePayload> {
return {
type: OPEN_ENDPOINT_EXPLORER_CONTEXT_MENU,
payload: { endpointService }
payload: { endpointService },
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -52,27 +52,32 @@ export interface EndpointEditorPayload extends EndpointServicePayload {
endpointEditorComponent?: ComponentClass<any>;
}
export function launchEndpointEditor(endpointEditorComponent: ComponentClass<any>,
endpointService?: IEndpointService): EndpointServiceAction<EndpointEditorPayload> {
export function launchEndpointEditor(
endpointEditorComponent: ComponentClass<any>,
endpointService?: IEndpointService
): EndpointServiceAction<EndpointEditorPayload> {
return {
type: LAUNCH_ENDPOINT_EDITOR,
payload: { endpointEditorComponent, endpointService }
payload: { endpointEditorComponent, endpointService },
};
}
export function openEndpointInEmulator(endpointService: IEndpointService, focusExistingChatIfAvailable: boolean = false)
: EndpointServiceAction<EndpointServicePayload> {
export function openEndpointInEmulator(
endpointService: IEndpointService,
focusExistingChatIfAvailable: boolean = false
): EndpointServiceAction<EndpointServicePayload> {
return {
type: OPEN_ENDPOINT_IN_EMULATOR,
payload: { endpointService, focusExistingChatIfAvailable }
payload: { endpointService, focusExistingChatIfAvailable },
};
}
export function openEndpointExplorerContextMenu(endpointEditorComponent: ComponentClass<any>,
endpointService?: IEndpointService)
: EndpointServiceAction<EndpointEditorPayload> {
export function openEndpointExplorerContextMenu(
endpointEditorComponent: ComponentClass<any>,
endpointService?: IEndpointService
): EndpointServiceAction<EndpointEditorPayload> {
return {
type: OPEN_ENDPOINT_CONTEXT_MENU,
payload: { endpointEditorComponent, endpointService }
payload: { endpointEditorComponent, endpointService },
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,12 +31,13 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { Action } from 'redux';
import { SortCriteria } from '../reducer/explorer';
export const CONNECTED_SERVICES_PANEL_ID = 'connectedServices';
export enum ExplorerActions {
Show = 'EXPLORER/SHOW',
Sort = 'EXPLORER/SORT'
Sort = 'EXPLORER/SORT',
}
export interface ExplorerAction<T> extends Action {
@ -52,13 +53,16 @@ export interface ExplorerPayload {
export function showExplorer(show: boolean): ExplorerAction<ExplorerPayload> {
return {
type: ExplorerActions.Show,
payload: { show }
payload: { show },
};
}
export function sortExplorerContents(panelId: string, sort: SortCriteria): ExplorerAction<ExplorerPayload> {
export function sortExplorerContents(
panelId: string,
sort: SortCriteria
): ExplorerAction<ExplorerPayload> {
return {
type: ExplorerActions.Sort,
payload: { sortSelectionByPanelId: { [panelId]: sort } }
payload: { sortSelectionByPanelId: { [panelId]: sort } },
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -37,33 +37,33 @@ export enum FileActions {
setRoot = 'FILE/SET_ROOT',
add = 'FILE/ADD',
remove = 'FILE/REMOVE',
clear = 'FILE/CLEAR'
clear = 'FILE/CLEAR',
}
export function addFile(payload: FileInfo) {
return {
type: FileActions.add,
payload
payload,
};
}
export function clear() {
return {
type: FileActions.clear,
payload: {}
payload: {},
};
}
export function removeFile(path: string) {
return {
type: FileActions.remove,
payload: { path }
payload: { path },
};
}
export function setRoot(path: string) {
return {
type: FileActions.setRoot,
payload: { path }
payload: { path },
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -32,13 +32,13 @@
//
export enum NavBarActions {
select = 'NAVBAR/SELECT'
select = 'NAVBAR/SELECT',
}
export interface SelectNavBarAction {
type: NavBarActions.select;
payload: {
selection: string
selection: string;
};
}
@ -48,7 +48,7 @@ export function select(selection: string): SelectNavBarAction {
return {
type: NavBarActions.select,
payload: {
selection
}
selection,
},
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -40,44 +40,44 @@ export enum NotificationActions {
finishRemove = 'NOTIFICATION/FINISH_REMOVE',
markAllAsRead = 'NOTIFICATION/MARK_ALL_AS_READ',
beginClear = 'NOTIFICATION/BEGIN_CLEAR',
finishClear = 'NOTIFICATION/FINISH_CLEAR'
finishClear = 'NOTIFICATION/FINISH_CLEAR',
}
export type NotificationAction =
BeginAddNotificationAction |
FinishAddNotificationAction |
BeginRemoveNotificationAction |
FinishRemoveNotificationAction |
MarkAllAsReadNotificationAction |
BeginClearNotificationAction |
FinishClearNotificationAction;
| BeginAddNotificationAction
| FinishAddNotificationAction
| BeginRemoveNotificationAction
| FinishRemoveNotificationAction
| MarkAllAsReadNotificationAction
| BeginClearNotificationAction
| FinishClearNotificationAction;
export interface BeginAddNotificationAction {
type: NotificationActions.beginAdd;
payload: {
notification: Notification,
read: boolean
notification: Notification;
read: boolean;
};
}
export interface FinishAddNotificationAction {
type: NotificationActions.finishAdd;
payload: {
notification: Notification
notification: Notification;
};
}
export interface BeginRemoveNotificationAction {
type: NotificationActions.beginRemove;
payload: {
id: string
id: string;
};
}
export interface FinishRemoveNotificationAction {
type: NotificationActions.finishRemove;
payload: {
id: string
id: string;
};
}
@ -96,22 +96,27 @@ export interface FinishClearNotificationAction {
payload: {};
}
export function beginAdd(notification: Notification, read: boolean = false): BeginAddNotificationAction {
export function beginAdd(
notification: Notification,
read: boolean = false
): BeginAddNotificationAction {
return {
type: NotificationActions.beginAdd,
payload: {
notification,
read
}
read,
},
};
}
export function finishAdd(notification: Notification): FinishAddNotificationAction {
export function finishAdd(
notification: Notification
): FinishAddNotificationAction {
return {
type: NotificationActions.finishAdd,
payload: {
notification
}
notification,
},
};
}
@ -119,8 +124,8 @@ export function beginRemove(id: string): BeginRemoveNotificationAction {
return {
type: NotificationActions.beginRemove,
payload: {
id
}
id,
},
};
}
@ -128,28 +133,28 @@ export function finishRemove(id: string): FinishRemoveNotificationAction {
return {
type: NotificationActions.finishRemove,
payload: {
id
}
id,
},
};
}
export function markAllAsRead(): MarkAllAsReadNotificationAction {
return {
type: NotificationActions.markAllAsRead,
payload: {}
payload: {},
};
}
export function beginClear(): BeginClearNotificationAction {
return {
type: NotificationActions.beginClear,
payload: {}
payload: {},
};
}
export function finishClear(): FinishClearNotificationAction {
return {
type: NotificationActions.finishClear,
payload: {}
payload: {},
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -33,7 +33,7 @@
export enum PresentationActions {
disable = 'PRESENTATION/DISABLE',
enable = 'PRESENTATION/ENABLE'
enable = 'PRESENTATION/ENABLE',
}
export interface EnablePresentationAction {
@ -47,19 +47,19 @@ export interface DisablePresentationAction {
}
export type PresentationAction =
EnablePresentationAction |
DisablePresentationAction;
| EnablePresentationAction
| DisablePresentationAction;
export function enable(): EnablePresentationAction {
return {
type: PresentationActions.enable,
payload: {}
payload: {},
};
}
export function disable(): DisablePresentationAction {
return {
type: PresentationActions.disable,
payload: {}
payload: {},
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,3 +1,35 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { Action } from 'redux';
export const UPDATE_PROGRESS_INDICATOR = 'UPDATE_PROGRESS_INDICATOR';
@ -12,17 +44,21 @@ export interface ProgressIndicatorPayload {
progress: number;
}
export function updateProgressIndicator({ label, progress }: ProgressIndicatorPayload)
: ProgressIndicatorAction<ProgressIndicatorPayload> {
export function updateProgressIndicator({
label,
progress,
}: ProgressIndicatorPayload): ProgressIndicatorAction<
ProgressIndicatorPayload
> {
return {
type: UPDATE_PROGRESS_INDICATOR,
payload: { label, progress }
payload: { label, progress },
};
}
export function cancelCurrentProcess(): ProgressIndicatorAction<void> {
return {
type: CANCEL_CURRENT_PROCESS,
payload: void(0)
payload: void 0,
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,6 +1,38 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { IFileService } from 'botframework-config/lib/schema';
import { Action } from 'redux';
import { ComponentClass } from 'react';
import { Action } from 'redux';
export const TRANSCRIPTS_UPDATED = 'TRANSCRIPTS_UPDATED';
export const TRANSCRIPTS_DIRECTORY_UPDATED = 'TRANSCRIPTS_DIRECTORY_UPDATED';
@ -16,67 +48,87 @@ export interface ResourcesAction<T> extends Action {
payload: T;
}
export function transcriptsUpdated(payload: IFileService[]): ResourcesAction<IFileService[]> {
export function transcriptsUpdated(
payload: IFileService[]
): ResourcesAction<IFileService[]> {
return {
type: TRANSCRIPTS_UPDATED,
payload
payload,
};
}
export function transcriptDirectoryUpdated(payload: string): ResourcesAction<string> {
export function transcriptDirectoryUpdated(
payload: string
): ResourcesAction<string> {
return {
type: TRANSCRIPTS_DIRECTORY_UPDATED,
payload
payload,
};
}
export function chatsDirectoryUpdated(payload: string): ResourcesAction<string> {
export function chatsDirectoryUpdated(
payload: string
): ResourcesAction<string> {
return {
type: CHATS_DIRECTORY_UPDATED,
payload
payload,
};
}
export function chatFilesUpdated(payload: IFileService[]): ResourcesAction<IFileService[]> {
export function chatFilesUpdated(
payload: IFileService[]
): ResourcesAction<IFileService[]> {
return {
type: CHAT_FILES_UPDATED,
payload
payload,
};
}
export function openContextMenuForResource(payload: IFileService): ResourcesAction<IFileService> {
export function openContextMenuForResource(
payload: IFileService
): ResourcesAction<IFileService> {
return {
type: OPEN_CONTEXT_MENU_FOR_RESOURCE,
payload
payload,
};
}
export function editResource(payload: IFileService): ResourcesAction<IFileService> {
export function editResource(
payload: IFileService
): ResourcesAction<IFileService> {
return {
type: EDIT_RESOURCE,
payload
payload,
};
}
export function renameResource(payload: IFileService): ResourcesAction<IFileService> {
export function renameResource(
payload: IFileService
): ResourcesAction<IFileService> {
return {
type: RENAME_RESOURCE,
payload
payload,
};
}
export function openResource(payload: IFileService): ResourcesAction<IFileService> {
export function openResource(
payload: IFileService
): ResourcesAction<IFileService> {
return {
type: OPEN_RESOURCE,
payload
payload,
};
}
declare type ResourceSettingsPayload = { dialog: ComponentClass<any> };
declare interface ResourceSettingsPayload {
dialog: ComponentClass<any>;
}
export function openResourcesSettings(payload: ResourceSettingsPayload): ResourcesAction<ResourceSettingsPayload> {
export function openResourcesSettings(
payload: ResourceSettingsPayload
): ResourcesAction<ResourceSettingsPayload> {
return {
type: OPEN_RESOURCE_SETTINGS,
payload
payload,
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,3 +1,35 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
export const SWITCH_THEME = 'switchTheme';
export declare type ThemeType = 'switchTheme';
@ -11,9 +43,12 @@ export interface SwitchThemePayload {
themeComponents: string[];
}
export function switchTheme(themeName: string, themeComponents: string[]): ThemeAction<SwitchThemePayload> {
export function switchTheme(
themeName: string,
themeComponents: string[]
): ThemeAction<SwitchThemePayload> {
return {
type: SWITCH_THEME,
payload: { themeName, themeComponents }
payload: { themeName, themeComponents },
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,3 +1,35 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { BotInfo } from '@bfemulator/app-shared';
import { Action } from 'redux';
@ -7,9 +39,11 @@ export interface WelcomePageAction<T> extends Action {
payload: T;
}
export function openContextMenuForBot(bot: BotInfo): WelcomePageAction<BotInfo> {
export function openContextMenuForBot(
bot: BotInfo
): WelcomePageAction<BotInfo> {
return {
type: OPEN_CONTEXT_MENU_FOR_BOT,
payload: bot
payload: bot,
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,6 +31,12 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import {
getActiveBot,
getBotInfoByPath,
pathExistsInRecentBots,
} from './botHelpers';
jest.mock('./store', () => ({
store: {
getState: () => ({
@ -41,19 +47,15 @@ jest.mock('./store', () => ({
padlock: 'padlock1',
services: [],
},
botFiles: [{
path: 'path1'
}]
}
})
}
botFiles: [
{
path: 'path1',
},
],
},
}),
},
}));
import {
getActiveBot,
getBotInfoByPath,
pathExistsInRecentBots
} from './botHelpers';
describe('Bot helpers tests', () => {
it('should get the active bot', () => {
const activeBot = getActiveBot();

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -33,6 +33,7 @@
import { BotInfo } from '@bfemulator/app-shared';
import { BotConfigWithPath } from '@bfemulator/sdk-shared';
import { store } from './store';
export function getActiveBot(): BotConfigWithPath {
@ -42,7 +43,9 @@ export function getActiveBot(): BotConfigWithPath {
const encoder = new (window as any).TextEncoder();
const decoder = new (window as any).TextDecoder();
export const generateBotHash = async (bot: BotConfigWithPath): Promise<string> => {
export const generateBotHash = async (
bot: BotConfigWithPath
): Promise<string> => {
const buffer = encoder.encode(JSON.stringify(bot));
const digest = await window.crypto.subtle.digest('SHA-256', buffer);
return btoa(encoder.encode(decoder.decode(digest)));

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -35,7 +35,7 @@ import { store } from './store';
export function documentIdForConversation(conversationId: string): string {
const state = store.getState();
for (let key in state.chat.chats) {
for (const key in state.chat.chats) {
if (state.chat.chats[key].conversationId === conversationId) {
return state.chat.chats[key].documentId;
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -39,13 +39,14 @@ class DebugConnection extends EventEmitter {
private onopen: any;
private onclose: any;
constructor(connection: any) {
public constructor(connection: any) {
super();
this._connection = connection;
this._connection.onmessage = event => {
console.info(`WS.recv: ${ event.data }`);
// eslint-disable-next-line no-console
console.info(`WS.recv: ${event.data}`);
this.emit('message', event);
if (this.onmessage) {
this.onmessage(event);
@ -53,6 +54,7 @@ class DebugConnection extends EventEmitter {
};
this._connection.onopen = () => {
// eslint-disable-next-line no-console
console.info(`WS.open`);
this.emit('open');
if (this.onopen) {
@ -61,6 +63,7 @@ class DebugConnection extends EventEmitter {
};
this._connection.onclose = () => {
// eslint-disable-next-line no-console
console.info(`WS.close`);
this.emit('close');
if (this.onclose) {
@ -69,16 +72,17 @@ class DebugConnection extends EventEmitter {
};
}
close() {
public close() {
this._connection.close();
}
end() {
public end() {
this._connection.end();
}
send(data: any) {
console.info(`WS.send: ${ data }`);
public send(data: any) {
// eslint-disable-next-line no-console
console.info(`WS.send: ${data}`);
this._connection.send(data);
}
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -32,14 +32,17 @@
//
import * as Constants from '../constants';
import * as EditorActions from './action/editorActions';
import { Editor } from './reducer/editor';
import { store } from './store';
export function hasNonGlobalTabs(tabGroups?: { [editorKey: string]: Editor }): number {
export function hasNonGlobalTabs(tabGroups?: {
[editorKey: string]: Editor;
}): number {
tabGroups = tabGroups || store.getState().editor.editors;
let count = 0;
for (let key in tabGroups) {
for (const key in tabGroups) {
if (tabGroups[key]) {
count += Object.keys(tabGroups[key].documents)
.map(documentId => tabGroups[key].documents[documentId])
@ -52,9 +55,12 @@ export function hasNonGlobalTabs(tabGroups?: { [editorKey: string]: Editor }): n
/**
* Returns name of editor group, or undefined if doc is not open
*/
export function getTabGroupForDocument(documentId: string, tabGroups?: { [editorKey: string]: Editor }): string {
export function getTabGroupForDocument(
documentId: string,
tabGroups?: { [editorKey: string]: Editor }
): string {
tabGroups = tabGroups || store.getState().editor.editors;
for (let key in tabGroups) {
for (const key in tabGroups) {
if (tabGroups[key] && tabGroups[key].documents) {
if (tabGroups[key].documents[documentId]) {
return key;
@ -66,15 +72,19 @@ export function getTabGroupForDocument(documentId: string, tabGroups?: { [editor
/** Takes a tab group key and returns the key of the other tab group */
export function getOtherTabGroup(tabGroup: string): string {
return tabGroup === Constants.EDITOR_KEY_PRIMARY ? Constants.EDITOR_KEY_SECONDARY : Constants.EDITOR_KEY_PRIMARY;
return tabGroup === Constants.EDITOR_KEY_PRIMARY
? Constants.EDITOR_KEY_SECONDARY
: Constants.EDITOR_KEY_PRIMARY;
}
export function showWelcomePage(): void {
store.dispatch(EditorActions.open({
contentType: Constants.CONTENT_TYPE_WELCOME_PAGE,
documentId: Constants.DOCUMENT_ID_WELCOME_PAGE,
isGlobal: true
}));
store.dispatch(
EditorActions.open({
contentType: Constants.CONTENT_TYPE_WELCOME_PAGE,
documentId: Constants.DOCUMENT_ID_WELCOME_PAGE,
isGlobal: true,
})
);
}
export function tabGroupHasDocuments(tabGroup: Editor): boolean {

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -30,8 +30,13 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
/* eslint-disable typescript/camelcase */
import {
azureArmTokenDataChanged,
invalidateArmToken,
} from '../action/azureAuthActions';
import { azureArmTokenDataChanged, invalidateArmToken, } from '../action/azureAuthActions';
import { azureAuth, AzureAuthState } from './azureAuthReducer';
describe('Azure auth reducer tests', () => {
@ -40,7 +45,7 @@ describe('Azure auth reducer tests', () => {
beforeEach(() => {
startingState = {
access_token: null,
persistLogin: false
persistLogin: false,
};
});

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -30,6 +30,7 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
/* eslint-disable typescript/camelcase */
import {
ArmTokenData,
@ -46,19 +47,20 @@ export interface AzureAuthState {
const initialState: AzureAuthState = {
access_token: null,
persistLogin: false
persistLogin: false,
};
export function azureAuth(state: AzureAuthState = initialState, action: AzureAuthAction<ArmTokenData | void>)
: AzureAuthState {
export function azureAuth(
state: AzureAuthState = initialState,
action: AzureAuthAction<ArmTokenData | void>
): AzureAuthState {
const { payload = {}, type = '' } = action || {};
const { access_token } = (payload || {}) as ArmTokenData;
switch (type) {
case AZURE_BEGIN_AUTH_WORKFLOW:
case AZURE_INVALIDATE_ARM_TOKEN:
return { ...state, access_token: ''};
return { ...state, access_token: '' };
case AZURE_ARM_TOKEN_DATA_CHANGED:
return { ...state, access_token };

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,16 +31,18 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { bot, BotState } from './bot';
import { BotAction, close, load, setActive } from '../action/botActions';
import { BotInfo } from '@bfemulator/app-shared';
import { BotConfigWithPath } from '@bfemulator/sdk-shared';
import { BotAction, close, load, setActive } from '../action/botActions';
import { bot, BotState } from './bot';
describe('Bot reducer tests', () => {
const DEFAULT_STATE: BotState = {
activeBot: null,
botFiles: [],
activeBotDigest: ''
activeBotDigest: '',
};
it('should return unaltered state for non-matching action type', () => {
@ -57,7 +59,7 @@ describe('Bot reducer tests', () => {
padlock: null,
services: [],
path: 'somePath',
version: '0.1'
version: '0.1',
};
it('should set a bot as active', () => {
@ -71,23 +73,23 @@ describe('Bot reducer tests', () => {
{
displayName: 'bot2',
path: 'path2',
secret: 'test-secret'
secret: 'test-secret',
},
{
displayName: 'bot3',
path: 'path3',
secret: null
secret: null,
},
{
displayName: 'bot1',
path: 'somePath',
secret: null
secret: null,
},
];
const startingState: BotState = {
...DEFAULT_STATE,
botFiles: testbots
botFiles: testbots,
};
const action = setActive(testbot);
@ -109,10 +111,10 @@ describe('Bot reducer tests', () => {
endpoint: 'someEndpointOverride',
appId: 'someAppId',
appPassword: 'someAppPw',
id: 'someEndpointOverride'
}
}
} as any
id: 'someEndpointOverride',
},
},
} as any,
};
const action = setActive(testbot);
@ -129,7 +131,7 @@ describe('Bot reducer tests', () => {
expect(endpointOverrides.appPassword).toBe('someAppPw');
});
it('should throw away overrides from the previous bot if they don\'t have the same path', () => {
it("should throw away overrides from the previous bot if they don't have the same path", () => {
const startingState: BotState = {
...DEFAULT_STATE,
activeBot: {
@ -143,10 +145,10 @@ describe('Bot reducer tests', () => {
endpoint: 'someEndpointOverride',
appId: 'someAppId',
appPassword: 'someAppPw',
id: 'someEndpointOverride'
}
}
} as any
id: 'someEndpointOverride',
},
},
} as any,
};
const action = setActive(testbot);
@ -163,19 +165,19 @@ describe('Bot reducer tests', () => {
{
displayName: 'bot1',
path: 'path1',
secret: null
secret: null,
},
{
displayName: 'bot2',
path: 'path2',
secret: 'test-secret'
secret: 'test-secret',
},
{
displayName: 'bot3',
path: 'path3',
secret: null
secret: null,
},
null
null,
];
const action = load(bots);
const state = bot(DEFAULT_STATE, action);
@ -185,18 +187,18 @@ describe('Bot reducer tests', () => {
{
displayName: 'bot1',
path: 'path1',
secret: null
secret: null,
},
{
displayName: 'bot2',
path: 'path2',
secret: 'test-secret'
secret: 'test-secret',
},
{
displayName: 'bot3',
path: 'path3',
secret: null
}
secret: null,
},
]);
});
@ -207,8 +209,8 @@ describe('Bot reducer tests', () => {
name: 'bot',
description: 'this is a test bot',
padlock: null,
services: []
} as any
services: [],
} as any,
};
const action = close();
const endingState = bot(startingState, action);

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -32,7 +32,12 @@
//
import { BotInfo } from '@bfemulator/app-shared';
import { applyBotConfigOverrides, BotConfigWithPath, botsAreTheSame } from '@bfemulator/sdk-shared';
import {
applyBotConfigOverrides,
BotConfigWithPath,
botsAreTheSame,
} from '@bfemulator/sdk-shared';
import { BotAction, BotActions } from '../action/botActions';
export interface BotState {
@ -44,12 +49,11 @@ export interface BotState {
const DEFAULT_STATE: BotState = {
activeBot: null,
activeBotDigest: null,
botFiles: []
botFiles: [],
};
export function bot(state: BotState = DEFAULT_STATE, action: BotAction) {
switch (action.type) {
case BotActions.load: {
state = setBotFilesState(action.payload.bots, state);
break;
@ -57,14 +61,21 @@ export function bot(state: BotState = DEFAULT_STATE, action: BotAction) {
case BotActions.setActive: {
// move active bot up to the top of the recent bots list
const mostRecentBot = state.botFiles.find(botArg => botArg && botArg.path === action.payload.bot.path);
let recentBots = state.botFiles.filter(botArg => botArg && botArg.path !== action.payload.bot.path);
const mostRecentBot = state.botFiles.find(
botArg => botArg && botArg.path === action.payload.bot.path
);
const recentBots = state.botFiles.filter(
botArg => botArg && botArg.path !== action.payload.bot.path
);
if (mostRecentBot) {
recentBots.unshift(mostRecentBot);
}
let newActiveBot = action.payload.bot;
if (botsAreTheSame(state.activeBot, newActiveBot)) {
newActiveBot = applyBotConfigOverrides(newActiveBot, state.activeBot.overrides);
newActiveBot = applyBotConfigOverrides(
newActiveBot,
state.activeBot.overrides
);
}
state = setBotFilesState(recentBots, state);
state = setActiveBot(newActiveBot, state);
@ -87,16 +98,17 @@ export function bot(state: BotState = DEFAULT_STATE, action: BotAction) {
}
function setActiveBot(botConfig: BotConfigWithPath, state: BotState): BotState {
return Object.assign({}, state, {
return {
...state,
get activeBot() {
// Clones only - this guarantees only pristine bots will exist in the store
return JSON.parse(JSON.stringify(botConfig));
}
});
},
};
}
function setBotFilesState(botFilesState: BotInfo[], state: BotState): BotState {
let newState = Object.assign({}, state);
const newState = { ...state };
newState.botFiles = botFilesState;
return newState;

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -32,6 +32,7 @@
//
import LogEntry from '@bfemulator/emulator-core/lib/types/log/entry';
import {
addTranscript,
appendToLog,
@ -43,9 +44,10 @@ import {
newDocument,
removeTranscript,
setInspectorObjects,
updateChat
updateChat,
} from '../action/chatActions';
import { closeNonGlobalTabs } from '../action/editorActions';
import { chat, ChatState } from './chat';
describe('Chat reducer tests', () => {
@ -55,9 +57,9 @@ describe('Chat reducer tests', () => {
chats: {
[testChatId]: {
log: {
entries: []
}
}
entries: [],
},
},
},
transcripts: [],
};
@ -116,13 +118,13 @@ describe('Chat reducer tests', () => {
...DEFAULT_STATE,
chats: {
...DEFAULT_STATE.chats,
[testChatId]: {}
}
[testChatId]: {},
},
};
const endingState = chat(startingState, action);
const expectedDoc = {
...endingState.chats[testChatId],
testing: true
testing: true,
};
expect(endingState.chats[testChatId]).toEqual(expectedDoc);
});
@ -135,10 +137,10 @@ describe('Chat reducer tests', () => {
type: 'text',
payload: {
level: 0,
text: 'testing'
}
}
]
text: 'testing',
},
},
],
};
const action = appendToLog(testChatId, logEntry);
const startingState = {
@ -147,10 +149,10 @@ describe('Chat reducer tests', () => {
...DEFAULT_STATE.chats,
[testChatId]: {
log: {
entries: []
}
}
}
entries: [],
},
},
},
};
const endingState = chat(startingState, action);
expect(endingState.chats[testChatId].log.entries[0]).toBeTruthy();
@ -165,10 +167,10 @@ describe('Chat reducer tests', () => {
type: 'text',
payload: {
level: 0,
text: 'testing'
}
}
]
text: 'testing',
},
},
],
};
const startingState = {
...DEFAULT_STATE,
@ -176,10 +178,10 @@ describe('Chat reducer tests', () => {
...DEFAULT_STATE.chats,
[testChatId]: {
log: {
entries: []
}
}
}
entries: [],
},
},
},
};
let state = chat(startingState, appendToLog(testChatId, logEntry));
@ -195,12 +197,16 @@ describe('Chat reducer tests', () => {
...DEFAULT_STATE,
chats: {
...DEFAULT_STATE.chats,
[testChatId]: {}
}
[testChatId]: {},
},
};
const endingState = chat(startingState, action);
expect(endingState.chats[testChatId].inspectorObjects.length).toBeGreaterThan(0);
expect(endingState.chats[testChatId].inspectorObjects[0]).toEqual({ testing: true });
expect(
endingState.chats[testChatId].inspectorObjects.length
).toBeGreaterThan(0);
expect(endingState.chats[testChatId].inspectorObjects[0]).toEqual({
testing: true,
});
});
it('should reset state on a "close all" editor action', () => {
@ -209,10 +215,10 @@ describe('Chat reducer tests', () => {
changeKey: 999,
chats: {
[tempChat]: {
testing: true
}
testing: true,
},
},
transcripts: ['xs1', 'xs2', 'xs3']
transcripts: ['xs1', 'xs2', 'xs3'],
};
const action = closeNonGlobalTabs();
const state = chat(alteredState, action);
@ -228,11 +234,14 @@ describe('Chat reducer tests', () => {
...DEFAULT_STATE.chats,
chat1: {
id: 'chat',
userId: 'userId'
}
}
userId: 'userId',
},
},
};
const action = updateChat('chat1', { id: 'updatedChatId', userId: 'updatedUserId' });
const action = updateChat('chat1', {
id: 'updatedChatId',
userId: 'updatedUserId',
});
const state = chat(startingState, action);
expect(state.chats.chat1.id).toBe('updatedChatId');
expect(state.chats.chat1.userId).toBe('updatedUserId');

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -47,7 +47,10 @@ const DEFAULT_STATE: ChatState = {
transcripts: [],
};
export function chat(state: ChatState = DEFAULT_STATE, action: ChatAction | EditorAction): ChatState {
export function chat(
state: ChatState = DEFAULT_STATE,
action: ChatAction | EditorAction
): ChatState {
switch (action.type) {
case ChatActions.addTranscript: {
const { payload } = action;
@ -78,8 +81,8 @@ export function chat(state: ChatState = DEFAULT_STATE, action: ChatAction | Edit
changeKey: state.changeKey + 1,
chats: {
...state.chats,
[payload.documentId]: { ...payload }
}
[payload.documentId]: { ...payload },
},
};
break;
}
@ -89,7 +92,7 @@ export function chat(state: ChatState = DEFAULT_STATE, action: ChatAction | Edit
// can't use the JSON.parse(JSON.stringify())
// trick with chats because Subscribers are circular
if (payload.documentId in state.chats) {
let copy = { ...state };
const copy = { ...state };
copy.changeKey += 1;
delete copy.chats[payload.documentId];
state = { ...copy };
@ -103,16 +106,16 @@ export function chat(state: ChatState = DEFAULT_STATE, action: ChatAction | Edit
if (document) {
document = {
...document,
...payload.options
...payload.options,
};
state = {
...state,
chats: {
...state.chats,
[payload.documentId]: {
...document
}
}
...document,
},
},
};
}
break;
@ -126,20 +129,17 @@ export function chat(state: ChatState = DEFAULT_STATE, action: ChatAction | Edit
...document,
log: {
...document.log,
entries: [
...document.log.entries,
payload.entry
]
}
entries: [...document.log.entries, payload.entry],
},
};
state = {
...state,
chats: {
...state.chats,
[payload.documentId]: {
...document
}
}
...document,
},
},
};
}
break;
@ -152,17 +152,17 @@ export function chat(state: ChatState = DEFAULT_STATE, action: ChatAction | Edit
document = {
...document,
log: {
entries: []
}
entries: [],
},
};
state = {
...state,
chats: {
...state.chats,
[payload.documentId]: {
...document
}
}
...document,
},
},
};
}
break;
@ -174,7 +174,7 @@ export function chat(state: ChatState = DEFAULT_STATE, action: ChatAction | Edit
if (document) {
document = {
...document,
inspectorObjects: payload.objs
inspectorObjects: payload.objs,
};
}
state = {
@ -182,9 +182,9 @@ export function chat(state: ChatState = DEFAULT_STATE, action: ChatAction | Edit
chats: {
...state.chats,
[payload.documentId]: {
...document
}
}
...document,
},
},
};
break;
}
@ -196,16 +196,16 @@ export function chat(state: ChatState = DEFAULT_STATE, action: ChatAction | Edit
if (document) {
document = {
...document,
...updatedValues
...updatedValues,
};
state = {
...state,
chats: {
...state.chats,
[payload.documentId]: {
...document
}
}
...document,
},
},
};
}
break;
@ -223,8 +223,11 @@ export function chat(state: ChatState = DEFAULT_STATE, action: ChatAction | Edit
return state;
}
function setTranscriptsState(transcripts: string[], state: ChatState): ChatState {
let newState = Object.assign({}, state);
function setTranscriptsState(
transcripts: string[],
state: ChatState
): ChatState {
const newState = { ...state };
newState.transcripts = transcripts;
newState.changeKey = state.changeKey + 1;

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,8 +1,46 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { ClientAwareSettings } from '@bfemulator/app-shared/built';
import { CLIENT_AWARE_SETTINGS_CHANGED, ClientAwareSettingsActions } from '../action/clientAwareSettingsActions';
export function clientAwareSettings(state: ClientAwareSettings = {} as any, action: ClientAwareSettingsActions)
: ClientAwareSettings {
import {
CLIENT_AWARE_SETTINGS_CHANGED,
ClientAwareSettingsActions,
} from '../action/clientAwareSettingsActions';
export function clientAwareSettings(
state: ClientAwareSettings = {} as any,
action: ClientAwareSettingsActions
): ClientAwareSettings {
switch (action.type) {
case CLIENT_AWARE_SETTINGS_CHANGED:
return { ...state, ...action.payload };

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -32,11 +32,12 @@
//
import { DialogAction, setShowing } from '../action/dialogActions';
import { dialog, DialogState } from './dialog';
describe('Dialog reducer tests', () => {
const DEFAULT_STATE: DialogState = {
showing: false
showing: false,
};
it('should return unaltered state for non-matching action type', () => {

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -38,10 +38,13 @@ export interface DialogState {
}
const DEFAULT_STATE: DialogState = {
showing: false
showing: false,
};
export function dialog(state: DialogState = DEFAULT_STATE, action: DialogAction): DialogState {
export function dialog(
state: DialogState = DEFAULT_STATE,
action: DialogAction
): DialogState {
switch (action.type) {
case DialogActions.setShowing: {
state = setShowing(action.payload.showing, state);
@ -55,5 +58,5 @@ export function dialog(state: DialogState = DEFAULT_STATE, action: DialogAction)
}
export function setShowing(showing: boolean, _state: DialogState): DialogState {
return { showing: showing };
return { showing };
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -32,6 +32,7 @@
//
import { deepCopySlow } from '@bfemulator/app-shared';
import * as Constants from '../../constants';
import {
addDocPendingChange,
@ -48,8 +49,9 @@ import {
splitTab,
swapTabs,
toggleDraggingTab,
updateDocument
updateDocument,
} from '../action/editorActions';
import {
editor,
Editor,
@ -59,26 +61,25 @@ import {
setActiveEditor,
setDraggingTab,
setEditorState,
setNewPrimaryEditor
setNewPrimaryEditor,
} from './editor';
let defaultState: EditorState;
jest.mock('../../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: function mock() {
return undefined;
},
AzureLoginSuccessDialogContainer: function mock() {
return undefined;
},
BotCreationDialog: function mock() {
return undefined;
},
DialogService: { showDialog: () => Promise.resolve(true) },
SecretPromptDialog: function mock() {
return undefined;
}
}
));
AzureLoginPromptDialogContainer: function mock() {
return undefined;
},
AzureLoginSuccessDialogContainer: function mock() {
return undefined;
},
BotCreationDialog: function mock() {
return undefined;
},
DialogService: { showDialog: () => Promise.resolve(true) },
SecretPromptDialog: function mock() {
return undefined;
},
}));
describe('Editor reducer tests', () => {
beforeEach(initializeDefaultState);
@ -98,17 +99,21 @@ describe('Editor reducer tests', () => {
...defaultState.editors,
[Constants.EDITOR_KEY_PRIMARY]: {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
tabOrder: ['doc1', 'doc2']
}
}
tabOrder: ['doc1', 'doc2'],
},
},
};
const srcEditorKey = Constants.EDITOR_KEY_PRIMARY;
const destEditorKey = Constants.EDITOR_KEY_PRIMARY;
const docIdToAppend = 'doc1';
const action = appendTab(srcEditorKey, destEditorKey, docIdToAppend);
const endingState = editor(startingState, action);
expect(endingState.editors[Constants.EDITOR_KEY_PRIMARY].tabOrder[0]).not.toBe(docIdToAppend);
expect(endingState.editors[Constants.EDITOR_KEY_PRIMARY].tabOrder[1]).toBe(docIdToAppend);
expect(
endingState.editors[Constants.EDITOR_KEY_PRIMARY].tabOrder[0]
).not.toBe(docIdToAppend);
expect(
endingState.editors[Constants.EDITOR_KEY_PRIMARY].tabOrder[1]
).toBe(docIdToAppend);
// assert that draggingTab is toggled off
expect(endingState.draggingTab).toBe(false);
@ -123,21 +128,21 @@ describe('Editor reducer tests', () => {
[Constants.EDITOR_KEY_PRIMARY]: {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
documents: {
'doc1': {},
'doc2': {}
doc1: {},
doc2: {},
},
tabOrder: ['doc1', 'doc2'],
recentTabs: ['doc1', 'doc2']
recentTabs: ['doc1', 'doc2'],
},
[Constants.EDITOR_KEY_SECONDARY]: {
...defaultState.editors[Constants.EDITOR_KEY_SECONDARY],
documents: {
'doc3': {}
doc3: {},
},
tabOrder: ['doc3'],
recentTabs: ['doc3']
}
}
recentTabs: ['doc3'],
},
},
};
const srcEditorKey = Constants.EDITOR_KEY_PRIMARY;
const destEditorKey = Constants.EDITOR_KEY_SECONDARY;
@ -165,20 +170,20 @@ describe('Editor reducer tests', () => {
[Constants.EDITOR_KEY_PRIMARY]: {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
documents: {
'doc1': {}
doc1: {},
},
tabOrder: ['doc1'],
recentTabs: ['doc1']
recentTabs: ['doc1'],
},
[Constants.EDITOR_KEY_SECONDARY]: {
...defaultState.editors[Constants.EDITOR_KEY_SECONDARY],
documents: {
'doc2': {}
doc2: {},
},
tabOrder: ['doc2'],
recentTabs: ['doc2']
}
}
recentTabs: ['doc2'],
},
},
};
const srcEditorKey = Constants.EDITOR_KEY_SECONDARY;
const destEditorKey = Constants.EDITOR_KEY_PRIMARY;
@ -197,28 +202,32 @@ describe('Editor reducer tests', () => {
[Constants.EDITOR_KEY_PRIMARY]: {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
documents: {
'doc1': {}
doc1: {},
},
tabOrder: ['doc1'],
recentTabs: ['doc1']
recentTabs: ['doc1'],
},
[Constants.EDITOR_KEY_SECONDARY]: {
...defaultState.editors[Constants.EDITOR_KEY_SECONDARY],
documents: {
'doc2': {}
doc2: {},
},
tabOrder: ['doc2'],
recentTabs: ['doc2']
}
}
recentTabs: ['doc2'],
},
},
};
const srcEditorKey = Constants.EDITOR_KEY_PRIMARY;
const destEditorKey = Constants.EDITOR_KEY_SECONDARY;
const action = appendTab(srcEditorKey, destEditorKey, 'doc1');
const endingState = editor(startingState, action);
expect(endingState.activeEditor).toBe(Constants.EDITOR_KEY_PRIMARY);
expect(endingState.editors[Constants.EDITOR_KEY_PRIMARY].tabOrder).toEqual(['doc2', 'doc1']);
expect(endingState.editors[Constants.EDITOR_KEY_SECONDARY].tabOrder).toEqual([]);
expect(
endingState.editors[Constants.EDITOR_KEY_PRIMARY].tabOrder
).toEqual(['doc2', 'doc1']);
expect(
endingState.editors[Constants.EDITOR_KEY_SECONDARY].tabOrder
).toEqual([]);
// assert that draggingTab is toggled off
expect(endingState.draggingTab).toBe(false);
@ -235,13 +244,13 @@ describe('Editor reducer tests', () => {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
activeDocumentId: 'doc1',
documents: {
'doc1': {},
'doc2': {}
doc1: {},
doc2: {},
},
recentTabs: ['doc1', 'doc2'],
tabOrder: ['doc1', 'doc2']
}
}
tabOrder: ['doc1', 'doc2'],
},
},
};
const editorKey = Constants.EDITOR_KEY_PRIMARY;
const action = close(editorKey, 'doc1');
@ -263,21 +272,21 @@ describe('Editor reducer tests', () => {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
activeDocumentId: 'doc1',
documents: {
'doc1': {}
doc1: {},
},
recentTabs: ['doc1'],
tabOrder: ['doc1']
tabOrder: ['doc1'],
},
[Constants.EDITOR_KEY_SECONDARY]: {
...defaultState.editors[Constants.EDITOR_KEY_SECONDARY],
activeDocumentId: 'doc2',
documents: {
'doc2': {}
doc2: {},
},
recentTabs: ['doc2'],
tabOrder: ['doc2']
}
}
tabOrder: ['doc2'],
},
},
};
const editorKey = Constants.EDITOR_KEY_PRIMARY;
const action = close(editorKey, 'doc1');
@ -287,7 +296,11 @@ describe('Editor reducer tests', () => {
expect(primaryEditor.recentTabs).toEqual(['doc2']);
expect(primaryEditor.tabOrder).toEqual(['doc2']);
expect(Object.keys(primaryEditor.documents)).toContain('doc2');
expect(Object.keys(endingState.editors[Constants.EDITOR_KEY_SECONDARY].documents)).not.toContain('doc1');
expect(
Object.keys(
endingState.editors[Constants.EDITOR_KEY_SECONDARY].documents
)
).not.toContain('doc1');
});
});
@ -300,7 +313,7 @@ describe('Editor reducer tests', () => {
it('should set the active editor', () => {
const startingState: EditorState = {
...defaultState,
activeEditor: Constants.EDITOR_KEY_SECONDARY
activeEditor: Constants.EDITOR_KEY_SECONDARY,
};
const action = setActiveEditorAction(Constants.EDITOR_KEY_PRIMARY);
const endingState = editor(startingState, action);
@ -317,22 +330,22 @@ describe('Editor reducer tests', () => {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
activeDocumentId: 'doc1',
documents: {
'doc1': {},
'doc2': {}
doc1: {},
doc2: {},
},
recentTabs: ['doc1', 'doc2'],
tabOrder: ['doc1', 'doc2']
tabOrder: ['doc1', 'doc2'],
},
[Constants.EDITOR_KEY_SECONDARY]: {
...defaultState.editors[Constants.EDITOR_KEY_SECONDARY],
activeDocumentId: 'doc3',
documents: {
'doc3': {}
doc3: {},
},
recentTabs: ['doc3'],
tabOrder: ['doc3']
}
}
tabOrder: ['doc3'],
},
},
};
const action = setActiveTab('doc2');
const endingState = editor(startingState, action);
@ -342,7 +355,7 @@ describe('Editor reducer tests', () => {
expect(newActiveEditor.recentTabs).toEqual(['doc2', 'doc1']);
});
it('should set a document\'s dirty flag', () => {
it("should set a document's dirty flag", () => {
const startingState: EditorState = {
...defaultState,
editors: {
@ -351,17 +364,20 @@ describe('Editor reducer tests', () => {
...defaultState.editors[Constants.EDITOR_KEY_SECONDARY],
activeDocumentId: 'doc3',
documents: {
'doc3': { dirty: false }
doc3: { dirty: false },
},
recentTabs: ['doc3'],
tabOrder: ['doc3']
}
}
tabOrder: ['doc3'],
},
},
};
const dirtyDocId = 'doc3';
const action = setDirtyFlag(dirtyDocId, true);
const endingState = editor(startingState, action);
expect(endingState.editors[Constants.EDITOR_KEY_SECONDARY].documents[dirtyDocId].dirty).toBe(true);
expect(
endingState.editors[Constants.EDITOR_KEY_SECONDARY].documents[dirtyDocId]
.dirty
).toBe(true);
});
it('should close all non-global tabs', () => {
@ -374,26 +390,26 @@ describe('Editor reducer tests', () => {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
activeDocumentId: 'doc2',
documents: {
'doc1': { isGlobal: true },
'doc2': { isGlobal: false },
'doc3': { isGlobal: false }
doc1: { isGlobal: true },
doc2: { isGlobal: false },
doc3: { isGlobal: false },
},
recentTabs: ['doc2', 'doc1', 'doc3'],
tabOrder: ['doc1', 'doc2', 'doc3']
tabOrder: ['doc1', 'doc2', 'doc3'],
},
[Constants.EDITOR_KEY_SECONDARY]: {
...defaultState.editors[Constants.EDITOR_KEY_SECONDARY],
activeDocumentId: 'doc6',
documents: {
'doc4': { isGlobal: false },
'doc5': { isGlobal: true },
'doc6': { isGlobal: true }
doc4: { isGlobal: false },
doc5: { isGlobal: true },
doc6: { isGlobal: true },
},
recentTabs: ['doc6', 'doc4', 'doc5'],
tabOrder: ['doc4', 'doc5', 'doc6']
}
tabOrder: ['doc4', 'doc5', 'doc6'],
},
},
docsWithPendingChanges: ['doc2', 'doc3']
docsWithPendingChanges: ['doc2', 'doc3'],
};
const action = closeNonGlobalTabs();
const endingState = editor(startingState, action);
@ -421,18 +437,22 @@ describe('Editor reducer tests', () => {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
activeDocumentId: 'doc1',
documents: {
'doc1': { documentId: 'doc1' }
doc1: { documentId: 'doc1' },
},
recentTabs: ['doc1'],
tabOrder: ['doc1']
}
}
tabOrder: ['doc1'],
},
},
};
const docToUpdateId = 'doc1';
const action = updateDocument(docToUpdateId, { isGlobal: true, dirty: true });
const action = updateDocument(docToUpdateId, {
isGlobal: true,
dirty: true,
});
const endingState = editor(startingState, action);
expect(endingState.editors[Constants.EDITOR_KEY_PRIMARY].documents[docToUpdateId])
.toEqual({ documentId: 'doc1', isGlobal: true, dirty: true });
expect(
endingState.editors[Constants.EDITOR_KEY_PRIMARY].documents[docToUpdateId]
).toEqual({ documentId: 'doc1', isGlobal: true, dirty: true });
});
describe('opening a document', () => {
@ -446,27 +466,33 @@ describe('Editor reducer tests', () => {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
activeDocumentId: 'doc1',
documents: {
'doc1': {}
doc1: {},
},
recentTabs: ['doc1'],
tabOrder: ['doc1']
tabOrder: ['doc1'],
},
[Constants.EDITOR_KEY_SECONDARY]: {
...defaultState.editors[Constants.EDITOR_KEY_SECONDARY],
activeDocumentId: 'doc2',
documents: {
'doc2': {},
'doc3': {}
doc2: {},
doc3: {},
},
recentTabs: ['doc3', 'doc2'],
tabOrder: ['doc2', 'doc3']
}
}
tabOrder: ['doc2', 'doc3'],
},
},
};
const action = open({ contentType: Constants.CONTENT_TYPE_APP_SETTINGS, documentId: 'doc2', isGlobal: true });
const action = open({
contentType: Constants.CONTENT_TYPE_APP_SETTINGS,
documentId: 'doc2',
isGlobal: true,
});
const endingState = editor(startingState, action);
expect(endingState.activeEditor).toBe(Constants.EDITOR_KEY_SECONDARY);
expect(endingState.editors[Constants.EDITOR_KEY_SECONDARY].recentTabs).toEqual(['doc2', 'doc3']);
expect(
endingState.editors[Constants.EDITOR_KEY_SECONDARY].recentTabs
).toEqual(['doc2', 'doc3']);
});
it('should focus an already existing document in the same tab group', () => {
@ -479,15 +505,19 @@ describe('Editor reducer tests', () => {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
activeDocumentId: 'doc1',
documents: {
'doc1': {},
'doc2': {}
doc1: {},
doc2: {},
},
recentTabs: ['doc1', 'doc2'],
tabOrder: ['doc1', 'doc2']
}
}
tabOrder: ['doc1', 'doc2'],
},
},
};
const action = open({ contentType: Constants.CONTENT_TYPE_APP_SETTINGS, documentId: 'doc2', isGlobal: true });
const action = open({
contentType: Constants.CONTENT_TYPE_APP_SETTINGS,
documentId: 'doc2',
isGlobal: true,
});
const endingState = editor(startingState, action);
const primaryEditor = endingState.editors[Constants.EDITOR_KEY_PRIMARY];
expect(primaryEditor.activeDocumentId).toBe('doc2');
@ -505,15 +535,19 @@ describe('Editor reducer tests', () => {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
activeDocumentId: 'doc1',
documents: {
'doc1': {},
'doc2': {}
doc1: {},
doc2: {},
},
recentTabs: ['doc1', 'doc2'],
tabOrder: ['doc1', 'doc2']
}
}
tabOrder: ['doc1', 'doc2'],
},
},
};
const action = open({ contentType: Constants.CONTENT_TYPE_APP_SETTINGS, documentId: 'doc3', isGlobal: true });
const action = open({
contentType: Constants.CONTENT_TYPE_APP_SETTINGS,
documentId: 'doc3',
isGlobal: true,
});
const endingState = editor(startingState, action);
const primaryEditor = endingState.editors[Constants.EDITOR_KEY_PRIMARY];
expect(primaryEditor.activeDocumentId).toBe('doc3');
@ -522,7 +556,7 @@ describe('Editor reducer tests', () => {
expect(Object.keys(primaryEditor.documents)).toContain('doc3');
});
it('should append new document if current activeDocument isn\'t found', () => {
it("should append new document if current activeDocument isn't found", () => {
const startingState: EditorState = {
...defaultState,
activeEditor: Constants.EDITOR_KEY_PRIMARY,
@ -532,15 +566,19 @@ describe('Editor reducer tests', () => {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
activeDocumentId: 'doc1234',
documents: {
'doc1': {},
'doc2': {}
doc1: {},
doc2: {},
},
recentTabs: ['doc1', 'doc2'],
tabOrder: ['doc1', 'doc2']
}
}
tabOrder: ['doc1', 'doc2'],
},
},
};
const action = open({ contentType: Constants.CONTENT_TYPE_APP_SETTINGS, documentId: 'doc3', isGlobal: true });
const action = open({
contentType: Constants.CONTENT_TYPE_APP_SETTINGS,
documentId: 'doc3',
isGlobal: true,
});
const endingState = editor(startingState, action);
const primaryEditor = endingState.editors[Constants.EDITOR_KEY_PRIMARY];
expect(primaryEditor.activeDocumentId).toBe('doc3');
@ -561,22 +599,22 @@ describe('Editor reducer tests', () => {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
activeDocumentId: 'doc2',
documents: {
'doc1': {},
'doc2': {}
doc1: {},
doc2: {},
},
recentTabs: ['doc1', 'doc2'],
tabOrder: ['doc1', 'doc2']
tabOrder: ['doc1', 'doc2'],
},
[Constants.EDITOR_KEY_SECONDARY]: {
...defaultState.editors[Constants.EDITOR_KEY_SECONDARY],
activeDocumentId: 'doc3',
documents: {
'doc3': {}
doc3: {},
},
recentTabs: ['doc3'],
tabOrder: ['doc3']
}
}
tabOrder: ['doc3'],
},
},
};
const action = splitTab(
Constants.CONTENT_TYPE_APP_SETTINGS,
@ -611,14 +649,14 @@ describe('Editor reducer tests', () => {
[Constants.EDITOR_KEY_PRIMARY]: {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
documents: {
'doc1': {},
'doc2': {},
'doc3': {}
doc1: {},
doc2: {},
doc3: {},
},
recentTabs: ['doc3', 'doc2', 'doc1'],
tabOrder: ['doc1', 'doc2', 'doc3']
}
}
tabOrder: ['doc1', 'doc2', 'doc3'],
},
},
};
const action = swapTabs(
Constants.EDITOR_KEY_PRIMARY,
@ -640,23 +678,23 @@ describe('Editor reducer tests', () => {
[Constants.EDITOR_KEY_PRIMARY]: {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
documents: {
'doc1': {},
'doc2': {}
doc1: {},
doc2: {},
},
recentTabs: ['doc2', 'doc1'],
tabOrder: ['doc1', 'doc2']
tabOrder: ['doc1', 'doc2'],
},
[Constants.EDITOR_KEY_SECONDARY]: {
...defaultState.editors[Constants.EDITOR_KEY_SECONDARY],
activeDocumentId: 'doc3',
documents: {
'doc3': {},
'doc4': {}
doc3: {},
doc4: {},
},
recentTabs: ['doc4', 'doc3'],
tabOrder: ['doc3', 'doc4']
}
}
tabOrder: ['doc3', 'doc4'],
},
},
};
const action = swapTabs(
Constants.EDITOR_KEY_SECONDARY,
@ -666,7 +704,8 @@ describe('Editor reducer tests', () => {
);
const endingState = editor(startingState, action);
const primaryEditor = endingState.editors[Constants.EDITOR_KEY_PRIMARY];
const secondaryEditor = endingState.editors[Constants.EDITOR_KEY_SECONDARY];
const secondaryEditor =
endingState.editors[Constants.EDITOR_KEY_SECONDARY];
expect(endingState.activeEditor).toBe(Constants.EDITOR_KEY_SECONDARY);
expect(Object.keys(primaryEditor.documents)).toContain('doc3');
@ -688,22 +727,22 @@ describe('Editor reducer tests', () => {
[Constants.EDITOR_KEY_PRIMARY]: {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
documents: {
'doc1': {},
'doc2': {}
doc1: {},
doc2: {},
},
recentTabs: ['doc2', 'doc1'],
tabOrder: ['doc1', 'doc2']
tabOrder: ['doc1', 'doc2'],
},
[Constants.EDITOR_KEY_SECONDARY]: {
...defaultState.editors[Constants.EDITOR_KEY_SECONDARY],
activeDocumentId: 'doc3',
documents: {
'doc3': {}
doc3: {},
},
recentTabs: ['doc3'],
tabOrder: ['doc3']
}
}
tabOrder: ['doc3'],
},
},
};
const action = swapTabs(
Constants.EDITOR_KEY_SECONDARY,
@ -725,21 +764,21 @@ describe('Editor reducer tests', () => {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
activeDocumentId: 'doc1',
documents: {
'doc1': {}
doc1: {},
},
recentTabs: ['doc1'],
tabOrder: ['doc1']
tabOrder: ['doc1'],
},
[Constants.EDITOR_KEY_SECONDARY]: {
...defaultState.editors[Constants.EDITOR_KEY_SECONDARY],
activeDocumentId: 'doc2',
documents: {
'doc2': {}
doc2: {},
},
recentTabs: ['doc2'],
tabOrder: ['doc2']
}
}
tabOrder: ['doc2'],
},
},
};
const action = swapTabs(
Constants.EDITOR_KEY_PRIMARY,
@ -749,7 +788,8 @@ describe('Editor reducer tests', () => {
);
const endingState = editor(startingState, action);
const primaryEditor = endingState.editors[Constants.EDITOR_KEY_PRIMARY];
const secondaryEditor = endingState.editors[Constants.EDITOR_KEY_SECONDARY];
const secondaryEditor =
endingState.editors[Constants.EDITOR_KEY_SECONDARY];
expect(endingState.activeEditor).toBe(Constants.EDITOR_KEY_PRIMARY);
expect(primaryEditor.activeDocumentId).toBe('doc2');
@ -768,32 +808,34 @@ describe('Editor reducer tests', () => {
it('should add a doc to the list', () => {
const startingState: EditorState = {
...defaultState,
docsWithPendingChanges: [
'doc1',
'doc2',
'doc3'
]
docsWithPendingChanges: ['doc1', 'doc2', 'doc3'],
};
const action1 = addDocPendingChange('doc4');
const endingState1 = editor(startingState, action1);
expect(endingState1.docsWithPendingChanges).toEqual(['doc1', 'doc2', 'doc3', 'doc4']);
expect(endingState1.docsWithPendingChanges).toEqual([
'doc1',
'doc2',
'doc3',
'doc4',
]);
// shouldn't allow duplicates
const action2 = addDocPendingChange('doc4');
const endingState2 = editor(endingState1, action2);
expect(endingState2.docsWithPendingChanges).toEqual(['doc1', 'doc2', 'doc3', 'doc4']);
expect(endingState2.docsWithPendingChanges).toEqual([
'doc1',
'doc2',
'doc3',
'doc4',
]);
});
it('should remove a doc from the list', () => {
const startingState: EditorState = {
...defaultState,
docsWithPendingChanges: [
'doc1',
'doc2',
'doc3'
]
docsWithPendingChanges: ['doc1', 'doc2', 'doc3'],
};
const action = removeDocPendingChange('doc2');
const endingState = editor(startingState, action);
@ -822,34 +864,50 @@ describe('Editor reducer utility function tests', () => {
[Constants.EDITOR_KEY_SECONDARY]: {
activeDocumentId: 'doc1',
documents: {
'doc1': {}
doc1: {},
},
tabOrder: ['doc1'],
recentTabs: ['doc1']
}
}
recentTabs: ['doc1'],
},
},
};
const newPrimaryEditor: Editor = {
activeDocumentId: 'doc2',
documents: {
'doc2': {}
doc2: {},
},
tabOrder: ['doc2'],
recentTabs: ['doc2']
recentTabs: ['doc2'],
};
const endingState = setNewPrimaryEditor(newPrimaryEditor, startingState);
expect(endingState.activeEditor).toBe(Constants.EDITOR_KEY_PRIMARY);
expect(endingState.editors[Constants.EDITOR_KEY_PRIMARY].activeDocumentId).toEqual('doc2');
expect(endingState.editors[Constants.EDITOR_KEY_PRIMARY].tabOrder).toEqual(['doc2']);
expect(endingState.editors[Constants.EDITOR_KEY_PRIMARY].recentTabs).toEqual(['doc2']);
expect(Object.keys(endingState.editors[Constants.EDITOR_KEY_PRIMARY].documents)).toContain('doc2');
expect(
endingState.editors[Constants.EDITOR_KEY_PRIMARY].activeDocumentId
).toEqual('doc2');
expect(endingState.editors[Constants.EDITOR_KEY_PRIMARY].tabOrder).toEqual([
'doc2',
]);
expect(
endingState.editors[Constants.EDITOR_KEY_PRIMARY].recentTabs
).toEqual(['doc2']);
expect(
Object.keys(endingState.editors[Constants.EDITOR_KEY_PRIMARY].documents)
).toContain('doc2');
expect(endingState.editors[Constants.EDITOR_KEY_SECONDARY].activeDocumentId).toBe(null);
expect(endingState.editors[Constants.EDITOR_KEY_SECONDARY].documents).toEqual({});
expect(endingState.editors[Constants.EDITOR_KEY_SECONDARY].recentTabs).toEqual([]);
expect(endingState.editors[Constants.EDITOR_KEY_SECONDARY].tabOrder).toEqual([]);
expect(
endingState.editors[Constants.EDITOR_KEY_SECONDARY].activeDocumentId
).toBe(null);
expect(
endingState.editors[Constants.EDITOR_KEY_SECONDARY].documents
).toEqual({});
expect(
endingState.editors[Constants.EDITOR_KEY_SECONDARY].recentTabs
).toEqual([]);
expect(
endingState.editors[Constants.EDITOR_KEY_SECONDARY].tabOrder
).toEqual([]);
});
it('setActiveEditor() functionality', () => {
@ -863,13 +921,23 @@ describe('Editor reducer utility function tests', () => {
activeDocumentId: 'testing',
documents: {},
tabOrder: ['testing'],
recentTabs: ['testing']
recentTabs: ['testing'],
};
const newState = setEditorState(Constants.EDITOR_KEY_PRIMARY, updatedEditor, defaultState);
const newState = setEditorState(
Constants.EDITOR_KEY_PRIMARY,
updatedEditor,
defaultState
);
expect(newState).not.toBe(defaultState);
expect(newState.editors[Constants.EDITOR_KEY_PRIMARY].activeDocumentId).toBe('testing');
expect(newState.editors[Constants.EDITOR_KEY_PRIMARY].tabOrder).toEqual(['testing']);
expect(newState.editors[Constants.EDITOR_KEY_PRIMARY].recentTabs).toEqual(['testing']);
expect(
newState.editors[Constants.EDITOR_KEY_PRIMARY].activeDocumentId
).toBe('testing');
expect(newState.editors[Constants.EDITOR_KEY_PRIMARY].tabOrder).toEqual([
'testing',
]);
expect(newState.editors[Constants.EDITOR_KEY_PRIMARY].recentTabs).toEqual([
'testing',
]);
});
describe('removeDocumentFromTabGroup() functionality', () => {
@ -879,19 +947,16 @@ describe('Editor reducer utility function tests', () => {
activeDocumentId: docToRemove,
documents: {
[docToRemove]: {},
'doc2': {}
doc2: {},
},
tabOrder: [
docToRemove,
'doc2'
],
recentTabs: [
docToRemove,
'doc2'
]
tabOrder: [docToRemove, 'doc2'],
recentTabs: [docToRemove, 'doc2'],
};
const modifiedEditor = removeDocumentFromTabGroup(tempEditor, docToRemove);
const modifiedEditor = removeDocumentFromTabGroup(
tempEditor,
docToRemove
);
expect(modifiedEditor).not.toBe(tempEditor);
expect(modifiedEditor.activeDocumentId).toBe('doc2');
expect(modifiedEditor.recentTabs).not.toContain(docToRemove);
@ -903,14 +968,10 @@ describe('Editor reducer utility function tests', () => {
const tempEditor: Editor = {
activeDocumentId: 'doc1',
documents: {
'doc1': {}
doc1: {},
},
tabOrder: [
'doc1'
],
recentTabs: [
'doc1'
]
tabOrder: ['doc1'],
recentTabs: ['doc1'],
};
const modifiedEditor = removeDocumentFromTabGroup(tempEditor, 'doc1');
@ -933,18 +994,18 @@ describe('Editor reducer utility function tests', () => {
activeDocumentId: null,
documents: {},
recentTabs: [],
tabOrder: []
tabOrder: [],
},
[Constants.EDITOR_KEY_SECONDARY]: {
...defaultState.editors[Constants.EDITOR_KEY_SECONDARY],
activeDocumentId: 'doc2',
documents: {
'doc2': {}
doc2: {},
},
recentTabs: ['doc2'],
tabOrder: ['doc2']
}
}
tabOrder: ['doc2'],
},
},
};
const endState = fixupTabGroups(startingState);
const primaryEditor = endState.editors[Constants.EDITOR_KEY_PRIMARY];
@ -972,19 +1033,19 @@ describe('Editor reducer utility function tests', () => {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
activeDocumentId: 'doc1',
documents: {
'doc1': {}
doc1: {},
},
recentTabs: ['doc1'],
tabOrder: ['doc1']
tabOrder: ['doc1'],
},
[Constants.EDITOR_KEY_SECONDARY]: {
...defaultState.editors[Constants.EDITOR_KEY_SECONDARY],
activeDocumentId: null,
documents: {},
recentTabs: [],
tabOrder: []
}
}
tabOrder: [],
},
},
};
const endState = fixupTabGroups(startingState);
expect(endState.activeEditor).toBe(Constants.EDITOR_KEY_PRIMARY);
@ -1000,21 +1061,21 @@ describe('Editor reducer utility function tests', () => {
...defaultState.editors[Constants.EDITOR_KEY_PRIMARY],
activeDocumentId: 'doc1',
documents: {
'doc1': {}
doc1: {},
},
recentTabs: ['doc1'],
tabOrder: ['doc1']
tabOrder: ['doc1'],
},
[Constants.EDITOR_KEY_SECONDARY]: {
...defaultState.editors[Constants.EDITOR_KEY_SECONDARY],
activeDocumentId: 'doc2',
documents: {
'doc2': {}
doc2: {},
},
recentTabs: ['doc2'],
tabOrder: ['doc2']
}
}
tabOrder: ['doc2'],
},
},
};
const endState = fixupTabGroups(startingState);
expect(endState).toEqual(startingState);
@ -1029,15 +1090,15 @@ describe('Editor reducer utility function tests', () => {
[Constants.EDITOR_KEY_PRIMARY]: {
activeDocumentId: 'doc2',
documents: {
'doc1': {},
'doc2': {},
'doc3': {}
doc1: {},
doc2: {},
doc3: {},
},
recentTabs: ['doc2', 'doc1', 'doc3'],
tabOrder: ['doc1', 'doc2', 'doc3']
tabOrder: ['doc1', 'doc2', 'doc3'],
},
[Constants.EDITOR_KEY_SECONDARY]: {}
}
[Constants.EDITOR_KEY_SECONDARY]: {},
},
};
const tabIdToDrop = 'doc1';
@ -1074,16 +1135,16 @@ function initializeDefaultState() {
activeDocumentId: null,
documents: {},
tabOrder: [],
recentTabs: []
recentTabs: [],
},
[Constants.EDITOR_KEY_SECONDARY]: {
activeDocumentId: null,
documents: {},
tabOrder: [],
recentTabs: []
}
recentTabs: [],
},
},
docsWithPendingChanges: []
docsWithPendingChanges: [],
};
defaultState = deepCopySlow(DEFAULT_STATE);
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -32,9 +32,10 @@
//
import { deepCopySlow } from '@bfemulator/app-shared';
import * as Constants from '../../constants';
import { EditorAction, EditorActions } from '../action/editorActions';
import { BotAction } from '../action/botActions';
import { EditorAction, EditorActions } from '../action/editorActions';
import { getOtherTabGroup, tabGroupHasDocuments } from '../editorHelpers';
export interface EditorState {
@ -72,12 +73,15 @@ const DEFAULT_STATE: EditorState = {
draggingTab: false,
editors: {
[Constants.EDITOR_KEY_PRIMARY]: getNewEditor(),
[Constants.EDITOR_KEY_SECONDARY]: getNewEditor()
[Constants.EDITOR_KEY_SECONDARY]: getNewEditor(),
},
docsWithPendingChanges: []
docsWithPendingChanges: [],
};
export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction | BotAction): EditorState => {
export const editor = (
state: EditorState = DEFAULT_STATE,
action: EditorAction | BotAction
): EditorState => {
Object.freeze(state);
switch (action.type) {
@ -87,11 +91,14 @@ export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction
/** if the tab is being appended to the end of its own editor, just re-adjust tab order */
if (srcEditorKey === destEditorKey) {
let tabOrder = [...state.editors[srcEditorKey].tabOrder];
tabOrder = [...tabOrder.filter(docId => docId !== action.payload.documentId), action.payload.documentId];
tabOrder = [
...tabOrder.filter(docId => docId !== action.payload.documentId),
action.payload.documentId,
];
let editorState: Editor = {
const editorState: Editor = {
...state.editors[srcEditorKey],
tabOrder: tabOrder
tabOrder,
};
state = setEditorState(srcEditorKey, editorState, state);
state = setDraggingTab(false, state);
@ -102,28 +109,44 @@ export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction
* if the tab is being appended to another editor,
* we need to modify both editors' docs, recent tabs, and tab order
*/
const docToAppend = state.editors[srcEditorKey].documents[action.payload.documentId];
const docToAppend =
state.editors[srcEditorKey].documents[action.payload.documentId];
// remove any trace of document from source editor
const srcEditor = removeDocumentFromTabGroup(state.editors[srcEditorKey], action.payload.documentId);
const srcEditor = removeDocumentFromTabGroup(
state.editors[srcEditorKey],
action.payload.documentId
);
// add the tab to the dest editor
const destTabOrder = [...state.editors[destEditorKey].tabOrder, action.payload.documentId];
const destRecentTabs = [...state.editors[destEditorKey].recentTabs, action.payload.documentId];
const destDocs = Object.assign({}, state.editors[destEditorKey].documents);
const destTabOrder = [
...state.editors[destEditorKey].tabOrder,
action.payload.documentId,
];
const destRecentTabs = [
...state.editors[destEditorKey].recentTabs,
action.payload.documentId,
];
const destDocs = { ...state.editors[destEditorKey].documents };
destDocs[action.payload.documentId] = docToAppend;
const destEditor: Editor = {
...state.editors[destEditorKey],
documents: destDocs,
recentTabs: destRecentTabs,
tabOrder: destTabOrder
tabOrder: destTabOrder,
};
if (!tabGroupHasDocuments(srcEditor) && srcEditorKey === Constants.EDITOR_KEY_PRIMARY) {
if (
!tabGroupHasDocuments(srcEditor) &&
srcEditorKey === Constants.EDITOR_KEY_PRIMARY
) {
state = setNewPrimaryEditor(destEditor, state);
} else {
state = setActiveEditor(!tabGroupHasDocuments(srcEditor) ? destEditorKey : state.activeEditor, state);
state = setActiveEditor(
!tabGroupHasDocuments(srcEditor) ? destEditorKey : state.activeEditor,
state
);
state = setEditorState(srcEditorKey, srcEditor, state);
state = setEditorState(destEditorKey, destEditor, state);
}
@ -138,11 +161,17 @@ export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction
const { editorKey } = action.payload;
// remove any trace of document from editor
const editor1 = removeDocumentFromTabGroup(state.editors[editorKey], action.payload.documentId);
const editor1 = removeDocumentFromTabGroup(
state.editors[editorKey],
action.payload.documentId
);
// close empty editor if there is another one able to take its place
const newPrimaryEditorKey = getOtherTabGroup(editorKey);
if (!tabGroupHasDocuments(editor1) && state.editors[newPrimaryEditorKey]) {
if (
!tabGroupHasDocuments(editor1) &&
state.editors[newPrimaryEditorKey]
) {
// if the editor being closed is the primary editor, have the secondary editor become the primary
const tmp: Editor = deepCopySlow(state.editors[newPrimaryEditorKey]);
state = setNewPrimaryEditor(tmp, state);
@ -157,14 +186,14 @@ export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction
return DEFAULT_STATE;
} else {
let newState: EditorState = {
...state
...state,
};
for (let key in state.editors) {
for (const key in state.editors) {
if (!state.editors.hasOwnProperty(key)) {
continue;
}
let tabGroup = state.editors[key];
const tabGroup = state.editors[key];
if (tabGroup) {
let newTabOrder = [...tabGroup.tabOrder];
let newRecentTabs = [...tabGroup.recentTabs];
@ -175,24 +204,28 @@ export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction
if (document.isGlobal) {
newDocs[documentId] = document;
} else {
newTabOrder = newTabOrder.filter(documentIdArg => documentIdArg !== documentId);
newRecentTabs = newRecentTabs.filter(documentIdArg => documentIdArg !== documentId);
newTabOrder = newTabOrder.filter(
documentIdArg => documentIdArg !== documentId
);
newRecentTabs = newRecentTabs.filter(
documentIdArg => documentIdArg !== documentId
);
}
});
let newTabGroup: Editor = {
const newTabGroup: Editor = {
activeDocumentId: newRecentTabs[0] || null,
documents: newDocs,
recentTabs: newRecentTabs,
tabOrder: newTabOrder
tabOrder: newTabOrder,
};
newState = {
...newState,
editors: {
...newState.editors,
[key]: newTabGroup
}
[key]: newTabGroup,
},
};
}
}
@ -207,15 +240,18 @@ export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction
const otherTabGroup = getOtherTabGroup(editorKey);
// if the document is already in another tab group, focus that one
if (tabGroupHasDocuments(state.editors[otherTabGroup])
&& state.editors[otherTabGroup].documents[action.payload.documentId]) {
const recentTabs = [...state.editors[otherTabGroup].recentTabs]
.filter(docId => docId !== action.payload.documentId);
if (
tabGroupHasDocuments(state.editors[otherTabGroup]) &&
state.editors[otherTabGroup].documents[action.payload.documentId]
) {
const recentTabs = [...state.editors[otherTabGroup].recentTabs].filter(
docId => docId !== action.payload.documentId
);
recentTabs.unshift(action.payload.documentId);
const tabGroupState: Editor = {
...state.editors[otherTabGroup],
activeDocumentId: action.payload.documentId,
recentTabs
recentTabs,
};
state = setEditorState(otherTabGroup, tabGroupState, state);
state = setActiveEditor(otherTabGroup, state);
@ -226,19 +262,30 @@ export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction
if (state.editors[editorKey].documents[action.payload.documentId]) {
newTabOrder = [...state.editors[editorKey].tabOrder];
} else {
const activeDocumentId = state.editors[state.activeEditor].activeDocumentId;
const activeIndex = state.editors[editorKey].tabOrder.indexOf(activeDocumentId);
const activeDocumentId =
state.editors[state.activeEditor].activeDocumentId;
const activeIndex = state.editors[editorKey].tabOrder.indexOf(
activeDocumentId
);
if (activeIndex != null && activeIndex !== -1) {
state.editors[editorKey].tabOrder.splice(activeIndex + 1, 0, action.payload.documentId);
state.editors[editorKey].tabOrder.splice(
activeIndex + 1,
0,
action.payload.documentId
);
newTabOrder = [...state.editors[editorKey].tabOrder];
} else {
newTabOrder = [...state.editors[editorKey].tabOrder, action.payload.documentId];
newTabOrder = [
...state.editors[editorKey].tabOrder,
action.payload.documentId,
];
}
}
// move document to top of recent tabs
const newRecentTabs = [...state.editors[editorKey].recentTabs]
.filter(docId => docId !== action.payload.documentId);
const newRecentTabs = [...state.editors[editorKey].recentTabs].filter(
docId => docId !== action.payload.documentId
);
newRecentTabs.unshift(action.payload.documentId);
// add document to tab group
@ -256,7 +303,7 @@ export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction
activeDocumentId: action.payload.documentId,
documents: newDocs,
recentTabs: newRecentTabs,
tabOrder: newTabOrder
tabOrder: newTabOrder,
};
state = setEditorState(editorKey, editorState, state);
state = setActiveEditor(editorKey, state);
@ -291,14 +338,19 @@ export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction
case EditorActions.setActiveTab: {
Constants.EditorKeys.forEach(editorKey => {
if (state.editors[editorKey] && state.editors[editorKey].documents[action.payload.documentId]) {
const recentTabs = state.editors[editorKey].recentTabs.filter(tabId => tabId !== action.payload.documentId);
if (
state.editors[editorKey] &&
state.editors[editorKey].documents[action.payload.documentId]
) {
const recentTabs = state.editors[editorKey].recentTabs.filter(
tabId => tabId !== action.payload.documentId
);
recentTabs.unshift(action.payload.documentId);
const editorState = {
...state.editors[editorKey],
activeDocumentId: action.payload.documentId,
recentTabs
recentTabs,
};
state = setEditorState(editorKey, editorState, state);
state = setActiveEditor(editorKey, state);
@ -309,14 +361,17 @@ export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction
case EditorActions.setDirtyFlag: {
Constants.EditorKeys.forEach(editorKey => {
if (state.editors[editorKey] && state.editors[editorKey].documents[action.payload.documentId]) {
if (
state.editors[editorKey] &&
state.editors[editorKey].documents[action.payload.documentId]
) {
const newDocs = deepCopySlow(state.editors[editorKey].documents);
const docToSet = newDocs[action.payload.documentId];
docToSet.dirty = action.payload.dirty;
const editorState: Editor = {
...state.editors[editorKey],
documents: newDocs
documents: newDocs,
};
state = setEditorState(editorKey, editorState, state);
}
@ -328,14 +383,19 @@ export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction
const { srcEditorKey } = action.payload;
const { destEditorKey } = action.payload;
const docToAppend = state.editors[srcEditorKey].documents[action.payload.documentId];
const docToAppend =
state.editors[srcEditorKey].documents[action.payload.documentId];
// remove any trace of document from source editor
const srcEditor = removeDocumentFromTabGroup(state.editors[srcEditorKey], action.payload.documentId);
const srcEditor = removeDocumentFromTabGroup(
state.editors[srcEditorKey],
action.payload.documentId
);
// add the document to the dest editor
const destEditor: Editor = state.editors[destEditorKey] ?
deepCopySlow(state.editors[destEditorKey]) : getNewEditor();
const destEditor: Editor = state.editors[destEditorKey]
? deepCopySlow(state.editors[destEditorKey])
: getNewEditor();
const destTabOrder = [...destEditor.tabOrder, action.payload.documentId];
const destRecentTabs = [...destEditor.recentTabs];
destRecentTabs.unshift(action.payload.documentId);
@ -362,16 +422,20 @@ export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction
if (srcEditorKey === destEditorKey) {
// only change tab order
const tabOrder = [...state.editors[srcEditorKey].tabOrder];
const srcTabIndex = tabOrder.findIndex(docId => docId === action.payload.srcTabId);
const destTabIndex1 = tabOrder.findIndex(docId => docId === action.payload.destTabId);
const srcTabIndex = tabOrder.findIndex(
docId => docId === action.payload.srcTabId
);
const destTabIndex1 = tabOrder.findIndex(
docId => docId === action.payload.destTabId
);
const destTab = tabOrder[destTabIndex1];
tabOrder[destTabIndex1] = tabOrder[srcTabIndex];
tabOrder[srcTabIndex] = destTab;
let editorState = {
const editorState = {
...state.editors[srcEditorKey],
tabOrder
tabOrder,
};
state = setEditorState(srcEditorKey, editorState, state);
@ -379,26 +443,44 @@ export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction
}
/** swapping tab into a different tab group */
const docToSwap = state.editors[srcEditorKey].documents[action.payload.srcTabId];
const docToSwap =
state.editors[srcEditorKey].documents[action.payload.srcTabId];
// remove any trace of document from source editor
const srcEditor = removeDocumentFromTabGroup(state.editors[srcEditorKey], action.payload.srcTabId);
const srcEditor = removeDocumentFromTabGroup(
state.editors[srcEditorKey],
action.payload.srcTabId
);
// add the document to the destination tab group
const destEditor: Editor = deepCopySlow(state.editors[destEditorKey]);
destEditor.documents[action.payload.srcTabId] = docToSwap;
const destRecentTabs = [...destEditor.recentTabs, action.payload.srcTabId];
const destRecentTabs = [
...destEditor.recentTabs,
action.payload.srcTabId,
];
destEditor.recentTabs = destRecentTabs;
// insert before the destination tab's position
const destTabIndex = destEditor.tabOrder.findIndex(docId => docId === action.payload.destTabId);
const destTabOrder = [...destEditor.tabOrder
.splice(0, destTabIndex + 1), action.payload.srcTabId, ...destEditor.tabOrder];
const destTabIndex = destEditor.tabOrder.findIndex(
docId => docId === action.payload.destTabId
);
const destTabOrder = [
...destEditor.tabOrder.splice(0, destTabIndex + 1),
action.payload.srcTabId,
...destEditor.tabOrder,
];
destEditor.tabOrder = destTabOrder;
if (!tabGroupHasDocuments(srcEditor) && srcEditorKey === Constants.EDITOR_KEY_PRIMARY) {
if (
!tabGroupHasDocuments(srcEditor) &&
srcEditorKey === Constants.EDITOR_KEY_PRIMARY
) {
state = setNewPrimaryEditor(destEditor, state);
} else {
state = setActiveEditor(!tabGroupHasDocuments(srcEditor) ? destEditorKey : state.activeEditor, state);
state = setActiveEditor(
!tabGroupHasDocuments(srcEditor) ? destEditorKey : state.activeEditor,
state
);
state = setEditorState(srcEditorKey, srcEditor, state);
state = setEditorState(destEditorKey, destEditor, state);
}
@ -411,16 +493,20 @@ export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction
}
case EditorActions.addDocPendingChange: {
let docsPendingChange = [
...state.docsWithPendingChanges.filter(d => d !== action.payload.documentId),
action.payload.documentId
const docsPendingChange = [
...state.docsWithPendingChanges.filter(
d => d !== action.payload.documentId
),
action.payload.documentId,
];
state = setDocsWithPendingChanges(docsPendingChange, state);
break;
}
case EditorActions.removeDocPendingChange: {
let docsPendingChange = [...state.docsWithPendingChanges].filter(d => d !== action.payload.documentId);
const docsPendingChange = [...state.docsWithPendingChanges].filter(
d => d !== action.payload.documentId
);
state = setDocsWithPendingChanges(docsPendingChange, state);
break;
}
@ -438,8 +524,12 @@ export const editor = (state: EditorState = DEFAULT_STATE, action: EditorAction
const primaryDocs = { [tabToIsolate]: docToIsolate };
// move all documents but the one being dropped to the secondary tab group
const secondaryTabOrder = state.editors[primary].tabOrder.filter(tabId => tabId !== tabToIsolate);
const secondaryRecentTabs = state.editors[primary].recentTabs.filter(tabId => tabId !== tabToIsolate);
const secondaryTabOrder = state.editors[primary].tabOrder.filter(
tabId => tabId !== tabToIsolate
);
const secondaryRecentTabs = state.editors[primary].recentTabs.filter(
tabId => tabId !== tabToIsolate
);
const secondaryDocs = state.editors[primary].documents;
delete secondaryDocs[tabToIsolate];
@ -472,47 +562,67 @@ function getNewEditor(): Editor {
activeDocumentId: null,
documents: {},
recentTabs: [],
tabOrder: []
tabOrder: [],
};
}
/** Removes all trace of a document from a tab group and returns
* the updated state, or a new editor if the tab group has no documents (empty)
*/
export function removeDocumentFromTabGroup(tabGroup: Editor, documentId: string): Editor {
const newTabOrder = [...tabGroup.tabOrder].filter(docId => docId !== documentId);
const newRecentTabs = [...tabGroup.recentTabs].filter(docId => docId !== documentId);
const newDocs = Object.assign({}, tabGroup.documents);
export function removeDocumentFromTabGroup(
tabGroup: Editor,
documentId: string
): Editor {
const newTabOrder = [...tabGroup.tabOrder].filter(
docId => docId !== documentId
);
const newRecentTabs = [...tabGroup.recentTabs].filter(
docId => docId !== documentId
);
const newDocs = { ...tabGroup.documents };
delete newDocs[documentId];
const newActiveDocumentId = newRecentTabs[0] || null;
const newTabGroup: Editor = Object.keys(newDocs).length === 0 ? getNewEditor() : {
...tabGroup,
activeDocumentId: newActiveDocumentId,
documents: newDocs,
recentTabs: newRecentTabs,
tabOrder: newTabOrder
};
const newTabGroup: Editor =
Object.keys(newDocs).length === 0
? getNewEditor()
: {
...tabGroup,
activeDocumentId: newActiveDocumentId,
documents: newDocs,
recentTabs: newRecentTabs,
tabOrder: newTabOrder,
};
return newTabGroup;
}
export function setEditorState(editorKey: string, editorState: Editor, state: EditorState): EditorState {
let newState = deepCopySlow(state);
export function setEditorState(
editorKey: string,
editorState: Editor,
state: EditorState
): EditorState {
const newState = deepCopySlow(state);
newState.editors[editorKey] = editorState;
return newState;
}
export function setActiveEditor(editorKey: string, state: EditorState): EditorState {
let newState = deepCopySlow(state);
export function setActiveEditor(
editorKey: string,
state: EditorState
): EditorState {
const newState = deepCopySlow(state);
newState.activeEditor = editorKey;
return newState;
}
/** Sets a new primary editor, and resets the secondary editor */
export function setNewPrimaryEditor(newPrimaryEditor: Editor, state: EditorState): EditorState {
let newState = deepCopySlow(state);
export function setNewPrimaryEditor(
newPrimaryEditor: Editor,
state: EditorState
): EditorState {
const newState = deepCopySlow(state);
newState.editors[Constants.EDITOR_KEY_SECONDARY] = getNewEditor();
newState.editors[Constants.EDITOR_KEY_PRIMARY] = newPrimaryEditor;
@ -520,8 +630,11 @@ export function setNewPrimaryEditor(newPrimaryEditor: Editor, state: EditorState
return newState;
}
export function setDraggingTab(dragging: boolean, state: EditorState): EditorState {
let newState = deepCopySlow(state);
export function setDraggingTab(
dragging: boolean,
state: EditorState
): EditorState {
const newState = deepCopySlow(state);
newState.draggingTab = dragging;
return newState;
@ -529,13 +642,20 @@ export function setDraggingTab(dragging: boolean, state: EditorState): EditorSta
/** Sets the secondary tab group as the primary if the primary is now empty */
export function fixupTabGroups(state: EditorState): EditorState {
if (!tabGroupHasDocuments(state.editors[Constants.EDITOR_KEY_PRIMARY])
&& tabGroupHasDocuments(state.editors[Constants.EDITOR_KEY_SECONDARY])) {
state = setNewPrimaryEditor(state.editors[Constants.EDITOR_KEY_SECONDARY], state);
if (
!tabGroupHasDocuments(state.editors[Constants.EDITOR_KEY_PRIMARY]) &&
tabGroupHasDocuments(state.editors[Constants.EDITOR_KEY_SECONDARY])
) {
state = setNewPrimaryEditor(
state.editors[Constants.EDITOR_KEY_SECONDARY],
state
);
}
if (state.activeEditor === Constants.EDITOR_KEY_SECONDARY
&& !tabGroupHasDocuments(state.editors[Constants.EDITOR_KEY_SECONDARY])) {
if (
state.activeEditor === Constants.EDITOR_KEY_SECONDARY &&
!tabGroupHasDocuments(state.editors[Constants.EDITOR_KEY_SECONDARY])
) {
state = setActiveEditor(Constants.EDITOR_KEY_PRIMARY, state);
}
@ -543,8 +663,11 @@ export function fixupTabGroups(state: EditorState): EditorState {
}
/** Sets the list of docs with pending changes */
export function setDocsWithPendingChanges(docs: string[], state: EditorState): EditorState {
let newState: EditorState = deepCopySlow(state);
export function setDocsWithPendingChanges(
docs: string[],
state: EditorState
): EditorState {
const newState: EditorState = deepCopySlow(state);
newState.docsWithPendingChanges = docs;
return newState;

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -32,12 +32,13 @@
//
import { ExplorerAction, showExplorer } from '../action/explorerActions';
import { explorer, ExplorerState } from './explorer';
describe('Explorer reducer tests', () => {
const DEFAULT_STATE: ExplorerState = {
showing: false,
sortSelectionByPanelId: {}
sortSelectionByPanelId: {},
};
it('should return unaltered state for non-matching action type', () => {

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -35,7 +35,7 @@ import {
CONNECTED_SERVICES_PANEL_ID,
ExplorerAction,
ExplorerActions,
ExplorerPayload
ExplorerPayload,
} from '../action/explorerActions';
export interface ExplorerState {
@ -47,25 +47,26 @@ export declare type SortCriteria = string;
const DEFAULT_STATE: ExplorerState = {
showing: true,
sortSelectionByPanelId: {[CONNECTED_SERVICES_PANEL_ID]: 'name'}
sortSelectionByPanelId: { [CONNECTED_SERVICES_PANEL_ID]: 'name' },
};
export function explorer(state: ExplorerState = DEFAULT_STATE, action: ExplorerAction<ExplorerPayload>)
: ExplorerState {
export function explorer(
state: ExplorerState = DEFAULT_STATE,
action: ExplorerAction<ExplorerPayload>
): ExplorerState {
switch (action.type) {
case ExplorerActions.Show:
state = { ...state, showing: action.payload.show };
break;
case ExplorerActions.Sort:
case ExplorerActions.Sort: {
const sortSelectionByPanelId = {
...state.sortSelectionByPanelId,
...action.payload.sortSelectionByPanelId
...action.payload.sortSelectionByPanelId,
};
state = { ...state, sortSelectionByPanelId };
break;
}
default:
break;

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -32,11 +32,12 @@
//
import { NavBarAction, select } from '../action/navBarActions';
import { navBar, NavBarState } from './navBar';
describe('NavBar reducer unit tests', () => {
const DEFAULT_STATE: NavBarState = {
selection: null
selection: null,
};
it('should return unaltered state for non-matching action type', () => {

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -39,15 +39,18 @@ export interface NavBarState {
}
const DEFAULT_STATE: NavBarState = {
selection: constants.NAVBAR_BOT_EXPLORER
selection: constants.NAVBAR_BOT_EXPLORER,
};
export function navBar(state: NavBarState = DEFAULT_STATE, action: NavBarAction): NavBarState {
export function navBar(
state: NavBarState = DEFAULT_STATE,
action: NavBarAction
): NavBarState {
switch (action.type) {
case NavBarActions.select: {
state = {
...state,
selection: action.payload.selection
selection: action.payload.selection,
};
break;
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,16 +31,23 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import notification, { NotificationState } from './notification';
import { finishAdd, finishRemove, finishClear, NotificationAction } from '../action/notificationActions';
import { newNotification } from '@bfemulator/app-shared';
import {
finishAdd,
finishRemove,
finishClear,
NotificationAction,
} from '../action/notificationActions';
import notification, { NotificationState } from './notification';
describe('Notification reducer tests', () => {
let defaultState: NotificationState;
beforeEach(() => {
defaultState = {
allIds: []
allIds: [],
};
});
@ -68,7 +75,7 @@ describe('Notification reducer tests', () => {
test('finishRemove', () => {
const idToRemove = 'id1';
const startingState: NotificationState = {
allIds: [idToRemove, 'id2']
allIds: [idToRemove, 'id2'],
};
const action: NotificationAction = finishRemove(idToRemove);
let endingState = notification(startingState, action);
@ -85,7 +92,7 @@ describe('Notification reducer tests', () => {
test('finishClear', () => {
const startingState: NotificationState = {
allIds: ['id1', 'id2', 'id3']
allIds: ['id1', 'id2', 'id3'],
};
const action: NotificationAction = finishClear();
const endingState = notification(startingState, action);

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,17 +31,23 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { NotificationAction, NotificationActions } from '../action/notificationActions';
import {
NotificationAction,
NotificationActions,
} from '../action/notificationActions';
export interface NotificationState {
allIds: string[];
}
const DEFAULT_STATE: NotificationState = {
allIds: []
allIds: [],
};
export function notification(state: NotificationState = DEFAULT_STATE, action: NotificationAction): NotificationState {
export function notification(
state: NotificationState = DEFAULT_STATE,
action: NotificationAction
): NotificationState {
switch (action.type) {
case NotificationActions.finishAdd: {
const { id: idToAdd } = action.payload.notification;
@ -52,7 +58,7 @@ export function notification(state: NotificationState = DEFAULT_STATE, action: N
allIds = state.allIds;
}
state = {
allIds
allIds,
};
break;
}
@ -61,14 +67,14 @@ export function notification(state: NotificationState = DEFAULT_STATE, action: N
const { id: idToRemove } = action.payload;
const allIds = state.allIds.filter(id => id !== idToRemove);
state = {
allIds
allIds,
};
break;
}
case NotificationActions.finishClear: {
state = {
allIds: []
allIds: [],
};
break;
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,27 +31,31 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { disable, enable, PresentationAction } from '../action/presentationActions';
import {
disable,
enable,
PresentationAction,
} from '../action/presentationActions';
import { presentation, PresentationState } from './presentation';
jest.mock('../../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: function mock() {
return undefined;
},
AzureLoginSuccessDialogContainer: function mock() {
return undefined;
},
BotCreationDialog: function mock() {
return undefined;
},
DialogService: { showDialog: () => Promise.resolve(true) },
SecretPromptDialog: function mock() {
return undefined;
}
}
));
AzureLoginPromptDialogContainer: function mock() {
return undefined;
},
AzureLoginSuccessDialogContainer: function mock() {
return undefined;
},
BotCreationDialog: function mock() {
return undefined;
},
DialogService: { showDialog: () => Promise.resolve(true) },
SecretPromptDialog: function mock() {
return undefined;
},
}));
describe('Presentation reducer tests', () => {
const DEFAULT_STATE: PresentationState = {
enabled: null
enabled: null,
};
it('should return unaltered state for non-matching action type', () => {

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,20 +31,26 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { PresentationAction, PresentationActions } from '../action/presentationActions';
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import {
PresentationAction,
PresentationActions,
} from '../action/presentationActions';
export interface PresentationState {
enabled: boolean;
}
const DEFAULT_STATE: PresentationState = {
enabled: false
enabled: false,
};
export const presentation = (state: PresentationState = DEFAULT_STATE, action: PresentationAction)
: PresentationState => {
export const presentation = (
state: PresentationState = DEFAULT_STATE,
action: PresentationAction
): PresentationState => {
switch (action.type) {
case PresentationActions.disable:
state = setEnabled(false, state);
@ -61,11 +67,17 @@ export const presentation = (state: PresentationState = DEFAULT_STATE, action: P
return state;
};
function setEnabled(enabled: boolean, state: PresentationState): PresentationState {
let newState = Object.assign({}, state);
function setEnabled(
enabled: boolean,
state: PresentationState
): PresentationState {
const newState = { ...state };
newState.enabled = enabled;
CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.SetFullscreen, enabled);
CommandServiceImpl.remoteCall(
SharedConstants.Commands.Electron.SetFullscreen,
enabled
);
return newState;
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,8 +1,40 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import {
CANCEL_CURRENT_PROCESS,
ProgressIndicatorAction,
ProgressIndicatorPayload,
UPDATE_PROGRESS_INDICATOR
UPDATE_PROGRESS_INDICATOR,
} from '../action/progressIndicatorActions';
export interface ProgressIndicatorState {
@ -14,20 +46,21 @@ export interface ProgressIndicatorState {
export const initialState: ProgressIndicatorState = {
progress: 0,
label: '',
canceled: false
canceled: false,
};
export function progressIndicator(
state: ProgressIndicatorState = initialState,
action: ProgressIndicatorAction<ProgressIndicatorPayload>): ProgressIndicatorState {
action: ProgressIndicatorAction<ProgressIndicatorPayload>
): ProgressIndicatorState {
switch (action.type) {
case UPDATE_PROGRESS_INDICATOR:
case UPDATE_PROGRESS_INDICATOR: {
const { label, progress } = action.payload;
return { ...state, label, progress };
}
case CANCEL_CURRENT_PROCESS:
return {...state, canceled: true};
return { ...state, canceled: true };
default:
return state;

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,11 +1,44 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { IFileService } from 'botframework-config/lib/schema';
import {
CHAT_FILES_UPDATED,
CHATS_DIRECTORY_UPDATED,
EDIT_RESOURCE,
ResourcesAction,
TRANSCRIPTS_UPDATED,
TRANSCRIPTS_DIRECTORY_UPDATED,
CHATS_DIRECTORY_UPDATED
TRANSCRIPTS_UPDATED,
} from '../action/resourcesAction';
export interface ResourcesState {
@ -21,12 +54,17 @@ const initialState: ResourcesState = {
transcriptsPath: '',
chats: [],
chatsPath: '',
resourceToRename: null
resourceToRename: null,
};
declare type ResourceActionType = ResourcesAction<IFileService | IFileService[] | string>;
declare type ResourceActionType = ResourcesAction<
IFileService | IFileService[] | string
>;
export function resources(state: ResourcesState = initialState, action: ResourceActionType): ResourcesState {
export function resources(
state: ResourcesState = initialState,
action: ResourceActionType
): ResourcesState {
switch (action.type) {
case TRANSCRIPTS_UPDATED:
return { ...state, transcripts: action.payload as IFileService[] };

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,3 +1,35 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { SwitchThemePayload, ThemeAction } from '../action/themeActions';
export interface ThemeState {
@ -6,11 +38,17 @@ export interface ThemeState {
themeComponents: string[];
}
export const initialState: ThemeState = { themeName: null, themeHref: null, themeComponents: [] };
export const initialState: ThemeState = {
themeName: null,
themeHref: null,
themeComponents: [],
};
export function theme(state: ThemeState = initialState, action: ThemeAction<SwitchThemePayload>): ThemeState {
export function theme(
state: ThemeState = initialState,
action: ThemeAction<SwitchThemePayload>
): ThemeState {
switch (action.type) {
case 'switchTheme':
return { ...state, ...action.payload };

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,39 +1,79 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { SharedConstants } from '@bfemulator/app-shared';
import { ServiceTypes } from 'botframework-config/lib/schema';
import { store } from '../store';
import {
azureArmTokenDataChanged,
beginAzureAuthWorkflow,
} from '../action/azureAuthActions';
import {
AzureLoginFailedDialogContainer,
AzureLoginPromptDialogContainer,
AzureLoginSuccessDialogContainer,
DialogService,
} from '../../ui/dialogs';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { registerCommands } from '../../commands/uiCommands';
import { azureAuthSagas } from './azureAuthSaga';
jest.mock('../../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: () => undefined,
AzureLoginSuccessDialogContainer: () => undefined,
BotCreationDialog: () => undefined,
DialogService: { showDialog: () => Promise.resolve(true) },
PostMigrationDialogContainer: () => undefined,
SecretPromptDialog: () => undefined
SecretPromptDialog: () => undefined,
}));
jest.mock('../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: () => Promise.resolve(true)
}
remoteCall: () => Promise.resolve(true),
},
}));
import { store } from '../store';
import { azureArmTokenDataChanged, beginAzureAuthWorkflow } from '../action/azureAuthActions';
import { azureAuthSagas } from './azureAuthSaga';
import {
AzureLoginFailedDialogContainer,
AzureLoginPromptDialogContainer,
AzureLoginSuccessDialogContainer,
DialogService
} from '../../ui/dialogs';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { SharedConstants } from '@bfemulator/app-shared';
import { registerCommands } from '../../commands/uiCommands';
import { ServiceTypes } from 'botframework-config/lib/schema';
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]();
const it = azureAuthSagas()
.next()
.value.FORK.args[1]();
let val = undefined;
let ct = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
const next = it.next(val);
if (next.done) {
@ -58,18 +98,21 @@ describe('The azureAuthSaga', () => {
it('should contain just 2 steps when the Azure login dialog prompt is canceled', async () => {
store.dispatch(azureArmTokenDataChanged(''));
// @ts-ignore
DialogService.showDialog = () => Promise.resolve(false);
const it = azureAuthSagas()
.next()
.value
.FORK
.args[1](beginAzureAuthWorkflow(
AzureLoginPromptDialogContainer,
{ serviceType: ServiceTypes.Luis },
AzureLoginSuccessDialogContainer,
AzureLoginFailedDialogContainer));
.value.FORK.args[1](
beginAzureAuthWorkflow(
AzureLoginPromptDialogContainer,
{ serviceType: ServiceTypes.Luis },
AzureLoginSuccessDialogContainer,
AzureLoginFailedDialogContainer
)
);
let val = undefined;
let ct = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
const next = it.next(val);
if (next.done) {
@ -91,20 +134,23 @@ describe('The azureAuthSaga', () => {
it('should contain 4 steps when the Azure login dialog prompt is confirmed but auth fails', async () => {
store.dispatch(azureArmTokenDataChanged(''));
// @ts-ignore
DialogService.showDialog = () => Promise.resolve(1);
(CommandServiceImpl as any).remoteCall = () => Promise.resolve(false);
const it = azureAuthSagas()
.next()
.value
.FORK
.args[1](beginAzureAuthWorkflow(
AzureLoginPromptDialogContainer,
{ serviceType: ServiceTypes.Luis },
AzureLoginSuccessDialogContainer,
AzureLoginFailedDialogContainer));
.value.FORK.args[1](
beginAzureAuthWorkflow(
AzureLoginPromptDialogContainer,
{ serviceType: ServiceTypes.Luis },
AzureLoginSuccessDialogContainer,
AzureLoginFailedDialogContainer
)
);
let val = undefined;
let ct = 0;
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall');
// eslint-disable-next-line no-constant-condition
while (true) {
const next = it.next(val);
if (next.done) {
@ -127,7 +173,9 @@ describe('The azureAuthSaga', () => {
if (ct === 2) {
// Login was unsuccessful
expect(val).toBe(false);
expect(remoteCallSpy).toHaveBeenCalledWith([SharedConstants.Commands.Azure.RetrieveArmToken]);
expect(remoteCallSpy).toHaveBeenCalledWith([
SharedConstants.Commands.Azure.RetrieveArmToken,
]);
}
}
}
@ -138,10 +186,12 @@ describe('The azureAuthSaga', () => {
it('should contain 6 steps when the Azure login dialog prompt is confirmed and auth succeeds', async () => {
store.dispatch(azureArmTokenDataChanged(''));
// @ts-ignore
DialogService.showDialog = () => Promise.resolve(1);
(CommandServiceImpl as any).remoteCall = args => {
switch (args[0]) {
case SharedConstants.Commands.Azure.RetrieveArmToken:
// eslint-disable-next-line typescript/camelcase
return Promise.resolve({ access_token: 'a valid access_token' });
case SharedConstants.Commands.Azure.PersistAzureLoginChanged:
@ -153,16 +203,18 @@ describe('The azureAuthSaga', () => {
};
const it = azureAuthSagas()
.next()
.value
.FORK
.args[1](beginAzureAuthWorkflow(
AzureLoginPromptDialogContainer,
{ serviceType: ServiceTypes.Luis },
AzureLoginSuccessDialogContainer,
AzureLoginFailedDialogContainer));
.value.FORK.args[1](
beginAzureAuthWorkflow(
AzureLoginPromptDialogContainer,
{ serviceType: ServiceTypes.Luis },
AzureLoginSuccessDialogContainer,
AzureLoginFailedDialogContainer
)
);
let val = undefined;
let ct = 0;
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall');
// eslint-disable-next-line no-constant-condition
while (true) {
const next = it.next(val);
if (next.done) {
@ -185,10 +237,15 @@ describe('The azureAuthSaga', () => {
if (ct === 2) {
// Login was successful
expect(val.access_token).toBe('a valid access_token');
expect(remoteCallSpy).toHaveBeenCalledWith([SharedConstants.Commands.Azure.RetrieveArmToken]);
expect(remoteCallSpy).toHaveBeenCalledWith([
SharedConstants.Commands.Azure.RetrieveArmToken,
]);
} else if (ct === 4) {
expect(val.persistLogin).toBe(true);
expect(remoteCallSpy).toHaveBeenCalledWith([SharedConstants.Commands.Azure.PersistAzureLoginChanged, 1]);
expect(remoteCallSpy).toHaveBeenCalledWith([
SharedConstants.Commands.Azure.PersistAzureLoginChanged,
1,
]);
}
}
} else if ('PUT' in val) {
@ -197,7 +254,9 @@ describe('The azureAuthSaga', () => {
ct++;
}
expect(ct).toBe(6);
expect(store.getState().azureAuth.access_token).toBe('a valid access_token');
expect(store.getState().azureAuth.access_token).toBe(
'a valid access_token'
);
});
});
});

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -30,35 +30,56 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { call, ForkEffect, put, select, takeEvery } from 'redux-saga/effects';
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { DialogService } from '../../ui/dialogs';
import {
AZURE_BEGIN_AUTH_WORKFLOW,
azureArmTokenDataChanged,
AzureAuthAction,
AzureAuthWorkflow
AzureAuthWorkflow,
} from '../action/azureAuthActions';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { SharedConstants } from '@bfemulator/app-shared';
import { RootState } from '../store';
import { DialogService } from '../../ui/dialogs';
import { AzureAuthState } from '../reducer/azureAuthReducer';
import { RootState } from '../store';
import { call, ForkEffect, put, select, takeEvery } from 'redux-saga/effects';
const getArmTokenFromState = (state: RootState) => state.azureAuth;
export function* getArmToken(action: AzureAuthAction<AzureAuthWorkflow>): IterableIterator<any> {
export function* getArmToken(
action: AzureAuthAction<AzureAuthWorkflow>
): IterableIterator<any> {
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
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;
azureAuth = yield call(CommandServiceImpl.remoteCall.bind(CommandServiceImpl), RetrieveArmToken);
const {
RetrieveArmToken,
PersistAzureLoginChanged,
} = SharedConstants.Commands.Azure;
azureAuth = yield call(
CommandServiceImpl.remoteCall.bind(CommandServiceImpl),
RetrieveArmToken
);
if (azureAuth && !('error' in azureAuth)) {
const persistLogin = yield DialogService.showDialog(action.payload.loginSuccessDialog, azureAuth);
yield call(CommandServiceImpl.remoteCall.bind(CommandServiceImpl), PersistAzureLoginChanged, persistLogin);
const persistLogin = yield DialogService.showDialog(
action.payload.loginSuccessDialog,
azureAuth
);
yield call(
CommandServiceImpl.remoteCall.bind(CommandServiceImpl),
PersistAzureLoginChanged,
persistLogin
);
} else {
yield DialogService.showDialog(action.payload.loginFailedDialog);
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,31 +31,32 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { BotActions, botHashGenerated, SetActiveBotAction } from '../action/botActions';
import { BotConfigWithPath } from '@bfemulator/sdk-shared';
import { SharedConstants } from '@bfemulator/app-shared';
import {
BotActions,
botHashGenerated,
SetActiveBotAction,
} from '../action/botActions';
import { generateBotHash } from '../botHelpers';
import {
botSagas,
browseForBot,
editorSelector,
generateHashForActiveBot
} from './botSagas';
import {
call,
put,
select,
takeEvery,
takeLatest
} from 'redux-saga/effects';
import { generateBotHash } from '../botHelpers';
import { SharedConstants } from '@bfemulator/app-shared';
generateHashForActiveBot,
} from './botSagas';
import { refreshConversationMenu } from './sharedSagas';
import { call, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
jest.mock('../../ui/dialogs', () => ({}));
jest.mock('../store', () => ({
get store() {
return {};
}
return {};
},
}));
const mockSharedConstants = SharedConstants;
@ -68,63 +69,51 @@ jest.mock('../../platform/commands/commandServiceImpl', () => ({
mockLocalCommandsCalled.push({ commandName, args: args });
switch (commandName) {
case mockSharedConstants.Commands.Bot.OpenBrowse:
case mockSharedConstants.Commands.Bot.OpenBrowse:
return Promise.resolve(true);
default:
default:
return Promise.resolve(true);
}
}
},
remoteCall: async (commandName: string, ... args: any[]) => {
mockRemoteCommandsCalled.push({ commandName, args: args});
remoteCall: async (commandName: string, ...args: any[]) => {
mockRemoteCommandsCalled.push({ commandName, args: args });
return Promise.resolve(true);
}
}
},
},
}));
describe('The botSagas', () => {
beforeEach(() => {
mockRemoteCommandsCalled = [];
mockLocalCommandsCalled = [];
});
it('should initialize the root saga', () => {
let gen = botSagas();
const gen = botSagas();
const browseForBotYield = gen.next().value;
expect(browseForBotYield).toEqual(
takeEvery(
BotActions.browse,
browseForBot
)
takeEvery(BotActions.browse, browseForBot)
);
const generateBotHashYield = gen.next().value;
expect(generateBotHashYield).toEqual(
takeEvery(
BotActions.setActive,
generateHashForActiveBot
)
takeEvery(BotActions.setActive, generateHashForActiveBot)
);
const refreshConversationMenuYield = gen.next().value;
expect(refreshConversationMenuYield).toEqual(
takeLatest(
[
BotActions.setActive,
BotActions.load,
BotActions.close
],
[BotActions.setActive, BotActions.load, BotActions.close],
refreshConversationMenu
)
);
expect(gen.next().done).toBe(true);
});
it('should generate a hash for an active bot', () => {
@ -134,14 +123,14 @@ describe('The botSagas', () => {
padlock: null,
services: [],
path: '/some/Path/something',
version: '0.1'
version: '0.1',
};
const setActiveBotAction: SetActiveBotAction = {
type: BotActions.setActive,
payload: {
bot: botConfigPath
}
bot: botConfigPath,
},
};
const gen = generateHashForActiveBot(setActiveBotAction);
const generatedHash = gen.next().value;

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,13 +31,26 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { call, ForkEffect, put, takeEvery, takeLatest } from 'redux-saga/effects';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { SharedConstants } from '@bfemulator/app-shared';
import { BotActions, botHashGenerated, SetActiveBotAction } from '../action/botActions';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import {
BotActions,
botHashGenerated,
SetActiveBotAction,
} from '../action/botActions';
import { generateBotHash } from '../botHelpers';
import { refreshConversationMenu } from './sharedSagas';
import {
call,
ForkEffect,
put,
takeEvery,
takeLatest,
} from 'redux-saga/effects';
/** Opens up native open file dialog to browse for a .bot file */
export function* browseForBot(): IterableIterator<any> {
yield CommandServiceImpl.call(SharedConstants.Commands.Bot.OpenBrowse)
@ -45,7 +58,9 @@ export function* browseForBot(): IterableIterator<any> {
.catch(_err => null);
}
export function* generateHashForActiveBot(action: SetActiveBotAction): IterableIterator<any> {
export function* generateHashForActiveBot(
action: SetActiveBotAction
): IterableIterator<any> {
const { bot } = action.payload;
const generatedHash = yield call(generateBotHash, bot);
yield put(botHashGenerated(generatedHash));
@ -55,11 +70,7 @@ export function* botSagas(): IterableIterator<ForkEffect> {
yield takeEvery(BotActions.browse, browseForBot);
yield takeEvery(BotActions.setActive, generateHashForActiveBot);
yield takeLatest(
[
BotActions.setActive,
BotActions.load,
BotActions.close
],
[BotActions.setActive, BotActions.load, BotActions.close],
refreshConversationMenu
);
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,16 +31,23 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { call, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
import { checkActiveDocForPendingChanges, editorSagas, promptUserToReloadDocument } from './editorSagas';
import { EditorActions, removeDocPendingChange } from '../action/editorActions';
import { SharedConstants } from '@bfemulator/app-shared';
import { EditorActions, removeDocPendingChange } from '../action/editorActions';
import {
checkActiveDocForPendingChanges,
editorSagas,
promptUserToReloadDocument,
} from './editorSagas';
import { refreshConversationMenu, editorSelector } from './sharedSagas';
import { call, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
jest.mock('../store', () => ({
get store() {
return {};
}
get store() {
return {};
},
}));
jest.mock('../../ui/dialogs', () => ({}));
@ -48,145 +55,150 @@ jest.mock('../../ui/dialogs', () => ({}));
const mockSharedConstants = SharedConstants;
let mockRemoteCommandsCalled = [];
let mockLocalCommandsCalled = [];
let mockMessageResponse = false;
const mockMessageResponse = false;
jest.mock('../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
call: async (commandName: string, ...args: any[]) => {
mockLocalCommandsCalled.push({ commandName, args: args });
},
remoteCall: async (commandName: string, ...args: any[]) => {
mockRemoteCommandsCalled.push({ commandName, args: args });
CommandServiceImpl: {
call: async (commandName: string, ...args: any[]) => {
mockLocalCommandsCalled.push({ commandName, args: args });
},
remoteCall: async (commandName: string, ...args: any[]) => {
mockRemoteCommandsCalled.push({ commandName, args: args });
switch (commandName) {
case mockSharedConstants.Commands.Electron.ShowMessageBox:
if (mockMessageResponse) {
return Promise.resolve(true);
} else {
return Promise.resolve(false);
}
default:
return Promise.resolve(true);
}
}
}
switch (commandName) {
case mockSharedConstants.Commands.Electron.ShowMessageBox:
if (mockMessageResponse) {
return Promise.resolve(true);
} else {
return Promise.resolve(false);
}
default:
return Promise.resolve(true);
}
},
},
}));
describe('The Editor Sagas', () => {
beforeEach(() => {
mockRemoteCommandsCalled = [];
mockLocalCommandsCalled = [];
});
beforeEach(() => {
mockRemoteCommandsCalled = [];
mockLocalCommandsCalled = [];
});
it('should check the active doc for pending changes', () => {
const gen = checkActiveDocForPendingChanges();
const stateData = gen.next().value;
it('should check the active doc for pending changes', () => {
const gen = checkActiveDocForPendingChanges();
const stateData = gen.next().value;
expect(stateData).toEqual(select(editorSelector));
expect(stateData).toEqual(select(editorSelector));
const mockActiveDocId = 'doc1';
const mockEditorState = {
editors: {
someEditor: {
activeDocumentId: mockActiveDocId
}
},
activeEditor: 'someEditor',
docsWithPendingChanges: [mockActiveDocId]
};
// should return the inner generator that we delegate to
const innerGen = gen.next(mockEditorState).value;
expect(innerGen).toEqual(call(promptUserToReloadDocument, mockActiveDocId));
const mockActiveDocId = 'doc1';
const mockEditorState = {
editors: {
someEditor: {
activeDocumentId: mockActiveDocId,
},
},
activeEditor: 'someEditor',
docsWithPendingChanges: [mockActiveDocId],
};
// should return the inner generator that we delegate to
const innerGen = gen.next(mockEditorState).value;
expect(innerGen).toEqual(call(promptUserToReloadDocument, mockActiveDocId));
expect(gen.next().done).toBe(true);
});
expect(gen.next().done).toBe(true);
});
it('should prompt the user to reload the document when the file is chatdown', () => {
const mockChatFileName = 'doc1.chat';
const options = {
buttons: ['Cancel', 'Reload'],
title: 'File change detected',
message: 'We have detected a change in this file on disk. Would you like to reload it in the Emulator?'
};
const gen = promptUserToReloadDocument(mockChatFileName);
it('should prompt the user to reload the document when the file is chatdown', () => {
const mockChatFileName = 'doc1.chat';
const options = {
buttons: ['Cancel', 'Reload'],
title: 'File change detected',
message:
'We have detected a change in this file on disk. Would you like to reload it in the Emulator?',
};
const gen = promptUserToReloadDocument(mockChatFileName);
gen.next();
gen.next();
const { ShowMessageBox } = SharedConstants.Commands.Electron;
expect(mockRemoteCommandsCalled).toHaveLength(1);
expect(mockRemoteCommandsCalled[0].commandName).toEqual(ShowMessageBox);
expect(mockRemoteCommandsCalled[0].args[0]).toEqual(options);
expect(gen.next(true).value).toEqual(put(removeDocPendingChange(mockChatFileName)));
gen.next();
const { OpenChatFile } = SharedConstants.Commands.Emulator;
const { ShowMessageBox } = SharedConstants.Commands.Electron;
expect(mockRemoteCommandsCalled).toHaveLength(1);
expect(mockRemoteCommandsCalled[0].commandName).toEqual(ShowMessageBox);
expect(mockRemoteCommandsCalled[0].args[0]).toEqual(options);
expect(gen.next(true).value).toEqual(
put(removeDocPendingChange(mockChatFileName))
);
expect(mockLocalCommandsCalled).toHaveLength(1);
expect(mockLocalCommandsCalled[0].commandName).toEqual(OpenChatFile);
expect(mockLocalCommandsCalled[0].args[0]).toBe(mockChatFileName);
expect(mockLocalCommandsCalled[0].args[1]).toBe(true);
expect(gen.next().done).toBe(true);
});
gen.next();
it('should prompt the user to reload the document when the file is a transcript', () => {
const mockTranscriptFile = 'doc2.transcript';
const options = {
buttons: ['Cancel', 'Reload'],
title: 'File change detected',
message: 'We have detected a change in this file on disk. Would you like to reload it in the Emulator?'
};
const gen = promptUserToReloadDocument(mockTranscriptFile);
const { OpenChatFile } = SharedConstants.Commands.Emulator;
gen.next();
expect(mockLocalCommandsCalled).toHaveLength(1);
expect(mockLocalCommandsCalled[0].commandName).toEqual(OpenChatFile);
expect(mockLocalCommandsCalled[0].args[0]).toBe(mockChatFileName);
expect(mockLocalCommandsCalled[0].args[1]).toBe(true);
expect(gen.next().done).toBe(true);
});
const { ShowMessageBox } = SharedConstants.Commands.Electron;
expect(mockRemoteCommandsCalled).toHaveLength(1);
expect(mockRemoteCommandsCalled[0].commandName).toEqual(ShowMessageBox);
expect(mockRemoteCommandsCalled[0].args[0]).toEqual(options);
expect(gen.next(true).value).toEqual(put(removeDocPendingChange(mockTranscriptFile)));
gen.next();
const { ReloadTranscript } = SharedConstants.Commands.Emulator;
it('should prompt the user to reload the document when the file is a transcript', () => {
const mockTranscriptFile = 'doc2.transcript';
const options = {
buttons: ['Cancel', 'Reload'],
title: 'File change detected',
message:
'We have detected a change in this file on disk. Would you like to reload it in the Emulator?',
};
const gen = promptUserToReloadDocument(mockTranscriptFile);
expect(mockLocalCommandsCalled).toHaveLength(1);
expect(mockLocalCommandsCalled[0].commandName).toEqual(ReloadTranscript);
expect(mockLocalCommandsCalled[0].args[0]).toBe(mockTranscriptFile);
expect(gen.next().done).toBe(true);
});
gen.next();
it('should initialize the root saga', () => {
let gen = editorSagas();
const checkActiveDocsYield = gen.next().value;
expect(checkActiveDocsYield).toEqual(
takeEvery(
[
EditorActions.addDocPendingChange,
EditorActions.setActiveEditor,
EditorActions.setActiveTab,
EditorActions.open
],
checkActiveDocForPendingChanges
)
);
const { ShowMessageBox } = SharedConstants.Commands.Electron;
expect(mockRemoteCommandsCalled).toHaveLength(1);
expect(mockRemoteCommandsCalled[0].commandName).toEqual(ShowMessageBox);
expect(mockRemoteCommandsCalled[0].args[0]).toEqual(options);
expect(gen.next(true).value).toEqual(
put(removeDocPendingChange(mockTranscriptFile))
);
gen.next();
const refreshConversationMenuYield = gen.next().value;
const { ReloadTranscript } = SharedConstants.Commands.Emulator;
expect(refreshConversationMenuYield).toEqual(
takeLatest(
[
EditorActions.close,
EditorActions.open,
EditorActions.setActiveEditor,
EditorActions.setActiveTab
],
refreshConversationMenu
)
);
expect(mockLocalCommandsCalled).toHaveLength(1);
expect(mockLocalCommandsCalled[0].commandName).toEqual(ReloadTranscript);
expect(mockLocalCommandsCalled[0].args[0]).toBe(mockTranscriptFile);
expect(gen.next().done).toBe(true);
});
expect(gen.next().done).toBe(true);
});
it('should initialize the root saga', () => {
const gen = editorSagas();
const checkActiveDocsYield = gen.next().value;
expect(checkActiveDocsYield).toEqual(
takeEvery(
[
EditorActions.addDocPendingChange,
EditorActions.setActiveEditor,
EditorActions.setActiveTab,
EditorActions.open,
],
checkActiveDocForPendingChanges
)
);
const refreshConversationMenuYield = gen.next().value;
expect(refreshConversationMenuYield).toEqual(
takeLatest(
[
EditorActions.close,
EditorActions.open,
EditorActions.setActiveEditor,
EditorActions.setActiveTab,
],
refreshConversationMenu
)
);
expect(gen.next().done).toBe(true);
});
});

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,27 +31,47 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { call, ForkEffect, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
import {
isChatFile,
isTranscriptFile,
SharedConstants,
} from '@bfemulator/app-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { EditorActions, removeDocPendingChange } from '../action/editorActions';
import { isChatFile, isTranscriptFile, SharedConstants } from '@bfemulator/app-shared';
import { editorSelector, refreshConversationMenu } from './sharedSagas';
export function* promptUserToReloadDocument(filename: string): IterableIterator<any> {
import {
call,
ForkEffect,
put,
select,
takeEvery,
takeLatest,
} from 'redux-saga/effects';
export function* promptUserToReloadDocument(
filename: string
): IterableIterator<any> {
const { Commands } = SharedConstants;
const options = {
buttons: ['Cancel', 'Reload'],
title: 'File change detected',
message: 'We have detected a change in this file on disk. Would you like to reload it in the Emulator?'
message:
'We have detected a change in this file on disk. Would you like to reload it in the Emulator?',
};
const confirmation = yield CommandServiceImpl.remoteCall(Commands.Electron.ShowMessageBox, options);
const confirmation = yield CommandServiceImpl.remoteCall(
Commands.Electron.ShowMessageBox,
options
);
// clear the doc of pending changes
yield put(removeDocPendingChange(filename));
// reload the file, otherwise proceed without reloading
const { OpenChatFile, ReloadTranscript } = SharedConstants.Commands.Emulator;
if (confirmation) {
if (isChatFile(filename)) {
yield CommandServiceImpl.call(OpenChatFile, filename, true);
@ -65,7 +85,8 @@ export function* checkActiveDocForPendingChanges(): IterableIterator<any> {
const stateData = yield select(editorSelector);
// if currently active document has pending changes, prompt the user to reload it
const activeDocId = stateData.editors[stateData.activeEditor].activeDocumentId;
const activeDocId =
stateData.editors[stateData.activeEditor].activeDocumentId;
if (stateData.docsWithPendingChanges.some(doc => doc === activeDocId)) {
// TODO: active document ID is not always the filename
yield call(promptUserToReloadDocument, activeDocId);
@ -81,7 +102,7 @@ export function* editorSagas(): IterableIterator<ForkEffect> {
EditorActions.addDocPendingChange,
EditorActions.setActiveEditor,
EditorActions.setActiveTab,
EditorActions.open
EditorActions.open,
],
checkActiveDocForPendingChanges
);

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,27 +1,65 @@
import { bot } from '../reducer/bot';
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { applyMiddleware, combineReducers, createStore } from 'redux';
import sagaMiddlewareFactory from 'redux-saga';
import { endpointSagas } from './endpointSagas';
import { load, setActive } from '../action/botActions';
import { launchEndpointEditor, openEndpointExplorerContextMenu } from '../action/endpointServiceActions';
import { Component } from 'react';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { DialogService } from '../../ui/dialogs/service';
import { SharedConstants } from '@bfemulator/app-shared';
const sagaMiddleWare = sagaMiddlewareFactory();
const mockStore = createStore(combineReducers({ bot }), {}, applyMiddleware(sagaMiddleWare));
sagaMiddleWare.run(endpointSagas);
const mockComponentClass = class extends Component<{}, {}> {
import { bot } from '../reducer/bot';
import { load, setActive } from '../action/botActions';
import {
launchEndpointEditor,
openEndpointExplorerContextMenu,
} from '../action/endpointServiceActions';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { DialogService } from '../../ui/dialogs/service';
};
jest.mock('../store', () => ({
get store() {
return mockStore;
}
})
import { endpointSagas } from './endpointSagas';
const sagaMiddleWare = sagaMiddlewareFactory();
const mockStore = createStore(
combineReducers({ bot }),
{},
applyMiddleware(sagaMiddleWare)
);
let mockBot = JSON.parse(`{
sagaMiddleWare.run(endpointSagas);
const mockComponentClass = class extends Component<{}, {}> {};
jest.mock('../store', () => ({
get store() {
return mockStore;
},
}));
const mockBot = JSON.parse(`{
"name": "TestBot",
"description": "",
"padlock": "",
@ -45,26 +83,24 @@ let mockBot = JSON.parse(`{
}`);
jest.mock('../../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: function mock() {
return undefined;
},
AzureLoginSuccessDialogContainer: function mock() {
return undefined;
},
BotCreationDialog: function mock() {
return undefined;
},
DialogService: {
showDialog: () => Promise.resolve(mockBot.services)
},
SecretPromptDialog: function mock() {
return undefined;
}
}
));
AzureLoginPromptDialogContainer: function mock() {
return undefined;
},
AzureLoginSuccessDialogContainer: function mock() {
return undefined;
},
BotCreationDialog: function mock() {
return undefined;
},
DialogService: {
showDialog: () => Promise.resolve(mockBot.services),
},
SecretPromptDialog: function mock() {
return undefined;
},
}));
describe('The endpoint sagas', () => {
beforeEach(() => {
mockStore.dispatch(load([mockBot]));
mockStore.dispatch(setActive(mockBot));
@ -72,11 +108,21 @@ describe('The endpoint sagas', () => {
it('should launch the endpoint editor and execute a command to save the edited services', async () => {
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall');
const dialogServiceSpy = jest.spyOn(DialogService, 'showDialog').mockResolvedValue(mockBot.services);
await mockStore.dispatch(launchEndpointEditor(mockComponentClass, mockBot.services[0]));
const dialogServiceSpy = jest
.spyOn(DialogService, 'showDialog')
.mockResolvedValue(mockBot.services);
await mockStore.dispatch(
launchEndpointEditor(mockComponentClass, mockBot.services[0])
);
const { AddOrUpdateService } = SharedConstants.Commands.Bot;
expect(dialogServiceSpy).toHaveBeenCalledWith(mockComponentClass, { endpointService: mockBot.services[0] });
expect(remoteCallSpy).toHaveBeenCalledWith(AddOrUpdateService, 'abs', mockBot.services[1]);
expect(dialogServiceSpy).toHaveBeenCalledWith(mockComponentClass, {
endpointService: mockBot.services[0],
});
expect(remoteCallSpy).toHaveBeenCalledWith(
AddOrUpdateService,
'abs',
mockBot.services[1]
);
});
describe(' openEndpointContextMenu', () => {
@ -84,28 +130,54 @@ describe('The endpoint sagas', () => {
{ label: 'Open in Emulator', id: 'open' },
{ label: 'Open in portal', id: 'absLink', enabled: jasmine.any(Boolean) },
{ label: 'Edit configuration', id: 'edit' },
{ label: 'Remove', id: 'forget' }
{ label: 'Remove', id: 'forget' },
];
const { DisplayContextMenu, ShowMessageBox } = SharedConstants.Commands.Electron;
const {
DisplayContextMenu,
ShowMessageBox,
} = SharedConstants.Commands.Electron;
const { NewLiveChat } = SharedConstants.Commands.Emulator;
it('should launch the endpoint editor when that menu option is chosen', () => {
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue({ id: 'edit' });
const dialogServiceSpy = jest.spyOn(DialogService, 'showDialog').mockResolvedValue(mockBot.services);
mockStore.dispatch(openEndpointExplorerContextMenu(mockComponentClass, mockBot.services[0]));
const commandServiceSpy = jest
.spyOn(CommandServiceImpl, 'remoteCall')
.mockResolvedValue({ id: 'edit' });
const dialogServiceSpy = jest
.spyOn(DialogService, 'showDialog')
.mockResolvedValue(mockBot.services);
mockStore.dispatch(
openEndpointExplorerContextMenu(mockComponentClass, mockBot.services[0])
);
expect(commandServiceSpy).toHaveBeenCalledWith(DisplayContextMenu, menuItems);
expect(dialogServiceSpy).toHaveBeenCalledWith(mockComponentClass, { endpointService: mockBot.services[0] });
expect(commandServiceSpy).toHaveBeenCalledWith(
DisplayContextMenu,
menuItems
);
expect(dialogServiceSpy).toHaveBeenCalledWith(mockComponentClass, {
endpointService: mockBot.services[0],
});
});
it('should open a deep link when that menu option is chosen', async () => {
const commandServiceRemoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall')
const commandServiceRemoteCallSpy = jest
.spyOn(CommandServiceImpl, 'remoteCall')
.mockResolvedValue({ id: 'open' });
const commandServiceCallSpy = jest.spyOn(CommandServiceImpl, 'call').mockResolvedValue(true);
const commandServiceCallSpy = jest
.spyOn(CommandServiceImpl, 'call')
.mockResolvedValue(true);
await mockStore.dispatch(openEndpointExplorerContextMenu(mockComponentClass, mockBot.services[0]));
expect(commandServiceRemoteCallSpy).toHaveBeenCalledWith(DisplayContextMenu, menuItems);
expect(commandServiceCallSpy).toHaveBeenCalledWith(NewLiveChat, mockBot.services[0], false);
await mockStore.dispatch(
openEndpointExplorerContextMenu(mockComponentClass, mockBot.services[0])
);
expect(commandServiceRemoteCallSpy).toHaveBeenCalledWith(
DisplayContextMenu,
menuItems
);
expect(commandServiceCallSpy).toHaveBeenCalledWith(
NewLiveChat,
mockBot.services[0],
false
);
});
it('should forget the service when that menu item is chosen', async () => {
@ -118,28 +190,32 @@ describe('The endpoint sagas', () => {
return true;
};
const { RemoveService } = SharedConstants.Commands.Bot;
await mockStore.dispatch(openEndpointExplorerContextMenu(mockComponentClass, mockBot.services[0]));
await mockStore.dispatch(
openEndpointExplorerContextMenu(mockComponentClass, mockBot.services[0])
);
await Promise.resolve();
expect(remoteCallArgs[0]).toEqual({ commandName: DisplayContextMenu, args: [menuItems] });
expect(remoteCallArgs[0]).toEqual({
commandName: DisplayContextMenu,
args: [menuItems],
});
expect(remoteCallArgs[1]).toEqual({
commandName: ShowMessageBox, args: [
commandName: ShowMessageBox,
args: [
true,
{
'buttons': [
'Cancel',
'OK'
],
'cancelId': 0,
'defaultId': 1,
'message': 'Remove endpoint https://testbot.botframework.com/api/messagesv3. Are you sure?',
'type': 'question'
}
buttons: ['Cancel', 'OK'],
cancelId: 0,
defaultId: 1,
message:
'Remove endpoint https://testbot.botframework.com/api/messagesv3. Are you sure?',
type: 'question',
},
],
});
expect(remoteCallArgs[2]).toEqual({
commandName: RemoveService,
args: [mockBot.services[0].type, mockBot.services[0].id]
args: [mockBot.services[0].type, mockBot.services[0].id],
});
});
});

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -32,9 +32,13 @@
//
import { SharedConstants } from '@bfemulator/app-shared';
import { IBotService, IEndpointService, ServiceTypes } from 'botframework-config/lib/schema';
import {
IBotService,
IEndpointService,
ServiceTypes,
} from 'botframework-config/lib/schema';
import { ComponentClass } from 'react';
import { call, ForkEffect, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { DialogService } from '../../ui/dialogs/service';
import { openServiceDeepLink } from '../action/connectedServiceActions';
@ -44,20 +48,36 @@ import {
EndpointServicePayload,
LAUNCH_ENDPOINT_EDITOR,
OPEN_ENDPOINT_CONTEXT_MENU,
OPEN_ENDPOINT_IN_EMULATOR
OPEN_ENDPOINT_IN_EMULATOR,
} from '../action/endpointServiceActions';
import { RootState } from '../store';
import {
call,
ForkEffect,
put,
select,
takeEvery,
takeLatest,
} from 'redux-saga/effects';
const getConnectedAbs = (state: RootState, endpointAppId: string) => {
return (state.bot.activeBot.services || []).find(service => {
return service.type === ServiceTypes.Bot && (service as IBotService).appId === endpointAppId;
return (
service.type === ServiceTypes.Bot &&
(service as IBotService).appId === endpointAppId
);
});
};
function* launchEndpointEditor(action: EndpointServiceAction<EndpointEditorPayload>): IterableIterator<any> {
function* launchEndpointEditor(
action: EndpointServiceAction<EndpointEditorPayload>
): IterableIterator<any> {
const { endpointEditorComponent, endpointService = {} } = action.payload;
const servicesToUpdate = yield DialogService.showDialog<ComponentClass<any>, IEndpointService[]>
(endpointEditorComponent, { endpointService });
const servicesToUpdate = yield DialogService.showDialog<
ComponentClass<any>,
IEndpointService[]
>(endpointEditorComponent, { endpointService });
if (servicesToUpdate) {
const { AddOrUpdateService, RemoveService } = SharedConstants.Commands.Bot;
@ -68,25 +88,43 @@ function* launchEndpointEditor(action: EndpointServiceAction<EndpointEditorPaylo
if (service.type === ServiceTypes.Bot) {
// Since we could end up with an invalid ABS
// naively validate and remove it if all fields are missing
const { serviceName, resourceGroup, subscriptionId, tenantId } = service as IBotService;
shouldBeRemoved = !serviceName && !resourceGroup && !subscriptionId && !tenantId;
const {
serviceName,
resourceGroup,
subscriptionId,
tenantId,
} = service as IBotService;
shouldBeRemoved =
!serviceName && !resourceGroup && !subscriptionId && !tenantId;
}
yield CommandServiceImpl.remoteCall(shouldBeRemoved ? RemoveService : AddOrUpdateService, service.type, service);
yield CommandServiceImpl.remoteCall(
shouldBeRemoved ? RemoveService : AddOrUpdateService,
service.type,
service
);
}
}
}
function* openEndpointContextMenu(action: EndpointServiceAction<EndpointServicePayload | EndpointEditorPayload>)
: IterableIterator<any> {
const connectedAbs = yield select<RootState, string>(getConnectedAbs, action.payload.endpointService.appId);
function* openEndpointContextMenu(
action: EndpointServiceAction<EndpointServicePayload | EndpointEditorPayload>
): IterableIterator<any> {
const connectedAbs = yield select<RootState, string>(
getConnectedAbs,
action.payload.endpointService.appId
);
const menuItems = [
{ label: 'Open in Emulator', id: 'open' },
{ label: 'Open in portal', id: 'absLink', enabled: !!connectedAbs },
{ label: 'Edit configuration', id: 'edit' },
{ label: 'Remove', id: 'forget' }
{ label: 'Remove', id: 'forget' },
];
const { DisplayContextMenu } = SharedConstants.Commands.Electron;
const response = yield call(CommandServiceImpl.remoteCall.bind(CommandServiceImpl), DisplayContextMenu, menuItems);
const response = yield call(
CommandServiceImpl.remoteCall.bind(CommandServiceImpl),
DisplayContextMenu,
menuItems
);
switch (response.id) {
case 'edit':
yield* launchEndpointEditor(action);
@ -104,27 +142,47 @@ function* openEndpointContextMenu(action: EndpointServiceAction<EndpointServiceP
yield* removeEndpointServiceFromActiveBot(action.payload.endpointService);
break;
default: // canceled context menu
default:
// canceled context menu
return;
}
}
function* openEndpointInEmulator(action: EndpointServiceAction<EndpointServicePayload>): IterableIterator<any> {
const { endpointService, focusExistingChatIfAvailable: focusExisting = false } = action.payload;
return CommandServiceImpl.call(SharedConstants.Commands.Emulator.NewLiveChat, endpointService, focusExisting);
// eslint-disable-next-line require-yield
function* openEndpointInEmulator(
action: EndpointServiceAction<EndpointServicePayload>
): IterableIterator<any> {
const {
endpointService,
focusExistingChatIfAvailable: focusExisting = false,
} = action.payload;
return CommandServiceImpl.call(
SharedConstants.Commands.Emulator.NewLiveChat,
endpointService,
focusExisting
);
}
function* removeEndpointServiceFromActiveBot(endpointService: IEndpointService): IterableIterator<any> {
const result = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.ShowMessageBox, true, {
type: 'question',
buttons: ['Cancel', 'OK'],
defaultId: 1,
message: `Remove endpoint ${endpointService.name}. Are you sure?`,
cancelId: 0,
});
function* removeEndpointServiceFromActiveBot(
endpointService: IEndpointService
): IterableIterator<any> {
const result = yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Electron.ShowMessageBox,
true,
{
type: 'question',
buttons: ['Cancel', 'OK'],
defaultId: 1,
message: `Remove endpoint ${endpointService.name}. Are you sure?`,
cancelId: 0,
}
);
if (result) {
yield CommandServiceImpl
.remoteCall(SharedConstants.Commands.Bot.RemoveService, endpointService.type, endpointService.id);
yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Bot.RemoveService,
endpointService.type,
endpointService.id
);
}
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,14 +31,14 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { azureAuthSagas } from './azureAuthSaga';
import { botSagas } from './botSagas';
import { editorSagas } from './editorSagas';
import { endpointSagas } from './endpointSagas';
import { servicesExplorerSagas } from './servicesExplorerSagas';
import { navBarSagas } from './navBarSagas';
import { notificationSagas } from './notificationSagas';
import { azureAuthSagas } from './azureAuthSaga';
import { resourceSagas } from './resourcesSagas';
import { servicesExplorerSagas } from './servicesExplorerSagas';
import { welcomePageSagas } from './welcomePageSagas';
export const applicationSagas = [
@ -50,5 +50,5 @@ export const applicationSagas = [
navBarSagas,
notificationSagas,
resourceSagas,
welcomePageSagas
welcomePageSagas,
];

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,31 +31,32 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { markNotificationsAsRead } from './navBarSagas';
import { select } from '../action/navBarActions';
import * as Constants from '../../constants';
import { markAllAsRead } from '../action/notificationActions';
import { markNotificationsAsRead } from './navBarSagas';
import { put } from 'redux-saga/effects';
jest.mock('../../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: function mock() {
return undefined;
},
AzureLoginSuccessDialogContainer: function mock() {
return undefined;
},
BotCreationDialog: function mock() {
return undefined;
},
DialogService: { showDialog: () => Promise.resolve(true) },
SecretPromptDialog: function mock() {
return undefined;
}
}
));
AzureLoginPromptDialogContainer: function mock() {
return undefined;
},
AzureLoginSuccessDialogContainer: function mock() {
return undefined;
},
BotCreationDialog: function mock() {
return undefined;
},
DialogService: { showDialog: () => Promise.resolve(true) },
SecretPromptDialog: function mock() {
return undefined;
},
}));
jest.mock('../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: () => Promise.resolve(true)
}
remoteCall: () => Promise.resolve(true),
},
}));
describe('Nav bar sagas', () => {
test('markNotificationsAsRead()', () => {

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,13 +31,16 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { ForkEffect, put, takeEvery, } from 'redux-saga/effects';
import * as Constants from '../../constants';
import { NavBarActions, SelectNavBarAction } from '../action/navBarActions';
import { markAllAsRead } from '../action/notificationActions';
import * as Constants from '../../constants';
import { ForkEffect, put, takeEvery } from 'redux-saga/effects';
/** Marks all notifications as read if the notifications pane is opened */
export function* markNotificationsAsRead(action: SelectNavBarAction): IterableIterator<any> {
export function* markNotificationsAsRead(
action: SelectNavBarAction
): IterableIterator<any> {
const navBarSelection = action.payload.selection;
if (navBarSelection === Constants.NAVBAR_NOTIFICATIONS) {
yield put(markAllAsRead());

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,23 +31,26 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { put } from 'redux-saga/effects';
import { newNotification, NotificationType } from '@bfemulator/app-shared';
import { NotificationManager } from '../../notificationManager';
import {
beginAdd,
finishAdd,
finishClear,
beginRemove,
finishRemove
finishRemove,
} from '../action/notificationActions';
import {
addNotification,
clearNotifications,
removeNotification,
markAllAsRead
markAllAsRead,
} from './notificationSagas';
import { put } from 'redux-saga/effects';
describe('Notification sagas', () => {
test('addNotification()', () => {
const notification = newNotification('someMessage', NotificationType.Info);

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -34,18 +34,21 @@
import { NotificationManager } from '../../notificationManager';
import {
BeginAddNotificationAction,
NotificationActions,
BeginRemoveNotificationAction,
finishAdd,
finishClear,
finishRemove
finishRemove,
NotificationActions,
} from '../action/notificationActions';
import { ForkEffect, takeEvery, put } from 'redux-saga/effects';
import { ForkEffect, put, takeEvery } from 'redux-saga/effects';
/** Adds a notification to the notification manager then
* adds it to the state store
*/
export function* addNotification(action: BeginAddNotificationAction): IterableIterator<any> {
export function* addNotification(
action: BeginAddNotificationAction
): IterableIterator<any> {
const { notification } = action.payload;
NotificationManager.set(notification.id, notification);
yield put(finishAdd(notification));
@ -62,7 +65,9 @@ export function* clearNotifications(): IterableIterator<any> {
/** Removes a single notification from the notification manager then
* removes it from the state store
*/
export function* removeNotification(action: BeginRemoveNotificationAction): IterableIterator<any> {
export function* removeNotification(
action: BeginRemoveNotificationAction
): IterableIterator<any> {
const { id: notificationId } = action.payload;
NotificationManager.delete(notificationId);
yield put(finishRemove(notificationId));

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,32 +1,71 @@
import { resources } from '../reducer/resourcesReducer';
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { applyMiddleware, combineReducers, createStore } from 'redux';
import { resourceSagas } from './resourcesSagas';
import { BotConfigWithPathImpl } from '@bfemulator/sdk-shared';
import { ServiceTypes } from 'botframework-config/lib/schema';
import sagaMiddlewareFactory from 'redux-saga';
import { SharedConstants } from '@bfemulator/app-shared/built';
import { Component } from 'react';
import {
openContextMenuForResource,
openResource,
openResourcesSettings,
renameResource
renameResource,
} from '../action/resourcesAction';
import { SharedConstants } from '@bfemulator/app-shared/built';
import { Component } from 'react';
import { resources } from '../reducer/resourcesReducer';
import { resourceSagas } from './resourcesSagas';
const sagaMiddleWare = sagaMiddlewareFactory();
const mockStore = createStore(combineReducers({ resources }), {}, applyMiddleware(sagaMiddleWare));
const mockStore = createStore(
combineReducers({ resources }),
{},
applyMiddleware(sagaMiddleWare)
);
sagaMiddleWare.run(resourceSagas);
jest.mock('../store', () => ({
get store() {
return mockStore;
}
},
}));
jest.mock('../../ui/dialogs/service', () => ({
DialogService: {
showDialog: () => Promise.resolve(true),
hideDialog: () => Promise.resolve({ path: 'somePath' }),
}
},
}));
const mockRemoteCommandsCalled = [];
@ -50,12 +89,11 @@ jest.mock('../../platform/commands/commandServiceImpl', () => ({
},
call: async (commandName: string, ...args: any[]) => {
mockLocalCommandsCalled.push({ commandName, args: args });
}
}
},
},
}));
describe('The ResourceSagas', () => {
beforeEach(() => {
mockRemoteCommandsCalled.length = 0;
mockLocalCommandsCalled.length = 0;
@ -67,7 +105,7 @@ describe('The ResourceSagas', () => {
mockResource = BotConfigWithPathImpl.serviceFromJSON({
type: ServiceTypes.File,
path: 'the/file/path',
name: 'testChat'
name: 'testChat',
} as any);
});
@ -77,31 +115,31 @@ describe('The ResourceSagas', () => {
expect(mockRemoteCommandsCalled.length).toBe(2);
[
{
'commandName': 'electron:display-context-menu',
'args': [
commandName: 'electron:display-context-menu',
args: [
[
{
'label': 'Open file location',
'id': 0
label: 'Open file location',
id: 0,
},
{
'label': 'Rename',
'id': 1
label: 'Rename',
id: 1,
},
{
'label': 'Delete',
'id': 2
}
]
]
label: 'Delete',
id: 2,
},
],
],
},
{
'commandName': 'shell:open-file-location',
'args': [
'the/file/path'
]
}
].forEach((command, index) => expect(mockRemoteCommandsCalled[index]).toEqual(command));
commandName: 'shell:open-file-location',
args: ['the/file/path'],
},
].forEach((command, index) =>
expect(mockRemoteCommandsCalled[index]).toEqual(command)
);
});
it('and put the resource in the store as the "resourceToRename" property when "edit" is chosen', async () => {
@ -118,25 +156,40 @@ describe('The ResourceSagas', () => {
expect(mockRemoteCommandsCalled.length).toBe(3);
[
{
'commandName': 'electron:display-context-menu',
'args': [[{ 'label': 'Open file location', 'id': 0 }, { 'label': 'Rename', 'id': 1 }, {
'label': 'Delete',
'id': 2
}]]
}, {
'commandName': 'shell:showExplorer-message-box',
'args': [true, {
'type': 'info',
'title': 'Delete this file',
'buttons': ['Cancel', 'Delete'],
'defaultId': 1,
'message': 'This action cannot be undone. Are you sure you want to delete testChat?',
'cancelId': 0
}]
}, {
'commandName': 'shell:unlink-file',
'args': ['the/file/path']
}].forEach((command, index) => expect(mockRemoteCommandsCalled[index]).toEqual(command));
commandName: 'electron:display-context-menu',
args: [
[
{ label: 'Open file location', id: 0 },
{ label: 'Rename', id: 1 },
{
label: 'Delete',
id: 2,
},
],
],
},
{
commandName: 'shell:showExplorer-message-box',
args: [
true,
{
type: 'info',
title: 'Delete this file',
buttons: ['Cancel', 'Delete'],
defaultId: 1,
message:
'This action cannot be undone. Are you sure you want to delete testChat?',
cancelId: 0,
},
],
},
{
commandName: 'shell:unlink-file',
args: ['the/file/path'],
},
].forEach((command, index) =>
expect(mockRemoteCommandsCalled[index]).toEqual(command)
);
});
});
@ -146,7 +199,7 @@ describe('The ResourceSagas', () => {
mockResource = BotConfigWithPathImpl.serviceFromJSON({
type: ServiceTypes.File,
path: 'the/file/path',
name: 'testChat'
name: 'testChat',
} as any);
});
@ -155,15 +208,18 @@ describe('The ResourceSagas', () => {
await mockStore.dispatch(renameResource(mockResource));
expect(mockRemoteCommandsCalled.length).toBe(1);
expect(mockRemoteCommandsCalled[0]).toEqual({
'commandName': 'shell:showExplorer-message-box',
'args': [true, {
'type': 'error',
'title': 'Invalid file name',
'buttons': ['Ok'],
'defaultId': 1,
'message': 'A valid file name must be used',
'cancelId': 0
}]
commandName: 'shell:showExplorer-message-box',
args: [
true,
{
type: 'error',
title: 'Invalid file name',
buttons: ['Ok'],
defaultId: 1,
message: 'A valid file name must be used',
cancelId: 0,
},
],
});
});
@ -171,11 +227,14 @@ describe('The ResourceSagas', () => {
await mockStore.dispatch(renameResource(mockResource));
expect(mockRemoteCommandsCalled.length).toBe(1);
expect(mockRemoteCommandsCalled[0]).toEqual({
'args': [{
'name': 'testChat',
'path': 'the/file/path',
'type': 'file'
}], 'commandName': 'shell:rename-file'
args: [
{
name: 'testChat',
path: 'the/file/path',
type: 'file',
},
],
commandName: 'shell:rename-file',
});
const { resourceToRename } = (mockStore.getState() as any).resources;
expect(resourceToRename).toBeNull();
@ -188,36 +247,39 @@ describe('The ResourceSagas', () => {
mockResource = BotConfigWithPathImpl.serviceFromJSON({
type: ServiceTypes.File,
path: 'the/file/path/chat.chat',
name: 'testChat'
name: 'testChat',
} as any);
});
it('should open a chat file', async () => {
await mockStore.dispatch(openResource(mockResource as any));
expect(mockLocalCommandsCalled).toEqual([{
'commandName': 'chat:open',
'args': ['the/file/path/chat.chat', true]
}]);
expect(mockLocalCommandsCalled).toEqual([
{
commandName: 'chat:open',
args: ['the/file/path/chat.chat', true],
},
]);
});
it('should open a transcript file', async () => {
mockResource.path = 'the/file/path/transcript.transcript';
await mockStore.dispatch(openResource(mockResource as any));
expect(mockLocalCommandsCalled).toEqual([{
'commandName': 'transcript:open',
'args': ['the/file/path/transcript.transcript']
}]);
expect(mockLocalCommandsCalled).toEqual([
{
commandName: 'transcript:open',
args: ['the/file/path/transcript.transcript'],
},
]);
});
});
it('should open the resource settings dialog and process the results as expected', async () => {
const mockClass = class extends Component {
};
const mockClass = class extends Component {};
await mockStore.dispatch(openResourcesSettings({ dialog: mockClass }));
await Promise.resolve();
expect(mockRemoteCommandsCalled).toEqual(
[{ commandName: 'bot:list:patch', args: [undefined, true] }]
);
expect(mockRemoteCommandsCalled).toEqual([
{ commandName: 'bot:list:patch', args: [undefined, true] },
]);
});
});

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,31 +1,79 @@
import { ForkEffect, put, takeEvery } from 'redux-saga/effects';
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import {
BotInfo,
isChatFile,
isTranscriptFile,
NotificationType,
SharedConstants,
} from '@bfemulator/app-shared';
import { newNotification } from '@bfemulator/app-shared/built';
import { IFileService } from 'botframework-config/lib/schema';
import { ComponentClass } from 'react';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { DialogService } from '../../ui/dialogs/service';
import { beginAdd } from '../action/notificationActions';
import {
editResource,
OPEN_CONTEXT_MENU_FOR_RESOURCE,
OPEN_RESOURCE,
OPEN_RESOURCE_SETTINGS,
RENAME_RESOURCE,
ResourcesAction
ResourcesAction,
} from '../action/resourcesAction';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { BotInfo, isChatFile, isTranscriptFile, NotificationType, SharedConstants } from '@bfemulator/app-shared';
import { IFileService } from 'botframework-config/lib/schema';
import { ComponentClass } from 'react';
import { DialogService } from '../../ui/dialogs/service';
import { beginAdd } from '../action/notificationActions';
import { newNotification } from '@bfemulator/app-shared/built';
function* openContextMenuForResource(action: ResourcesAction<IFileService>): IterableIterator<any> {
import { ForkEffect, put, takeEvery } from 'redux-saga/effects';
function* openContextMenuForResource(
action: ResourcesAction<IFileService>
): IterableIterator<any> {
const menuItems = [
{ label: 'Open file location', id: 0 },
{ label: 'Rename', id: 1 },
{ label: 'Delete', id: 2 }
{ label: 'Delete', id: 2 },
];
const result = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.DisplayContextMenu, menuItems);
const result = yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Electron.DisplayContextMenu,
menuItems
);
switch (result.id) {
case 0:
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.OpenFileLocation, action.payload.path);
yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Electron.OpenFileLocation,
action.payload.path
);
break;
case 1:
@ -42,7 +90,9 @@ function* openContextMenuForResource(action: ResourcesAction<IFileService>): Ite
}
}
function* deleteFile(action: ResourcesAction<IFileService>): IterableIterator<any> {
function* deleteFile(
action: ResourcesAction<IFileService>
): IterableIterator<any> {
const { name, path } = action.payload;
const { ShowMessageBox, UnlinkFile } = SharedConstants.Commands.Electron;
const result = yield CommandServiceImpl.remoteCall(ShowMessageBox, true, {
@ -75,7 +125,9 @@ function* doRename(action: ResourcesAction<IFileService>) {
yield put(editResource(null));
}
function* doOpenResource(action: ResourcesAction<IFileService>): IterableIterator<any> {
function* doOpenResource(
action: ResourcesAction<IFileService>
): IterableIterator<any> {
const { OpenChatFile, OpenTranscript } = SharedConstants.Commands.Emulator;
const { path } = action.payload;
if (isChatFile(path)) {
@ -86,13 +138,24 @@ function* doOpenResource(action: ResourcesAction<IFileService>): IterableIterato
// unknown types just fall into the abyss
}
function* launchResourcesSettingsModal(action: ResourcesAction<{ dialog: ComponentClass<any> }>) {
const result: Partial<BotInfo> = yield DialogService.showDialog(action.payload.dialog);
function* launchResourcesSettingsModal(
action: ResourcesAction<{ dialog: ComponentClass<any> }>
) {
const result: Partial<BotInfo> = yield DialogService.showDialog(
action.payload.dialog
);
if (result) {
try {
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.PatchBotList, result.path, result);
yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Bot.PatchBotList,
result.path,
result
);
} catch (e) {
const notification = newNotification('Unable to save resource settings', NotificationType.Error);
const notification = newNotification(
'Unable to save resource settings',
NotificationType.Error
);
yield put(beginAdd(notification));
}
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,13 +1,46 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { ServiceCodes, SharedConstants } from '@bfemulator/app-shared';
import { ServiceTypes } from 'botframework-config/lib/schema';
import { applyMiddleware, combineReducers, createStore } from 'redux';
import sagaMiddlewareFactory from 'redux-saga';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import {
AzureLoginFailedDialogContainer,
AzureLoginSuccessDialogContainer,
ConnectServicePromptDialogContainer,
GetStartedWithCSDialogContainer
GetStartedWithCSDialogContainer,
} from '../../ui/dialogs';
import { DialogService } from '../../ui/dialogs/service'; // ☣☣ careful! ☣☣
import { ConnectedServicePickerContainer } from '../../ui/shell/explorer/servicesExplorer';
@ -21,62 +54,71 @@ import {
launchConnectedServicePicker,
openAddServiceContextMenu,
openContextMenuForConnectedService,
openServiceDeepLink
openServiceDeepLink,
} from '../action/connectedServiceActions';
import { azureAuth } from '../reducer/azureAuthReducer';
import { bot } from '../reducer/bot';
import { servicesExplorerSagas } from './servicesExplorerSagas';
const sagaMiddleWare = sagaMiddlewareFactory();
const mockStore = createStore(combineReducers({ azureAuth, bot }), {}, applyMiddleware(sagaMiddleWare));
const mockStore = createStore(
combineReducers({ azureAuth, bot }),
{},
applyMiddleware(sagaMiddleWare)
);
sagaMiddleWare.run(servicesExplorerSagas);
const mockArmToken = 'bm90aGluZw==.eyJ1cG4iOiJnbGFzZ293QHNjb3RsYW5kLmNvbSJ9.7gjdshgfdsk98458205jfds9843fjds';
const mockArmToken =
'bm90aGluZw==.eyJ1cG4iOiJnbGFzZ293QHNjb3RsYW5kLmNvbSJ9.7gjdshgfdsk98458205jfds9843fjds';
jest.mock('../../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: function mock() {
return undefined;
},
AzureLoginSuccessDialogContainer: function mock() {
return undefined;
},
BotCreationDialog: function mock() {
return undefined;
},
DialogService: {
showDialog: () => Promise.resolve(true)
},
SecretPromptDialog: function mock() {
return undefined;
}
}
));
jest.mock('../../ui/shell/explorer/servicesExplorer/connectedServiceEditor', () => ({
ConnectedServiceEditorContainer: function mock() {
AzureLoginPromptDialogContainer: function mock() {
return undefined;
}
},
AzureLoginSuccessDialogContainer: function mock() {
return undefined;
},
BotCreationDialog: function mock() {
return undefined;
},
DialogService: {
showDialog: () => Promise.resolve(true),
},
SecretPromptDialog: function mock() {
return undefined;
},
}));
jest.mock(
'../../ui/shell/explorer/servicesExplorer/connectedServiceEditor',
() => ({
ConnectedServiceEditorContainer: function mock() {
return undefined;
},
})
);
jest.mock('../../ui/shell/explorer/servicesExplorer', () => ({
ConnectedServicePicker: function mock() {
return undefined;
}
},
}));
jest.mock('../store', () => ({
get store() {
return mockStore;
}
},
}));
jest.mock('./azureAuthSaga', () => ({
getArmToken: function* () {
getArmToken: function*() {
// eslint-disable-next-line typescript/camelcase
yield { access_token: mockArmToken };
}
},
}));
CommandServiceImpl.remoteCall = async function (type: string) {
CommandServiceImpl.remoteCall = async function(type: string) {
switch (type) {
case SharedConstants.Commands.ConnectedService.GetConnectedServicesByType:
return { services: [{ id: 'a luis service' }], code: ServiceCodes.OK };
@ -96,24 +138,29 @@ describe('The ServiceExplorerSagas', () => {
azureAuthWorkflowComponents: {
loginFailedDialog: AzureLoginFailedDialogContainer,
loginSuccessDialog: AzureLoginSuccessDialogContainer,
promptDialog: ConnectServicePromptDialogContainer
promptDialog: ConnectServicePromptDialogContainer,
},
getStartedDialog: GetStartedWithCSDialogContainer,
editorComponent: ConnectedServiceEditorContainer,
pickerComponent: ConnectedServicePickerContainer,
};
launchConnectedServicePickerGen = servicesExplorerSagas().next().value.FORK.args[1];
launchConnectedServicePickerGen = servicesExplorerSagas().next().value
.FORK.args[1];
mockStore.dispatch(azureArmTokenDataChanged(mockArmToken));
});
it('should retrieve the arm token from the store', () => {
const token = launchConnectedServicePickerGen().next().value.SELECT.selector(mockStore.getState());
const token = launchConnectedServicePickerGen()
.next()
.value.SELECT.selector(mockStore.getState());
expect(token.access_token).toBe(mockArmToken);
});
it('should prompt the user to login if the armToken does not exist in the store', () => {
mockStore.dispatch(azureArmTokenDataChanged(''));
const it = launchConnectedServicePickerGen(launchConnectedServicePicker(payload));
const it = launchConnectedServicePickerGen(
launchConnectedServicePicker(payload)
);
let token = it.next().value.SELECT.selector(mockStore.getState());
expect(token.access_token).toBe('');
token = it.next().value;
@ -133,7 +180,8 @@ describe('The ServiceExplorerSagas', () => {
});
it('should launch the luis models picklist after the luis models are retrieved', async () => {
DialogService.showDialog = () => Promise.resolve([{ id: 'a new service to add' }]) as any;
DialogService.showDialog = () =>
Promise.resolve([{ id: 'a new service to add' }]) as any;
const action = launchConnectedServicePicker(payload);
const it = launchConnectedServicePickerGen(action);
let token = it.next().value.SELECT.selector(mockStore.getState());
@ -162,7 +210,8 @@ describe('The ServiceExplorerSagas', () => {
mockStore.dispatch(load([mockBot]));
mockStore.dispatch(setActive(mockBot));
DialogService.showDialog = () => Promise.resolve([{ id: 'a new service to add' }]) as any;
DialogService.showDialog = () =>
Promise.resolve([{ id: 'a new service to add' }]) as any;
const action = launchConnectedServicePicker(payload);
const it = launchConnectedServicePickerGen(action);
let token = it.next().value.SELECT.selector(mockStore.getState());
@ -171,10 +220,12 @@ describe('The ServiceExplorerSagas', () => {
const luisModels = await it.next(token).value;
const newModels = await it.next(luisModels).value;
const botConfig = it.next(newModels).value.SELECT.selector(mockStore.getState());
const botConfig = it
.next(newModels)
.value.SELECT.selector(mockStore.getState());
let _type;
let _args;
CommandServiceImpl.remoteCall = function (type: string, ...args: any[]) {
CommandServiceImpl.remoteCall = function(type: string, ...args: any[]) {
_type = type;
_args = args;
return Promise.resolve(true);
@ -202,8 +253,9 @@ describe('The ServiceExplorerSagas', () => {
beforeEach(() => {
const sagaIt = servicesExplorerSagas();
action = openContextMenuForConnectedService<ConnectedServiceAction<ConnectedServicePayload>>
(ConnectedServiceEditorContainer, mockService);
action = openContextMenuForConnectedService<
ConnectedServiceAction<ConnectedServicePayload>
>(ConnectedServiceEditorContainer, mockService);
let i = 4;
while (i--) {
contextMenuGen = sagaIt.next().value.FORK.args[1];
@ -222,7 +274,9 @@ describe('The ServiceExplorerSagas', () => {
await it.next(result).value;
expect(window.open).toHaveBeenCalledWith(
`https://luis.ai/applications/${ mockService.appId }/versions/${ mockService.version }/build`
`https://luis.ai/applications/${mockService.appId}/versions/${
mockService.version
}/build`
);
});
@ -369,7 +423,7 @@ describe('The ServiceExplorerSagas', () => {
azureAuthWorkflowComponents: {
loginFailedDialog: AzureLoginFailedDialogContainer,
loginSuccessDialog: AzureLoginSuccessDialogContainer,
promptDialog: ConnectServicePromptDialogContainer
promptDialog: ConnectServicePromptDialogContainer,
},
getStartedDialog: GetStartedWithCSDialogContainer,
editorComponent: ConnectedServiceEditorContainer,
@ -396,7 +450,12 @@ describe('The ServiceExplorerSagas', () => {
});
describe('openConnectedServiceDeepLink', () => {
const mockModel = { type: ServiceTypes.Luis, appId: '1234', version: '0.1', region: 'westeurope' };
const mockModel = {
type: ServiceTypes.Luis,
appId: '1234',
version: '0.1',
region: 'westeurope',
};
let openConnectedServiceGen;
beforeEach(() => {
const sagaIt = servicesExplorerSagas();
@ -435,61 +494,67 @@ describe('The ServiceExplorerSagas', () => {
it('should open the correct service url for CosmosDB services', () => {
window.open = jest.fn();
const mock = {
'type': ServiceTypes.CosmosDB,
'collection': 'fdsa',
'database': 'fsa',
'endpoint': 'fsda',
'id': '206',
'name': 'Cosmos',
'resourceGroup': 'db-service',
'serviceName': 'cosmosdb',
'subscriptionId': '0389857f-aaaa-ssss-fff-gfsgfdsfgssdgfd',
'tenantId': 'microsoft.com'
type: ServiceTypes.CosmosDB,
collection: 'fdsa',
database: 'fsa',
endpoint: 'fsda',
id: '206',
name: 'Cosmos',
resourceGroup: 'db-service',
serviceName: 'cosmosdb',
subscriptionId: '0389857f-aaaa-ssss-fff-gfsgfdsfgssdgfd',
tenantId: 'microsoft.com',
};
const action = openServiceDeepLink(mock as any);
openConnectedServiceGen(action).next();
expect(window.open).toHaveBeenCalledWith('https://ms.portal.azure.com/#@microsoft.com/resource/subscriptions/' +
'0389857f-aaaa-ssss-fff-gfsgfdsfgssdgfd/resourceGroups/db-service/providers/Microsoft.DocumentDb/' +
'databaseAccounts/cosmosdb/overview');
expect(window.open).toHaveBeenCalledWith(
'https://ms.portal.azure.com/#@microsoft.com/resource/subscriptions/' +
'0389857f-aaaa-ssss-fff-gfsgfdsfgssdgfd/resourceGroups/db-service/providers/Microsoft.DocumentDb/' +
'databaseAccounts/cosmosdb/overview'
);
});
it('should open the correct service url for BlobStorage services', () => {
window.open = jest.fn();
const mock = {
'type': ServiceTypes.BlobStorage,
'id': '206',
'name': 'Blob',
'resourceGroup': 'blob-service',
'serviceName': 'blob',
'subscriptionId': '0389857f-aaaa-ssss-fff-gfsgfdsfgssdgfd',
'tenantId': 'microsoft.com'
type: ServiceTypes.BlobStorage,
id: '206',
name: 'Blob',
resourceGroup: 'blob-service',
serviceName: 'blob',
subscriptionId: '0389857f-aaaa-ssss-fff-gfsgfdsfgssdgfd',
tenantId: 'microsoft.com',
};
const action = openServiceDeepLink(mock as any);
openConnectedServiceGen(action).next();
expect(window.open).toHaveBeenCalledWith('https://ms.portal.azure.com/#@microsoft.com/resource/subscriptions/' +
'0389857f-aaaa-ssss-fff-gfsgfdsfgssdgfd/resourceGroups/blob-service/providers/Microsoft.DocumentDB/' +
'storageAccounts/blob/overview');
expect(window.open).toHaveBeenCalledWith(
'https://ms.portal.azure.com/#@microsoft.com/resource/subscriptions/' +
'0389857f-aaaa-ssss-fff-gfsgfdsfgssdgfd/resourceGroups/blob-service/providers/Microsoft.DocumentDB/' +
'storageAccounts/blob/overview'
);
});
it('should open the correct service url for AppInsights services', () => {
window.open = jest.fn();
const mock = {
'type': ServiceTypes.AppInsights,
'id': '206',
'name': 'appInsights',
'resourceGroup': 'appInsights-service',
'serviceName': 'appInsights',
'subscriptionId': '0389857f-aaaa-ssss-fff-gfsgfdsfgssdgfd',
'tenantId': 'microsoft.com'
type: ServiceTypes.AppInsights,
id: '206',
name: 'appInsights',
resourceGroup: 'appInsights-service',
serviceName: 'appInsights',
subscriptionId: '0389857f-aaaa-ssss-fff-gfsgfdsfgssdgfd',
tenantId: 'microsoft.com',
};
const action = openServiceDeepLink(mock as any);
openConnectedServiceGen(action).next();
expect(window.open).toHaveBeenCalledWith('https://ms.portal.azure.com/#@microsoft.com/resource/subscriptions/' +
'0389857f-aaaa-ssss-fff-gfsgfdsfgssdgfd/resourceGroups/appInsights-service/providers/microsoft.insights/' +
'components/appInsights/overview');
expect(window.open).toHaveBeenCalledWith(
'https://ms.portal.azure.com/#@microsoft.com/resource/subscriptions/' +
'0389857f-aaaa-ssss-fff-gfsgfdsfgssdgfd/resourceGroups/appInsights-service/providers/microsoft.insights/' +
'components/appInsights/overview'
);
});
it('should open the correct service URL for qnaMaker services', () => {
@ -499,7 +564,9 @@ describe('The ServiceExplorerSagas', () => {
const action = openServiceDeepLink(mockModel as any);
openConnectedServiceGen(action).next();
expect(window.open).toHaveBeenCalledWith('https://qnamaker.ai/Edit/KnowledgeBase?kbid=45432');
expect(window.open).toHaveBeenCalledWith(
'https://qnamaker.ai/Edit/KnowledgeBase?kbid=45432'
);
});
});
});

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -40,13 +40,16 @@ import {
IGenericService,
ILuisService,
IQnAService,
ServiceTypes
ServiceTypes,
} from 'botframework-config/lib/schema';
import { ForkEffect, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { DialogService } from '../../ui/dialogs/service';
import { serviceTypeLabels } from '../../utils/serviceTypeLables';
import { ArmTokenData, beginAzureAuthWorkflow } from '../action/azureAuthActions';
import {
ArmTokenData,
beginAzureAuthWorkflow,
} from '../action/azureAuthActions';
import {
ConnectedServiceAction,
ConnectedServicePayload,
@ -56,35 +59,57 @@ import {
OPEN_ADD_CONNECTED_SERVICE_CONTEXT_MENU,
OPEN_CONNECTED_SERVICE_SORT_CONTEXT_MENU,
OPEN_CONTEXT_MENU_FOR_CONNECTED_SERVICE,
OPEN_SERVICE_DEEP_LINK
OPEN_SERVICE_DEEP_LINK,
} from '../action/connectedServiceActions';
import { sortExplorerContents } from '../action/explorerActions';
import { SortCriteria } from '../reducer/explorer';
import { RootState } from '../store';
import { getArmToken } from './azureAuthSaga';
declare type ServicesPayload = { services: IConnectedService[], code: ServiceCodes };
import {
ForkEffect,
put,
select,
takeEvery,
takeLatest,
} from 'redux-saga/effects';
const getArmTokenFromState = (state: RootState): ArmTokenData => state.azureAuth;
const geBotConfigFromState = (state: RootState): BotConfigWithPath => state.bot.activeBot;
const getSortSelection = (state: RootState): { [paneldId: string]: SortCriteria } =>
declare interface ServicesPayload {
services: IConnectedService[];
code: ServiceCodes;
}
const getArmTokenFromState = (state: RootState): ArmTokenData =>
state.azureAuth;
const geBotConfigFromState = (state: RootState): BotConfigWithPath =>
state.bot.activeBot;
const getSortSelection = (
state: RootState
): { [paneldId: string]: SortCriteria } =>
state.explorer.sortSelectionByPanelId;
function* launchConnectedServicePicker(action: ConnectedServiceAction<ConnectedServicePickerPayload>)
: IterableIterator<any> {
function* launchConnectedServicePicker(
action: ConnectedServiceAction<ConnectedServicePickerPayload>
): IterableIterator<any> {
// To retrieve azure services, luis models and KBs,
// we must have the authoring key.
// To get the authoring key, we need the arm token.
let armTokenData: ArmTokenData & number = yield select(getArmTokenFromState);
if (!armTokenData || !armTokenData.access_token) {
const { promptDialog, loginSuccessDialog, loginFailedDialog } = action.payload.azureAuthWorkflowComponents;
const {
promptDialog,
loginSuccessDialog,
loginFailedDialog,
} = action.payload.azureAuthWorkflowComponents;
armTokenData = yield* getArmToken(
beginAzureAuthWorkflow(
promptDialog,
{ serviceType: action.payload.serviceType },
loginSuccessDialog,
loginFailedDialog
));
)
);
}
// 2 means the user has chosen to manually enter the connected service
@ -97,12 +122,15 @@ function* launchConnectedServicePicker(action: ConnectedServiceAction<ConnectedS
}
// Add the authenticated user to the action since we now have the token
const pJson = JSON.parse(atob(armTokenData.access_token.split('.')[1]));
action.payload.authenticatedUser = (pJson.upn || pJson.unique_name || pJson.name || pJson.email);
action.payload.authenticatedUser =
pJson.upn || pJson.unique_name || pJson.name || pJson.email;
const { serviceType, progressIndicatorComponent } = action.payload;
if (progressIndicatorComponent) {
DialogService.showDialog(progressIndicatorComponent).catch();
}
let payload: ServicesPayload = yield* retrieveServicesByServiceType(serviceType);
const payload: ServicesPayload = yield* retrieveServicesByServiceType(
serviceType
);
if (progressIndicatorComponent) {
DialogService.hideDialog();
@ -113,7 +141,7 @@ function* launchConnectedServicePicker(action: ConnectedServiceAction<ConnectedS
const result = yield DialogService.showDialog(getStartedDialog, {
serviceType,
authenticatedUser,
showNoModelsFoundContent: !payload.services.length
showNoModelsFoundContent: !payload.services.length,
});
// Sign up with XXXX
if (result === 1) {
@ -124,7 +152,11 @@ function* launchConnectedServicePicker(action: ConnectedServiceAction<ConnectedS
yield* launchConnectedServiceEditor(action);
}
} else {
const servicesToAdd = yield* launchConnectedServicePickList(action, payload.services, serviceType);
const servicesToAdd = yield* launchConnectedServicePickList(
action,
payload.services,
serviceType
);
if (servicesToAdd) {
const botFile: BotConfigWithPath = yield select(geBotConfigFromState);
botFile.services.push(...servicesToAdd);
@ -139,18 +171,21 @@ function* launchConnectedServicePickList(
availableServices: IConnectedService[],
serviceType: ServiceTypes
): IterableIterator<any> {
const { pickerComponent, authenticatedUser, serviceType: type } = action.payload;
const {
pickerComponent,
authenticatedUser,
serviceType: type,
} = action.payload;
let result = yield DialogService.showDialog(pickerComponent, {
availableServices,
authenticatedUser,
serviceType
serviceType,
});
if (result === 1) {
action.payload.connectedService = BotConfigurationBase.serviceFromJSON({
type,
hostname: '' /* defect workaround */
hostname: '' /* defect workaround */,
} as any);
result = yield* launchConnectedServiceEditor(action);
}
@ -158,35 +193,58 @@ function* launchConnectedServicePickList(
return result;
}
function* retrieveServicesByServiceType(serviceType: ServiceTypes): IterableIterator<any> {
let armTokenData: ArmTokenData = yield select(getArmTokenFromState);
function* retrieveServicesByServiceType(
serviceType: ServiceTypes
): IterableIterator<any> {
const armTokenData: ArmTokenData = yield select(getArmTokenFromState);
if (!armTokenData || !armTokenData.access_token) {
throw new Error('Auth credentials do not exist.');
}
const { GetConnectedServicesByType } = SharedConstants.Commands.ConnectedService;
const {
GetConnectedServicesByType,
} = SharedConstants.Commands.ConnectedService;
let payload: ServicesPayload;
try {
payload = yield CommandServiceImpl.remoteCall(GetConnectedServicesByType, armTokenData.access_token, serviceType);
payload = yield CommandServiceImpl.remoteCall(
GetConnectedServicesByType,
armTokenData.access_token,
serviceType
);
} catch (e) {
payload = { services: [], code: ServiceCodes.Error };
}
return payload;
}
function* openConnectedServiceDeepLink(action: ConnectedServiceAction<ConnectedServicePayload>): IterableIterator<any> {
// eslint-disable-next-line require-yield
function* openConnectedServiceDeepLink(
action: ConnectedServiceAction<ConnectedServicePayload>
): IterableIterator<any> {
const { connectedService } = action.payload;
switch (connectedService.type) {
case ServiceTypes.AppInsights:
return openAzureProviderDeepLink('microsoft.insights/components', connectedService as IAzureService);
return openAzureProviderDeepLink(
'microsoft.insights/components',
connectedService as IAzureService
);
case ServiceTypes.BlobStorage:
return openAzureProviderDeepLink('Microsoft.DocumentDB/storageAccounts', connectedService as IAzureService);
return openAzureProviderDeepLink(
'Microsoft.DocumentDB/storageAccounts',
connectedService as IAzureService
);
case ServiceTypes.Bot:
return openAzureProviderDeepLink('Microsoft.BotService/botServices', connectedService as IAzureService);
return openAzureProviderDeepLink(
'Microsoft.BotService/botServices',
connectedService as IAzureService
);
case ServiceTypes.CosmosDB:
return openAzureProviderDeepLink('Microsoft.DocumentDb/databaseAccounts', connectedService as IAzureService);
return openAzureProviderDeepLink(
'Microsoft.DocumentDb/databaseAccounts',
connectedService as IAzureService
);
case ServiceTypes.Generic:
return window.open((connectedService as IGenericService).url);
@ -202,14 +260,18 @@ function* openConnectedServiceDeepLink(action: ConnectedServiceAction<ConnectedS
}
}
function* openContextMenuForService(action: ConnectedServiceAction<ConnectedServicePayload>)
: IterableIterator<any> {
function* openContextMenuForService(
action: ConnectedServiceAction<ConnectedServicePayload>
): IterableIterator<any> {
const menuItems = [
{ label: 'Manage service', id: 'open' },
{ label: 'Edit configuration', id: 'edit' },
{ label: 'Disconnect this service', id: 'forget' }
{ label: 'Disconnect this service', id: 'forget' },
];
const response = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.DisplayContextMenu, menuItems);
const response = yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Electron.DisplayContextMenu,
menuItems
);
const { connectedService } = action.payload;
action.payload.serviceType = connectedService.type;
switch (response.id) {
@ -225,13 +287,15 @@ function* openContextMenuForService(action: ConnectedServiceAction<ConnectedServ
yield* removeServiceFromActiveBot(connectedService);
break;
default: // canceled context menu
default:
// canceled context menu
return;
}
}
function* openAddConnectedServiceContextMenu(action: ConnectedServiceAction<ConnectedServicePickerPayload>)
: IterableIterator<any> {
function* openAddConnectedServiceContextMenu(
action: ConnectedServiceAction<ConnectedServicePickerPayload>
): IterableIterator<any> {
const menuItems = [
{ label: 'Add Language Understanding (LUIS)', id: ServiceTypes.Luis },
{ label: 'Add QnA Maker', id: ServiceTypes.QnA },
@ -244,69 +308,119 @@ function* openAddConnectedServiceContextMenu(action: ConnectedServiceAction<Conn
{ label: 'Add other service …', id: ServiceTypes.Generic },
];
const response = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.DisplayContextMenu, menuItems);
const response = yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Electron.DisplayContextMenu,
menuItems
);
const { id: serviceType } = response;
action.payload.serviceType = serviceType;
if (serviceType === ServiceTypes.Generic ||
serviceType === ServiceTypes.AppInsights) {
if (
serviceType === ServiceTypes.Generic ||
serviceType === ServiceTypes.AppInsights
) {
yield* launchConnectedServiceEditor(action);
} else {
yield* launchConnectedServicePicker(action);
}
}
function* openSortContextMenu(action: ConnectedServiceAction<ConnectedServicePayload>): IterableIterator<any> {
function* openSortContextMenu(
action: ConnectedServiceAction<ConnectedServicePayload>
): IterableIterator<any> {
const sortSelectionByPanelId = yield select(getSortSelection);
const currentSort = sortSelectionByPanelId[action.payload.panelId];
const menuItems = [
{ label: 'Sort by name', id: 'name', type: 'checkbox', checked: currentSort === 'name' },
{ label: 'Sort by type', id: 'type', type: 'checkbox', checked: currentSort === 'type' },
{
label: 'Sort by name',
id: 'name',
type: 'checkbox',
checked: currentSort === 'name',
},
{
label: 'Sort by type',
id: 'type',
type: 'checkbox',
checked: currentSort === 'type',
},
];
const response = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.DisplayContextMenu, menuItems);
yield response.id ? put(sortExplorerContents(action.payload.panelId, response.id)) : null;
const response = yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Electron.DisplayContextMenu,
menuItems
);
yield response.id
? put(sortExplorerContents(action.payload.panelId, response.id))
: null;
}
function* removeServiceFromActiveBot(connectedService: IConnectedService): IterableIterator<any> {
function* removeServiceFromActiveBot(
connectedService: IConnectedService
): IterableIterator<any> {
// TODO - localization
const result = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.ShowMessageBox, true, {
type: 'question',
buttons: ['Cancel', 'OK'],
defaultId: 1,
message: `Remove ${ serviceTypeLabels[connectedService.type] } service: ${ connectedService.name }. Are you sure?`,
cancelId: 0,
});
const result = yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Electron.ShowMessageBox,
true,
{
type: 'question',
buttons: ['Cancel', 'OK'],
defaultId: 1,
message: `Remove ${serviceTypeLabels[connectedService.type]} service: ${
connectedService.name
}. Are you sure?`,
cancelId: 0,
}
);
if (result) {
const { RemoveService } = SharedConstants.Commands.Bot;
yield CommandServiceImpl.remoteCall(RemoveService, connectedService.type, connectedService.id);
yield CommandServiceImpl.remoteCall(
RemoveService,
connectedService.type,
connectedService.id
);
}
}
function* launchConnectedServiceEditor(action: ConnectedServiceAction<ConnectedServicePayload>)
: IterableIterator<any> {
const { editorComponent, authenticatedUser, connectedService, serviceType } = action.payload;
const servicesToUpdate: IConnectedService[] = yield DialogService.showDialog(editorComponent, {
connectedService,
function* launchConnectedServiceEditor(
action: ConnectedServiceAction<ConnectedServicePayload>
): IterableIterator<any> {
const {
editorComponent,
authenticatedUser,
serviceType
});
connectedService,
serviceType,
} = action.payload;
const servicesToUpdate: IConnectedService[] = yield DialogService.showDialog(
editorComponent,
{
connectedService,
authenticatedUser,
serviceType,
}
);
if (servicesToUpdate) {
let i = servicesToUpdate.length;
while (i--) {
const service = servicesToUpdate[i];
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.AddOrUpdateService, service.type, service);
yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Bot.AddOrUpdateService,
service.type,
service
);
}
}
return null;
}
function openAzureProviderDeepLink(provider: string, azureService: IAzureService): void {
function openAzureProviderDeepLink(
provider: string,
azureService: IAzureService
): void {
const { tenantId, subscriptionId, resourceGroup, serviceName } = azureService;
const bits = [
`https://ms.portal.azure.com/#@${ tenantId }/resource/`,
`subscriptions/${ subscriptionId }/`,
`resourceGroups/${ encodeURI(resourceGroup) }/`,
`providers/${ provider }/${ encodeURI(serviceName) }/overview`
`https://ms.portal.azure.com/#@${tenantId}/resource/`,
`subscriptions/${subscriptionId}/`,
`resourceGroups/${encodeURI(resourceGroup)}/`,
`providers/${provider}/${encodeURI(serviceName)}/overview`,
];
window.open(bits.join(''));
@ -328,23 +442,49 @@ function openLuisDeepLink(luisService: ILuisService) {
regionPrefix = '';
break;
}
const linkArray = ['https://', `${ encodeURI(regionPrefix) }`, 'luis.ai/applications/'];
linkArray.push(`${ encodeURI(appId) }`, '/versions/', `${ encodeURI(version) }`, '/build');
const linkArray = [
'https://',
`${encodeURI(regionPrefix)}`,
'luis.ai/applications/',
];
linkArray.push(
`${encodeURI(appId)}`,
'/versions/',
`${encodeURI(version)}`,
'/build'
);
const link = linkArray.join('');
window.open(link);
}
function openQnaMakerDeepLink(service: IQnAService) {
const { kbId } = service;
const link = `https://qnamaker.ai/Edit/KnowledgeBase?kbid=${ encodeURIComponent(kbId) }`;
const link = `https://qnamaker.ai/Edit/KnowledgeBase?kbid=${encodeURIComponent(
kbId
)}`;
window.open(link);
}
export function* servicesExplorerSagas(): IterableIterator<ForkEffect> {
yield takeLatest(LAUNCH_CONNECTED_SERVICE_PICKER, launchConnectedServicePicker);
yield takeLatest(LAUNCH_CONNECTED_SERVICE_EDITOR, launchConnectedServiceEditor);
yield takeLatest(
LAUNCH_CONNECTED_SERVICE_PICKER,
launchConnectedServicePicker
);
yield takeLatest(
LAUNCH_CONNECTED_SERVICE_EDITOR,
launchConnectedServiceEditor
);
yield takeEvery(OPEN_SERVICE_DEEP_LINK, openConnectedServiceDeepLink);
yield takeEvery(OPEN_CONTEXT_MENU_FOR_CONNECTED_SERVICE, openContextMenuForService);
yield takeEvery(OPEN_ADD_CONNECTED_SERVICE_CONTEXT_MENU, openAddConnectedServiceContextMenu);
yield takeEvery(OPEN_CONNECTED_SERVICE_SORT_CONTEXT_MENU, openSortContextMenu);
yield takeEvery(
OPEN_CONTEXT_MENU_FOR_CONNECTED_SERVICE,
openContextMenuForService
);
yield takeEvery(
OPEN_ADD_CONNECTED_SERVICE_CONTEXT_MENU,
openAddConnectedServiceContextMenu
);
yield takeEvery(
OPEN_CONNECTED_SERVICE_SORT_CONTEXT_MENU,
openSortContextMenu
);
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,24 +31,29 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { editorSelector, refreshConversationMenu } from './sharedSagas';
import { RootState } from '../store';
import { select } from 'redux-saga/effects';
import { SharedConstants } from '@bfemulator/app-shared';
import { RootState } from '../store';
import { editorSelector, refreshConversationMenu } from './sharedSagas';
import { select } from 'redux-saga/effects';
let mockRemoteCommandsCalled = [];
jest.mock('../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: async (commandName: string, ...args: any[]) => {
mockRemoteCommandsCalled.push({ commandName, args: args });
}
}
},
},
}));
describe('The sharedSagas', () => {
const editorState = { activeEditor: 'primary' };
beforeEach(() => { mockRemoteCommandsCalled = []; });
beforeEach(() => {
mockRemoteCommandsCalled = [];
});
it('should select the editor state from the store', () => {
const state: RootState = { editor: editorState };
@ -65,7 +70,9 @@ describe('The sharedSagas', () => {
gen.next(editorState);
expect(mockRemoteCommandsCalled).toHaveLength(1);
const refreshConversationCall = mockRemoteCommandsCalled[0];
expect(refreshConversationCall.commandName).toBe(SharedConstants.Commands.Electron.UpdateConversationMenu);
expect(refreshConversationCall.commandName).toBe(
SharedConstants.Commands.Electron.UpdateConversationMenu
);
expect(refreshConversationCall.args).toHaveLength(1);
expect(refreshConversationCall.args[0]).toEqual(editorState);

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,10 +31,12 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { select } from 'redux-saga/effects';
import { RootState } from '../store';
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { RootState } from '../store';
import { select } from 'redux-saga/effects';
export function editorSelector(state: RootState) {
return state.editor;
@ -42,5 +44,8 @@ export function editorSelector(state: RootState) {
export function* refreshConversationMenu(): IterableIterator<any> {
const stateData = yield select(editorSelector);
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.UpdateConversationMenu, stateData);
yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Electron.UpdateConversationMenu,
stateData
);
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,48 +1,83 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { SharedConstants } from '@bfemulator/app-shared';
import sagaMiddlewareFactory from 'redux-saga';
import { applyMiddleware, combineReducers, createStore } from 'redux';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { openContextMenuForBot } from '../action/welcomePageActions';
import { bot } from '../reducer/bot';
import notification from '../reducer/notification';
import { notificationSagas } from './notificationSagas';
import { welcomePageSagas } from './welcomePageSagas';
import sagaMiddlewareFactory from 'redux-saga';
import { applyMiddleware, combineReducers, createStore } from 'redux';
const mockBot = {
path: '/some/path.bot',
displayName: 'AuthBot',
secret: 'secret'
secret: 'secret',
};
jest.mock('../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: async () => null
}
remoteCall: async () => null,
},
}));
const sagaMiddleWare = sagaMiddlewareFactory();
const mockStore = createStore(combineReducers({ bot, notification }), {
bot: { botFiles: [mockBot] }
}, applyMiddleware(sagaMiddleWare));
const mockStore = createStore(
combineReducers({ bot, notification }),
{
bot: { botFiles: [mockBot] },
},
applyMiddleware(sagaMiddleWare)
);
jest.mock('../store', () => ({
get store() {
return mockStore;
}
},
}));
sagaMiddleWare.run(welcomePageSagas);
sagaMiddleWare.run(notificationSagas);
describe('The WelcomePageSagas', () => {
describe(', when invoking a context menu over a bot in the list', () => {
it('should call the series of commands that move the bot file to a new location.', async () => {
const remoteCalls = [];
CommandServiceImpl.remoteCall = async function (...args: any[]) {
CommandServiceImpl.remoteCall = async function(...args: any[]) {
remoteCalls.push(args);
switch (args[0]) {
case SharedConstants.Commands.Electron.DisplayContextMenu:
return { id: 0 };
@ -60,58 +95,55 @@ describe('The WelcomePageSagas', () => {
'electron:display-context-menu',
[
{
'label': 'Move...',
'id': 0
label: 'Move...',
id: 0,
},
{
'label': 'Open file location',
'id': 1
label: 'Open file location',
id: 1,
},
{
'label': 'Forget this bot',
'id': 2
}
]
label: 'Forget this bot',
id: 2,
},
],
]);
expect(remoteCalls[1]).toEqual([
'shell:showExplorer-save-dialog',
{
'defaultPath': '/some/path.bot',
'buttonLabel': 'Move',
'nameFieldLabel': 'Name',
'filters': [
defaultPath: '/some/path.bot',
buttonLabel: 'Move',
nameFieldLabel: 'Name',
filters: [
{
'extensions': [
'.bot'
]
}
]
}
extensions: ['.bot'],
},
],
},
]);
expect(remoteCalls[2]).toEqual([
'shell:rename-file',
{
'path': '/some/path.bot',
'newPath': 'this/is/a/new/location.bot'
}
path: '/some/path.bot',
newPath: 'this/is/a/new/location.bot',
},
]);
expect(remoteCalls[3]).toEqual([
'bot:list:patch',
'/some/path.bot',
{
'path': 'this/is/a/new/location.bot',
'displayName': 'AuthBot',
'secret': 'secret'
}
path: 'this/is/a/new/location.bot',
displayName: 'AuthBot',
secret: 'secret',
},
]);
});
it('should add a notification if a remote command fails when moving a bot file', async () => {
CommandServiceImpl.remoteCall = async function (...args: any[]) {
CommandServiceImpl.remoteCall = async function(...args: any[]) {
switch (args[0]) {
case SharedConstants.Commands.Electron.DisplayContextMenu:
return { id: 0 };
@ -134,14 +166,13 @@ describe('The WelcomePageSagas', () => {
it('should call the appropriate command when opening the bot file location', async () => {
let openFileLocationArgs;
CommandServiceImpl.remoteCall = async function (...args: any[]) {
CommandServiceImpl.remoteCall = async function(...args: any[]) {
switch (args[0]) {
case SharedConstants.Commands.Electron.DisplayContextMenu:
return { id: 1 };
case SharedConstants.Commands.Electron.OpenFileLocation:
return openFileLocationArgs = args;
return (openFileLocationArgs = args);
default:
return null;
@ -150,19 +181,21 @@ describe('The WelcomePageSagas', () => {
await mockStore.dispatch(openContextMenuForBot(mockBot));
await Promise.resolve(true);
expect(openFileLocationArgs).toEqual(['shell:open-file-location', 'this/is/a/new/location.bot']);
expect(openFileLocationArgs).toEqual([
'shell:open-file-location',
'this/is/a/new/location.bot',
]);
});
it('should call the appropriate command when removing a bot from the list', async () => {
let removeBotFromListArgs;
CommandServiceImpl.remoteCall = async function (...args: any[]) {
CommandServiceImpl.remoteCall = async function(...args: any[]) {
switch (args[0]) {
case SharedConstants.Commands.Electron.DisplayContextMenu:
return { id: 2 };
case SharedConstants.Commands.Bot.RemoveFromBotList:
return removeBotFromListArgs = args;
return (removeBotFromListArgs = args);
default:
return null;
@ -170,7 +203,10 @@ describe('The WelcomePageSagas', () => {
};
await mockStore.dispatch(openContextMenuForBot(mockBot));
await Promise.resolve(true);
expect(removeBotFromListArgs).toEqual(['bot:list:remove', 'this/is/a/new/location.bot']);
expect(removeBotFromListArgs).toEqual([
'bot:list:remove',
'this/is/a/new/location.bot',
]);
});
});
});

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -1,28 +1,80 @@
import { BotInfo, newNotification, SharedConstants } from '@bfemulator/app-shared';
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import {
BotInfo,
newNotification,
SharedConstants,
} from '@bfemulator/app-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { beginAdd } from '../action/notificationActions';
import { OPEN_CONTEXT_MENU_FOR_BOT, WelcomePageAction } from '../action/welcomePageActions';
import {
OPEN_CONTEXT_MENU_FOR_BOT,
WelcomePageAction,
} from '../action/welcomePageActions';
import { ForkEffect, put, takeEvery } from 'redux-saga/effects';
function* openContextMenuForBot(action: WelcomePageAction<BotInfo>): IterableIterator<any> {
function* openContextMenuForBot(
action: WelcomePageAction<BotInfo>
): IterableIterator<any> {
const menuItems = [
{ label: 'Move...', id: 0 },
{ label: 'Open file location', id: 1 },
{ label: 'Forget this bot', id: 2 }
{ label: 'Forget this bot', id: 2 },
];
const result = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.DisplayContextMenu, menuItems);
const result = yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Electron.DisplayContextMenu,
menuItems
);
switch (result.id) {
case 0:
yield* moveBotToNewLocation(action.payload);
break;
case 1:
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.OpenFileLocation, action.payload.path);
yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Electron.OpenFileLocation,
action.payload.path
);
break;
case 2:
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.RemoveFromBotList, action.payload.path);
yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Bot.RemoveFromBotList,
action.payload.path
);
break;
default:
@ -32,20 +84,30 @@ function* openContextMenuForBot(action: WelcomePageAction<BotInfo>): IterableIte
}
function* moveBotToNewLocation(bot: BotInfo): IterableIterator<any> {
const newPath = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.ShowSaveDialog, {
defaultPath: bot.path,
buttonLabel: 'Move',
nameFieldLabel: 'Name',
filters: [{ extensions: ['.bot'] }]
});
const newPath = yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Electron.ShowSaveDialog,
{
defaultPath: bot.path,
buttonLabel: 'Move',
nameFieldLabel: 'Name',
filters: [{ extensions: ['.bot'] }],
}
);
if (!newPath) {
return;
}
try {
const { path: oldPath } = bot;
bot.path = newPath;
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.RenameFile, { path: oldPath, newPath });
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.PatchBotList, oldPath, bot);
yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Electron.RenameFile,
{ path: oldPath, newPath }
);
yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Bot.PatchBotList,
oldPath,
bot
);
} catch (e) {
const errMsg = `Error occurred while moving the bot file: ${e}`;
const notification = newNotification(errMsg);

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,25 +31,28 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { ClientAwareSettings } from '@bfemulator/app-shared';
import { applyMiddleware, combineReducers, createStore, Store } from 'redux';
import promiseMiddleware from 'redux-promise-middleware';
import sagaMiddlewareFactory from 'redux-saga';
import { azureAuth, AzureAuthState } from './reducer/azureAuthReducer';
import { bot, BotState } from './reducer/bot';
import { chat, ChatState } from './reducer/chat';
import { clientAwareSettings } from './reducer/clientAwareSettingsReducer';
import { dialog, DialogState } from './reducer/dialog';
import { editor, EditorState } from './reducer/editor';
import { explorer, ExplorerState } from './reducer/explorer';
import { azureAuth, AzureAuthState } from './reducer/azureAuthReducer';
import { navBar, NavBarState } from './reducer/navBar';
import { notification, NotificationState } from './reducer/notification';
import { presentation, PresentationState } from './reducer/presentation';
import { progressIndicator, ProgressIndicatorState } from './reducer/progressIndicator';
import {
progressIndicator,
ProgressIndicatorState,
} from './reducer/progressIndicator';
import { resources, ResourcesState } from './reducer/resourcesReducer';
import { theme, ThemeState } from './reducer/themeReducer';
import { clientAwareSettings } from './reducer/clientAwareSettingsReducer';
import { applicationSagas } from './sagas';
import { ClientAwareSettings } from '@bfemulator/app-shared';
export interface RootState {
azureAuth?: AzureAuthState;
@ -70,28 +73,28 @@ export interface RootState {
const sagaMiddleWare = sagaMiddlewareFactory();
const DEFAULT_STATE: RootState = {};
const configureStore = (initialState: RootState = DEFAULT_STATE): Store<RootState> => createStore<RootState>(
combineReducers({
azureAuth,
bot,
chat,
clientAwareSettings,
dialog,
editor,
explorer,
navBar,
notification,
presentation,
progressIndicator,
resources,
theme,
}),
initialState,
applyMiddleware(
sagaMiddleWare,
promiseMiddleware()
)
);
const configureStore = (
initialState: RootState = DEFAULT_STATE
): Store<RootState> =>
createStore<RootState>(
combineReducers({
azureAuth,
bot,
chat,
clientAwareSettings,
dialog,
editor,
explorer,
navBar,
notification,
presentation,
progressIndicator,
resources,
theme,
}),
initialState,
applyMiddleware(sagaMiddleWare, promiseMiddleware())
);
const store = configureStore();
applicationSagas.forEach(saga => sagaMiddleWare.run(saga));

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,29 +31,30 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { SharedConstants } from '@bfemulator/app-shared';
import {
CommandRegistryImpl,
CommandService,
CommandServiceImpl,
ExtensionConfig,
ExtensionInspector
ExtensionInspector,
} from '@bfemulator/sdk-shared';
import { ElectronIPC } from './ipc';
import { SharedConstants } from '@bfemulator/app-shared';
// =============================================================================
export class Extension {
private _ext: CommandService;
get unid(): string {
public get unid(): string {
return this._unid;
}
get config(): ExtensionConfig {
public get config(): ExtensionConfig {
return this._config;
}
constructor(private _config: ExtensionConfig, private _unid: string) {
public constructor(private _config: ExtensionConfig, private _unid: string) {
this._ext = new CommandServiceImpl(ElectronIPC, `ext-${this._unid}`);
/*
this._ext.remoteCall('ext-ping')
@ -64,11 +65,15 @@ export class Extension {
public inspectorForObject(obj: any): GetInspectorResult | null {
const inspectors = this.config.client.inspectors || [];
const inspector = inspectors.find(inspectorArg => InspectorAPI.canInspect(inspectorArg, obj));
return inspector ? {
extension: this,
inspector
} : null;
const inspector = inspectors.find(inspectorArg =>
InspectorAPI.canInspect(inspectorArg, obj)
);
return inspector
? {
extension: this,
inspector,
}
: null;
}
public call(commandName: string, ...args: any[]): Promise<any> {
@ -129,7 +134,10 @@ export class InspectorAPI {
}
}
export function getValueFromPath(source: { [prop: string]: any }, path: string): any {
export function getValueFromPath(
source: { [prop: string]: any },
path: string
): any {
const parts = path.split('.');
let val = source;
for (let i = 0; i < parts.length; i++) {
@ -158,15 +166,19 @@ export interface ExtensionManager {
getExtensions(): Extension[];
inspectorForObject(obj: any, defaultToJson: boolean): GetInspectorResult | null;
inspectorForObject(
obj: any,
defaultToJson: boolean
): GetInspectorResult | null;
}
// =============================================================================
export const ExtensionManager = new class implements ExtensionManager {
class EmulatorExtensionManager implements ExtensionManager {
private extensions: { [unid: string]: Extension } = {};
public addExtension(config: ExtensionConfig, unid: string) {
this.removeExtension(unid);
// eslint-disable-next-line no-console
console.log(`adding extension ${config.name}`);
const ext = new Extension(config, unid);
this.extensions[unid] = ext;
@ -174,30 +186,40 @@ export const ExtensionManager = new class implements ExtensionManager {
public removeExtension(unid: string) {
if (this.extensions[unid]) {
// eslint-disable-next-line no-console
console.log(`removing extension ${this.extensions[unid].config.name}`);
delete this.extensions[unid];
}
}
public findExtension(name: string): Extension {
return this.getExtensions().find(extension => extension.config.name === name);
return this.getExtensions().find(
extension => extension.config.name === name
);
}
public getExtensions(): Extension[] {
return Object.keys(this.extensions).map(key => this.extensions[key]) || [];
}
public inspectorForObject(obj: any, defaultToJson: boolean): GetInspectorResult | null {
public inspectorForObject(
obj: any,
defaultToJson: boolean
): GetInspectorResult | null {
let result = this.getExtensions()
.map(extension => extension.inspectorForObject(obj))
.filter(resultArg => !!resultArg).shift();
.filter(resultArg => !!resultArg)
.shift();
if (!result && defaultToJson) {
// Default to the JSON inspector
// eslint-disable-next-line typescript/no-use-before-define
const jsonExtension = ExtensionManager.findExtension('JSON');
if (jsonExtension) {
result = {
extension: jsonExtension,
inspector: jsonExtension.config.client.inspectors ? jsonExtension.config.client.inspectors[0] : null
inspector: jsonExtension.config.client.inspectors
? jsonExtension.config.client.inspectors[0]
: null,
};
}
}
@ -207,11 +229,15 @@ export const ExtensionManager = new class implements ExtensionManager {
public registerCommands(commandRegistry: CommandRegistryImpl) {
const { Connect, Disconnect } = SharedConstants.Commands.Extension;
commandRegistry.registerCommand(Connect, (config: ExtensionConfig) => {
// eslint-disable-next-line typescript/no-use-before-define
ExtensionManager.addExtension(config, config.location);
});
commandRegistry.registerCommand(Disconnect, (location: string) => {
// eslint-disable-next-line typescript/no-use-before-define
ExtensionManager.removeExtension(location);
});
}
};
}
export const ExtensionManager = new EmulatorExtensionManager();

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,12 +31,14 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import * as URL from 'url';
import { SharedConstants } from '@bfemulator/app-shared';
import { uniqueId } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from './platform/commands/commandServiceImpl';
const Electron = (window as any).require('electron');
const { shell } = Electron;
import { uniqueId } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from './platform/commands/commandServiceImpl';
import * as URL from 'url';
import { SharedConstants } from '@bfemulator/app-shared';
export function navigate(url: string) {
try {
@ -55,14 +57,21 @@ export function navigate(url: string) {
function navigateEmulatedOAuthUrl(oauthParam: string) {
const { Commands } = SharedConstants;
let parts = oauthParam.split('&&&');
CommandServiceImpl
.remoteCall(Commands.OAuth.SendTokenResponse, parts[0], parts[1], 'emulatedToken_' + uniqueId())
.catch();
const parts = oauthParam.split('&&&');
CommandServiceImpl.remoteCall(
Commands.OAuth.SendTokenResponse,
parts[0],
parts[1],
'emulatedToken_' + uniqueId()
).catch();
}
function navigateOAuthUrl(oauthParam: string) {
const { Commands } = SharedConstants;
let parts = oauthParam.split('&&&');
CommandServiceImpl.remoteCall(Commands.OAuth.CreateOAuthWindow, parts[0], parts[1]).catch();
const parts = oauthParam.split('&&&');
CommandServiceImpl.remoteCall(
Commands.OAuth.CreateOAuthWindow,
parts[0],
parts[1]
).catch();
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -32,17 +32,18 @@
//
// for hot reloading
import { Provider } from 'react-redux';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as React from 'react';
import { newNotification, SharedConstants } from '@bfemulator/app-shared';
import { LogService } from './platform/log/logService';
import interceptError from './interceptError';
import interceptHyperlink from './interceptHyperlink';
import Main from './ui/shell/mainContainer';
import { store } from './data/store';
import { CommandServiceImpl } from './platform/commands/commandServiceImpl';
import { LogService } from './platform/log/logService';
import { showWelcomePage } from './data/editorHelpers';
import { CommandRegistry, registerAllCommands } from './commands';
import { SharedConstants, newNotification } from '@bfemulator/app-shared';
import { beginAdd } from './data/action/notificationActions';
import { globalHandlers } from './utils/eventHandlers';
import './ui/styles/globals.scss';
@ -66,7 +67,9 @@ CommandServiceImpl.remoteCall(SharedConstants.Commands.ClientInit.Loaded)
.then(() => {
showWelcomePage();
// do actions on main side that might open a document, so that they will be active over the welcome screen
CommandServiceImpl.remoteCall(SharedConstants.Commands.ClientInit.PostWelcomeScreen);
CommandServiceImpl.remoteCall(
SharedConstants.Commands.ClientInit.PostWelcomeScreen
);
window.addEventListener('keydown', globalHandlers);
})
.catch(err => {

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,6 +31,7 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// eslint-disable-next-line typescript/no-var-requires
const { ipcRenderer, remote } = require('electron');
ipcRenderer.on('inspect', (sender, obj) => {
@ -42,7 +43,7 @@ ipcRenderer.on('bot-updated', (sender, bot) => {
window.host.dispatch('bot-updated', bot);
});
ipcRenderer.on('toggle-dev-tools', (sender) => {
ipcRenderer.on('toggle-dev-tools', () => {
remote.getCurrentWebContents().toggleDevTools();
});
@ -55,29 +56,35 @@ ipcRenderer.on('theme', (sender, ...args) => {
});
window.host = {
handlers: {
'inspect': [],
'bot-updated': [],
'accessory-click': [],
'theme': []
},
bot: {},
handlers: {
'accessory-click': [],
'bot-updated': [],
inspect: [],
theme: [],
},
logger: {
error: function(message) {
ipcRenderer.sendToHost('logger.error', message);
},
log: function(message) {
ipcRenderer.sendToHost('logger.log', message);
},
error: function(message) {
ipcRenderer.sendToHost('logger.error', message);
}
},
on: function(event, handler) {
if (handler && Array.isArray(this.handlers[event]) && !this.handlers[event].includes(handler)) {
if (
handler &&
Array.isArray(this.handlers[event]) &&
!this.handlers[event].includes(handler)
) {
this.handlers[event].push(handler);
}
return () => {
this.handlers[event] = this.handlers[event].filter(item => item !== handler);
}
this.handlers[event] = this.handlers[event].filter(
item => item !== handler
);
};
},
enableAccessory: function(id, enabled) {

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -34,13 +34,19 @@
// import * as log from './v1/log';
export default function interceptError() {
(process as NodeJS.EventEmitter).on('uncaughtException', _error => {
// log.error('[err-client]', error.message, error.stack);
});
(process as NodeJS.EventEmitter).on('uncaughtException', _error => {
// log.error('[err-client]', error.message, error.stack);
});
window.onerror = (_message: string, _filename?: string, _lineno?: number, _colno?: number, _error?: Error) => {
// log.error('[err-client]', message, filename, lineno, colno, error);
window.onerror = (
_message: string,
_filename?: string,
_lineno?: number,
_colno?: number,
_error?: Error
) => {
// log.error('[err-client]', message, filename, lineno, colno, error);
return true; // prevent default handler
};
return true; // prevent default handler
};
}

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -31,11 +31,11 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
const electron = (window as any).require('electron');
import { Channel, Disposable, IPC } from '@bfemulator/sdk-shared';
export const ElectronIPC = new class extends IPC {
const electron = (window as any).require('electron');
class ElectronIPCImpl extends IPC {
constructor() {
super();
electron.ipcRenderer.on('ipc:message', (_sender: any, ...args: any[]) => {
@ -47,11 +47,13 @@ export const ElectronIPC = new class extends IPC {
});
}
send(...args: any[]): void {
public send(...args: any[]): void {
electron.ipcRenderer.send('ipc:message', ...args);
}
registerChannel(channel: Channel): Disposable {
public registerChannel(channel: Channel): Disposable {
return super.registerChannel(channel);
}
};
}
export const ElectronIPC = new ElectronIPCImpl();

ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ„Π°ΠΉΠ»

@ -36,35 +36,48 @@ import {
CommandService,
CommandServiceImpl as InternalSharedService,
Disposable,
DisposableImpl
DisposableImpl,
} from '@bfemulator/sdk-shared';
import { CommandRegistry } from '../../commands';
import { ElectronIPC } from '../../ipc';
export const CommandServiceImpl = new class extends DisposableImpl implements CommandService {
class CServiceImpl extends DisposableImpl implements CommandService {
private readonly _service: InternalSharedService;
init() { return null; }
public init() {
return null;
}
public get registry() { return this._service.registry; }
public get registry() {
return this._service.registry;
}
constructor() {
super();
this._service = new InternalSharedService(ElectronIPC, 'command-service', CommandRegistry);
this._service = new InternalSharedService(
ElectronIPC,
'command-service',
CommandRegistry
);
super.toDispose(this._service);
}
call(commandName: string, ...args: any[]): Promise<any> {
public call(commandName: string, ...args: any[]): Promise<any> {
return this._service.call(commandName, ...args);
}
remoteCall(commandName: string, ...args: any[]): Promise<any> {
public remoteCall(commandName: string, ...args: any[]): Promise<any> {
return this._service.remoteCall(commandName, ...args);
}
on(event: string, handler?: CommandHandler): Disposable;
on(event: 'command-not-found', handler?: (commandName: string, ...args: any[]) => any) {
public on(event: string, handler?: CommandHandler): Disposable;
public on(
event: 'command-not-found',
handler?: (commandName: string, ...args: any[]) => any
) {
return this._service.on(event, handler);
}
};
}
export const CommandServiceImpl = new CServiceImpl();

НСкоторыС Ρ„Π°ΠΉΠ»Ρ‹ Π½Π΅ Π±Ρ‹Π»ΠΈ ΠΏΠΎΠΊΠ°Π·Π°Π½Ρ‹ ΠΈΠ·-Π·Π° слишком большого количСства ΠΈΠ·ΠΌΠ΅Π½Π΅Π½Π½Ρ‹Ρ… Ρ„Π°ΠΉΠ»ΠΎΠ² ΠŸΠΎΠΊΠ°Π·Π°Ρ‚ΡŒ большС