Emulator Launch, Commands and Extension Tech Debt Buydown (#1577)
* Tech debt buydown
This commit is contained in:
Родитель
a562689caf
Коммит
a1fa135f1e
10
.babelrc
10
.babelrc
|
@ -4,7 +4,7 @@
|
|||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"node": "8"
|
||||
"node": "10"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -13,14 +13,14 @@
|
|||
"ignore": ["**/*.d.ts"],
|
||||
"sourceMaps": "inline",
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-react-jsx",
|
||||
"@babel/plugin-transform-runtime",
|
||||
[
|
||||
"@babel/proposal-decorators",
|
||||
"@babel/plugin-proposal-decorators",
|
||||
{
|
||||
"decoratorsBeforeExport": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-runtime"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -7,14 +7,12 @@
|
|||
"packages/emulator/cli",
|
||||
"packages/emulator/core",
|
||||
"packages/sdk/client",
|
||||
"packages/sdk/main",
|
||||
"packages/sdk/shared",
|
||||
"packages/sdk/ui-react",
|
||||
"packages/extensions/qnamaker",
|
||||
"packages/extensions/qnamaker/client",
|
||||
"packages/extensions/debug",
|
||||
"packages/extensions/debug/client",
|
||||
"packages/extensions/debug/main",
|
||||
"packages/extensions/luis",
|
||||
"packages/extensions/luis/client",
|
||||
"packages/extensions/json",
|
||||
|
|
|
@ -826,9 +826,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/plugin-transform-runtime": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.4.3.tgz",
|
||||
"integrity": "sha512-7Q61bU+uEI7bCUFReT1NKn7/X6sDQsZ7wL1sJ9IYMAO7cI+eg6x9re1cEw2fCRMbbTVyoeUKWSV1M6azEfKCfg==",
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.4.4.tgz",
|
||||
"integrity": "sha512-aMVojEjPszvau3NRg+TIH14ynZLvPewH4xhlCW1w6A3rkxTS1m4uwzRclYR9oS+rl/dr+kT+pzbfHuAWP/lc7Q==",
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/helper-plugin-utils": "^7.0.0",
|
||||
|
@ -3847,14 +3847,14 @@
|
|||
}
|
||||
},
|
||||
"babel-loader": {
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.5.tgz",
|
||||
"integrity": "sha512-NTnHnVRd2JnRqPC0vW+iOQWU5pchDbYXsG2E6DMXEpMfUcQKclF9gmf3G3ZMhzG7IG9ji4coL0cm+FxeWxDpnw==",
|
||||
"version": "8.0.6",
|
||||
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz",
|
||||
"integrity": "sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==",
|
||||
"requires": {
|
||||
"find-cache-dir": "^2.0.0",
|
||||
"loader-utils": "^1.0.2",
|
||||
"mkdirp": "^0.5.1",
|
||||
"util.promisify": "^1.0.0"
|
||||
"pify": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"babel-messages": {
|
||||
|
@ -4780,9 +4780,9 @@
|
|||
}
|
||||
},
|
||||
"botframework-config": {
|
||||
"version": "4.0.0-preview1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/botframework-config/-/botframework-config-4.0.0-preview1.3.4.tgz",
|
||||
"integrity": "sha512-44X8ej+o1M43PGBjCsfs8fLA9L6WD74NAjqhvr52hxMrhMbaceFD+EsBBBdAdsaNmIjmtTTgkz5M7O6b3nZ9gA==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/botframework-config/-/botframework-config-4.4.0.tgz",
|
||||
"integrity": "sha512-5YyLkr3Zv60NJPZNTXZ/YEIj/yBRqfACw7+D90hPL3ueOtWi0Vc440a3cfggSq/RszSGDOVfX3VROVV85+Vm5g==",
|
||||
"requires": {
|
||||
"fs-extra": "^7.0.0",
|
||||
"process": "^0.11.10",
|
||||
|
@ -23274,6 +23274,23 @@
|
|||
"execa": "^0.7.0"
|
||||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-4.0.0.tgz",
|
||||
"integrity": "sha512-dOapGTU0hETFl1tCo4t56FN+2jffoKyER9qBGoUFyZ6y7WLoKT0bF+lAYi6B6YsILcGF3q1C2FBh8QcKSCgkgA==",
|
||||
"requires": {
|
||||
"commander": "^2.19.0",
|
||||
"source-map": "~0.6.1",
|
||||
"source-map-support": "~0.5.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"test-exclude": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.3.tgz",
|
||||
|
@ -23623,18 +23640,6 @@
|
|||
"utf8-byte-length": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"ts-loader": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-4.5.0.tgz",
|
||||
"integrity": "sha512-ihgVaSmgrX4crGV4n7yuoHPoCHbDzj9aepCZR9TgIx4SgJ9gdnB6xLHgUBb7bsFM/f0K6x9iXa65KY/Fu1Klkw==",
|
||||
"requires": {
|
||||
"chalk": "^2.3.0",
|
||||
"enhanced-resolve": "^4.0.0",
|
||||
"loader-utils": "^1.0.2",
|
||||
"micromatch": "^3.1.4",
|
||||
"semver": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
|
||||
|
@ -23799,92 +23804,84 @@
|
|||
}
|
||||
},
|
||||
"uglifyjs-webpack-plugin": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz",
|
||||
"integrity": "sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw==",
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-2.1.3.tgz",
|
||||
"integrity": "sha512-/lRkCaFbI6pT3CxsQHDhBcqB6tocOnqba0vJqJ2DzSWFLRgOIiip8q0nVFydyXk+n8UtF7ZuS6hvWopcYH5FuA==",
|
||||
"requires": {
|
||||
"cacache": "^10.0.4",
|
||||
"find-cache-dir": "^1.0.0",
|
||||
"schema-utils": "^0.4.5",
|
||||
"serialize-javascript": "^1.4.0",
|
||||
"cacache": "^11.3.2",
|
||||
"find-cache-dir": "^2.1.0",
|
||||
"is-wsl": "^1.1.0",
|
||||
"schema-utils": "^1.0.0",
|
||||
"serialize-javascript": "^1.7.0",
|
||||
"source-map": "^0.6.1",
|
||||
"uglify-es": "^3.3.4",
|
||||
"webpack-sources": "^1.1.0",
|
||||
"worker-farm": "^1.5.2"
|
||||
"uglify-js": "^3.5.12",
|
||||
"webpack-sources": "^1.3.0",
|
||||
"worker-farm": "^1.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
|
||||
"integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA=="
|
||||
},
|
||||
"find-cache-dir": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz",
|
||||
"integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=",
|
||||
"cacache": {
|
||||
"version": "11.3.2",
|
||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.2.tgz",
|
||||
"integrity": "sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg==",
|
||||
"requires": {
|
||||
"commondir": "^1.0.1",
|
||||
"make-dir": "^1.0.0",
|
||||
"pkg-dir": "^2.0.0"
|
||||
"bluebird": "^3.5.3",
|
||||
"chownr": "^1.1.1",
|
||||
"figgy-pudding": "^3.5.1",
|
||||
"glob": "^7.1.3",
|
||||
"graceful-fs": "^4.1.15",
|
||||
"lru-cache": "^5.1.1",
|
||||
"mississippi": "^3.0.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"move-concurrently": "^1.0.1",
|
||||
"promise-inflight": "^1.0.1",
|
||||
"rimraf": "^2.6.2",
|
||||
"ssri": "^6.0.1",
|
||||
"unique-filename": "^1.1.1",
|
||||
"y18n": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"find-up": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
|
||||
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
|
||||
"lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||
"requires": {
|
||||
"locate-path": "^2.0.0"
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
|
||||
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
|
||||
"requires": {
|
||||
"p-locate": "^2.0.0",
|
||||
"path-exists": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
||||
"integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
|
||||
"requires": {
|
||||
"pify": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
|
||||
"integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
|
||||
"requires": {
|
||||
"p-try": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
|
||||
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
|
||||
"requires": {
|
||||
"p-limit": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"p-try": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
|
||||
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
|
||||
},
|
||||
"pify": {
|
||||
"mississippi": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
|
||||
},
|
||||
"pkg-dir": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
|
||||
"integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=",
|
||||
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
|
||||
"integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==",
|
||||
"requires": {
|
||||
"find-up": "^2.1.0"
|
||||
"concat-stream": "^1.5.0",
|
||||
"duplexify": "^3.4.2",
|
||||
"end-of-stream": "^1.1.0",
|
||||
"flush-write-stream": "^1.0.0",
|
||||
"from2": "^2.1.0",
|
||||
"parallel-transform": "^1.1.0",
|
||||
"pump": "^3.0.0",
|
||||
"pumpify": "^1.3.3",
|
||||
"stream-each": "^1.1.0",
|
||||
"through2": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
|
||||
"integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
|
||||
"requires": {
|
||||
"ajv": "^6.1.0",
|
||||
"ajv-errors": "^1.0.0",
|
||||
"ajv-keywords": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
|
@ -23892,14 +23889,27 @@
|
|||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
},
|
||||
"uglify-es": {
|
||||
"version": "3.3.9",
|
||||
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
|
||||
"integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==",
|
||||
"ssri": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
|
||||
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
|
||||
"requires": {
|
||||
"commander": "~2.13.0",
|
||||
"figgy-pudding": "^3.5.1"
|
||||
}
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.5.15",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.15.tgz",
|
||||
"integrity": "sha512-fe7aYFotptIddkwcm6YuA0HmknBZ52ZzOsUxZEdhhkSsz7RfjHDX2QDxwKTiv4JQ5t5NhfmpgAK+J7LiDhKSqg==",
|
||||
"requires": {
|
||||
"commander": "~2.20.0",
|
||||
"source-map": "~0.6.1"
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
|
||||
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -24603,6 +24613,11 @@
|
|||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
|
||||
"integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw=="
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
|
||||
"integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA=="
|
||||
},
|
||||
"eslint-scope": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
|
||||
|
@ -24611,6 +24626,104 @@
|
|||
"esrecurse": "^4.1.0",
|
||||
"estraverse": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"find-cache-dir": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz",
|
||||
"integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=",
|
||||
"requires": {
|
||||
"commondir": "^1.0.1",
|
||||
"make-dir": "^1.0.0",
|
||||
"pkg-dir": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"find-up": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
|
||||
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
|
||||
"requires": {
|
||||
"locate-path": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
|
||||
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
|
||||
"requires": {
|
||||
"p-locate": "^2.0.0",
|
||||
"path-exists": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
||||
"integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
|
||||
"requires": {
|
||||
"pify": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
|
||||
"integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
|
||||
"requires": {
|
||||
"p-try": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
|
||||
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
|
||||
"requires": {
|
||||
"p-limit": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"p-try": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
|
||||
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
|
||||
},
|
||||
"pify": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
|
||||
},
|
||||
"pkg-dir": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
|
||||
"integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=",
|
||||
"requires": {
|
||||
"find-up": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
},
|
||||
"uglify-es": {
|
||||
"version": "3.3.9",
|
||||
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
|
||||
"integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==",
|
||||
"requires": {
|
||||
"commander": "~2.13.0",
|
||||
"source-map": "~0.6.1"
|
||||
}
|
||||
},
|
||||
"uglifyjs-webpack-plugin": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz",
|
||||
"integrity": "sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw==",
|
||||
"requires": {
|
||||
"cacache": "^10.0.4",
|
||||
"find-cache-dir": "^1.0.0",
|
||||
"schema-utils": "^0.4.5",
|
||||
"serialize-javascript": "^1.4.0",
|
||||
"source-map": "^0.6.1",
|
||||
"uglify-es": "^3.3.4",
|
||||
"webpack-sources": "^1.1.0",
|
||||
"worker-farm": "^1.5.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -24986,9 +25099,9 @@
|
|||
"integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus="
|
||||
},
|
||||
"worker-farm": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz",
|
||||
"integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==",
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz",
|
||||
"integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==",
|
||||
"requires": {
|
||||
"errno": "~0.1.7"
|
||||
}
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"chrome": "58",
|
||||
"esmodules": true
|
||||
"chrome": "68"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -16,8 +15,14 @@
|
|||
],
|
||||
"sourceMaps": "inline",
|
||||
"plugins": [
|
||||
"@babel/proposal-class-properties",
|
||||
"@babel/plugin-transform-react-jsx",
|
||||
[
|
||||
"@babel/proposal-decorators",
|
||||
{
|
||||
"decoratorsBeforeExport": true
|
||||
}
|
||||
],
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-runtime"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -46,9 +46,10 @@
|
|||
"@babel/plugin-proposal-class-properties": "^7.1.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
|
||||
"@babel/plugin-transform-react-jsx": "^7.0.0",
|
||||
"@babel/plugin-transform-runtime": "^7.1.0",
|
||||
"@babel/plugin-transform-runtime": "^7.4.4",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/preset-typescript": "^7.1.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.4.0",
|
||||
"@types/enzyme": "^3.1.10",
|
||||
"@types/deep-diff": "^1.0.0",
|
||||
"@types/jest": "^22.2.3",
|
||||
|
@ -57,7 +58,7 @@
|
|||
"@types/request": "^2.47.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "23.6.0",
|
||||
"babel-loader": "^8.0.2",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-preset-react-app": "^3.1.1",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"coveralls": "^3.0.1",
|
||||
|
@ -86,9 +87,10 @@
|
|||
"rimraf": "^2.6.2",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.21.0",
|
||||
"ts-loader": "^4.4.2",
|
||||
"terser": "^4.0.0",
|
||||
"typescript": "3.1.1",
|
||||
"typings-for-css-modules-loader": "^1.7.0",
|
||||
"uglifyjs-webpack-plugin": "^2.1.3",
|
||||
"url-loader": "^1.0.1",
|
||||
"webpack": "4.16.4",
|
||||
"webpack-cli": "^3.1.1",
|
||||
|
@ -105,7 +107,7 @@
|
|||
"@uifabric/merge-styles": "^6.2.0",
|
||||
"@uifabric/styling": "^5.20.0",
|
||||
"base64url": "2.0.0",
|
||||
"botframework-config": "4.0.0-preview1.3.4",
|
||||
"botframework-config": "4.4.0",
|
||||
"botframework-schema": "^4.3.4",
|
||||
"botframework-webchat": "^4.4.1",
|
||||
"botframework-webchat-core": "^4.3.0",
|
||||
|
|
|
@ -31,16 +31,45 @@
|
|||
// 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,
|
||||
CommandRegistry,
|
||||
CommandServiceImpl,
|
||||
CommandServiceInstance,
|
||||
} 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 { BotCommands } from './botCommands';
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
const mockBotInfo = {
|
||||
path: 'some/path.bot',
|
||||
|
@ -74,24 +103,27 @@ jest.mock('../data/store', () => ({
|
|||
return mockStore;
|
||||
},
|
||||
}));
|
||||
jest.mock('../ui/dialogs/', () => ({}));
|
||||
|
||||
describe('The bot commands', () => {
|
||||
let registry: CommandRegistryImpl;
|
||||
let commandService: CommandServiceImpl;
|
||||
let registry: CommandRegistry;
|
||||
beforeAll(() => {
|
||||
registry = new CommandRegistryImpl();
|
||||
registerCommands(registry);
|
||||
new BotCommands();
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
registry = commandService.registry;
|
||||
});
|
||||
|
||||
it('should make the appropriate calls to switch bots', () => {
|
||||
it('should make the appropriate calls to switch bots', async () => {
|
||||
const remoteCallArgs = [];
|
||||
CommandServiceImpl.remoteCall = async (...args: any[]) => {
|
||||
commandService.remoteCall = async (...args: any[]) => {
|
||||
remoteCallArgs.push(args);
|
||||
return true;
|
||||
return true as any;
|
||||
};
|
||||
const spy = jest.spyOn(ActiveBotHelper, 'confirmAndSwitchBots');
|
||||
const { handler } = registry.getCommand(SharedConstants.Commands.Bot.Switch);
|
||||
handler({});
|
||||
const spy = jest.spyOn(ActiveBotHelper, 'confirmAndSwitchBots').mockResolvedValueOnce(true);
|
||||
const handler = registry.getCommand(SharedConstants.Commands.Bot.Switch);
|
||||
await handler({});
|
||||
expect(spy).toHaveBeenCalledWith({});
|
||||
expect(remoteCallArgs[0][0]).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
|
||||
expect(remoteCallArgs[0][1]).toBe('bot_open');
|
||||
|
@ -103,14 +135,14 @@ describe('The bot commands', () => {
|
|||
|
||||
it('should make the appropriate calls to close a bot', () => {
|
||||
const spy = jest.spyOn(ActiveBotHelper, 'confirmAndCloseBot');
|
||||
const { handler } = registry.getCommand(SharedConstants.Commands.Bot.Close);
|
||||
const handler = registry.getCommand(SharedConstants.Commands.Bot.Close);
|
||||
handler();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should make the appropriate calls to load a bot when the bot does not yet exist', () => {
|
||||
const createSpy = jest.spyOn(ActiveBotHelper, 'confirmAndCreateBot');
|
||||
const { handler } = registry.getCommand(SharedConstants.Commands.Bot.Load);
|
||||
const handler = registry.getCommand(SharedConstants.Commands.Bot.Load);
|
||||
handler({});
|
||||
|
||||
expect(createSpy).toHaveBeenCalledWith({}, '');
|
||||
|
@ -118,7 +150,7 @@ describe('The bot commands', () => {
|
|||
|
||||
it('should make the appropriate calls to load a bot when the bot exists', () => {
|
||||
const switchSpy = jest.spyOn(ActiveBotHelper, 'confirmAndSwitchBots');
|
||||
const { handler } = registry.getCommand(SharedConstants.Commands.Bot.Load);
|
||||
const handler = registry.getCommand(SharedConstants.Commands.Bot.Load);
|
||||
handler({ path: 'some/path.bot' });
|
||||
|
||||
expect(switchSpy).toHaveBeenCalledWith({ path: 'some/path.bot' });
|
||||
|
@ -126,8 +158,8 @@ 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 remoteCallSpy = jest.spyOn(commandService, 'remoteCall');
|
||||
const handler = registry.getCommand(SharedConstants.Commands.Bot.SyncBotList);
|
||||
handler([{}]);
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(BotActions.loadBotInfos([{}]));
|
||||
|
@ -136,11 +168,11 @@ describe('The bot commands', () => {
|
|||
|
||||
it('should make the appropriate call when setting the active bot', async () => {
|
||||
const remoteCallArgs = [];
|
||||
CommandServiceImpl.remoteCall = async (...args: any[]) => {
|
||||
commandService.remoteCall = async (...args: any[]) => {
|
||||
remoteCallArgs.push(args);
|
||||
return true;
|
||||
return true as any;
|
||||
};
|
||||
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);
|
||||
|
@ -149,10 +181,8 @@ 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 transcriptFilesUpdated = registry.getCommand(SharedConstants.Commands.Bot.TranscriptFilesUpdated);
|
||||
const transcriptPathUpdated = registry.getCommand(SharedConstants.Commands.Bot.TranscriptsPathUpdated);
|
||||
transcriptFilesUpdated([{ path: 'transcript/path.transcript' }]);
|
||||
transcriptPathUpdated('transcript/');
|
||||
const state: any = mockStore.getState();
|
||||
|
@ -161,8 +191,8 @@ describe('The bot commands', () => {
|
|||
});
|
||||
|
||||
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 chatFilesUpdated = registry.getCommand(SharedConstants.Commands.Bot.ChatFilesUpdated);
|
||||
const chatPathUpdated = registry.getCommand(SharedConstants.Commands.Bot.ChatsPathUpdated);
|
||||
chatFilesUpdated([{ path: 'chat/path.chat' }]);
|
||||
chatPathUpdated('chat/');
|
||||
const state: any = mockStore.getState();
|
||||
|
|
|
@ -32,8 +32,9 @@
|
|||
//
|
||||
|
||||
import { BotInfo, getBotDisplayName, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { BotConfigWithPath, CommandRegistryImpl } from '@bfemulator/sdk-shared';
|
||||
import { BotConfigWithPath, Command, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
import { IFileService } from 'botframework-config/lib/schema';
|
||||
import { newNotification } from '@bfemulator/app-shared';
|
||||
|
||||
import * as BotActions from '../data/action/botActions';
|
||||
import * as FileActions from '../data/action/fileActions';
|
||||
|
@ -45,82 +46,105 @@ import {
|
|||
} from '../data/action/resourcesAction';
|
||||
import { pathExistsInRecentBots } from '../data/botHelpers';
|
||||
import { store } from '../data/store';
|
||||
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
|
||||
import { ActiveBotHelper } from '../ui/helpers/activeBotHelper';
|
||||
import { beginAdd } from '../data/action/notificationActions';
|
||||
|
||||
const Commands = SharedConstants.Commands;
|
||||
|
||||
/** Registers bot commands */
|
||||
export function registerCommands(commandRegistry: CommandRegistryImpl) {
|
||||
const Commands = SharedConstants.Commands;
|
||||
export class BotCommands {
|
||||
@CommandServiceInstance()
|
||||
private commandService: CommandServiceImpl;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Switches the current active bot
|
||||
commandRegistry.registerCommand(Commands.Bot.Switch, (bot: BotConfigWithPath | string) => {
|
||||
@Command(Commands.Bot.Switch)
|
||||
protected async switchBot(bot: BotConfigWithPath | string) {
|
||||
let numOfServices;
|
||||
if (typeof bot !== 'string') {
|
||||
numOfServices = bot.services && bot.services.length;
|
||||
}
|
||||
CommandServiceImpl.remoteCall(Commands.Telemetry.TrackEvent, 'bot_open', {
|
||||
method: 'bots_list',
|
||||
numOfServices,
|
||||
}).catch(_e => void 0);
|
||||
return ActiveBotHelper.confirmAndSwitchBots(bot);
|
||||
});
|
||||
try {
|
||||
await this.commandService.remoteCall(Commands.Telemetry.TrackEvent, 'bot_open', {
|
||||
method: 'bots_list',
|
||||
numOfServices,
|
||||
});
|
||||
return ActiveBotHelper.confirmAndSwitchBots(bot);
|
||||
} catch (e) {
|
||||
await beginAdd(newNotification(e));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Closes the current active bot
|
||||
commandRegistry.registerCommand(Commands.Bot.Close, () => ActiveBotHelper.confirmAndCloseBot());
|
||||
@Command(Commands.Bot.Close)
|
||||
protected async closeBot() {
|
||||
try {
|
||||
await ActiveBotHelper.confirmAndCloseBot();
|
||||
} catch (e) {
|
||||
await beginAdd(newNotification(e));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Browse for a .bot file and open it
|
||||
commandRegistry.registerCommand(Commands.Bot.OpenBrowse, () => ActiveBotHelper.confirmAndOpenBotFromFile());
|
||||
@Command(Commands.Bot.OpenBrowse)
|
||||
protected async browseForBotFile() {
|
||||
try {
|
||||
await ActiveBotHelper.confirmAndOpenBotFromFile();
|
||||
} catch (e) {
|
||||
await beginAdd(newNotification(e));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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, '');
|
||||
}
|
||||
return ActiveBotHelper.confirmAndSwitchBots(bot);
|
||||
@Command(Commands.Bot.Load)
|
||||
protected loadBot(bot: BotConfigWithPath): Promise<any> {
|
||||
if (!pathExistsInRecentBots(bot.path)) {
|
||||
// create and switch bots
|
||||
return ActiveBotHelper.confirmAndCreateBot(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.loadBotInfos(bots));
|
||||
await CommandServiceImpl.remoteCall(Commands.Electron.UpdateFileMenu);
|
||||
}
|
||||
);
|
||||
@Command(Commands.Bot.SyncBotList)
|
||||
protected async syncBotList(bots: BotInfo[]): Promise<void> {
|
||||
store.dispatch(BotActions.loadBotInfos(bots));
|
||||
await this.commandService.remoteCall(Commands.Electron.UpdateFileMenu);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sets a bot as active (called from server-side)
|
||||
commandRegistry.registerCommand(Commands.Bot.SetActive, async (bot: BotConfigWithPath, botDirectory: string) => {
|
||||
@Command(Commands.Bot.SetActive)
|
||||
protected async setActiveBot(bot: BotConfigWithPath, botDirectory: string) {
|
||||
store.dispatch(BotActions.setActiveBot(bot));
|
||||
store.dispatch(FileActions.setRoot(botDirectory));
|
||||
await Promise.all([
|
||||
CommandServiceImpl.remoteCall(Commands.Electron.UpdateFileMenu),
|
||||
CommandServiceImpl.remoteCall(Commands.Electron.SetTitleBar, getBotDisplayName(bot)),
|
||||
this.commandService.remoteCall(Commands.Electron.UpdateFileMenu),
|
||||
this.commandService.remoteCall(Commands.Electron.SetTitleBar, getBotDisplayName(bot)),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
commandRegistry.registerCommand(Commands.Bot.TranscriptFilesUpdated, (transcripts: IFileService[]) => {
|
||||
@Command(Commands.Bot.TranscriptFilesUpdated)
|
||||
protected transcriptFilesUpdated(transcripts: IFileService[]) {
|
||||
store.dispatch(transcriptsUpdated(transcripts));
|
||||
});
|
||||
}
|
||||
|
||||
commandRegistry.registerCommand(Commands.Bot.ChatFilesUpdated, (chatFiles: IFileService[]) => {
|
||||
@Command(Commands.Bot.ChatFilesUpdated)
|
||||
protected chatFilesUpdated(chatFiles: IFileService[]) {
|
||||
store.dispatch(chatFilesUpdated(chatFiles));
|
||||
});
|
||||
}
|
||||
|
||||
commandRegistry.registerCommand(Commands.Bot.TranscriptsPathUpdated, (path: string) => {
|
||||
@Command(Commands.Bot.TranscriptsPathUpdated)
|
||||
protected transcriptsPathUpdated(path: string) {
|
||||
store.dispatch(transcriptDirectoryUpdated(path));
|
||||
});
|
||||
}
|
||||
|
||||
commandRegistry.registerCommand(Commands.Bot.ChatsPathUpdated, (path: string) => {
|
||||
@Command(Commands.Bot.ChatsPathUpdated)
|
||||
protected chatsPathUpdated(path: string) {
|
||||
store.dispatch(chatsDirectoryUpdated(path));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
//
|
||||
// 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';
|
||||
|
||||
export const CommandRegistry = new CommandRegistryImpl();
|
|
@ -32,36 +32,41 @@
|
|||
//
|
||||
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
|
||||
import { Command } from '@bfemulator/sdk-shared';
|
||||
|
||||
const { Electron } = SharedConstants.Commands;
|
||||
|
||||
/** Registers electron commands */
|
||||
export function registerCommands(commandRegistry: CommandRegistryImpl) {
|
||||
const { Electron } = SharedConstants.Commands;
|
||||
export class ElectronCommands {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Toggle inspector dev tools for all open inspectors
|
||||
commandRegistry.registerCommand(Electron.ToggleDevTools, () => {
|
||||
@Command(Electron.ToggleDevTools)
|
||||
protected toggleDevTools() {
|
||||
window.dispatchEvent(new Event('toggle-inspector-devtools'));
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// An update is ready to install
|
||||
commandRegistry.registerCommand(Electron.UpdateAvailable, (...args: any[]) => {
|
||||
@Command(Electron.UpdateAvailable)
|
||||
protected emulatorUpdateAvailable(...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, () => {
|
||||
@Command(Electron.UpdateNotAvailable)
|
||||
protected emulatorUpdateNotAvailable() {
|
||||
// TODO: Show a notification
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Application is up to date');
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Open About dialog
|
||||
commandRegistry.registerCommand(Electron.ShowAboutDialog, () => {
|
||||
@Command(Electron.ShowAboutDialog)
|
||||
protected showAboutDialog() {
|
||||
// TODO: Show about dialog (native dialog box)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,8 +31,8 @@
|
|||
// 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 { CommandRegistry, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { clientAwareSettingsChanged } from '../data/action/clientAwareSettingsActions';
|
||||
import { beginAdd } from '../data/action/notificationActions';
|
||||
|
@ -42,10 +42,9 @@ import { clientAwareSettings } from '../data/reducer/clientAwareSettingsReducer'
|
|||
import { editor } from '../data/reducer/editor';
|
||||
import { framework } from '../data/reducer/frameworkSettingsReducer';
|
||||
import { RootState } from '../data/store';
|
||||
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
|
||||
import { frameworkSettingsChanged } from '../data/action/frameworkSettingsActions';
|
||||
|
||||
import { registerCommands } from './emulatorCommands';
|
||||
import { EmulatorCommands } from './emulatorCommands';
|
||||
|
||||
const mockEndpoint = {
|
||||
endpoint: 'https://localhost:8080/api/messages',
|
||||
|
@ -57,13 +56,41 @@ jest.mock('../data/store', () => ({
|
|||
return mockStore;
|
||||
},
|
||||
}));
|
||||
jest.mock('../ui/dialogs/', () => ({}));
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
describe('The emulator commands', () => {
|
||||
let registry: CommandRegistryImpl;
|
||||
let commandService: CommandServiceImpl;
|
||||
let registry: CommandRegistry;
|
||||
beforeAll(() => {
|
||||
registry = new CommandRegistryImpl();
|
||||
registerCommands(registry);
|
||||
new EmulatorCommands();
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
registry = commandService.registry;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -75,12 +102,14 @@ describe('The emulator commands', () => {
|
|||
locale: 'en-us',
|
||||
serverUrl: 'https://localhost',
|
||||
debugMode: 1,
|
||||
appPath: '',
|
||||
savedBotUrls: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
|
@ -93,7 +122,7 @@ describe('The emulator commands', () => {
|
|||
it('should open a new emulator tabbed document for an endpoint and use the custom user id', () => {
|
||||
let state: RootState = mockStore.getState();
|
||||
mockStore.dispatch(frameworkSettingsChanged({ ...state.framework, userGUID: 'customUserId' }));
|
||||
const { handler } = registry.getCommand(SharedConstants.Commands.Emulator.NewLiveChat);
|
||||
const handler = registry.getCommand(SharedConstants.Commands.Emulator.NewLiveChat);
|
||||
const documentId = handler(mockEndpoint, false);
|
||||
state = mockStore.getState();
|
||||
const document = state.chat.chats[documentId];
|
||||
|
@ -101,7 +130,7 @@ 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',
|
||||
|
@ -114,7 +143,7 @@ describe('The emulator commands', () => {
|
|||
});
|
||||
|
||||
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);
|
||||
|
||||
|
@ -124,9 +153,9 @@ 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 callSpy = jest.spyOn(CommandServiceImpl, 'call').mockResolvedValue(null);
|
||||
const handler = registry.getCommand(SharedConstants.Commands.Emulator.PromptToOpenTranscript);
|
||||
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue('transcript.transcript');
|
||||
const callSpy = jest.spyOn(commandService, 'call').mockResolvedValue(null);
|
||||
|
||||
await handler();
|
||||
|
||||
|
@ -144,9 +173,9 @@ describe('The emulator commands', () => {
|
|||
});
|
||||
|
||||
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(() => {
|
||||
const handler = registry.getCommand(SharedConstants.Commands.Emulator.PromptToOpenTranscript);
|
||||
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue('transcript.transcript');
|
||||
const callSpy = jest.spyOn(commandService, 'call').mockImplementationOnce(() => {
|
||||
throw new Error('Oh noes!');
|
||||
});
|
||||
const dispatchSpy = jest.spyOn(mockStore, 'dispatch');
|
||||
|
@ -163,21 +192,21 @@ describe('The emulator commands', () => {
|
|||
});
|
||||
|
||||
it('should reload a transcript', async () => {
|
||||
const { handler: openTranscriptHandler } = registry.getCommand(SharedConstants.Commands.Emulator.OpenTranscript);
|
||||
const 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);
|
||||
});
|
||||
|
||||
it('should open a chat file', async () => {
|
||||
const callSpy = jest.spyOn(CommandServiceImpl, 'call').mockResolvedValue(true);
|
||||
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue(true);
|
||||
const callSpy = jest.spyOn(commandService, 'call').mockResolvedValue(true);
|
||||
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue(true);
|
||||
|
||||
const { handler: openChatFileHandler } = registry.getCommand(SharedConstants.Commands.Emulator.OpenChatFile);
|
||||
const openChatFileHandler = registry.getCommand(SharedConstants.Commands.Emulator.OpenChatFile);
|
||||
await openChatFileHandler('some/path.chat', true);
|
||||
expect(remoteCallSpy).toHaveBeenCalledWith(SharedConstants.Commands.Emulator.OpenChatFile, 'some/path.chat');
|
||||
expect(callSpy).toHaveBeenCalledWith(
|
||||
|
|
|
@ -33,9 +33,10 @@
|
|||
// import base64Url from 'base64url';
|
||||
// import { createDirectLine } from 'botframework-webchat';
|
||||
import { DebugMode, newNotification, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { CommandRegistryImpl, isLocalHostUrl, uniqueId } from '@bfemulator/sdk-shared';
|
||||
import { CommandServiceImpl, CommandServiceInstance, isLocalHostUrl, uniqueId } from '@bfemulator/sdk-shared';
|
||||
import { IEndpointService } from 'botframework-config/lib/schema';
|
||||
import { Activity } from 'botframework-schema';
|
||||
import { Command } from '@bfemulator/sdk-shared';
|
||||
|
||||
import * as Constants from '../constants';
|
||||
import * as ChatActions from '../data/action/chatActions';
|
||||
|
@ -43,107 +44,105 @@ import * as EditorActions from '../data/action/editorActions';
|
|||
import { beginAdd } from '../data/action/notificationActions';
|
||||
import { getTabGroupForDocument } from '../data/editorHelpers';
|
||||
import { store } from '../data/store';
|
||||
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
|
||||
|
||||
/** Registers emulator (actual conversation emulation logic) commands */
|
||||
export function registerCommands(commandRegistry: CommandRegistryImpl) {
|
||||
const {
|
||||
Emulator,
|
||||
Telemetry: { TrackEvent },
|
||||
} = SharedConstants.Commands;
|
||||
const {
|
||||
Emulator,
|
||||
Telemetry: { TrackEvent },
|
||||
} = SharedConstants.Commands;
|
||||
|
||||
export class EmulatorCommands {
|
||||
@CommandServiceInstance()
|
||||
private commandService: CommandServiceImpl;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Open a new emulator tabbed document
|
||||
commandRegistry.registerCommand(
|
||||
Emulator.NewLiveChat,
|
||||
(
|
||||
endpoint: IEndpointService,
|
||||
focusExistingChat: boolean = false,
|
||||
conversationId: string,
|
||||
mode: ChatActions.ChatMode = 'livechat'
|
||||
) => {
|
||||
const state = store.getState();
|
||||
let documentId: string;
|
||||
@Command(Emulator.NewLiveChat)
|
||||
protected newLiveChat(
|
||||
endpoint: IEndpointService,
|
||||
focusExistingChat: boolean = false,
|
||||
conversationId: string,
|
||||
mode: ChatActions.ChatMode = 'livechat'
|
||||
) {
|
||||
const state = store.getState();
|
||||
let documentId: string;
|
||||
|
||||
if (focusExistingChat && state.chat.chats) {
|
||||
const { chats } = state.chat;
|
||||
documentId = Object.keys(chats).find(docId => {
|
||||
const { [docId]: chat } = chats;
|
||||
// If we have a conversationId, the match must include it.
|
||||
return chat.endpointUrl === endpoint.endpoint && (!conversationId || chat.conversationId === conversationId);
|
||||
});
|
||||
}
|
||||
|
||||
if (!documentId) {
|
||||
documentId = uniqueId();
|
||||
const { currentUserId } = state.clientAwareSettings.users;
|
||||
const customUserId = state.framework.userGUID;
|
||||
const action = ChatActions.newChat(documentId, mode, {
|
||||
botId: 'bot',
|
||||
endpointId: endpoint.id,
|
||||
endpointUrl: endpoint.endpoint,
|
||||
userId: customUserId || currentUserId,
|
||||
conversationId,
|
||||
// directLine: createDirectLine({
|
||||
// secret: base64Url.encode(JSON.stringify({ conversationId, endpointId: endpoint.id })),
|
||||
// domain: `${ state.clientAwareSettings.serverUrl }/v3/directline`,
|
||||
// webSocket: false,
|
||||
// })
|
||||
});
|
||||
if (state.clientAwareSettings.debugMode === DebugMode.Sidecar) {
|
||||
action.payload.ui.horizontalSplitter[0].percentage = 75;
|
||||
action.payload.ui.verticalSplitter[0].percentage = 25;
|
||||
}
|
||||
store.dispatch(action);
|
||||
}
|
||||
|
||||
if (!isLocalHostUrl(endpoint.endpoint)) {
|
||||
CommandServiceImpl.remoteCall(TrackEvent, 'livechat_openRemote').catch(_e => void 0);
|
||||
}
|
||||
|
||||
store.dispatch(
|
||||
EditorActions.open({
|
||||
contentType: Constants.CONTENT_TYPE_LIVE_CHAT,
|
||||
documentId,
|
||||
isGlobal: false,
|
||||
})
|
||||
);
|
||||
return documentId;
|
||||
if (focusExistingChat && state.chat.chats) {
|
||||
const { chats } = state.chat;
|
||||
documentId = Object.keys(chats).find(docId => {
|
||||
const { [docId]: chat } = chats;
|
||||
// If we have a conversationId, the match must include it.
|
||||
return chat.endpointUrl === endpoint.endpoint && (!conversationId || chat.conversationId === conversationId);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (!documentId) {
|
||||
documentId = uniqueId();
|
||||
const { currentUserId } = state.clientAwareSettings.users;
|
||||
const customUserId = state.framework.userGUID;
|
||||
const action = ChatActions.newChat(documentId, mode, {
|
||||
botId: 'bot',
|
||||
endpointId: endpoint.id,
|
||||
endpointUrl: endpoint.endpoint,
|
||||
userId: customUserId || currentUserId,
|
||||
conversationId,
|
||||
// directLine: createDirectLine({
|
||||
// secret: base64Url.encode(JSON.stringify({ conversationId, endpointId: endpoint.id })),
|
||||
// domain: `${ state.clientAwareSettings.serverUrl }/v3/directline`,
|
||||
// webSocket: false,
|
||||
// })
|
||||
});
|
||||
if (state.clientAwareSettings.debugMode === DebugMode.Sidecar) {
|
||||
action.payload.ui.horizontalSplitter[0].percentage = 75;
|
||||
action.payload.ui.verticalSplitter[0].percentage = 25;
|
||||
}
|
||||
store.dispatch(action);
|
||||
}
|
||||
|
||||
if (!isLocalHostUrl(endpoint.endpoint)) {
|
||||
this.commandService.remoteCall(TrackEvent, 'livechat_openRemote').catch(_e => void 0);
|
||||
}
|
||||
|
||||
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,
|
||||
(filePath: string, fileName: string, additionalData?: object) => {
|
||||
const tabGroup = getTabGroupForDocument(filePath);
|
||||
const { currentUserId } = store.getState().clientAwareSettings.users;
|
||||
if (!tabGroup) {
|
||||
store.dispatch(
|
||||
ChatActions.newChat(filePath, 'transcript', {
|
||||
...additionalData,
|
||||
botId: 'bot',
|
||||
userId: currentUserId,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Command(Emulator.OpenTranscript)
|
||||
protected openTranscript(filePath: string, fileName: string, additionalData?: object) {
|
||||
const tabGroup = getTabGroupForDocument(filePath);
|
||||
const { currentUserId } = store.getState().clientAwareSettings.users;
|
||||
if (!tabGroup) {
|
||||
store.dispatch(
|
||||
EditorActions.open({
|
||||
contentType: Constants.CONTENT_TYPE_TRANSCRIPT,
|
||||
documentId: filePath,
|
||||
fileName,
|
||||
filePath,
|
||||
isGlobal: false,
|
||||
ChatActions.newChat(filePath, 'transcript', {
|
||||
...additionalData,
|
||||
botId: 'bot',
|
||||
userId: currentUserId,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
store.dispatch(
|
||||
EditorActions.open({
|
||||
contentType: Constants.CONTENT_TYPE_TRANSCRIPT,
|
||||
documentId: filePath,
|
||||
fileName,
|
||||
filePath,
|
||||
isGlobal: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Prompt to open a transcript file, then open it
|
||||
commandRegistry.registerCommand(Emulator.PromptToOpenTranscript, async () => {
|
||||
@Command(Emulator.PromptToOpenTranscript)
|
||||
protected async promptToOpenTranscript() {
|
||||
const dialogOptions = {
|
||||
title: 'Open transcript file',
|
||||
buttonLabel: 'Choose file',
|
||||
|
@ -157,53 +156,54 @@ export function registerCommands(commandRegistry: CommandRegistryImpl) {
|
|||
};
|
||||
try {
|
||||
const { ShowOpenDialog } = SharedConstants.Commands.Electron;
|
||||
const filename = await CommandServiceImpl.remoteCall(ShowOpenDialog, dialogOptions);
|
||||
const filename = await this.commandService.remoteCall(ShowOpenDialog, dialogOptions);
|
||||
if (filename) {
|
||||
await CommandServiceImpl.call(Emulator.OpenTranscript, filename);
|
||||
CommandServiceImpl.remoteCall(TrackEvent, 'transcriptFile_open', {
|
||||
method: 'file_menu',
|
||||
}).catch(_e => void 0);
|
||||
await this.commandService.call(Emulator.OpenTranscript, filename);
|
||||
this.commandService
|
||||
.remoteCall(TrackEvent, 'transcriptFile_open', {
|
||||
method: 'file_menu',
|
||||
})
|
||||
.catch(_e => void 0);
|
||||
}
|
||||
} catch (e) {
|
||||
const errMsg = `Error while opening transcript file: ${e}`;
|
||||
const notification = newNotification(errMsg);
|
||||
store.dispatch(beginAdd(notification));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Same as open transcript, except that it closes the transcript first, before reopening it
|
||||
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(ChatActions.closeDocument(filePath));
|
||||
}
|
||||
store.dispatch(
|
||||
ChatActions.newChat(filePath, 'transcript', {
|
||||
...additionalData,
|
||||
botId: 'bot',
|
||||
userId: currentUserId,
|
||||
})
|
||||
);
|
||||
store.dispatch(
|
||||
EditorActions.open({
|
||||
contentType: Constants.CONTENT_TYPE_TRANSCRIPT,
|
||||
documentId: filePath,
|
||||
filePath,
|
||||
fileName,
|
||||
isGlobal: false,
|
||||
})
|
||||
);
|
||||
@Command(Emulator.ReloadTranscript)
|
||||
protected 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(ChatActions.closeDocument(filePath));
|
||||
}
|
||||
);
|
||||
store.dispatch(
|
||||
ChatActions.newChat(filePath, 'transcript', {
|
||||
...additionalData,
|
||||
botId: 'bot',
|
||||
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) => {
|
||||
@Command(Emulator.OpenChatFile)
|
||||
protected async openChatFile(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 {
|
||||
|
@ -212,16 +212,16 @@ export function registerCommands(commandRegistry: CommandRegistryImpl) {
|
|||
}: {
|
||||
activities: Activity[];
|
||||
fileName: string;
|
||||
} = await CommandServiceImpl.remoteCall(Emulator.OpenChatFile, filePath);
|
||||
} = await this.commandService.remoteCall<any>(Emulator.OpenChatFile, filePath);
|
||||
|
||||
// open or reload the transcript
|
||||
if (reload) {
|
||||
await CommandServiceImpl.call(Emulator.ReloadTranscript, filePath, fileName, { activities, inMemory: true });
|
||||
await this.commandService.call(Emulator.ReloadTranscript, filePath, fileName, { activities, inMemory: true });
|
||||
} else {
|
||||
await CommandServiceImpl.call(Emulator.OpenTranscript, filePath, fileName, { activities, inMemory: true });
|
||||
await this.commandService.call(Emulator.OpenTranscript, filePath, fileName, { activities, inMemory: true });
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`Error while retrieving activities from main side: ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,39 +32,44 @@
|
|||
//
|
||||
|
||||
import { isChatFile, isTranscriptFile, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
|
||||
import { Command } from '@bfemulator/sdk-shared';
|
||||
|
||||
import * as EditorActions from '../data/action/editorActions';
|
||||
import * as FileActions from '../data/action/fileActions';
|
||||
import { store } from '../data/store';
|
||||
|
||||
const { File } = SharedConstants.Commands;
|
||||
|
||||
/** Registers file commands */
|
||||
export function registerCommands(commandRegistry: CommandRegistryImpl) {
|
||||
const { File } = SharedConstants.Commands;
|
||||
export class FileCommands {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Adds a file to the file store
|
||||
commandRegistry.registerCommand(File.Add, payload => {
|
||||
@Command(File.Add)
|
||||
protected addFileToStore(payload) {
|
||||
store.dispatch(FileActions.addFile(payload));
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Removes a file from the file store
|
||||
commandRegistry.registerCommand(File.Remove, path => {
|
||||
@Command(File.Remove)
|
||||
protected removeFileFromStore(path) {
|
||||
store.dispatch(FileActions.removeFile(path));
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Clears the file store
|
||||
commandRegistry.registerCommand(File.Clear, () => {
|
||||
@Command(File.Clear)
|
||||
protected clearFileStore() {
|
||||
store.dispatch(FileActions.clear());
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Called for files in the bot's directory whose contents have changed on disk
|
||||
commandRegistry.registerCommand(File.Changed, (filename: string) => {
|
||||
@Command(File.Changed)
|
||||
protected fileChangedOnDisk(filename: string) {
|
||||
// add the filename to pending updates and prompt the user once the document is focused again
|
||||
if (isChatFile(filename) || isTranscriptFile(filename)) {
|
||||
store.dispatch(EditorActions.addDocPendingChange(filename));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,5 +31,22 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
export * from './commandRegistry';
|
||||
export * from './registerAllCommands';
|
||||
import { BotCommands } from './botCommands';
|
||||
import { ElectronCommands } from './electronCommands';
|
||||
import { EmulatorCommands } from './emulatorCommands';
|
||||
import { FileCommands } from './fileCommands';
|
||||
import { NotificationCommands } from './notificationCommands';
|
||||
import { SettingsCommands } from './settingsCommands';
|
||||
import { UiCommands } from './uiCommands';
|
||||
import { MiscCommands } from './miscCommands';
|
||||
|
||||
export const commands = [
|
||||
new BotCommands(),
|
||||
new ElectronCommands(),
|
||||
new EmulatorCommands(),
|
||||
new FileCommands(),
|
||||
new NotificationCommands(),
|
||||
new SettingsCommands(),
|
||||
new UiCommands(),
|
||||
new MiscCommands(),
|
||||
];
|
||||
|
|
|
@ -32,16 +32,18 @@
|
|||
//
|
||||
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
|
||||
import { Command } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { store } from '../data/store';
|
||||
|
||||
const Commands = SharedConstants.Commands.Misc;
|
||||
|
||||
/** Registers miscellaneous commands */
|
||||
export function registerCommands(commandRegistry: CommandRegistryImpl) {
|
||||
const Commands = SharedConstants.Commands.Misc;
|
||||
export class MiscCommands {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Returns the store's state
|
||||
commandRegistry.registerCommand(Commands.GetStoreState, () => {
|
||||
@Command(Commands.GetStoreState)
|
||||
protected getStoreState() {
|
||||
return store.getState();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,28 +32,31 @@
|
|||
//
|
||||
|
||||
import { Notification, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
|
||||
import { Command } from '@bfemulator/sdk-shared';
|
||||
|
||||
import * as NotificationActions from '../data/action/notificationActions';
|
||||
import { store } from '../data/store';
|
||||
import { getGlobal } from '../utils';
|
||||
|
||||
const Commands = SharedConstants.Commands.Notifications;
|
||||
|
||||
/** Registers notification commands */
|
||||
export function registerCommands(commandRegistry: CommandRegistryImpl) {
|
||||
const Commands = SharedConstants.Commands.Notifications;
|
||||
export class NotificationCommands {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Adds a notification from the main side to the store / notification manager
|
||||
commandRegistry.registerCommand(Commands.Add, (notification: Notification) => {
|
||||
@Command(Commands.Add)
|
||||
protected addNotificationFromMain(notification: Notification) {
|
||||
if (!notification) {
|
||||
notification = getGlobal(SharedConstants.NOTIFICATION_FROM_MAIN);
|
||||
}
|
||||
|
||||
store.dispatch(NotificationActions.beginAdd(notification));
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Removes a notification from the store / notification manager
|
||||
commandRegistry.registerCommand(Commands.Remove, (id: string) => {
|
||||
@Command(Commands.Remove)
|
||||
protected removeNotificationFromStore(id: string) {
|
||||
store.dispatch(NotificationActions.beginRemove(id));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
//
|
||||
// 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 { 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 registerSettingsCommand } from './settingsCommands';
|
||||
import { registerCommands as registerUICommands } from './uiCommands';
|
||||
|
||||
/** Registers all commands */
|
||||
export function registerAllCommands(commandRegistry: CommandRegistryImpl) {
|
||||
LogService.registerCommands(commandRegistry);
|
||||
ExtensionManager.registerCommands(commandRegistry);
|
||||
registerBotCommands(commandRegistry);
|
||||
registerElectronCommands(commandRegistry);
|
||||
registerEmulatorCommands(commandRegistry);
|
||||
registerFileCommands(commandRegistry);
|
||||
registerMiscCommands(commandRegistry);
|
||||
registerNotificationCommands(commandRegistry);
|
||||
registerUICommands(commandRegistry);
|
||||
registerSettingsCommand(commandRegistry);
|
||||
}
|
|
@ -31,16 +31,15 @@
|
|||
// 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 { CommandRegistry, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
import { DebugMode, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { combineReducers, createStore } from 'redux';
|
||||
import { DebugMode } from '@bfemulator/app-shared';
|
||||
|
||||
import { clientAwareSettings } from '../data/reducer/clientAwareSettingsReducer';
|
||||
import { store } from '../data/store';
|
||||
import { clientAwareSettingsChanged } from '../data/action/clientAwareSettingsActions';
|
||||
|
||||
import { registerCommands } from './settingsCommands';
|
||||
import { SettingsCommands } from './settingsCommands';
|
||||
|
||||
const mockStore = createStore(combineReducers({ clientAwareSettings }));
|
||||
jest.mock('../data/store', () => ({
|
||||
|
@ -49,15 +48,44 @@ jest.mock('../data/store', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
describe('the settings commands', () => {
|
||||
let registry: CommandRegistryImpl;
|
||||
let commandService: CommandServiceImpl;
|
||||
let registry: CommandRegistry;
|
||||
beforeAll(() => {
|
||||
registry = new CommandRegistryImpl();
|
||||
registerCommands(registry);
|
||||
new SettingsCommands();
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
registry = commandService.registry;
|
||||
});
|
||||
|
||||
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);
|
||||
const dispatchSpy = jest.spyOn(store, 'dispatch');
|
||||
command({ debugMode: DebugMode.Normal });
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(clientAwareSettingsChanged({ debugMode: DebugMode.Normal } as any));
|
||||
|
|
|
@ -32,20 +32,24 @@
|
|||
//
|
||||
|
||||
import { ClientAwareSettings, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
|
||||
import { Command, CommandServiceImpl, CommandServiceInstance } 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;
|
||||
const { Settings } = SharedConstants.Commands;
|
||||
|
||||
commandRegistry.registerCommand(Settings.ReceiveGlobalSettings, async (settings: ClientAwareSettings) => {
|
||||
/** Registers settings commands */
|
||||
export class SettingsCommands {
|
||||
@CommandServiceInstance()
|
||||
private commandService: CommandServiceImpl;
|
||||
|
||||
@Command(Settings.ReceiveGlobalSettings)
|
||||
protected async receiveGlobalSettings(settings: ClientAwareSettings) {
|
||||
const state = store.getState();
|
||||
store.dispatch(clientAwareSettingsChanged(settings));
|
||||
if (state.clientAwareSettings.debugMode !== settings.debugMode) {
|
||||
await commandRegistry.getCommand(SharedConstants.Commands.UI.SwitchDebugMode).handler(settings.debugMode);
|
||||
await this.commandService.call(SharedConstants.Commands.UI.SwitchDebugMode, settings.debugMode);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
import { DebugMode, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
|
||||
import { CommandRegistry, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CONTENT_TYPE_APP_SETTINGS, DOCUMENT_ID_APP_SETTINGS } from '../constants';
|
||||
import { AzureAuthAction, AzureAuthWorkflow, invalidateArmToken } from '../data/action/azureAuthActions';
|
||||
|
@ -47,53 +47,73 @@ import {
|
|||
OpenBotDialogContainer,
|
||||
SecretPromptDialogContainer,
|
||||
} from '../ui/dialogs';
|
||||
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
|
||||
import { ExplorerActions } from '../data/action/explorerActions';
|
||||
import { SWITCH_DEBUG_MODE } from '../data/action/debugModeAction';
|
||||
import { ActiveBotHelper } from '../ui/helpers/activeBotHelper';
|
||||
|
||||
import { registerCommands } from './uiCommands';
|
||||
import { UiCommands } from './uiCommands';
|
||||
|
||||
jest.mock('../ui/dialogs', () => ({
|
||||
AzureLoginPromptDialogContainer: class {},
|
||||
AzureLoginSuccessDialogContainer: class {},
|
||||
BotCreationDialog: class {},
|
||||
DialogService: { showDialog: () => Promise.resolve(true) },
|
||||
SecretPromptDialog: class {},
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
const Commands = SharedConstants.Commands.UI;
|
||||
|
||||
describe('the uiCommands', () => {
|
||||
let registry: CommandRegistryImpl;
|
||||
let commandService: CommandServiceImpl;
|
||||
let registry: CommandRegistry;
|
||||
beforeAll(() => {
|
||||
registry = new CommandRegistryImpl();
|
||||
registerCommands(registry);
|
||||
new UiCommands();
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
registry = commandService.registry;
|
||||
});
|
||||
|
||||
it('should showExplorer the welcome page when the ShowWelcomePage command is dispatched', async () => {
|
||||
const spy = jest.spyOn(editorHelpers, 'showWelcomePage');
|
||||
await registry.getCommand(Commands.ShowWelcomePage).handler();
|
||||
await registry.getCommand(Commands.ShowWelcomePage)();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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 spy = jest.spyOn(DialogService, 'showDialog').mockResolvedValueOnce(true);
|
||||
const result = await registry.getCommand(Commands.ShowBotCreationDialog)();
|
||||
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 spy = jest.spyOn(DialogService, 'showDialog').mockResolvedValueOnce(true);
|
||||
const result = await registry.getCommand(Commands.ShowSecretPromptDialog)();
|
||||
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 spy = jest.spyOn(DialogService, 'showDialog').mockResolvedValueOnce(true);
|
||||
const result = await registry.getCommand(Commands.ShowOpenBotDialog)();
|
||||
expect(spy).toHaveBeenCalledWith(OpenBotDialogContainer);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
@ -103,7 +123,7 @@ describe('the uiCommands', () => {
|
|||
// eslint-disable-next-line prefer-const
|
||||
let arg: SelectNavBarAction = {} as SelectNavBarAction;
|
||||
store.dispatch = action => ((arg as any) = action);
|
||||
registry.getCommand(Commands.SwitchNavBarTab).handler('Do it Nauuuw!');
|
||||
registry.getCommand(Commands.SwitchNavBarTab)('Do it Nauuuw!');
|
||||
expect(arg.type).toBe(NavBarActions.select);
|
||||
expect(arg.payload.selection).toBe('Do it Nauuuw!');
|
||||
});
|
||||
|
@ -112,7 +132,7 @@ describe('the uiCommands', () => {
|
|||
// eslint-disable-next-line prefer-const
|
||||
let arg: OpenEditorAction = {} as OpenEditorAction;
|
||||
store.dispatch = action => ((arg as any) = action);
|
||||
registry.getCommand(Commands.ShowAppSettings).handler();
|
||||
registry.getCommand(Commands.ShowAppSettings)();
|
||||
expect(arg.type).toBe(EditorActions.open);
|
||||
expect(arg.payload.contentType).toBe(CONTENT_TYPE_APP_SETTINGS);
|
||||
expect(arg.payload.documentId).toBe(DOCUMENT_ID_APP_SETTINGS);
|
||||
|
@ -123,7 +143,7 @@ describe('the uiCommands', () => {
|
|||
// 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();
|
||||
registry.getCommand(Commands.SignInToAzure)();
|
||||
expect(arg.payload.loginSuccessDialog).toBe(AzureLoginSuccessDialogContainer);
|
||||
expect(arg.payload.promptDialog).toBe(AzureLoginPromptDialogContainer);
|
||||
});
|
||||
|
@ -132,17 +152,17 @@ describe('the uiCommands', () => {
|
|||
// eslint-disable-next-line prefer-const
|
||||
let arg: AzureAuthAction<void> = {} as AzureAuthAction<void>;
|
||||
store.dispatch = action => ((arg as any) = action);
|
||||
registry.getCommand(Commands.InvalidateAzureArmToken).handler();
|
||||
registry.getCommand(Commands.InvalidateAzureArmToken)();
|
||||
expect(arg).toEqual(invalidateArmToken());
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the proper href on the theme tag when the SwitchTheme command is dispatched', () => {
|
||||
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall');
|
||||
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall');
|
||||
const link = document.createElement('link');
|
||||
link.id = 'themeVars';
|
||||
document.querySelector('head').appendChild(link);
|
||||
registry.getCommand(Commands.SwitchTheme).handler('light', './light.css');
|
||||
registry.getCommand(Commands.SwitchTheme)('light', './light.css');
|
||||
expect(link.href).toBe('http://localhost/light.css');
|
||||
expect(remoteCallSpy).toHaveBeenCalledWith(SharedConstants.Commands.Telemetry.TrackEvent, 'app_chooseTheme', {
|
||||
themeName: 'light',
|
||||
|
@ -156,7 +176,7 @@ describe('the uiCommands', () => {
|
|||
return action;
|
||||
};
|
||||
const closeActiveBotSpy = jest.spyOn(ActiveBotHelper, 'closeActiveBot').mockResolvedValueOnce(true);
|
||||
await registry.getCommand(Commands.SwitchDebugMode).handler(DebugMode.Sidecar);
|
||||
await registry.getCommand(Commands.SwitchDebugMode)(DebugMode.Sidecar);
|
||||
expect(dispatchedActions.length).toBe(2);
|
||||
expect(closeActiveBotSpy).toHaveBeenCalled();
|
||||
[ExplorerActions.Show, SWITCH_DEBUG_MODE].forEach((type, index) =>
|
||||
|
@ -171,9 +191,7 @@ describe('the uiCommands', () => {
|
|||
dispatchedActions.push(action);
|
||||
return action;
|
||||
};
|
||||
await registry
|
||||
.getCommand(Commands.ShowMarkdownPage)
|
||||
.handler('http://localhost', 'Yo!', { navigator: { onLine: false } });
|
||||
await registry.getCommand(Commands.ShowMarkdownPage)('http://localhost', 'Yo!', { navigator: { onLine: false } });
|
||||
expect(dispatchedActions.length).toBe(1);
|
||||
expect(dispatchedActions[0].payload.meta).toEqual({
|
||||
markdown: '',
|
||||
|
@ -188,10 +206,8 @@ describe('the uiCommands', () => {
|
|||
dispatchedActions.push(action);
|
||||
return action;
|
||||
};
|
||||
jest.spyOn(CommandServiceImpl, 'remoteCall').mockRejectedValueOnce('oh noes! ENOTFOUND');
|
||||
await registry
|
||||
.getCommand(Commands.ShowMarkdownPage)
|
||||
.handler('http://localhost', 'Yo!', { navigator: { onLine: true } });
|
||||
jest.spyOn(commandService, 'remoteCall').mockRejectedValueOnce('oh noes! ENOTFOUND');
|
||||
await registry.getCommand(Commands.ShowMarkdownPage)('http://localhost', 'Yo!', { navigator: { onLine: true } });
|
||||
expect(dispatchedActions.length).toBe(1);
|
||||
expect(dispatchedActions[0].payload.meta).toEqual({
|
||||
markdown: '',
|
||||
|
@ -206,10 +222,8 @@ describe('the uiCommands', () => {
|
|||
dispatchedActions.push(action);
|
||||
return action;
|
||||
};
|
||||
jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValueOnce(true);
|
||||
await registry
|
||||
.getCommand(Commands.ShowMarkdownPage)
|
||||
.handler('http://localhost', 'Yo!', { navigator: { onLine: true } });
|
||||
jest.spyOn(commandService, 'remoteCall').mockResolvedValueOnce(true);
|
||||
await registry.getCommand(Commands.ShowMarkdownPage)('http://localhost', 'Yo!', { navigator: { onLine: true } });
|
||||
expect(dispatchedActions.length).toBe(1);
|
||||
expect(dispatchedActions[0].payload).toEqual({
|
||||
contentType: 'application/vnd.microsoft.bfemulator.document.markdown',
|
||||
|
|
|
@ -32,8 +32,9 @@
|
|||
//
|
||||
|
||||
import { DebugMode, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { CommandRegistry } from '@bfemulator/sdk-shared';
|
||||
import { Command, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
import { ServiceTypes } from 'botframework-config/lib/schema';
|
||||
import { newNotification } from '@bfemulator/app-shared';
|
||||
|
||||
import * as Constants from '../constants';
|
||||
import { azureArmTokenDataChanged, beginAzureAuthWorkflow, invalidateArmToken } from '../data/action/azureAuthActions';
|
||||
|
@ -46,7 +47,6 @@ import { switchTheme } from '../data/action/themeActions';
|
|||
import { getTabGroupForDocument, showMarkdownPage, showWelcomePage } from '../data/editorHelpers';
|
||||
import { AzureAuthState } from '../data/reducer/azureAuthReducer';
|
||||
import { store } from '../data/store';
|
||||
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
|
||||
import {
|
||||
AzureLoginFailedDialogContainer,
|
||||
AzureLoginPromptDialogContainer,
|
||||
|
@ -63,93 +63,96 @@ import {
|
|||
import * as ExplorerActions from '../data/action/explorerActions';
|
||||
import { closeConversation } from '../data/action/chatActions';
|
||||
import { ActiveBotHelper } from '../ui/helpers/activeBotHelper';
|
||||
import { beginAdd } from '../data/action/notificationActions';
|
||||
|
||||
const { UI, Telemetry } = SharedConstants.Commands;
|
||||
|
||||
/** Register UI commands (toggling UI) */
|
||||
export function registerCommands(commandRegistry: CommandRegistry) {
|
||||
const { UI, Telemetry } = SharedConstants.Commands;
|
||||
export class UiCommands {
|
||||
@CommandServiceInstance()
|
||||
private commandService: CommandServiceImpl;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shows the welcome page
|
||||
commandRegistry.registerCommand(UI.ShowWelcomePage, () => {
|
||||
@Command(UI.ShowWelcomePage)
|
||||
protected showWelcomePageDispatcher() {
|
||||
return showWelcomePage();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shows the markdown page after retrieving the remote source
|
||||
commandRegistry.registerCommand(
|
||||
UI.ShowMarkdownPage,
|
||||
async (urlOrMarkdown: string, label: string, windowRef = window) => {
|
||||
let markdown = '';
|
||||
let { onLine } = windowRef.navigator;
|
||||
if (!onLine) {
|
||||
return showMarkdownPage(markdown, label, onLine);
|
||||
}
|
||||
try {
|
||||
new URL(urlOrMarkdown); // Is this a valid URL?
|
||||
const bytes: ArrayBuffer = await CommandServiceImpl.remoteCall(
|
||||
SharedConstants.Commands.Electron.FetchRemote,
|
||||
urlOrMarkdown
|
||||
);
|
||||
markdown = new TextDecoder().decode(bytes);
|
||||
} catch (e) {
|
||||
if (typeof e === 'string' && ('' + e).includes('ENOTFOUND')) {
|
||||
onLine = false;
|
||||
} else {
|
||||
// assume this is markdown text
|
||||
markdown = urlOrMarkdown;
|
||||
}
|
||||
}
|
||||
@Command(UI.ShowMarkdownPage)
|
||||
protected async showMarkdownPage(urlOrMarkdown: string, label: string, windowRef = window) {
|
||||
let markdown = '';
|
||||
let { onLine } = windowRef.navigator;
|
||||
if (!onLine) {
|
||||
return showMarkdownPage(markdown, label, onLine);
|
||||
}
|
||||
);
|
||||
try {
|
||||
new URL(urlOrMarkdown); // Is this a valid URL?
|
||||
const bytes = await this.commandService.remoteCall<ArrayBuffer>(
|
||||
SharedConstants.Commands.Electron.FetchRemote,
|
||||
urlOrMarkdown
|
||||
);
|
||||
markdown = new TextDecoder().decode(bytes);
|
||||
} catch (e) {
|
||||
if (typeof e === 'string' && ('' + e).includes('ENOTFOUND')) {
|
||||
onLine = false;
|
||||
} else {
|
||||
// assume this is markdown text
|
||||
markdown = urlOrMarkdown;
|
||||
}
|
||||
}
|
||||
return showMarkdownPage(markdown, label, onLine);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shows a bot creation dialog
|
||||
commandRegistry.registerCommand(UI.ShowBotCreationDialog, async () => {
|
||||
@Command(UI.ShowBotCreationDialog)
|
||||
protected async showBotCreationPage() {
|
||||
return await DialogService.showDialog(BotCreationDialog);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shows a bot creation dialog
|
||||
commandRegistry.registerCommand(UI.ShowOpenBotDialog, async () => {
|
||||
@Command(UI.ShowOpenBotDialog)
|
||||
protected async showOpenBotDialog() {
|
||||
return await DialogService.showDialog(OpenBotDialogContainer);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shows a dialog prompting the user for a bot secret
|
||||
commandRegistry.registerCommand(UI.ShowSecretPromptDialog, async () => {
|
||||
@Command(UI.ShowSecretPromptDialog)
|
||||
protected async showSecretePromptDialog() {
|
||||
return await DialogService.showDialog(SecretPromptDialogContainer);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Switches navbar tab selection
|
||||
commandRegistry.registerCommand(
|
||||
UI.SwitchNavBarTab,
|
||||
(tabName: string): void => {
|
||||
store.dispatch(NavBarActions.select(tabName));
|
||||
}
|
||||
);
|
||||
@Command(UI.SwitchNavBarTab)
|
||||
protected switchNavBar(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,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
@Command(UI.ShowAppSettings)
|
||||
protected 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) => {
|
||||
@Command(UI.SwitchTheme)
|
||||
protected switchTheme(themeName: string, themeHref: string) {
|
||||
const linkTags = document.querySelectorAll<HTMLLinkElement>('[data-theme-component="true"]');
|
||||
const themeTag = document.getElementById('themeVars') as HTMLLinkElement;
|
||||
if (themeTag) {
|
||||
|
@ -157,14 +160,17 @@ export function registerCommands(commandRegistry: CommandRegistry) {
|
|||
}
|
||||
const themeComponents = Array.prototype.map.call(linkTags, link => link.href); // href is fully qualified
|
||||
store.dispatch(switchTheme(themeName, themeComponents));
|
||||
CommandServiceImpl.remoteCall(Telemetry.TrackEvent, 'app_chooseTheme', {
|
||||
themeName,
|
||||
}).catch();
|
||||
});
|
||||
this.commandService
|
||||
.remoteCall(Telemetry.TrackEvent, 'app_chooseTheme', {
|
||||
themeName,
|
||||
})
|
||||
.catch();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Debug mode from main
|
||||
commandRegistry.registerCommand(UI.SwitchDebugMode, async (debugMode: DebugMode) => {
|
||||
@Command(UI.SwitchDebugMode)
|
||||
protected async switchDebugMode(debugMode: DebugMode) {
|
||||
const {
|
||||
editor: { editors, activeEditor },
|
||||
} = store.getState();
|
||||
|
@ -180,11 +186,12 @@ export function registerCommands(commandRegistry: CommandRegistry) {
|
|||
store.dispatch(closeConversation(documentId));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Azure sign in
|
||||
commandRegistry.registerCommand(UI.SignInToAzure, (serviceType: ServiceTypes) => {
|
||||
@Command(UI.SignInToAzure)
|
||||
protected signIntoAzure(serviceType: ServiceTypes) {
|
||||
store.dispatch(
|
||||
beginAzureAuthWorkflow(
|
||||
AzureLoginPromptDialogContainer,
|
||||
|
@ -193,53 +200,64 @@ export function registerCommands(commandRegistry: CommandRegistry) {
|
|||
AzureLoginFailedDialogContainer
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
commandRegistry.registerCommand(UI.ArmTokenReceivedOnStartup, (azureAuth: AzureAuthState) => {
|
||||
@Command(UI.ArmTokenReceivedOnStartup)
|
||||
protected armTokenReceivedOnStartup(azureAuth: AzureAuthState) {
|
||||
store.dispatch(azureArmTokenDataChanged(azureAuth.access_token));
|
||||
});
|
||||
}
|
||||
|
||||
commandRegistry.registerCommand(UI.InvalidateAzureArmToken, () => {
|
||||
@Command(UI.InvalidateAzureArmToken)
|
||||
protected invalidateAzureArmToken() {
|
||||
store.dispatch(invalidateArmToken());
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Show post migration dialog on startup if the user has just been migrated
|
||||
commandRegistry.registerCommand(UI.ShowPostMigrationDialog, () => {
|
||||
@Command(UI.ShowPostMigrationDialog)
|
||||
protected showPostMigrationDialog() {
|
||||
return DialogService.showDialog(PostMigrationDialogContainer);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shows the progress indicator component
|
||||
commandRegistry.registerCommand(UI.ShowProgressIndicator, async (props?: ProgressIndicatorPayload) => {
|
||||
return await DialogService.showDialog(
|
||||
ProgressIndicatorContainer,
|
||||
props
|
||||
// eslint-disable-next-line no-console
|
||||
).catch(e => console.error(e));
|
||||
});
|
||||
@Command(UI.ShowProgressIndicator)
|
||||
protected async showProgressIndicator(props?: ProgressIndicatorPayload) {
|
||||
try {
|
||||
return await DialogService.showDialog(ProgressIndicatorContainer, props);
|
||||
} catch (e) {
|
||||
beginAdd(newNotification(e));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Updates the progress of the progress indicator component
|
||||
commandRegistry.registerCommand(UI.UpdateProgressIndicator, (value: ProgressIndicatorPayload) => {
|
||||
@Command(UI.UpdateProgressIndicator)
|
||||
protected 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,
|
||||
// eslint-disable-next-line no-console
|
||||
}).catch(e => console.error(e));
|
||||
});
|
||||
@Command(UI.ShowUpdateAvailableDialog)
|
||||
protected async showUpdateAvailableDialog(version: string = '') {
|
||||
try {
|
||||
return await DialogService.showDialog(UpdateAvailableDialogContainer, {
|
||||
version,
|
||||
});
|
||||
} catch (e) {
|
||||
beginAdd(newNotification(e));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shows the dialog telling the user that an update is unavailable
|
||||
commandRegistry.registerCommand(UI.ShowUpdateUnavailableDialog, async () => {
|
||||
return await DialogService.showDialog(
|
||||
UpdateUnavailableDialogContainer
|
||||
// eslint-disable-next-line no-console
|
||||
).catch(e => console.error(e));
|
||||
});
|
||||
@Command(UI.ShowUpdateUnavailableDialog)
|
||||
protected async showUpdateUnavailableDialog() {
|
||||
try {
|
||||
return await DialogService.showDialog(UpdateUnavailableDialogContainer);
|
||||
} catch (e) {
|
||||
beginAdd(newNotification(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ export interface ActiveInspectorChangedPayload {
|
|||
inspectorWebView: HTMLWebViewElement;
|
||||
}
|
||||
|
||||
export interface NewChatPayload {
|
||||
export interface NewChatPayload extends ClearLogPayload {
|
||||
[propName: string]: any;
|
||||
|
||||
documentId: string;
|
||||
|
@ -97,6 +97,7 @@ export interface AppendLogPayload {
|
|||
|
||||
export interface ClearLogPayload {
|
||||
documentId: string;
|
||||
resolver?: Function;
|
||||
}
|
||||
|
||||
export interface SetInspectorObjectsPayload {
|
||||
|
@ -179,7 +180,12 @@ export function updatePendingSpeechTokenRetrieval(pending: boolean): ChatAction<
|
|||
};
|
||||
}
|
||||
|
||||
export function newChat(documentId: string, mode: ChatMode, additionalData?: object): ChatAction<NewChatPayload> {
|
||||
export function newChat(
|
||||
documentId: string,
|
||||
mode: ChatMode,
|
||||
additionalData?: object,
|
||||
resolver?: Function
|
||||
): ChatAction<NewChatPayload> {
|
||||
return {
|
||||
type: ChatActions.newChat,
|
||||
payload: {
|
||||
|
@ -256,11 +262,12 @@ export function appendToLog(documentId: string, entry: LogEntry): ChatAction<App
|
|||
};
|
||||
}
|
||||
|
||||
export function clearLog(documentId: string): ChatAction<ClearLogPayload> {
|
||||
export function clearLog(documentId: string, resolver?: Function): ChatAction<ClearLogPayload> {
|
||||
return {
|
||||
type: ChatActions.clearLog,
|
||||
payload: {
|
||||
documentId,
|
||||
resolver,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -30,30 +30,38 @@
|
|||
// 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';
|
||||
|
||||
import { Channel, Disposable, IPC } from '@bfemulator/sdk-shared';
|
||||
const { ipcRenderer } = (window as any).require('electron');
|
||||
export const EXECUTE_COMMAND = 'EXECUTE_COMMAND';
|
||||
|
||||
class ElectronIPCImpl extends IPC {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
ipcRenderer.on('ipc:message', (_sender: any, ...args: any[]) => {
|
||||
const channelName = args.shift();
|
||||
const channel = this._channels[channelName];
|
||||
if (channel) {
|
||||
channel.onMessage(...args);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public send(...args: any[]): void {
|
||||
ipcRenderer.send('ipc:message', ...args);
|
||||
}
|
||||
|
||||
public registerChannel(channel: Channel): Disposable {
|
||||
return super.registerChannel(channel);
|
||||
}
|
||||
export interface CommandAction<T> extends Action {
|
||||
payload: T;
|
||||
}
|
||||
|
||||
export const ElectronIPC = new ElectronIPCImpl();
|
||||
export interface CommandActionPayload {
|
||||
isRemote: boolean;
|
||||
commandName: string;
|
||||
args: any[];
|
||||
resolver?: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a command and calls the resolve function
|
||||
* with the result when complete.
|
||||
*
|
||||
* @param isRemote
|
||||
* @param commandName
|
||||
* @param resolver
|
||||
* @param args
|
||||
*/
|
||||
export function executeCommand(
|
||||
isRemote: boolean,
|
||||
commandName,
|
||||
resolver: Function = null,
|
||||
...args: any[]
|
||||
): CommandAction<CommandActionPayload> {
|
||||
return {
|
||||
type: EXECUTE_COMMAND,
|
||||
payload: { isRemote, commandName, resolver, args },
|
||||
};
|
||||
}
|
|
@ -30,34 +30,25 @@
|
|||
// 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 enum PresentationActions {
|
||||
disable = 'PRESENTATION/DISABLE',
|
||||
enable = 'PRESENTATION/ENABLE',
|
||||
}
|
||||
|
||||
export interface EnablePresentationAction {
|
||||
type: PresentationActions.enable;
|
||||
payload: {};
|
||||
export interface PresentationAction extends Action {
|
||||
type: PresentationActions;
|
||||
}
|
||||
|
||||
export interface DisablePresentationAction {
|
||||
type: PresentationActions.disable;
|
||||
payload: {};
|
||||
}
|
||||
|
||||
export type PresentationAction = EnablePresentationAction | DisablePresentationAction;
|
||||
|
||||
export function enable(): EnablePresentationAction {
|
||||
export function enable(): PresentationAction {
|
||||
return {
|
||||
type: PresentationActions.enable,
|
||||
payload: {},
|
||||
};
|
||||
}
|
||||
|
||||
export function disable(): DisablePresentationAction {
|
||||
export function disable(): PresentationAction {
|
||||
return {
|
||||
type: PresentationActions.disable,
|
||||
payload: {},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
import { LogEntry } from '@bfemulator/sdk-shared';
|
||||
import { LogEntry, LogItemType } from '@bfemulator/sdk-shared';
|
||||
|
||||
import {
|
||||
addTranscript,
|
||||
|
@ -40,8 +40,8 @@ import {
|
|||
clearLog,
|
||||
clearTranscripts,
|
||||
closeDocument,
|
||||
newConversation,
|
||||
newChat,
|
||||
newConversation,
|
||||
removeTranscript,
|
||||
setInspectorObjects,
|
||||
updateChat,
|
||||
|
@ -62,7 +62,7 @@ describe('Chat reducer tests', () => {
|
|||
},
|
||||
},
|
||||
transcripts: [],
|
||||
};
|
||||
} as any;
|
||||
|
||||
it('should return unaltered state for non-matching action type', () => {
|
||||
const emptyAction: ChatAction = { type: null, payload: null };
|
||||
|
@ -134,7 +134,7 @@ describe('Chat reducer tests', () => {
|
|||
timestamp: 123,
|
||||
items: [
|
||||
{
|
||||
type: 'text',
|
||||
type: LogItemType.Text,
|
||||
payload: {
|
||||
level: 0,
|
||||
text: 'testing',
|
||||
|
@ -164,7 +164,7 @@ describe('Chat reducer tests', () => {
|
|||
timestamp: 1234,
|
||||
items: [
|
||||
{
|
||||
type: 'text',
|
||||
type: LogItemType.Text,
|
||||
payload: {
|
||||
level: 0,
|
||||
text: 'testing',
|
||||
|
@ -217,7 +217,7 @@ describe('Chat reducer tests', () => {
|
|||
},
|
||||
},
|
||||
transcripts: ['xs1', 'xs2', 'xs3'],
|
||||
};
|
||||
} as any;
|
||||
const action = closeNonGlobalTabs();
|
||||
const state = chat(alteredState, action);
|
||||
expect(state.changeKey).toBe(0);
|
||||
|
|
|
@ -31,9 +31,6 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
import { PresentationAction, PresentationActions } from '../action/presentationActions';
|
||||
|
||||
export interface PresentationState {
|
||||
|
@ -68,7 +65,5 @@ function setEnabled(enabled: boolean, state: PresentationState): PresentationSta
|
|||
const newState = { ...state };
|
||||
newState.enabled = enabled;
|
||||
|
||||
CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.SetFullscreen, enabled);
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
|
|
@ -30,7 +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.
|
||||
//
|
||||
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { ServiceTypes } from 'botframework-config/lib/schema';
|
||||
|
||||
|
@ -42,27 +42,46 @@ import {
|
|||
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,
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
remoteCall: () => Promise.resolve(true),
|
||||
},
|
||||
jest.mock('../../ui/dialogs', () => ({
|
||||
DialogService: { showDialog: () => Promise.resolve(true) },
|
||||
}));
|
||||
|
||||
describe('The azureAuthSaga', () => {
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
});
|
||||
|
||||
it('should contain a single step if the token in the store is valid', () => {
|
||||
store.dispatch(azureArmTokenDataChanged('a valid access_token'));
|
||||
const it = azureAuthSagas()
|
||||
|
@ -87,12 +106,6 @@ describe('The azureAuthSaga', () => {
|
|||
});
|
||||
|
||||
describe('with an invalid token in the store', () => {
|
||||
let registry: CommandRegistryImpl;
|
||||
beforeAll(() => {
|
||||
registry = new CommandRegistryImpl();
|
||||
registerCommands(registry);
|
||||
});
|
||||
|
||||
it('should contain just 2 steps when the Azure login dialog prompt is canceled', async () => {
|
||||
store.dispatch(azureArmTokenDataChanged(''));
|
||||
// @ts-ignore
|
||||
|
@ -133,7 +146,7 @@ describe('The azureAuthSaga', () => {
|
|||
store.dispatch(azureArmTokenDataChanged(''));
|
||||
// @ts-ignore
|
||||
DialogService.showDialog = () => Promise.resolve(1);
|
||||
(CommandServiceImpl as any).remoteCall = () => Promise.resolve(false);
|
||||
jest.spyOn(commandService, 'remoteCall').mockResolvedValueOnce(false);
|
||||
const it = azureAuthSagas()
|
||||
.next()
|
||||
.value.FORK.args[1](
|
||||
|
@ -146,7 +159,7 @@ describe('The azureAuthSaga', () => {
|
|||
);
|
||||
let val = undefined;
|
||||
let ct = 0;
|
||||
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall');
|
||||
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue(true);
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const next = it.next(val);
|
||||
|
@ -165,7 +178,7 @@ describe('The azureAuthSaga', () => {
|
|||
}
|
||||
} else if ('CALL' in val) {
|
||||
val = val.CALL.fn.call(null, val.CALL.args);
|
||||
if (val instanceof Promise) {
|
||||
if (val[Symbol.toStringTag] === 'Promise') {
|
||||
val = await val;
|
||||
if (ct === 2) {
|
||||
// Login was unsuccessful
|
||||
|
@ -184,7 +197,7 @@ describe('The azureAuthSaga', () => {
|
|||
store.dispatch(azureArmTokenDataChanged(''));
|
||||
// @ts-ignore
|
||||
DialogService.showDialog = () => Promise.resolve(1);
|
||||
(CommandServiceImpl as any).remoteCall = args => {
|
||||
commandService.remoteCall = args => {
|
||||
switch (args[0]) {
|
||||
case SharedConstants.Commands.Azure.RetrieveArmToken:
|
||||
// eslint-disable-next-line typescript/camelcase
|
||||
|
@ -194,7 +207,7 @@ describe('The azureAuthSaga', () => {
|
|||
return Promise.resolve({ persistLogin: true });
|
||||
|
||||
default:
|
||||
return Promise.resolve(false);
|
||||
return Promise.resolve(false) as any;
|
||||
}
|
||||
};
|
||||
const it = azureAuthSagas()
|
||||
|
@ -209,7 +222,7 @@ describe('The azureAuthSaga', () => {
|
|||
);
|
||||
let val = undefined;
|
||||
let ct = 0;
|
||||
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall');
|
||||
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall');
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const next = it.next(val);
|
||||
|
@ -228,13 +241,13 @@ describe('The azureAuthSaga', () => {
|
|||
}
|
||||
} else if ('CALL' in val) {
|
||||
val = val.CALL.fn.call(null, val.CALL.args);
|
||||
if (val instanceof Promise) {
|
||||
if (val[Symbol.toStringTag] === 'Promise') {
|
||||
val = await val;
|
||||
if (ct === 2) {
|
||||
if (ct === 3) {
|
||||
// Login was successful
|
||||
expect(val.access_token).toBe('a valid access_token');
|
||||
expect(remoteCallSpy).toHaveBeenCalledWith([SharedConstants.Commands.Azure.RetrieveArmToken]);
|
||||
} else if (ct === 4) {
|
||||
} else if (ct === 5) {
|
||||
expect(val.persistLogin).toBe(true);
|
||||
expect(remoteCallSpy).toHaveBeenCalledWith([SharedConstants.Commands.Azure.PersistAzureLoginChanged, 1]);
|
||||
}
|
||||
|
|
|
@ -32,8 +32,8 @@
|
|||
//
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { call, ForkEffect, put, select, takeEvery } from 'redux-saga/effects';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
import { DialogService } from '../../ui/dialogs';
|
||||
import {
|
||||
AZURE_BEGIN_AUTH_WORKFLOW,
|
||||
|
@ -46,31 +46,40 @@ import { RootState } from '../store';
|
|||
|
||||
const getArmTokenFromState = (state: RootState) => state.azureAuth;
|
||||
|
||||
export function* getArmToken(action: AzureAuthAction<AzureAuthWorkflow>): IterableIterator<any> {
|
||||
let azureAuth: AzureAuthState = yield select(getArmTokenFromState);
|
||||
if (azureAuth.access_token) {
|
||||
export class AzureAuthSaga {
|
||||
@CommandServiceInstance()
|
||||
private static commandService: CommandServiceImpl;
|
||||
|
||||
public static *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
|
||||
return result;
|
||||
}
|
||||
const { RetrieveArmToken, PersistAzureLoginChanged } = SharedConstants.Commands.Azure;
|
||||
const { TrackEvent } = SharedConstants.Commands.Telemetry;
|
||||
azureAuth = yield call([AzureAuthSaga.commandService, AzureAuthSaga.commandService.remoteCall], RetrieveArmToken);
|
||||
if (azureAuth && !('error' in azureAuth)) {
|
||||
const persistLogin = yield DialogService.showDialog(action.payload.loginSuccessDialog, azureAuth);
|
||||
yield call(
|
||||
AzureAuthSaga.commandService.remoteCall.bind(AzureAuthSaga.commandService),
|
||||
PersistAzureLoginChanged,
|
||||
persistLogin
|
||||
);
|
||||
AzureAuthSaga.commandService.remoteCall(TrackEvent, 'signIn_success').catch(_e => void 0);
|
||||
} else {
|
||||
yield DialogService.showDialog(action.payload.loginFailedDialog);
|
||||
AzureAuthSaga.commandService.remoteCall(TrackEvent, 'signIn_failure').catch(_e => void 0);
|
||||
}
|
||||
yield put(azureArmTokenDataChanged(azureAuth.access_token));
|
||||
return azureAuth;
|
||||
}
|
||||
const result = yield DialogService.showDialog(action.payload.promptDialog, action.payload.promptDialogProps);
|
||||
if (result !== 1) {
|
||||
// Result must be 1 which is a confirmation to sign in to Azure
|
||||
return result;
|
||||
}
|
||||
const { RetrieveArmToken, PersistAzureLoginChanged } = SharedConstants.Commands.Azure;
|
||||
const { TrackEvent } = SharedConstants.Commands.Telemetry;
|
||||
azureAuth = yield call(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);
|
||||
CommandServiceImpl.remoteCall(TrackEvent, 'signIn_success').catch(_e => void 0);
|
||||
} else {
|
||||
yield DialogService.showDialog(action.payload.loginFailedDialog);
|
||||
CommandServiceImpl.remoteCall(TrackEvent, 'signIn_failure').catch(_e => void 0);
|
||||
}
|
||||
yield put(azureArmTokenDataChanged(azureAuth.access_token));
|
||||
return azureAuth;
|
||||
}
|
||||
|
||||
export function* azureAuthSagas(): IterableIterator<ForkEffect> {
|
||||
yield takeEvery(AZURE_BEGIN_AUTH_WORKFLOW, getArmToken);
|
||||
yield takeEvery(AZURE_BEGIN_AUTH_WORKFLOW, AzureAuthSaga.getArmToken);
|
||||
}
|
||||
|
|
|
@ -31,9 +31,10 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
import { newNotification, SharedConstants, DebugMode } from '@bfemulator/app-shared';
|
||||
import { DebugMode, newNotification, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { BotConfigWithPath, ConversationService } from '@bfemulator/sdk-shared';
|
||||
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { ActiveBotHelper } from '../../ui/helpers/activeBotHelper';
|
||||
import {
|
||||
|
@ -45,12 +46,9 @@ import {
|
|||
} from '../action/botActions';
|
||||
import { beginAdd } from '../action/notificationActions';
|
||||
import { generateHash } from '../botHelpers';
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
|
||||
import { botSagas, browseForBot, generateHashForActiveBot, openBotViaFilePath, openBotViaUrl } from './botSagas';
|
||||
import { refreshConversationMenu } from './sharedSagas';
|
||||
|
||||
jest.mock('../../ui/dialogs', () => ({}));
|
||||
import { botSagas, BotSagas } from './botSagas';
|
||||
import { SharedSagas } from './sharedSagas';
|
||||
|
||||
jest.mock('../store', () => ({
|
||||
get store() {
|
||||
|
@ -58,31 +56,59 @@ jest.mock('../store', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
const mockSharedConstants = SharedConstants;
|
||||
let mockRemoteCommandsCalled = [];
|
||||
let mockLocalCommandsCalled = [];
|
||||
|
||||
jest.mock('../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
call: async (commandName: string, ...args: any[]) => {
|
||||
describe('The botSagas', () => {
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
|
||||
commandService.call = async (commandName: string, ...args: any[]) => {
|
||||
mockLocalCommandsCalled.push({ commandName, args: args });
|
||||
|
||||
switch (commandName) {
|
||||
case mockSharedConstants.Commands.Bot.OpenBrowse:
|
||||
return Promise.resolve(true);
|
||||
default:
|
||||
return Promise.resolve(true);
|
||||
return Promise.resolve(true) as any;
|
||||
}
|
||||
},
|
||||
remoteCall: async (commandName: string, ...args: any[]) => {
|
||||
};
|
||||
commandService.remoteCall = async (commandName: string, ...args: any[]) => {
|
||||
mockRemoteCommandsCalled.push({ commandName, args: args });
|
||||
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
},
|
||||
}));
|
||||
return Promise.resolve(true) as any;
|
||||
};
|
||||
});
|
||||
|
||||
describe('The botSagas', () => {
|
||||
beforeEach(() => {
|
||||
mockRemoteCommandsCalled = [];
|
||||
mockLocalCommandsCalled = [];
|
||||
|
@ -92,14 +118,17 @@ describe('The botSagas', () => {
|
|||
it('should initialize the root saga', () => {
|
||||
const gen = botSagas();
|
||||
|
||||
expect(gen.next().value).toEqual(takeEvery(BotActionType.browse, browseForBot));
|
||||
expect(gen.next().value).toEqual(takeEvery(BotActionType.browse, BotSagas.browseForBot));
|
||||
|
||||
expect(gen.next().value).toEqual(takeEvery(BotActionType.openViaUrl, openBotViaUrl));
|
||||
expect(gen.next().value).toEqual(takeEvery(BotActionType.openViaFilePath, openBotViaFilePath));
|
||||
expect(gen.next().value).toEqual(takeEvery(BotActionType.setActive, generateHashForActiveBot));
|
||||
expect(gen.next().value).toEqual(takeEvery(BotActionType.openViaUrl, BotSagas.openBotViaUrl));
|
||||
expect(gen.next().value).toEqual(takeEvery(BotActionType.openViaFilePath, BotSagas.openBotViaFilePath));
|
||||
expect(gen.next().value).toEqual(takeEvery(BotActionType.setActive, BotSagas.generateHashForActiveBot));
|
||||
|
||||
expect(gen.next().value).toEqual(
|
||||
takeLatest([BotActionType.setActive, BotActionType.load, BotActionType.close], refreshConversationMenu)
|
||||
takeLatest(
|
||||
[BotActionType.setActive, BotActionType.load, BotActionType.close],
|
||||
SharedSagas.refreshConversationMenu
|
||||
)
|
||||
);
|
||||
|
||||
expect(gen.next().done).toBe(true);
|
||||
|
@ -121,7 +150,7 @@ describe('The botSagas', () => {
|
|||
bot: botConfigPath,
|
||||
},
|
||||
};
|
||||
const gen = generateHashForActiveBot(setActiveBotAction);
|
||||
const gen = BotSagas.generateHashForActiveBot(setActiveBotAction);
|
||||
const generatedHash = gen.next().value;
|
||||
|
||||
expect(generatedHash).toEqual(call(generateHash, botConfigPath));
|
||||
|
@ -132,13 +161,13 @@ describe('The botSagas', () => {
|
|||
});
|
||||
|
||||
it('should open native open file dialog to browse for .bot file', () => {
|
||||
const gen = browseForBot();
|
||||
const gen = BotSagas.browseForBot();
|
||||
expect(gen.next().value).toEqual(call([ActiveBotHelper, ActiveBotHelper.confirmAndOpenBotFromFile]));
|
||||
expect(gen.next().done).toBe(true);
|
||||
});
|
||||
|
||||
it('should open a bot from a url', () => {
|
||||
const gen = openBotViaUrl(
|
||||
const gen = BotSagas.openBotViaUrl(
|
||||
openBotViaUrlAction({
|
||||
appPassword: 'password',
|
||||
appId: '1234abcd',
|
||||
|
@ -159,7 +188,7 @@ describe('The botSagas', () => {
|
|||
const callToSaveUrl = gen.next(DebugMode.Normal).value;
|
||||
expect(callToSaveUrl).toEqual(
|
||||
call(
|
||||
[CommandServiceImpl, CommandServiceImpl.remoteCall],
|
||||
[commandService, commandService.remoteCall],
|
||||
SharedConstants.Commands.Settings.SaveBotUrl,
|
||||
'http://localhost/api/messages'
|
||||
)
|
||||
|
@ -169,7 +198,7 @@ describe('The botSagas', () => {
|
|||
});
|
||||
|
||||
it('should open a bot from a url with the custom user id', () => {
|
||||
const gen = openBotViaUrl(
|
||||
const gen = BotSagas.openBotViaUrl(
|
||||
openBotViaUrlAction({
|
||||
appPassword: 'password',
|
||||
appId: '1234abcd',
|
||||
|
@ -186,7 +215,7 @@ describe('The botSagas', () => {
|
|||
const callToSetCurrentUser = gen.next(users).value;
|
||||
expect(callToSetCurrentUser).toEqual(
|
||||
call(
|
||||
[CommandServiceImpl, CommandServiceImpl.remoteCall],
|
||||
[commandService, commandService.remoteCall],
|
||||
SharedConstants.Commands.Emulator.SetCurrentUser,
|
||||
'customUserId'
|
||||
)
|
||||
|
@ -199,7 +228,7 @@ describe('The botSagas', () => {
|
|||
const callToSaveUrl = gen.next(DebugMode.Normal).value;
|
||||
expect(callToSaveUrl).toEqual(
|
||||
call(
|
||||
[CommandServiceImpl, CommandServiceImpl.remoteCall],
|
||||
[commandService, commandService.remoteCall],
|
||||
SharedConstants.Commands.Settings.SaveBotUrl,
|
||||
'http://localhost/api/messages'
|
||||
)
|
||||
|
@ -209,7 +238,7 @@ describe('The botSagas', () => {
|
|||
});
|
||||
|
||||
it('should send a notification if opening a bot from a URL fails', () => {
|
||||
const gen = openBotViaUrl(
|
||||
const gen = BotSagas.openBotViaUrl(
|
||||
openBotViaUrlAction({
|
||||
appPassword: 'password',
|
||||
appId: '1234abcd',
|
||||
|
@ -240,7 +269,7 @@ describe('The botSagas', () => {
|
|||
});
|
||||
|
||||
it('should send the "/INSPECT open" command when in debug mode and opening from url', () => {
|
||||
const gen = openBotViaUrl(
|
||||
const gen = BotSagas.openBotViaUrl(
|
||||
openBotViaUrlAction({
|
||||
appPassword: 'password',
|
||||
appId: '1234abcd',
|
||||
|
@ -268,7 +297,7 @@ describe('The botSagas', () => {
|
|||
};
|
||||
expect(callToPostActivity).toEqual(
|
||||
call(
|
||||
[CommandServiceImpl, CommandServiceImpl.remoteCall],
|
||||
[commandService, commandService.remoteCall],
|
||||
SharedConstants.Commands.Emulator.PostActivityToConversation,
|
||||
'someConversationId',
|
||||
activity
|
||||
|
@ -278,7 +307,7 @@ describe('The botSagas', () => {
|
|||
const callToRememberEndpoint = gen.next({ statusCode: 200 });
|
||||
expect(callToRememberEndpoint.value).toEqual(
|
||||
call(
|
||||
[CommandServiceImpl, CommandServiceImpl.remoteCall],
|
||||
[commandService, commandService.remoteCall],
|
||||
SharedConstants.Commands.Settings.SaveBotUrl,
|
||||
'http://localhost/api/messages'
|
||||
)
|
||||
|
@ -288,7 +317,7 @@ describe('The botSagas', () => {
|
|||
});
|
||||
|
||||
it('should spawn a notification if posting the "/INSPECT open" command fails', () => {
|
||||
const gen = openBotViaUrl(
|
||||
const gen = BotSagas.openBotViaUrl(
|
||||
openBotViaUrlAction({
|
||||
appPassword: 'password',
|
||||
appId: '1234abcd',
|
||||
|
@ -319,7 +348,7 @@ describe('The botSagas', () => {
|
|||
});
|
||||
|
||||
it('should spawn a notification if parsing the conversation id from the response fails', () => {
|
||||
const gen = openBotViaUrl(
|
||||
const gen = BotSagas.openBotViaUrl(
|
||||
openBotViaUrlAction({
|
||||
appPassword: 'password',
|
||||
appId: '1234abcd',
|
||||
|
@ -350,7 +379,7 @@ describe('The botSagas', () => {
|
|||
});
|
||||
|
||||
it('should open a bot from a file path', () => {
|
||||
const gen = openBotViaFilePath(openBotViaFilePathAction('/some/path.bot'));
|
||||
const gen = BotSagas.openBotViaFilePath(openBotViaFilePathAction('/some/path.bot'));
|
||||
|
||||
jest.spyOn(ActiveBotHelper, 'confirmAndOpenBotFromFile').mockResolvedValue(true);
|
||||
expect(gen.next().value).toEqual(
|
||||
|
@ -359,7 +388,7 @@ describe('The botSagas', () => {
|
|||
});
|
||||
|
||||
it('should send a notification when opening a bot from a file path fails', () => {
|
||||
const gen = openBotViaFilePath(openBotViaFilePathAction('/some/path.bot'));
|
||||
const gen = BotSagas.openBotViaFilePath(openBotViaFilePathAction('/some/path.bot'));
|
||||
const callOpenBot = gen.next().value;
|
||||
expect(callOpenBot).toEqual(call([ActiveBotHelper, ActiveBotHelper.confirmAndOpenBotFromFile], '/some/path.bot'));
|
||||
const putNotification = gen.throw(new Error('oh noes!'));
|
||||
|
|
|
@ -34,102 +34,113 @@
|
|||
import { DebugMode, newNotification, SharedConstants, UserSettings } from '@bfemulator/app-shared';
|
||||
import { ConversationService, StartConversationParams } from '@bfemulator/sdk-shared';
|
||||
import { call, ForkEffect, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { ActiveBotHelper } from '../../ui/helpers/activeBotHelper';
|
||||
import { BotAction, BotActionType, BotConfigWithPathPayload, botHashGenerated } from '../action/botActions';
|
||||
import { beginAdd } from '../action/notificationActions';
|
||||
import { generateHash } from '../botHelpers';
|
||||
import { RootState } from '../store';
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
|
||||
import { refreshConversationMenu } from './sharedSagas';
|
||||
import { SharedSagas } from './sharedSagas';
|
||||
|
||||
/** Opens up native open file dialog to browse for a .bot file */
|
||||
export function* browseForBot(): IterableIterator<any> {
|
||||
yield call([ActiveBotHelper, ActiveBotHelper.confirmAndOpenBotFromFile]);
|
||||
}
|
||||
export class BotSagas {
|
||||
@CommandServiceInstance()
|
||||
private static commandService: CommandServiceImpl;
|
||||
|
||||
export function* generateHashForActiveBot(action: BotAction<BotConfigWithPathPayload>): IterableIterator<any> {
|
||||
const { bot } = action.payload;
|
||||
const generatedHash = yield call(generateHash, bot);
|
||||
yield put(botHashGenerated(generatedHash));
|
||||
}
|
||||
|
||||
export function* openBotViaFilePath(action: BotAction<string>) {
|
||||
try {
|
||||
yield call([ActiveBotHelper, ActiveBotHelper.confirmAndOpenBotFromFile], action.payload);
|
||||
} catch (e) {
|
||||
const errorNotification = beginAdd(newNotification(`An Error occurred opening the bot at ${action.payload}: ${e}`));
|
||||
yield put(errorNotification);
|
||||
public static *browseForBot(): IterableIterator<any> {
|
||||
yield call([ActiveBotHelper, ActiveBotHelper.confirmAndOpenBotFromFile]);
|
||||
}
|
||||
}
|
||||
|
||||
export function* openBotViaUrl(action: BotAction<Partial<StartConversationParams>>) {
|
||||
const serverUrl = yield select((state: RootState) => state.clientAwareSettings.serverUrl);
|
||||
if (!action.payload.user) {
|
||||
// If no user is provided, select the current user
|
||||
const customUserId = yield select((state: RootState) => state.framework.userGUID);
|
||||
const users: UserSettings = yield select((state: RootState) => state.clientAwareSettings.users);
|
||||
action.payload.user = customUserId || users.usersById[users.currentUserId];
|
||||
if (customUserId) {
|
||||
action.payload.user = customUserId;
|
||||
yield call(
|
||||
[CommandServiceImpl, CommandServiceImpl.remoteCall],
|
||||
SharedConstants.Commands.Emulator.SetCurrentUser,
|
||||
customUserId
|
||||
public static *generateHashForActiveBot(action: BotAction<BotConfigWithPathPayload>): IterableIterator<any> {
|
||||
const { bot } = action.payload;
|
||||
const generatedHash = yield call(generateHash, bot);
|
||||
yield put(botHashGenerated(generatedHash));
|
||||
}
|
||||
|
||||
public static *openBotViaFilePath(action: BotAction<string>) {
|
||||
try {
|
||||
yield call([ActiveBotHelper, ActiveBotHelper.confirmAndOpenBotFromFile], action.payload);
|
||||
} catch (e) {
|
||||
const errorNotification = beginAdd(
|
||||
newNotification(`An Error occurred opening the bot at ${action.payload}: ${e}`)
|
||||
);
|
||||
yield put(errorNotification);
|
||||
}
|
||||
}
|
||||
let error;
|
||||
try {
|
||||
const response = yield ConversationService.startConversation(serverUrl, action.payload);
|
||||
if (!response.ok) {
|
||||
error = `An Error occurred opening the bot at ${action.payload.endpoint}: ${response.statusText}`;
|
||||
}
|
||||
const debugMode = yield select((state: RootState) => state.clientAwareSettings.debugMode);
|
||||
if (debugMode === DebugMode.Sidecar) {
|
||||
// extract the conversation id from the body
|
||||
const parsedBody = yield response.json();
|
||||
const conversationId = parsedBody.id || '';
|
||||
if (conversationId) {
|
||||
// post debug init command to conversation
|
||||
const activity = {
|
||||
type: 'message',
|
||||
text: '/INSPECT open',
|
||||
};
|
||||
const postActivityResponse = yield call(
|
||||
[CommandServiceImpl, CommandServiceImpl.remoteCall],
|
||||
SharedConstants.Commands.Emulator.PostActivityToConversation,
|
||||
conversationId,
|
||||
activity
|
||||
|
||||
public static *openBotViaUrl(action: BotAction<Partial<StartConversationParams>>) {
|
||||
const serverUrl = yield select((state: RootState) => state.clientAwareSettings.serverUrl);
|
||||
if (!action.payload.user) {
|
||||
// If no user is provided, select the current user
|
||||
const customUserId = yield select((state: RootState) => state.framework.userGUID);
|
||||
const users: UserSettings = yield select((state: RootState) => state.clientAwareSettings.users);
|
||||
action.payload.user = customUserId || users.usersById[users.currentUserId];
|
||||
if (customUserId) {
|
||||
action.payload.user = customUserId;
|
||||
yield call(
|
||||
[BotSagas.commandService, BotSagas.commandService.remoteCall],
|
||||
SharedConstants.Commands.Emulator.SetCurrentUser,
|
||||
customUserId
|
||||
);
|
||||
if (postActivityResponse.statusCode >= 400) {
|
||||
throw new Error(`An error occurred while POSTing "/INSPECT open" command to conversation ${conversationId}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error('An error occurred while trying to grab conversation ID from new conversation.');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
if (error) {
|
||||
const errorNotification = beginAdd(newNotification(error));
|
||||
yield put(errorNotification);
|
||||
} else {
|
||||
// remember the endpoint
|
||||
yield call(
|
||||
[CommandServiceImpl, CommandServiceImpl.remoteCall],
|
||||
SharedConstants.Commands.Settings.SaveBotUrl,
|
||||
action.payload.endpoint
|
||||
);
|
||||
let error;
|
||||
try {
|
||||
const response = yield ConversationService.startConversation(serverUrl, action.payload);
|
||||
if (!response.ok) {
|
||||
error = `An Error occurred opening the bot at ${action.payload.endpoint}: ${response.statusText}`;
|
||||
}
|
||||
const debugMode = yield select((state: RootState) => state.clientAwareSettings.debugMode);
|
||||
if (debugMode === DebugMode.Sidecar) {
|
||||
// extract the conversation id from the body
|
||||
const parsedBody = yield response.json();
|
||||
const conversationId = parsedBody.id || '';
|
||||
if (conversationId) {
|
||||
// post debug init command to conversation
|
||||
const activity = {
|
||||
type: 'message',
|
||||
text: '/INSPECT open',
|
||||
};
|
||||
const postActivityResponse = yield call(
|
||||
[BotSagas.commandService, BotSagas.commandService.remoteCall],
|
||||
SharedConstants.Commands.Emulator.PostActivityToConversation,
|
||||
conversationId,
|
||||
activity
|
||||
);
|
||||
if (postActivityResponse.statusCode >= 400) {
|
||||
throw new Error(
|
||||
`An error occurred while POSTing "/INSPECT open" command to conversation ${conversationId}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error('An error occurred while trying to grab conversation ID from new conversation.');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
if (error) {
|
||||
const errorNotification = beginAdd(newNotification(error));
|
||||
yield put(errorNotification);
|
||||
} else {
|
||||
// remember the endpoint
|
||||
yield call(
|
||||
[BotSagas.commandService, BotSagas.commandService.remoteCall],
|
||||
SharedConstants.Commands.Settings.SaveBotUrl,
|
||||
action.payload.endpoint
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* botSagas(): IterableIterator<ForkEffect> {
|
||||
yield takeEvery(BotActionType.browse, browseForBot);
|
||||
yield takeEvery(BotActionType.openViaUrl, openBotViaUrl);
|
||||
yield takeEvery(BotActionType.openViaFilePath, openBotViaFilePath);
|
||||
yield takeEvery(BotActionType.setActive, generateHashForActiveBot);
|
||||
yield takeLatest([BotActionType.setActive, BotActionType.load, BotActionType.close], refreshConversationMenu);
|
||||
yield takeEvery(BotActionType.browse, BotSagas.browseForBot);
|
||||
yield takeEvery(BotActionType.openViaUrl, BotSagas.openBotViaUrl);
|
||||
yield takeEvery(BotActionType.openViaFilePath, BotSagas.openBotViaFilePath);
|
||||
yield takeEvery(BotActionType.setActive, BotSagas.generateHashForActiveBot);
|
||||
yield takeLatest(
|
||||
[BotActionType.setActive, BotActionType.load, BotActionType.close],
|
||||
SharedSagas.refreshConversationMenu
|
||||
);
|
||||
}
|
||||
|
|
|
@ -35,13 +35,13 @@ import sagaMiddlewareFactory from 'redux-saga';
|
|||
import { ActivityTypes } from 'botframework-schema';
|
||||
import * as Electron from 'electron';
|
||||
import { SharedConstants, ValueTypes } from '@bfemulator/app-shared';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { bot } from '../reducer/bot';
|
||||
import { chat } from '../reducer/chat';
|
||||
import { editor } from '../reducer/editor';
|
||||
import { presentation } from '../reducer/presentation';
|
||||
import * as Constants from '../../constants';
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
import { closeConversation, newChat, showContextMenuForActivity } from '../action/chatActions';
|
||||
|
||||
import { chatSagas } from './chatSagas';
|
||||
|
@ -58,7 +58,29 @@ jest.mock('../store', () => ({
|
|||
|
||||
jest.mock('electron', () => {
|
||||
return {
|
||||
clipboard: { writeText: () => true },
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
clipboard: { writeText: (textFromActivity: string) => true },
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -597,8 +619,14 @@ describe('The ChatSagas,', () => {
|
|||
});
|
||||
|
||||
describe('when showing a context menu for an activity', () => {
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
});
|
||||
it('should handle the "copy message" selection', async () => {
|
||||
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue({ id: 'copy' });
|
||||
const commandServiceSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue({ id: 'copy' });
|
||||
const clipboardSpy = jest.spyOn(Electron.clipboard, 'writeText');
|
||||
const activity = {
|
||||
valueType: ValueTypes.Activity,
|
||||
|
@ -612,21 +640,21 @@ describe('The ChatSagas,', () => {
|
|||
});
|
||||
|
||||
it('should handle the "copy json" selection', async () => {
|
||||
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue({ id: 'json' });
|
||||
const commandServiceSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue({ id: 'json' });
|
||||
const clipboardSpy = jest.spyOn(Electron.clipboard, 'writeText');
|
||||
const activity = {
|
||||
valueType: '',
|
||||
type: ActivityTypes.Trace,
|
||||
value: { type: ActivityTypes.Message, text: 'Hello Bot!' },
|
||||
};
|
||||
mockStore.dispatch(showContextMenuForActivity(activity));
|
||||
await mockStore.dispatch(showContextMenuForActivity(activity));
|
||||
await Promise.resolve(true);
|
||||
expect(commandServiceSpy).toHaveBeenCalled();
|
||||
expect(clipboardSpy).toHaveBeenCalledWith(JSON.stringify(activity, null, 2));
|
||||
});
|
||||
|
||||
it('should handle the "Compare with previous" selection', async () => {
|
||||
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue({ id: 'diff' });
|
||||
const commandServiceSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue({ id: 'diff' });
|
||||
const activity = mockStoreState.chat.chats.doc1.log.entries[2].items[0].payload.obj;
|
||||
mockStore.dispatch(showContextMenuForActivity(activity));
|
||||
await Promise.resolve(true);
|
||||
|
@ -705,36 +733,36 @@ describe('The ChatSagas,', () => {
|
|||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('when closing a document it should notify the main process so it can remove the conversation', async () => {
|
||||
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue(true);
|
||||
mockStore.dispatch(closeConversation('doc1'));
|
||||
expect(commandServiceSpy).toHaveBeenCalledWith(SharedConstants.Commands.Emulator.DeleteConversation, 'convo1');
|
||||
await Promise.resolve(); // wait for the comand service call to complete
|
||||
expect(mockStore.getState().chat.chats.doc1).toBeUndefined();
|
||||
});
|
||||
it('when closing a document it should notify the main process so it can remove the conversation', async () => {
|
||||
const commandServiceSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue(true);
|
||||
mockStore.dispatch(closeConversation('doc1'));
|
||||
expect(commandServiceSpy).toHaveBeenCalledWith(SharedConstants.Commands.Emulator.DeleteConversation, 'convo1');
|
||||
await Promise.resolve(); // wait for the comand service call to complete
|
||||
expect(mockStore.getState().chat.chats.doc1).toBeUndefined();
|
||||
});
|
||||
|
||||
it('when starting a new conversation, should create a speech token ponyfill factory', async () => {
|
||||
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue('mockSpeechToken');
|
||||
it('when starting a new conversation, should create a speech token ponyfill factory', async () => {
|
||||
const commandServiceSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue('mockSpeechToken');
|
||||
|
||||
mockStore.dispatch(
|
||||
newChat('doc2', 'livechat', {
|
||||
conversationId: 'convo2',
|
||||
endpointId: 'endpoint2',
|
||||
userId: 'someUserId2',
|
||||
})
|
||||
);
|
||||
mockStore.dispatch(
|
||||
newChat('doc2', 'livechat', {
|
||||
conversationId: 'convo2',
|
||||
endpointId: 'endpoint2',
|
||||
userId: 'someUserId2',
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
const state = mockStore.getState();
|
||||
expect(state.chat.chats.doc2).not.toBeUndefined();
|
||||
expect(state.chat.webSpeechFactories.doc2).not.toBeUndefined();
|
||||
expect(state.chat.webSpeechFactories.doc2()).toBe('Yay! ponyfill!');
|
||||
expect(commandServiceSpy).toHaveBeenCalledWith(
|
||||
SharedConstants.Commands.Emulator.GetSpeechToken,
|
||||
'endpoint2',
|
||||
false
|
||||
);
|
||||
await Promise.resolve();
|
||||
const state = mockStore.getState();
|
||||
expect(state.chat.chats.doc2).not.toBeUndefined();
|
||||
expect(state.chat.webSpeechFactories.doc2).not.toBeUndefined();
|
||||
expect(state.chat.webSpeechFactories.doc2()).toBe('Yay! ponyfill!');
|
||||
expect(commandServiceSpy).toHaveBeenCalledWith(
|
||||
SharedConstants.Commands.Emulator.GetSpeechToken,
|
||||
'endpoint2',
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -34,7 +34,13 @@ import * as Electron from 'electron';
|
|||
import { MenuItemConstructorOptions } from 'electron';
|
||||
import { Activity } from 'botframework-schema';
|
||||
import { SharedConstants, ValueTypes } from '@bfemulator/app-shared';
|
||||
import { InspectableObjectLogItem, LogItem, LogItemType } from '@bfemulator/sdk-shared';
|
||||
import {
|
||||
CommandServiceImpl,
|
||||
CommandServiceInstance,
|
||||
InspectableObjectLogItem,
|
||||
LogItem,
|
||||
LogItemType,
|
||||
} from '@bfemulator/sdk-shared';
|
||||
import { diff } from 'deep-diff';
|
||||
import { IEndpointService } from 'botframework-config/lib/schema';
|
||||
import { createCognitiveServicesBingSpeechPonyfillFactory } from 'botframework-webchat';
|
||||
|
@ -53,7 +59,6 @@ import {
|
|||
webChatStoreUpdated,
|
||||
webSpeechFactoryUpdated,
|
||||
} from '../action/chatActions';
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
import { RootState } from '../store';
|
||||
import { isSpeechEnabled } from '../../utils';
|
||||
|
||||
|
@ -103,175 +108,186 @@ const getChatFromDocumentId = (state: RootState, documentId: string): any => {
|
|||
return state.chat.chats[documentId];
|
||||
};
|
||||
|
||||
export function* showContextMenuForActivity(action: ChatAction<Activity>): Iterable<any> {
|
||||
const { payload: activity } = action;
|
||||
const previousBotState = yield select(getPreviousBotState, activity);
|
||||
const diffEnabled = activity.valueType.endsWith('botState') && !!previousBotState;
|
||||
const menuItems = [
|
||||
{ label: 'Copy text', id: 'copy' },
|
||||
{ label: 'Copy json', id: 'json' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Compare with previous', id: 'diff', enabled: diffEnabled },
|
||||
] as MenuItemConstructorOptions[];
|
||||
export class ChatSagas {
|
||||
@CommandServiceInstance()
|
||||
private static commandService: CommandServiceImpl;
|
||||
|
||||
const { DisplayContextMenu } = SharedConstants.Commands.Electron;
|
||||
const response: { id: string } = yield call(
|
||||
CommandServiceImpl.remoteCall.bind(CommandServiceImpl),
|
||||
DisplayContextMenu,
|
||||
menuItems
|
||||
);
|
||||
public static *showContextMenuForActivity(action: ChatAction<Activity>): Iterable<any> {
|
||||
const { payload: activity } = action;
|
||||
const previousBotState = yield select(getPreviousBotState, activity);
|
||||
const diffEnabled = activity.valueType.endsWith('botState') && !!previousBotState;
|
||||
const menuItems = [
|
||||
{ label: 'Copy text', id: 'copy' },
|
||||
{ label: 'Copy json', id: 'json' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Compare with previous', id: 'diff', enabled: diffEnabled },
|
||||
] as MenuItemConstructorOptions[];
|
||||
|
||||
if (!response) {
|
||||
return; // canceled context menu
|
||||
}
|
||||
switch (response.id) {
|
||||
case 'copy':
|
||||
return Electron.clipboard.writeText(getTextFromActivity(activity));
|
||||
const { DisplayContextMenu } = SharedConstants.Commands.Electron;
|
||||
const response: { id: string } = yield call(
|
||||
[ChatSagas.commandService, ChatSagas.commandService.remoteCall],
|
||||
DisplayContextMenu,
|
||||
menuItems
|
||||
);
|
||||
|
||||
case 'json':
|
||||
return Electron.clipboard.writeText(JSON.stringify(activity, null, 2));
|
||||
|
||||
default:
|
||||
yield* diffWithPreviousBotState(activity);
|
||||
}
|
||||
}
|
||||
|
||||
export function* closeConversation(action: ChatAction<DocumentIdPayload>): Iterable<any> {
|
||||
const conversationId = yield select(getConversationIdFromDocumentId, action.payload.documentId);
|
||||
const { DeleteConversation } = SharedConstants.Commands.Emulator;
|
||||
const { documentId } = action.payload;
|
||||
const chat = yield select(getChatFromDocumentId, documentId);
|
||||
if (chat.directLine) {
|
||||
chat.directLine.end(); // stop polling
|
||||
}
|
||||
yield put(closeDocument(documentId));
|
||||
// remove the webchat store when the document is closed
|
||||
yield put(webChatStoreUpdated(documentId, null));
|
||||
yield call([CommandServiceImpl, CommandServiceImpl.remoteCall], DeleteConversation, conversationId);
|
||||
}
|
||||
|
||||
export function* newChat(action: ChatAction<NewChatPayload>): Iterable<any> {
|
||||
const { documentId } = action.payload;
|
||||
// Create a new webchat store for this documentId
|
||||
yield put(webChatStoreUpdated(documentId, createWebChatStore()));
|
||||
// Each time a new chat is open, retrieve the speech token
|
||||
// if the endpoint is speech enabled and create a bind speech
|
||||
// pony fill factory. This is consumed by WebChat...
|
||||
yield put(webSpeechFactoryUpdated(documentId, null)); // remove the old factory
|
||||
const endpoint: IEndpointService = yield select(getEndpointServiceByDocumentId, documentId);
|
||||
if (!isSpeechEnabled(endpoint)) {
|
||||
return;
|
||||
}
|
||||
yield put(updatePendingSpeechTokenRetrieval(true));
|
||||
// If an existing factory is found, refresh the token
|
||||
const existingFactory: string = yield select(getWebSpeechFactoryForDocumentId, documentId);
|
||||
const { GetSpeechToken: command } = SharedConstants.Commands.Emulator;
|
||||
const token = yield call(
|
||||
[CommandServiceImpl, CommandServiceImpl.remoteCall],
|
||||
command,
|
||||
endpoint.id,
|
||||
!!existingFactory
|
||||
);
|
||||
if (token) {
|
||||
const factory = yield call(createCognitiveServicesBingSpeechPonyfillFactory, {
|
||||
authorizationToken: token,
|
||||
});
|
||||
yield put(webSpeechFactoryUpdated(documentId, factory)); // Provide the new factory to the store
|
||||
}
|
||||
yield put(updatePendingSpeechTokenRetrieval(false));
|
||||
}
|
||||
|
||||
export function* diffWithPreviousBotState(currentBotState: Activity): Iterable<any> {
|
||||
const previousBotState: Activity = yield select(getPreviousBotState, currentBotState);
|
||||
|
||||
const lhs = [];
|
||||
const rhs = [];
|
||||
const deltas = diff(previousBotState.value, currentBotState.value);
|
||||
(deltas || []).forEach(diff => {
|
||||
switch (diff.kind) {
|
||||
case 'A':
|
||||
{
|
||||
const { item, path } = diff;
|
||||
path.push(diff.index);
|
||||
if (item.kind === 'D') {
|
||||
lhs.push(path);
|
||||
} else if (item.kind === 'E') {
|
||||
rhs.push(path);
|
||||
lhs.push(path);
|
||||
} else {
|
||||
rhs.push(path);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'D':
|
||||
lhs.push(diff.path);
|
||||
break;
|
||||
|
||||
case 'E':
|
||||
rhs.push(diff.path);
|
||||
lhs.push(diff.path);
|
||||
break;
|
||||
|
||||
case 'N':
|
||||
rhs.push(diff.path);
|
||||
break;
|
||||
if (!response) {
|
||||
return; // canceled context menu
|
||||
}
|
||||
});
|
||||
switch (response.id) {
|
||||
case 'copy':
|
||||
return Electron.clipboard.writeText(ChatSagas.getTextFromActivity(activity));
|
||||
|
||||
// Clone the bot state and update the keys to show changes
|
||||
const botStateClone: Activity = JSON.parse(
|
||||
JSON.stringify(currentBotState, (key: string, value: any) => {
|
||||
if (value instanceof Array) {
|
||||
return Object.keys(value).reduce((conversion: any, key) => {
|
||||
conversion['' + key] = value[key];
|
||||
return conversion;
|
||||
}, {});
|
||||
case 'json':
|
||||
return Electron.clipboard.writeText(JSON.stringify(activity, null, 2));
|
||||
|
||||
default:
|
||||
yield* ChatSagas.diffWithPreviousBotState(activity);
|
||||
}
|
||||
}
|
||||
|
||||
public static *closeConversation(action: ChatAction<DocumentIdPayload>): Iterable<any> {
|
||||
const conversationId = yield select(getConversationIdFromDocumentId, action.payload.documentId);
|
||||
const { DeleteConversation } = SharedConstants.Commands.Emulator;
|
||||
const { documentId } = action.payload;
|
||||
const chat = yield select(getChatFromDocumentId, documentId);
|
||||
if (chat && chat.directLine) {
|
||||
chat.directLine.end(); // stop polling
|
||||
}
|
||||
yield put(closeDocument(documentId));
|
||||
// remove the webchat store when the document is closed
|
||||
yield put(webChatStoreUpdated(documentId, null));
|
||||
yield call([ChatSagas.commandService, ChatSagas.commandService.remoteCall], DeleteConversation, conversationId);
|
||||
}
|
||||
|
||||
public static *newChat(action: ChatAction<NewChatPayload>): Iterable<any> {
|
||||
const { documentId, resolver } = action.payload;
|
||||
// Create a new webchat store for this documentId
|
||||
yield put(webChatStoreUpdated(documentId, createWebChatStore()));
|
||||
// Each time a new chat is open, retrieve the speech token
|
||||
// if the endpoint is speech enabled and create a bind speech
|
||||
// pony fill factory. This is consumed by WebChat...
|
||||
yield put(webSpeechFactoryUpdated(documentId, null)); // remove the old factory
|
||||
const endpoint: IEndpointService = yield select(getEndpointServiceByDocumentId, documentId);
|
||||
if (!isSpeechEnabled(endpoint)) {
|
||||
if (resolver) {
|
||||
resolver();
|
||||
}
|
||||
return value;
|
||||
})
|
||||
);
|
||||
botStateClone.valueType = ValueTypes.Diff;
|
||||
// values that were added
|
||||
rhs.forEach(path => {
|
||||
buildDiff('+', path, botStateClone.value, botStateClone.value);
|
||||
});
|
||||
// values that were removed
|
||||
lhs.forEach(path => {
|
||||
buildDiff('-', path, botStateClone.value, previousBotState.value);
|
||||
});
|
||||
const documentId = yield select(getCurrentDocumentId);
|
||||
yield put(setHighlightedObjects(documentId, [previousBotState, currentBotState]));
|
||||
yield put(setInspectorObjects(documentId, botStateClone));
|
||||
}
|
||||
|
||||
function getTextFromActivity(activity: Activity): string {
|
||||
if (activity.valueType === ValueTypes.Command) {
|
||||
return activity.value;
|
||||
} else if (activity.valueType === ValueTypes.Activity) {
|
||||
return 'text' in activity.value ? activity.value.text : activity.label;
|
||||
}
|
||||
return activity.text || activity.label || '';
|
||||
}
|
||||
|
||||
function buildDiff(prependWith: string, path: (string | number)[], target: any, source: any): void {
|
||||
let key;
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
key = path[i];
|
||||
if (key in target && target[key] !== null && typeof target[key] === 'object') {
|
||||
target = target[key];
|
||||
source = source[key];
|
||||
} else {
|
||||
break;
|
||||
return;
|
||||
}
|
||||
yield put(updatePendingSpeechTokenRetrieval(true));
|
||||
// If an existing factory is found, refresh the token
|
||||
const existingFactory: string = yield select(getWebSpeechFactoryForDocumentId, documentId);
|
||||
const { GetSpeechToken: command } = SharedConstants.Commands.Emulator;
|
||||
const token = yield call(
|
||||
[ChatSagas.commandService, ChatSagas.commandService.remoteCall],
|
||||
command,
|
||||
endpoint.id,
|
||||
!!existingFactory
|
||||
);
|
||||
if (token) {
|
||||
const factory = yield call(createCognitiveServicesBingSpeechPonyfillFactory, {
|
||||
authorizationToken: token,
|
||||
});
|
||||
yield put(webSpeechFactoryUpdated(documentId, factory)); // Provide the new factory to the store
|
||||
}
|
||||
yield put(updatePendingSpeechTokenRetrieval(false));
|
||||
if (resolver) {
|
||||
resolver();
|
||||
}
|
||||
}
|
||||
const value = source[key];
|
||||
delete target[key];
|
||||
target[prependWith + key] = value;
|
||||
|
||||
public static *diffWithPreviousBotState(currentBotState: Activity): Iterable<any> {
|
||||
const previousBotState: Activity = yield select(getPreviousBotState, currentBotState);
|
||||
|
||||
const lhs = [];
|
||||
const rhs = [];
|
||||
const deltas = diff(previousBotState.value, currentBotState.value);
|
||||
(deltas || []).forEach(diff => {
|
||||
switch (diff.kind) {
|
||||
case 'A':
|
||||
{
|
||||
const { item, path } = diff;
|
||||
path.push(diff.index);
|
||||
if (item.kind === 'D') {
|
||||
lhs.push(path);
|
||||
} else if (item.kind === 'E') {
|
||||
rhs.push(path);
|
||||
lhs.push(path);
|
||||
} else {
|
||||
rhs.push(path);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'D':
|
||||
lhs.push(diff.path);
|
||||
break;
|
||||
|
||||
case 'E':
|
||||
rhs.push(diff.path);
|
||||
lhs.push(diff.path);
|
||||
break;
|
||||
|
||||
case 'N':
|
||||
rhs.push(diff.path);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Clone the bot state and update the keys to show changes
|
||||
const botStateClone: Activity = JSON.parse(
|
||||
JSON.stringify(currentBotState, (key: string, value: any) => {
|
||||
if (value instanceof Array) {
|
||||
return Object.keys(value).reduce((conversion: any, key) => {
|
||||
conversion['' + key] = value[key];
|
||||
return conversion;
|
||||
}, {});
|
||||
}
|
||||
return value;
|
||||
})
|
||||
);
|
||||
botStateClone.valueType = ValueTypes.Diff;
|
||||
// values that were added
|
||||
rhs.forEach(path => {
|
||||
ChatSagas.buildDiff('+', path, botStateClone.value, botStateClone.value);
|
||||
});
|
||||
// values that were removed
|
||||
lhs.forEach(path => {
|
||||
ChatSagas.buildDiff('-', path, botStateClone.value, previousBotState.value);
|
||||
});
|
||||
const documentId = yield select(getCurrentDocumentId);
|
||||
yield put(setHighlightedObjects(documentId, [previousBotState, currentBotState]));
|
||||
yield put(setInspectorObjects(documentId, botStateClone));
|
||||
}
|
||||
|
||||
private static getTextFromActivity(activity: Activity): string {
|
||||
if (activity.valueType === ValueTypes.Command) {
|
||||
return activity.value;
|
||||
} else if (activity.valueType === ValueTypes.Activity) {
|
||||
return 'text' in activity.value ? activity.value.text : activity.label;
|
||||
}
|
||||
return activity.text || activity.label || '';
|
||||
}
|
||||
|
||||
public static buildDiff(prependWith: string, path: (string | number)[], target: any, source: any): void {
|
||||
let key;
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
key = path[i];
|
||||
if (key in target && target[key] !== null && typeof target[key] === 'object') {
|
||||
target = target[key];
|
||||
source = source[key];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const value = source[key];
|
||||
delete target[key];
|
||||
target[prependWith + key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
export function* chatSagas(): IterableIterator<ForkEffect> {
|
||||
yield takeEvery(ChatActions.showContextMenuForActivity, showContextMenuForActivity);
|
||||
yield takeEvery(ChatActions.closeConversation, closeConversation);
|
||||
yield takeLatest([ChatActions.newChat, ChatActions.clearLog], newChat);
|
||||
yield takeEvery(ChatActions.showContextMenuForActivity, ChatSagas.showContextMenuForActivity);
|
||||
yield takeEvery(ChatActions.closeConversation, ChatSagas.closeConversation);
|
||||
yield takeLatest([ChatActions.newChat, ChatActions.clearLog], ChatSagas.newChat);
|
||||
}
|
||||
|
|
|
@ -30,40 +30,35 @@
|
|||
// 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 { ForkEffect, put, takeEvery } from 'redux-saga/effects';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
import { newNotification } from '@bfemulator/app-shared';
|
||||
|
||||
import { IPC, isObject } from '@bfemulator/sdk-shared';
|
||||
import { CommandAction, CommandActionPayload, EXECUTE_COMMAND } from '../action/commandAction';
|
||||
import { beginAdd } from '../action/notificationActions';
|
||||
|
||||
export interface Process {
|
||||
pid: number;
|
||||
send?(message: any);
|
||||
on(event: 'message', listener: NodeJS.MessageListener);
|
||||
on(event: 'exit', listener: NodeJS.ExitListener);
|
||||
}
|
||||
export class CommandSagas {
|
||||
@CommandServiceInstance()
|
||||
private static commandService: CommandServiceImpl;
|
||||
|
||||
export class ProcessIPC extends IPC {
|
||||
get id(): number {
|
||||
return this._process.pid;
|
||||
}
|
||||
|
||||
constructor(private _process: Process) {
|
||||
super();
|
||||
this._process.on('message', message => {
|
||||
if (isObject(message) && message.type === 'ipc:message' && Array.isArray(message.args)) {
|
||||
const channelName = message.args.shift();
|
||||
const channel = super.getChannel(channelName);
|
||||
if (channel) {
|
||||
channel.onMessage(...message.args);
|
||||
}
|
||||
public static *executeCommand(action: CommandAction<CommandActionPayload>): IterableIterator<any> {
|
||||
const { isRemote, commandName, args, resolver } = action.payload;
|
||||
try {
|
||||
const result = isRemote
|
||||
? yield CommandSagas.commandService.remoteCall(commandName, ...args)
|
||||
: yield CommandSagas.commandService.call(commandName, ...args);
|
||||
if (resolver) {
|
||||
resolver(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public send(...args: any[]): void {
|
||||
if (this._process.send) {
|
||||
this._process.send({
|
||||
type: 'ipc:message',
|
||||
args,
|
||||
});
|
||||
} catch (e) {
|
||||
const type = isRemote ? 'remote' : 'local';
|
||||
yield put(
|
||||
beginAdd(newNotification(`An error occurred while executing the ${type} command: ${commandName}\n ${e}`))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* commandSagas(): IterableIterator<ForkEffect> {
|
||||
yield takeEvery(EXECUTE_COMMAND, CommandSagas.executeCommand);
|
||||
}
|
|
@ -33,11 +33,12 @@
|
|||
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { call, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { EditorActions, removeDocPendingChange } from '../action/editorActions';
|
||||
|
||||
import { checkActiveDocForPendingChanges, editorSagas, promptUserToReloadDocument } from './editorSagas';
|
||||
import { refreshConversationMenu, editorSelector } from './sharedSagas';
|
||||
import { editorSagas, EditorSagas } from './editorSagas';
|
||||
import { SharedSagas, editorSelector } from './sharedSagas';
|
||||
|
||||
jest.mock('../store', () => ({
|
||||
get store() {
|
||||
|
@ -45,43 +46,63 @@ jest.mock('../store', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../ui/dialogs', () => ({}));
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
const mockSharedConstants = SharedConstants;
|
||||
let mockRemoteCommandsCalled = [];
|
||||
let mockLocalCommandsCalled = [];
|
||||
const mockMessageResponse = false;
|
||||
|
||||
jest.mock('../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
call: async (commandName: string, ...args: any[]) => {
|
||||
describe('The Editor Sagas', () => {
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
commandService.call = async (commandName: string, ...args: any[]) => {
|
||||
mockLocalCommandsCalled.push({ commandName, args: args });
|
||||
},
|
||||
remoteCall: async (commandName: string, ...args: any[]) => {
|
||||
return true as any;
|
||||
};
|
||||
commandService.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);
|
||||
}
|
||||
case SharedConstants.Commands.Electron.ShowMessageBox:
|
||||
return Promise.resolve(false);
|
||||
default:
|
||||
return Promise.resolve(true);
|
||||
return Promise.resolve(true) as any;
|
||||
}
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
describe('The Editor Sagas', () => {
|
||||
beforeEach(() => {
|
||||
mockRemoteCommandsCalled = [];
|
||||
mockLocalCommandsCalled = [];
|
||||
});
|
||||
|
||||
it('should check the active doc for pending changes', () => {
|
||||
const gen = checkActiveDocForPendingChanges();
|
||||
const gen = EditorSagas.checkActiveDocForPendingChanges();
|
||||
const stateData = gen.next().value;
|
||||
|
||||
expect(stateData).toEqual(select(editorSelector));
|
||||
|
@ -98,7 +119,7 @@ describe('The Editor Sagas', () => {
|
|||
};
|
||||
// should return the inner generator that we delegate to
|
||||
const innerGen = gen.next(mockEditorState).value;
|
||||
expect(innerGen).toEqual(call(promptUserToReloadDocument, mockActiveDocId));
|
||||
expect(innerGen).toEqual(call(EditorSagas.promptUserToReloadDocument, mockActiveDocId));
|
||||
|
||||
expect(gen.next().done).toBe(true);
|
||||
});
|
||||
|
@ -110,7 +131,7 @@ describe('The Editor Sagas', () => {
|
|||
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);
|
||||
const gen = EditorSagas.promptUserToReloadDocument(mockChatFileName);
|
||||
|
||||
gen.next();
|
||||
|
||||
|
@ -138,7 +159,7 @@ describe('The Editor Sagas', () => {
|
|||
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 gen = EditorSagas.promptUserToReloadDocument(mockTranscriptFile);
|
||||
|
||||
gen.next();
|
||||
|
||||
|
@ -170,7 +191,7 @@ describe('The Editor Sagas', () => {
|
|||
EditorActions.setActiveTab,
|
||||
EditorActions.open,
|
||||
],
|
||||
checkActiveDocForPendingChanges
|
||||
EditorSagas.checkActiveDocForPendingChanges
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -179,7 +200,7 @@ describe('The Editor Sagas', () => {
|
|||
expect(refreshConversationMenuYield).toEqual(
|
||||
takeLatest(
|
||||
[EditorActions.close, EditorActions.open, EditorActions.setActiveEditor, EditorActions.setActiveTab],
|
||||
refreshConversationMenu
|
||||
SharedSagas.refreshConversationMenu
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -33,46 +33,51 @@
|
|||
|
||||
import { isChatFile, isTranscriptFile, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { call, ForkEffect, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
import { EditorActions, removeDocPendingChange } from '../action/editorActions';
|
||||
|
||||
import { editorSelector, refreshConversationMenu } from './sharedSagas';
|
||||
import { editorSelector, SharedSagas } from './sharedSagas';
|
||||
|
||||
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?',
|
||||
};
|
||||
const confirmation = yield CommandServiceImpl.remoteCall(Commands.Electron.ShowMessageBox, options);
|
||||
export class EditorSagas {
|
||||
@CommandServiceInstance()
|
||||
private static commandService: CommandServiceImpl;
|
||||
|
||||
// clear the doc of pending changes
|
||||
yield put(removeDocPendingChange(filename));
|
||||
public static *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?',
|
||||
};
|
||||
const confirmation = yield EditorSagas.commandService.remoteCall(Commands.Electron.ShowMessageBox, options);
|
||||
|
||||
// reload the file, otherwise proceed without reloading
|
||||
const { OpenChatFile, ReloadTranscript } = SharedConstants.Commands.Emulator;
|
||||
// clear the doc of pending changes
|
||||
yield put(removeDocPendingChange(filename));
|
||||
|
||||
if (confirmation) {
|
||||
if (isChatFile(filename)) {
|
||||
yield CommandServiceImpl.call(OpenChatFile, filename, true);
|
||||
} else if (isTranscriptFile(filename)) {
|
||||
yield CommandServiceImpl.call(ReloadTranscript, filename);
|
||||
// reload the file, otherwise proceed without reloading
|
||||
const { OpenChatFile, ReloadTranscript } = SharedConstants.Commands.Emulator;
|
||||
|
||||
if (confirmation) {
|
||||
if (isChatFile(filename)) {
|
||||
yield EditorSagas.commandService.call(OpenChatFile, filename, true);
|
||||
} else if (isTranscriptFile(filename)) {
|
||||
yield EditorSagas.commandService.call(ReloadTranscript, filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* checkActiveDocForPendingChanges(): IterableIterator<any> {
|
||||
const stateData = yield select(editorSelector);
|
||||
public static *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;
|
||||
if (stateData.docsWithPendingChanges.some(doc => doc === activeDocId)) {
|
||||
// TODO: active document ID is not always the filename
|
||||
yield call(promptUserToReloadDocument, activeDocId);
|
||||
// if currently active document has pending changes, prompt the user to reload it
|
||||
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(EditorSagas.promptUserToReloadDocument, activeDocId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
export function* editorSagas(): IterableIterator<ForkEffect> {
|
||||
|
@ -80,11 +85,11 @@ export function* editorSagas(): IterableIterator<ForkEffect> {
|
|||
// is focused, check to see if the active document has pending changes
|
||||
yield takeEvery(
|
||||
[EditorActions.addDocPendingChange, EditorActions.setActiveEditor, EditorActions.setActiveTab, EditorActions.open],
|
||||
checkActiveDocForPendingChanges
|
||||
EditorSagas.checkActiveDocForPendingChanges
|
||||
);
|
||||
|
||||
yield takeLatest(
|
||||
[EditorActions.close, EditorActions.open, EditorActions.setActiveEditor, EditorActions.setActiveTab],
|
||||
refreshConversationMenu
|
||||
SharedSagas.refreshConversationMenu
|
||||
);
|
||||
}
|
||||
|
|
|
@ -34,11 +34,11 @@ import { applyMiddleware, combineReducers, createStore } from 'redux';
|
|||
import sagaMiddlewareFactory from 'redux-saga';
|
||||
import { Component } from 'react';
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { bot } from '../reducer/bot';
|
||||
import { loadBotInfos, setActiveBot } from '../action/botActions';
|
||||
import { launchEndpointEditor, openEndpointExplorerContextMenu } from '../action/endpointServiceActions';
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
import { DialogService } from '../../ui/dialogs/service';
|
||||
|
||||
import { endpointSagas } from './endpointSagas';
|
||||
|
@ -75,32 +75,46 @@ const 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;
|
||||
},
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
describe('The endpoint sagas', () => {
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockStore.dispatch(loadBotInfos([mockBot]));
|
||||
mockStore.dispatch(setActiveBot(mockBot));
|
||||
});
|
||||
|
||||
it('should launch the endpoint editor and execute a command to save the edited services', async () => {
|
||||
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall');
|
||||
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall');
|
||||
const dialogServiceSpy = jest.spyOn(DialogService, 'showDialog').mockResolvedValue(mockBot.services);
|
||||
await mockStore.dispatch(launchEndpointEditor(mockComponentClass, mockBot.services[0]));
|
||||
const { AddOrUpdateService } = SharedConstants.Commands.Bot;
|
||||
|
@ -121,7 +135,7 @@ describe('The endpoint sagas', () => {
|
|||
const { DisplayContextMenu, ShowMessageBox } = SharedConstants.Commands.Electron;
|
||||
const { NewLiveChat } = SharedConstants.Commands.Emulator;
|
||||
it('should launch the endpoint editor when that menu option is chosen', () => {
|
||||
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue({ id: 'edit' });
|
||||
const commandServiceSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue({ id: 'edit' });
|
||||
const dialogServiceSpy = jest.spyOn(DialogService, 'showDialog').mockResolvedValue(mockBot.services);
|
||||
mockStore.dispatch(openEndpointExplorerContextMenu(mockComponentClass, mockBot.services[0]));
|
||||
|
||||
|
@ -132,10 +146,8 @@ describe('The endpoint sagas', () => {
|
|||
});
|
||||
|
||||
it('should open a deep link when that menu option is chosen', async () => {
|
||||
const commandServiceRemoteCallSpy = jest
|
||||
.spyOn(CommandServiceImpl, 'remoteCall')
|
||||
.mockResolvedValue({ id: 'open' });
|
||||
const commandServiceCallSpy = jest.spyOn(CommandServiceImpl, 'call').mockResolvedValue(true);
|
||||
const commandServiceRemoteCallSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue({ id: 'open' });
|
||||
const commandServiceCallSpy = jest.spyOn(commandService, 'call').mockResolvedValue(true);
|
||||
|
||||
await mockStore.dispatch(openEndpointExplorerContextMenu(mockComponentClass, mockBot.services[0]));
|
||||
expect(commandServiceRemoteCallSpy).toHaveBeenCalledWith(DisplayContextMenu, menuItems);
|
||||
|
@ -144,12 +156,12 @@ describe('The endpoint sagas', () => {
|
|||
|
||||
it('should forget the service when that menu item is chosen', async () => {
|
||||
const remoteCallArgs = [];
|
||||
CommandServiceImpl.remoteCall = async (commandName, ...args) => {
|
||||
commandService.remoteCall = async (commandName, ...args) => {
|
||||
remoteCallArgs.push({ commandName, args: args });
|
||||
if (commandName === DisplayContextMenu) {
|
||||
return { id: 'forget' };
|
||||
}
|
||||
return true;
|
||||
return true as any;
|
||||
};
|
||||
const { RemoveService } = SharedConstants.Commands.Bot;
|
||||
await mockStore.dispatch(openEndpointExplorerContextMenu(mockComponentClass, mockBot.services[0]));
|
||||
|
|
|
@ -35,8 +35,8 @@ import { SharedConstants } from '@bfemulator/app-shared';
|
|||
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, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
import { DialogService } from '../../ui/dialogs/service';
|
||||
import { openServiceDeepLink } from '../action/connectedServiceActions';
|
||||
import {
|
||||
|
@ -55,90 +55,111 @@ const getConnectedAbs = (state: RootState, endpointAppId: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
function* launchEndpointEditor(action: EndpointServiceAction<EndpointEditorPayload>): IterableIterator<any> {
|
||||
const { endpointEditorComponent, endpointService = {} } = action.payload;
|
||||
const servicesToUpdate = yield DialogService.showDialog<ComponentClass<any>, IEndpointService[]>(
|
||||
endpointEditorComponent,
|
||||
{ endpointService }
|
||||
);
|
||||
export class EndpointSagas {
|
||||
@CommandServiceInstance()
|
||||
private static commandService: CommandServiceImpl;
|
||||
|
||||
if (servicesToUpdate) {
|
||||
const { AddOrUpdateService, RemoveService } = SharedConstants.Commands.Bot;
|
||||
let i = servicesToUpdate.length;
|
||||
while (i--) {
|
||||
const service = servicesToUpdate[i];
|
||||
let shouldBeRemoved = false;
|
||||
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;
|
||||
public static *launchEndpointEditor(action: EndpointServiceAction<EndpointEditorPayload>): IterableIterator<any> {
|
||||
const { endpointEditorComponent, endpointService = {} } = action.payload;
|
||||
const servicesToUpdate = yield DialogService.showDialog<ComponentClass<any>, IEndpointService[]>(
|
||||
endpointEditorComponent,
|
||||
{ endpointService }
|
||||
);
|
||||
|
||||
if (servicesToUpdate) {
|
||||
const { AddOrUpdateService, RemoveService } = SharedConstants.Commands.Bot;
|
||||
let i = servicesToUpdate.length;
|
||||
while (i--) {
|
||||
const service = servicesToUpdate[i];
|
||||
let shouldBeRemoved = false;
|
||||
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;
|
||||
}
|
||||
yield EndpointSagas.commandService.remoteCall(
|
||||
shouldBeRemoved ? RemoveService : AddOrUpdateService,
|
||||
service.type,
|
||||
service
|
||||
);
|
||||
}
|
||||
yield CommandServiceImpl.remoteCall(shouldBeRemoved ? RemoveService : AddOrUpdateService, service.type, service);
|
||||
}
|
||||
}
|
||||
|
||||
public static *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' },
|
||||
];
|
||||
const { DisplayContextMenu } = SharedConstants.Commands.Electron;
|
||||
const response = yield call(
|
||||
[EndpointSagas.commandService, EndpointSagas.commandService.remoteCall],
|
||||
DisplayContextMenu,
|
||||
menuItems
|
||||
);
|
||||
switch (response.id) {
|
||||
case 'edit':
|
||||
yield* EndpointSagas.launchEndpointEditor(action);
|
||||
break;
|
||||
|
||||
case 'open':
|
||||
yield* EndpointSagas.openEndpointInEmulator(action);
|
||||
break;
|
||||
|
||||
case 'absLink':
|
||||
yield put(openServiceDeepLink(connectedAbs));
|
||||
break;
|
||||
|
||||
case 'forget':
|
||||
yield* EndpointSagas.removeEndpointServiceFromActiveBot(action.payload.endpointService);
|
||||
break;
|
||||
|
||||
default:
|
||||
// canceled context menu
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-yield
|
||||
public static *openEndpointInEmulator(action: EndpointServiceAction<EndpointServicePayload>): IterableIterator<any> {
|
||||
const { endpointService, focusExistingChatIfAvailable: focusExisting = false } = action.payload;
|
||||
return EndpointSagas.commandService.call(
|
||||
SharedConstants.Commands.Emulator.NewLiveChat,
|
||||
endpointService,
|
||||
focusExisting
|
||||
);
|
||||
}
|
||||
|
||||
public static *removeEndpointServiceFromActiveBot(endpointService: IEndpointService): IterableIterator<any> {
|
||||
const result = yield EndpointSagas.commandService.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 EndpointSagas.commandService.remoteCall(
|
||||
SharedConstants.Commands.Bot.RemoveService,
|
||||
endpointService.type,
|
||||
endpointService.id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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' },
|
||||
];
|
||||
const { DisplayContextMenu } = SharedConstants.Commands.Electron;
|
||||
const response = yield call(CommandServiceImpl.remoteCall.bind(CommandServiceImpl), DisplayContextMenu, menuItems);
|
||||
switch (response.id) {
|
||||
case 'edit':
|
||||
yield* launchEndpointEditor(action);
|
||||
break;
|
||||
|
||||
case 'open':
|
||||
yield* openEndpointInEmulator(action);
|
||||
break;
|
||||
|
||||
case 'absLink':
|
||||
yield put(openServiceDeepLink(connectedAbs));
|
||||
break;
|
||||
|
||||
case 'forget':
|
||||
yield* removeEndpointServiceFromActiveBot(action.payload.endpointService);
|
||||
break;
|
||||
|
||||
default:
|
||||
// canceled context menu
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
});
|
||||
if (result) {
|
||||
yield CommandServiceImpl.remoteCall(
|
||||
SharedConstants.Commands.Bot.RemoveService,
|
||||
endpointService.type,
|
||||
endpointService.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function* endpointSagas(): IterableIterator<ForkEffect> {
|
||||
yield takeLatest(LAUNCH_ENDPOINT_EDITOR, launchEndpointEditor);
|
||||
yield takeEvery(OPEN_ENDPOINT_CONTEXT_MENU, openEndpointContextMenu);
|
||||
yield takeEvery(OPEN_ENDPOINT_IN_EMULATOR, openEndpointInEmulator);
|
||||
yield takeLatest(LAUNCH_ENDPOINT_EDITOR, EndpointSagas.launchEndpointEditor);
|
||||
yield takeEvery(OPEN_ENDPOINT_CONTEXT_MENU, EndpointSagas.openEndpointContextMenu);
|
||||
yield takeEvery(OPEN_ENDPOINT_IN_EMULATOR, EndpointSagas.openEndpointInEmulator);
|
||||
}
|
||||
|
|
|
@ -30,47 +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 { SharedConstants, newNotification } from '@bfemulator/app-shared';
|
||||
import { newNotification, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { applyMiddleware, combineReducers, createStore } from 'redux';
|
||||
import sagaMiddlewareFactory from 'redux-saga';
|
||||
import { call, put, takeEvery, select } from 'redux-saga/effects';
|
||||
import { call, put, select, takeEvery } from 'redux-saga/effects';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CONTENT_TYPE_APP_SETTINGS, DOCUMENT_ID_APP_SETTINGS } from '../../constants';
|
||||
import * as EditorActions from '../action/editorActions';
|
||||
import {
|
||||
getFrameworkSettings as getFrameworkSettingsAction,
|
||||
saveFrameworkSettings as saveFrameworkSettingsAction,
|
||||
} from '../action/frameworkSettingsActions';
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
import { beginAdd } from '../action/notificationActions';
|
||||
import { editor } from '../reducer/editor';
|
||||
import {
|
||||
frameworkSettingsChanged,
|
||||
GET_FRAMEWORK_SETTINGS,
|
||||
getFrameworkSettings as getFrameworkSettingsAction,
|
||||
SAVE_FRAMEWORK_SETTINGS,
|
||||
saveFrameworkSettings as saveFrameworkSettingsAction,
|
||||
} from '../action/frameworkSettingsActions';
|
||||
import { beginAdd } from '../action/notificationActions';
|
||||
import { editor } from '../reducer/editor';
|
||||
import { framework } from '../reducer/frameworkSettingsReducer';
|
||||
|
||||
import {
|
||||
activeDocumentSelector,
|
||||
frameworkSettingsSagas,
|
||||
getFrameworkSettings,
|
||||
FrameworkSettingsSagas,
|
||||
normalizeSettingsData,
|
||||
saveFrameworkSettings,
|
||||
} from './frameworkSettingsSagas';
|
||||
|
||||
jest.mock(
|
||||
'../../ui/dialogs/',
|
||||
() =>
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return {};
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
const sagaMiddleWare = sagaMiddlewareFactory();
|
||||
const mockStore = createStore(combineReducers({ framework, editor }), {}, applyMiddleware(sagaMiddleWare));
|
||||
|
@ -91,16 +100,23 @@ mockStore.dispatch(
|
|||
})
|
||||
);
|
||||
describe('The frameworkSettingsSagas', () => {
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
});
|
||||
|
||||
it('should register the expected generators', () => {
|
||||
const it = frameworkSettingsSagas();
|
||||
expect(it.next().value).toEqual(takeEvery(GET_FRAMEWORK_SETTINGS, getFrameworkSettings));
|
||||
expect(it.next().value).toEqual(takeEvery(SAVE_FRAMEWORK_SETTINGS, saveFrameworkSettings));
|
||||
expect(it.next().value).toEqual(takeEvery(GET_FRAMEWORK_SETTINGS, FrameworkSettingsSagas.getFrameworkSettings));
|
||||
expect(it.next().value).toEqual(takeEvery(SAVE_FRAMEWORK_SETTINGS, FrameworkSettingsSagas.saveFrameworkSettings));
|
||||
});
|
||||
|
||||
it('should get the framework settings when using the happy path', async () => {
|
||||
const it = getFrameworkSettings();
|
||||
const it = FrameworkSettingsSagas.getFrameworkSettings();
|
||||
let next = it.next();
|
||||
expect(next.value).toEqual(CommandServiceImpl.remoteCall(SharedConstants.Commands.Settings.LoadAppSettings));
|
||||
expect(next.value).toEqual(commandService.remoteCall(SharedConstants.Commands.Settings.LoadAppSettings));
|
||||
|
||||
next = it.next({});
|
||||
const normalized = await next.value.CALL.fn({});
|
||||
|
@ -108,23 +124,23 @@ describe('The frameworkSettingsSagas', () => {
|
|||
});
|
||||
|
||||
it('should send a notification when something goes wrong while getting the framework settings', () => {
|
||||
const it = getFrameworkSettings();
|
||||
const it = FrameworkSettingsSagas.getFrameworkSettings();
|
||||
it.next();
|
||||
const errMsg = `Error while loading emulator settings: oh noes!`;
|
||||
const notification = newNotification(errMsg);
|
||||
notification.timestamp = jasmine.any(Number);
|
||||
notification.id = jasmine.any(String);
|
||||
notification.timestamp = jasmine.any(Number) as any;
|
||||
notification.id = jasmine.any(String) as any;
|
||||
expect(it.throw('oh noes!').value).toEqual(put(beginAdd(notification)));
|
||||
});
|
||||
|
||||
it('should save the framework settings', async () => {
|
||||
const it = saveFrameworkSettings(saveFrameworkSettingsAction({}));
|
||||
const it = FrameworkSettingsSagas.saveFrameworkSettings(saveFrameworkSettingsAction({}));
|
||||
const normalize = it.next().value;
|
||||
expect(normalize).toEqual(call(normalizeSettingsData, {}));
|
||||
const normalized = await normalize.CALL.fn({});
|
||||
// remote call to save the settings
|
||||
expect(it.next(normalized).value).toEqual(
|
||||
CommandServiceImpl.remoteCall(SharedConstants.Commands.Settings.SaveAppSettings, normalized)
|
||||
commandService.remoteCall(SharedConstants.Commands.Settings.SaveAppSettings, normalized)
|
||||
);
|
||||
// get the settings from the main side again
|
||||
expect(it.next().value).toEqual(put(getFrameworkSettingsAction()));
|
||||
|
@ -137,12 +153,12 @@ describe('The frameworkSettingsSagas', () => {
|
|||
});
|
||||
|
||||
it('should send a notification when saving the settings fails', () => {
|
||||
const it = saveFrameworkSettings(saveFrameworkSettingsAction({}));
|
||||
const it = FrameworkSettingsSagas.saveFrameworkSettings(saveFrameworkSettingsAction({}));
|
||||
it.next();
|
||||
const errMsg = `Error while saving emulator settings: oh noes!`;
|
||||
const notification = newNotification(errMsg);
|
||||
notification.timestamp = jasmine.any(Number);
|
||||
notification.id = jasmine.any(String);
|
||||
notification.timestamp = jasmine.any(Number) as any;
|
||||
notification.id = jasmine.any(String) as any;
|
||||
expect(it.throw('oh noes!').value).toEqual(put(beginAdd(notification)));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,8 +32,8 @@
|
|||
//
|
||||
import { frameworkDefault, FrameworkSettings, newNotification, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { call, ForkEffect, put, select, takeEvery } from 'redux-saga/effects';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
import * as EditorActions from '../action/editorActions';
|
||||
import {
|
||||
FrameworkSettingsAction,
|
||||
|
@ -61,34 +61,44 @@ export const activeDocumentSelector = (state: RootState) => {
|
|||
return editors[activeEditor].documents[activeDocumentId];
|
||||
};
|
||||
|
||||
export function* getFrameworkSettings(): IterableIterator<any> {
|
||||
try {
|
||||
const framework = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Settings.LoadAppSettings);
|
||||
const normalized = yield call(normalizeSettingsData, framework);
|
||||
yield put(frameworkSettingsChanged(normalized));
|
||||
} catch (e) {
|
||||
const errMsg = `Error while loading emulator settings: ${e}`;
|
||||
const notification = newNotification(errMsg);
|
||||
yield put(beginAdd(notification));
|
||||
}
|
||||
}
|
||||
export class FrameworkSettingsSagas {
|
||||
@CommandServiceInstance()
|
||||
private static commandService: CommandServiceImpl;
|
||||
|
||||
export function* saveFrameworkSettings(action: FrameworkSettingsAction<FrameworkSettings>): IterableIterator<any> {
|
||||
try {
|
||||
// trim keys that do not belong and generate a hash
|
||||
const normalized = yield call(normalizeSettingsData, action.payload);
|
||||
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Settings.SaveAppSettings, normalized);
|
||||
yield put(getFrameworkSettingsAction()); // sync with main - do not assume main hasn't processed this in some way
|
||||
const activeDoc: Document = yield select(activeDocumentSelector);
|
||||
yield put(EditorActions.setDirtyFlag(activeDoc.documentId, false)); // mark as clean
|
||||
} catch (e) {
|
||||
const errMsg = `Error while saving emulator settings: ${e}`;
|
||||
const notification = newNotification(errMsg);
|
||||
yield put(beginAdd(notification));
|
||||
public static *getFrameworkSettings(): IterableIterator<any> {
|
||||
try {
|
||||
const framework = yield FrameworkSettingsSagas.commandService.remoteCall(
|
||||
SharedConstants.Commands.Settings.LoadAppSettings
|
||||
);
|
||||
const normalized = yield call(normalizeSettingsData, framework);
|
||||
yield put(frameworkSettingsChanged(normalized));
|
||||
} catch (e) {
|
||||
const errMsg = `Error while loading emulator settings: ${e}`;
|
||||
const notification = newNotification(errMsg);
|
||||
yield put(beginAdd(notification));
|
||||
}
|
||||
}
|
||||
|
||||
public static *saveFrameworkSettings(action: FrameworkSettingsAction<FrameworkSettings>): IterableIterator<any> {
|
||||
try {
|
||||
// trim keys that do not belong and generate a hash
|
||||
const normalized = yield call(normalizeSettingsData, action.payload);
|
||||
yield FrameworkSettingsSagas.commandService.remoteCall(
|
||||
SharedConstants.Commands.Settings.SaveAppSettings,
|
||||
normalized
|
||||
);
|
||||
yield put(getFrameworkSettingsAction()); // sync with main - do not assume main hasn't processed this in some way
|
||||
const activeDoc: Document = yield select(activeDocumentSelector);
|
||||
yield put(EditorActions.setDirtyFlag(activeDoc.documentId, false)); // mark as clean
|
||||
} catch (e) {
|
||||
const errMsg = `Error while saving emulator settings: ${e}`;
|
||||
const notification = newNotification(errMsg);
|
||||
yield put(beginAdd(notification));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* frameworkSettingsSagas(): IterableIterator<ForkEffect> {
|
||||
yield takeEvery(GET_FRAMEWORK_SETTINGS, getFrameworkSettings);
|
||||
yield takeEvery(SAVE_FRAMEWORK_SETTINGS, saveFrameworkSettings);
|
||||
yield takeEvery(GET_FRAMEWORK_SETTINGS, FrameworkSettingsSagas.getFrameworkSettings);
|
||||
yield takeEvery(SAVE_FRAMEWORK_SETTINGS, FrameworkSettingsSagas.saveFrameworkSettings);
|
||||
}
|
||||
|
|
|
@ -42,16 +42,20 @@ import { resourceSagas } from './resourcesSagas';
|
|||
import { servicesExplorerSagas } from './servicesExplorerSagas';
|
||||
import { welcomePageSagas } from './welcomePageSagas';
|
||||
import { chatSagas } from './chatSagas';
|
||||
import { commandSagas } from './commandSagas';
|
||||
import { presentationSagas } from './presentationSagas';
|
||||
|
||||
export const applicationSagas = [
|
||||
azureAuthSagas,
|
||||
botSagas,
|
||||
chatSagas,
|
||||
commandSagas,
|
||||
editorSagas,
|
||||
endpointSagas,
|
||||
frameworkSettingsSagas,
|
||||
navBarSagas,
|
||||
notificationSagas,
|
||||
presentationSagas,
|
||||
resourceSagas,
|
||||
servicesExplorerSagas,
|
||||
welcomePageSagas,
|
||||
|
|
|
@ -39,26 +39,6 @@ import { markAllAsRead } from '../action/notificationActions';
|
|||
|
||||
import { markNotificationsAsRead } from './navBarSagas';
|
||||
|
||||
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('../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
remoteCall: () => Promise.resolve(true),
|
||||
},
|
||||
}));
|
||||
describe('Nav bar sagas', () => {
|
||||
test('markNotificationsAsRead()', () => {
|
||||
const gen = markNotificationsAsRead(select(Constants.NAVBAR_NOTIFICATIONS));
|
||||
|
|
|
@ -30,38 +30,26 @@
|
|||
// 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, takeEvery } from 'redux-saga/effects';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
|
||||
export interface Disposable {
|
||||
dispose(): void;
|
||||
}
|
||||
import { PresentationAction, PresentationActions } from '../action/presentationActions';
|
||||
|
||||
export function isDisposable(obj: any): boolean {
|
||||
return obj && typeof obj.dispose === 'function';
|
||||
}
|
||||
export class PresentationSagas {
|
||||
@CommandServiceInstance()
|
||||
private static commandService: CommandServiceImpl;
|
||||
|
||||
export function dispose<T extends Disposable>(obj: T): T;
|
||||
export function dispose<T extends Disposable>(...arr: T[]): T[];
|
||||
export function dispose<T extends Disposable>(arr: T[]): T[];
|
||||
export function dispose<T extends Disposable>(arg: T | T[]): T | T[] {
|
||||
if (Array.isArray(arg)) {
|
||||
arg.forEach(elem => elem && elem.dispose());
|
||||
return [];
|
||||
} else {
|
||||
if (arg) {
|
||||
arg.dispose();
|
||||
}
|
||||
return undefined;
|
||||
public static *presentationModeChanged(action: PresentationAction): IterableIterator<any> {
|
||||
const enabled = action.type === PresentationActions.enable;
|
||||
yield call(
|
||||
[PresentationSagas.commandService, PresentationSagas.commandService.remoteCall],
|
||||
SharedConstants.Commands.Electron.SetFullscreen,
|
||||
enabled
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class DisposableImpl implements Disposable {
|
||||
private _toDispose: Disposable[] = [];
|
||||
|
||||
public dispose(): void {
|
||||
this._toDispose = dispose(this._toDispose);
|
||||
}
|
||||
|
||||
public toDispose(...objs: Disposable[]): void {
|
||||
this._toDispose.push(...objs);
|
||||
}
|
||||
export function* presentationSagas(): IterableIterator<ForkEffect> {
|
||||
yield takeEvery([PresentationActions.disable, PresentationActions.enable], PresentationSagas.presentationModeChanged);
|
||||
}
|
|
@ -32,7 +32,7 @@
|
|||
//
|
||||
|
||||
import { applyMiddleware, combineReducers, createStore } from 'redux';
|
||||
import { BotConfigWithPathImpl } from '@bfemulator/sdk-shared';
|
||||
import { BotConfigWithPathImpl, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
import { ServiceTypes } from 'botframework-config/lib/schema';
|
||||
import sagaMiddlewareFactory from 'redux-saga';
|
||||
import { SharedConstants } from '@bfemulator/app-shared/built';
|
||||
|
@ -57,6 +57,32 @@ jest.mock('../store', () => ({
|
|||
return mockStore;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../ui/dialogs/service', () => ({
|
||||
DialogService: {
|
||||
showDialog: () => Promise.resolve(true),
|
||||
|
@ -68,9 +94,14 @@ const mockRemoteCommandsCalled = [];
|
|||
const mockLocalCommandsCalled = [];
|
||||
const mockSharedConstants = SharedConstants; // thanks Jest!
|
||||
let mockContextMenuResponse;
|
||||
jest.mock('../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
remoteCall: async (commandName: string, ...args: any[]) => {
|
||||
|
||||
describe('The ResourceSagas', () => {
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
commandService.remoteCall = async (commandName: string, ...args: any[]) => {
|
||||
mockRemoteCommandsCalled.push({ commandName, args: args });
|
||||
switch (commandName) {
|
||||
case mockSharedConstants.Commands.Electron.DisplayContextMenu:
|
||||
|
@ -82,14 +113,14 @@ jest.mock('../../platform/commands/commandServiceImpl', () => ({
|
|||
default:
|
||||
return true;
|
||||
}
|
||||
},
|
||||
call: async (commandName: string, ...args: any[]) => {
|
||||
mockLocalCommandsCalled.push({ commandName, args: args });
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
commandService.call = async (commandName: string, ...args: any[]) => {
|
||||
mockLocalCommandsCalled.push({ commandName, args: args });
|
||||
return true as any;
|
||||
};
|
||||
});
|
||||
|
||||
describe('The ResourceSagas', () => {
|
||||
beforeEach(() => {
|
||||
mockRemoteCommandsCalled.length = 0;
|
||||
mockLocalCommandsCalled.length = 0;
|
||||
|
|
|
@ -35,8 +35,8 @@ import { newNotification } from '@bfemulator/app-shared/built';
|
|||
import { IFileService } from 'botframework-config/lib/schema';
|
||||
import { ComponentClass } from 'react';
|
||||
import { ForkEffect, put, takeEvery } from 'redux-saga/effects';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
import { DialogService } from '../../ui/dialogs/service';
|
||||
import { beginAdd } from '../action/notificationActions';
|
||||
import {
|
||||
|
@ -48,93 +48,106 @@ import {
|
|||
ResourcesAction,
|
||||
} from '../action/resourcesAction';
|
||||
|
||||
function* openContextMenuForResource(action: ResourcesAction<IFileService>): IterableIterator<any> {
|
||||
const menuItems = [{ label: 'Open file location', id: 0 }, { label: 'Rename', id: 1 }, { label: 'Delete', id: 2 }];
|
||||
export class ResourcesSagas {
|
||||
@CommandServiceInstance()
|
||||
private static commandService: CommandServiceImpl;
|
||||
|
||||
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);
|
||||
break;
|
||||
public static *openContextMenuForResource(action: ResourcesAction<IFileService>): IterableIterator<any> {
|
||||
const menuItems = [{ label: 'Open file location', id: 0 }, { label: 'Rename', id: 1 }, { label: 'Delete', id: 2 }];
|
||||
|
||||
case 1:
|
||||
yield put(editResource(action.payload));
|
||||
break;
|
||||
const result = yield ResourcesSagas.commandService.remoteCall(
|
||||
SharedConstants.Commands.Electron.DisplayContextMenu,
|
||||
menuItems
|
||||
);
|
||||
switch (result.id) {
|
||||
case 0:
|
||||
yield ResourcesSagas.commandService.remoteCall(
|
||||
SharedConstants.Commands.Electron.OpenFileLocation,
|
||||
action.payload.path
|
||||
);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
yield* deleteFile(action);
|
||||
break;
|
||||
case 1:
|
||||
yield put(editResource(action.payload));
|
||||
break;
|
||||
|
||||
default:
|
||||
// Canceled context menu
|
||||
break;
|
||||
case 2:
|
||||
yield* ResourcesSagas.deleteFile(action);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Canceled context menu
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, {
|
||||
type: 'info',
|
||||
title: 'Delete this file',
|
||||
buttons: ['Cancel', 'Delete'],
|
||||
defaultId: 1,
|
||||
message: `This action cannot be undone. Are you sure you want to delete ${name}?`,
|
||||
cancelId: 0,
|
||||
});
|
||||
if (result) {
|
||||
yield CommandServiceImpl.remoteCall(UnlinkFile, path);
|
||||
}
|
||||
}
|
||||
|
||||
function* doRename(action: ResourcesAction<IFileService>) {
|
||||
const { payload } = action;
|
||||
const { ShowMessageBox, RenameFile } = SharedConstants.Commands.Electron;
|
||||
if (!payload.name) {
|
||||
return CommandServiceImpl.remoteCall(ShowMessageBox, true, {
|
||||
type: 'error',
|
||||
title: 'Invalid file name',
|
||||
buttons: ['Ok'],
|
||||
public static *deleteFile(action: ResourcesAction<IFileService>): IterableIterator<any> {
|
||||
const { name, path } = action.payload;
|
||||
const { ShowMessageBox, UnlinkFile } = SharedConstants.Commands.Electron;
|
||||
const result = yield ResourcesSagas.commandService.remoteCall(ShowMessageBox, true, {
|
||||
type: 'info',
|
||||
title: 'Delete this file',
|
||||
buttons: ['Cancel', 'Delete'],
|
||||
defaultId: 1,
|
||||
message: `A valid file name must be used`,
|
||||
message: `This action cannot be undone. Are you sure you want to delete ${name}?`,
|
||||
cancelId: 0,
|
||||
});
|
||||
if (result) {
|
||||
yield ResourcesSagas.commandService.remoteCall(UnlinkFile, path);
|
||||
}
|
||||
}
|
||||
yield CommandServiceImpl.remoteCall(RenameFile, payload);
|
||||
yield put(editResource(null));
|
||||
}
|
||||
|
||||
function* doOpenResource(action: ResourcesAction<IFileService>): IterableIterator<any> {
|
||||
const { OpenChatFile, OpenTranscript } = SharedConstants.Commands.Emulator;
|
||||
const { TrackEvent } = SharedConstants.Commands.Telemetry;
|
||||
const { path, name } = action.payload;
|
||||
if (isChatFile(path)) {
|
||||
yield CommandServiceImpl.call(OpenChatFile, path, true);
|
||||
CommandServiceImpl.remoteCall(TrackEvent, 'chatFile_open').catch(_e => void 0);
|
||||
} else if (isTranscriptFile(path)) {
|
||||
yield CommandServiceImpl.call(OpenTranscript, path, name);
|
||||
CommandServiceImpl.remoteCall(TrackEvent, 'transcriptFile_open', {
|
||||
method: 'resources_pane',
|
||||
}).catch(_e => void 0);
|
||||
public static *doRename(action: ResourcesAction<IFileService>) {
|
||||
const { payload } = action;
|
||||
const { ShowMessageBox, RenameFile } = SharedConstants.Commands.Electron;
|
||||
if (!payload.name) {
|
||||
return ResourcesSagas.commandService.remoteCall(ShowMessageBox, true, {
|
||||
type: 'error',
|
||||
title: 'Invalid file name',
|
||||
buttons: ['Ok'],
|
||||
defaultId: 1,
|
||||
message: `A valid file name must be used`,
|
||||
cancelId: 0,
|
||||
});
|
||||
}
|
||||
yield ResourcesSagas.commandService.remoteCall(RenameFile, payload);
|
||||
yield put(editResource(null));
|
||||
}
|
||||
// unknown types just fall into the abyss
|
||||
}
|
||||
|
||||
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);
|
||||
} catch (e) {
|
||||
const notification = newNotification('Unable to save resource settings', NotificationType.Error);
|
||||
yield put(beginAdd(notification));
|
||||
public static *doOpenResource(action: ResourcesAction<IFileService>): IterableIterator<any> {
|
||||
const { OpenChatFile, OpenTranscript } = SharedConstants.Commands.Emulator;
|
||||
const { TrackEvent } = SharedConstants.Commands.Telemetry;
|
||||
const { path, name } = action.payload;
|
||||
if (isChatFile(path)) {
|
||||
yield ResourcesSagas.commandService.call(OpenChatFile, path, true);
|
||||
ResourcesSagas.commandService.remoteCall(TrackEvent, 'chatFile_open').catch(_e => void 0);
|
||||
} else if (isTranscriptFile(path)) {
|
||||
yield ResourcesSagas.commandService.call(OpenTranscript, path, name);
|
||||
ResourcesSagas.commandService
|
||||
.remoteCall(TrackEvent, 'transcriptFile_open', {
|
||||
method: 'resources_pane',
|
||||
})
|
||||
.catch();
|
||||
}
|
||||
// unknown types just fall into the abyss
|
||||
}
|
||||
|
||||
public static *launchResourcesSettingsModal(action: ResourcesAction<{ dialog: ComponentClass<any> }>) {
|
||||
const result: Partial<BotInfo> = yield DialogService.showDialog(action.payload.dialog);
|
||||
if (result) {
|
||||
try {
|
||||
yield ResourcesSagas.commandService.remoteCall(SharedConstants.Commands.Bot.PatchBotList, result.path, result);
|
||||
} catch (e) {
|
||||
const notification = newNotification('Unable to save resource settings', NotificationType.Error);
|
||||
yield put(beginAdd(notification));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* resourceSagas(): IterableIterator<ForkEffect> {
|
||||
yield takeEvery(OPEN_CONTEXT_MENU_FOR_RESOURCE, openContextMenuForResource);
|
||||
yield takeEvery(RENAME_RESOURCE, doRename);
|
||||
yield takeEvery(OPEN_RESOURCE, doOpenResource);
|
||||
yield takeEvery(OPEN_RESOURCE_SETTINGS, launchResourcesSettingsModal);
|
||||
yield takeEvery(OPEN_CONTEXT_MENU_FOR_RESOURCE, ResourcesSagas.openContextMenuForResource);
|
||||
yield takeEvery(RENAME_RESOURCE, ResourcesSagas.doRename);
|
||||
yield takeEvery(OPEN_RESOURCE, ResourcesSagas.doOpenResource);
|
||||
yield takeEvery(OPEN_RESOURCE_SETTINGS, ResourcesSagas.launchResourcesSettingsModal);
|
||||
}
|
||||
|
|
|
@ -35,8 +35,9 @@ import { ServiceTypes } from 'botframework-config/lib/schema';
|
|||
import { applyMiddleware, combineReducers, createStore } from 'redux';
|
||||
import sagaMiddlewareFactory from 'redux-saga';
|
||||
import { call } from 'redux-saga/effects';
|
||||
import { CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
import { CommandServiceImpl } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
import {
|
||||
AzureLoginFailedDialogContainer,
|
||||
AzureLoginSuccessDialogContainer,
|
||||
|
@ -61,7 +62,7 @@ import {
|
|||
import { azureAuth } from '../reducer/azureAuthReducer';
|
||||
import { bot } from '../reducer/bot';
|
||||
|
||||
import { launchExternalLink as launchExternalLinkSaga, servicesExplorerSagas } from './servicesExplorerSagas';
|
||||
import { ServicesExplorerSagas, servicesExplorerSagas } from './servicesExplorerSagas';
|
||||
|
||||
const sagaMiddleWare = sagaMiddlewareFactory();
|
||||
const mockStore = createStore(combineReducers({ azureAuth, bot }), {}, applyMiddleware(sagaMiddleWare));
|
||||
|
@ -69,66 +70,62 @@ sagaMiddleWare.run(servicesExplorerSagas);
|
|||
|
||||
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() {
|
||||
return undefined;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../ui/shell/explorer/servicesExplorer', () => ({
|
||||
ConnectedServicePicker: function mock() {
|
||||
return undefined;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../store', () => ({
|
||||
get store() {
|
||||
return mockStore;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('./azureAuthSaga', () => ({
|
||||
getArmToken: function*() {
|
||||
// eslint-disable-next-line typescript/camelcase
|
||||
yield { access_token: mockArmToken };
|
||||
AzureAuthSaga: {
|
||||
getArmToken: function*() {
|
||||
// eslint-disable-next-line typescript/camelcase
|
||||
yield { access_token: mockArmToken };
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
remoteCall: () => Promise.resolve(true),
|
||||
},
|
||||
}));
|
||||
|
||||
CommandServiceImpl.remoteCall = async function(type: string) {
|
||||
switch (type) {
|
||||
case SharedConstants.Commands.ConnectedService.GetConnectedServicesByType:
|
||||
return { services: [{ id: 'a luis service' }], code: ServiceCodes.OK };
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
describe('The ServiceExplorerSagas', () => {
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
commandService.remoteCall = (type: string) => {
|
||||
switch (type) {
|
||||
case SharedConstants.Commands.ConnectedService.GetConnectedServicesByType:
|
||||
return { services: [{ id: 'a luis service' }], code: ServiceCodes.OK };
|
||||
|
||||
default:
|
||||
return null as any;
|
||||
}
|
||||
};
|
||||
});
|
||||
describe(' launchConnectedServicePicker happy path', () => {
|
||||
let launchConnectedServicePickerGen;
|
||||
let payload: ConnectedServicePickerPayload;
|
||||
|
@ -218,11 +215,11 @@ describe('The ServiceExplorerSagas', () => {
|
|||
const botConfig = it.next(newModels).value.SELECT.selector(mockStore.getState());
|
||||
let _type;
|
||||
let _args;
|
||||
CommandServiceImpl.remoteCall = function(type: string, ...args: any[]) {
|
||||
commandService.remoteCall = function(type: string, ...args: any[]) {
|
||||
_type = type;
|
||||
_args = args;
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
} as any;
|
||||
|
||||
const result = await it.next(botConfig).value;
|
||||
expect(result).toBeTruthy();
|
||||
|
@ -257,7 +254,7 @@ describe('The ServiceExplorerSagas', () => {
|
|||
});
|
||||
|
||||
it('should open the service deep link when the "open" menu item is selected', async () => {
|
||||
CommandServiceImpl.remoteCall = async () => ({ id: 'open' });
|
||||
commandService.remoteCall = async () => ({ id: 'open' } as any);
|
||||
|
||||
const it = contextMenuGen(action);
|
||||
const result = await it.next().value;
|
||||
|
@ -273,14 +270,14 @@ describe('The ServiceExplorerSagas', () => {
|
|||
});
|
||||
|
||||
it('should open the luis editor when the "edit" item is selected', async () => {
|
||||
CommandServiceImpl.remoteCall = async () => ({ id: 'edit' });
|
||||
commandService.remoteCall = async () => ({ id: 'edit' } as any);
|
||||
|
||||
const it = contextMenuGen(action);
|
||||
const result = await it.next().value;
|
||||
expect(result.id).toBe('edit');
|
||||
|
||||
DialogService.showDialog = () => Promise.resolve(mockService);
|
||||
CommandServiceImpl.remoteCall = () => Promise.resolve(true);
|
||||
commandService.remoteCall = () => Promise.resolve(true) as any;
|
||||
|
||||
const responseFromEditor = await it.next(result).value;
|
||||
expect(responseFromEditor).toEqual(mockService);
|
||||
|
@ -290,17 +287,17 @@ describe('The ServiceExplorerSagas', () => {
|
|||
});
|
||||
|
||||
it('should ask the main process to remove the selected luis service from the active bot', async () => {
|
||||
CommandServiceImpl.remoteCall = async () => ({ id: 'forget' });
|
||||
commandService.remoteCall = async () => ({ id: 'forget' } as any);
|
||||
const it = contextMenuGen(action);
|
||||
let result = await it.next().value;
|
||||
expect(result.id).toBe('forget');
|
||||
|
||||
let _type;
|
||||
let _args;
|
||||
CommandServiceImpl.remoteCall = async (type: string, ...args: any[]) => {
|
||||
commandService.remoteCall = async (type: string, ...args: any[]) => {
|
||||
_type = type;
|
||||
_args = args;
|
||||
return true;
|
||||
return true as any;
|
||||
};
|
||||
|
||||
result = await it.next(result).value;
|
||||
|
@ -321,7 +318,7 @@ describe('The ServiceExplorerSagas', () => {
|
|||
});
|
||||
|
||||
it('should ask the main process to remove the selected QnA Maker service from the active bot', async () => {
|
||||
CommandServiceImpl.remoteCall = async () => ({ id: 'forget' });
|
||||
commandService.remoteCall = async () => ({ id: 'forget' } as any);
|
||||
|
||||
action.payload.connectedService = JSON.parse(`{
|
||||
"type": "qna",
|
||||
|
@ -339,10 +336,10 @@ describe('The ServiceExplorerSagas', () => {
|
|||
|
||||
let _type;
|
||||
let _args;
|
||||
CommandServiceImpl.remoteCall = async (type: string, ...args: any[]) => {
|
||||
commandService.remoteCall = async (type: string, ...args: any[]) => {
|
||||
_type = type;
|
||||
_args = args;
|
||||
return true;
|
||||
return true as any;
|
||||
};
|
||||
|
||||
result = await it.next(result).value;
|
||||
|
@ -363,7 +360,7 @@ describe('The ServiceExplorerSagas', () => {
|
|||
});
|
||||
|
||||
it('should ask the main process to remove the selected dispatch Maker service from the active bot', async () => {
|
||||
CommandServiceImpl.remoteCall = async () => ({ id: 'forget' });
|
||||
commandService.remoteCall = async () => ({ id: 'forget' } as any);
|
||||
|
||||
action.payload.connectedService = JSON.parse(`{
|
||||
"type": "dispatch",
|
||||
|
@ -381,10 +378,10 @@ describe('The ServiceExplorerSagas', () => {
|
|||
|
||||
let _type;
|
||||
let _args;
|
||||
CommandServiceImpl.remoteCall = async (type: string, ...args: any[]) => {
|
||||
commandService.remoteCall = async (type: string, ...args: any[]) => {
|
||||
_type = type;
|
||||
_args = args;
|
||||
return true;
|
||||
return true as any;
|
||||
};
|
||||
|
||||
result = await it.next(result).value;
|
||||
|
@ -407,7 +404,6 @@ describe('The ServiceExplorerSagas', () => {
|
|||
|
||||
describe(' launchExternalLink', () => {
|
||||
let action: ConnectedServiceAction<ConnectedServicePayload>;
|
||||
let openConnectedServiceGen;
|
||||
let sagaIt;
|
||||
|
||||
it(' should open a LIUS external link', async () => {
|
||||
|
@ -424,14 +420,14 @@ describe('The ServiceExplorerSagas', () => {
|
|||
};
|
||||
|
||||
action = launchExternalLink(payload as any);
|
||||
sagaIt = launchExternalLinkSaga;
|
||||
sagaIt = ServicesExplorerSagas.launchExternalLink;
|
||||
|
||||
const it = sagaIt(action);
|
||||
const result = it.next().value;
|
||||
|
||||
expect(result).toEqual(
|
||||
call(
|
||||
[CommandServiceImpl, CommandServiceImpl.remoteCall],
|
||||
[commandService, commandService.remoteCall],
|
||||
SharedConstants.Commands.Electron.OpenExternal,
|
||||
'https://luis.ai'
|
||||
)
|
||||
|
@ -452,14 +448,14 @@ describe('The ServiceExplorerSagas', () => {
|
|||
};
|
||||
|
||||
action = launchExternalLink(payload as any);
|
||||
sagaIt = launchExternalLinkSaga;
|
||||
sagaIt = ServicesExplorerSagas.launchExternalLink;
|
||||
|
||||
const it = sagaIt(action);
|
||||
const result = it.next().value;
|
||||
|
||||
expect(result).toEqual(
|
||||
call(
|
||||
[CommandServiceImpl, CommandServiceImpl.remoteCall],
|
||||
[commandService, commandService.remoteCall],
|
||||
SharedConstants.Commands.Electron.OpenExternal,
|
||||
'https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-tutorial-dispatch?view=azure-bot-service-4.0&tabs=csharp'
|
||||
)
|
||||
|
@ -480,14 +476,14 @@ describe('The ServiceExplorerSagas', () => {
|
|||
};
|
||||
|
||||
action = launchExternalLink(payload as any);
|
||||
sagaIt = launchExternalLinkSaga;
|
||||
sagaIt = ServicesExplorerSagas.launchExternalLink;
|
||||
|
||||
const it = sagaIt(action);
|
||||
const result = it.next().value;
|
||||
|
||||
expect(result).toEqual(
|
||||
call(
|
||||
[CommandServiceImpl, CommandServiceImpl.remoteCall],
|
||||
[commandService, commandService.remoteCall],
|
||||
SharedConstants.Commands.Electron.OpenExternal,
|
||||
'https://www.qnamaker.ai/'
|
||||
)
|
||||
|
@ -520,7 +516,7 @@ describe('The ServiceExplorerSagas', () => {
|
|||
});
|
||||
|
||||
it('should launch the luis connected service picker workflow when the luis menu item is selected', async () => {
|
||||
CommandServiceImpl.remoteCall = async () => ({ id: ServiceTypes.Luis });
|
||||
commandService.remoteCall = async () => ({ id: ServiceTypes.Luis } as any);
|
||||
const it = contextMenuGen(action);
|
||||
let result = await it.next().value;
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
//
|
||||
|
||||
import { ServiceCodes, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { BotConfigWithPath } from '@bfemulator/sdk-shared';
|
||||
import { BotConfigWithPath, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
import { BotConfigurationBase } from 'botframework-config/lib/botConfigurationBase';
|
||||
import {
|
||||
IAzureService,
|
||||
|
@ -44,7 +44,6 @@ import {
|
|||
} from 'botframework-config/lib/schema';
|
||||
import { call, 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';
|
||||
|
@ -54,17 +53,17 @@ import {
|
|||
ConnectedServicePickerPayload,
|
||||
LAUNCH_CONNECTED_SERVICE_EDITOR,
|
||||
LAUNCH_CONNECTED_SERVICE_PICKER,
|
||||
LAUNCH_EXTERNAL_LINK,
|
||||
OPEN_ADD_CONNECTED_SERVICE_CONTEXT_MENU,
|
||||
OPEN_CONNECTED_SERVICE_SORT_CONTEXT_MENU,
|
||||
OPEN_CONTEXT_MENU_FOR_CONNECTED_SERVICE,
|
||||
OPEN_SERVICE_DEEP_LINK,
|
||||
LAUNCH_EXTERNAL_LINK,
|
||||
} from '../action/connectedServiceActions';
|
||||
import { sortExplorerContents } from '../action/explorerActions';
|
||||
import { SortCriteria } from '../reducer/explorer';
|
||||
import { RootState } from '../store';
|
||||
|
||||
import { getArmToken } from './azureAuthSaga';
|
||||
import { AzureAuthSaga } from './azureAuthSaga';
|
||||
|
||||
declare interface ServicesPayload {
|
||||
services: IConnectedService[];
|
||||
|
@ -76,325 +75,373 @@ const geBotConfigFromState = (state: RootState): BotConfigWithPath => state.bot.
|
|||
const getSortSelection = (state: RootState): { [paneldId: string]: SortCriteria } =>
|
||||
state.explorer.sortSelectionByPanelId;
|
||||
|
||||
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;
|
||||
armTokenData = yield* getArmToken(
|
||||
beginAzureAuthWorkflow(
|
||||
promptDialog,
|
||||
{ serviceType: action.payload.serviceType },
|
||||
loginSuccessDialog,
|
||||
loginFailedDialog
|
||||
)
|
||||
);
|
||||
export class ServicesExplorerSagas {
|
||||
@CommandServiceInstance()
|
||||
private static commandService: CommandServiceImpl;
|
||||
|
||||
public static *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;
|
||||
armTokenData = yield* AzureAuthSaga.getArmToken(
|
||||
beginAzureAuthWorkflow(
|
||||
promptDialog,
|
||||
{ serviceType: action.payload.serviceType },
|
||||
loginSuccessDialog,
|
||||
loginFailedDialog
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 2 means the user has chosen to manually enter the connected service
|
||||
if (armTokenData === 2) {
|
||||
yield* ServicesExplorerSagas.launchConnectedServiceEditor(action);
|
||||
return;
|
||||
}
|
||||
if (!armTokenData || 'error' in armTokenData) {
|
||||
return null; // canceled or failed somewhere
|
||||
}
|
||||
// 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;
|
||||
const { serviceType, progressIndicatorComponent } = action.payload;
|
||||
if (progressIndicatorComponent) {
|
||||
DialogService.showDialog(progressIndicatorComponent).catch();
|
||||
}
|
||||
const payload: ServicesPayload = yield* ServicesExplorerSagas.retrieveServicesByServiceType(serviceType);
|
||||
|
||||
if (progressIndicatorComponent) {
|
||||
DialogService.hideDialog();
|
||||
}
|
||||
|
||||
if (payload.code !== ServiceCodes.OK || !payload.services.length) {
|
||||
const { getStartedDialog, authenticatedUser } = action.payload;
|
||||
const result = yield DialogService.showDialog(getStartedDialog, {
|
||||
serviceType,
|
||||
authenticatedUser,
|
||||
showNoModelsFoundContent: !payload.services.length,
|
||||
});
|
||||
// Sign up with XXXX
|
||||
if (result === 1) {
|
||||
yield* ServicesExplorerSagas.launchExternalLink(action);
|
||||
}
|
||||
// Add services manually
|
||||
if (result === 2) {
|
||||
yield* ServicesExplorerSagas.launchConnectedServiceEditor(action);
|
||||
}
|
||||
} else {
|
||||
const servicesToAdd = yield* ServicesExplorerSagas.launchConnectedServicePickList(
|
||||
action,
|
||||
payload.services,
|
||||
serviceType
|
||||
);
|
||||
if (servicesToAdd) {
|
||||
const botFile: BotConfigWithPath = yield select(geBotConfigFromState);
|
||||
botFile.services.push(...servicesToAdd);
|
||||
const { Bot } = SharedConstants.Commands;
|
||||
yield ServicesExplorerSagas.commandService.remoteCall(Bot.Save, botFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2 means the user has chosen to manually enter the connected service
|
||||
if (armTokenData === 2) {
|
||||
yield* launchConnectedServiceEditor(action);
|
||||
return;
|
||||
}
|
||||
if (!armTokenData || 'error' in armTokenData) {
|
||||
return null; // canceled or failed somewhere
|
||||
}
|
||||
// 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;
|
||||
const { serviceType, progressIndicatorComponent } = action.payload;
|
||||
if (progressIndicatorComponent) {
|
||||
DialogService.showDialog(progressIndicatorComponent).catch();
|
||||
}
|
||||
const payload: ServicesPayload = yield* retrieveServicesByServiceType(serviceType);
|
||||
|
||||
if (progressIndicatorComponent) {
|
||||
DialogService.hideDialog();
|
||||
}
|
||||
|
||||
if (payload.code !== ServiceCodes.OK || !payload.services.length) {
|
||||
const { getStartedDialog, authenticatedUser } = action.payload;
|
||||
const result = yield DialogService.showDialog(getStartedDialog, {
|
||||
serviceType,
|
||||
public static *launchConnectedServicePickList(
|
||||
action: ConnectedServiceAction<ConnectedServicePickerPayload>,
|
||||
availableServices: IConnectedService[],
|
||||
serviceType: ServiceTypes
|
||||
): IterableIterator<any> {
|
||||
const { pickerComponent, authenticatedUser, serviceType: type } = action.payload;
|
||||
let result = yield DialogService.showDialog(pickerComponent, {
|
||||
availableServices,
|
||||
authenticatedUser,
|
||||
showNoModelsFoundContent: !payload.services.length,
|
||||
serviceType,
|
||||
});
|
||||
// Sign up with XXXX
|
||||
|
||||
if (result === 1) {
|
||||
yield* launchExternalLink(action);
|
||||
action.payload.connectedService = BotConfigurationBase.serviceFromJSON({
|
||||
type,
|
||||
hostname: '' /* defect workaround */,
|
||||
} as any);
|
||||
result = yield* ServicesExplorerSagas.launchConnectedServiceEditor(action);
|
||||
}
|
||||
// Add services manually
|
||||
if (result === 2) {
|
||||
yield* launchConnectedServiceEditor(action);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static *retrieveServicesByServiceType(serviceType: ServiceTypes): IterableIterator<any> {
|
||||
const armTokenData: ArmTokenData = yield select(getArmTokenFromState);
|
||||
if (!armTokenData || !armTokenData.access_token) {
|
||||
throw new Error('Auth credentials do not exist.');
|
||||
}
|
||||
} else {
|
||||
const servicesToAdd = yield* launchConnectedServicePickList(action, payload.services, serviceType);
|
||||
if (servicesToAdd) {
|
||||
const botFile: BotConfigWithPath = yield select(geBotConfigFromState);
|
||||
botFile.services.push(...servicesToAdd);
|
||||
const { Bot } = SharedConstants.Commands;
|
||||
yield CommandServiceImpl.remoteCall(Bot.Save, botFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* launchConnectedServicePickList(
|
||||
action: ConnectedServiceAction<ConnectedServicePickerPayload>,
|
||||
availableServices: IConnectedService[],
|
||||
serviceType: ServiceTypes
|
||||
): IterableIterator<any> {
|
||||
const { pickerComponent, authenticatedUser, serviceType: type } = action.payload;
|
||||
let result = yield DialogService.showDialog(pickerComponent, {
|
||||
availableServices,
|
||||
authenticatedUser,
|
||||
serviceType,
|
||||
});
|
||||
|
||||
if (result === 1) {
|
||||
action.payload.connectedService = BotConfigurationBase.serviceFromJSON({
|
||||
type,
|
||||
hostname: '' /* defect workaround */,
|
||||
} as any);
|
||||
result = yield* launchConnectedServiceEditor(action);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
let payload: ServicesPayload;
|
||||
try {
|
||||
payload = yield CommandServiceImpl.remoteCall(GetConnectedServicesByType, armTokenData.access_token, serviceType);
|
||||
} catch (e) {
|
||||
payload = { services: [], code: ServiceCodes.Error };
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
case ServiceTypes.BlobStorage:
|
||||
return openAzureProviderDeepLink('Microsoft.DocumentDB/storageAccounts', connectedService as IAzureService);
|
||||
|
||||
case ServiceTypes.Bot:
|
||||
return openAzureProviderDeepLink('Microsoft.BotService/botServices', connectedService as IAzureService);
|
||||
|
||||
case ServiceTypes.CosmosDB:
|
||||
return openAzureProviderDeepLink('Microsoft.DocumentDb/databaseAccounts', connectedService as IAzureService);
|
||||
|
||||
case ServiceTypes.Generic:
|
||||
return window.open((connectedService as IGenericService).url);
|
||||
|
||||
case ServiceTypes.Luis:
|
||||
return openLuisDeepLink(connectedService as ILuisService);
|
||||
|
||||
case ServiceTypes.QnA:
|
||||
return openQnaMakerDeepLink(connectedService as IQnAService);
|
||||
|
||||
default:
|
||||
return window.open('https://portal.azure.com');
|
||||
}
|
||||
}
|
||||
|
||||
export function* launchExternalLink(action: ConnectedServiceAction<ConnectedServicePayload>): IterableIterator<any> {
|
||||
const serviceType = action.payload.serviceType;
|
||||
switch (serviceType) {
|
||||
case ServiceTypes.QnA:
|
||||
yield call(
|
||||
[CommandServiceImpl, CommandServiceImpl.remoteCall],
|
||||
SharedConstants.Commands.Electron.OpenExternal,
|
||||
'https://www.qnamaker.ai/'
|
||||
const { GetConnectedServicesByType } = SharedConstants.Commands.ConnectedService;
|
||||
let payload: ServicesPayload;
|
||||
try {
|
||||
payload = yield ServicesExplorerSagas.commandService.remoteCall(
|
||||
GetConnectedServicesByType,
|
||||
armTokenData.access_token,
|
||||
serviceType
|
||||
);
|
||||
break;
|
||||
|
||||
case ServiceTypes.Dispatch:
|
||||
yield call(
|
||||
[CommandServiceImpl, CommandServiceImpl.remoteCall],
|
||||
SharedConstants.Commands.Electron.OpenExternal,
|
||||
'https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-tutorial-dispatch?view=azure-bot-service-4.0&tabs=csharp'
|
||||
);
|
||||
break;
|
||||
|
||||
case ServiceTypes.Luis:
|
||||
yield call(
|
||||
[CommandServiceImpl, CommandServiceImpl.remoteCall],
|
||||
SharedConstants.Commands.Electron.OpenExternal,
|
||||
'https://luis.ai'
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
} catch (e) {
|
||||
payload = { services: [], code: ServiceCodes.Error };
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
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' },
|
||||
];
|
||||
const response = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.DisplayContextMenu, menuItems);
|
||||
const { connectedService } = action.payload;
|
||||
action.payload.serviceType = connectedService.type;
|
||||
switch (response.id) {
|
||||
case 'open':
|
||||
yield* openConnectedServiceDeepLink(action);
|
||||
break;
|
||||
// eslint-disable-next-line require-yield
|
||||
public static *openConnectedServiceDeepLink(
|
||||
action: ConnectedServiceAction<ConnectedServicePayload>
|
||||
): IterableIterator<any> {
|
||||
const { connectedService } = action.payload;
|
||||
switch (connectedService.type) {
|
||||
case ServiceTypes.AppInsights:
|
||||
return ServicesExplorerSagas.openAzureProviderDeepLink(
|
||||
'microsoft.insights/components',
|
||||
connectedService as IAzureService
|
||||
);
|
||||
|
||||
case 'edit':
|
||||
yield* launchConnectedServiceEditor(action);
|
||||
break;
|
||||
case ServiceTypes.BlobStorage:
|
||||
return ServicesExplorerSagas.openAzureProviderDeepLink(
|
||||
'Microsoft.DocumentDB/storageAccounts',
|
||||
connectedService as IAzureService
|
||||
);
|
||||
|
||||
case 'forget':
|
||||
yield* removeServiceFromActiveBot(connectedService);
|
||||
break;
|
||||
case ServiceTypes.Bot:
|
||||
return ServicesExplorerSagas.openAzureProviderDeepLink(
|
||||
'Microsoft.BotService/botServices',
|
||||
connectedService as IAzureService
|
||||
);
|
||||
|
||||
default:
|
||||
// canceled context menu
|
||||
return;
|
||||
}
|
||||
}
|
||||
case ServiceTypes.CosmosDB:
|
||||
return ServicesExplorerSagas.openAzureProviderDeepLink(
|
||||
'Microsoft.DocumentDb/databaseAccounts',
|
||||
connectedService as IAzureService
|
||||
);
|
||||
|
||||
function* openAddConnectedServiceContextMenu(
|
||||
action: ConnectedServiceAction<ConnectedServicePickerPayload>
|
||||
): IterableIterator<any> {
|
||||
const menuItems = [
|
||||
{ label: 'Add Language Understanding (LUIS)', id: ServiceTypes.Luis },
|
||||
{ label: 'Add QnA Maker', id: ServiceTypes.QnA },
|
||||
{ label: 'Add Dispatch', id: ServiceTypes.Dispatch },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Add Azure Cosmos DB account', id: ServiceTypes.CosmosDB },
|
||||
{ label: 'Add Azure Storage account', id: ServiceTypes.BlobStorage },
|
||||
{ label: 'Add Azure Application Insights', id: ServiceTypes.AppInsights },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Add other service …', id: ServiceTypes.Generic },
|
||||
];
|
||||
case ServiceTypes.Generic:
|
||||
return window.open((connectedService as IGenericService).url);
|
||||
|
||||
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) {
|
||||
yield* launchConnectedServiceEditor(action);
|
||||
} else {
|
||||
yield* launchConnectedServicePicker(action);
|
||||
}
|
||||
}
|
||||
case ServiceTypes.Luis:
|
||||
return ServicesExplorerSagas.openLuisDeepLink(connectedService as ILuisService);
|
||||
|
||||
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',
|
||||
},
|
||||
];
|
||||
const response = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.DisplayContextMenu, menuItems);
|
||||
yield response.id ? put(sortExplorerContents(action.payload.panelId, response.id)) : null;
|
||||
}
|
||||
case ServiceTypes.QnA:
|
||||
return ServicesExplorerSagas.openQnaMakerDeepLink(connectedService as IQnAService);
|
||||
|
||||
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,
|
||||
});
|
||||
if (result) {
|
||||
const { RemoveService } = SharedConstants.Commands.Bot;
|
||||
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,
|
||||
authenticatedUser,
|
||||
serviceType,
|
||||
});
|
||||
|
||||
if (servicesToUpdate) {
|
||||
let i = servicesToUpdate.length;
|
||||
while (i--) {
|
||||
const service = servicesToUpdate[i];
|
||||
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.AddOrUpdateService, service.type, service);
|
||||
default:
|
||||
return window.open('https://portal.azure.com');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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`,
|
||||
];
|
||||
public static *launchExternalLink(action: ConnectedServiceAction<ConnectedServicePayload>): IterableIterator<any> {
|
||||
const serviceType = action.payload.serviceType;
|
||||
switch (serviceType) {
|
||||
case ServiceTypes.QnA:
|
||||
yield call(
|
||||
[ServicesExplorerSagas.commandService, ServicesExplorerSagas.commandService.remoteCall],
|
||||
SharedConstants.Commands.Electron.OpenExternal,
|
||||
'https://www.qnamaker.ai/'
|
||||
);
|
||||
break;
|
||||
|
||||
window.open(bits.join(''));
|
||||
}
|
||||
case ServiceTypes.Dispatch:
|
||||
yield call(
|
||||
[ServicesExplorerSagas.commandService, ServicesExplorerSagas.commandService.remoteCall],
|
||||
SharedConstants.Commands.Electron.OpenExternal,
|
||||
'https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-tutorial-dispatch?view=azure-bot-service-4.0&tabs=csharp'
|
||||
);
|
||||
break;
|
||||
|
||||
function openLuisDeepLink(luisService: ILuisService) {
|
||||
const { appId, version, region } = luisService;
|
||||
let regionPrefix: string;
|
||||
switch (region) {
|
||||
case 'westeurope':
|
||||
regionPrefix = 'eu.';
|
||||
break;
|
||||
case ServiceTypes.Luis:
|
||||
yield call(
|
||||
[ServicesExplorerSagas.commandService, ServicesExplorerSagas.commandService.remoteCall],
|
||||
SharedConstants.Commands.Electron.OpenExternal,
|
||||
'https://luis.ai'
|
||||
);
|
||||
break;
|
||||
|
||||
case 'australiaeast':
|
||||
regionPrefix = 'au.';
|
||||
break;
|
||||
|
||||
default:
|
||||
regionPrefix = '';
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
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://www.qnamaker.ai/Edit/KnowledgeBase?kbId=${encodeURIComponent(kbId)}`;
|
||||
window.open(link);
|
||||
public static *openContextMenuForService(
|
||||
action: ConnectedServiceAction<ConnectedServicePayload>
|
||||
): IterableIterator<any> {
|
||||
const menuItems = [
|
||||
{ label: 'Manage service', id: 'open' },
|
||||
{ label: 'Edit configuration', id: 'edit' },
|
||||
{ label: 'Disconnect this service', id: 'forget' },
|
||||
];
|
||||
const response = yield ServicesExplorerSagas.commandService.remoteCall(
|
||||
SharedConstants.Commands.Electron.DisplayContextMenu,
|
||||
menuItems
|
||||
);
|
||||
const { connectedService } = action.payload;
|
||||
action.payload.serviceType = connectedService.type;
|
||||
switch (response.id) {
|
||||
case 'open':
|
||||
yield* ServicesExplorerSagas.openConnectedServiceDeepLink(action);
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
yield* ServicesExplorerSagas.launchConnectedServiceEditor(action);
|
||||
break;
|
||||
|
||||
case 'forget':
|
||||
yield* ServicesExplorerSagas.removeServiceFromActiveBot(connectedService);
|
||||
break;
|
||||
|
||||
default:
|
||||
// canceled context menu
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static *openAddConnectedServiceContextMenu(
|
||||
action: ConnectedServiceAction<ConnectedServicePickerPayload>
|
||||
): IterableIterator<any> {
|
||||
const menuItems = [
|
||||
{ label: 'Add Language Understanding (LUIS)', id: ServiceTypes.Luis },
|
||||
{ label: 'Add QnA Maker', id: ServiceTypes.QnA },
|
||||
{ label: 'Add Dispatch', id: ServiceTypes.Dispatch },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Add Azure Cosmos DB account', id: ServiceTypes.CosmosDB },
|
||||
{ label: 'Add Azure Storage account', id: ServiceTypes.BlobStorage },
|
||||
{ label: 'Add Azure Application Insights', id: ServiceTypes.AppInsights },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Add other service …', id: ServiceTypes.Generic },
|
||||
];
|
||||
|
||||
const response = yield ServicesExplorerSagas.commandService.remoteCall(
|
||||
SharedConstants.Commands.Electron.DisplayContextMenu,
|
||||
menuItems
|
||||
);
|
||||
const { id: serviceType } = response;
|
||||
action.payload.serviceType = serviceType;
|
||||
if (serviceType === ServiceTypes.Generic || serviceType === ServiceTypes.AppInsights) {
|
||||
yield* ServicesExplorerSagas.launchConnectedServiceEditor(action);
|
||||
} else {
|
||||
yield* ServicesExplorerSagas.launchConnectedServicePicker(action);
|
||||
}
|
||||
}
|
||||
|
||||
public static *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',
|
||||
},
|
||||
];
|
||||
const response = yield ServicesExplorerSagas.commandService.remoteCall(
|
||||
SharedConstants.Commands.Electron.DisplayContextMenu,
|
||||
menuItems
|
||||
);
|
||||
yield response.id ? put(sortExplorerContents(action.payload.panelId, response.id)) : null;
|
||||
}
|
||||
|
||||
public static *removeServiceFromActiveBot(connectedService: IConnectedService): IterableIterator<any> {
|
||||
// TODO - localization
|
||||
const result = yield ServicesExplorerSagas.commandService.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 ServicesExplorerSagas.commandService.remoteCall(RemoveService, connectedService.type, connectedService.id);
|
||||
}
|
||||
}
|
||||
|
||||
public static *launchConnectedServiceEditor(
|
||||
action: ConnectedServiceAction<ConnectedServicePayload>
|
||||
): IterableIterator<any> {
|
||||
const { editorComponent, authenticatedUser, 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 ServicesExplorerSagas.commandService.remoteCall(
|
||||
SharedConstants.Commands.Bot.AddOrUpdateService,
|
||||
service.type,
|
||||
service
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static 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`,
|
||||
];
|
||||
|
||||
window.open(bits.join(''));
|
||||
}
|
||||
|
||||
public static openLuisDeepLink(luisService: ILuisService) {
|
||||
const { appId, version, region } = luisService;
|
||||
let regionPrefix: string;
|
||||
switch (region) {
|
||||
case 'westeurope':
|
||||
regionPrefix = 'eu.';
|
||||
break;
|
||||
|
||||
case 'australiaeast':
|
||||
regionPrefix = 'au.';
|
||||
break;
|
||||
|
||||
default:
|
||||
regionPrefix = '';
|
||||
break;
|
||||
}
|
||||
const linkArray = ['https://', `${encodeURI(regionPrefix)}`, 'luis.ai/applications/'];
|
||||
linkArray.push(`${encodeURI(appId)}`, '/versions/', `${encodeURI(version)}`, '/build');
|
||||
const link = linkArray.join('');
|
||||
window.open(link);
|
||||
}
|
||||
|
||||
public static openQnaMakerDeepLink(service: IQnAService) {
|
||||
const { kbId } = service;
|
||||
const link = `https://www.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 takeEvery(LAUNCH_EXTERNAL_LINK, launchExternalLink);
|
||||
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 takeLatest(LAUNCH_CONNECTED_SERVICE_PICKER, ServicesExplorerSagas.launchConnectedServicePicker);
|
||||
yield takeLatest(LAUNCH_CONNECTED_SERVICE_EDITOR, ServicesExplorerSagas.launchConnectedServiceEditor);
|
||||
yield takeEvery(LAUNCH_EXTERNAL_LINK, ServicesExplorerSagas.launchExternalLink);
|
||||
yield takeEvery(OPEN_SERVICE_DEEP_LINK, ServicesExplorerSagas.openConnectedServiceDeepLink);
|
||||
yield takeEvery(OPEN_CONTEXT_MENU_FOR_CONNECTED_SERVICE, ServicesExplorerSagas.openContextMenuForService);
|
||||
yield takeEvery(OPEN_ADD_CONNECTED_SERVICE_CONTEXT_MENU, ServicesExplorerSagas.openAddConnectedServiceContextMenu);
|
||||
yield takeEvery(OPEN_CONNECTED_SERVICE_SORT_CONTEXT_MENU, ServicesExplorerSagas.openSortContextMenu);
|
||||
}
|
||||
|
|
|
@ -33,22 +33,50 @@
|
|||
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { select } from 'redux-saga/effects';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { RootState } from '../store';
|
||||
|
||||
import { editorSelector, refreshConversationMenu } from './sharedSagas';
|
||||
import { editorSelector, SharedSagas } from './sharedSagas';
|
||||
|
||||
let mockRemoteCommandsCalled = [];
|
||||
jest.mock('../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
remoteCall: async (commandName: string, ...args: any[]) => {
|
||||
mockRemoteCommandsCalled.push({ commandName, args: args });
|
||||
},
|
||||
},
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
describe('The sharedSagas', () => {
|
||||
const editorState = { activeEditor: 'primary' };
|
||||
let mockRemoteCommandsCalled = [];
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
commandService.remoteCall = async (commandName: string, ...args: any[]) => {
|
||||
mockRemoteCommandsCalled.push({ commandName, args: args });
|
||||
return true as any;
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockRemoteCommandsCalled = [];
|
||||
|
@ -61,7 +89,7 @@ describe('The sharedSagas', () => {
|
|||
});
|
||||
|
||||
it('should refresh the conversation menu', () => {
|
||||
const gen = refreshConversationMenu();
|
||||
const gen = SharedSagas.refreshConversationMenu();
|
||||
|
||||
const editorSelection = gen.next().value;
|
||||
expect(editorSelection).toEqual(select(editorSelector));
|
||||
|
|
|
@ -33,15 +33,20 @@
|
|||
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { select } from 'redux-saga/effects';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
import { RootState } from '../store';
|
||||
|
||||
export function editorSelector(state: RootState) {
|
||||
return state.editor;
|
||||
}
|
||||
|
||||
export function* refreshConversationMenu(): IterableIterator<any> {
|
||||
const stateData = yield select(editorSelector);
|
||||
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.UpdateConversationMenu, stateData);
|
||||
export class SharedSagas {
|
||||
@CommandServiceInstance()
|
||||
private static commandService: CommandServiceImpl;
|
||||
|
||||
public static *refreshConversationMenu(): IterableIterator<any> {
|
||||
const stateData = yield select(editorSelector);
|
||||
yield SharedSagas.commandService.remoteCall(SharedConstants.Commands.Electron.UpdateConversationMenu, stateData);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,8 +33,8 @@
|
|||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import sagaMiddlewareFactory from 'redux-saga';
|
||||
import { applyMiddleware, combineReducers, createStore } from 'redux';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
import { openContextMenuForBot } from '../action/welcomePageActions';
|
||||
import { bot } from '../reducer/bot';
|
||||
import notification from '../reducer/notification';
|
||||
|
@ -47,11 +47,6 @@ const mockBot = {
|
|||
displayName: 'AuthBot',
|
||||
secret: 'secret',
|
||||
};
|
||||
jest.mock('../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
remoteCall: async () => null,
|
||||
},
|
||||
}));
|
||||
|
||||
const sagaMiddleWare = sagaMiddlewareFactory();
|
||||
const mockStore = createStore(
|
||||
|
@ -68,14 +63,46 @@ jest.mock('../store', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
sagaMiddleWare.run(welcomePageSagas);
|
||||
sagaMiddleWare.run(notificationSagas);
|
||||
|
||||
describe('The WelcomePageSagas', () => {
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
});
|
||||
|
||||
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[]) {
|
||||
commandService.remoteCall = async function(...args: any[]) {
|
||||
remoteCalls.push(args);
|
||||
switch (args[0]) {
|
||||
case SharedConstants.Commands.Electron.DisplayContextMenu:
|
||||
|
@ -85,7 +112,7 @@ describe('The WelcomePageSagas', () => {
|
|||
return 'this/is/a/new/location.bot';
|
||||
|
||||
default:
|
||||
return null;
|
||||
return null as any;
|
||||
}
|
||||
};
|
||||
await mockStore.dispatch(openContextMenuForBot(mockBot));
|
||||
|
@ -142,7 +169,7 @@ describe('The WelcomePageSagas', () => {
|
|||
});
|
||||
|
||||
it('should add a notification if a remote command fails when moving a bot file', async () => {
|
||||
CommandServiceImpl.remoteCall = async function(...args: any[]) {
|
||||
commandService.remoteCall = async function(...args: any[]) {
|
||||
switch (args[0]) {
|
||||
case SharedConstants.Commands.Electron.DisplayContextMenu:
|
||||
return { id: 0 };
|
||||
|
@ -154,7 +181,7 @@ describe('The WelcomePageSagas', () => {
|
|||
throw new Error('oh noes!');
|
||||
|
||||
default:
|
||||
return null;
|
||||
return null as any;
|
||||
}
|
||||
};
|
||||
await mockStore.dispatch(openContextMenuForBot(mockBot));
|
||||
|
@ -166,7 +193,7 @@ describe('The WelcomePageSagas', () => {
|
|||
|
||||
it('should call the appropriate command when opening the bot file location', async () => {
|
||||
let openFileLocationArgs;
|
||||
CommandServiceImpl.remoteCall = async function(...args: any[]) {
|
||||
commandService.remoteCall = async function(...args: any[]) {
|
||||
switch (args[0]) {
|
||||
case SharedConstants.Commands.Electron.DisplayContextMenu:
|
||||
return { id: 1 };
|
||||
|
@ -175,7 +202,7 @@ describe('The WelcomePageSagas', () => {
|
|||
return (openFileLocationArgs = args);
|
||||
|
||||
default:
|
||||
return null;
|
||||
return null as any;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -185,8 +212,8 @@ describe('The WelcomePageSagas', () => {
|
|||
});
|
||||
|
||||
it('should call the appropriate command when removing a bot from the list', async () => {
|
||||
let removeBotFromListArgs;
|
||||
CommandServiceImpl.remoteCall = async function(...args: any[]) {
|
||||
let removeBotFromListArgs = null;
|
||||
commandService.remoteCall = async function(...args: any[]) {
|
||||
switch (args[0]) {
|
||||
case SharedConstants.Commands.Electron.DisplayContextMenu:
|
||||
return { id: 2 };
|
||||
|
@ -195,7 +222,7 @@ describe('The WelcomePageSagas', () => {
|
|||
return (removeBotFromListArgs = args);
|
||||
|
||||
default:
|
||||
return null;
|
||||
return null as any;
|
||||
}
|
||||
};
|
||||
await mockStore.dispatch(openContextMenuForBot(mockBot));
|
||||
|
|
|
@ -32,60 +32,75 @@
|
|||
//
|
||||
import { BotInfo, newNotification, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { ForkEffect, put, takeEvery } from 'redux-saga/effects';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
import { beginAdd } from '../action/notificationActions';
|
||||
import { OPEN_CONTEXT_MENU_FOR_BOT, WelcomePageAction } from '../action/welcomePageActions';
|
||||
export class WelcomePageSagas {
|
||||
@CommandServiceInstance()
|
||||
private static commandService: CommandServiceImpl;
|
||||
|
||||
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 },
|
||||
];
|
||||
public static *openContextMenuForBot(action: WelcomePageAction<BotInfo>): IterableIterator<any> {
|
||||
const menuItems = [
|
||||
{ label: 'Move...', id: 0 },
|
||||
{ label: 'Open file location', id: 1 },
|
||||
{ label: 'Forget this bot', id: 2 },
|
||||
];
|
||||
|
||||
const result = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.DisplayContextMenu, menuItems);
|
||||
switch (result.id) {
|
||||
case 0:
|
||||
yield* moveBotToNewLocation(action.payload);
|
||||
break;
|
||||
const result = yield WelcomePageSagas.commandService.remoteCall(
|
||||
SharedConstants.Commands.Electron.DisplayContextMenu,
|
||||
menuItems
|
||||
);
|
||||
switch (result.id) {
|
||||
case 0:
|
||||
yield* WelcomePageSagas.moveBotToNewLocation(action.payload);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.OpenFileLocation, action.payload.path);
|
||||
break;
|
||||
case 1:
|
||||
yield WelcomePageSagas.commandService.remoteCall(
|
||||
SharedConstants.Commands.Electron.OpenFileLocation,
|
||||
action.payload.path
|
||||
);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.RemoveFromBotList, action.payload.path);
|
||||
break;
|
||||
case 2:
|
||||
yield WelcomePageSagas.commandService.remoteCall(
|
||||
SharedConstants.Commands.Bot.RemoveFromBotList,
|
||||
action.payload.path
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Canceled context menu
|
||||
break;
|
||||
default:
|
||||
// Canceled context menu
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static *moveBotToNewLocation(bot: BotInfo): IterableIterator<any> {
|
||||
const newPath = yield WelcomePageSagas.commandService.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 WelcomePageSagas.commandService.remoteCall(SharedConstants.Commands.Electron.RenameFile, {
|
||||
path: oldPath,
|
||||
newPath,
|
||||
});
|
||||
yield WelcomePageSagas.commandService.remoteCall(SharedConstants.Commands.Bot.PatchBotList, oldPath, bot);
|
||||
} catch (e) {
|
||||
const errMsg = `Error occurred while moving the bot file: ${e}`;
|
||||
const notification = newNotification(errMsg);
|
||||
yield put(beginAdd(notification));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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'] }],
|
||||
});
|
||||
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);
|
||||
} catch (e) {
|
||||
const errMsg = `Error occurred while moving the bot file: ${e}`;
|
||||
const notification = newNotification(errMsg);
|
||||
yield put(beginAdd(notification));
|
||||
}
|
||||
}
|
||||
|
||||
export function* welcomePageSagas(): IterableIterator<ForkEffect> {
|
||||
yield takeEvery(OPEN_CONTEXT_MENU_FOR_BOT, openContextMenuForBot);
|
||||
yield takeEvery(OPEN_CONTEXT_MENU_FOR_BOT, WelcomePageSagas.openContextMenuForBot);
|
||||
}
|
||||
|
|
|
@ -32,20 +32,10 @@
|
|||
//
|
||||
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import {
|
||||
CommandRegistryImpl,
|
||||
CommandService,
|
||||
CommandServiceImpl,
|
||||
ExtensionConfig,
|
||||
ExtensionInspector,
|
||||
} from '@bfemulator/sdk-shared';
|
||||
|
||||
import { ElectronIPC } from './ipc';
|
||||
import { Command, ExtensionConfig, ExtensionInspector } from '@bfemulator/sdk-shared';
|
||||
|
||||
// =============================================================================
|
||||
export class Extension {
|
||||
private _ext: CommandService;
|
||||
|
||||
public get unid(): string {
|
||||
return this._unid;
|
||||
}
|
||||
|
@ -54,14 +44,7 @@ export class Extension {
|
|||
return this._config;
|
||||
}
|
||||
|
||||
public constructor(private _config: ExtensionConfig, private _unid: string) {
|
||||
this._ext = new CommandServiceImpl(ElectronIPC, `ext-${this._unid}`);
|
||||
/*
|
||||
this._ext.remoteCall('ext-ping')
|
||||
.then(reply => console.log(reply))
|
||||
.catch(err => console.log('ping failed', err));
|
||||
*/
|
||||
}
|
||||
public constructor(private _config: ExtensionConfig, private _unid: string) {}
|
||||
|
||||
public inspectorForObject(obj: any): GetInspectorResult | null {
|
||||
const inspectors = this.config.client.inspectors || [];
|
||||
|
@ -73,10 +56,6 @@ export class Extension {
|
|||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
public call(commandName: string, ...args: any[]): Promise<any> {
|
||||
return this._ext.remoteCall(commandName, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
@ -149,29 +128,15 @@ export interface GetInspectorResult {
|
|||
inspector: ExtensionInspector;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
export interface ExtensionManager {
|
||||
registerCommands(commandRegistry: CommandRegistryImpl);
|
||||
|
||||
addExtension(config: ExtensionConfig, unid: string);
|
||||
|
||||
removeExtension(unid: string);
|
||||
|
||||
getExtensions(): Extension[];
|
||||
|
||||
inspectorForObject(obj: any, defaultToJson: boolean): GetInspectorResult | null;
|
||||
}
|
||||
const { Connect, Disconnect } = SharedConstants.Commands.Extension;
|
||||
|
||||
// =============================================================================
|
||||
class EmulatorExtensionManager implements ExtensionManager {
|
||||
class EmulatorExtensionManager {
|
||||
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;
|
||||
this.extensions[unid] = new Extension(config, unid);
|
||||
}
|
||||
|
||||
public removeExtension(unid: string) {
|
||||
|
@ -198,7 +163,7 @@ class EmulatorExtensionManager implements ExtensionManager {
|
|||
if (!result && defaultToJson) {
|
||||
// Default to the JSON inspector
|
||||
// eslint-disable-next-line typescript/no-use-before-define
|
||||
const jsonExtension = ExtensionManager.findExtension('JSON');
|
||||
const jsonExtension = this.findExtension('JSON');
|
||||
if (jsonExtension) {
|
||||
result = {
|
||||
extension: jsonExtension,
|
||||
|
@ -209,17 +174,16 @@ class EmulatorExtensionManager implements ExtensionManager {
|
|||
return result;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
@Command(Connect)
|
||||
protected connectExtension(config: ExtensionConfig) {
|
||||
// eslint-disable-next-line typescript/no-use-before-define
|
||||
this.addExtension(config, config.location);
|
||||
}
|
||||
|
||||
commandRegistry.registerCommand(Disconnect, (location: string) => {
|
||||
// eslint-disable-next-line typescript/no-use-before-define
|
||||
ExtensionManager.removeExtension(location);
|
||||
});
|
||||
@Command(Disconnect)
|
||||
protected disconnectExtension(location: string) {
|
||||
// eslint-disable-next-line typescript/no-use-before-define
|
||||
this.removeExtension(location);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,8 +32,9 @@
|
|||
//
|
||||
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { navigate } from './hyperlinkHandler';
|
||||
import { HyperlinkHandler } from './hyperlinkHandler';
|
||||
|
||||
let mockParse;
|
||||
jest.mock('url', () => ({
|
||||
|
@ -41,23 +42,45 @@ jest.mock('url', () => ({
|
|||
return mockParse;
|
||||
},
|
||||
}));
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
const mockUniqueId = 'id1234';
|
||||
jest.mock('@bfemulator/sdk-shared', () => ({
|
||||
jest.mock('@bfemulator/sdk-shared/build/utils/misc', () => ({
|
||||
uniqueId: () => mockUniqueId,
|
||||
}));
|
||||
|
||||
let mockRemoteCallsMade;
|
||||
let mockRemoteCall;
|
||||
jest.mock('./platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
get remoteCall() {
|
||||
return mockRemoteCall;
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('hyperlinkHandler', () => {
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
});
|
||||
beforeEach(() => {
|
||||
mockParse = jest.fn(url => {
|
||||
if (url) {
|
||||
|
@ -69,7 +92,7 @@ describe('hyperlinkHandler', () => {
|
|||
});
|
||||
mockRemoteCallsMade = [];
|
||||
(window as any)._openExternal.mockClear();
|
||||
mockRemoteCall = jest.fn((commandName: string, ...args: any[]) => {
|
||||
commandService.remoteCall = jest.fn((commandName: string, ...args: any[]) => {
|
||||
mockRemoteCallsMade.push({ commandName, args });
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
|
@ -77,7 +100,7 @@ describe('hyperlinkHandler', () => {
|
|||
|
||||
it('should navigate to an emulated ouath url', async () => {
|
||||
const url = 'oauth://someoauthurl.com/auth&&&ending';
|
||||
await navigate(url);
|
||||
await HyperlinkHandler.navigate(url);
|
||||
|
||||
expect(mockRemoteCallsMade).toHaveLength(1);
|
||||
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.OAuth.SendTokenResponse);
|
||||
|
@ -86,7 +109,7 @@ describe('hyperlinkHandler', () => {
|
|||
|
||||
it('should navigate to an ouath url', async () => {
|
||||
const url = 'oauthlink://someoauthurl.com/auth&&&ending';
|
||||
await navigate(url);
|
||||
await HyperlinkHandler.navigate(url);
|
||||
|
||||
expect(mockRemoteCallsMade).toHaveLength(1);
|
||||
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.OAuth.CreateOAuthWindow);
|
||||
|
@ -95,7 +118,7 @@ describe('hyperlinkHandler', () => {
|
|||
|
||||
it('should open a data url', async () => {
|
||||
const url = 'data:image/png;base64;somedata';
|
||||
await navigate(url);
|
||||
await HyperlinkHandler.navigate(url);
|
||||
|
||||
expect(mockRemoteCallsMade).toHaveLength(1);
|
||||
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
|
||||
|
@ -105,7 +128,7 @@ describe('hyperlinkHandler', () => {
|
|||
|
||||
it('should open a normal url', async () => {
|
||||
const url = 'https://aka.ms/bot-framework-emulator';
|
||||
await navigate(url);
|
||||
await HyperlinkHandler.navigate(url);
|
||||
|
||||
expect(mockRemoteCallsMade).toHaveLength(1);
|
||||
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
|
||||
|
@ -119,7 +142,7 @@ describe('hyperlinkHandler', () => {
|
|||
throw new Error();
|
||||
});
|
||||
const url = '';
|
||||
await navigate(url);
|
||||
await HyperlinkHandler.navigate(url);
|
||||
|
||||
expect(mockRemoteCallsMade).toHaveLength(1);
|
||||
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
|
||||
|
|
|
@ -34,51 +34,52 @@
|
|||
import * as URL from 'url';
|
||||
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { uniqueId } from '@bfemulator/sdk-shared';
|
||||
import { CommandServiceImpl, CommandServiceInstance, uniqueId } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CommandServiceImpl } from './platform/commands/commandServiceImpl';
|
||||
const Electron = (window as any).require('electron');
|
||||
const { shell } = Electron;
|
||||
|
||||
export function navigate(url: string = '') {
|
||||
const { TrackEvent } = SharedConstants.Commands.Telemetry;
|
||||
try {
|
||||
const parsed = URL.parse(url) || { protocol: '' };
|
||||
if ((parsed.protocol || '').startsWith('oauth:')) {
|
||||
navigateEmulatedOAuthUrl(url.substring(8));
|
||||
} else if (parsed.protocol.startsWith('oauthlink:')) {
|
||||
navigateOAuthUrl(url.substring(12));
|
||||
} else {
|
||||
CommandServiceImpl.remoteCall(TrackEvent, 'app_openLink', { url }).catch(_e => void 0);
|
||||
// manually create and click a download link for data url's
|
||||
if (url.startsWith('data:')) {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = '';
|
||||
a.click();
|
||||
export class HyperlinkHandler {
|
||||
@CommandServiceInstance()
|
||||
private static commandService: CommandServiceImpl;
|
||||
|
||||
public static navigate(url: string = '') {
|
||||
const { TrackEvent } = SharedConstants.Commands.Telemetry;
|
||||
try {
|
||||
const parsed = URL.parse(url) || { protocol: '' };
|
||||
if ((parsed.protocol || '').startsWith('oauth:')) {
|
||||
this.navigateEmulatedOAuthUrl(url.substring(8));
|
||||
} else if (parsed.protocol.startsWith('oauthlink:')) {
|
||||
this.navigateOAuthUrl(url.substring(12));
|
||||
} else {
|
||||
shell.openExternal(url, { activate: true });
|
||||
this.commandService.remoteCall(TrackEvent, 'app_openLink', { url }).catch(_e => void 0);
|
||||
// manually create and click a download link for data url's
|
||||
if (url.startsWith('data:')) {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = '';
|
||||
a.click();
|
||||
} else {
|
||||
shell.openExternal(url, { activate: true });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.commandService.remoteCall(TrackEvent, 'app_openLink', { url }).catch(_e => void 0);
|
||||
shell.openExternal(url, { activate: true });
|
||||
}
|
||||
} catch (e) {
|
||||
CommandServiceImpl.remoteCall(TrackEvent, 'app_openLink', { url }).catch(_e => void 0);
|
||||
shell.openExternal(url, { activate: true });
|
||||
}
|
||||
|
||||
private static navigateEmulatedOAuthUrl(oauthParam: string) {
|
||||
const { Commands } = SharedConstants;
|
||||
const parts = oauthParam.split('&&&');
|
||||
this.commandService
|
||||
.remoteCall(Commands.OAuth.SendTokenResponse, parts[0], parts[1], 'emulatedToken_' + uniqueId())
|
||||
.catch();
|
||||
}
|
||||
|
||||
private static navigateOAuthUrl(oauthParam: string) {
|
||||
const { Commands } = SharedConstants;
|
||||
const parts = oauthParam.split('&&&');
|
||||
this.commandService.remoteCall(Commands.OAuth.CreateOAuthWindow, parts[0], parts[1]).catch();
|
||||
}
|
||||
}
|
||||
|
||||
function navigateEmulatedOAuthUrl(oauthParam: string) {
|
||||
const { Commands } = SharedConstants;
|
||||
const parts = oauthParam.split('&&&');
|
||||
CommandServiceImpl.remoteCall(
|
||||
Commands.OAuth.SendTokenResponse,
|
||||
parts[0],
|
||||
parts[1],
|
||||
'emulatedToken_' + uniqueId()
|
||||
).catch();
|
||||
}
|
||||
|
||||
function navigateOAuthUrl(oauthParam: string) {
|
||||
const { Commands } = SharedConstants;
|
||||
const parts = oauthParam.split('&&&');
|
||||
CommandServiceImpl.remoteCall(Commands.OAuth.CreateOAuthWindow, parts[0], parts[1]).catch();
|
||||
}
|
||||
|
|
|
@ -34,22 +34,16 @@
|
|||
import { Provider } from 'react-redux';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import * as React from 'react';
|
||||
import { newNotification, SharedConstants } from '@bfemulator/app-shared';
|
||||
import './commands';
|
||||
|
||||
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 { showWelcomePage } from './data/editorHelpers';
|
||||
import { CommandRegistry, registerAllCommands } from './commands';
|
||||
import { beginAdd } from './data/action/notificationActions';
|
||||
import { globalHandlers } from './utils/eventHandlers';
|
||||
import './ui/styles/globals.scss';
|
||||
|
||||
interceptError();
|
||||
interceptHyperlink();
|
||||
registerAllCommands(CommandRegistry);
|
||||
|
||||
// Start rendering the UI
|
||||
ReactDOM.render(
|
||||
|
@ -57,21 +51,6 @@ ReactDOM.render(
|
|||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// Tell the main process we're loaded
|
||||
CommandServiceImpl.remoteCall(SharedConstants.Commands.ClientInit.Loaded)
|
||||
.then(async () => {
|
||||
showWelcomePage();
|
||||
// do actions on main side that might open a document, so that they will be active over the welcome screen
|
||||
await CommandServiceImpl.remoteCall(SharedConstants.Commands.ClientInit.PostWelcomeScreen);
|
||||
window.addEventListener('keydown', globalHandlers, true);
|
||||
})
|
||||
.catch(err => {
|
||||
const errMsg = `Error occurred while client was loading: ${err}`;
|
||||
const notification = newNotification(errMsg);
|
||||
store.dispatch(beginAdd(notification));
|
||||
window.removeEventListener('keydown', globalHandlers, true);
|
||||
});
|
||||
|
||||
if (module.hasOwnProperty('hot')) {
|
||||
(module as any).hot.accept();
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
import { navigate } from './hyperlinkHandler';
|
||||
import { HyperlinkHandler } from './hyperlinkHandler';
|
||||
|
||||
export default function interceptHyperlink() {
|
||||
const interceptClickEvent = (e: Event) => {
|
||||
|
@ -40,7 +40,7 @@ export default function interceptHyperlink() {
|
|||
while (target) {
|
||||
if (target.href) {
|
||||
e.preventDefault();
|
||||
navigate(target.href);
|
||||
HyperlinkHandler.navigate(target.href);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,6 @@ export default function interceptHyperlink() {
|
|||
|
||||
// Monkey patch window.open
|
||||
window.open = (url: string): any => {
|
||||
navigate(url);
|
||||
HyperlinkHandler.navigate(url);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
//
|
||||
// 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 {
|
||||
CommandHandler,
|
||||
CommandService,
|
||||
CommandServiceImpl as InternalSharedService,
|
||||
Disposable,
|
||||
DisposableImpl,
|
||||
} from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CommandRegistry } from '../../commands';
|
||||
import { ElectronIPC } from '../../ipc';
|
||||
|
||||
class CServiceImpl extends DisposableImpl implements CommandService {
|
||||
private readonly _service: InternalSharedService;
|
||||
|
||||
public get registry() {
|
||||
return this._service.registry;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._service = new InternalSharedService(ElectronIPC, 'command-service', CommandRegistry);
|
||||
super.toDispose(this._service);
|
||||
}
|
||||
|
||||
public call(commandName: string, ...args: any[]): Promise<any> {
|
||||
return this._service.call(commandName, ...args);
|
||||
}
|
||||
|
||||
public remoteCall(commandName: string, ...args: any[]): Promise<any> {
|
||||
return this._service.remoteCall(commandName, ...args);
|
||||
}
|
||||
|
||||
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();
|
|
@ -32,31 +32,29 @@
|
|||
//
|
||||
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { CommandRegistryImpl, LogEntry } from '@bfemulator/sdk-shared';
|
||||
import { Command, LogEntry } from '@bfemulator/sdk-shared';
|
||||
|
||||
import * as ChatActions from '../../data/action/chatActions';
|
||||
import * as chatHelpers from '../../data/chatHelpers';
|
||||
import { store } from '../../data/store';
|
||||
|
||||
export class LogService {
|
||||
public static logToChat(conversationId: string, entry: LogEntry): void {
|
||||
class LogService {
|
||||
public logToChat(conversationId: string, entry: LogEntry): void {
|
||||
const documentId = chatHelpers.documentIdForConversation(conversationId);
|
||||
if (documentId) {
|
||||
// eslint-disable-next-line typescript/no-use-before-define
|
||||
LogService.logToDocument(documentId, entry);
|
||||
this.logToDocument(documentId, entry);
|
||||
}
|
||||
}
|
||||
|
||||
public static logToDocument(documentId: string, entry: LogEntry): void {
|
||||
public logToDocument(documentId: string, entry: LogEntry): void {
|
||||
store.dispatch(ChatActions.appendToLog(documentId, entry));
|
||||
}
|
||||
|
||||
@Command(SharedConstants.Commands.Emulator.AppendToLog)
|
||||
protected appendToLog(conversationId: string, entry: LogEntry): any {
|
||||
this.logToChat(conversationId, entry);
|
||||
}
|
||||
}
|
||||
|
||||
export function registerCommands(commandRegistry: CommandRegistryImpl) {
|
||||
commandRegistry.registerCommand(
|
||||
SharedConstants.Commands.Emulator.AppendToLog,
|
||||
(conversationId: string, entry: LogEntry): any => {
|
||||
LogService.logToChat(conversationId, entry);
|
||||
}
|
||||
);
|
||||
}
|
||||
export const logService = new LogService();
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
//
|
||||
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { CommandRegistryImpl, DisposableImpl } from '@bfemulator/sdk-shared';
|
||||
import { Command } from '@bfemulator/sdk-shared';
|
||||
|
||||
export interface EmulatorSettings {
|
||||
url?: string;
|
||||
|
@ -50,6 +50,7 @@ class EmulatorSettingsImpl implements EmulatorSettings {
|
|||
}
|
||||
return this._url;
|
||||
}
|
||||
|
||||
set url(value: string) {
|
||||
this._url = value;
|
||||
}
|
||||
|
@ -75,8 +76,8 @@ class EmulatorSettingsImpl implements EmulatorSettings {
|
|||
}
|
||||
}
|
||||
|
||||
class EmulatorSettingsService extends DisposableImpl {
|
||||
private readonly _emulator: EmulatorSettingsImpl;
|
||||
class EmulatorSettingsService {
|
||||
private _emulator: EmulatorSettingsImpl;
|
||||
|
||||
get emulator(): EmulatorSettingsImpl {
|
||||
return this._emulator;
|
||||
|
@ -87,19 +88,14 @@ class EmulatorSettingsService extends DisposableImpl {
|
|||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._emulator = new EmulatorSettingsImpl();
|
||||
}
|
||||
|
||||
@Command(SharedConstants.Commands.Settings.ReceiveGlobalSettings)
|
||||
protected receiveGlobalSettings(settings: { url: string; cwd: string }): any {
|
||||
this.emulator.url = (settings.url || '').replace('[::]', 'localhost');
|
||||
this.emulator.cwd = (settings.cwd || '').replace(/\\/g, '/');
|
||||
}
|
||||
}
|
||||
|
||||
export const SettingsService = new EmulatorSettingsService();
|
||||
|
||||
export function registerCommands(commandRegistry: CommandRegistryImpl) {
|
||||
commandRegistry.registerCommand(
|
||||
SharedConstants.Commands.Settings.ReceiveGlobalSettings,
|
||||
(settings: { url: string; cwd: string }): any => {
|
||||
SettingsService.emulator.url = (settings.url || '').replace('[::]', 'localhost');
|
||||
SettingsService.emulator.cwd = (settings.cwd || '').replace(/\\/g, '/');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -33,13 +33,12 @@
|
|||
|
||||
import { mount } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
|
||||
import { ActiveBotHelper } from '../../helpers/activeBotHelper';
|
||||
|
||||
import { BotCreationDialog, BotCreationDialogState } from './botCreationDialog';
|
||||
|
||||
jest.mock('./botCreationDialog.scss', () => ({}));
|
||||
jest.mock('../index', () => null);
|
||||
jest.mock('../../../utils', () => ({
|
||||
generateBotSecret: () => {
|
||||
|
@ -47,6 +46,31 @@ jest.mock('../../../utils', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../helpers/activeBotHelper', () => ({
|
||||
ActiveBotHelper: {
|
||||
confirmAndCreateBot: async () => true,
|
||||
|
@ -54,6 +78,13 @@ jest.mock('../../helpers/activeBotHelper', () => ({
|
|||
}));
|
||||
|
||||
describe('BotCreationDialog tests', () => {
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
});
|
||||
|
||||
let testWrapper;
|
||||
beforeEach(() => {
|
||||
testWrapper = mount(<BotCreationDialog />);
|
||||
|
@ -174,7 +205,7 @@ describe('BotCreationDialog tests', () => {
|
|||
|
||||
it('should save and connect', async () => {
|
||||
const instance = testWrapper.instance();
|
||||
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue('some/path');
|
||||
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue('some/path');
|
||||
const confirmAndCreateSpy = jest.spyOn(ActiveBotHelper, 'confirmAndCreateBot').mockResolvedValue(true);
|
||||
await instance.onSaveAndConnect();
|
||||
expect(remoteCallSpy).toHaveBeenCalledWith('shell:showExplorer-save-dialog', {
|
||||
|
|
|
@ -47,10 +47,10 @@ import { EndpointService } from 'botframework-config/lib/models';
|
|||
import { IEndpointService, ServiceTypes } from 'botframework-config/lib/schema';
|
||||
import { ChangeEvent } from 'react';
|
||||
import * as React from 'react';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { beginAdd } from '../../../data/action/notificationActions';
|
||||
import { store } from '../../../data/store';
|
||||
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
|
||||
import { generateBotSecret } from '../../../utils';
|
||||
import { ActiveBotHelper } from '../../helpers/activeBotHelper';
|
||||
import { DialogService } from '../service';
|
||||
|
@ -67,6 +67,9 @@ export interface BotCreationDialogState {
|
|||
}
|
||||
|
||||
export class BotCreationDialog extends React.Component<{}, BotCreationDialogState> {
|
||||
@CommandServiceInstance()
|
||||
public commandService: CommandServiceImpl;
|
||||
|
||||
public constructor(props: {}, context: BotCreationDialogState) {
|
||||
super(props, context);
|
||||
|
||||
|
@ -321,7 +324,7 @@ export class BotCreationDialog extends React.Component<{}, BotCreationDialogStat
|
|||
private showBotSaveDialog = async (): Promise<any> => {
|
||||
const { Commands } = SharedConstants;
|
||||
// get a safe bot file name
|
||||
const botFileName = await CommandServiceImpl.remoteCall(Commands.File.SanitizeString, this.state.bot.name);
|
||||
const botFileName = await this.commandService.remoteCall(Commands.File.SanitizeString, this.state.bot.name);
|
||||
// TODO - Localization
|
||||
const dialogOptions = {
|
||||
filters: [
|
||||
|
@ -336,7 +339,7 @@ export class BotCreationDialog extends React.Component<{}, BotCreationDialogStat
|
|||
buttonLabel: 'Save',
|
||||
};
|
||||
|
||||
return CommandServiceImpl.remoteCall(Commands.Electron.ShowSaveDialog, dialogOptions);
|
||||
return this.commandService.remoteCall(Commands.Electron.ShowSaveDialog, dialogOptions);
|
||||
};
|
||||
|
||||
/** Checks the endpoint to see if it has the correct route syntax at the end (/api/messages) */
|
||||
|
|
|
@ -38,6 +38,7 @@ import { mount } from 'enzyme';
|
|||
import { combineReducers, createStore } from 'redux';
|
||||
import { BotConfigWithPathImpl } from '@bfemulator/sdk-shared';
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { bot } from '../../../data/reducer/bot';
|
||||
import { setActiveBot } from '../../../data/action/botActions';
|
||||
|
@ -73,7 +74,6 @@ const mockWindow = {
|
|||
},
|
||||
};
|
||||
|
||||
jest.mock('./botSettingsEditor.scss', () => ({}));
|
||||
jest.mock('../../../data/store', () => ({
|
||||
get store() {
|
||||
return mockStore;
|
||||
|
@ -87,26 +87,33 @@ jest.mock('../service', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
const mockRemoteCommandsCalled = [];
|
||||
const mockSharedConstants = SharedConstants; // thanks Jest!
|
||||
jest.mock('../../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
remoteCall: async (commandName: string, ...args: any[]) => {
|
||||
mockRemoteCommandsCalled.push({ commandName, args: args });
|
||||
switch (commandName) {
|
||||
case mockSharedConstants.Commands.File.SanitizeString:
|
||||
return args[0];
|
||||
|
||||
case mockSharedConstants.Commands.Electron.ShowSaveDialog:
|
||||
return '/test/path';
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
},
|
||||
},
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
const mockRemoteCommandsCalled = [];
|
||||
|
||||
jest.mock('../../../utils', () => ({
|
||||
generateBotSecret: () => {
|
||||
return Math.random() + '';
|
||||
|
@ -114,6 +121,25 @@ jest.mock('../../../utils', () => ({
|
|||
}));
|
||||
|
||||
describe('The BotSettingsEditor dialog should', () => {
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
commandService.remoteCall = async (commandName: string, ...args: any[]) => {
|
||||
mockRemoteCommandsCalled.push({ commandName, args: args });
|
||||
switch (commandName) {
|
||||
case SharedConstants.Commands.File.SanitizeString:
|
||||
return args[0];
|
||||
|
||||
case SharedConstants.Commands.Electron.ShowSaveDialog:
|
||||
return '/test/path';
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
};
|
||||
});
|
||||
let parent;
|
||||
let node;
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -46,9 +46,9 @@ import {
|
|||
import { IConnectedService, ServiceTypes } from 'botframework-config/lib/schema';
|
||||
import { ChangeEvent } from 'react';
|
||||
import * as React from 'react';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { getBotInfoByPath } from '../../../data/botHelpers';
|
||||
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
|
||||
import { generateBotSecret } from '../../../utils';
|
||||
import { ActiveBotHelper } from '../../helpers/activeBotHelper';
|
||||
|
||||
|
@ -70,6 +70,9 @@ export interface BotSettingsEditorState extends BotConfigWithPath {
|
|||
}
|
||||
|
||||
export class BotSettingsEditor extends React.Component<BotSettingsEditorProps, BotSettingsEditorState> {
|
||||
@CommandServiceInstance()
|
||||
private commandService: CommandServiceImpl;
|
||||
|
||||
private _generatedSecret: string;
|
||||
|
||||
constructor(props: BotSettingsEditorProps, context: BotSettingsEditorState) {
|
||||
|
@ -225,14 +228,14 @@ export class BotSettingsEditor extends React.Component<BotSettingsEditorProps, B
|
|||
path: newPath,
|
||||
secret: this.state.secret,
|
||||
};
|
||||
await CommandServiceImpl.remoteCall(PatchBotList, SharedConstants.TEMP_BOT_IN_MEMORY_PATH, botInfo);
|
||||
await CommandServiceImpl.remoteCall(Save, bot);
|
||||
await this.commandService.remoteCall(PatchBotList, SharedConstants.TEMP_BOT_IN_MEMORY_PATH, botInfo);
|
||||
await this.commandService.remoteCall(Save, bot);
|
||||
// need to set the new bot as active now that it is no longer a placeholder bot in memory
|
||||
await ActiveBotHelper.setActiveBot(bot);
|
||||
this.setState({ ...bot });
|
||||
|
||||
if (connectArg && endpointService) {
|
||||
await CommandServiceImpl.call(SharedConstants.Commands.Emulator.NewLiveChat, endpointService);
|
||||
await this.commandService.call(SharedConstants.Commands.Emulator.NewLiveChat, endpointService);
|
||||
}
|
||||
this.props.cancel();
|
||||
};
|
||||
|
@ -243,11 +246,11 @@ export class BotSettingsEditor extends React.Component<BotSettingsEditorProps, B
|
|||
// write updated bot entry to bots.json so main side can pick up possible changes to secret
|
||||
const botInfo: BotInfo = getBotInfoByPath(bot.path) || {};
|
||||
botInfo.secret = this.state.secret;
|
||||
await CommandServiceImpl.remoteCall(PatchBotList, bot.path, botInfo);
|
||||
await this.commandService.remoteCall(PatchBotList, bot.path, botInfo);
|
||||
|
||||
// save bot
|
||||
try {
|
||||
await CommandServiceImpl.remoteCall(Save, bot);
|
||||
await this.commandService.remoteCall(Save, bot);
|
||||
} catch {
|
||||
const note = newNotification(
|
||||
'There was an error updating your bot settings. ' +
|
||||
|
@ -257,7 +260,7 @@ export class BotSettingsEditor extends React.Component<BotSettingsEditorProps, B
|
|||
this.props.sendNotification(note);
|
||||
return;
|
||||
}
|
||||
await CommandServiceImpl.remoteCall(PatchBotList, bot.path, botInfo);
|
||||
await this.commandService.remoteCall(PatchBotList, bot.path, botInfo);
|
||||
this.props.cancel();
|
||||
};
|
||||
|
||||
|
@ -265,7 +268,7 @@ export class BotSettingsEditor extends React.Component<BotSettingsEditorProps, B
|
|||
// get a safe bot file name
|
||||
// TODO - localization
|
||||
const { SanitizeString } = SharedConstants.Commands.File;
|
||||
const botFileName = await CommandServiceImpl.remoteCall(SanitizeString, this.state.name);
|
||||
const botFileName = await this.commandService.remoteCall(SanitizeString, this.state.name);
|
||||
const dialogOptions = {
|
||||
filters: [
|
||||
{
|
||||
|
@ -278,7 +281,7 @@ export class BotSettingsEditor extends React.Component<BotSettingsEditorProps, B
|
|||
title: 'Save as',
|
||||
buttonLabel: 'Save',
|
||||
};
|
||||
return CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.ShowSaveDialog, dialogOptions);
|
||||
return this.commandService.remoteCall(SharedConstants.Commands.Electron.ShowSaveDialog, dialogOptions);
|
||||
};
|
||||
|
||||
private onRevealSecretClick = (): void => {
|
||||
|
|
|
@ -36,8 +36,8 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { beginAdd } from '../../../data/action/notificationActions';
|
||||
import { RootState } from '../../../data/store';
|
||||
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
|
||||
import { DialogService } from '../service';
|
||||
import { executeCommand } from '../../../data/action/commandAction';
|
||||
|
||||
import { BotSettingsEditor, BotSettingsEditorProps } from './botSettingsEditor';
|
||||
|
||||
|
@ -53,7 +53,7 @@ const mapDispatchToProps = dispatch => ({
|
|||
cancel: () => DialogService.hideDialog(0),
|
||||
sendNotification: notification => dispatch(beginAdd(notification)),
|
||||
onAnchorClick: (url: string) => {
|
||||
CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.OpenExternal, url).catch();
|
||||
dispatch(executeCommand(true, SharedConstants.Commands.Electron.OpenExternal, null, url));
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -34,18 +34,42 @@ import * as React from 'react';
|
|||
import { Provider } from 'react-redux';
|
||||
import { mount } from 'enzyme';
|
||||
import { combineReducers, createStore } from 'redux';
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
|
||||
import { bot } from '../../../data/reducer/bot';
|
||||
import { resources } from '../../../data/reducer/resourcesReducer';
|
||||
import { loadBotInfos, setActiveBot } from '../../../data/action/botActions';
|
||||
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
|
||||
import { executeCommand } from '../../../data/action/commandAction';
|
||||
|
||||
import { ResourcesSettings } from './resourcesSettings';
|
||||
import { ResourcesSettingsContainer } from './resourcesSettingsContainer';
|
||||
|
||||
const mockStore = createStore(combineReducers({ resources, bot }));
|
||||
jest.mock('./resourcesSettings.scss', () => ({}));
|
||||
jest.mock('../dialogStyles.scss', () => ({}));
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../service', () => ({
|
||||
DialogService: {
|
||||
|
@ -54,17 +78,6 @@ jest.mock('../service', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
remoteCall: async (commandName: string, ...args: any[]) => {
|
||||
//
|
||||
},
|
||||
call: async (commandName: string, ...args: any[]) => {
|
||||
//
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../data/store', () => ({
|
||||
RootState: () => ({}),
|
||||
get store() {
|
||||
|
@ -79,6 +92,8 @@ jest.mock('../../../data/botHelpers', () => ({
|
|||
describe('The ResourcesSettings component should', () => {
|
||||
let parent;
|
||||
let node;
|
||||
let dispatchSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockBot = JSON.parse(`{
|
||||
"name": "TestBot",
|
||||
|
@ -96,6 +111,7 @@ describe('The ResourcesSettings component should', () => {
|
|||
|
||||
mockStore.dispatch(loadBotInfos([mockBot]));
|
||||
mockStore.dispatch(setActiveBot(mockBot));
|
||||
dispatchSpy = jest.spyOn(mockStore, 'dispatch');
|
||||
parent = mount(
|
||||
<Provider store={mockStore}>
|
||||
<ResourcesSettingsContainer label="test" progress={50} />
|
||||
|
@ -137,10 +153,11 @@ describe('The ResourcesSettings component should', () => {
|
|||
|
||||
it('should open the browse dialog when the browse anchor is clicked', async () => {
|
||||
const instance = node.instance();
|
||||
const spy = jest.spyOn(CommandServiceImpl, 'remoteCall');
|
||||
await instance.onBrowseClick({
|
||||
currentTarget: { getAttribute: () => 'attr' },
|
||||
});
|
||||
expect(spy).toHaveBeenCalled();
|
||||
} as any);
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
executeCommand(true, SharedConstants.Commands.Electron.ShowOpenDialog, null, { properties: ['openDirectory'] })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,8 +35,8 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { getBotInfoByPath } from '../../../data/botHelpers';
|
||||
import { RootState } from '../../../data/store';
|
||||
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
|
||||
import { DialogService } from '../service';
|
||||
import { executeCommand } from '../../../data/action/commandAction';
|
||||
|
||||
import { ResourcesSettings, ResourcesSettingsProps } from './resourcesSettings';
|
||||
|
||||
|
@ -47,10 +47,12 @@ const mapStateToProps = (state: RootState, ownProps: ResourcesSettingsProps) =>
|
|||
return { transcriptsPath, chatsPath, path, ...ownProps };
|
||||
};
|
||||
|
||||
const mapDispatchToProps = _dispatch => ({
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
save: (settings: Partial<BotInfo>) => DialogService.hideDialog(settings),
|
||||
showOpenDialog: () =>
|
||||
CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.ShowOpenDialog, { properties: ['openDirectory'] }),
|
||||
dispatch(
|
||||
executeCommand(true, SharedConstants.Commands.Electron.ShowOpenDialog, null, { properties: ['openDirectory'] })
|
||||
),
|
||||
cancel: () => DialogService.hideDialog(0),
|
||||
});
|
||||
export const ResourcesSettingsContainer = connect(
|
||||
|
|
|
@ -46,15 +46,6 @@ import { SecretPromptDialog } from './secretPromptDialog';
|
|||
const mockStore = createStore(combineReducers({ bot }));
|
||||
const mockBot = BotConfigWithPathImpl.fromJSON({});
|
||||
|
||||
jest.mock('../../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
remoteCall: async () => true,
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../dialogStyles.scss', () => ({}));
|
||||
|
||||
jest.mock('./secretPromptDialog.scss', () => ({}));
|
||||
jest.mock('../../../data/store', () => ({
|
||||
get store() {
|
||||
return mockStore;
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
import { mount } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
import { combineReducers, createStore } from 'redux';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import * as EditorActions from '../../../data/action/editorActions';
|
||||
import {
|
||||
|
@ -47,39 +48,52 @@ import { framework } from '../../../data/reducer/frameworkSettingsReducer';
|
|||
import { AppSettingsEditor } from './appSettingsEditor';
|
||||
import { AppSettingsEditorContainer } from './appSettingsEditorConainer';
|
||||
|
||||
jest.mock('./appSettingsEditor.scss', () => ({}));
|
||||
jest.mock('../../layout/genericDocument.scss', () => ({}));
|
||||
jest.mock(
|
||||
'../../dialogs/',
|
||||
() =>
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return {};
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
const mockCallsMade = [];
|
||||
const mockRemoteCallsMade = [];
|
||||
jest.mock('../../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
call: (commandName, ...args) => {
|
||||
mockCallsMade.push({ commandName, args });
|
||||
return Promise.resolve();
|
||||
},
|
||||
remoteCall: (commandName, ...args) => {
|
||||
mockRemoteCallsMade.push({ commandName, args });
|
||||
return Promise.resolve('hai!');
|
||||
},
|
||||
},
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
describe('The AppSettingsEditorContainer', () => {
|
||||
let instance: AppSettingsEditor;
|
||||
let node;
|
||||
let mockDispatch;
|
||||
let mockStore;
|
||||
let commandService: CommandServiceImpl;
|
||||
const mockCallsMade = [];
|
||||
const mockRemoteCallsMade = [];
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
commandService.call = (commandName, ...args) => {
|
||||
mockCallsMade.push({ commandName, args });
|
||||
return Promise.resolve(true) as any;
|
||||
};
|
||||
commandService.remoteCall = (commandName, ...args) => {
|
||||
mockRemoteCallsMade.push({ commandName, args });
|
||||
return Promise.resolve('hai!') as any;
|
||||
};
|
||||
});
|
||||
beforeEach(() => {
|
||||
mockStore = createStore(combineReducers({ framework }));
|
||||
mockStore.dispatch(
|
||||
|
@ -130,23 +144,19 @@ describe('The AppSettingsEditorContainer', () => {
|
|||
});
|
||||
|
||||
it('should call a remote command to open a browse window when "onClickBrowse" is called', async () => {
|
||||
const dispatchSpy = jest.spyOn(mockStore, 'dispatch');
|
||||
await (instance as any).onClickBrowse();
|
||||
expect(mockRemoteCallsMade[0]).toEqual({
|
||||
args: [
|
||||
{
|
||||
buttonLabel: 'Select ngrok',
|
||||
properties: ['openFile'],
|
||||
title: 'Browse for ngrok',
|
||||
},
|
||||
],
|
||||
commandName: 'shell:showExplorer-open-dialog',
|
||||
const dispatchSpy = jest.spyOn(mockStore, 'dispatch').mockImplementation(action => {
|
||||
if (action.payload.resolver) {
|
||||
action.payload.resolver('some/path');
|
||||
}
|
||||
});
|
||||
await (instance as any).onClickBrowse();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
expect(dispatchSpy).toHaveBeenLastCalledWith({
|
||||
payload: { dirty: true, documentId: undefined },
|
||||
type: 'EDITOR/SET_DIRTY_FLAG',
|
||||
});
|
||||
|
||||
expect(instance.state.ngrokPath).toBe('some/path');
|
||||
});
|
||||
|
||||
it('should discard the changes when "discardChanges" is called', () => {
|
||||
|
|
|
@ -93,9 +93,22 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
|
|||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { state } = this;
|
||||
const {
|
||||
ngrokPath = '',
|
||||
useCustomId = false,
|
||||
bypassNgrokLocalhost = true,
|
||||
runNgrokAtStartup = false,
|
||||
localhost = '',
|
||||
locale = '',
|
||||
use10Tokens = false,
|
||||
useCodeValidation = false,
|
||||
userGUID = '',
|
||||
autoUpdate = false,
|
||||
usePrereleases = false,
|
||||
} = this.state;
|
||||
|
||||
const inputProps = {
|
||||
disabled: !state.useCustomId,
|
||||
disabled: !useCustomId,
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -123,7 +136,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
|
|||
className={styles.appSettingsInput}
|
||||
inputContainerClassName={styles.inputContainer}
|
||||
readOnly={false}
|
||||
value={state.ngrokPath}
|
||||
value={ngrokPath}
|
||||
onChange={this.onInputChange}
|
||||
name="ngrokPath"
|
||||
label={'Path to ngrok'}
|
||||
|
@ -132,7 +145,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
|
|||
</Row>
|
||||
<Checkbox
|
||||
className={styles.checkboxOverrides}
|
||||
checked={state.bypassNgrokLocalhost}
|
||||
checked={bypassNgrokLocalhost}
|
||||
onChange={this.onChangeCheckBox}
|
||||
id="ngrok-bypass"
|
||||
label="Bypass ngrok for local addresses"
|
||||
|
@ -140,7 +153,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
|
|||
/>
|
||||
<Checkbox
|
||||
className={styles.checkboxOverrides}
|
||||
checked={state.runNgrokAtStartup}
|
||||
checked={runNgrokAtStartup}
|
||||
onChange={this.onChangeCheckBox}
|
||||
id="ngrok-startup"
|
||||
label="Run ngrok when the Emulator starts up"
|
||||
|
@ -151,7 +164,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
|
|||
className={styles.appSettingsInput}
|
||||
inputContainerClassName={styles.inputContainer}
|
||||
readOnly={false}
|
||||
value={state.localhost}
|
||||
value={localhost}
|
||||
onChange={this.onInputChange}
|
||||
name="localhost"
|
||||
label="localhost override"
|
||||
|
@ -162,7 +175,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
|
|||
className={styles.appSettingsInput}
|
||||
inputContainerClassName={styles.inputContainer}
|
||||
readOnly={false}
|
||||
value={state.locale}
|
||||
value={locale}
|
||||
name="locale"
|
||||
onChange={this.onInputChange}
|
||||
label="Locale"
|
||||
|
@ -173,7 +186,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
|
|||
<SmallHeader>User settings</SmallHeader>
|
||||
<Checkbox
|
||||
className={styles.checkboxOverrides}
|
||||
checked={state.use10Tokens}
|
||||
checked={use10Tokens}
|
||||
onChange={this.onChangeCheckBox}
|
||||
id="auth-token-version"
|
||||
label="Use version 1.0 authentication tokens"
|
||||
|
@ -181,7 +194,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
|
|||
/>
|
||||
<Checkbox
|
||||
className={styles.checkboxOverrides}
|
||||
checked={state.useCodeValidation}
|
||||
checked={useCodeValidation}
|
||||
onChange={this.onChangeCheckBox}
|
||||
id="use-validation-code"
|
||||
label="Use a sign-in verification code for OAuthCards"
|
||||
|
@ -189,7 +202,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
|
|||
/>
|
||||
<Checkbox
|
||||
className={styles.checkboxOverrides}
|
||||
checked={state.useCustomId}
|
||||
checked={useCustomId}
|
||||
onChange={this.onChangeCheckBox}
|
||||
id="use-custom-id"
|
||||
label="Use your own user ID to communicate with the bot"
|
||||
|
@ -200,29 +213,29 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
|
|||
<TextField
|
||||
{...inputProps}
|
||||
label="User ID"
|
||||
placeholder={state.useCustomId ? '' : 'There is no ID configured'}
|
||||
placeholder={useCustomId ? '' : 'There is no ID configured'}
|
||||
className={styles.appSettingsInput}
|
||||
inputContainerClassName={styles.inputContainer}
|
||||
readOnly={false}
|
||||
value={state.userGUID}
|
||||
value={userGUID}
|
||||
name="userGUID"
|
||||
onChange={this.onInputChange}
|
||||
required={state.useCustomId}
|
||||
errorMessage={state.userGUID ? '' : 'Enter a User ID'}
|
||||
required={useCustomId}
|
||||
errorMessage={userGUID ? '' : 'Enter a User ID'}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
<SmallHeader>Application Updates</SmallHeader>
|
||||
<Checkbox
|
||||
className={styles.checkboxOverrides}
|
||||
checked={state.autoUpdate}
|
||||
checked={autoUpdate}
|
||||
onChange={this.onChangeCheckBox}
|
||||
label="Automatically download and install updates"
|
||||
name="autoUpdate"
|
||||
/>
|
||||
<Checkbox
|
||||
className={styles.checkboxOverrides}
|
||||
checked={state.usePrereleases}
|
||||
checked={usePrereleases}
|
||||
onChange={this.onChangeCheckBox}
|
||||
label="Use pre-release versions"
|
||||
name="usePrereleases"
|
||||
|
|
|
@ -39,9 +39,9 @@ import * as EditorActions from '../../../data/action/editorActions';
|
|||
import { getFrameworkSettings } from '../../../data/action/frameworkSettingsActions';
|
||||
import { getTabGroupForDocument } from '../../../data/editorHelpers';
|
||||
import { RootState } from '../../../data/store';
|
||||
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
|
||||
import { debounce } from '../../../utils';
|
||||
import { saveFrameworkSettings } from '../../../data/action/frameworkSettingsActions';
|
||||
import { executeCommand } from '../../../data/action/commandAction';
|
||||
|
||||
import { AppSettingsEditor, AppSettingsEditorProps } from './appSettingsEditor';
|
||||
|
||||
|
@ -59,7 +59,9 @@ const mapDispatchToProps = (dispatch: (action: Action) => void, ownProps: AppSet
|
|||
buttonLabel: 'Select ngrok',
|
||||
properties: ['openFile'],
|
||||
};
|
||||
return CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.ShowOpenDialog, dialogOptions);
|
||||
return new Promise(resolve => {
|
||||
dispatch(executeCommand(true, SharedConstants.Commands.Electron.ShowOpenDialog, resolve, dialogOptions));
|
||||
});
|
||||
},
|
||||
getFrameworkSettings: () => dispatch(getFrameworkSettings()),
|
||||
saveFrameworkSettings: (framework: FrameworkSettings) => dispatch(saveFrameworkSettings(framework)),
|
||||
|
|
|
@ -35,14 +35,14 @@ import * as React from 'react';
|
|||
import { Provider } from 'react-redux';
|
||||
import { createStore } from 'redux';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { DebugMode, newNotification, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { DebugMode, SharedConstants } from '@bfemulator/app-shared';
|
||||
import base64Url from 'base64url';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { disable, enable } from '../../../data/action/presentationActions';
|
||||
import { clearLog, newConversation, setInspectorObjects } from '../../../data/action/chatActions';
|
||||
import { updateDocument } from '../../../data/action/editorActions';
|
||||
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
|
||||
import { beginAdd } from '../../../data/action/notificationActions';
|
||||
import { executeCommand } from '../../../data/action/commandAction';
|
||||
|
||||
import { Emulator, RestartConversationOptions } from './emulator';
|
||||
import { EmulatorContainer } from './emulatorContainer';
|
||||
|
@ -51,28 +51,6 @@ const { encode } = base64Url;
|
|||
|
||||
let mockCallsMade, mockRemoteCallsMade;
|
||||
const mockSharedConstants = SharedConstants;
|
||||
jest.mock('../../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
call: (commandName, ...args) => {
|
||||
mockCallsMade.push({ commandName, args });
|
||||
return Promise.resolve();
|
||||
},
|
||||
remoteCall: (commandName, ...args) => {
|
||||
mockRemoteCallsMade.push({ commandName, args });
|
||||
if (commandName === mockSharedConstants.Commands.Emulator.NewTranscript) {
|
||||
return Promise.resolve({ conversationId: 'someConvoId' });
|
||||
}
|
||||
if (commandName === mockSharedConstants.Commands.Emulator.FeedTranscriptFromDisk) {
|
||||
return Promise.resolve({ meta: 'some file info' });
|
||||
}
|
||||
if (commandName === mockSharedConstants.Commands.Settings.LoadAppSettings) {
|
||||
return Promise.resolve({ framework: { userGUID: '' } });
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('./chatPanel/chatPanel', () => {
|
||||
return jest.fn(() => <div />);
|
||||
});
|
||||
|
@ -89,7 +67,7 @@ jest.mock('./parts', () => {
|
|||
jest.mock('./toolbar/toolbar', () => {
|
||||
return jest.fn(() => <div />);
|
||||
});
|
||||
jest.mock('@bfemulator/sdk-shared', () => ({
|
||||
jest.mock('@bfemulator/sdk-shared/build/utils/misc', () => ({
|
||||
uniqueId: () => 'someUniqueId',
|
||||
uniqueIdv4: () => 'newUserId',
|
||||
}));
|
||||
|
@ -98,6 +76,31 @@ jest.mock('botframework-webchat', () => ({
|
|||
createDirectLine: args => ({ ...args }),
|
||||
}));
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
describe('<EmulatorContainer/>', () => {
|
||||
let wrapper;
|
||||
let node;
|
||||
|
@ -106,6 +109,30 @@ describe('<EmulatorContainer/>', () => {
|
|||
let mockStoreState;
|
||||
const mockUnsubscribe = jest.fn(() => null);
|
||||
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
commandService.call = (commandName, ...args) => {
|
||||
mockCallsMade.push({ commandName, args });
|
||||
return Promise.resolve() as any;
|
||||
};
|
||||
commandService.remoteCall = (commandName, ...args) => {
|
||||
mockRemoteCallsMade.push({ commandName, args });
|
||||
if (commandName === mockSharedConstants.Commands.Emulator.NewTranscript) {
|
||||
return Promise.resolve({ conversationId: 'someConvoId' });
|
||||
}
|
||||
if (commandName === mockSharedConstants.Commands.Emulator.FeedTranscriptFromDisk) {
|
||||
return Promise.resolve({ meta: 'some file info' });
|
||||
}
|
||||
if (commandName === mockSharedConstants.Commands.Settings.LoadAppSettings) {
|
||||
return Promise.resolve({ framework: { userGUID: '' } });
|
||||
}
|
||||
return Promise.resolve() as any;
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockUnsubscribe.mockClear();
|
||||
mockCallsMade = [];
|
||||
|
@ -134,7 +161,11 @@ describe('<EmulatorContainer/>', () => {
|
|||
clientAwareSettings: { debugMode: DebugMode.Normal },
|
||||
};
|
||||
const mockStore = createStore((_state, _action) => mockStoreState);
|
||||
mockDispatch = jest.spyOn(mockStore, 'dispatch');
|
||||
mockDispatch = jest.spyOn(mockStore, 'dispatch').mockImplementation(action => {
|
||||
if (action && action.payload && action.payload.resolver) {
|
||||
action.payload.resolver();
|
||||
}
|
||||
});
|
||||
wrapper = mount(
|
||||
<Provider store={mockStore}>
|
||||
<EmulatorContainer documentId={'doc1'} url={'someUrl'} mode={'livechat'} conversationId={'convo1'} />
|
||||
|
@ -288,18 +319,10 @@ describe('<EmulatorContainer/>', () => {
|
|||
it('should export a transcript', async () => {
|
||||
await instance.onExportTranscriptClick();
|
||||
|
||||
expect(mockRemoteCallsMade).toHaveLength(3);
|
||||
expect(mockRemoteCallsMade[2].commandName).toBe(SharedConstants.Commands.Emulator.SaveTranscriptToFile);
|
||||
expect(mockRemoteCallsMade[2].args).toEqual([32, 'convo1']);
|
||||
});
|
||||
|
||||
it('should report a notification when exporting a transcript fails', async () => {
|
||||
jest.spyOn(CommandServiceImpl, 'remoteCall').mockRejectedValueOnce({ message: 'oh noes!' });
|
||||
await instance.onExportTranscriptClick();
|
||||
const notification = newNotification('oh noes!');
|
||||
notification.timestamp = jasmine.any(Number) as any;
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith(beginAdd(notification));
|
||||
expect(mockRemoteCallsMade).toHaveLength(2);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
executeCommand(true, SharedConstants.Commands.Emulator.SaveTranscriptToFile, null, 32, 'convo1')
|
||||
);
|
||||
});
|
||||
|
||||
it('should start a new conversation', async () => {
|
||||
|
@ -358,18 +381,13 @@ describe('<EmulatorContainer/>', () => {
|
|||
const mockStartNewConversation = jest.fn(async () => Promise.resolve(true));
|
||||
instance.startNewConversation = mockStartNewConversation;
|
||||
await instance.onStartOverClick();
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith(clearLog('doc1'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(clearLog('doc1', jasmine.any(Function)));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(setInspectorObjects('doc1', []));
|
||||
expect(mockRemoteCallsMade).toHaveLength(3);
|
||||
expect(mockRemoteCallsMade.some(cmd => cmd.commandName === SharedConstants.Commands.Telemetry.TrackEvent)).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
mockRemoteCallsMade.some(cmd => {
|
||||
return cmd.args[0] === 'conversation_restart' && cmd.args[1].userId === 'new';
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, 'conversation_restart', {
|
||||
userId: 'new',
|
||||
})
|
||||
).toBe(true);
|
||||
);
|
||||
expect(mockStartNewConversation).toHaveBeenCalledWith(undefined, true, true);
|
||||
});
|
||||
|
||||
|
@ -378,17 +396,13 @@ describe('<EmulatorContainer/>', () => {
|
|||
instance.startNewConversation = mockStartNewConversation;
|
||||
await instance.onStartOverClick(RestartConversationOptions.SameUserId);
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith(clearLog('doc1'));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(clearLog('doc1', jasmine.any(Function)));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(setInspectorObjects('doc1', []));
|
||||
expect(mockRemoteCallsMade).toHaveLength(3);
|
||||
expect(mockRemoteCallsMade.some(cmd => cmd.commandName === SharedConstants.Commands.Telemetry.TrackEvent)).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
mockRemoteCallsMade.some(cmd => {
|
||||
return cmd.args[0] === 'conversation_restart' && cmd.args[1].userId === 'same';
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, 'conversation_restart', {
|
||||
userId: 'same',
|
||||
})
|
||||
).toBe(true);
|
||||
);
|
||||
expect(mockStartNewConversation).toHaveBeenCalledWith(undefined, true, false);
|
||||
});
|
||||
|
||||
|
|
|
@ -45,9 +45,10 @@ import {
|
|||
SharedConstants,
|
||||
ValueTypesMask,
|
||||
} from '@bfemulator/app-shared';
|
||||
import { CommandServiceImpl } from '@bfemulator/sdk-shared';
|
||||
import { CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { Document } from '../../../data/reducer/editor';
|
||||
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
|
||||
import { debounce } from '../../../utils';
|
||||
|
||||
import ChatPanel from './chatPanel/chatPanel';
|
||||
|
@ -68,7 +69,7 @@ export type EmulatorMode = 'transcript' | 'livechat';
|
|||
|
||||
export interface EmulatorProps {
|
||||
activeDocumentId?: string;
|
||||
clearLog?: (documentId: string) => void;
|
||||
clearLog?: (documentId: string) => Promise<void>;
|
||||
conversationId?: string;
|
||||
createErrorNotification?: (notification: Notification) => void;
|
||||
debugMode?: DebugMode;
|
||||
|
@ -90,6 +91,9 @@ export interface EmulatorProps {
|
|||
}
|
||||
|
||||
export class Emulator extends React.Component<EmulatorProps, {}> {
|
||||
@CommandServiceInstance()
|
||||
private commandService: CommandServiceImpl;
|
||||
|
||||
private readonly onVerticalSizeChange = debounce(sizes => {
|
||||
this.props.document.ui = {
|
||||
...this.props.document.ui,
|
||||
|
@ -157,12 +161,12 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
|
|||
userId = uniqueIdv4();
|
||||
} else {
|
||||
// use the previous id, or custom id
|
||||
const framework: FrameworkSettings = await CommandServiceImpl.remoteCall(
|
||||
const framework: FrameworkSettings = await this.commandService.remoteCall(
|
||||
SharedConstants.Commands.Settings.LoadAppSettings
|
||||
);
|
||||
userId = props.document.userId || framework.userGUID;
|
||||
}
|
||||
await CommandServiceImpl.remoteCall(SharedConstants.Commands.Emulator.SetCurrentUser, userId);
|
||||
await this.commandService.remoteCall(SharedConstants.Commands.Emulator.SetCurrentUser, userId);
|
||||
|
||||
const options = {
|
||||
conversationId,
|
||||
|
@ -171,15 +175,11 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
|
|||
userId,
|
||||
};
|
||||
|
||||
if (props.document.directLine) {
|
||||
props.document.directLine.end();
|
||||
}
|
||||
await Promise.resolve();
|
||||
this.initConversation(props, options);
|
||||
|
||||
if (props.mode === 'transcript') {
|
||||
try {
|
||||
const conversation = await CommandServiceImpl.remoteCall(
|
||||
const conversation = await this.commandService.remoteCall<any>(
|
||||
SharedConstants.Commands.Emulator.NewTranscript,
|
||||
conversationId
|
||||
);
|
||||
|
@ -188,7 +188,7 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
|
|||
try {
|
||||
// transcript was deep linked via protocol or is generated in-memory via chatdown,
|
||||
// and should just be fed its own activities attached to the document
|
||||
await CommandServiceImpl.remoteCall(
|
||||
await this.commandService.remoteCall<any>(
|
||||
SharedConstants.Commands.Emulator.FeedTranscriptFromMemory,
|
||||
conversation.conversationId,
|
||||
props.document.botId,
|
||||
|
@ -204,7 +204,7 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
|
|||
const fileInfo: {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
} = await CommandServiceImpl.remoteCall(
|
||||
} = await this.commandService.remoteCall<any>(
|
||||
SharedConstants.Commands.Emulator.FeedTranscriptFromDisk,
|
||||
conversation.conversationId,
|
||||
props.document.botId,
|
||||
|
@ -363,8 +363,11 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
|
|||
|
||||
private onStartOverClick = async (option: string = RestartConversationOptions.NewUserId): Promise<void> => {
|
||||
const { NewUserId, SameUserId } = RestartConversationOptions;
|
||||
this.props.clearLog(this.props.document.documentId);
|
||||
this.props.setInspectorObjects(this.props.document.documentId, []);
|
||||
if (this.props.document.directLine) {
|
||||
this.props.document.directLine.end();
|
||||
}
|
||||
await this.props.clearLog(this.props.document.documentId);
|
||||
|
||||
switch (option) {
|
||||
case NewUserId: {
|
||||
|
|
|
@ -31,8 +31,7 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
import { connect } from 'react-redux';
|
||||
import { Notification, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { ValueTypesMask } from '@bfemulator/app-shared/src';
|
||||
import { Notification, SharedConstants, ValueTypesMask } from '@bfemulator/app-shared';
|
||||
|
||||
import { RootState } from '../../../data/store';
|
||||
import * as PresentationActions from '../../../data/action/presentationActions';
|
||||
|
@ -40,7 +39,7 @@ import * as ChatActions from '../../../data/action/chatActions';
|
|||
import { Document } from '../../../data/reducer/editor';
|
||||
import { updateDocument } from '../../../data/action/editorActions';
|
||||
import { beginAdd } from '../../../data/action/notificationActions';
|
||||
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
|
||||
import { executeCommand } from '../../../data/action/commandAction';
|
||||
|
||||
import { Emulator, EmulatorProps } from './emulator';
|
||||
|
||||
|
@ -59,15 +58,21 @@ const mapDispatchToProps = (dispatch): EmulatorProps => ({
|
|||
enablePresentationMode: enable =>
|
||||
enable ? dispatch(PresentationActions.enable()) : dispatch(PresentationActions.disable()),
|
||||
setInspectorObjects: (documentId, objects) => dispatch(ChatActions.setInspectorObjects(documentId, objects)),
|
||||
clearLog: documentId => dispatch(ChatActions.clearLog(documentId)),
|
||||
clearLog: (documentId: string) => {
|
||||
return new Promise(resolve => {
|
||||
dispatch(ChatActions.clearLog(documentId, resolve));
|
||||
});
|
||||
},
|
||||
newConversation: (documentId, options) => dispatch(ChatActions.newConversation(documentId, options)),
|
||||
updateChat: (documentId: string, updatedValues: any) => dispatch(ChatActions.updateChat(documentId, updatedValues)),
|
||||
updateDocument: (documentId, updatedValues: Partial<Document>) => dispatch(updateDocument(documentId, updatedValues)),
|
||||
createErrorNotification: (notification: Notification) => dispatch(beginAdd(notification)),
|
||||
trackEvent: (name: string, properties?: { [key: string]: any }) =>
|
||||
CommandServiceImpl.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, name, properties).catch(),
|
||||
dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, name, properties)),
|
||||
exportItems: (valueTypes: ValueTypesMask, conversationId: string) =>
|
||||
CommandServiceImpl.remoteCall(SharedConstants.Commands.Emulator.SaveTranscriptToFile, valueTypes, conversationId),
|
||||
dispatch(
|
||||
executeCommand(true, SharedConstants.Commands.Emulator.SaveTranscriptToFile, null, valueTypes, conversationId)
|
||||
),
|
||||
});
|
||||
|
||||
export const EmulatorContainer = connect(
|
||||
|
|
|
@ -38,18 +38,44 @@ import ReactWebChat, { createDirectLine } from 'botframework-webchat';
|
|||
import { ActivityTypes } from 'botframework-schema';
|
||||
import { DebugMode, ValueTypes } from '@bfemulator/app-shared';
|
||||
import { combineReducers, createStore } from 'redux';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CommandServiceImpl } from '../../../../../platform/commands/commandServiceImpl';
|
||||
import { EmulatorMode } from '../../emulator';
|
||||
import { bot } from '../../../../../data/reducer/bot';
|
||||
import { chat } from '../../../../../data/reducer/chat';
|
||||
import { editor } from '../../../../../data/reducer/editor';
|
||||
import { clientAwareSettings } from '../../../../../data/reducer/clientAwareSettingsReducer';
|
||||
import { BotCommands } from '../../../../../commands/botCommands';
|
||||
|
||||
import webChatStyleOptions from './webChatTheme';
|
||||
import { ChatContainer } from './chatContainer';
|
||||
import { ChatProps } from './chat';
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
const mockStore = createStore(combineReducers({ bot, chat, clientAwareSettings, editor }), {
|
||||
clientAwareSettings: {
|
||||
currentUser: { id: '123', name: 'Current User' },
|
||||
|
@ -95,6 +121,14 @@ function render(overrides: Partial<ChatProps> = {}): ReactWrapper {
|
|||
}
|
||||
|
||||
describe('<ChatContainer />', () => {
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
new BotCommands();
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
});
|
||||
|
||||
describe('when there is no direct line client', () => {
|
||||
it('renders a `not connected` message', () => {
|
||||
const component = render({ document: {} } as any);
|
||||
|
@ -208,7 +242,7 @@ describe('<ChatContainer />', () => {
|
|||
|
||||
describe('speech services', () => {
|
||||
it('displays a message when fetching the speech token', () => {
|
||||
(CommandServiceImpl as any).remoteCall = jest.fn();
|
||||
commandService.remoteCall = jest.fn().mockResolvedValueOnce(true);
|
||||
const component = render({ pendingSpeechTokenRetrieval: true });
|
||||
expect(component.find('div').text()).toEqual('Connecting...');
|
||||
});
|
||||
|
|
|
@ -30,7 +30,14 @@
|
|||
// 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 { logEntry, LogLevel, textItem, luisEditorDeepLinkItem } from '@bfemulator/sdk-shared';
|
||||
import {
|
||||
CommandServiceImpl,
|
||||
CommandServiceInstance,
|
||||
logEntry,
|
||||
LogLevel,
|
||||
luisEditorDeepLinkItem,
|
||||
textItem,
|
||||
} from '@bfemulator/sdk-shared';
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { mount } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
|
@ -43,7 +50,8 @@ import { bot } from '../../../../../data/reducer/bot';
|
|||
import { clientAwareSettings } from '../../../../../data/reducer/clientAwareSettingsReducer';
|
||||
import { theme } from '../../../../../data/reducer/themeReducer';
|
||||
import { ExtensionManager } from '../../../../../extensions';
|
||||
import { LogService } from '../../../../../platform/log/logService';
|
||||
import { logService } from '../../../../../platform/log/logService';
|
||||
import { executeCommand } from '../../../../../data/action/commandAction';
|
||||
|
||||
import { Inspector } from './inspector';
|
||||
import { InspectorContainer } from './inspectorContainer';
|
||||
|
@ -52,22 +60,35 @@ const mockStore = createStore(combineReducers({ theme, bot, clientAwareSettings
|
|||
clientAwareSettings: { appPath: 'app-path' },
|
||||
});
|
||||
|
||||
jest.mock('../../../panel/panel.scss', () => ({}));
|
||||
|
||||
jest.mock('../../../../../data/store', () => ({
|
||||
get store() {
|
||||
return mockStore;
|
||||
},
|
||||
}));
|
||||
|
||||
let mockRemoteCallsMade;
|
||||
jest.mock('../../../../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
remoteCall: (commandName, ...args) => {
|
||||
mockRemoteCallsMade.push({ commandName, args });
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
const mockState = {
|
||||
|
@ -273,6 +294,18 @@ describe('The Inspector component', () => {
|
|||
return el;
|
||||
};
|
||||
|
||||
let commandService: CommandServiceImpl;
|
||||
let mockRemoteCallsMade = [];
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
commandService.remoteCall = (commandName, ...args) => {
|
||||
mockRemoteCallsMade.push({ commandName, args });
|
||||
return true as any;
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockStore.dispatch(switchTheme('light', ['vars.css', 'light.css']));
|
||||
mockStore.dispatch(loadBotInfos([mockState.bot]));
|
||||
|
@ -331,7 +364,9 @@ describe('The Inspector component', () => {
|
|||
});
|
||||
|
||||
describe('when there is an object to be inspected', () => {
|
||||
let dispatchSpy;
|
||||
beforeEach(() => {
|
||||
dispatchSpy = jest.spyOn(mockStore, 'dispatch');
|
||||
parent = mount(
|
||||
<Provider store={mockStore}>
|
||||
<InspectorContainer document={mockState.document} inspector={{ src }} />
|
||||
|
@ -434,7 +469,7 @@ describe('The Inspector component', () => {
|
|||
|
||||
it('"logger.log" or "logger.error"', () => {
|
||||
event.channel = 'logger.log';
|
||||
const logSpy = jest.spyOn(LogService, 'logToDocument');
|
||||
const logSpy = jest.spyOn(logService, 'logToDocument');
|
||||
const inspectorName = mockExtensions[0].name;
|
||||
const text = `[${inspectorName}] ${event.args[0]}`;
|
||||
instance.ipcMessageEventHandler(event);
|
||||
|
@ -444,7 +479,7 @@ describe('The Inspector component', () => {
|
|||
|
||||
it('"logger.luis-editor-deep-link"', () => {
|
||||
event.channel = 'logger.luis-editor-deep-link';
|
||||
const logSpy = jest.spyOn(LogService, 'logToDocument');
|
||||
const logSpy = jest.spyOn(logService, 'logToDocument');
|
||||
const inspectorName = mockExtensions[0].name;
|
||||
const text = `[${inspectorName}] ${event.args[0]}`;
|
||||
instance.ipcMessageEventHandler(event);
|
||||
|
@ -452,25 +487,20 @@ describe('The Inspector component', () => {
|
|||
expect(logSpy).toHaveBeenCalledWith(mockState.document.documentId, logEntry(luisEditorDeepLinkItem(text)));
|
||||
});
|
||||
|
||||
it('"track-event"', () => {
|
||||
it('"track-event"', async () => {
|
||||
event.channel = 'track-event';
|
||||
event.args[0] = 'someEvent';
|
||||
event.args[1] = { some: 'data' };
|
||||
instance.ipcMessageEventHandler(event);
|
||||
await instance.ipcMessageEventHandler(event);
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, ...event.args)
|
||||
);
|
||||
|
||||
expect(mockRemoteCallsMade).toHaveLength(1);
|
||||
expect(mockRemoteCallsMade[0]).toEqual({
|
||||
commandName: SharedConstants.Commands.Telemetry.TrackEvent,
|
||||
args: ['someEvent', { some: 'data' }],
|
||||
});
|
||||
|
||||
event.args[1] = undefined;
|
||||
event.args[1] = {};
|
||||
instance.ipcMessageEventHandler(event);
|
||||
expect(mockRemoteCallsMade).toHaveLength(2);
|
||||
expect(mockRemoteCallsMade[1]).toEqual({
|
||||
commandName: SharedConstants.Commands.Telemetry.TrackEvent,
|
||||
args: ['someEvent', {}],
|
||||
});
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, ...event.args)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -46,7 +46,7 @@ import { IBotConfiguration } from 'botframework-config/lib/schema';
|
|||
import * as React from 'react';
|
||||
|
||||
import { ExtensionManager, GetInspectorResult, InspectorAPI } from '../../../../../extensions';
|
||||
import { LogService } from '../../../../../platform/log/logService';
|
||||
import { logService } from '../../../../../platform/log/logService';
|
||||
import Panel, { PanelContent, PanelControls } from '../../../panel/panel';
|
||||
|
||||
import * as styles from './inspector.scss';
|
||||
|
@ -383,7 +383,7 @@ export class Inspector extends React.Component<InspectorProps, InspectorState> {
|
|||
const { documentId } = this.props.document;
|
||||
const inspectorName = this._state.titleOverride || this.state.inspector.name || 'inspector';
|
||||
const text = `[${inspectorName}] ${event.args[0]}`;
|
||||
LogService.logToDocument(documentId, logEntry(textItem(logLevel, text)));
|
||||
logService.logToDocument(documentId, logEntry(textItem(logLevel, text)));
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -391,7 +391,7 @@ export class Inspector extends React.Component<InspectorProps, InspectorState> {
|
|||
const { documentId } = this.props.document;
|
||||
const inspectorName = this._state.titleOverride || this.state.inspector.name || 'inspector';
|
||||
const text = `[${inspectorName}] ${event.args[0]}`;
|
||||
LogService.logToDocument(documentId, logEntry(luisEditorDeepLinkItem(text)));
|
||||
logService.logToDocument(documentId, logEntry(luisEditorDeepLinkItem(text)));
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ import { connect } from 'react-redux';
|
|||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
|
||||
import { RootState } from '../../../../../data/store';
|
||||
import { CommandServiceImpl } from '../../../../../platform/commands/commandServiceImpl';
|
||||
import { executeCommand } from '../../../../../data/action/commandAction';
|
||||
|
||||
import { Inspector } from './inspector';
|
||||
|
||||
|
@ -50,12 +50,10 @@ const mapStateToProps = (state: RootState, ownProps: any) => {
|
|||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = _dispatch => {
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
trackEvent: (name: string, properties?: { [key: string]: any }) => {
|
||||
CommandServiceImpl.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, name, properties).catch(
|
||||
_e => void 0
|
||||
);
|
||||
dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, name, properties));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -39,10 +39,29 @@ import { mount, ReactWrapper } from 'enzyme';
|
|||
import { Log, LogProps } from './log';
|
||||
import { LogEntry } from './logEntry';
|
||||
|
||||
jest.mock('./log.scss', () => ({}));
|
||||
jest.mock('../../../../../platform/commands/commandServiceImpl', () => ({
|
||||
call: (...args: any[]) => null,
|
||||
remoteCall: (...args: any[]) => null,
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
describe('log component', () => {
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
import { LogLevel, textItem } from '@bfemulator/sdk-shared';
|
||||
import { CommandServiceImpl, CommandServiceInstance, LogLevel, textItem } from '@bfemulator/sdk-shared';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
@ -50,6 +50,7 @@ import {
|
|||
} from '../../../../dialogs';
|
||||
import { ConnectedServiceEditorContainer } from '../../../../shell/explorer/servicesExplorer/connectedServiceEditor';
|
||||
import { ConnectedServicePickerContainer } from '../../../../shell/explorer/servicesExplorer';
|
||||
import { executeCommand } from '../../../../../data/action/commandAction';
|
||||
|
||||
import { LogEntry as LogEntryContainer } from './logEntryContainer';
|
||||
import { LogEntry, LogEntryProps, number2, timestamp } from './logEntry';
|
||||
|
@ -62,17 +63,30 @@ jest.mock('./log.scss', () => ({}));
|
|||
|
||||
let mockRemoteCallsMade;
|
||||
let mockCallsMade;
|
||||
jest.mock('../../../../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
call: (commandName, ...args) => {
|
||||
mockCallsMade.push({ commandName, args });
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
remoteCall: (commandName, ...args) => {
|
||||
mockRemoteCallsMade.push({ commandName, args });
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
},
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
describe('logEntry component', () => {
|
||||
|
@ -82,6 +96,21 @@ describe('logEntry component', () => {
|
|||
let props: LogEntryProps;
|
||||
let mockDispatch;
|
||||
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
commandService.remoteCall = (commandName: string, ...args: any[]) => {
|
||||
mockRemoteCallsMade.push({ commandName, args });
|
||||
return Promise.resolve(true) as any;
|
||||
};
|
||||
commandService.call = (commandName: string, ...args: any[]) => {
|
||||
mockCallsMade.push({ commandName, args });
|
||||
return Promise.resolve(true) as any;
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockRemoteCallsMade = [];
|
||||
mockCallsMade = [];
|
||||
|
@ -159,14 +188,14 @@ describe('logEntry component', () => {
|
|||
const mockInspectableObj = { some: 'data', type: 'message', id: 'someId' };
|
||||
instance.inspectAndHighlightInWebchat(mockInspectableObj);
|
||||
|
||||
expect(mockRemoteCallsMade).toHaveLength(1);
|
||||
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
|
||||
expect(mockRemoteCallsMade[0].args).toEqual(['log_inspectActivity', { type: 'message' }]);
|
||||
expect(mockDispatch).toHaveBeenLastCalledWith(
|
||||
executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, 'log_inspectActivity', {
|
||||
type: 'message',
|
||||
})
|
||||
);
|
||||
|
||||
mockInspectableObj.type = undefined;
|
||||
instance.inspectAndHighlightInWebchat(mockInspectableObj);
|
||||
|
||||
expect(mockRemoteCallsMade[1].args).toEqual(['log_inspectActivity', { type: '' }]);
|
||||
});
|
||||
|
||||
it('should highlight an object', () => {
|
||||
|
|
|
@ -36,17 +36,17 @@ import { connect } from 'react-redux';
|
|||
import { ServiceTypes } from 'botframework-config/lib/schema';
|
||||
|
||||
import * as ConnectedServiceActions from '../../../../../data/action/connectedServiceActions';
|
||||
import { CommandServiceImpl } from '../../../../../platform/commands/commandServiceImpl';
|
||||
import {
|
||||
ConnectServicePromptDialogContainer,
|
||||
AzureLoginSuccessDialogContainer,
|
||||
AzureLoginFailedDialogContainer,
|
||||
AzureLoginSuccessDialogContainer,
|
||||
ConnectServicePromptDialogContainer,
|
||||
GetStartedWithCSDialogContainer,
|
||||
ProgressIndicatorContainer,
|
||||
} from '../../../../dialogs';
|
||||
import { ConnectedServicePickerContainer } from '../../../../shell/explorer/servicesExplorer';
|
||||
import { ConnectedServiceEditorContainer } from '../../../../shell/explorer/servicesExplorer/connectedServiceEditor';
|
||||
import { setHighlightedObjects, setInspectorObjects } from '../../../../../data/action/chatActions';
|
||||
import { executeCommand } from '../../../../../data/action/commandAction';
|
||||
|
||||
import { LogEntry as LogEntryComponent, LogEntryProps } from './logEntry';
|
||||
|
||||
|
@ -71,17 +71,14 @@ function mapDispatchToProps(dispatch: any): Partial<LogEntryProps> {
|
|||
setInspectorObjects: (documentId: string, obj: any) => dispatch(setInspectorObjects(documentId, obj)),
|
||||
setHighlightedObjects: (documentId: string, obj: any) => dispatch(setHighlightedObjects(documentId, obj)),
|
||||
reconnectNgrok: () => {
|
||||
const { Ngrok } = SharedConstants.Commands;
|
||||
return CommandServiceImpl.remoteCall(Ngrok.Reconnect);
|
||||
dispatch(executeCommand(true, SharedConstants.Commands.Ngrok.Reconnect));
|
||||
},
|
||||
showAppSettings: () => {
|
||||
const { UI } = SharedConstants.Commands;
|
||||
return CommandServiceImpl.call(UI.ShowAppSettings);
|
||||
return dispatch(executeCommand(false, UI.ShowAppSettings));
|
||||
},
|
||||
trackEvent: (name: string, properties?: { [key: string]: any }) => {
|
||||
CommandServiceImpl.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, name, properties).catch(
|
||||
_e => void 0
|
||||
);
|
||||
dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, name, properties));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ import * as BotActions from '../../../data/action/botActions';
|
|||
import { beginAdd } from '../../../data/action/notificationActions';
|
||||
import { openContextMenuForBot } from '../../../data/action/welcomePageActions';
|
||||
import { bot } from '../../../data/reducer/bot';
|
||||
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
|
||||
import { executeCommand } from '../../../data/action/commandAction';
|
||||
|
||||
import { RecentBotsList } from './recentBotsList';
|
||||
import { RecentBotsListContainer } from './recentBotsListContainer';
|
||||
|
@ -93,23 +93,7 @@ describe('The RecentBotsList', () => {
|
|||
expect(mockDispatch).toHaveBeenCalledWith(openContextMenuForBot((mockStore.getState() as any).bot.botFiles[0]));
|
||||
});
|
||||
|
||||
it('should send a notification when a bot fails to delete', async () => {
|
||||
await instance.onDeleteBotClick({
|
||||
currentTarget: {
|
||||
dataset: {
|
||||
index: 1,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
const message = `An Error occurred on the Recent Bots List: TypeError: Cannot read property 'path' of undefined`;
|
||||
const notification = beginAdd(newNotification(message));
|
||||
notification.payload.notification.timestamp = jasmine.any(Number) as any;
|
||||
notification.payload.notification.id = jasmine.any(String) as any;
|
||||
expect(mockDispatch).toHaveBeenCalledWith(notification);
|
||||
});
|
||||
|
||||
it('should call the appropriate command when a bot from the list is deleted', async () => {
|
||||
const spy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue(true);
|
||||
await instance.onDeleteBotClick({
|
||||
currentTarget: {
|
||||
dataset: {
|
||||
|
@ -118,7 +102,7 @@ describe('The RecentBotsList', () => {
|
|||
},
|
||||
} as any);
|
||||
const { RemoveFromBotList } = SharedConstants.Commands.Bot;
|
||||
expect(spy).toHaveBeenCalledWith(RemoveFromBotList, '/some/path');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(executeCommand(true, RemoveFromBotList, null, '/some/path'));
|
||||
});
|
||||
|
||||
it('should call the onBotSelected function passed in the props when a bot it selected from the list', () => {
|
||||
|
|
|
@ -40,9 +40,8 @@ import * as styles from './recentBotsList.scss';
|
|||
|
||||
export interface RecentBotsListProps {
|
||||
onBotSelected?: (bot: BotInfo) => void;
|
||||
onDeleteBotClick?: (path: string) => Promise<any>;
|
||||
onDeleteBotClick?: (path: string) => void;
|
||||
recentBots?: BotInfo[];
|
||||
sendNotification?: (error: Error) => void;
|
||||
showContextMenuForBot?: (bot: BotInfo) => void;
|
||||
}
|
||||
|
||||
|
@ -101,10 +100,6 @@ export class RecentBotsList extends Component<RecentBotsListProps, {}> {
|
|||
private onDeleteBotClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const { index } = event.currentTarget.dataset;
|
||||
const bot = this.props.recentBots[index];
|
||||
try {
|
||||
await this.props.onDeleteBotClick(bot.path);
|
||||
} catch (e) {
|
||||
this.props.sendNotification(e);
|
||||
}
|
||||
this.props.onDeleteBotClick(bot.path);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,14 +31,13 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
import { BotInfo, newNotification, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { BotInfo, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { connect } from 'react-redux';
|
||||
import { Action } from 'redux';
|
||||
|
||||
import { beginAdd } from '../../../data/action/notificationActions';
|
||||
import { openContextMenuForBot } from '../../../data/action/welcomePageActions';
|
||||
import { RootState } from '../../../data/store';
|
||||
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
|
||||
import { executeCommand } from '../../../data/action/commandAction';
|
||||
|
||||
import { RecentBotsList, RecentBotsListProps } from './recentBotsList';
|
||||
|
||||
|
@ -51,10 +50,8 @@ const mapStateToProps = (state: RootState, ownProps: { [propName: string]: any }
|
|||
|
||||
const mapDispatchToProps = (dispatch: (action: Action) => void): RecentBotsListProps => {
|
||||
return {
|
||||
onDeleteBotClick: (path: string): Promise<any> =>
|
||||
CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.RemoveFromBotList, path),
|
||||
sendNotification: (error: Error) =>
|
||||
dispatch(beginAdd(newNotification(`An Error occurred on the Recent Bots List: ${error}`))),
|
||||
onDeleteBotClick: (path: string): void =>
|
||||
dispatch(executeCommand(true, SharedConstants.Commands.Bot.RemoveFromBotList, null, path)),
|
||||
showContextMenuForBot: (bot: BotInfo): void => dispatch(openContextMenuForBot(bot)),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -41,7 +41,7 @@ import * as BotActions from '../../../data/action/botActions';
|
|||
import { azureAuth } from '../../../data/reducer/azureAuthReducer';
|
||||
import { clientAwareSettings } from '../../../data/reducer/clientAwareSettingsReducer';
|
||||
import { bot } from '../../../data/reducer/bot';
|
||||
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
|
||||
import { executeCommand } from '../../../data/action/commandAction';
|
||||
|
||||
import { WelcomePage } from './welcomePage';
|
||||
import { WelcomePageContainer } from './welcomePageContainer';
|
||||
|
@ -54,6 +54,31 @@ jest.mock('../../../data/store', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
const mockArmToken = 'bm90aGluZw==.eyJ1cG4iOiJnbGFzZ293QHNjb3RsYW5kLmNvbSJ9.7gjdshgfdsk98458205jfds9843fjds';
|
||||
const bots = [
|
||||
{
|
||||
|
@ -73,9 +98,11 @@ describe('The AzureLoginFailedDialogContainer component should', () => {
|
|||
let parent;
|
||||
let node;
|
||||
let instance: any;
|
||||
let mockDispatch;
|
||||
beforeEach(() => {
|
||||
mockStore.dispatch(azureArmTokenDataChanged(mockArmToken));
|
||||
mockStore.dispatch(BotActions.loadBotInfos(bots));
|
||||
mockDispatch = jest.spyOn(mockStore, 'dispatch');
|
||||
parent = mount(
|
||||
<Provider store={mockStore}>
|
||||
<WelcomePageContainer />
|
||||
|
@ -91,25 +118,22 @@ describe('The AzureLoginFailedDialogContainer component should', () => {
|
|||
});
|
||||
|
||||
it('should call the appropriate command when a recent bot is clicked', () => {
|
||||
const spy = jest.spyOn(CommandServiceImpl, 'call');
|
||||
instance.onBotSelected(bots[1]);
|
||||
const { Switch } = SharedConstants.Commands.Bot;
|
||||
expect(spy).toHaveBeenCalledWith(Switch, '/Users/microsoft/Documents/testbot/TestBot.bot');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
executeCommand(false, SharedConstants.Commands.Bot.Switch, null, '/Users/microsoft/Documents/testbot/TestBot.bot')
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the appropriate command when onOpenBotClick is called', async () => {
|
||||
const spy = jest.spyOn(CommandServiceImpl, 'call');
|
||||
await instance.onOpenBotClick();
|
||||
expect(spy).toHaveBeenCalledWith(SharedConstants.Commands.UI.ShowOpenBotDialog);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(executeCommand(false, SharedConstants.Commands.UI.ShowOpenBotDialog));
|
||||
});
|
||||
|
||||
it('should call the appropriate command when openBotInspectorDocs is called', async () => {
|
||||
const callSpy = jest.spyOn(CommandServiceImpl, 'call');
|
||||
instance.props.openBotInspectorDocs();
|
||||
expect(callSpy).toHaveBeenCalledWith(
|
||||
SharedConstants.Commands.UI.ShowMarkdownPage,
|
||||
SharedConstants.Channels.ReadmeUrl,
|
||||
SharedConstants.Channels.HelpLabel
|
||||
const { Commands, Channels } = SharedConstants;
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
executeCommand(false, Commands.UI.ShowMarkdownPage, null, Channels.ReadmeUrl, Channels.HelpLabel)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -45,11 +45,11 @@ import * as styles from './welcomePage.scss';
|
|||
export interface WelcomePageProps {
|
||||
accessToken?: string;
|
||||
onNewBotClick?: () => void;
|
||||
showOpenBotDialog: () => Promise<any>;
|
||||
showOpenBotDialog: () => void;
|
||||
sendNotification?: (error: Error) => void;
|
||||
signInWithAzure?: () => void;
|
||||
signOutWithAzure?: () => void;
|
||||
switchToBot?: (path: string) => Promise<any>;
|
||||
switchToBot?: (path: string) => void;
|
||||
openBotInspectorDocs: () => void;
|
||||
debugMode?: number;
|
||||
}
|
||||
|
@ -134,19 +134,11 @@ export class WelcomePage extends React.Component<WelcomePageProps, {}> {
|
|||
}
|
||||
|
||||
private onBotSelected = async (bot: BotInfo) => {
|
||||
try {
|
||||
await this.props.switchToBot(bot.path);
|
||||
} catch (e) {
|
||||
this.props.sendNotification(e);
|
||||
}
|
||||
this.props.switchToBot(bot.path);
|
||||
};
|
||||
|
||||
private onOpenBotClick = async () => {
|
||||
try {
|
||||
await this.props.showOpenBotDialog();
|
||||
} catch (e) {
|
||||
this.props.sendNotification(e);
|
||||
}
|
||||
this.props.showOpenBotDialog();
|
||||
};
|
||||
|
||||
private get signInSection(): JSX.Element {
|
||||
|
|
|
@ -31,13 +31,12 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
import { newNotification, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { connect } from 'react-redux';
|
||||
import { Action } from 'redux';
|
||||
|
||||
import { beginAdd } from '../../../data/action/notificationActions';
|
||||
import { RootState } from '../../../data/store';
|
||||
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
|
||||
import { executeCommand } from '../../../data/action/commandAction';
|
||||
|
||||
import { WelcomePage, WelcomePageProps } from './welcomePage';
|
||||
|
||||
|
@ -52,22 +51,16 @@ function mapStateToProps(state: RootState, ownProps: WelcomePageProps): WelcomeP
|
|||
function mapDispatchToProps(dispatch: (action: Action) => void): WelcomePageProps {
|
||||
const { Commands, Channels } = SharedConstants;
|
||||
return {
|
||||
onNewBotClick: () => {
|
||||
CommandServiceImpl.call(Commands.UI.ShowBotCreationDialog).catch();
|
||||
},
|
||||
showOpenBotDialog: (): Promise<any> => CommandServiceImpl.call(SharedConstants.Commands.UI.ShowOpenBotDialog),
|
||||
sendNotification: (error: Error) =>
|
||||
dispatch(beginAdd(newNotification(`An Error occurred on the Welcome page: ${error}`))),
|
||||
signInWithAzure: () => {
|
||||
CommandServiceImpl.call(Commands.UI.SignInToAzure).catch();
|
||||
},
|
||||
onNewBotClick: () => dispatch(executeCommand(false, Commands.UI.ShowBotCreationDialog)),
|
||||
showOpenBotDialog: () => dispatch(executeCommand(false, SharedConstants.Commands.UI.ShowOpenBotDialog)),
|
||||
signInWithAzure: () => dispatch(executeCommand(false, Commands.UI.SignInToAzure)),
|
||||
signOutWithAzure: () => {
|
||||
CommandServiceImpl.remoteCall(Commands.Azure.SignUserOutOfAzure).catch();
|
||||
CommandServiceImpl.call(Commands.UI.InvalidateAzureArmToken).catch();
|
||||
dispatch(executeCommand(true, Commands.Azure.SignUserOutOfAzure));
|
||||
dispatch(executeCommand(false, Commands.UI.InvalidateAzureArmToken));
|
||||
},
|
||||
switchToBot: (path: string) => CommandServiceImpl.call(Commands.Bot.Switch, path),
|
||||
switchToBot: (path: string) => dispatch(executeCommand(false, Commands.Bot.Switch, null, path)),
|
||||
openBotInspectorDocs: () =>
|
||||
CommandServiceImpl.call(Commands.UI.ShowMarkdownPage, Channels.ReadmeUrl, Channels.HelpLabel),
|
||||
dispatch(executeCommand(false, Commands.UI.ShowMarkdownPage, null, Channels.ReadmeUrl, Channels.HelpLabel)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -33,37 +33,52 @@
|
|||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { BotConfigWithPath } from '@bfemulator/sdk-shared';
|
||||
import { IEndpointService, ServiceTypes } from 'botframework-config/lib/schema';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import * as botHelpers from '../../data/botHelpers';
|
||||
import * as editorHelpers from '../../data/editorHelpers';
|
||||
import { store } from '../../data/store';
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
|
||||
import { ActiveBotHelper } from './activeBotHelper';
|
||||
|
||||
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('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
describe('ActiveBotHelper tests', () => {
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
});
|
||||
it('confirmSwitchBot() functionality', async () => {
|
||||
(editorHelpers as any).hasNonGlobalTabs = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(true)
|
||||
.mockReturnValueOnce(false);
|
||||
(CommandServiceImpl as any).remoteCall = jest.fn().mockResolvedValue('done');
|
||||
commandService.remoteCall = jest.fn().mockResolvedValue('done');
|
||||
|
||||
const result1 = await ActiveBotHelper.confirmSwitchBot();
|
||||
expect(result1).toBe('done');
|
||||
|
@ -78,7 +93,7 @@ describe('ActiveBotHelper tests', () => {
|
|||
.mockReturnValueOnce(true)
|
||||
.mockReturnValueOnce(false);
|
||||
|
||||
(CommandServiceImpl as any).remoteCall = jest.fn().mockResolvedValue('done');
|
||||
commandService.remoteCall = jest.fn().mockResolvedValue('done');
|
||||
|
||||
const result1 = await ActiveBotHelper.confirmCloseBot();
|
||||
expect(result1).toBe('done');
|
||||
|
@ -89,21 +104,21 @@ describe('ActiveBotHelper tests', () => {
|
|||
|
||||
it('closeActiveBot() functionality', async () => {
|
||||
const mockRemoteCall1 = jest.fn().mockResolvedValue(true);
|
||||
(CommandServiceImpl as any).remoteCall = mockRemoteCall1;
|
||||
commandService.remoteCall = mockRemoteCall1;
|
||||
(store as any).dispatch = () => null;
|
||||
|
||||
await ActiveBotHelper.closeActiveBot();
|
||||
expect(mockRemoteCall1).toHaveBeenCalledTimes(2);
|
||||
|
||||
const mockRemoteCall2 = jest.fn().mockRejectedValue('err');
|
||||
(CommandServiceImpl as any).remoteCall = mockRemoteCall2;
|
||||
commandService.remoteCall = mockRemoteCall2;
|
||||
|
||||
expect(ActiveBotHelper.closeActiveBot()).rejects.toEqual(new Error('Error while closing active bot: err'));
|
||||
});
|
||||
|
||||
it('botAlreadyOpen() functionality', async () => {
|
||||
const mockRemoteCall = jest.fn().mockResolvedValue(true);
|
||||
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
|
||||
commandService.remoteCall = mockRemoteCall;
|
||||
|
||||
await ActiveBotHelper.botAlreadyOpen();
|
||||
|
||||
|
@ -112,7 +127,7 @@ describe('ActiveBotHelper tests', () => {
|
|||
|
||||
it('browseForBotFile() functionality', async () => {
|
||||
const mockRemoteCall = jest.fn().mockResolvedValue(true);
|
||||
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
|
||||
commandService.remoteCall = mockRemoteCall;
|
||||
|
||||
await ActiveBotHelper.browseForBotFile();
|
||||
|
||||
|
@ -160,7 +175,7 @@ describe('ActiveBotHelper tests', () => {
|
|||
(store as any).dispatch = mockDispatch;
|
||||
|
||||
let mockRemoteCall = jest.fn().mockResolvedValue({});
|
||||
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
|
||||
commandService.remoteCall = mockRemoteCall;
|
||||
|
||||
await ActiveBotHelper.setActiveBot(bot);
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(2);
|
||||
|
@ -168,7 +183,7 @@ describe('ActiveBotHelper tests', () => {
|
|||
expect(mockRemoteCall).toHaveBeenCalledWith(SharedConstants.Commands.Bot.SetActive, bot);
|
||||
|
||||
mockRemoteCall = jest.fn().mockRejectedValueOnce('error');
|
||||
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
|
||||
commandService.remoteCall = mockRemoteCall;
|
||||
expect(ActiveBotHelper.setActiveBot(bot)).rejects.toEqual(new Error('Error while setting active bot: error'));
|
||||
});
|
||||
|
||||
|
@ -199,8 +214,8 @@ describe('ActiveBotHelper tests', () => {
|
|||
};
|
||||
let mockRemoteCall = jest.fn().mockResolvedValue(bot);
|
||||
const mockCall = jest.fn().mockResolvedValue(null);
|
||||
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
|
||||
(CommandServiceImpl as any).call = mockCall;
|
||||
commandService.remoteCall = mockRemoteCall;
|
||||
commandService.call = mockCall;
|
||||
|
||||
await ActiveBotHelper.confirmAndCreateBot(bot, 'someSecret');
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(3);
|
||||
|
@ -208,8 +223,8 @@ describe('ActiveBotHelper tests', () => {
|
|||
expect(mockRemoteCall).toHaveBeenCalledWith(SharedConstants.Commands.Bot.Create, bot, 'someSecret');
|
||||
|
||||
mockRemoteCall = jest.fn().mockRejectedValue('err');
|
||||
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
|
||||
(CommandServiceImpl as any).call = mockCall;
|
||||
commandService.remoteCall = mockRemoteCall;
|
||||
commandService.call = mockCall;
|
||||
|
||||
expect(ActiveBotHelper.confirmAndCreateBot(bot, 'someSecret')).rejects.toEqual(
|
||||
new Error('Error during bot create: err')
|
||||
|
@ -255,8 +270,8 @@ describe('ActiveBotHelper tests', () => {
|
|||
.mockResolvedValueOnce(bot)
|
||||
.mockResolvedValue(null);
|
||||
const mockCall = jest.fn().mockResolvedValue(null);
|
||||
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
|
||||
(CommandServiceImpl as any).call = mockCall;
|
||||
commandService.remoteCall = mockRemoteCall;
|
||||
commandService.call = mockCall;
|
||||
|
||||
await ActiveBotHelper.confirmAndOpenBotFromFile();
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
|
@ -305,8 +320,8 @@ describe('ActiveBotHelper tests', () => {
|
|||
.mockResolvedValueOnce(bot)
|
||||
.mockResolvedValue(null);
|
||||
const mockCall = jest.fn().mockResolvedValue(null);
|
||||
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
|
||||
(CommandServiceImpl as any).call = mockCall;
|
||||
commandService.remoteCall = mockRemoteCall;
|
||||
commandService.call = mockCall;
|
||||
|
||||
await ActiveBotHelper.confirmAndOpenBotFromFile();
|
||||
expect(mockRemoteCall).toHaveBeenCalledWith(SharedConstants.Commands.Emulator.SetCurrentUser, 'customUserId');
|
||||
|
@ -328,7 +343,7 @@ describe('ActiveBotHelper tests', () => {
|
|||
});
|
||||
|
||||
it('should throw when confirmAndSwitchBots fails', async () => {
|
||||
jest.spyOn(CommandServiceImpl, 'call').mockRejectedValueOnce('oh noes!');
|
||||
jest.spyOn(commandService, 'call').mockRejectedValueOnce('oh noes!');
|
||||
jest.spyOn(botHelpers, 'getActiveBot').mockReturnValueOnce({ path: '' });
|
||||
try {
|
||||
await ActiveBotHelper.confirmAndSwitchBots('');
|
||||
|
@ -375,8 +390,8 @@ describe('ActiveBotHelper tests', () => {
|
|||
(botHelpers.getActiveBot as any) = () => otherBot;
|
||||
const mockRemoteCall = jest.fn().mockResolvedValue(bot);
|
||||
const mockCall = jest.fn().mockResolvedValue(null);
|
||||
(CommandServiceImpl as any).call = mockCall;
|
||||
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
|
||||
commandService.call = mockCall;
|
||||
commandService.remoteCall = mockRemoteCall;
|
||||
ActiveBotHelper.confirmSwitchBot = () => new Promise((resolve, reject) => resolve(true));
|
||||
ActiveBotHelper.setActiveBot = (arg: any) => new Promise((resolve, reject) => resolve(null));
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
import { getBotDisplayName, newNotification, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { BotConfigWithPath, mergeEndpoints } from '@bfemulator/sdk-shared';
|
||||
import { IEndpointService, ServiceTypes } from 'botframework-config/lib/schema';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import * as Constants from '../../constants';
|
||||
import * as BotActions from '../../data/action/botActions';
|
||||
|
@ -45,14 +46,16 @@ import { beginAdd } from '../../data/action/notificationActions';
|
|||
import { getActiveBot } from '../../data/botHelpers';
|
||||
import { hasNonGlobalTabs } from '../../data/editorHelpers';
|
||||
import { store } from '../../data/store';
|
||||
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
|
||||
|
||||
const { Bot, Electron, Telemetry } = SharedConstants.Commands;
|
||||
|
||||
export const ActiveBotHelper = new (class {
|
||||
async confirmSwitchBot(): Promise<any> {
|
||||
export class ActiveBotHelper {
|
||||
@CommandServiceInstance()
|
||||
private static commandService: CommandServiceImpl;
|
||||
|
||||
static async confirmSwitchBot(): Promise<any> {
|
||||
if (hasNonGlobalTabs()) {
|
||||
return await CommandServiceImpl.remoteCall(Electron.ShowMessageBox, true, {
|
||||
return await this.commandService.remoteCall(Electron.ShowMessageBox, true, {
|
||||
buttons: ['Cancel', 'OK'],
|
||||
cancelId: 0,
|
||||
defaultId: 1,
|
||||
|
@ -64,11 +67,11 @@ export const ActiveBotHelper = new (class {
|
|||
}
|
||||
}
|
||||
|
||||
confirmCloseBot(): Promise<any> {
|
||||
static confirmCloseBot(): Promise<any> {
|
||||
const hasTabs = hasNonGlobalTabs();
|
||||
// TODO - localization
|
||||
if (hasTabs) {
|
||||
return CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.ShowMessageBox, true, {
|
||||
return this.commandService.remoteCall(SharedConstants.Commands.Electron.ShowMessageBox, true, {
|
||||
type: 'question',
|
||||
buttons: ['Cancel', 'OK'],
|
||||
defaultId: 1,
|
||||
|
@ -83,17 +86,17 @@ export const ActiveBotHelper = new (class {
|
|||
/** Sets a bot as active
|
||||
* @param bot Bot to set as active
|
||||
*/
|
||||
async setActiveBot(bot: BotConfigWithPath): Promise<void> {
|
||||
static async setActiveBot(bot: BotConfigWithPath): Promise<void> {
|
||||
try {
|
||||
// set the bot as active on the server side
|
||||
const botDirectory = await CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.SetActive, bot);
|
||||
const botDirectory = await this.commandService.remoteCall<string>(SharedConstants.Commands.Bot.SetActive, bot);
|
||||
store.dispatch(BotActions.setActiveBot(bot));
|
||||
store.dispatch(FileActions.setRoot(botDirectory));
|
||||
|
||||
// update the app file menu and title bar
|
||||
await Promise.all([
|
||||
CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.UpdateFileMenu),
|
||||
CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.SetTitleBar, getBotDisplayName(bot)),
|
||||
this.commandService.remoteCall(SharedConstants.Commands.Electron.UpdateFileMenu),
|
||||
this.commandService.remoteCall(SharedConstants.Commands.Electron.SetTitleBar, getBotDisplayName(bot)),
|
||||
]);
|
||||
} catch (e) {
|
||||
const errMsg = `Error while setting active bot: ${e}`;
|
||||
|
@ -104,11 +107,11 @@ export const ActiveBotHelper = new (class {
|
|||
}
|
||||
|
||||
/** tell the server-side the active bot is now closed */
|
||||
async closeActiveBot(): Promise<void> {
|
||||
static async closeActiveBot(): Promise<void> {
|
||||
try {
|
||||
await CommandServiceImpl.remoteCall(Bot.Close);
|
||||
await this.commandService.remoteCall(Bot.Close);
|
||||
store.dispatch(BotActions.closeBot());
|
||||
await CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.SetTitleBar, '');
|
||||
await this.commandService.remoteCall(SharedConstants.Commands.Electron.SetTitleBar, '');
|
||||
} catch (err) {
|
||||
const errMsg = `Error while closing active bot: ${err}`;
|
||||
const notification = newNotification(errMsg);
|
||||
|
@ -116,9 +119,9 @@ export const ActiveBotHelper = new (class {
|
|||
}
|
||||
}
|
||||
|
||||
async botAlreadyOpen(): Promise<void> {
|
||||
static async botAlreadyOpen(): Promise<void> {
|
||||
// TODO - localization
|
||||
return await CommandServiceImpl.remoteCall(Electron.ShowMessageBox, true, {
|
||||
return await this.commandService.remoteCall<void>(Electron.ShowMessageBox, true, {
|
||||
buttons: ['OK'],
|
||||
cancelId: 0,
|
||||
defaultId: 0,
|
||||
|
@ -129,7 +132,7 @@ export const ActiveBotHelper = new (class {
|
|||
});
|
||||
}
|
||||
|
||||
async confirmAndCreateBot(botToCreate: BotConfigWithPath, secret: string): Promise<void> {
|
||||
static async confirmAndCreateBot(botToCreate: BotConfigWithPath, secret: string): Promise<void> {
|
||||
// prompt the user to confirm the switch
|
||||
const result = await this.confirmSwitchBot();
|
||||
|
||||
|
@ -138,7 +141,7 @@ export const ActiveBotHelper = new (class {
|
|||
|
||||
try {
|
||||
// create the bot and save to disk
|
||||
const bot: BotConfigWithPath = await CommandServiceImpl.remoteCall(
|
||||
const bot = await this.commandService.remoteCall<BotConfigWithPath>(
|
||||
SharedConstants.Commands.Bot.Create,
|
||||
botToCreate,
|
||||
secret
|
||||
|
@ -151,7 +154,7 @@ export const ActiveBotHelper = new (class {
|
|||
) as IEndpointService;
|
||||
|
||||
if (endpoint) {
|
||||
CommandServiceImpl.call(SharedConstants.Commands.Emulator.NewLiveChat, endpoint);
|
||||
this.commandService.call(SharedConstants.Commands.Emulator.NewLiveChat, endpoint);
|
||||
}
|
||||
|
||||
store.dispatch(NavBarActions.select(Constants.NAVBAR_BOT_EXPLORER));
|
||||
|
@ -165,8 +168,8 @@ export const ActiveBotHelper = new (class {
|
|||
}
|
||||
}
|
||||
|
||||
browseForBotFile(): Promise<any> {
|
||||
return CommandServiceImpl.remoteCall(Electron.ShowOpenDialog, {
|
||||
static browseForBotFile(): Promise<any> {
|
||||
return this.commandService.remoteCall(Electron.ShowOpenDialog, {
|
||||
buttonLabel: 'Choose file',
|
||||
filters: [
|
||||
{
|
||||
|
@ -179,7 +182,7 @@ export const ActiveBotHelper = new (class {
|
|||
});
|
||||
}
|
||||
|
||||
async confirmAndOpenBotFromFile(filename?: string): Promise<any> {
|
||||
static async confirmAndOpenBotFromFile(filename?: string): Promise<any> {
|
||||
try {
|
||||
if (!filename) {
|
||||
filename = await this.browseForBotFile();
|
||||
|
@ -188,27 +191,32 @@ export const ActiveBotHelper = new (class {
|
|||
if (filename) {
|
||||
const activeBot = getActiveBot();
|
||||
if (activeBot && activeBot.path === filename) {
|
||||
await CommandServiceImpl.call(SharedConstants.Commands.Bot.Switch, activeBot);
|
||||
await this.commandService.call(SharedConstants.Commands.Bot.Switch, activeBot);
|
||||
return;
|
||||
}
|
||||
const result = this.confirmSwitchBot();
|
||||
|
||||
if (result) {
|
||||
store.dispatch(EditorActions.closeNonGlobalTabs());
|
||||
const bot = await CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.Open, filename);
|
||||
const bot = await this.commandService.remoteCall<BotConfigWithPath>(
|
||||
SharedConstants.Commands.Bot.Open,
|
||||
filename
|
||||
);
|
||||
if (!bot) {
|
||||
return;
|
||||
}
|
||||
const state = store.getState();
|
||||
const currentUserId = state.clientAwareSettings.users.currentUserId || state.framework.userGUID;
|
||||
await CommandServiceImpl.remoteCall(SharedConstants.Commands.Emulator.SetCurrentUser, currentUserId);
|
||||
await CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.SetActive, bot);
|
||||
await CommandServiceImpl.call(SharedConstants.Commands.Bot.Load, bot);
|
||||
await this.commandService.remoteCall(SharedConstants.Commands.Emulator.SetCurrentUser, currentUserId);
|
||||
await this.commandService.remoteCall(SharedConstants.Commands.Bot.SetActive, bot);
|
||||
await this.commandService.call(SharedConstants.Commands.Bot.Load, bot);
|
||||
const numOfServices = bot.services && bot.services.length;
|
||||
CommandServiceImpl.remoteCall(Telemetry.TrackEvent, `bot_open`, {
|
||||
method: 'file_browse',
|
||||
numOfServices,
|
||||
}).catch(_e => void 0);
|
||||
this.commandService
|
||||
.remoteCall(Telemetry.TrackEvent, `bot_open`, {
|
||||
method: 'file_browse',
|
||||
numOfServices,
|
||||
})
|
||||
.catch(_e => void 0);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -221,14 +229,14 @@ export const ActiveBotHelper = new (class {
|
|||
* a livechat session.
|
||||
* @param bot The bot to be switched to. Can be a bot object with a path, or the bot path itself
|
||||
*/
|
||||
async confirmAndSwitchBots(bot: BotConfigWithPath | string): Promise<any> {
|
||||
static async confirmAndSwitchBots(bot: BotConfigWithPath | string): Promise<any> {
|
||||
const currentActiveBot = getActiveBot();
|
||||
const botPath = typeof bot === 'object' ? bot.path : bot;
|
||||
|
||||
if (currentActiveBot && currentActiveBot.path === botPath) {
|
||||
// the bot is already open, so open a new live chat tab
|
||||
try {
|
||||
await CommandServiceImpl.call(SharedConstants.Commands.Emulator.NewLiveChat, currentActiveBot.services[0]);
|
||||
await this.commandService.call(SharedConstants.Commands.Emulator.NewLiveChat, currentActiveBot.services[0]);
|
||||
} catch (e) {
|
||||
throw new Error(`[confirmAndSwitchBots] Error while trying to open bot at ${botPath}: ${e}`);
|
||||
}
|
||||
|
@ -249,7 +257,10 @@ export const ActiveBotHelper = new (class {
|
|||
let newActiveBot: BotConfigWithPath;
|
||||
if (typeof bot === 'string') {
|
||||
try {
|
||||
newActiveBot = await CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.Open, bot);
|
||||
newActiveBot = await this.commandService.remoteCall<BotConfigWithPath>(
|
||||
SharedConstants.Commands.Bot.Open,
|
||||
bot
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`[confirmAndSwitchBots] Error while trying to open bot at ${botPath}: ${e}`);
|
||||
}
|
||||
|
@ -280,7 +291,7 @@ export const ActiveBotHelper = new (class {
|
|||
|
||||
// open a livechat with the configured endpoint
|
||||
if (endpoint) {
|
||||
await CommandServiceImpl.call(SharedConstants.Commands.Emulator.NewLiveChat, endpoint);
|
||||
await this.commandService.call(SharedConstants.Commands.Emulator.NewLiveChat, endpoint);
|
||||
}
|
||||
|
||||
store.dispatch(NavBarActions.select(Constants.NAVBAR_BOT_EXPLORER));
|
||||
|
@ -294,7 +305,7 @@ export const ActiveBotHelper = new (class {
|
|||
}
|
||||
}
|
||||
|
||||
confirmAndCloseBot(): Promise<any> {
|
||||
static confirmAndCloseBot(): Promise<any> {
|
||||
const activeBot = getActiveBot();
|
||||
if (!activeBot) {
|
||||
return Promise.resolve();
|
||||
|
@ -317,4 +328,4 @@ export const ActiveBotHelper = new (class {
|
|||
throw new Error(errMsg);
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
|
|
@ -40,8 +40,8 @@ import { combineReducers, createStore } from 'redux';
|
|||
import { beginAdd } from '../../../../data/action/notificationActions';
|
||||
import { bot } from '../../../../data/reducer/bot';
|
||||
import { chat } from '../../../../data/reducer/chat';
|
||||
import { CommandServiceImpl } from '../../../../platform/commands/commandServiceImpl';
|
||||
import { ActiveBotHelper } from '../../../helpers/activeBotHelper';
|
||||
import { executeCommand } from '../../../../data/action/commandAction';
|
||||
|
||||
import { BotNotOpenExplorer } from './botNotOpenExplorer';
|
||||
import { BotNotOpenExplorerContainer } from './botNotOpenExplorerContainer';
|
||||
|
@ -55,7 +55,31 @@ jest.mock('../../../dialogs', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('./botNotOpenExplorer.scss', () => ({}));
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../data/store', () => ({
|
||||
get store() {
|
||||
return mockStore;
|
||||
|
@ -67,6 +91,7 @@ describe('The EndpointExplorer component should', () => {
|
|||
let node;
|
||||
let mockDispatch;
|
||||
let instance;
|
||||
|
||||
beforeEach(() => {
|
||||
mockDispatch = jest.spyOn(mockStore, 'dispatch');
|
||||
parent = mount(
|
||||
|
@ -79,22 +104,10 @@ describe('The EndpointExplorer component should', () => {
|
|||
});
|
||||
|
||||
it('should make the appropriate calls when onCreateNewBotClick in called', async () => {
|
||||
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'call').mockResolvedValue(true);
|
||||
await instance.onCreateNewBotClick();
|
||||
expect(commandServiceSpy).toHaveBeenLastCalledWith(SharedConstants.Commands.UI.ShowBotCreationDialog);
|
||||
});
|
||||
|
||||
it('should send a notification when onCreateNewBotClick fails', async () => {
|
||||
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'call').mockRejectedValue('oh noes!');
|
||||
await instance.onCreateNewBotClick();
|
||||
const message = `An Error occurred on the Bot Not Open Explorer: oh noes!`;
|
||||
const notification = newNotification(message);
|
||||
const action = beginAdd(notification);
|
||||
notification.timestamp = jasmine.any(Number) as any;
|
||||
notification.id = jasmine.any(String) as any;
|
||||
expect(mockDispatch).toHaveBeenLastCalledWith(action);
|
||||
|
||||
expect(commandServiceSpy).toHaveBeenLastCalledWith(SharedConstants.Commands.UI.ShowBotCreationDialog);
|
||||
expect(mockDispatch).toHaveBeenLastCalledWith(
|
||||
executeCommand(false, SharedConstants.Commands.UI.ShowBotCreationDialog)
|
||||
);
|
||||
});
|
||||
|
||||
it('should make the appropriate calls when onOpenBotFileClick in called', async () => {
|
||||
|
|
|
@ -39,8 +39,7 @@ import * as styles from './botNotOpenExplorer.scss';
|
|||
export interface BotNotOpenExplorerProps {
|
||||
hasChat?: boolean;
|
||||
openBotFile?: () => Promise<any>;
|
||||
showCreateNewBotDialog?: () => Promise<void>;
|
||||
sendNotification: (error: Error) => void;
|
||||
showCreateNewBotDialog?: () => void;
|
||||
}
|
||||
|
||||
export class BotNotOpenExplorer extends React.Component<BotNotOpenExplorerProps, {}> {
|
||||
|
@ -70,18 +69,10 @@ export class BotNotOpenExplorer extends React.Component<BotNotOpenExplorerProps,
|
|||
}
|
||||
|
||||
private onCreateNewBotClick = async () => {
|
||||
try {
|
||||
await this.props.showCreateNewBotDialog();
|
||||
} catch (e) {
|
||||
this.props.sendNotification(e);
|
||||
}
|
||||
this.props.showCreateNewBotDialog();
|
||||
};
|
||||
|
||||
private onOpenBotFileClick = async () => {
|
||||
try {
|
||||
await this.props.openBotFile();
|
||||
} catch (e) {
|
||||
this.props.sendNotification(e);
|
||||
}
|
||||
await this.props.openBotFile();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,25 +31,30 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
import { newNotification, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { connect } from 'react-redux';
|
||||
import { Action } from 'redux';
|
||||
import { newNotification } from '@bfemulator/app-shared';
|
||||
|
||||
import { beginAdd } from '../../../../data/action/notificationActions';
|
||||
import { RootState } from '../../../../data/store';
|
||||
import { CommandServiceImpl } from '../../../../platform/commands/commandServiceImpl';
|
||||
import { ActiveBotHelper } from '../../../helpers/activeBotHelper';
|
||||
import { executeCommand } from '../../../../data/action/commandAction';
|
||||
import { beginAdd } from '../../../../data/action/notificationActions';
|
||||
|
||||
import { BotNotOpenExplorer as BotNotOpenExplorerComp, BotNotOpenExplorerProps } from './botNotOpenExplorer';
|
||||
|
||||
const mapStateToProps = (state: RootState): any => ({
|
||||
hasChat: !!Object.keys(state.chat.chats).length,
|
||||
showCreateNewBotDialog: () => CommandServiceImpl.call(SharedConstants.Commands.UI.ShowBotCreationDialog),
|
||||
});
|
||||
const mapDispatchToProps = (dispatch: (action: Action) => void): BotNotOpenExplorerProps => ({
|
||||
openBotFile: () => ActiveBotHelper.confirmAndOpenBotFromFile(),
|
||||
sendNotification: (error: Error) =>
|
||||
dispatch(beginAdd(newNotification(`An Error occurred on the Bot Not Open Explorer: ${error}`))),
|
||||
openBotFile: async () => {
|
||||
try {
|
||||
await ActiveBotHelper.confirmAndOpenBotFromFile();
|
||||
} catch (e) {
|
||||
dispatch(beginAdd(newNotification(`An Error occurred on the Bot Not Open Explorer: ${e}`)));
|
||||
}
|
||||
},
|
||||
showCreateNewBotDialog: () => dispatch(executeCommand(false, SharedConstants.Commands.UI.ShowBotCreationDialog)),
|
||||
});
|
||||
|
||||
export const BotNotOpenExplorerContainer = connect(
|
||||
|
|
|
@ -164,7 +164,8 @@ describe('The ConnectedServiceEditor component should render the correct content
|
|||
"version": "0.1",
|
||||
"appId": "121221",
|
||||
"authoringKey": "poo",
|
||||
"subscriptionKey": "emoji"
|
||||
"subscriptionKey": "emoji",
|
||||
"hostname": "http://localhost"
|
||||
}`);
|
||||
const services = [
|
||||
ServiceTypes.Luis,
|
||||
|
|
|
@ -44,30 +44,17 @@ function mockProxy() {
|
|||
}
|
||||
);
|
||||
}
|
||||
jest.mock('./main.scss', () => ({}));
|
||||
jest.mock('./explorer', () => mockProxy());
|
||||
jest.mock('./mdi', () => mockProxy());
|
||||
jest.mock('./navBar', () => mockProxy());
|
||||
jest.mock('./statusBar/statusBar.scss', () => ({}));
|
||||
jest.mock('../debug/storeVisualizer.scss', () => ({}));
|
||||
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;
|
||||
},
|
||||
}));
|
||||
describe('The Main component', () => {
|
||||
it('should pass an empty test', () => {
|
||||
const parent = shallow(<Main />);
|
||||
const parent = shallow(<Main applicationMountComplete={() => void 0} />);
|
||||
expect(parent.find(Main)).not.toBe(null);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -48,6 +48,7 @@ import { NavBar } from './navBar';
|
|||
import { StatusBar } from './statusBar/statusBar';
|
||||
|
||||
export interface MainProps {
|
||||
applicationMountComplete?: () => void;
|
||||
primaryEditor?: Editor;
|
||||
secondaryEditor?: Editor;
|
||||
explorerIsVisible?: boolean;
|
||||
|
@ -77,6 +78,10 @@ export class Main extends React.Component<MainProps, MainState> {
|
|||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.applicationMountComplete();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const tabGroup1 = this.props.primaryEditor && (
|
||||
<div className={styles.mdiWrapper} key={'primaryEditor'}>
|
||||
|
|
|
@ -31,10 +31,14 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
import { connect } from 'react-redux';
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
|
||||
import * as Constants from '../../constants';
|
||||
import * as PresentationActions from '../../data/action/presentationActions';
|
||||
import { RootState } from '../../data/store';
|
||||
import { executeCommand } from '../../data/action/commandAction';
|
||||
import { showWelcomePage } from '../../data/editorHelpers';
|
||||
import { globalHandlers } from '../../utils/eventHandlers';
|
||||
|
||||
import { Main, MainProps } from './main';
|
||||
|
||||
|
@ -52,6 +56,16 @@ const mapDispatchToProps = (dispatch): MainProps => ({
|
|||
dispatch(PresentationActions.disable());
|
||||
}
|
||||
},
|
||||
applicationMountComplete: async () => {
|
||||
await new Promise(resolve => {
|
||||
dispatch(executeCommand(true, SharedConstants.Commands.ClientInit.Loaded, resolve));
|
||||
});
|
||||
showWelcomePage();
|
||||
await new Promise(resolve => {
|
||||
dispatch(executeCommand(true, SharedConstants.Commands.ClientInit.PostWelcomeScreen, resolve));
|
||||
});
|
||||
window.addEventListener('keydown', globalHandlers, true);
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -36,10 +36,10 @@ import { mount } from 'enzyme';
|
|||
import { Provider } from 'react-redux';
|
||||
import { createStore } from 'redux';
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { MouseEvent } from 'react';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { enable } from '../../../../data/action/presentationActions';
|
||||
import { setActiveTab, appendTab, splitTab } from '../../../../data/action/editorActions';
|
||||
import { appendTab, setActiveTab, splitTab } from '../../../../data/action/editorActions';
|
||||
import {
|
||||
CONTENT_TYPE_APP_SETTINGS,
|
||||
CONTENT_TYPE_LIVE_CHAT,
|
||||
|
@ -47,6 +47,7 @@ import {
|
|||
CONTENT_TYPE_TRANSCRIPT,
|
||||
CONTENT_TYPE_WELCOME_PAGE,
|
||||
} from '../../../../constants';
|
||||
import { executeCommand } from '../../../../data/action/commandAction';
|
||||
|
||||
import { TabBarContainer } from './tabBarContainer';
|
||||
import { TabBar } from './tabBar';
|
||||
|
@ -57,29 +58,45 @@ const mockTab = class Tab extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
jest.mock('./tabBar.scss', () => ({}));
|
||||
jest.mock('../../../../data/reducer/editor', () => ({
|
||||
Document: {},
|
||||
Editor: {},
|
||||
}));
|
||||
|
||||
jest.mock('../../../../data/editorHelpers', () => ({
|
||||
getTabGroupForDocument: () => null,
|
||||
getOtherTabGroup: (tabGroup: string) => (tabGroup === 'primary' ? 'secondary' : 'primary'),
|
||||
}));
|
||||
|
||||
jest.mock('../tab/tab', () => ({
|
||||
get Tab() {
|
||||
return mockTab;
|
||||
},
|
||||
}));
|
||||
|
||||
let mockRemoteCallsMade;
|
||||
jest.mock('../../../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
remoteCall: (commandName, ...args) => {
|
||||
mockRemoteCallsMade.push({ commandName, args });
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
},
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
describe('TabBar', () => {
|
||||
|
@ -88,6 +105,18 @@ describe('TabBar', () => {
|
|||
let instance;
|
||||
let mockStore;
|
||||
let mockDispatch;
|
||||
let commandService: CommandServiceImpl;
|
||||
let mockRemoteCallsMade = [];
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
commandService.remoteCall = (commandName, ...args) => {
|
||||
mockRemoteCallsMade.push({ commandName, args });
|
||||
return true as any;
|
||||
};
|
||||
commandService.call = () => true as any;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const defaultState = {
|
||||
|
@ -132,11 +161,10 @@ describe('TabBar', () => {
|
|||
|
||||
it('should enable presentation mode', async () => {
|
||||
await instance.onPresentationModeClick();
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, 'tabBar_presentationMode')
|
||||
);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(enable());
|
||||
expect(mockRemoteCallsMade).toHaveLength(1);
|
||||
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
|
||||
expect(mockRemoteCallsMade[0].args).toEqual(['tabBar_presentationMode']);
|
||||
});
|
||||
|
||||
it('should load widgets', () => {
|
||||
|
@ -189,9 +217,9 @@ describe('TabBar', () => {
|
|||
});
|
||||
|
||||
it('should handle a key press', () => {
|
||||
const mockOtherKeyPress = { key: 'a', currentTarget: { dataset: { index: 0 } } as any };
|
||||
const mockSpaceKeyPress = { key: ' ', currentTarget: { dataset: { index: 0 } } as any };
|
||||
const mockEnterKeyPress = { key: 'enter', currentTarget: { dataset: { index: 0 } } as any };
|
||||
const mockOtherKeyPress = { key: 'a', currentTarget: { dataset: { index: 0 } } as any } as any;
|
||||
const mockSpaceKeyPress = { key: ' ', currentTarget: { dataset: { index: 0 } } as any } as any;
|
||||
const mockEnterKeyPress = { key: 'enter', currentTarget: { dataset: { index: 0 } } as any } as any;
|
||||
|
||||
// simulate neither key press
|
||||
instance.handleKeyDown(mockOtherKeyPress);
|
||||
|
@ -206,18 +234,17 @@ describe('TabBar', () => {
|
|||
expect(mockDispatch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should handle a split click', () => {
|
||||
instance.onSplitClick();
|
||||
|
||||
it('should handle a split click', async () => {
|
||||
await instance.onSplitClick();
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, 'tabBar_splitTab')
|
||||
);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(splitTab('transcript', 'doc1', 'primary', 'secondary'));
|
||||
expect(mockRemoteCallsMade).toHaveLength(1);
|
||||
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
|
||||
expect(mockRemoteCallsMade[0].args).toEqual(['tabBar_splitTab']);
|
||||
});
|
||||
|
||||
it('should handle a drag enter event', () => {
|
||||
const mockPreventDefault = jest.fn(() => null);
|
||||
const mockDragEvent = { preventDefault: mockPreventDefault };
|
||||
const mockDragEvent = { preventDefault: mockPreventDefault } as any;
|
||||
|
||||
instance.onDragEnter(mockDragEvent);
|
||||
expect(mockPreventDefault).toHaveBeenCalled();
|
||||
|
|
|
@ -39,7 +39,7 @@ import { appendTab, close, setActiveTab, splitTab } from '../../../../data/actio
|
|||
import { enable as enablePresentationMode } from '../../../../data/action/presentationActions';
|
||||
import { getTabGroupForDocument } from '../../../../data/editorHelpers';
|
||||
import { RootState } from '../../../../data/store';
|
||||
import { CommandServiceImpl } from '../../../../platform/commands/commandServiceImpl';
|
||||
import { executeCommand } from '../../../../data/action/commandAction';
|
||||
|
||||
import { TabBar, TabBarProps } from './tabBar';
|
||||
|
||||
|
@ -56,13 +56,13 @@ const mapStateToProps = (state: RootState, ownProps: TabBarProps): TabBarProps =
|
|||
|
||||
const mapDispatchToProps = (dispatch): TabBarProps => ({
|
||||
splitTab: (contentType: string, documentId: string, srcEditorKey: string, destEditorKey: string) => {
|
||||
CommandServiceImpl.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, 'tabBar_splitTab').catch(_e => void 0);
|
||||
dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, 'tabBar_splitTab'));
|
||||
dispatch(splitTab(contentType, documentId, srcEditorKey, destEditorKey));
|
||||
},
|
||||
appendTab: (srcEditorKey: string, destEditorKey: string, tabId: string) =>
|
||||
dispatch(appendTab(srcEditorKey, destEditorKey, tabId)),
|
||||
enablePresentationMode: async () => {
|
||||
await CommandServiceImpl.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, 'tabBar_presentationMode');
|
||||
dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, 'tabBar_presentationMode'));
|
||||
dispatch(enablePresentationMode());
|
||||
},
|
||||
setActiveTab: (documentId: string) => dispatch(setActiveTab(documentId)),
|
||||
|
|
|
@ -35,27 +35,17 @@ import * as React from 'react';
|
|||
import { mount, shallow } from 'enzyme';
|
||||
import { createStore } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import * as Constants from '../../../constants';
|
||||
import { select } from '../../../data/action/navBarActions';
|
||||
import { open } from '../../../data/action/editorActions';
|
||||
import { showExplorer } from '../../../data/action/explorerActions';
|
||||
import { BotCommands } from '../../../commands/botCommands';
|
||||
|
||||
import { NavBarComponent as NavBar } from './navBar';
|
||||
import { NavBar as NavBarContainer } from './navBarContainer';
|
||||
|
||||
let mockRemoteCallsMade;
|
||||
jest.mock('../../../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
remoteCall: jest.fn((commandName, ...args) => {
|
||||
mockRemoteCallsMade.push({ commandName, args });
|
||||
return Promise.resolve();
|
||||
}),
|
||||
},
|
||||
}));
|
||||
jest.mock('./navBar.scss', () => ({}));
|
||||
|
||||
let mockState;
|
||||
const mockNotifications = {
|
||||
id1: { read: true },
|
||||
|
@ -68,11 +58,48 @@ jest.mock('../../../notificationManager', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
describe('<NavBar/>', () => {
|
||||
let mockDispatch;
|
||||
let wrapper;
|
||||
let instance;
|
||||
let node;
|
||||
let mockRemoteCallsMade;
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
new BotCommands();
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
commandService.remoteCall = (...args) => {
|
||||
mockRemoteCallsMade.push(args);
|
||||
return true as any;
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockState = {
|
||||
|
@ -119,11 +146,7 @@ describe('<NavBar/>', () => {
|
|||
const mockEvent = {
|
||||
currentTarget,
|
||||
};
|
||||
instance.onLinkClick(mockEvent);
|
||||
|
||||
expect(mockRemoteCallsMade).toHaveLength(1);
|
||||
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
|
||||
expect(mockRemoteCallsMade[0].args).toEqual(['navbar_selection', { selection: 'notifications' }]);
|
||||
instance.onLinkClick(mockEvent as any);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(select('navbar.notifications'));
|
||||
expect(instance.state.selection).toBe('navbar.notifications');
|
||||
});
|
||||
|
|
|
@ -39,7 +39,7 @@ import * as EditorActions from '../../../data/action/editorActions';
|
|||
import * as ExplorerActions from '../../../data/action/explorerActions';
|
||||
import * as NavBarActions from '../../../data/action/navBarActions';
|
||||
import { RootState } from '../../../data/store';
|
||||
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
|
||||
import { executeCommand } from '../../../data/action/commandAction';
|
||||
|
||||
import { NavBarComponent, NavBarProps } from './navBar';
|
||||
|
||||
|
@ -62,9 +62,8 @@ const mapDispatchToProps = (dispatch): NavBarProps => ({
|
|||
})
|
||||
);
|
||||
},
|
||||
trackEvent: (name: string, properties?: { [key: string]: any }) => {
|
||||
CommandServiceImpl.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, name, properties).catch(_e => void 0);
|
||||
},
|
||||
trackEvent: (name: string, properties?: { [key: string]: any }) =>
|
||||
dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, name, properties)),
|
||||
});
|
||||
|
||||
export const NavBar = connect(
|
||||
|
|
|
@ -32,8 +32,7 @@
|
|||
//
|
||||
|
||||
import { SharedConstants } from '@bfemulator/app-shared';
|
||||
|
||||
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { globalHandlers } from './eventHandlers';
|
||||
|
||||
|
@ -44,16 +43,43 @@ const {
|
|||
} = SharedConstants;
|
||||
|
||||
let mockLocalCommandsCalled = [];
|
||||
|
||||
jest.mock('../platform/commands/commandServiceImpl', () => ({
|
||||
CommandServiceImpl: {
|
||||
call: async (commandName: string, ...args: any[]) => {
|
||||
mockLocalCommandsCalled.push({ commandName, args: args });
|
||||
},
|
||||
},
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
ipcRenderer: new Proxy(
|
||||
{},
|
||||
{
|
||||
get(): any {
|
||||
return () => ({});
|
||||
},
|
||||
has() {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
describe('#globalHandlers', () => {
|
||||
let commandService: CommandServiceImpl;
|
||||
beforeAll(() => {
|
||||
const decorator = CommandServiceInstance();
|
||||
const descriptor = decorator({ descriptor: {} }, 'none') as any;
|
||||
commandService = descriptor.descriptor.get();
|
||||
|
||||
commandService.call = (commandName: string, ...args: any[]) => {
|
||||
mockLocalCommandsCalled.push({ commandName, args: args });
|
||||
return true as any;
|
||||
};
|
||||
});
|
||||
beforeEach(() => {
|
||||
mockLocalCommandsCalled = [];
|
||||
});
|
||||
|
@ -102,7 +128,7 @@ describe('#globalHandlers', () => {
|
|||
|
||||
it('should send a notification if a command fails', async () => {
|
||||
const event = new KeyboardEvent('keydown', { ctrlKey: true, key: 'N' });
|
||||
const spy = jest.spyOn(CommandServiceImpl, 'call').mockRejectedValueOnce('oh noes!');
|
||||
const spy = jest.spyOn(commandService, 'call').mockRejectedValueOnce('oh noes!');
|
||||
|
||||
await globalHandlers(event);
|
||||
expect(spy).toHaveBeenLastCalledWith('notification:add', {
|
||||
|
|
|
@ -32,40 +32,46 @@
|
|||
//
|
||||
|
||||
import { Notification, NotificationType, SharedConstants } from '@bfemulator/app-shared';
|
||||
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
|
||||
|
||||
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
|
||||
class EventHandlers {
|
||||
@CommandServiceInstance()
|
||||
public static commandService: CommandServiceImpl;
|
||||
|
||||
export const globalHandlers: EventListener = async (event: KeyboardEvent): Promise<any> => {
|
||||
// Meta corresponds to 'Command' on Mac
|
||||
const ctrlOrCmdPressed = event.ctrlKey || event.metaKey;
|
||||
const key = event.key.toLowerCase();
|
||||
const {
|
||||
Commands: {
|
||||
UI: { ShowBotCreationDialog, ShowOpenBotDialog },
|
||||
Notifications: { Add },
|
||||
},
|
||||
} = SharedConstants;
|
||||
public static async globalHandles(event: KeyboardEvent): Promise<any> {
|
||||
// Meta corresponds to 'Command' on Mac
|
||||
const ctrlOrCmdPressed = event.ctrlKey || event.metaKey;
|
||||
const key = event.key.toLowerCase();
|
||||
const {
|
||||
Commands: {
|
||||
UI: { ShowBotCreationDialog, ShowOpenBotDialog },
|
||||
Notifications: { Add },
|
||||
},
|
||||
} = SharedConstants;
|
||||
|
||||
let awaitable: Promise<any>;
|
||||
if (ctrlOrCmdPressed && key === 'o') {
|
||||
awaitable = CommandServiceImpl.call(ShowOpenBotDialog);
|
||||
}
|
||||
let awaitable: Promise<any>;
|
||||
if (ctrlOrCmdPressed && key === 'o') {
|
||||
awaitable = EventHandlers.commandService.call(ShowOpenBotDialog);
|
||||
}
|
||||
|
||||
if (ctrlOrCmdPressed && key === 'n') {
|
||||
awaitable = CommandServiceImpl.call(ShowBotCreationDialog);
|
||||
}
|
||||
if (ctrlOrCmdPressed && key === 'n') {
|
||||
awaitable = EventHandlers.commandService.call(ShowBotCreationDialog);
|
||||
}
|
||||
|
||||
if (awaitable) {
|
||||
// Prevents the char from showing up if an input is focused
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await awaitable;
|
||||
} catch (e) {
|
||||
await CommandServiceImpl.call(Add, {
|
||||
message: '' + e,
|
||||
type: NotificationType.Error,
|
||||
} as Notification);
|
||||
if (awaitable) {
|
||||
// Prevents the char from showing up if an input is focused
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await awaitable;
|
||||
} catch (e) {
|
||||
await EventHandlers.commandService.call(Add, {
|
||||
message: '' + e,
|
||||
type: NotificationType.Error,
|
||||
} as Notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const globalHandlers = EventHandlers.globalHandles;
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "build/dist"
|
||||
"outDir": "build/dist",
|
||||
"lib": ["dom", "es2015.proxy", "es5", "esnext", "es7"],
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "esnext"
|
||||
},
|
||||
"include": ["./src"]
|
||||
}
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче