From 9ef446af3b68eeccff276426c0825a62badebf86 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Tue, 4 Aug 2020 17:47:37 +0800 Subject: [PATCH 001/105] lu reference --- Composer/packages/server/package.json | 3 +- .../server/src/models/bot/luPublisher.ts | 22 ++-- .../server/src/models/bot/luResolver.ts | 55 ++++++++ Composer/yarn.lock | 119 ++++++++++++++++-- 4 files changed, 178 insertions(+), 21 deletions(-) create mode 100644 Composer/packages/server/src/models/bot/luResolver.ts diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index 391ea3f1fa..bf295283af 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -22,7 +22,7 @@ }, "author": "", "nodemonConfig": { - "exec": "cross-env TS_NODE_FILES=true node -r ts-node/register src/init.ts", + "exec": "cross-env TS_NODE_FILES=true node --inspect=9228 -r ts-node/register src/init.ts", "watch": [ "src" ], @@ -60,6 +60,7 @@ "@bfc/lu-languageserver": "*", "@bfc/plugin-loader": "*", "@bfc/shared": "*", + "@bfcomposer/bf-lu": "^1.4.4", "@microsoft/bf-dispatcher": "^4.10.0-preview.141651", "@microsoft/bf-lu": "^4.10.0-dev.20200728.930f45e", "archiver": "^3.0.0", diff --git a/Composer/packages/server/src/models/bot/luPublisher.ts b/Composer/packages/server/src/models/bot/luPublisher.ts index 6734b75757..1502a232c7 100644 --- a/Composer/packages/server/src/models/bot/luPublisher.ts +++ b/Composer/packages/server/src/models/bot/luPublisher.ts @@ -7,17 +7,18 @@ import { Path } from '../../utility/path'; import { IFileStorage } from '../storage/interface'; import log from '../../logger'; +import { importResolverGenerator } from './luResolver'; import { ComposerReservoirSampler } from './sampler/ReservoirSampler'; import { ComposerBootstrapSampler } from './sampler/BootstrapSampler'; // eslint-disable-next-line @typescript-eslint/no-var-requires -const crossTrainer = require('@microsoft/bf-lu/lib/parser/cross-train/crossTrainer.js'); +const crossTrainer = require('@bfcomposer/bf-lu/lib/parser/cross-train/crossTrainer.js'); // eslint-disable-next-line @typescript-eslint/no-var-requires -const luBuild = require('@microsoft/bf-lu/lib/parser/lubuild/builder.js'); +const luBuild = require('@bfcomposer/bf-lu/lib/parser/lubuild/builder.js'); // eslint-disable-next-line @typescript-eslint/no-var-requires -const LuisBuilder = require('@microsoft/bf-lu/lib/parser/luis/luisBuilder'); +const LuisBuilder = require('@bfcomposer/bf-lu/lib/parser/luis/luisBuilder'); // eslint-disable-next-line @typescript-eslint/no-var-requires -const luisToLuContent = require('@microsoft/bf-lu/lib/parser/luis/luConverter'); +const luisToLuContent = require('@bfcomposer/bf-lu/lib/parser/luis/luConverter'); const GENERATEDFOLDER = 'generated'; const INTERUPTION = 'interuption'; @@ -113,7 +114,8 @@ export class LuPublisher { return { content: file.content, id: file.name }; }); - const result = await crossTrainer.crossTrain(luContents, [], this.crossTrainConfig); + const resolver = importResolverGenerator(files); + const result = await crossTrainer.crossTrain(luContents, [], this.crossTrainConfig, resolver); await this.writeFiles(result.luResult); } @@ -162,7 +164,8 @@ export class LuPublisher { throw new Error('No LUIS files exist'); } - const loadResult = await this._loadLuContents(config.models); + const resolver = importResolverGenerator(files); + const loadResult = await this._loadLuContents(config.models, resolver); loadResult.luContents = await this.downsizeUtterances(loadResult.luContents); const authoringEndpoint = config.authoringEndpoint ?? `https://${config.region}.api.cognitive.microsoft.com`; @@ -174,6 +177,7 @@ export class LuPublisher { config.botName, config.suffix, config.fallbackLocal, + true, false, loadResult.multiRecognizers, loadResult.settings @@ -231,12 +235,14 @@ export class LuPublisher { return luConfig; }; - private _loadLuContents = async (paths: string[]) => { + private _loadLuContents = async (paths: string[], resolver) => { return await this.builder.loadContents( paths, this._locale, this.config?.environment || '', - this.config?.authoringRegion || '' + this.config?.authoringRegion || '', + null, + resolver ); }; diff --git a/Composer/packages/server/src/models/bot/luResolver.ts b/Composer/packages/server/src/models/bot/luResolver.ts new file mode 100644 index 0000000000..706326d55a --- /dev/null +++ b/Composer/packages/server/src/models/bot/luResolver.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { FileInfo } from '@bfc/shared'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const luObject = require('@bfcomposer/bf-lu/lib/parser/lu/lu.js'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const luOptions = require('@bfcomposer/bf-lu/lib/parser/lu/luOptions.js'); + +function getFileName(path: string): string { + return path.split('/').pop() || path; +} + +export function importResolverGenerator(files: FileInfo[]) { + /** + * @param srcId current file id + * @param idsToFind imported file id + * for example: + * in todosample.en-us.lu: + * [help](help.lu) + * + * would resolve to help.en-us.lu || help.lu + * + * sourceId = todosample.en-us.lu + * idsToFind = [help.lu] + * + * Overlap implemented built-in fs resolver in + * botframework-cli/packages/lu/src/parser/lu/luMerger.js#findLuFilesInDir + * Diffrence is composer support import by id, but not support * / ** file match + */ + return (srcId: string, idsToFind: any[]) => { + const ext = '.lu'; + // eslint-disable-next-line security/detect-non-literal-regexp + const extReg = new RegExp(ext + '$'); + const sourceId = getFileName(srcId).replace(extReg, ''); + const locale = /\w\.\w/.test(sourceId) ? sourceId.split('.').pop() : 'en-us'; + + const luObjects = idsToFind.map((file) => { + const fileId = file.filePath; + const targetId = getFileName(fileId).replace(extReg, ''); + + const targetFile = + files.find(({ name }) => name === `${targetId}.${locale}${ext}`) || + files.find(({ name }) => name === `${targetId}${ext}`); + + if (!targetFile) throw new Error(`File not found`); + + const options = new luOptions(targetFile.name, file.includeInCollate, locale, targetFile.path); + return new luObject(targetFile.content, options); + }); + + return luObjects; + }; +} diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 28fb96dabd..34fb347b12 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -2317,6 +2317,30 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@bfcomposer/bf-lu@^1.4.4": + version "1.4.4" + resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.4.tgz#aafc8ae7eec37c0389666683064c275c1051c0ef" + integrity sha1-qvyK5+7DfAOJZmaDBkwnXBBRwO8= + dependencies: + "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" + "@azure/ms-rest-azure-js" "2.0.1" + "@oclif/command" "~1.5.19" + "@oclif/errors" "~1.2.2" + "@types/node-fetch" "~2.5.5" + antlr4 "^4.7.2" + chalk "2.4.1" + console-stream "^0.1.1" + deep-equal "^1.0.1" + delay "^4.3.0" + fs-extra "^8.1.0" + get-stdin "^6.0.0" + globby "^10.0.1" + intercept-stdout "^0.1.2" + lodash "^4.17.19" + node-fetch "~2.6.0" + semver "^5.5.1" + tslib "^1.10.0" + "@cnakazawa/watch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" @@ -8467,7 +8491,7 @@ elegant-spinner@^1.0.1: resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= -elliptic@^6.0.0, elliptic@^6.5.3: +elliptic@^6.0.0: version "6.5.3" resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" integrity sha1-y1nrLv2vc6C9eMzXAVpirW4Pk9Y= @@ -11209,6 +11233,11 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-buffer@^1.0.2, is-buffer@^1.1.5: + version "1.1.6" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha1-76ouqdqg16suoTqXsritUf776L4= + is-buffer@^2.0.0, is-buffer@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" @@ -11462,7 +11491,7 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= -is-plain-object@^2.0.1, is-plain-object@^2.0.4: +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== @@ -12403,7 +12432,33 @@ killable@^1.0.1: resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== -kind-of@^2.0.1, kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0, kind-of@^4.0.0, kind-of@^5.0.0, kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: +kind-of@^2.0.1: + version "2.0.1" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" + integrity sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU= + dependencies: + is-buffer "^1.0.2" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha1-cpyR4thXt6QZofmqZWhcTDP1hF0= + +kind-of@^6.0.0, kind-of@^6.0.2: version "6.0.3" resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha1-B8BQNKbDSfoG4k+jWqdttFgM5N0= @@ -12828,7 +12883,12 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.15, "lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: +lodash@4.17.15: + version "4.17.15" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha1-tEf2ZwoEVbv+7dETku/zMOoJdUg= + +"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: version "4.17.19" resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha1-5I3e2+MLMyF4PFtDAfvTU7weSks= @@ -13269,6 +13329,11 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: dependencies: brace-expansion "^1.1.7" +minimist@0.0.8: + version "0.0.8" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + minimist@1.2.5, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" @@ -13349,7 +13414,14 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" -mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.2, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1: +mkdirp@0.5.1: + version "0.5.1" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1: version "0.5.5" resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha1-2Rzv1i0UNsoPQWIOJRKI1CAJne8= @@ -16804,7 +16876,17 @@ serialize-error@^5.0.0: dependencies: type-fest "^0.8.0" -serialize-javascript@^1.7.0, serialize-javascript@^2.1.2, serialize-javascript@^3.1.0: +serialize-javascript@^1.7.0: + version "1.9.1" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb" + integrity sha1-z8IArvd7YAxH2pu4FJyUPnmML9s= + +serialize-javascript@^2.1.2: + version "2.1.2" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" + integrity sha1-7OxTsOAxe9yV73arcHS3OEeF+mE= + +serialize-javascript@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.1.0.tgz#8bf3a9170712664ef2561b44b691eafe399214ea" integrity sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg== @@ -16849,12 +16931,25 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= -set-value@^0.4.3, set-value@^2.0.0, set-value@^3.0.2: - version "3.0.2" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90" - integrity sha1-dOjs0CPDPQ93GZ1BVAmkDyHmG5A= +set-value@^0.4.3: + version "0.4.3" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" + integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE= dependencies: - is-plain-object "^2.0.4" + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.1" + to-object-path "^0.3.0" + +set-value@^2.0.0: + version "2.0.1" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha1-oY1AUw5vB95CKMfe/kInr4ytAFs= + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" @@ -17259,7 +17354,7 @@ split-on-first@^1.0.0: resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== -split-string@^3.0.2: +split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== From 6b667471d480849611917aa313640cdc4945a7a2 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Tue, 11 Aug 2020 17:42:27 +0800 Subject: [PATCH 002/105] update --- Composer/packages/server/package.json | 2 +- .../server/src/models/bot/luResolver.ts | 19 ++++++++++++++++--- Composer/yarn.lock | 8 ++++---- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index bf295283af..29cb82f9ce 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -60,7 +60,7 @@ "@bfc/lu-languageserver": "*", "@bfc/plugin-loader": "*", "@bfc/shared": "*", - "@bfcomposer/bf-lu": "^1.4.4", + "@bfcomposer/bf-lu": "^1.4.5", "@microsoft/bf-dispatcher": "^4.10.0-preview.141651", "@microsoft/bf-lu": "^4.10.0-dev.20200728.930f45e", "archiver": "^3.0.0", diff --git a/Composer/packages/server/src/models/bot/luResolver.ts b/Composer/packages/server/src/models/bot/luResolver.ts index 706326d55a..2dced1b17f 100644 --- a/Composer/packages/server/src/models/bot/luResolver.ts +++ b/Composer/packages/server/src/models/bot/luResolver.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { FileInfo } from '@bfc/shared'; +import { FileInfo, ResolverResource } from '@bfc/shared'; // eslint-disable-next-line @typescript-eslint/no-var-requires const luObject = require('@bfcomposer/bf-lu/lib/parser/lu/lu.js'); @@ -11,6 +11,19 @@ const luOptions = require('@bfcomposer/bf-lu/lib/parser/lu/luOptions.js'); function getFileName(path: string): string { return path.split('/').pop() || path; } +function getBaseName(filename?: string): string | any { + if (typeof filename !== 'string') return filename; + return filename.substring(0, filename.lastIndexOf('.')) || filename; +} + +export function fileInfoToResources(files: FileInfo[]): ResolverResource[] { + return files.map((file) => { + return { + id: getBaseName(file.name), + content: file.content, + }; + }); +} export function importResolverGenerator(files: FileInfo[]) { /** @@ -23,7 +36,7 @@ export function importResolverGenerator(files: FileInfo[]) { * would resolve to help.en-us.lu || help.lu * * sourceId = todosample.en-us.lu - * idsToFind = [help.lu] + * idsToFind = [{ filePath: help.lu, includeInCollate: true}, ...] * * Overlap implemented built-in fs resolver in * botframework-cli/packages/lu/src/parser/lu/luMerger.js#findLuFilesInDir @@ -46,7 +59,7 @@ export function importResolverGenerator(files: FileInfo[]) { if (!targetFile) throw new Error(`File not found`); - const options = new luOptions(targetFile.name, file.includeInCollate, locale, targetFile.path); + const options = new luOptions(targetId, file.includeInCollate, locale, targetFile.path); return new luObject(targetFile.content, options); }); diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 34fb347b12..a79c6beda0 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -2317,10 +2317,10 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@bfcomposer/bf-lu@^1.4.4": - version "1.4.4" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.4.tgz#aafc8ae7eec37c0389666683064c275c1051c0ef" - integrity sha1-qvyK5+7DfAOJZmaDBkwnXBBRwO8= +"@bfcomposer/bf-lu@^1.4.5": + version "1.4.5" + resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.5.tgz#c913fb9835f86ab75a1b8cde29628735c373e2d2" + integrity sha1-yRP7mDX4ardaG4zeKWKHNcNz4tI= dependencies: "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" "@azure/ms-rest-azure-js" "2.0.1" From f7dd5b3ad137620a50714b438d84a3f450f80dd7 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Wed, 12 Aug 2020 14:57:00 +0800 Subject: [PATCH 003/105] update --- Composer/packages/client/src/utils/luUtil.ts | 10 ++- .../__tests__/models/bot/luResolver.test.ts | 64 +++++++++++++++++++ .../server/src/models/bot/luPublisher.ts | 6 +- .../server/src/models/bot/luResolver.ts | 9 ++- 4 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 Composer/packages/server/__tests__/models/bot/luResolver.test.ts diff --git a/Composer/packages/client/src/utils/luUtil.ts b/Composer/packages/client/src/utils/luUtil.ts index 52b91a231b..c36b230344 100644 --- a/Composer/packages/client/src/utils/luUtil.ts +++ b/Composer/packages/client/src/utils/luUtil.ts @@ -22,6 +22,10 @@ export function getReferredFiles(luFiles: LuFile[], dialogs: DialogInfo[]) { }); } +function getCommonFiles(luFiles: LuFile[]) { + return luFiles.filter((file) => getBaseName(file.id) === 'common' && file.empty === false); +} + function createConfigId(fileId) { return `${fileId}.lu`; } @@ -156,7 +160,9 @@ function generateErrorMessage(invalidLuFile: LuFile[]) { export function checkLuisPublish(luFiles: LuFile[], dialogs: DialogInfo[]) { const referred = getReferredFiles(luFiles, dialogs); - const invalidLuFile = referred.filter( + const commons = getCommonFiles(luFiles); + const filesToPublish = [...referred, ...commons]; + const invalidLuFile = filesToPublish.filter( (file) => file.diagnostics.filter((n) => n.severity === DiagnosticSeverity.Error).length !== 0 ); if (invalidLuFile.length !== 0) { @@ -169,6 +175,6 @@ export function checkLuisPublish(luFiles: LuFile[], dialogs: DialogInfo[]) { throw new Error(formatMessage(`You have the following empty LuFile(s): `) + msg); } // supported LUIS locale. - const supported = BotIndexer.filterLUISFilesToPublish(referred); + const supported = BotIndexer.filterLUISFilesToPublish(filesToPublish); return supported; } diff --git a/Composer/packages/server/__tests__/models/bot/luResolver.test.ts b/Composer/packages/server/__tests__/models/bot/luResolver.test.ts new file mode 100644 index 0000000000..e82f63dd64 --- /dev/null +++ b/Composer/packages/server/__tests__/models/bot/luResolver.test.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ResolverResource } from '@bfc/shared'; + +import { luImportResolverGenerator } from '../../../src/models/bot/luResolver'; + +const files = [ + { + id: 'common.en-us', + content: '> common.en-us.lu', + }, + { + id: 'a.en-us', + content: '> a.en-us.lu', + }, + { + id: 'b.en-us', + content: '> b.en-us.lu', + }, +] as ResolverResource[]; + +describe('Lu Resolver', () => { + const resolver = luImportResolverGenerator(files); + + it('should resolve file without locale', async () => { + const case1 = resolver('a.en-us', [{ filePath: 'common.lu', includeInCollate: true }]); + expect(case1.length).toEqual(1); + expect(case1[0]).toEqual({ + content: '> common.en-us.lu', + id: 'common', + includeInCollate: true, + language: 'en-us', + path: '', + name: 'common.en-us.lu', + }); + }); + + it('should resolve file with locale', async () => { + const case1 = resolver('a.en-us', [{ filePath: 'common.en-us.lu', includeInCollate: true }]); + expect(case1.length).toEqual(1); + expect(case1[0]).toEqual({ + content: '> common.en-us.lu', + id: 'common.en-us', + includeInCollate: true, + language: 'en-us', + path: '', + name: 'common.en-us.en-us.lu', + }); + }); + + it('should resolve file with path', async () => { + const case1 = resolver('a.en-us', [{ filePath: './common.lu', includeInCollate: true }]); + expect(case1.length).toEqual(1); + expect(case1[0]).toEqual({ + content: '> common.en-us.lu', + id: 'common', + includeInCollate: true, + language: 'en-us', + path: '', + name: 'common.en-us.lu', + }); + }); +}); diff --git a/Composer/packages/server/src/models/bot/luPublisher.ts b/Composer/packages/server/src/models/bot/luPublisher.ts index 1502a232c7..3792c94f4f 100644 --- a/Composer/packages/server/src/models/bot/luPublisher.ts +++ b/Composer/packages/server/src/models/bot/luPublisher.ts @@ -7,7 +7,7 @@ import { Path } from '../../utility/path'; import { IFileStorage } from '../storage/interface'; import log from '../../logger'; -import { importResolverGenerator } from './luResolver'; +import { luImportResolverGenerator, fileInfoToResources } from './luResolver'; import { ComposerReservoirSampler } from './sampler/ReservoirSampler'; import { ComposerBootstrapSampler } from './sampler/BootstrapSampler'; @@ -114,7 +114,7 @@ export class LuPublisher { return { content: file.content, id: file.name }; }); - const resolver = importResolverGenerator(files); + const resolver = luImportResolverGenerator(fileInfoToResources(files)); const result = await crossTrainer.crossTrain(luContents, [], this.crossTrainConfig, resolver); await this.writeFiles(result.luResult); @@ -164,7 +164,7 @@ export class LuPublisher { throw new Error('No LUIS files exist'); } - const resolver = importResolverGenerator(files); + const resolver = luImportResolverGenerator(fileInfoToResources(files)); const loadResult = await this._loadLuContents(config.models, resolver); loadResult.luContents = await this.downsizeUtterances(loadResult.luContents); const authoringEndpoint = config.authoringEndpoint ?? `https://${config.region}.api.cognitive.microsoft.com`; diff --git a/Composer/packages/server/src/models/bot/luResolver.ts b/Composer/packages/server/src/models/bot/luResolver.ts index 2dced1b17f..e27068aac2 100644 --- a/Composer/packages/server/src/models/bot/luResolver.ts +++ b/Composer/packages/server/src/models/bot/luResolver.ts @@ -25,7 +25,7 @@ export function fileInfoToResources(files: FileInfo[]): ResolverResource[] { }); } -export function importResolverGenerator(files: FileInfo[]) { +export function luImportResolverGenerator(files: ResolverResource[]) { /** * @param srcId current file id * @param idsToFind imported file id @@ -54,12 +54,11 @@ export function importResolverGenerator(files: FileInfo[]) { const targetId = getFileName(fileId).replace(extReg, ''); const targetFile = - files.find(({ name }) => name === `${targetId}.${locale}${ext}`) || - files.find(({ name }) => name === `${targetId}${ext}`); + files.find(({ id }) => id === `${targetId}.${locale}`) || files.find(({ id }) => id === `${targetId}`); - if (!targetFile) throw new Error(`File not found`); + if (!targetFile) throw new Error(`File: ${file.filePath} not found`); - const options = new luOptions(targetId, file.includeInCollate, locale, targetFile.path); + const options = new luOptions(targetId, file.includeInCollate, locale); return new luObject(targetFile.content, options); }); From 54bb8791ac46a021f940ef8965724107bd42ea1e Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Thu, 13 Aug 2020 10:55:57 +0800 Subject: [PATCH 004/105] minimal publish model --- Composer/packages/client/src/utils/luUtil.ts | 10 ++-------- .../packages/server/src/models/bot/botProject.ts | 2 +- .../server/src/models/bot/luPublisher.ts | 16 ++++++++-------- .../packages/server/src/models/bot/luResolver.ts | 4 ++++ 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Composer/packages/client/src/utils/luUtil.ts b/Composer/packages/client/src/utils/luUtil.ts index c36b230344..52b91a231b 100644 --- a/Composer/packages/client/src/utils/luUtil.ts +++ b/Composer/packages/client/src/utils/luUtil.ts @@ -22,10 +22,6 @@ export function getReferredFiles(luFiles: LuFile[], dialogs: DialogInfo[]) { }); } -function getCommonFiles(luFiles: LuFile[]) { - return luFiles.filter((file) => getBaseName(file.id) === 'common' && file.empty === false); -} - function createConfigId(fileId) { return `${fileId}.lu`; } @@ -160,9 +156,7 @@ function generateErrorMessage(invalidLuFile: LuFile[]) { export function checkLuisPublish(luFiles: LuFile[], dialogs: DialogInfo[]) { const referred = getReferredFiles(luFiles, dialogs); - const commons = getCommonFiles(luFiles); - const filesToPublish = [...referred, ...commons]; - const invalidLuFile = filesToPublish.filter( + const invalidLuFile = referred.filter( (file) => file.diagnostics.filter((n) => n.severity === DiagnosticSeverity.Error).length !== 0 ); if (invalidLuFile.length !== 0) { @@ -175,6 +169,6 @@ export function checkLuisPublish(luFiles: LuFile[], dialogs: DialogInfo[]) { throw new Error(formatMessage(`You have the following empty LuFile(s): `) + msg); } // supported LUIS locale. - const supported = BotIndexer.filterLUISFilesToPublish(filesToPublish); + const supported = BotIndexer.filterLUISFilesToPublish(referred); return supported; } diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index ca74437098..993d678a98 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -403,7 +403,7 @@ export class BotProject implements IBotProject { }); this.luPublisher.setPublishConfig(luisConfig, crossTrainConfig, this.settings.downsampling); - await this.luPublisher.publish(luFiles); + await this.luPublisher.publish(luFiles, Array.from(this.files.values())); } }; diff --git a/Composer/packages/server/src/models/bot/luPublisher.ts b/Composer/packages/server/src/models/bot/luPublisher.ts index 3792c94f4f..240961565b 100644 --- a/Composer/packages/server/src/models/bot/luPublisher.ts +++ b/Composer/packages/server/src/models/bot/luPublisher.ts @@ -7,7 +7,7 @@ import { Path } from '../../utility/path'; import { IFileStorage } from '../storage/interface'; import log from '../../logger'; -import { luImportResolverGenerator, fileInfoToResources } from './luResolver'; +import { luImportResolverGenerator, fileInfoToResources, getLUFiles } from './luResolver'; import { ComposerReservoirSampler } from './sampler/ReservoirSampler'; import { ComposerBootstrapSampler } from './sampler/BootstrapSampler'; @@ -65,14 +65,14 @@ export class LuPublisher { this._locale = locale; } - public publish = async (files: FileInfo[]) => { + public publish = async (files: FileInfo[], allFiles: FileInfo[]) => { try { await this.createGeneratedDir(); //do cross train before publish - await this.crossTrain(files); + await this.crossTrain(files, allFiles); - await this.runBuild(files); + await this.runBuild(files, allFiles); //remove the cross train result await this.cleanCrossTrain(); } catch (error) { @@ -108,13 +108,13 @@ export class LuPublisher { return this.crossTrainConfig.rootIds.length > 0; } - private async crossTrain(files: FileInfo[]) { + private async crossTrain(files: FileInfo[], allFiles: FileInfo[]) { if (!this.needCrossTrain()) return; const luContents = files.map((file) => { return { content: file.content, id: file.name }; }); - const resolver = luImportResolverGenerator(fileInfoToResources(files)); + const resolver = luImportResolverGenerator(fileInfoToResources(getLUFiles(allFiles))); const result = await crossTrainer.crossTrain(luContents, [], this.crossTrainConfig, resolver); await this.writeFiles(result.luResult); @@ -158,13 +158,13 @@ export class LuPublisher { } } - private async runBuild(files: FileInfo[]) { + private async runBuild(files: FileInfo[], allFiles: FileInfo[]) { const config = await this._getConfig(files); if (config.models.length === 0) { throw new Error('No LUIS files exist'); } - const resolver = luImportResolverGenerator(fileInfoToResources(files)); + const resolver = luImportResolverGenerator(fileInfoToResources(getLUFiles(allFiles))); const loadResult = await this._loadLuContents(config.models, resolver); loadResult.luContents = await this.downsizeUtterances(loadResult.luContents); const authoringEndpoint = config.authoringEndpoint ?? `https://${config.region}.api.cognitive.microsoft.com`; diff --git a/Composer/packages/server/src/models/bot/luResolver.ts b/Composer/packages/server/src/models/bot/luResolver.ts index e27068aac2..62038d3af5 100644 --- a/Composer/packages/server/src/models/bot/luResolver.ts +++ b/Composer/packages/server/src/models/bot/luResolver.ts @@ -16,6 +16,10 @@ function getBaseName(filename?: string): string | any { return filename.substring(0, filename.lastIndexOf('.')) || filename; } +export function getLUFiles(files: FileInfo[]): FileInfo[] { + return files.filter(({ name }) => name.endsWith('.lu')); +} + export function fileInfoToResources(files: FileInfo[]): ResolverResource[] { return files.map((file) => { return { From 6caa48cb991ae8b8557e3203e7808070d8deec96 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Thu, 20 Aug 2020 16:25:10 +0800 Subject: [PATCH 005/105] update resolver --- .../__tests__/models/bot/luResolver.test.ts | 84 ++++++++++++++++++- .../server/src/models/bot/luResolver.ts | 16 +++- 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/Composer/packages/server/__tests__/models/bot/luResolver.test.ts b/Composer/packages/server/__tests__/models/bot/luResolver.test.ts index e82f63dd64..0b153f84b6 100644 --- a/Composer/packages/server/__tests__/models/bot/luResolver.test.ts +++ b/Composer/packages/server/__tests__/models/bot/luResolver.test.ts @@ -23,7 +23,7 @@ const files = [ describe('Lu Resolver', () => { const resolver = luImportResolverGenerator(files); - it('should resolve file without locale', async () => { + it('should resolve <.lu file id> without locale', async () => { const case1 = resolver('a.en-us', [{ filePath: 'common.lu', includeInCollate: true }]); expect(case1.length).toEqual(1); expect(case1[0]).toEqual({ @@ -36,7 +36,7 @@ describe('Lu Resolver', () => { }); }); - it('should resolve file with locale', async () => { + it('should resolve <.lu file id> with locale', async () => { const case1 = resolver('a.en-us', [{ filePath: 'common.en-us.lu', includeInCollate: true }]); expect(case1.length).toEqual(1); expect(case1[0]).toEqual({ @@ -49,7 +49,7 @@ describe('Lu Resolver', () => { }); }); - it('should resolve file with path', async () => { + it('should resolve <.lu file path>', async () => { const case1 = resolver('a.en-us', [{ filePath: './common.lu', includeInCollate: true }]); expect(case1.length).toEqual(1); expect(case1[0]).toEqual({ @@ -61,4 +61,82 @@ describe('Lu Resolver', () => { name: 'common.en-us.lu', }); }); + + it('should resolve <.lu file path>#*utterances*', async () => { + const case1 = resolver('a.en-us', [{ filePath: 'common.lu#*utterances*', includeInCollate: true }]); + expect(case1.length).toEqual(1); + expect(case1[0]).toEqual({ + content: '> common.en-us.lu', + id: 'common', + includeInCollate: true, + language: 'en-us', + path: '', + name: 'common.en-us.lu', + }); + }); + + it('should resolve <.lu file path>#*patterns*', async () => { + const case1 = resolver('a.en-us', [{ filePath: 'common.lu#*patterns*', includeInCollate: true }]); + expect(case1.length).toEqual(1); + expect(case1[0]).toEqual({ + content: '> common.en-us.lu', + id: 'common', + includeInCollate: true, + language: 'en-us', + path: '', + name: 'common.en-us.lu', + }); + }); + + it('should resolve <.lu file path>#*utterancesAndPatterns*', async () => { + const case1 = resolver('a.en-us', [{ filePath: 'common.lu#*utterancesAndPatterns*', includeInCollate: true }]); + expect(case1.length).toEqual(1); + expect(case1[0]).toEqual({ + content: '> common.en-us.lu', + id: 'common', + includeInCollate: true, + language: 'en-us', + path: '', + name: 'common.en-us.lu', + }); + }); + + it('should resolve <.lu file path>#', async () => { + const case1 = resolver('a.en-us', [{ filePath: './common.lu#Help', includeInCollate: true }]); + expect(case1.length).toEqual(1); + expect(case1[0]).toEqual({ + content: '> common.en-us.lu', + id: 'common', + includeInCollate: true, + language: 'en-us', + path: '', + name: 'common.en-us.lu', + }); + }); + + it('should resolve <.lu file path>#*utterances*', async () => { + const case1 = resolver('a.en-us', [{ filePath: 'common.lu#Help*utterances*', includeInCollate: true }]); + expect(case1.length).toEqual(1); + expect(case1[0]).toEqual({ + content: '> common.en-us.lu', + id: 'common', + includeInCollate: true, + language: 'en-us', + path: '', + name: 'common.en-us.lu', + }); + }); + + it('should resolve <.lu file path>#*patterns*', async () => { + const case1 = resolver('a.en-us', [{ filePath: 'common.lu#Help*patterns*', includeInCollate: true }]); + expect(case1.length).toEqual(1); + expect(case1[0]).toEqual({ + content: '> common.en-us.lu', + id: 'common', + includeInCollate: true, + language: 'en-us', + path: '', + name: 'common.en-us.lu', + }); + }); }); diff --git a/Composer/packages/server/src/models/bot/luResolver.ts b/Composer/packages/server/src/models/bot/luResolver.ts index 62038d3af5..34cdf41860 100644 --- a/Composer/packages/server/src/models/bot/luResolver.ts +++ b/Composer/packages/server/src/models/bot/luResolver.ts @@ -46,16 +46,24 @@ export function luImportResolverGenerator(files: ResolverResource[]) { * botframework-cli/packages/lu/src/parser/lu/luMerger.js#findLuFilesInDir * Diffrence is composer support import by id, but not support * / ** file match */ + + /** + * common.lu#Help + * common.lu#*utterances* + * common.lu#*patterns* + */ + const fragmentReg = new RegExp('#.*$'); + const ext = '.lu'; + // eslint-disable-next-line security/detect-non-literal-regexp + const extReg = new RegExp(ext + '$'); + return (srcId: string, idsToFind: any[]) => { - const ext = '.lu'; - // eslint-disable-next-line security/detect-non-literal-regexp - const extReg = new RegExp(ext + '$'); const sourceId = getFileName(srcId).replace(extReg, ''); const locale = /\w\.\w/.test(sourceId) ? sourceId.split('.').pop() : 'en-us'; const luObjects = idsToFind.map((file) => { const fileId = file.filePath; - const targetId = getFileName(fileId).replace(extReg, ''); + const targetId = getFileName(fileId).replace(fragmentReg, '').replace(extReg, ''); const targetFile = files.find(({ id }) => id === `${targetId}.${locale}`) || files.find(({ id }) => id === `${targetId}`); From f9e98dc1b708fe5a1c97d2b64edfe92b32af017a Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Mon, 24 Aug 2020 15:37:59 +0800 Subject: [PATCH 006/105] update package --- Composer/packages/lib/indexers/package.json | 2 +- Composer/packages/server/package.json | 3 +-- .../language-servers/language-understanding/package.json | 2 +- Composer/plugins/azurePublish/package.json | 2 +- Composer/plugins/azurePublish/yarn.lock | 8 ++++---- Composer/yarn.lock | 8 ++++---- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Composer/packages/lib/indexers/package.json b/Composer/packages/lib/indexers/package.json index c96b7bbebf..9a7900503b 100644 --- a/Composer/packages/lib/indexers/package.json +++ b/Composer/packages/lib/indexers/package.json @@ -28,7 +28,7 @@ }, "dependencies": { "@bfc/shared": "*", - "@microsoft/bf-lu": "^4.10.0-dev.20200728.930f45e", + "@microsoft/bf-lu": "^4.11.0-dev.20200824.de66343", "adaptive-expressions": "4.10.0-preview-147186", "botbuilder-lg": "^4.10.0-preview-150886", "lodash": "^4.17.19" diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index c55e5efa9a..a3db9a744d 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -61,9 +61,8 @@ "@bfc/lu-languageserver": "*", "@bfc/plugin-loader": "*", "@bfc/shared": "*", - "@bfcomposer/bf-lu": "^1.4.5", "@microsoft/bf-dispatcher": "^4.10.0-preview.141651", - "@microsoft/bf-lu": "^4.10.0-dev.20200728.930f45e", + "@microsoft/bf-lu": "^4.11.0-dev.20200824.de66343", "archiver": "^3.0.0", "axios": "^0.19.2", "azure-storage": "^2.10.3", diff --git a/Composer/packages/tools/language-servers/language-understanding/package.json b/Composer/packages/tools/language-servers/language-understanding/package.json index 93493aa6a4..340f5fa946 100644 --- a/Composer/packages/tools/language-servers/language-understanding/package.json +++ b/Composer/packages/tools/language-servers/language-understanding/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@microsoft/bf-cli-command": "^4.10.0-preview.141651", - "@microsoft/bf-lu": "^4.10.0-dev.20200728.930f45e", + "@microsoft/bf-lu": "^4.11.0-dev.20200824.de66343", "express": "^4.15.2", "monaco-languageclient": "^0.10.0", "normalize-url": "^2.0.1", diff --git a/Composer/plugins/azurePublish/package.json b/Composer/plugins/azurePublish/package.json index 48575a89e8..c72f3dba79 100644 --- a/Composer/plugins/azurePublish/package.json +++ b/Composer/plugins/azurePublish/package.json @@ -19,7 +19,7 @@ "@azure/graph": "5.0.1", "@azure/ms-rest-browserauth": "0.1.4", "@azure/ms-rest-nodeauth": "3.0.3", - "@microsoft/bf-lu": "^4.10.0-dev.20200728.930f45e", + "@microsoft/bf-lu": "^4.11.0-dev.20200824.de66343", "@microsoft/bf-luis-cli": "^4.10.0-dev.20200721.8bb21ac", "@types/archiver": "3.1.0", "@types/fs-extra": "8.1.0", diff --git a/Composer/plugins/azurePublish/yarn.lock b/Composer/plugins/azurePublish/yarn.lock index c258b0d25d..766664ac81 100644 --- a/Composer/plugins/azurePublish/yarn.lock +++ b/Composer/plugins/azurePublish/yarn.lock @@ -195,10 +195,10 @@ semver "^5.5.1" tslib "^1.10.0" -"@microsoft/bf-lu@^4.10.0-dev.20200728.930f45e": - version "4.10.0-dev.20200728.930f45e" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-4.10.0-dev.20200728.930f45e.tgz#d8a5867d1e3028124b9e0171877b1f063b2bd08e" - integrity sha1-2KWGfR4wKBJLngFxh3sfBjsr0I4= +"@microsoft/bf-lu@^4.11.0-dev.20200824.de66343": + version "4.11.0-dev.20200824.de66343" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-4.11.0-dev.20200824.de66343.tgz#a3ca8277967e201efa9045bc94a1412f411b948e" + integrity sha1-o8qCd5Z+IB76kEW8lKFBL0EblI4= dependencies: "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" "@azure/ms-rest-azure-js" "2.0.1" diff --git a/Composer/yarn.lock b/Composer/yarn.lock index cd7b360b67..cc9bb7bf7c 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -2953,10 +2953,10 @@ semver "^5.5.1" tslib "^1.10.0" -"@microsoft/bf-lu@^4.10.0-dev.20200728.930f45e": - version "4.10.0-dev.20200728.930f45e" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-4.10.0-dev.20200728.930f45e.tgz#d8a5867d1e3028124b9e0171877b1f063b2bd08e" - integrity sha1-2KWGfR4wKBJLngFxh3sfBjsr0I4= +"@microsoft/bf-lu@^4.11.0-dev.20200824.de66343": + version "4.11.0-dev.20200824.de66343" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-4.11.0-dev.20200824.de66343.tgz#a3ca8277967e201efa9045bc94a1412f411b948e" + integrity sha1-o8qCd5Z+IB76kEW8lKFBL0EblI4= dependencies: "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" "@azure/ms-rest-azure-js" "2.0.1" From f0731150e76a7604c6095438aa8b7ecfa570e24d Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Mon, 24 Aug 2020 15:46:06 +0800 Subject: [PATCH 007/105] update --- Composer/packages/server/src/models/bot/luPublisher.ts | 8 ++++---- Composer/packages/server/src/models/bot/luResolver.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Composer/packages/server/src/models/bot/luPublisher.ts b/Composer/packages/server/src/models/bot/luPublisher.ts index 240961565b..092ab6d50f 100644 --- a/Composer/packages/server/src/models/bot/luPublisher.ts +++ b/Composer/packages/server/src/models/bot/luPublisher.ts @@ -12,13 +12,13 @@ import { ComposerReservoirSampler } from './sampler/ReservoirSampler'; import { ComposerBootstrapSampler } from './sampler/BootstrapSampler'; // eslint-disable-next-line @typescript-eslint/no-var-requires -const crossTrainer = require('@bfcomposer/bf-lu/lib/parser/cross-train/crossTrainer.js'); +const crossTrainer = require('@microsoft/bf-lu/lib/parser/cross-train/crossTrainer.js'); // eslint-disable-next-line @typescript-eslint/no-var-requires -const luBuild = require('@bfcomposer/bf-lu/lib/parser/lubuild/builder.js'); +const luBuild = require('@microsoft/bf-lu/lib/parser/lubuild/builder.js'); // eslint-disable-next-line @typescript-eslint/no-var-requires -const LuisBuilder = require('@bfcomposer/bf-lu/lib/parser/luis/luisBuilder'); +const LuisBuilder = require('@microsoft/bf-lu/lib/parser/luis/luisBuilder'); // eslint-disable-next-line @typescript-eslint/no-var-requires -const luisToLuContent = require('@bfcomposer/bf-lu/lib/parser/luis/luConverter'); +const luisToLuContent = require('@microsoft/bf-lu/lib/parser/luis/luConverter'); const GENERATEDFOLDER = 'generated'; const INTERUPTION = 'interuption'; diff --git a/Composer/packages/server/src/models/bot/luResolver.ts b/Composer/packages/server/src/models/bot/luResolver.ts index 34cdf41860..5976cf0bee 100644 --- a/Composer/packages/server/src/models/bot/luResolver.ts +++ b/Composer/packages/server/src/models/bot/luResolver.ts @@ -4,9 +4,9 @@ import { FileInfo, ResolverResource } from '@bfc/shared'; // eslint-disable-next-line @typescript-eslint/no-var-requires -const luObject = require('@bfcomposer/bf-lu/lib/parser/lu/lu.js'); +const luObject = require('@microsoft/bf-lu/lib/parser/lu/lu.js'); // eslint-disable-next-line @typescript-eslint/no-var-requires -const luOptions = require('@bfcomposer/bf-lu/lib/parser/lu/luOptions.js'); +const luOptions = require('@microsoft/bf-lu/lib/parser/lu/luOptions.js'); function getFileName(path: string): string { return path.split('/').pop() || path; From 229f87a7459a43626de7319526365d6b68286217 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Tue, 25 Aug 2020 18:21:54 +0800 Subject: [PATCH 008/105] support path based resolve --- .../__tests__/models/bot/luResolver.test.ts | 127 +++++++----------- .../server/src/models/bot/luPublisher.ts | 6 +- .../server/src/models/bot/luResolver.ts | 66 +++++++-- 3 files changed, 104 insertions(+), 95 deletions(-) diff --git a/Composer/packages/server/__tests__/models/bot/luResolver.test.ts b/Composer/packages/server/__tests__/models/bot/luResolver.test.ts index 0b153f84b6..5b3472bc83 100644 --- a/Composer/packages/server/__tests__/models/bot/luResolver.test.ts +++ b/Composer/packages/server/__tests__/models/bot/luResolver.test.ts @@ -1,142 +1,107 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ResolverResource } from '@bfc/shared'; +import { FileInfo } from '@bfc/shared'; import { luImportResolverGenerator } from '../../../src/models/bot/luResolver'; const files = [ { - id: 'common.en-us', + name: 'common.en-us.lu', content: '> common.en-us.lu', + path: '/users/foo/mybot/language-understanding/en-us/common.en-us.lu', + relativePath: 'language-understanding/en-us/common.en-us.lu', }, { - id: 'a.en-us', + name: 'a.en-us.lu', content: '> a.en-us.lu', + path: '/users/foo/mybot/dialogs/a/language-understanding/en-us/a.en-us.lu', + relativePath: 'dialogs/a/language-understanding/en-us/a.en-us.lu', }, { - id: 'b.en-us', + name: 'b.en-us.lu', content: '> b.en-us.lu', + path: '/users/foo/mybot/dialogs/b/language-understanding/en-us/b.en-us.lu', + relativePath: 'dialogs/b/language-understanding/en-us/b.en-us.lu', }, -] as ResolverResource[]; +] as FileInfo[]; describe('Lu Resolver', () => { const resolver = luImportResolverGenerator(files); - it('should resolve <.lu file id> without locale', async () => { + const resultOfCommon = { + content: '> common.en-us.lu', + id: '/users/foo/mybot/language-understanding/en-us/common.en-us.lu', + language: 'en-us', + }; + + it('should resolve ./*', async () => { + const case1 = resolver('a.en-us', [{ filePath: './*', includeInCollate: true }]); + expect(case1.length).toEqual(3); + expect(case1[0]).toMatchObject(resultOfCommon); + }); + + it('should resolve <.lu file id> no locale', async () => { const case1 = resolver('a.en-us', [{ filePath: 'common.lu', includeInCollate: true }]); expect(case1.length).toEqual(1); - expect(case1[0]).toEqual({ - content: '> common.en-us.lu', - id: 'common', - includeInCollate: true, - language: 'en-us', - path: '', - name: 'common.en-us.lu', - }); + expect(case1[0]).toMatchObject(resultOfCommon); }); it('should resolve <.lu file id> with locale', async () => { const case1 = resolver('a.en-us', [{ filePath: 'common.en-us.lu', includeInCollate: true }]); expect(case1.length).toEqual(1); - expect(case1[0]).toEqual({ - content: '> common.en-us.lu', - id: 'common.en-us', - includeInCollate: true, - language: 'en-us', - path: '', - name: 'common.en-us.en-us.lu', - }); + expect(case1[0]).toMatchObject(resultOfCommon); + }); + + it('should resolve <.lu file path> no locale', async () => { + const case1 = resolver('a.en-us', [ + { filePath: '../../../../../language-understanding/en-us/common.lu', includeInCollate: true }, + ]); + expect(case1.length).toEqual(1); + expect(case1[0]).toMatchObject(resultOfCommon); }); - it('should resolve <.lu file path>', async () => { - const case1 = resolver('a.en-us', [{ filePath: './common.lu', includeInCollate: true }]); + it('should resolve <.lu file path> with locale', async () => { + const case1 = resolver('a.en-us', [ + { filePath: '../../../../../language-understanding/en-us/common.en-us.lu', includeInCollate: true }, + ]); expect(case1.length).toEqual(1); - expect(case1[0]).toEqual({ - content: '> common.en-us.lu', - id: 'common', - includeInCollate: true, - language: 'en-us', - path: '', - name: 'common.en-us.lu', - }); + expect(case1[0]).toMatchObject(resultOfCommon); }); it('should resolve <.lu file path>#*utterances*', async () => { const case1 = resolver('a.en-us', [{ filePath: 'common.lu#*utterances*', includeInCollate: true }]); expect(case1.length).toEqual(1); - expect(case1[0]).toEqual({ - content: '> common.en-us.lu', - id: 'common', - includeInCollate: true, - language: 'en-us', - path: '', - name: 'common.en-us.lu', - }); + expect(case1[0]).toMatchObject(resultOfCommon); }); it('should resolve <.lu file path>#*patterns*', async () => { const case1 = resolver('a.en-us', [{ filePath: 'common.lu#*patterns*', includeInCollate: true }]); expect(case1.length).toEqual(1); - expect(case1[0]).toEqual({ - content: '> common.en-us.lu', - id: 'common', - includeInCollate: true, - language: 'en-us', - path: '', - name: 'common.en-us.lu', - }); + expect(case1[0]).toMatchObject(resultOfCommon); }); it('should resolve <.lu file path>#*utterancesAndPatterns*', async () => { const case1 = resolver('a.en-us', [{ filePath: 'common.lu#*utterancesAndPatterns*', includeInCollate: true }]); expect(case1.length).toEqual(1); - expect(case1[0]).toEqual({ - content: '> common.en-us.lu', - id: 'common', - includeInCollate: true, - language: 'en-us', - path: '', - name: 'common.en-us.lu', - }); + expect(case1[0]).toMatchObject(resultOfCommon); }); it('should resolve <.lu file path>#', async () => { - const case1 = resolver('a.en-us', [{ filePath: './common.lu#Help', includeInCollate: true }]); + const case1 = resolver('a.en-us', [{ filePath: 'common.lu#Help', includeInCollate: true }]); expect(case1.length).toEqual(1); - expect(case1[0]).toEqual({ - content: '> common.en-us.lu', - id: 'common', - includeInCollate: true, - language: 'en-us', - path: '', - name: 'common.en-us.lu', - }); + expect(case1[0]).toMatchObject(resultOfCommon); }); it('should resolve <.lu file path>#*utterances*', async () => { const case1 = resolver('a.en-us', [{ filePath: 'common.lu#Help*utterances*', includeInCollate: true }]); expect(case1.length).toEqual(1); - expect(case1[0]).toEqual({ - content: '> common.en-us.lu', - id: 'common', - includeInCollate: true, - language: 'en-us', - path: '', - name: 'common.en-us.lu', - }); + expect(case1[0]).toMatchObject(resultOfCommon); }); it('should resolve <.lu file path>#*patterns*', async () => { const case1 = resolver('a.en-us', [{ filePath: 'common.lu#Help*patterns*', includeInCollate: true }]); expect(case1.length).toEqual(1); - expect(case1[0]).toEqual({ - content: '> common.en-us.lu', - id: 'common', - includeInCollate: true, - language: 'en-us', - path: '', - name: 'common.en-us.lu', - }); + expect(case1[0]).toMatchObject(resultOfCommon); }); }); diff --git a/Composer/packages/server/src/models/bot/luPublisher.ts b/Composer/packages/server/src/models/bot/luPublisher.ts index 092ab6d50f..acb5594ada 100644 --- a/Composer/packages/server/src/models/bot/luPublisher.ts +++ b/Composer/packages/server/src/models/bot/luPublisher.ts @@ -7,7 +7,7 @@ import { Path } from '../../utility/path'; import { IFileStorage } from '../storage/interface'; import log from '../../logger'; -import { luImportResolverGenerator, fileInfoToResources, getLUFiles } from './luResolver'; +import { luImportResolverGenerator, getLUFiles } from './luResolver'; import { ComposerReservoirSampler } from './sampler/ReservoirSampler'; import { ComposerBootstrapSampler } from './sampler/BootstrapSampler'; @@ -114,7 +114,7 @@ export class LuPublisher { return { content: file.content, id: file.name }; }); - const resolver = luImportResolverGenerator(fileInfoToResources(getLUFiles(allFiles))); + const resolver = luImportResolverGenerator(getLUFiles(allFiles)); const result = await crossTrainer.crossTrain(luContents, [], this.crossTrainConfig, resolver); await this.writeFiles(result.luResult); @@ -164,7 +164,7 @@ export class LuPublisher { throw new Error('No LUIS files exist'); } - const resolver = luImportResolverGenerator(fileInfoToResources(getLUFiles(allFiles))); + const resolver = luImportResolverGenerator(getLUFiles(allFiles)); const loadResult = await this._loadLuContents(config.models, resolver); loadResult.luContents = await this.downsizeUtterances(loadResult.luContents); const authoringEndpoint = config.authoringEndpoint ?? `https://${config.region}.api.cognitive.microsoft.com`; diff --git a/Composer/packages/server/src/models/bot/luResolver.ts b/Composer/packages/server/src/models/bot/luResolver.ts index 5976cf0bee..6bf35f13af 100644 --- a/Composer/packages/server/src/models/bot/luResolver.ts +++ b/Composer/packages/server/src/models/bot/luResolver.ts @@ -3,6 +3,8 @@ import { FileInfo, ResolverResource } from '@bfc/shared'; +import { Path } from '../../utility/path'; + // eslint-disable-next-line @typescript-eslint/no-var-requires const luObject = require('@microsoft/bf-lu/lib/parser/lu/lu.js'); // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -16,6 +18,14 @@ function getBaseName(filename?: string): string | any { return filename.substring(0, filename.lastIndexOf('.')) || filename; } +function isWildcardPattern(str: string): boolean { + return str.endsWith('/*') || str.endsWith('/**'); +} + +function isURIContainsPath(str: string): boolean { + return str.startsWith('/') || str.startsWith('./') || str.startsWith('../'); +} + export function getLUFiles(files: FileInfo[]): FileInfo[] { return files.filter(({ name }) => name.endsWith('.lu')); } @@ -29,10 +39,10 @@ export function fileInfoToResources(files: FileInfo[]): ResolverResource[] { }); } -export function luImportResolverGenerator(files: ResolverResource[]) { +export function luImportResolverGenerator(files: FileInfo[]) { /** - * @param srcId current file id - * @param idsToFind imported file id + * @param srcId current file id + * @param idsToFind imported file id * for example: * in todosample.en-us.lu: * [help](help.lu) @@ -44,7 +54,7 @@ export function luImportResolverGenerator(files: ResolverResource[]) { * * Overlap implemented built-in fs resolver in * botframework-cli/packages/lu/src/parser/lu/luMerger.js#findLuFilesInDir - * Diffrence is composer support import by id, but not support * / ** file match + * Not only by path, composer also support import by id and without locale */ /** @@ -61,19 +71,53 @@ export function luImportResolverGenerator(files: ResolverResource[]) { const sourceId = getFileName(srcId).replace(extReg, ''); const locale = /\w\.\w/.test(sourceId) ? sourceId.split('.').pop() : 'en-us'; - const luObjects = idsToFind.map((file) => { - const fileId = file.filePath; - const targetId = getFileName(fileId).replace(fragmentReg, '').replace(extReg, ''); + const sourceFile = + files.find(({ name }) => name === `${sourceId}.${locale}${ext}`) || + files.find(({ name }) => name === `${sourceId}${ext}`); + + if (!sourceFile) throw new Error(`File: ${srcId} not found`); + + const wildcardIds = idsToFind.filter((item) => isWildcardPattern(item.filePath)); + const fileIds = idsToFind.filter((item) => !isWildcardPattern(item.filePath)); + + const luObjectFromWildCardIds = wildcardIds.reduce((prev, currValue) => { + const luObjects = files.map((item) => { + const options = new luOptions(item.path, currValue.includeInCollate, locale); + return new luObject(item.content, options); + }); + + return prev.concat(luObjects); + }, []); + + const luObjects = fileIds.map((file) => { + const targetPath = file.filePath; + const targetId = getFileName(targetPath).replace(fragmentReg, '').replace(extReg, ''); - const targetFile = - files.find(({ id }) => id === `${targetId}.${locale}`) || files.find(({ id }) => id === `${targetId}`); + let targetFile: FileInfo | undefined; + if (isURIContainsPath(targetPath)) { + // by path + const targetFullPath = Path.resolve(sourceFile.path, targetPath.replace(fragmentReg, '')); + const targetFullPath2 = Path.resolve( + sourceFile.path, + targetPath.replace(fragmentReg, '').replace(extReg, `.${locale}${ext}`) + ); + targetFile = + files.find(({ path }) => path === targetFullPath) || files.find(({ path }) => path === targetFullPath2); + } else { + // by id + targetFile = + files.find(({ name }) => name === `${targetId}.${locale}${ext}`) || + files.find(({ name }) => name === `${targetId}${ext}`); + } if (!targetFile) throw new Error(`File: ${file.filePath} not found`); - const options = new luOptions(targetId, file.includeInCollate, locale); + // lubuild use targetId index all luObjects, it should be uniq for each lu file. + // absolute file path is an idea identifier. + const options = new luOptions(targetFile.path, file.includeInCollate, locale); return new luObject(targetFile.content, options); }); - return luObjects; + return luObjects.concat(luObjectFromWildCardIds); }; } From bbec33c84612610466ce37180e2d6b8309a6a18d Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Wed, 26 Aug 2020 11:21:17 +0800 Subject: [PATCH 009/105] support wildcard match * / ** --- .../__tests__/models/bot/luResolver.test.ts | 60 ++++++++- Composer/packages/server/package.json | 1 + .../server/src/models/bot/luResolver.ts | 47 ++++--- Composer/yarn.lock | 118 ++++++++++++++++-- 4 files changed, 185 insertions(+), 41 deletions(-) diff --git a/Composer/packages/server/__tests__/models/bot/luResolver.test.ts b/Composer/packages/server/__tests__/models/bot/luResolver.test.ts index 5b3472bc83..c917696245 100644 --- a/Composer/packages/server/__tests__/models/bot/luResolver.test.ts +++ b/Composer/packages/server/__tests__/models/bot/luResolver.test.ts @@ -12,12 +12,24 @@ const files = [ path: '/users/foo/mybot/language-understanding/en-us/common.en-us.lu', relativePath: 'language-understanding/en-us/common.en-us.lu', }, + { + name: 'greeting.en-us.lu', + content: '> greeting.en-us.lu', + path: '/users/foo/mybot/language-understanding/en-us/greeting.en-us.lu', + relativePath: 'language-understanding/en-us/greeting.en-us.lu', + }, { name: 'a.en-us.lu', content: '> a.en-us.lu', path: '/users/foo/mybot/dialogs/a/language-understanding/en-us/a.en-us.lu', relativePath: 'dialogs/a/language-understanding/en-us/a.en-us.lu', }, + { + name: 'aa.en-us.lu', + content: '> aa.en-us.lu', + path: '/users/foo/mybot/dialogs/a/language-understanding/en-us/imports/aa.en-us.lu', + relativePath: 'dialogs/a/language-understanding/en-us/imports/aa.en-us.lu', + }, { name: 'b.en-us.lu', content: '> b.en-us.lu', @@ -29,16 +41,58 @@ const files = [ describe('Lu Resolver', () => { const resolver = luImportResolverGenerator(files); + const resultOfA = { + content: '> a.en-us.lu', + id: '/users/foo/mybot/dialogs/a/language-understanding/en-us/a.en-us.lu', + language: 'en-us', + }; + + const resultOfAA = { + content: '> aa.en-us.lu', + id: '/users/foo/mybot/dialogs/a/language-understanding/en-us/imports/aa.en-us.lu', + language: 'en-us', + }; + const resultOfCommon = { content: '> common.en-us.lu', id: '/users/foo/mybot/language-understanding/en-us/common.en-us.lu', language: 'en-us', }; + const resultOfGreeting = { + content: '> greeting.en-us.lu', + id: '/users/foo/mybot/language-understanding/en-us/greeting.en-us.lu', + language: 'en-us', + }; it('should resolve ./*', async () => { const case1 = resolver('a.en-us', [{ filePath: './*', includeInCollate: true }]); - expect(case1.length).toEqual(3); + expect(case1.length).toEqual(1); + expect(case1[0]).toMatchObject(resultOfA); + }); + + it('should resolve ./**', async () => { + const case1 = resolver('a.en-us', [{ filePath: './**', includeInCollate: true }]); + expect(case1.length).toEqual(2); + expect(case1[0]).toMatchObject(resultOfA); + expect(case1[1]).toMatchObject(resultOfAA); + }); + + it('should resolve <.lu file path>/*', async () => { + const case1 = resolver('a.en-us', [ + { filePath: '../../../../language-understanding/en-us/*', includeInCollate: true }, + ]); + expect(case1.length).toEqual(2); + expect(case1[0]).toMatchObject(resultOfCommon); + expect(case1[1]).toMatchObject(resultOfGreeting); + }); + + it('should resolve <.lu file path>/**', async () => { + const case1 = resolver('a.en-us', [ + { filePath: '../../../../language-understanding/en-us/**', includeInCollate: true }, + ]); + expect(case1.length).toEqual(2); expect(case1[0]).toMatchObject(resultOfCommon); + expect(case1[1]).toMatchObject(resultOfGreeting); }); it('should resolve <.lu file id> no locale', async () => { @@ -55,7 +109,7 @@ describe('Lu Resolver', () => { it('should resolve <.lu file path> no locale', async () => { const case1 = resolver('a.en-us', [ - { filePath: '../../../../../language-understanding/en-us/common.lu', includeInCollate: true }, + { filePath: '../../../../language-understanding/en-us/common.lu', includeInCollate: true }, ]); expect(case1.length).toEqual(1); expect(case1[0]).toMatchObject(resultOfCommon); @@ -63,7 +117,7 @@ describe('Lu Resolver', () => { it('should resolve <.lu file path> with locale', async () => { const case1 = resolver('a.en-us', [ - { filePath: '../../../../../language-understanding/en-us/common.en-us.lu', includeInCollate: true }, + { filePath: '../../../../language-understanding/en-us/common.en-us.lu', includeInCollate: true }, ]); expect(case1.length).toEqual(1); expect(case1[0]).toMatchObject(resultOfCommon); diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index a3db9a744d..0549898baf 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -86,6 +86,7 @@ "luis-apis": "2.5.1", "minimatch": "^3.0.4", "morgan": "^1.9.1", + "multimatch": "^4.0.0", "passport": "^0.4.1", "path-to-regexp": "^6.1.0", "portfinder": "1.0.25", diff --git a/Composer/packages/server/src/models/bot/luResolver.ts b/Composer/packages/server/src/models/bot/luResolver.ts index 6bf35f13af..edb14c0bc4 100644 --- a/Composer/packages/server/src/models/bot/luResolver.ts +++ b/Composer/packages/server/src/models/bot/luResolver.ts @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { FileInfo, ResolverResource } from '@bfc/shared'; +import { FileInfo } from '@bfc/shared'; +import multimatch from 'multimatch'; import { Path } from '../../utility/path'; @@ -10,14 +11,6 @@ const luObject = require('@microsoft/bf-lu/lib/parser/lu/lu.js'); // eslint-disable-next-line @typescript-eslint/no-var-requires const luOptions = require('@microsoft/bf-lu/lib/parser/lu/luOptions.js'); -function getFileName(path: string): string { - return path.split('/').pop() || path; -} -function getBaseName(filename?: string): string | any { - if (typeof filename !== 'string') return filename; - return filename.substring(0, filename.lastIndexOf('.')) || filename; -} - function isWildcardPattern(str: string): boolean { return str.endsWith('/*') || str.endsWith('/**'); } @@ -26,19 +19,19 @@ function isURIContainsPath(str: string): boolean { return str.startsWith('/') || str.startsWith('./') || str.startsWith('../'); } -export function getLUFiles(files: FileInfo[]): FileInfo[] { - return files.filter(({ name }) => name.endsWith('.lu')); -} +function globMatch(files: FileInfo[], sourceFileDir: string, targetPath: string): FileInfo[] { + const targetFullPath = Path.resolve(sourceFileDir, targetPath); -export function fileInfoToResources(files: FileInfo[]): ResolverResource[] { - return files.map((file) => { - return { - id: getBaseName(file.name), - content: file.content, - }; + return files.filter((item) => { + const match = multimatch([item.path], targetFullPath); + return match.length; }); } +export function getLUFiles(files: FileInfo[]): FileInfo[] { + return files.filter(({ name }) => name.endsWith('.lu')); +} + export function luImportResolverGenerator(files: FileInfo[]) { /** * @param srcId current file id @@ -68,7 +61,7 @@ export function luImportResolverGenerator(files: FileInfo[]) { const extReg = new RegExp(ext + '$'); return (srcId: string, idsToFind: any[]) => { - const sourceId = getFileName(srcId).replace(extReg, ''); + const sourceId = Path.basename(srcId).replace(extReg, ''); const locale = /\w\.\w/.test(sourceId) ? sourceId.split('.').pop() : 'en-us'; const sourceFile = @@ -76,13 +69,17 @@ export function luImportResolverGenerator(files: FileInfo[]) { files.find(({ name }) => name === `${sourceId}${ext}`); if (!sourceFile) throw new Error(`File: ${srcId} not found`); + const sourceFileDir = Path.dirname(sourceFile.path); const wildcardIds = idsToFind.filter((item) => isWildcardPattern(item.filePath)); const fileIds = idsToFind.filter((item) => !isWildcardPattern(item.filePath)); - const luObjectFromWildCardIds = wildcardIds.reduce((prev, currValue) => { - const luObjects = files.map((item) => { - const options = new luOptions(item.path, currValue.includeInCollate, locale); + const luObjectFromWildCardIds = wildcardIds.reduce((prev, file) => { + const targetPath = file.filePath; + const referdFiles = globMatch(files, sourceFileDir, targetPath); + + const luObjects = referdFiles.map((item) => { + const options = new luOptions(item.path, file.includeInCollate, locale); return new luObject(item.content, options); }); @@ -91,14 +88,14 @@ export function luImportResolverGenerator(files: FileInfo[]) { const luObjects = fileIds.map((file) => { const targetPath = file.filePath; - const targetId = getFileName(targetPath).replace(fragmentReg, '').replace(extReg, ''); + const targetId = Path.basename(targetPath).replace(fragmentReg, '').replace(extReg, ''); let targetFile: FileInfo | undefined; if (isURIContainsPath(targetPath)) { // by path - const targetFullPath = Path.resolve(sourceFile.path, targetPath.replace(fragmentReg, '')); + const targetFullPath = Path.resolve(sourceFileDir, targetPath.replace(fragmentReg, '')); const targetFullPath2 = Path.resolve( - sourceFile.path, + sourceFileDir, targetPath.replace(fragmentReg, '').replace(extReg, `.${locale}${ext}`) ); targetFile = diff --git a/Composer/yarn.lock b/Composer/yarn.lock index cc9bb7bf7c..e64e7c2f10 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -3631,7 +3631,7 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== -"@types/minimatch@*", "@types/minimatch@3.0.3": +"@types/minimatch@*", "@types/minimatch@3.0.3", "@types/minimatch@^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== @@ -4816,6 +4816,11 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= +array-differ@^3.0.0: + version "3.0.0" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" + integrity sha1-PLs9DzFoEOr8xHYkc0I31q7krms= + array-filter@~0.0.0: version "0.0.1" resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" @@ -4893,6 +4898,11 @@ arrify@^1.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= +arrify@^2.0.1: + version "2.0.1" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha1-yWVekzHgq81YjSp8rX6ZVvZnAfo= + asap@~2.0.3, asap@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -8484,7 +8494,7 @@ elegant-spinner@^1.0.1: resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= -elliptic@^6.0.0, elliptic@^6.5.3: +elliptic@^6.0.0: version "6.5.3" resolved "https://botbuilder.myget.org/F/botframework-cli/npm/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" integrity sha1-y1nrLv2vc6C9eMzXAVpirW4Pk9Y= @@ -11231,6 +11241,11 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-buffer@^1.0.2, is-buffer@^1.1.5: + version "1.1.6" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha1-76ouqdqg16suoTqXsritUf776L4= + is-buffer@^2.0.0, is-buffer@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" @@ -11484,7 +11499,7 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= -is-plain-object@^2.0.1, is-plain-object@^2.0.4: +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== @@ -12425,7 +12440,33 @@ killable@^1.0.1: resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== -kind-of@^2.0.1, kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0, kind-of@^4.0.0, kind-of@^5.0.0, kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: +kind-of@^2.0.1: + version "2.0.1" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" + integrity sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU= + dependencies: + is-buffer "^1.0.2" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha1-cpyR4thXt6QZofmqZWhcTDP1hF0= + +kind-of@^6.0.0, kind-of@^6.0.2: version "6.0.3" resolved "https://botbuilder.myget.org/F/botframework-cli/npm/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha1-B8BQNKbDSfoG4k+jWqdttFgM5N0= @@ -12850,7 +12891,12 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.15, "lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: +lodash@4.17.15: + version "4.17.15" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha1-tEf2ZwoEVbv+7dETku/zMOoJdUg= + +"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: version "4.17.20" resolved "https://botbuilder.myget.org/F/botframework-cli/npm/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI= @@ -13291,6 +13337,11 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: dependencies: brace-expansion "^1.1.7" +minimist@0.0.8: + version "0.0.8" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + minimist@1.2.5, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" @@ -13371,7 +13422,14 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" -mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.2, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1: +mkdirp@0.5.1: + version "0.5.1" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1: version "0.5.5" resolved "https://botbuilder.myget.org/F/botframework-cli/npm/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha1-2Rzv1i0UNsoPQWIOJRKI1CAJne8= @@ -13478,6 +13536,17 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" +multimatch@^4.0.0: + version "4.0.0" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3" + integrity sha1-jDwPbj6ESa2grz3SnvtJGjdRkbM= + dependencies: + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" + minimatch "^3.0.4" + mute-stream@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" @@ -16831,7 +16900,17 @@ serialize-error@^5.0.0: dependencies: type-fest "^0.8.0" -serialize-javascript@^1.7.0, serialize-javascript@^2.1.2, serialize-javascript@^3.1.0: +serialize-javascript@^1.7.0: + version "1.9.1" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb" + integrity sha1-z8IArvd7YAxH2pu4FJyUPnmML9s= + +serialize-javascript@^2.1.2: + version "2.1.2" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" + integrity sha1-7OxTsOAxe9yV73arcHS3OEeF+mE= + +serialize-javascript@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.1.0.tgz#8bf3a9170712664ef2561b44b691eafe399214ea" integrity sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg== @@ -16876,12 +16955,25 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= -set-value@^0.4.3, set-value@^2.0.0, set-value@^3.0.2: - version "3.0.2" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90" - integrity sha1-dOjs0CPDPQ93GZ1BVAmkDyHmG5A= +set-value@^0.4.3: + version "0.4.3" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" + integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE= dependencies: - is-plain-object "^2.0.4" + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.1" + to-object-path "^0.3.0" + +set-value@^2.0.0: + version "2.0.1" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha1-oY1AUw5vB95CKMfe/kInr4ytAFs= + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" @@ -17286,7 +17378,7 @@ split-on-first@^1.0.0: resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== -split-string@^3.0.2: +split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== From 609773c1ec02d69f57c1ca2fb84c210730b38557 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Wed, 26 Aug 2020 12:02:00 +0800 Subject: [PATCH 010/105] update --- Composer/packages/server/src/models/bot/builder.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Composer/packages/server/src/models/bot/builder.ts b/Composer/packages/server/src/models/bot/builder.ts index 097f1eb6f4..2fb83b54f3 100644 --- a/Composer/packages/server/src/models/bot/builder.ts +++ b/Composer/packages/server/src/models/bot/builder.ts @@ -202,12 +202,14 @@ export class Builder { // if (config.models.length === 0) { // throw new Error('No LUIS files exist'); // } - + const resolver = luImportResolverGenerator(getLUFiles(allFiles)); const loadResult = await this.luBuilder.loadContents( config.models, config.fallbackLocal, config.suffix, - config.region + config.region, + null, + resolver ); loadResult.luContents = await this.downsizeUtterances(loadResult.luContents); const authoringEndpoint = config.authoringEndpoint ?? `https://${config.region}.api.cognitive.microsoft.com`; From 99355fcc020000091b1ced680458d9e4148f22af Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Wed, 26 Aug 2020 14:18:12 +0800 Subject: [PATCH 011/105] resolver support .qna file ext --- .../__tests__/models/bot/luResolver.test.ts | 4 +- .../packages/server/src/models/bot/builder.ts | 6 +- .../server/src/models/bot/luResolver.ts | 3 +- Composer/yarn.lock | 95 +++---------------- 4 files changed, 18 insertions(+), 90 deletions(-) diff --git a/Composer/packages/server/__tests__/models/bot/luResolver.test.ts b/Composer/packages/server/__tests__/models/bot/luResolver.test.ts index c917696245..2a68ae0e0a 100644 --- a/Composer/packages/server/__tests__/models/bot/luResolver.test.ts +++ b/Composer/packages/server/__tests__/models/bot/luResolver.test.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { FileInfo } from '@bfc/shared'; +import { FileInfo, FileExtensions } from '@bfc/shared'; import { luImportResolverGenerator } from '../../../src/models/bot/luResolver'; @@ -39,7 +39,7 @@ const files = [ ] as FileInfo[]; describe('Lu Resolver', () => { - const resolver = luImportResolverGenerator(files); + const resolver = luImportResolverGenerator(files, FileExtensions.Lu); const resultOfA = { content: '> a.en-us.lu', diff --git a/Composer/packages/server/src/models/bot/builder.ts b/Composer/packages/server/src/models/bot/builder.ts index 2fb83b54f3..65fa41b15f 100644 --- a/Composer/packages/server/src/models/bot/builder.ts +++ b/Composer/packages/server/src/models/bot/builder.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { FileInfo, IConfig } from '@bfc/shared'; +import { FileInfo, IConfig, FileExtensions } from '@bfc/shared'; import { Path } from '../../utility/path'; import { IFileStorage } from '../storage/interface'; @@ -127,7 +127,7 @@ export class Builder { const qnaContents = qnaFiles.map((file) => { return { content: file.content, id: file.name }; }); - const resolver = luImportResolverGenerator(getLUFiles(allFiles)); + const resolver = luImportResolverGenerator(getLUFiles(allFiles), FileExtensions.Lu); const result = await crossTrainer.crossTrain(luContents, qnaContents, this.crossTrainConfig, resolver); await this.writeFiles(result.luResult); @@ -202,7 +202,7 @@ export class Builder { // if (config.models.length === 0) { // throw new Error('No LUIS files exist'); // } - const resolver = luImportResolverGenerator(getLUFiles(allFiles)); + const resolver = luImportResolverGenerator(getLUFiles(allFiles), FileExtensions.Lu); const loadResult = await this.luBuilder.loadContents( config.models, config.fallbackLocal, diff --git a/Composer/packages/server/src/models/bot/luResolver.ts b/Composer/packages/server/src/models/bot/luResolver.ts index edb14c0bc4..ec1e313510 100644 --- a/Composer/packages/server/src/models/bot/luResolver.ts +++ b/Composer/packages/server/src/models/bot/luResolver.ts @@ -32,7 +32,7 @@ export function getLUFiles(files: FileInfo[]): FileInfo[] { return files.filter(({ name }) => name.endsWith('.lu')); } -export function luImportResolverGenerator(files: FileInfo[]) { +export function luImportResolverGenerator(files: FileInfo[], ext: string) { /** * @param srcId current file id * @param idsToFind imported file id @@ -56,7 +56,6 @@ export function luImportResolverGenerator(files: FileInfo[]) { * common.lu#*patterns* */ const fragmentReg = new RegExp('#.*$'); - const ext = '.lu'; // eslint-disable-next-line security/detect-non-literal-regexp const extReg = new RegExp(ext + '$'); diff --git a/Composer/yarn.lock b/Composer/yarn.lock index e64e7c2f10..30b6f6c5d1 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -8494,7 +8494,7 @@ elegant-spinner@^1.0.1: resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= -elliptic@^6.0.0: +elliptic@^6.0.0, elliptic@^6.5.3: version "6.5.3" resolved "https://botbuilder.myget.org/F/botframework-cli/npm/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" integrity sha1-y1nrLv2vc6C9eMzXAVpirW4Pk9Y= @@ -11241,11 +11241,6 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-buffer@^1.0.2, is-buffer@^1.1.5: - version "1.1.6" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha1-76ouqdqg16suoTqXsritUf776L4= - is-buffer@^2.0.0, is-buffer@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" @@ -11499,7 +11494,7 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= -is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: +is-plain-object@^2.0.1, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== @@ -12440,33 +12435,7 @@ killable@^1.0.1: resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== -kind-of@^2.0.1: - version "2.0.1" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" - integrity sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU= - dependencies: - is-buffer "^1.0.2" - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha1-cpyR4thXt6QZofmqZWhcTDP1hF0= - -kind-of@^6.0.0, kind-of@^6.0.2: +kind-of@^2.0.1, kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0, kind-of@^4.0.0, kind-of@^5.0.0, kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" resolved "https://botbuilder.myget.org/F/botframework-cli/npm/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha1-B8BQNKbDSfoG4k+jWqdttFgM5N0= @@ -12891,12 +12860,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.15: - version "4.17.15" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha1-tEf2ZwoEVbv+7dETku/zMOoJdUg= - -"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: +lodash@4.17.15, "lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: version "4.17.20" resolved "https://botbuilder.myget.org/F/botframework-cli/npm/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI= @@ -13337,11 +13301,6 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: dependencies: brace-expansion "^1.1.7" -minimist@0.0.8: - version "0.0.8" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - minimist@1.2.5, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" @@ -13422,14 +13381,7 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" -mkdirp@0.5.1: - version "0.5.1" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - -mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1: +mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.2, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1: version "0.5.5" resolved "https://botbuilder.myget.org/F/botframework-cli/npm/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha1-2Rzv1i0UNsoPQWIOJRKI1CAJne8= @@ -16900,17 +16852,7 @@ serialize-error@^5.0.0: dependencies: type-fest "^0.8.0" -serialize-javascript@^1.7.0: - version "1.9.1" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb" - integrity sha1-z8IArvd7YAxH2pu4FJyUPnmML9s= - -serialize-javascript@^2.1.2: - version "2.1.2" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" - integrity sha1-7OxTsOAxe9yV73arcHS3OEeF+mE= - -serialize-javascript@^3.1.0: +serialize-javascript@^1.7.0, serialize-javascript@^2.1.2, serialize-javascript@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.1.0.tgz#8bf3a9170712664ef2561b44b691eafe399214ea" integrity sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg== @@ -16955,25 +16897,12 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= -set-value@^0.4.3: - version "0.4.3" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" - integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE= - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.1" - to-object-path "^0.3.0" - -set-value@^2.0.0: - version "2.0.1" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha1-oY1AUw5vB95CKMfe/kInr4ytAFs= +set-value@^0.4.3, set-value@^2.0.0, set-value@^3.0.2: + version "3.0.2" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90" + integrity sha1-dOjs0CPDPQ93GZ1BVAmkDyHmG5A= dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" + is-plain-object "^2.0.4" setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" @@ -17378,7 +17307,7 @@ split-on-first@^1.0.0: resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== -split-string@^3.0.1, split-string@^3.0.2: +split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== From 8bc0f814f20de80529f93615107925c45b43b8c4 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Wed, 26 Aug 2020 15:41:51 +0800 Subject: [PATCH 012/105] define source qna in bot structure --- .../packages/lib/shared/src/types/indexers.ts | 1 + .../__tests__/models/bot/botStructure.test.ts | 68 +++++++++++++++---- .../server/src/models/bot/botStructure.ts | 29 +++++++- 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/Composer/packages/lib/shared/src/types/indexers.ts b/Composer/packages/lib/shared/src/types/indexers.ts index 7cf3f581f6..5ce22c3423 100644 --- a/Composer/packages/lib/shared/src/types/indexers.ts +++ b/Composer/packages/lib/shared/src/types/indexers.ts @@ -13,6 +13,7 @@ export enum FileExtensions { Lu = '.lu', Lg = '.lg', Qna = '.qna', + SourceQnA = '.source.qna', Setting = 'appsettings.json', } diff --git a/Composer/packages/server/__tests__/models/bot/botStructure.test.ts b/Composer/packages/server/__tests__/models/bot/botStructure.test.ts index 7e56c70ad7..204449d2c8 100644 --- a/Composer/packages/server/__tests__/models/bot/botStructure.test.ts +++ b/Composer/packages/server/__tests__/models/bot/botStructure.test.ts @@ -1,26 +1,26 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { defaultFilePath, parseFileName } from '../../../src/models/bot/botStructure'; +import { defaultFilePath, parseFileName, parseSourceFileName } from '../../../src/models/bot/botStructure'; const botName = 'Mybot'; const defaultLocale = 'en-us'; describe('Bot structure file path', () => { // entry dialog - it('should get entry dialog file path', async () => { + it('should get entry .dialog file path', async () => { const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.dialog'); expect(targetPath).toEqual('mybot.dialog'); }); // common.lg - it('should get common lg file path', async () => { + it('should get common.lg file path', async () => { const targetPath = defaultFilePath(botName, defaultLocale, 'common.en-us.lg'); expect(targetPath).toEqual('language-generation/en-us/common.en-us.lg'); }); // common.zh-cn.lg - it('should get common lg file path', async () => { + it('should get common..lg file path', async () => { const targetPath = defaultFilePath(botName, defaultLocale, 'common.zh-cn.lg'); expect(targetPath).toEqual('language-generation/zh-cn/common.zh-cn.lg'); }); @@ -32,19 +32,19 @@ describe('Bot structure file path', () => { }); // mybot.en-us.lg - it('should get entry dialog-lg file path', async () => { + it('should get entry dialog.lg file path', async () => { const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.en-us.lg'); expect(targetPath).toEqual('language-generation/en-us/mybot.en-us.lg'); }); // mybot.zh-cn.lg - it('should get entry dialog-lg file path', async () => { + it('should get entry dialog.lg file path', async () => { const targetPath = defaultFilePath('my-bot', defaultLocale, 'my-bot.zh-cn.lg'); expect(targetPath).toEqual('language-generation/zh-cn/my-bot.zh-cn.lg'); }); // entry dialog's lu - it('should get entry dialog-lu file path', async () => { + it('should get entry dialog.lu file path', async () => { const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.en-us.lu'); expect(targetPath).toEqual('language-understanding/en-us/mybot.en-us.lu'); }); @@ -56,11 +56,17 @@ describe('Bot structure file path', () => { }); // entry dialog's qna - it('should get entry dialog-qna file path', async () => { + it('should get entry dialog.qna file path', async () => { const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.en-us.qna'); expect(targetPath).toEqual('knowledge-base/en-us/mybot.en-us.qna'); }); + // entry dialog's source qna + it('should get entry dialog.source.qna file path', async () => { + const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.myimport1.source.qna'); + expect(targetPath).toEqual('knowledge-base/source/myimport1.source.qna'); + }); + // child dialog's lg it('should get child dialog-lg file path', async () => { const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.en-us.lg'); @@ -78,6 +84,12 @@ describe('Bot structure file path', () => { const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.en-us.qna'); expect(targetPath).toEqual('dialogs/greeting/knowledge-base/en-us/greeting.en-us.qna'); }); + + // child dialog's source qna + it('should get child dialog.source.qna file path', async () => { + const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.myimport1.source.qna'); + expect(targetPath).toEqual('dialogs/greeting/knowledge-base/source/myimport1.source.qna'); + }); }); describe('Parse file name', () => { @@ -90,7 +102,7 @@ describe('Parse file name', () => { }); // entry dialog's lg - it('should parse entry dialog-lg file name', async () => { + it('should parse entry dialog.lg file name', async () => { const { fileId, locale, fileType } = parseFileName('mybot.en-us.lg', defaultLocale); expect(fileId).toEqual('mybot'); expect(locale).toEqual('en-us'); @@ -98,13 +110,29 @@ describe('Parse file name', () => { }); // entry dialog's lu - it('should parse entry dialog-lu file name', async () => { + it('should parse entry dialog.lu file name', async () => { const { fileId, locale, fileType } = parseFileName('mybot.en-us.lu', defaultLocale); expect(fileId).toEqual('mybot'); expect(locale).toEqual('en-us'); expect(fileType).toEqual('.lu'); }); + // entry dialog's qna + it('should parse entry dialog.qna file name', async () => { + const { fileId, locale, fileType } = parseFileName('mybot.en-us.qna', defaultLocale); + expect(fileId).toEqual('mybot'); + expect(locale).toEqual('en-us'); + expect(fileType).toEqual('.qna'); + }); + + // entry dialog's source qna + it('should parse entry dialog.qna file name', async () => { + const { fileId, dialogId, fileType } = parseSourceFileName('mybot.myimport1.source.qna'); + expect(dialogId).toEqual('mybot'); + expect(fileId).toEqual('myimport1'); + expect(fileType).toEqual('.source.qna'); + }); + // child dialog it('should parse child dialog file name', async () => { const { fileId, locale, fileType } = parseFileName('greeting.dialog', defaultLocale); @@ -114,7 +142,7 @@ describe('Parse file name', () => { }); // child dialog's lg - it('should parse entry dialog-lg file name', async () => { + it('should parse child dialog.lg file name', async () => { const { fileId, locale, fileType } = parseFileName('greeting.zh-cn.lg', defaultLocale); expect(fileId).toEqual('greeting'); expect(locale).toEqual('zh-cn'); @@ -122,10 +150,26 @@ describe('Parse file name', () => { }); // child dialog's lu - it('should parse entry dialog-lu file name', async () => { + it('should parse child dialog.lu file name', async () => { const { fileId, locale, fileType } = parseFileName('greeting.en-us.lu', defaultLocale); expect(fileId).toEqual('greeting'); expect(locale).toEqual('en-us'); expect(fileType).toEqual('.lu'); }); + + // child dialog's qna + it('should parse child dialog.qna file name', async () => { + const { fileId, locale, fileType } = parseFileName('greeting.en-us.qna', defaultLocale); + expect(fileId).toEqual('greeting'); + expect(locale).toEqual('en-us'); + expect(fileType).toEqual('.qna'); + }); + + // child dialog's source qna + it('should parse child dialog.qna file name', async () => { + const { fileId, dialogId, fileType } = parseSourceFileName('greeting.myimport1.source.qna'); + expect(dialogId).toEqual('greeting'); + expect(fileId).toEqual('myimport1'); + expect(fileType).toEqual('.source.qna'); + }); }); diff --git a/Composer/packages/server/src/models/bot/botStructure.ts b/Composer/packages/server/src/models/bot/botStructure.ts index 319c93ac87..54f89ded72 100644 --- a/Composer/packages/server/src/models/bot/botStructure.ts +++ b/Composer/packages/server/src/models/bot/botStructure.ts @@ -11,6 +11,7 @@ const BotStructureTemplate = { lg: 'language-generation/${LOCALE}/${BOTNAME}.${LOCALE}.lg', lu: 'language-understanding/${LOCALE}/${BOTNAME}.${LOCALE}.lu', qna: 'knowledge-base/en-us/${BOTNAME}.en-us.qna', + sourceQnA: 'knowledge-base/source/${FILENAME}.source.qna', dialogSchema: '${BOTNAME}.dialog.schema', schema: '${FILENAME}', settings: 'settings/${FILENAME}', @@ -22,6 +23,7 @@ const BotStructureTemplate = { lg: 'dialogs/${DIALOGNAME}/language-generation/${LOCALE}/${DIALOGNAME}.${LOCALE}.lg', lu: 'dialogs/${DIALOGNAME}/language-understanding/${LOCALE}/${DIALOGNAME}.${LOCALE}.lu', qna: 'dialogs/${DIALOGNAME}/knowledge-base/en-us/${DIALOGNAME}.en-us.qna', + sourceQnA: 'dialogs/${DIALOGNAME}/knowledge-base/source/${FILENAME}.source.qna', dialogSchema: 'dialogs/${DIALOGNAME}/${DIALOGNAME}.dialog.schema', }, skillManifests: 'manifests/${MANIFESTFILENAME}', @@ -30,6 +32,7 @@ const BotStructureTemplate = { const templateInterpolate = (str: string, obj: { [key: string]: string }) => str.replace(/\${([^}]+)}/g, (_, prop) => obj[prop]); +// parse file name: [fileId].[locale].[fileType] export const parseFileName = (name: string, defaultLocale: string) => { const fileType = Path.extname(name); const id = Path.basename(name, fileType); @@ -44,11 +47,33 @@ export const parseFileName = (name: string, defaultLocale: string) => { return { fileId, locale, fileType }; }; -// only +// parse QnA source file name: [dialogId].[fileId].source.qna, ignore locale for now. +export const parseSourceFileName = (name: string) => { + const fileType = FileExtensions.SourceQnA; + const id = Path.basename(name, fileType); + const [dialogId, fileId] = id.split('.'); + + if (!dialogId) { + throw new Error(`parse source file name ${name} error.`); + } + return { fileId: fileId || dialogId, dialogId, fileType }; +}; + export const defaultFilePath = (botName: string, defaultLocale: string, filename: string): string => { - const { fileId, locale, fileType } = parseFileName(filename, defaultLocale); const BOTNAME = botName.toLowerCase(); const CommonFileId = 'common'; + + if (filename.endsWith(FileExtensions.SourceQnA)) { + const { fileId: FILENAME, dialogId: DIALOGNAME } = parseSourceFileName(filename); + const isRootFile = BOTNAME === DIALOGNAME.toLowerCase(); + const TemplatePath = isRootFile ? BotStructureTemplate.sourceQnA : BotStructureTemplate.dialogs.sourceQnA; + return templateInterpolate(TemplatePath, { + FILENAME, + DIALOGNAME, + }); + } + + const { fileId, locale, fileType } = parseFileName(filename, defaultLocale); const LOCALE = locale; // 1. Even appsettings.json hit FileExtensions.Manifest, but it never use this do created. From e45ce62ea1fd5e97cd709e2132993a1a767f59ec Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Wed, 26 Aug 2020 17:01:44 +0800 Subject: [PATCH 013/105] include dialog name in source qna name --- .../packages/server/__tests__/models/bot/botStructure.test.ts | 4 ++-- Composer/packages/server/src/models/bot/botStructure.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Composer/packages/server/__tests__/models/bot/botStructure.test.ts b/Composer/packages/server/__tests__/models/bot/botStructure.test.ts index 204449d2c8..92b9e2e48c 100644 --- a/Composer/packages/server/__tests__/models/bot/botStructure.test.ts +++ b/Composer/packages/server/__tests__/models/bot/botStructure.test.ts @@ -64,7 +64,7 @@ describe('Bot structure file path', () => { // entry dialog's source qna it('should get entry dialog.source.qna file path', async () => { const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.myimport1.source.qna'); - expect(targetPath).toEqual('knowledge-base/source/myimport1.source.qna'); + expect(targetPath).toEqual('knowledge-base/source/mybot.myimport1.source.qna'); }); // child dialog's lg @@ -88,7 +88,7 @@ describe('Bot structure file path', () => { // child dialog's source qna it('should get child dialog.source.qna file path', async () => { const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.myimport1.source.qna'); - expect(targetPath).toEqual('dialogs/greeting/knowledge-base/source/myimport1.source.qna'); + expect(targetPath).toEqual('dialogs/greeting/knowledge-base/source/greeting.myimport1.source.qna'); }); }); diff --git a/Composer/packages/server/src/models/bot/botStructure.ts b/Composer/packages/server/src/models/bot/botStructure.ts index 54f89ded72..f1f520cb6d 100644 --- a/Composer/packages/server/src/models/bot/botStructure.ts +++ b/Composer/packages/server/src/models/bot/botStructure.ts @@ -11,7 +11,7 @@ const BotStructureTemplate = { lg: 'language-generation/${LOCALE}/${BOTNAME}.${LOCALE}.lg', lu: 'language-understanding/${LOCALE}/${BOTNAME}.${LOCALE}.lu', qna: 'knowledge-base/en-us/${BOTNAME}.en-us.qna', - sourceQnA: 'knowledge-base/source/${FILENAME}.source.qna', + sourceQnA: 'knowledge-base/source/${DIALOGNAME}.${FILENAME}.source.qna', dialogSchema: '${BOTNAME}.dialog.schema', schema: '${FILENAME}', settings: 'settings/${FILENAME}', @@ -23,7 +23,7 @@ const BotStructureTemplate = { lg: 'dialogs/${DIALOGNAME}/language-generation/${LOCALE}/${DIALOGNAME}.${LOCALE}.lg', lu: 'dialogs/${DIALOGNAME}/language-understanding/${LOCALE}/${DIALOGNAME}.${LOCALE}.lu', qna: 'dialogs/${DIALOGNAME}/knowledge-base/en-us/${DIALOGNAME}.en-us.qna', - sourceQnA: 'dialogs/${DIALOGNAME}/knowledge-base/source/${FILENAME}.source.qna', + sourceQnA: 'dialogs/${DIALOGNAME}/knowledge-base/source/${DIALOGNAME}.${FILENAME}.source.qna', dialogSchema: 'dialogs/${DIALOGNAME}/${DIALOGNAME}.dialog.schema', }, skillManifests: 'manifests/${MANIFESTFILENAME}', From cab4b28b2c9db7a19704529ffcf8e2039ae34cd6 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Wed, 26 Aug 2020 18:30:53 +0800 Subject: [PATCH 014/105] qnaIndexer update --- .../lib/indexers/__tests__/qnaIndexer.test.ts | 63 +++++++++++++++++++ .../packages/lib/indexers/src/qnaIndexer.ts | 7 +-- 2 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 Composer/packages/lib/indexers/__tests__/qnaIndexer.test.ts diff --git a/Composer/packages/lib/indexers/__tests__/qnaIndexer.test.ts b/Composer/packages/lib/indexers/__tests__/qnaIndexer.test.ts new file mode 100644 index 0000000000..9dd0d61684 --- /dev/null +++ b/Composer/packages/lib/indexers/__tests__/qnaIndexer.test.ts @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { FileInfo } from '@bfc/shared'; + +import { qnaIndexer } from '../src/qnaIndexer'; + +const { parse, index } = qnaIndexer; + +const content1 = `> # QnA Definitions +### ? who is the ceo? + \`\`\` + You can change the default message if you use the QnAMakerDialog. + See [this link](https://docs.botframework.com/en-us/azure-bot-service/templates/qnamaker/#navtitle) for details. + \`\`\` + + +### ? How do I programmatically update my KB? + \`\`\` + You can use our REST apis to manage your KB. + \#1. See here for details: https://westus.dev.cognitive.microsoft.com/docs/services/58994a073d9e04097c7ba6fe/operations/58994a073d9e041ad42d9baa + \`\`\` +`; +const content2 = `> # QnA pairs + +> !# @qna.pair.source = onlineFile.pdf + + + +## ? With Windows 10 + +\`\`\`markdown +**With Windows 10** +\`\`\``; + +describe('parse', () => { + it('should parse QnA file', () => { + const result1: any = parse(content1); + expect(result1.diagnostics.length).toEqual(0); + expect(result1.empty).toEqual(false); + expect(result1.qnaSections.length).toEqual(2); + + const result2: any = parse(content2); + expect(result2.diagnostics.length).toEqual(0); + expect(result2.empty).toEqual(false); + expect(result2.qnaSections.length).toEqual(1); + }); +}); + +describe('index', () => { + const file = { + name: 'test.qna', + relativePath: './test.qna', + content: content1, + path: '/', + } as FileInfo; + + it('should index qna file', () => { + const result: any = index([file])[0]; + expect(result.id).toEqual('test'); + expect(result.qnaSections.length).toEqual(2); + }); +}); diff --git a/Composer/packages/lib/indexers/src/qnaIndexer.ts b/Composer/packages/lib/indexers/src/qnaIndexer.ts index 00073bcbe2..e77dfd93df 100644 --- a/Composer/packages/lib/indexers/src/qnaIndexer.ts +++ b/Composer/packages/lib/indexers/src/qnaIndexer.ts @@ -27,8 +27,7 @@ function convertQnADiagnostic(d: any, source: string): Diagnostic { function parse(content: string, id = '') { const { Sections, Errors } = LUParser.parse(content); - const qnaSections: any[] = []; - Sections.forEach((section) => { + const qnaSections = Sections.filter(({ SectionType }) => SectionType === 'qnaSection').map((section) => { const { Answer, Body, @@ -54,7 +53,7 @@ function parse(content: string, id = '') { }; }); - qnaSections.push({ + return { Answer, Body, FilterPairs, @@ -69,7 +68,7 @@ function parse(content: string, id = '') { source, range, uuid: nanoid(6), - }); + }; }); const diagnostics = Errors.map((e) => convertQnADiagnostic(e, id)); return { From 5fa21909b31719abeaba30038b8a1cee088fe444 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Wed, 26 Aug 2020 18:31:57 +0800 Subject: [PATCH 015/105] update --- .../__tests__/models/bot/botStructure.test.ts | 6 +- .../server/src/models/bot/botStructure.ts | 56 ++++++++++--------- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/Composer/packages/server/__tests__/models/bot/botStructure.test.ts b/Composer/packages/server/__tests__/models/bot/botStructure.test.ts index 92b9e2e48c..0950110d26 100644 --- a/Composer/packages/server/__tests__/models/bot/botStructure.test.ts +++ b/Composer/packages/server/__tests__/models/bot/botStructure.test.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { defaultFilePath, parseFileName, parseSourceFileName } from '../../../src/models/bot/botStructure'; +import { defaultFilePath, parseFileName } from '../../../src/models/bot/botStructure'; const botName = 'Mybot'; const defaultLocale = 'en-us'; @@ -127,7 +127,7 @@ describe('Parse file name', () => { // entry dialog's source qna it('should parse entry dialog.qna file name', async () => { - const { fileId, dialogId, fileType } = parseSourceFileName('mybot.myimport1.source.qna'); + const { fileId, dialogId, fileType } = parseFileName('mybot.myimport1.source.qna', defaultLocale); expect(dialogId).toEqual('mybot'); expect(fileId).toEqual('myimport1'); expect(fileType).toEqual('.source.qna'); @@ -167,7 +167,7 @@ describe('Parse file name', () => { // child dialog's source qna it('should parse child dialog.qna file name', async () => { - const { fileId, dialogId, fileType } = parseSourceFileName('greeting.myimport1.source.qna'); + const { fileId, dialogId, fileType } = parseFileName('greeting.myimport1.source.qna', defaultLocale); expect(dialogId).toEqual('greeting'); expect(fileId).toEqual('myimport1'); expect(fileType).toEqual('.source.qna'); diff --git a/Composer/packages/server/src/models/bot/botStructure.ts b/Composer/packages/server/src/models/bot/botStructure.ts index f1f520cb6d..a7f421b52e 100644 --- a/Composer/packages/server/src/models/bot/botStructure.ts +++ b/Composer/packages/server/src/models/bot/botStructure.ts @@ -32,8 +32,23 @@ const BotStructureTemplate = { const templateInterpolate = (str: string, obj: { [key: string]: string }) => str.replace(/\${([^}]+)}/g, (_, prop) => obj[prop]); +// parse QnA source file name: [dialogId].[fileId].source.qna, ignore locale for now. +const parseSourceFileName = (name: string, locale: string) => { + const fileType = FileExtensions.SourceQnA; + const id = Path.basename(name, fileType); + const [dialogId, fileId] = id.split('.'); + + if (!dialogId) { + throw new Error(`parse source file name ${name} error.`); + } + return { fileId: fileId || dialogId, dialogId, fileType, locale }; +}; + // parse file name: [fileId].[locale].[fileType] export const parseFileName = (name: string, defaultLocale: string) => { + if (name.endsWith(FileExtensions.SourceQnA)) { + return parseSourceFileName(name, defaultLocale); + } const fileType = Path.extname(name); const id = Path.basename(name, fileType); @@ -44,36 +59,15 @@ export const parseFileName = (name: string, defaultLocale: string) => { fileId = id.slice(0, index); locale = id.slice(index + 1); } - return { fileId, locale, fileType }; -}; - -// parse QnA source file name: [dialogId].[fileId].source.qna, ignore locale for now. -export const parseSourceFileName = (name: string) => { - const fileType = FileExtensions.SourceQnA; - const id = Path.basename(name, fileType); - const [dialogId, fileId] = id.split('.'); - - if (!dialogId) { - throw new Error(`parse source file name ${name} error.`); - } - return { fileId: fileId || dialogId, dialogId, fileType }; + const dialogId = fileId; + return { dialogId, fileId, locale, fileType }; }; export const defaultFilePath = (botName: string, defaultLocale: string, filename: string): string => { const BOTNAME = botName.toLowerCase(); const CommonFileId = 'common'; - if (filename.endsWith(FileExtensions.SourceQnA)) { - const { fileId: FILENAME, dialogId: DIALOGNAME } = parseSourceFileName(filename); - const isRootFile = BOTNAME === DIALOGNAME.toLowerCase(); - const TemplatePath = isRootFile ? BotStructureTemplate.sourceQnA : BotStructureTemplate.dialogs.sourceQnA; - return templateInterpolate(TemplatePath, { - FILENAME, - DIALOGNAME, - }); - } - - const { fileId, locale, fileType } = parseFileName(filename, defaultLocale); + const { fileId, locale, fileType, dialogId } = parseFileName(filename, defaultLocale); const LOCALE = locale; // 1. Even appsettings.json hit FileExtensions.Manifest, but it never use this do created. @@ -91,9 +85,21 @@ export const defaultFilePath = (botName: string, defaultLocale: string, filename }); } - const DIALOGNAME = fileId; + const DIALOGNAME = dialogId; const isRootFile = BOTNAME === DIALOGNAME.toLowerCase(); + if (fileType === FileExtensions.SourceQnA) { + const TemplatePath = isRootFile ? BotStructureTemplate.sourceQnA : BotStructureTemplate.dialogs.sourceQnA; + return templateInterpolate(TemplatePath, { + FILENAME: fileId, + DIALOGNAME, + }); + + return templateInterpolate(BotStructureTemplate.skillManifests, { + MANIFESTFILENAME: filename, + }); + } + let TemplatePath = ''; if (fileType === FileExtensions.Dialog) { TemplatePath = isRootFile ? BotStructureTemplate.entry : BotStructureTemplate.dialogs.entry; From ae5ab556051bf25c1f4c97afd244e0a7b2071771 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Thu, 27 Aug 2020 11:03:51 +0800 Subject: [PATCH 016/105] create qna file for imported url & name --- .../components/CreationFlow/CreationFlow.tsx | 7 +- .../client/src/pages/design/DesignPage.tsx | 11 ++- .../knowledge-base/ImportQnAFromUrlModal.tsx | 55 +++++++++++++-- .../src/pages/knowledge-base/QnAPage.tsx | 11 +-- .../client/src/recoilModel/dispatchers/qna.ts | 67 +++++++++++++++++-- .../src/recoilModel/persistence/types.ts | 1 + 6 files changed, 129 insertions(+), 23 deletions(-) diff --git a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx index f3d9eca532..11fcc8bf54 100644 --- a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx +++ b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx @@ -17,6 +17,7 @@ import { storagesState, focusedStorageFolderState, localeState, + qnaFilesState, } from '../../recoilModel'; import Home from '../../pages/home/Home'; import ImportQnAFromUrlModal from '../../pages/knowledge-base/ImportQnAFromUrlModal'; @@ -52,6 +53,7 @@ const CreationFlow: React.FC = () => { const storages = useRecoilValue(storagesState); const focusedStorageFolder = useRecoilValue(focusedStorageFolderState); const locale = useRecoilValue(localeState); + const qnaFiles = useRecoilValue(qnaFilesState); const cachedProjectId = useProjectIdCache(); const currentStorageIndex = useRef(0); const storage = storages[currentStorageIndex.current]; @@ -110,13 +112,13 @@ const CreationFlow: React.FC = () => { saveProjectAs(projectId, formData.name, formData.description, formData.location); }; - const handleCreateQnA = async (urls: string[]) => { + const handleCreateQnA = async ({ name, urls }) => { saveTemplateId(QnABotTemplateId); handleDismiss(); await handleCreateNew(formData, QnABotTemplateId); // import qna from urls if (urls.length > 0) { - await importQnAFromUrls({ id: `${formData.name.toLocaleLowerCase()}.${locale}`, urls }); + await importQnAFromUrls({ id: `${formData.name.toLocaleLowerCase()}.${locale}`, name, urls }); } }; @@ -180,6 +182,7 @@ const CreationFlow: React.FC = () => { diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index a50379e240..b52e5dfbcc 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -560,7 +560,7 @@ const DesignPage: React.FC { + const handleCreateQnA = async ({ name, urls }) => { cancelImportQnAModal(); const formData = { $kind: qnaMatcherKey, @@ -575,7 +575,7 @@ const DesignPage: React.FC 0) { - await importQnAFromUrls({ id: `${dialogId}.${locale}`, urls }); + await importQnAFromUrls({ id: `${dialogId}.${locale}`, name, urls }); } } }; @@ -689,7 +689,12 @@ const DesignPage: React.FC )} {importQnAModalVisibility && ( - + )} {displaySkillManifest && ( diff --git a/Composer/packages/client/src/pages/knowledge-base/ImportQnAFromUrlModal.tsx b/Composer/packages/client/src/pages/knowledge-base/ImportQnAFromUrlModal.tsx index 86c22c848c..a7c7b1f040 100644 --- a/Composer/packages/client/src/pages/knowledge-base/ImportQnAFromUrlModal.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/ImportQnAFromUrlModal.tsx @@ -13,9 +13,10 @@ import { Link } from 'office-ui-fabric-react/lib/Link'; import { FontWeights } from '@uifabric/styling'; import { FontSizes, SharedColors, NeutralColors } from '@uifabric/fluent-theme'; import { RouteComponentProps } from '@reach/router'; +import { QnAFile } from '@bfc/shared'; import { QnAMakerLearningUrl, knowledgeBaseSourceUrl } from '../../constants'; -import { FieldConfig, useForm } from '../../hooks/useForm'; +import { FieldConfig, useForm, FieldValidator } from '../../hooks/useForm'; const styles = { dialog: { @@ -82,9 +83,10 @@ interface ImportQnAFromUrlModalProps location: string; }> { dialogId: string; + qnaFiles: QnAFile[]; subscriptionKey?: string; onDismiss: () => void; - onSubmit: (urls: string[]) => void; + onSubmit: (formData: ImportQnAFormData) => void; } const DialogTitle = () => { @@ -111,8 +113,9 @@ const DialogTitle = () => { ); }; -interface FormField { +export interface ImportQnAFormData { urls: string[]; + name: string; } const validateUrls = (urls: string[]) => { @@ -135,21 +138,49 @@ const validateUrls = (urls: string[]) => { return errors; }; -const formConfig: FieldConfig = { +const QnANameRegex = /^\w[-\w]*$/; + +const validateName = (sources: QnAFile[]): FieldValidator => { + return (name: string) => { + let currentError = ''; + if (name) { + if (!QnANameRegex.test(name)) { + currentError = formatMessage('Name contains invalid charactors'); + } + + const duplicatedItemIndex = sources.findIndex((item) => item.name === name); + if (duplicatedItemIndex > -1) { + currentError = formatMessage('Duplicate imported QnA name'); + } + } + return currentError; + }; +}; + +const formConfig: FieldConfig = { urls: { required: true, defaultValue: [''], }, + name: { + required: true, + defaultValue: '', + }, }; export const ImportQnAFromUrlModal: React.FC = (props) => { - const { onDismiss, onSubmit, dialogId } = props; + const { onDismiss, onSubmit, dialogId, qnaFiles } = props; const [urlErrors, setUrlErrors] = useState(['']); - const { formData, updateField, hasErrors } = useForm(formConfig); + formConfig.name.validate = validateName(qnaFiles); + const { formData, updateField, hasErrors, formErrors } = useForm(formConfig); const isQnAFileselected = !(dialogId === 'all'); const disabled = !isQnAFileselected || hasErrors || urlErrors.some((e) => !!e) || formData.urls.some((url) => !url); + const updateName = (name = '') => { + updateField('name', name); + }; + const addNewUrl = () => { const urls = [...formData.urls, '']; updateField('urls', urls); @@ -185,6 +216,16 @@ export const ImportQnAFromUrlModal: React.FC = (prop onDismiss={onDismiss} >
+ + updateName(name)} + /> + {formData.urls.map((l, index) => { return ( @@ -244,7 +285,7 @@ export const ImportQnAFromUrlModal: React.FC = (prop if (hasErrors) { return; } - onSubmit(formData.urls); + onSubmit(formData); }} /> diff --git a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx index 586e8bf833..cd9015e237 100644 --- a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx @@ -15,12 +15,12 @@ import { navigateTo } from '../../utils/navigation'; import { TestController } from '../../components/TestController/TestController'; import { INavTreeItem } from '../../components/NavTree'; import { Page } from '../../components/Page'; -import { dialogsState, projectIdState, qnaAllUpViewStatusState } from '../../recoilModel/atoms/botState'; +import { dialogsState, projectIdState, qnaAllUpViewStatusState, qnaFilesState } from '../../recoilModel/atoms/botState'; import { dispatcherState } from '../../recoilModel'; import { QnAAllUpViewStatus } from '../../recoilModel/types'; import TableView from './table-view'; -import { ImportQnAFromUrlModal } from './ImportQnAFromUrlModal'; +import { ImportQnAFromUrlModal, ImportQnAFormData } from './ImportQnAFromUrlModal'; const CodeEditor = React.lazy(() => import('./code-editor')); @@ -31,6 +31,7 @@ interface QnAPageProps extends RouteComponentProps<{}> { const QnAPage: React.FC = (props) => { const actions = useRecoilValue(dispatcherState); const dialogs = useRecoilValue(dialogsState); + const qnaFiles = useRecoilValue(qnaFilesState); const projectId = useRecoilValue(projectIdState); //To do: support other languages const locale = 'en-us'; @@ -132,9 +133,9 @@ const QnAPage: React.FC = (props) => { setImportQnAFromUrlModalVisiability(false); }; - const onSubmit = async (urls: string[]) => { + const onSubmit = async ({ name, urls }: ImportQnAFormData) => { onDismiss(); - await actions.importQnAFromUrls({ id: `${dialogId}.${locale}`, urls }); + await actions.importQnAFromUrls({ id: `${dialogId}.${locale}`, name, urls }); }; return ( @@ -156,7 +157,7 @@ const QnAPage: React.FC = (props) => { )} {importQnAFromUrlModalVisiability && ( - + )} diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index 62dc9bbfc5..acda31f2ba 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -74,6 +74,45 @@ export const removeQnAFileState = async (callbackHelpers: CallbackInterface, { i set(qnaFilesState, qnaFiles); }; +export const createSourceQnAFileState = async ( + callbackHelpers: CallbackInterface, + { id, name, content }: { id: string; name: string; content: string } +) => { + const { set, snapshot } = callbackHelpers; + const projectId = await snapshot.getPromise(projectIdState); + const qnaFiles = await snapshot.getPromise(qnaFilesState); + const createdSourceQnAId = `${getBaseName(id)}.${name}.source`; + const updatedQnAId = id; + const updatedOriginQnAFile = qnaFiles.find((f) => f.id === updatedQnAId); + + if (qnaFiles.find((qna) => qna.id === createdSourceQnAId)) { + throw new Error(`source qna file ${createdSourceQnAId}.qna already exist`); + } + + if (!updatedOriginQnAFile) { + throw new Error(`update qna file ${updatedQnAId}.qna not exist`); + } + + const createdQnAFile = (await qnaWorker.parse(createdSourceQnAId, content)) as QnAFile; + + const contentForDialogQnA = `[${name}](../source/${createdSourceQnAId}.qna)\n`; + const updatedContent = updatedOriginQnAFile + ? contentForDialogQnA + updatedOriginQnAFile.content + : contentForDialogQnA; + const updatedQnAFile = (await qnaWorker.parse(id, updatedContent)) as QnAFile; + + const newQnAFiles = qnaFiles.map((file) => { + if (file.id === updatedQnAId) { + return updatedQnAFile; + } + return file; + }); + + qnaFileStatusStorage.updateFileStatus(projectId, updatedQnAId); + qnaFileStatusStorage.updateFileStatus(projectId, createdSourceQnAId); + set(qnaFilesState, [...newQnAFiles, createdQnAFile]); +}; + export const qnaDispatcher = () => { const updateQnAFile = useRecoilCallback( (callbackHelpers: CallbackInterface) => async ({ id, content }: { id: string; content: string }) => { @@ -88,18 +127,34 @@ export const qnaDispatcher = () => { ); const importQnAFromUrls = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async ({ id, urls }: { id: string; urls: string[] }) => { - const { set, snapshot } = callbackHelpers; - const qnaFiles = await snapshot.getPromise(qnaFilesState); - const qnaFile = qnaFiles.find((f) => f.id === id); + (callbackHelpers: CallbackInterface) => async ({ + id, + name, + urls, + }: { + id: string; // dialogId.locale + name: string; + urls: string[]; + }) => { + const { set } = callbackHelpers; + set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Loading); try { const response = await httpClient.get(`/utilities/qna/parse`, { params: { urls: encodeURIComponent(urls.join(',')) }, }); - const content = qnaFile ? qnaFile.content + '\n' + response.data : response.data; - await updateQnAFileState(callbackHelpers, { id, content }); + const contentForSourceQnA = `> !# @source.urls = ${urls} +> !# @source.name = ${name} +${response.data} +`; + + await createSourceQnAFileState(callbackHelpers, { + id, + name, + content: contentForSourceQnA, + }); + set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Success); } catch (err) { setError(callbackHelpers, err); diff --git a/Composer/packages/client/src/recoilModel/persistence/types.ts b/Composer/packages/client/src/recoilModel/persistence/types.ts index d1e344c4d0..ef26f5f2de 100644 --- a/Composer/packages/client/src/recoilModel/persistence/types.ts +++ b/Composer/packages/client/src/recoilModel/persistence/types.ts @@ -14,6 +14,7 @@ export enum FileExtensions { Lu = '.lu', Lg = '.lg', QnA = '.qna', + SourceQnA = '.source.qna', Setting = 'appsettings.json', } From 539ee3a3b7fe61c6276b7066fa8513ee0f12a952 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Thu, 27 Aug 2020 14:41:14 +0800 Subject: [PATCH 017/105] update --- .../client/src/recoilModel/dispatchers/qna.ts | 2 +- .../__tests__/models/bot/botStructure.test.ts | 18 ++++++++++++++-- .../server/src/models/bot/botStructure.ts | 21 ++++++++++++------- .../packages/server/src/models/bot/builder.ts | 8 +++---- .../server/src/models/bot/luResolver.ts | 14 +++++++++---- 5 files changed, 45 insertions(+), 18 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index acda31f2ba..d76a70e08b 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -81,7 +81,7 @@ export const createSourceQnAFileState = async ( const { set, snapshot } = callbackHelpers; const projectId = await snapshot.getPromise(projectIdState); const qnaFiles = await snapshot.getPromise(qnaFilesState); - const createdSourceQnAId = `${getBaseName(id)}.${name}.source`; + const createdSourceQnAId = `${name}.source`; const updatedQnAId = id; const updatedOriginQnAFile = qnaFiles.find((f) => f.id === updatedQnAId); diff --git a/Composer/packages/server/__tests__/models/bot/botStructure.test.ts b/Composer/packages/server/__tests__/models/bot/botStructure.test.ts index 0950110d26..56e58e82c3 100644 --- a/Composer/packages/server/__tests__/models/bot/botStructure.test.ts +++ b/Composer/packages/server/__tests__/models/bot/botStructure.test.ts @@ -64,7 +64,13 @@ describe('Bot structure file path', () => { // entry dialog's source qna it('should get entry dialog.source.qna file path', async () => { const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.myimport1.source.qna'); - expect(targetPath).toEqual('knowledge-base/source/mybot.myimport1.source.qna'); + expect(targetPath).toEqual('knowledge-base/source/myimport1.source.qna'); + }); + + // shared source qna + it('should get shared .source.qna file path', async () => { + const targetPath = defaultFilePath(botName, defaultLocale, 'myimport1.source.qna'); + expect(targetPath).toEqual('knowledge-base/source/myimport1.source.qna'); }); // child dialog's lg @@ -88,7 +94,7 @@ describe('Bot structure file path', () => { // child dialog's source qna it('should get child dialog.source.qna file path', async () => { const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.myimport1.source.qna'); - expect(targetPath).toEqual('dialogs/greeting/knowledge-base/source/greeting.myimport1.source.qna'); + expect(targetPath).toEqual('dialogs/greeting/knowledge-base/source/myimport1.source.qna'); }); }); @@ -133,6 +139,14 @@ describe('Parse file name', () => { expect(fileType).toEqual('.source.qna'); }); + // all shared source qna + it('should parse .source.qna file name', async () => { + const { fileId, dialogId, fileType } = parseFileName('myimport1.source.qna', defaultLocale); + expect(dialogId).toEqual(''); + expect(fileId).toEqual('myimport1'); + expect(fileType).toEqual('.source.qna'); + }); + // child dialog it('should parse child dialog file name', async () => { const { fileId, locale, fileType } = parseFileName('greeting.dialog', defaultLocale); diff --git a/Composer/packages/server/src/models/bot/botStructure.ts b/Composer/packages/server/src/models/bot/botStructure.ts index a7f421b52e..9d1a8252f3 100644 --- a/Composer/packages/server/src/models/bot/botStructure.ts +++ b/Composer/packages/server/src/models/bot/botStructure.ts @@ -11,7 +11,7 @@ const BotStructureTemplate = { lg: 'language-generation/${LOCALE}/${BOTNAME}.${LOCALE}.lg', lu: 'language-understanding/${LOCALE}/${BOTNAME}.${LOCALE}.lu', qna: 'knowledge-base/en-us/${BOTNAME}.en-us.qna', - sourceQnA: 'knowledge-base/source/${DIALOGNAME}.${FILENAME}.source.qna', + sourceQnA: 'knowledge-base/source/${FILENAME}.source.qna', dialogSchema: '${BOTNAME}.dialog.schema', schema: '${FILENAME}', settings: 'settings/${FILENAME}', @@ -23,7 +23,7 @@ const BotStructureTemplate = { lg: 'dialogs/${DIALOGNAME}/language-generation/${LOCALE}/${DIALOGNAME}.${LOCALE}.lg', lu: 'dialogs/${DIALOGNAME}/language-understanding/${LOCALE}/${DIALOGNAME}.${LOCALE}.lu', qna: 'dialogs/${DIALOGNAME}/knowledge-base/en-us/${DIALOGNAME}.en-us.qna', - sourceQnA: 'dialogs/${DIALOGNAME}/knowledge-base/source/${DIALOGNAME}.${FILENAME}.source.qna', + sourceQnA: 'dialogs/${DIALOGNAME}/knowledge-base/source/${FILENAME}.source.qna', dialogSchema: 'dialogs/${DIALOGNAME}/${DIALOGNAME}.dialog.schema', }, skillManifests: 'manifests/${MANIFESTFILENAME}', @@ -33,15 +33,21 @@ const templateInterpolate = (str: string, obj: { [key: string]: string }) => str.replace(/\${([^}]+)}/g, (_, prop) => obj[prop]); // parse QnA source file name: [dialogId].[fileId].source.qna, ignore locale for now. +// [fileId].source.qna would store to bot root folder. const parseSourceFileName = (name: string, locale: string) => { const fileType = FileExtensions.SourceQnA; const id = Path.basename(name, fileType); - const [dialogId, fileId] = id.split('.'); - if (!dialogId) { - throw new Error(`parse source file name ${name} error.`); + let dialogId = '', + fileId = ''; + + if (id.includes('.')) { + [dialogId, fileId] = id.split('.'); + } else { + fileId = id; } - return { fileId: fileId || dialogId, dialogId, fileType, locale }; + + return { fileId, dialogId, fileType, locale }; }; // parse file name: [fileId].[locale].[fileType] @@ -89,7 +95,8 @@ export const defaultFilePath = (botName: string, defaultLocale: string, filename const isRootFile = BOTNAME === DIALOGNAME.toLowerCase(); if (fileType === FileExtensions.SourceQnA) { - const TemplatePath = isRootFile ? BotStructureTemplate.sourceQnA : BotStructureTemplate.dialogs.sourceQnA; + const TemplatePath = + isRootFile || !dialogId ? BotStructureTemplate.sourceQnA : BotStructureTemplate.dialogs.sourceQnA; return templateInterpolate(TemplatePath, { FILENAME: fileId, DIALOGNAME, diff --git a/Composer/packages/server/src/models/bot/builder.ts b/Composer/packages/server/src/models/bot/builder.ts index 65fa41b15f..e34e12088a 100644 --- a/Composer/packages/server/src/models/bot/builder.ts +++ b/Composer/packages/server/src/models/bot/builder.ts @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { FileInfo, IConfig, FileExtensions } from '@bfc/shared'; +import { FileInfo, IConfig } from '@bfc/shared'; import { Path } from '../../utility/path'; import { IFileStorage } from '../storage/interface'; import log from '../../logger'; -import { luImportResolverGenerator, getLUFiles } from './luResolver'; +import { luImportResolverGenerator, getLUFiles, getQnAFiles } from './luResolver'; import { ComposerReservoirSampler } from './sampler/ReservoirSampler'; import { ComposerBootstrapSampler } from './sampler/BootstrapSampler'; @@ -127,7 +127,7 @@ export class Builder { const qnaContents = qnaFiles.map((file) => { return { content: file.content, id: file.name }; }); - const resolver = luImportResolverGenerator(getLUFiles(allFiles), FileExtensions.Lu); + const resolver = luImportResolverGenerator([...getLUFiles(allFiles), ...getQnAFiles(allFiles)]); const result = await crossTrainer.crossTrain(luContents, qnaContents, this.crossTrainConfig, resolver); await this.writeFiles(result.luResult); @@ -202,7 +202,7 @@ export class Builder { // if (config.models.length === 0) { // throw new Error('No LUIS files exist'); // } - const resolver = luImportResolverGenerator(getLUFiles(allFiles), FileExtensions.Lu); + const resolver = luImportResolverGenerator(getLUFiles(allFiles)); const loadResult = await this.luBuilder.loadContents( config.models, config.fallbackLocal, diff --git a/Composer/packages/server/src/models/bot/luResolver.ts b/Composer/packages/server/src/models/bot/luResolver.ts index ec1e313510..64c5a9590c 100644 --- a/Composer/packages/server/src/models/bot/luResolver.ts +++ b/Composer/packages/server/src/models/bot/luResolver.ts @@ -32,7 +32,11 @@ export function getLUFiles(files: FileInfo[]): FileInfo[] { return files.filter(({ name }) => name.endsWith('.lu')); } -export function luImportResolverGenerator(files: FileInfo[], ext: string) { +export function getQnAFiles(files: FileInfo[]): FileInfo[] { + return files.filter(({ name }) => name.endsWith('.qna')); +} + +export function luImportResolverGenerator(files: FileInfo[]) { /** * @param srcId current file id * @param idsToFind imported file id @@ -56,10 +60,12 @@ export function luImportResolverGenerator(files: FileInfo[], ext: string) { * common.lu#*patterns* */ const fragmentReg = new RegExp('#.*$'); - // eslint-disable-next-line security/detect-non-literal-regexp - const extReg = new RegExp(ext + '$'); return (srcId: string, idsToFind: any[]) => { + const ext = Path.extname(srcId); + // eslint-disable-next-line security/detect-non-literal-regexp + const extReg = new RegExp(ext + '$'); + const sourceId = Path.basename(srcId).replace(extReg, ''); const locale = /\w\.\w/.test(sourceId) ? sourceId.split('.').pop() : 'en-us'; @@ -67,7 +73,7 @@ export function luImportResolverGenerator(files: FileInfo[], ext: string) { files.find(({ name }) => name === `${sourceId}.${locale}${ext}`) || files.find(({ name }) => name === `${sourceId}${ext}`); - if (!sourceFile) throw new Error(`File: ${srcId} not found`); + if (!sourceFile) throw new Error(`Resolve file: ${srcId} not found`); const sourceFileDir = Path.dirname(sourceFile.path); const wildcardIds = idsToFind.filter((item) => isWildcardPattern(item.filePath)); From f29fe412fa386e5bb31f3c554d6e7593383fe471 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Thu, 27 Aug 2020 16:03:07 +0800 Subject: [PATCH 018/105] enable source qna in all up view --- .../src/pages/knowledge-base/QnAPage.tsx | 22 +++++- .../src/pages/knowledge-base/code-editor.tsx | 5 +- .../src/pages/knowledge-base/table-view.tsx | 13 ++-- .../client/src/recoilModel/dispatchers/qna.ts | 67 +++++++++---------- 4 files changed, 64 insertions(+), 43 deletions(-) diff --git a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx index cd9015e237..5c785d0b6a 100644 --- a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx @@ -18,6 +18,7 @@ import { Page } from '../../components/Page'; import { dialogsState, projectIdState, qnaAllUpViewStatusState, qnaFilesState } from '../../recoilModel/atoms/botState'; import { dispatcherState } from '../../recoilModel'; import { QnAAllUpViewStatus } from '../../recoilModel/types'; +import { getBaseName } from '../../utils/fileUtil'; import TableView from './table-view'; import { ImportQnAFromUrlModal, ImportQnAFormData } from './ImportQnAFromUrlModal'; @@ -67,6 +68,25 @@ const QnAPage: React.FC = (props) => { return newDialogLinks; }, [dialogs]); + const sourceQnAFiles = qnaFiles.filter(({ id }) => id.endsWith('.source')); + + const sourceQnANavLinks: INavTreeItem[] = sourceQnAFiles.map(({ id }) => { + return { + id, + name: getBaseName(id), + ariaLabel: formatMessage('qna file'), + url: `/bot/${projectId}/knowledge-base/${id}`, + }; + }); + + sourceQnANavLinks.unshift({ + id: 'source', + name: '--- Imported From Urls ----', + ariaLabel: formatMessage('Imported From Urls'), + disabled: true, + url: `/bot/${projectId}/knowledge-base/source`, + }); + useEffect(() => { const activeDialog = dialogs.find(({ id }) => id === dialogId); if (!activeDialog && dialogs.length && dialogId !== 'all') { @@ -142,7 +162,7 @@ const QnAPage: React.FC = (props) => { = (props) => { const projectId = useRecoilValue(projectIdState); const userSettings = useRecoilValue(userSettingsState); const { dialogId } = props; - const file = qnaFiles.find(({ id }) => id === `${dialogId}.${locale}`); + const targetFileId = dialogId.endsWith('.source') ? dialogId : `${dialogId}.${locale}`; + const file = qnaFiles.find(({ id }) => id === targetFileId); const hash = props.location?.hash ?? ''; const hashLine = querystring.parse(hash).L; const line = Array.isArray(hashLine) ? +hashLine[0] : typeof hashLine === 'string' ? +hashLine : 0; @@ -65,7 +66,7 @@ const CodeEditor: React.FC = (props) => { const onChangeContent = useMemo( () => debounce((newContent: string) => { - actions.updateQnAFile({ id: `${dialogId}.${locale}`, content: newContent }); + actions.updateQnAFile({ id: targetFileId, content: newContent }); }, 500), [projectId] ); diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index 1ec64c37be..f723b06c36 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -67,7 +67,8 @@ const TableView: React.FC = (props) => { const locale = 'en-us'; //const locale = useRecoilValue(localeState); const { dialogId } = props; - const file = qnaFiles.find(({ id }) => id === `${dialogId}.${locale}`); + const targetFileId = dialogId.endsWith('.source') ? dialogId : `${dialogId}.${locale}`; + const file = qnaFiles.find(({ id }) => id === targetFileId); const fileRef = useRef(file); fileRef.current = file; const dialogIdRef = useRef(dialogId); @@ -111,11 +112,11 @@ const TableView: React.FC = (props) => { const createOrUpdateQuestion = () => { if (editMode === EditMode.Creating && question) { const updatedQnAFileContent = addQuestion(question, qnaSections, qnaSectionIndex); - actions.updateQnAFile({ id: `${dialogIdRef.current}.${localeRef.current}`, content: updatedQnAFileContent }); + actions.updateQnAFile({ id: targetFileId, content: updatedQnAFileContent }); } if (editMode === EditMode.Updating && qnaSections[qnaSectionIndex].Questions[questionIndex].content !== question) { const updatedQnAFileContent = updateQuestion(question, questionIndex, qnaSections, qnaSectionIndex); - actions.updateQnAFile({ id: `${dialogIdRef.current}.${localeRef.current}`, content: updatedQnAFileContent }); + actions.updateQnAFile({ id: targetFileId, content: updatedQnAFileContent }); } cancelQuestionEditOperation(); }; @@ -123,7 +124,7 @@ const TableView: React.FC = (props) => { const updateAnswer = () => { if (editMode === EditMode.Updating && qnaSections[qnaSectionIndex].Answer !== answer) { const updatedQnAFileContent = updateAnswerUtil(answer, qnaSections, qnaSectionIndex); - actions.updateQnAFile({ id: `${dialogIdRef.current}.${localeRef.current}`, content: updatedQnAFileContent }); + actions.updateQnAFile({ id: targetFileId, content: updatedQnAFileContent }); } cancelAnswerEditOperation(); }; @@ -246,7 +247,7 @@ const TableView: React.FC = (props) => { if (fileRef && fileRef.current) { const updatedQnAFileContent = removeSection(qnaSectionIndex, fileRef.current.content); actions.updateQnAFile({ - id: `${dialogIdRef.current}.${localeRef.current}`, + id: targetFileId, content: updatedQnAFileContent, }); } @@ -526,7 +527,7 @@ const TableView: React.FC = (props) => { const newQnAPair = generateQnAPair(); const content = get(fileRef.current, 'content', ''); const newContent = insertSection(0, content, newQnAPair); - actions.updateQnAFile({ id: `${dialogIdRef.current}.${localeRef.current}`, content: newContent }); + actions.updateQnAFile({ id: targetFileId, content: newContent }); const newArray = [false, ...showQnAPairDetails]; setShowQnAPairDetails(newArray); }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index d76a70e08b..ac21931303 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -82,33 +82,37 @@ export const createSourceQnAFileState = async ( const projectId = await snapshot.getPromise(projectIdState); const qnaFiles = await snapshot.getPromise(qnaFilesState); const createdSourceQnAId = `${name}.source`; - const updatedQnAId = id; - const updatedOriginQnAFile = qnaFiles.find((f) => f.id === updatedQnAId); if (qnaFiles.find((qna) => qna.id === createdSourceQnAId)) { throw new Error(`source qna file ${createdSourceQnAId}.qna already exist`); } - if (!updatedOriginQnAFile) { - throw new Error(`update qna file ${updatedQnAId}.qna not exist`); - } - const createdQnAFile = (await qnaWorker.parse(createdSourceQnAId, content)) as QnAFile; - const contentForDialogQnA = `[${name}](../source/${createdSourceQnAId}.qna)\n`; - const updatedContent = updatedOriginQnAFile - ? contentForDialogQnA + updatedOriginQnAFile.content - : contentForDialogQnA; - const updatedQnAFile = (await qnaWorker.parse(id, updatedContent)) as QnAFile; + const contentForDialogQnA = `[${name}](${createdSourceQnAId}.qna)\n`; - const newQnAFiles = qnaFiles.map((file) => { - if (file.id === updatedQnAId) { - return updatedQnAFile; + let newQnAFiles = [...qnaFiles]; + + // if created on a dialog, need update this dialog's qna ref + if (id.includes('.source') === false) { + const updatedQnAId = id; + const updatedOriginQnAFile = qnaFiles.find((f) => f.id === updatedQnAId); + if (!updatedOriginQnAFile) { + throw new Error(`update qna file ${updatedQnAId}.qna not exist`); } - return file; - }); + const updatedContent = updatedOriginQnAFile + ? contentForDialogQnA + updatedOriginQnAFile.content + : contentForDialogQnA; + const updatedQnAFile = (await qnaWorker.parse(id, updatedContent)) as QnAFile; + newQnAFiles = qnaFiles.map((file) => { + if (file.id === updatedQnAId) { + return updatedQnAFile; + } + return file; + }); + qnaFileStatusStorage.updateFileStatus(projectId, updatedQnAId); + } - qnaFileStatusStorage.updateFileStatus(projectId, updatedQnAId); qnaFileStatusStorage.updateFileStatus(projectId, createdSourceQnAId); set(qnaFilesState, [...newQnAFiles, createdQnAFile]); }; @@ -139,28 +143,23 @@ export const qnaDispatcher = () => { const { set } = callbackHelpers; set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Loading); - try { - const response = await httpClient.get(`/utilities/qna/parse`, { - params: { urls: encodeURIComponent(urls.join(',')) }, - }); - const contentForSourceQnA = `> !# @source.urls = ${urls} + const response = await httpClient.get(`/utilities/qna/parse`, { + params: { urls: encodeURIComponent(urls.join(',')) }, + }); + + const contentForSourceQnA = `> !# @source.urls = ${urls} > !# @source.name = ${name} ${response.data} `; - await createSourceQnAFileState(callbackHelpers, { - id, - name, - content: contentForSourceQnA, - }); - - set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Success); - } catch (err) { - setError(callbackHelpers, err); - } finally { - set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Success); - } + await createSourceQnAFileState(callbackHelpers, { + id, + name, + content: contentForSourceQnA, + }); + + set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Success); } ); From 8b85aa461fe6e212e6b8eb30f8dd3707e830d8aa Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Thu, 27 Aug 2020 16:22:23 +0800 Subject: [PATCH 019/105] import url sync all locale --- .../client/src/recoilModel/dispatchers/qna.ts | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index ac21931303..d0196d15ac 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -93,23 +93,23 @@ export const createSourceQnAFileState = async ( let newQnAFiles = [...qnaFiles]; - // if created on a dialog, need update this dialog's qna ref + // if created on a dialog, need update this dialog's all locale qna ref if (id.includes('.source') === false) { const updatedQnAId = id; - const updatedOriginQnAFile = qnaFiles.find((f) => f.id === updatedQnAId); - if (!updatedOriginQnAFile) { + if (!qnaFiles.find((f) => f.id === updatedQnAId)) { throw new Error(`update qna file ${updatedQnAId}.qna not exist`); } - const updatedContent = updatedOriginQnAFile - ? contentForDialogQnA + updatedOriginQnAFile.content - : contentForDialogQnA; - const updatedQnAFile = (await qnaWorker.parse(id, updatedContent)) as QnAFile; + newQnAFiles = qnaFiles.map((file) => { - if (file.id === updatedQnAId) { - return updatedQnAFile; + if (!file.id.endsWith('.source') && getBaseName(file.id) === getBaseName(updatedQnAId)) { + return { + ...file, + content: contentForDialogQnA + file.content, + }; } return file; }); + qnaFileStatusStorage.updateFileStatus(projectId, updatedQnAId); } @@ -144,9 +144,16 @@ export const qnaDispatcher = () => { set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Loading); - const response = await httpClient.get(`/utilities/qna/parse`, { - params: { urls: encodeURIComponent(urls.join(',')) }, - }); + let response; + try { + response = await httpClient.get(`/utilities/qna/parse`, { + params: { urls: encodeURIComponent(urls.join(',')) }, + }); + } catch (err) { + setError(callbackHelpers, err); + set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Success); + return; + } const contentForSourceQnA = `> !# @source.urls = ${urls} > !# @source.name = ${name} From d88c696df6d4a17b2708eee33ad98621b0ab505a Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Thu, 27 Aug 2020 16:25:04 +0800 Subject: [PATCH 020/105] update import name --- Composer/packages/client/src/recoilModel/dispatchers/qna.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index d0196d15ac..8b82c3040d 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -89,7 +89,7 @@ export const createSourceQnAFileState = async ( const createdQnAFile = (await qnaWorker.parse(createdSourceQnAId, content)) as QnAFile; - const contentForDialogQnA = `[${name}](${createdSourceQnAId}.qna)\n`; + const contentForDialogQnA = `[import](${createdSourceQnAId}.qna)\n`; let newQnAFiles = [...qnaFiles]; From ceb1175ffe54dafad8daf1e31baca8548af93982 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Fri, 28 Aug 2020 15:10:54 +0800 Subject: [PATCH 021/105] qna parse more infomation --- .../lib/indexers/__tests__/qnaIndexer.test.ts | 33 ++++- .../packages/lib/indexers/src/qnaIndexer.ts | 113 +++++++++++------- .../packages/lib/shared/src/types/indexers.ts | 1 + 3 files changed, 98 insertions(+), 49 deletions(-) diff --git a/Composer/packages/lib/indexers/__tests__/qnaIndexer.test.ts b/Composer/packages/lib/indexers/__tests__/qnaIndexer.test.ts index 9dd0d61684..641c76ff94 100644 --- a/Composer/packages/lib/indexers/__tests__/qnaIndexer.test.ts +++ b/Composer/packages/lib/indexers/__tests__/qnaIndexer.test.ts @@ -35,16 +35,43 @@ const content2 = `> # QnA pairs describe('parse', () => { it('should parse QnA file', () => { - const result1: any = parse(content1); + const result1 = parse(content1); expect(result1.diagnostics.length).toEqual(0); expect(result1.empty).toEqual(false); expect(result1.qnaSections.length).toEqual(2); - const result2: any = parse(content2); + const result2 = parse(content2); expect(result2.diagnostics.length).toEqual(0); expect(result2.empty).toEqual(false); expect(result2.qnaSections.length).toEqual(1); }); + + it('should parse QnA file with import', () => { + const content = `[import](windows-guide.source.qna) + [import](../common/aks.qna) + `; + + const result = parse(content); + expect(result.imports.length).toEqual(2); + expect(result.imports[0]).toEqual('windows-guide.source.qna'); + expect(result.imports[1]).toEqual('../common/aks.qna'); + expect(result.empty).toEqual(false); + expect(result.qnaSections.length).toEqual(0); + }); + + it('should parse QnA file with info options ', () => { + const content = `> !# @source.urls = https://aka.ms/surface-pro-4-user-guide-EN.pdf +> !# @source.name = guide +> # QnA pairs +`; + + const result = parse(content); + expect(result.infos).toEqual([ + '> !# @source.urls = https://aka.ms/surface-pro-4-user-guide-EN.pdf', + '> !# @source.name = guide', + ]); + expect(result.empty).toEqual(false); + }); }); describe('index', () => { @@ -56,7 +83,7 @@ describe('index', () => { } as FileInfo; it('should index qna file', () => { - const result: any = index([file])[0]; + const result = index([file])[0]; expect(result.id).toEqual('test'); expect(result.qnaSections.length).toEqual(2); }); diff --git a/Composer/packages/lib/indexers/src/qnaIndexer.ts b/Composer/packages/lib/indexers/src/qnaIndexer.ts index e77dfd93df..50365f56ff 100644 --- a/Composer/packages/lib/indexers/src/qnaIndexer.ts +++ b/Composer/packages/lib/indexers/src/qnaIndexer.ts @@ -2,13 +2,19 @@ // Licensed under the MIT License. import LUParser from '@microsoft/bf-lu/lib/parser/lufile/luParser'; import { FileInfo, QnAFile } from '@bfc/shared'; -import get from 'lodash/get'; +import isEmpty from 'lodash/isEmpty'; import { Diagnostic, Position, Range, DiagnosticSeverity } from '@bfc/shared'; import { nanoid } from 'nanoid'; import { getBaseName } from './utils/help'; import { FileExtensions } from './utils/fileExtensions'; +enum SectionTypes { + QnASection = 'qnaSection', + ImportSection = 'importSection', + LUModelInfo = 'modelInfoSection', +} + function convertQnADiagnostic(d: any, source: string): Diagnostic { const severityMap = { ERROR: DiagnosticSeverity.Error, @@ -25,54 +31,69 @@ function convertQnADiagnostic(d: any, source: string): Diagnostic { return result; } -function parse(content: string, id = '') { - const { Sections, Errors } = LUParser.parse(content); - const qnaSections = Sections.filter(({ SectionType }) => SectionType === 'qnaSection').map((section) => { - const { - Answer, - Body, - FilterPairs, - Id, - QAPairId, - Questions, - SectionType, - StartLine, - StopLine, - prompts, - promptsText, - source, - } = section; - const range = { - startLineNumber: get(section, 'ParseTree.start.line', 0), - endLineNumber: get(section, 'ParseTree.stop.line', 0), - }; - const QuestionsWithId = Questions.map((Q) => { +function parse(content: string, id = ''): QnAFile { + const result = LUParser.parse(content); + const qnaSections = result.Sections.filter(({ SectionType }) => SectionType === SectionTypes.QnASection).map( + (section) => { + const { + Answer, + Body, + FilterPairs, + Id, + QAPairId, + Questions, + SectionType, + StartLine, + StopLine, + prompts, + promptsText, + source, + } = section; + const range = new Range( + new Position(section.Range.Start.Line, section.Range.Start.Character), + new Position(section.Range.End.Line, section.Range.End.Character) + ); + const QuestionsWithId = Questions.map((Q) => { + return { + content: Q, + id: nanoid(6), + }; + }); + return { - content: Q, - id: nanoid(6), + Answer, + Body, + FilterPairs, + Id, + QAPairId, + Questions: QuestionsWithId, + SectionType, + StartLine, + StopLine, + prompts, + promptsText, + source, + range, + uuid: nanoid(6), }; - }); + } + ); + + const imports = result.Sections.filter(({ SectionType }) => SectionType === SectionTypes.ImportSection).map( + ({ Path }) => Path + ); + + const infos = result.Sections.filter(({ SectionType }) => SectionType === SectionTypes.LUModelInfo).map( + ({ ModelInfo }) => ModelInfo + ); - return { - Answer, - Body, - FilterPairs, - Id, - QAPairId, - Questions: QuestionsWithId, - SectionType, - StartLine, - StopLine, - prompts, - promptsText, - source, - range, - uuid: nanoid(6), - }; - }); - const diagnostics = Errors.map((e) => convertQnADiagnostic(e, id)); + const diagnostics = result.Errors.map((e) => convertQnADiagnostic(e, id)); return { - empty: !Sections.length, + id, + content, + imports, + infos, + empty: isEmpty(result.Sections), qnaSections, fileId: id, diagnostics, @@ -87,7 +108,7 @@ function index(files: FileInfo[]): QnAFile[] { if (name.endsWith(FileExtensions.QnA)) { const id = getBaseName(name, FileExtensions.QnA); const data = parse(content, id); - qnaFiles.push({ id, content, ...data }); + qnaFiles.push(data); } } return qnaFiles; diff --git a/Composer/packages/lib/shared/src/types/indexers.ts b/Composer/packages/lib/shared/src/types/indexers.ts index 5ce22c3423..1083e62586 100644 --- a/Composer/packages/lib/shared/src/types/indexers.ts +++ b/Composer/packages/lib/shared/src/types/indexers.ts @@ -116,6 +116,7 @@ export interface QnAFile { id: string; content: string; qnaSections: QnASection[]; + imports: string[]; [key: string]: any; } From ae3d9401bed72bd64182c0cf755154177ea03a6d Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Fri, 28 Aug 2020 17:32:14 +0800 Subject: [PATCH 022/105] keep header in qna crud --- .../packages/lib/indexers/src/qnaIndexer.ts | 3 +++ .../lib/indexers/src/utils/qnaUtil.ts | 22 +++++++++---------- .../packages/lib/shared/src/types/indexers.ts | 1 + 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Composer/packages/lib/indexers/src/qnaIndexer.ts b/Composer/packages/lib/indexers/src/qnaIndexer.ts index 50365f56ff..cb5fbeb802 100644 --- a/Composer/packages/lib/indexers/src/qnaIndexer.ts +++ b/Composer/packages/lib/indexers/src/qnaIndexer.ts @@ -87,10 +87,13 @@ function parse(content: string, id = ''): QnAFile { ({ ModelInfo }) => ModelInfo ); + const headers = qnaSections.length ? content.substring(0, content.indexOf(qnaSections[0].Body)) : content; + const diagnostics = result.Errors.map((e) => convertQnADiagnostic(e, id)); return { id, content, + headers, imports, infos, empty: isEmpty(result.Sections), diff --git a/Composer/packages/lib/indexers/src/utils/qnaUtil.ts b/Composer/packages/lib/indexers/src/utils/qnaUtil.ts index 13f1d73471..124a053887 100644 --- a/Composer/packages/lib/indexers/src/utils/qnaUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/qnaUtil.ts @@ -7,7 +7,7 @@ */ //import isEmpty from 'lodash/isEmpty'; -import { QnASection } from '@bfc/shared'; +import { QnASection, QnAFile } from '@bfc/shared'; import { sectionHandler } from '@microsoft/bf-lu/lib/parser/composerindex'; import cloneDeep from 'lodash/cloneDeep'; @@ -59,7 +59,8 @@ export function getParsedDiagnostics(newContent: string) { return diagnostics; } -export function addQuestion(newContent: string, qnaSections: QnASection[], qnaSectionIndex: number) { +export function addQuestion(newContent: string, qnaFile: QnAFile, qnaSectionIndex: number) { + const { qnaSections, headers } = qnaFile; const qnaFileContent = qnaSections.reduce((result, qnaSection, index) => { if (index !== qnaSectionIndex) { result = result + (index === 0 ? qnaSection.Body : '\n' + qnaSection.Body); @@ -69,15 +70,11 @@ export function addQuestion(newContent: string, qnaSections: QnASection[], qnaSe } return result; }, ''); - return qnaFileContent; + return headers + qnaFileContent; } -export function updateQuestion( - newContent: string, - questionIndex: number, - qnaSections: QnASection[], - qnaSectionIndex: number -) { +export function updateQuestion(newContent: string, questionIndex: number, qnaFile: QnAFile, qnaSectionIndex: number) { + const { qnaSections, headers } = qnaFile; const qnaFileContent = qnaSections.reduce((result, qnaSection, index) => { if (index !== qnaSectionIndex) { result = result + (index === 0 ? qnaSection.Body : '\n' + qnaSection.Body); @@ -87,10 +84,11 @@ export function updateQuestion( } return result; }, ''); - return qnaFileContent; + return headers + qnaFileContent; } -export function updateAnswer(newContent: string, qnaSections: QnASection[], qnaSectionIndex: number) { +export function updateAnswer(newContent: string, qnaFile: QnAFile, qnaSectionIndex: number) { + const { qnaSections, headers } = qnaFile; const qnaFileContent = qnaSections.reduce((result, qnaSection, index) => { if (index !== qnaSectionIndex) { result = result + (index === 0 ? qnaSection.Body : '\n' + qnaSection.Body); @@ -100,7 +98,7 @@ export function updateAnswer(newContent: string, qnaSections: QnASection[], qnaS } return result; }, ''); - return qnaFileContent; + return headers + qnaFileContent; } function updateAnswerInQnASection(qnaSection: QnASection, answer: string) { diff --git a/Composer/packages/lib/shared/src/types/indexers.ts b/Composer/packages/lib/shared/src/types/indexers.ts index 1083e62586..2f4403a69b 100644 --- a/Composer/packages/lib/shared/src/types/indexers.ts +++ b/Composer/packages/lib/shared/src/types/indexers.ts @@ -117,6 +117,7 @@ export interface QnAFile { content: string; qnaSections: QnASection[]; imports: string[]; + headers: string; // options, imports, avoid be wiped when do rebuild text [key: string]: any; } From dd9c7eab0eacdb52294a6b53bd2780817694d6ba Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Fri, 28 Aug 2020 17:32:53 +0800 Subject: [PATCH 023/105] group source.qna in dialog.qna --- .../src/pages/knowledge-base/table-view.tsx | 79 ++++++++++++++++--- 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index f723b06c36..deec75d9da 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -20,6 +20,7 @@ import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky'; import formatMessage from 'format-message'; import { RouteComponentProps } from '@reach/router'; import get from 'lodash/get'; +import { QnAFile } from '@bfc/shared/src/types'; import { addQuestion, @@ -31,6 +32,7 @@ import { } from '../../utils/qnaUtil'; import { dialogsState, qnaFilesState, projectIdState } from '../../recoilModel/atoms/botState'; import { dispatcherState } from '../../recoilModel'; +import { getBaseName, getFileName } from '../../utils/fileUtil'; import { formCell, @@ -76,6 +78,23 @@ const TableView: React.FC = (props) => { const localeRef = useRef(locale); localeRef.current = locale; const limitedNumber = useRef(1).current; + + const importedFiles = useMemo(() => { + const file = fileRef.current; + const results: QnAFile[] = []; + if (file && file.imports) { + file.imports.forEach((path) => { + const fileName = getFileName(path); + const fileId = getBaseName(fileName); + const target = qnaFiles.find(({ id }) => id === fileId); + if (target) results.push(target); + }); + } + + return results; + }, [fileRef.current, qnaFiles]); + const importedSourceFiles = importedFiles.filter(({ id }) => id.endsWith('.source')); + const generateQnASections = (file) => { return get(file, 'qnaSections', []).map((qnaSection, index) => { const qnaDialog = dialogs.find((dialog) => file.id === `${dialog.id}.${locale}`); @@ -89,6 +108,12 @@ const TableView: React.FC = (props) => { }; }); }; + + const importedSourceFileSections = importedSourceFiles.reduce((result: any[], qnaFile) => { + const res = generateQnASections(qnaFile); + return result.concat(res); + }, []); + const allQnASections = qnaFiles.reduce((result: any[], qnaFile) => { const res = generateQnASections(qnaFile); return result.concat(res); @@ -102,6 +127,7 @@ const TableView: React.FC = (props) => { return singleFileQnASections; } }, [dialogIdRef.current, qnaFiles]); + const [showQnAPairDetails, setShowQnAPairDetails] = useState(Array(qnaSections.length).fill(false)); const [qnaSectionIndex, setQnASectionIndex] = useState(-1); const [questionIndex, setQuestionIndex] = useState(-1); //used in QnASection.Questions array @@ -110,20 +136,24 @@ const TableView: React.FC = (props) => { const [isUpdatingAnswer, setIsUpdatingAnswer] = useState(false); const [answer, setAnswer] = useState(''); const createOrUpdateQuestion = () => { - if (editMode === EditMode.Creating && question) { - const updatedQnAFileContent = addQuestion(question, qnaSections, qnaSectionIndex); + if (editMode === EditMode.Creating && question && file) { + const updatedQnAFileContent = addQuestion(question, file, qnaSectionIndex); actions.updateQnAFile({ id: targetFileId, content: updatedQnAFileContent }); } - if (editMode === EditMode.Updating && qnaSections[qnaSectionIndex].Questions[questionIndex].content !== question) { - const updatedQnAFileContent = updateQuestion(question, questionIndex, qnaSections, qnaSectionIndex); + if ( + editMode === EditMode.Updating && + qnaSections[qnaSectionIndex].Questions[questionIndex].content !== question && + file + ) { + const updatedQnAFileContent = updateQuestion(question, questionIndex, file, qnaSectionIndex); actions.updateQnAFile({ id: targetFileId, content: updatedQnAFileContent }); } cancelQuestionEditOperation(); }; const updateAnswer = () => { - if (editMode === EditMode.Updating && qnaSections[qnaSectionIndex].Answer !== answer) { - const updatedQnAFileContent = updateAnswerUtil(answer, qnaSections, qnaSectionIndex); + if (editMode === EditMode.Updating && qnaSections[qnaSectionIndex].Answer !== answer && file) { + const updatedQnAFileContent = updateAnswerUtil(answer, file, qnaSectionIndex); actions.updateQnAFile({ id: targetFileId, content: updatedQnAFileContent }); } cancelAnswerEditOperation(); @@ -306,6 +336,8 @@ const TableView: React.FC = (props) => { const showingQuestions = showQnAPairDetails[qnaIndex] ? questions : questions.slice(0, limitedNumber); //This question content of this qna Section is '#?' const isQuestionEmpty = showingQuestions.length === 1 && showingQuestions[0].content === ''; + const isSourceSectionInDialog = item.fileId.endsWith('.source') && !dialogId.endsWith('.source'); + const isAllowEdit = dialogId !== 'all' && !isSourceSectionInDialog; return (
{showingQuestions.map((q, qIndex) => { @@ -317,7 +349,7 @@ const TableView: React.FC = (props) => { role={'textbox'} tabIndex={0} onClick={(e) => - dialogId !== 'all' ? handleUpdateingAlternatives(e, qnaIndex, qIndex, q.content) : () => {} + isAllowEdit ? handleUpdateingAlternatives(e, qnaIndex, qIndex, q.content) : () => {} } onKeyDown={(e) => { e.preventDefault(); @@ -355,7 +387,7 @@ const TableView: React.FC = (props) => { } })} - {isCreatingNewQuestionOnIthQnASection(qnaIndex, editMode) && dialogId !== 'all' && ( + {isCreatingNewQuestionOnIthQnASection(qnaIndex, editMode) && isAllowEdit && ( { @@ -368,7 +400,7 @@ const TableView: React.FC = (props) => { onKeyDown={(e) => handleQuestionKeydown(e)} /> )} - {!isCreatingNewQuestionOnIthQnASection(qnaIndex, editMode) && dialogId !== 'all' && ( + {!isCreatingNewQuestionOnIthQnASection(qnaIndex, editMode) && isAllowEdit && ( = (props) => { isResizable: true, data: 'string', onRender: (item, qnaIndex) => { + const isSourceSectionInDialog = item.fileId.endsWith('.source') && !dialogId.endsWith('.source'); + const isAllowEdit = dialogId !== 'all' && !isSourceSectionInDialog; return (
{!isUpdateingIthQnASectionAnswer(qnaIndex, isUpdatingAnswer, editMode) && ( @@ -398,7 +432,7 @@ const TableView: React.FC = (props) => { css={contentAnswer(showQnAPairDetails[qnaIndex])} role={'textbox'} tabIndex={0} - onClick={(e) => (dialogId !== 'all' ? handleUpdateingAnswer(e, qnaIndex, item.Answer) : () => {})} + onClick={(e) => (isAllowEdit ? handleUpdateingAnswer(e, qnaIndex, item.Answer) : () => {})} onKeyDown={(e) => { e.preventDefault(); if (e.key === 'Enter') { @@ -441,10 +475,15 @@ const TableView: React.FC = (props) => { fieldName: 'buttons', data: 'string', onRender: (item, qnaIndex) => { + const isSourceSectionInDialog = item.fileId.endsWith('.source') && !dialogId.endsWith('.source'); + const isAllowEdit = dialogId !== 'all' && !isSourceSectionInDialog; + return (
); diff --git a/Composer/packages/lib/indexers/package.json b/Composer/packages/lib/indexers/package.json index 764ddb093a..9616671130 100644 --- a/Composer/packages/lib/indexers/package.json +++ b/Composer/packages/lib/indexers/package.json @@ -28,7 +28,7 @@ }, "dependencies": { "@bfc/shared": "*", - "@bfcomposer/bf-lu": "^1.4.6", + "@bfcomposer/bf-lu": "^1.4.7", "@microsoft/bf-lu": "^4.11.0-dev.20200824.de66343", "adaptive-expressions": "4.10.0-preview-147186", "botbuilder-lg": "^4.10.0-preview-150886", diff --git a/Composer/yarn.lock b/Composer/yarn.lock index cb434fed8c..84c0e0bcb8 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -2332,10 +2332,10 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@bfcomposer/bf-lu@^1.4.6": - version "1.4.6" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.6.tgz#56c2fb3f0af0bd4e2f29dbc646a2a14e8ff9e502" - integrity sha1-VsL7PwrwvU4vKdvGRqKhTo/55QI= +"@bfcomposer/bf-lu@^1.4.7": + version "1.4.7" + resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.7.tgz#27b0cf4bc1a56f369b5ec3b971c86e7badf45c74" + integrity sha1-J7DPS8GlbzabXsO5cchue630XHQ= dependencies: "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" "@azure/ms-rest-azure-js" "2.0.1" From 5e52f4325281c143842e54f4bf4403259dabbd8b Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Wed, 9 Sep 2020 16:28:05 +0800 Subject: [PATCH 037/105] update --- .../src/pages/knowledge-base/QnAPage.tsx | 2 + .../src/pages/knowledge-base/code-editor.tsx | 3 +- .../client/src/pages/knowledge-base/styles.ts | 4 +- .../src/pages/knowledge-base/table-view.tsx | 104 +++++++++--------- 4 files changed, 61 insertions(+), 52 deletions(-) diff --git a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx index 75925d0251..7d2f5193cf 100644 --- a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx @@ -151,6 +151,8 @@ const QnAPage: React.FC = (props) => { }> + + {qnaAllUpViewStatus !== QnAAllUpViewStatus.Loading && } {qnaAllUpViewStatus === QnAAllUpViewStatus.Loading && ( diff --git a/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx b/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx index 6279c23f63..6fb8b333d2 100644 --- a/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx @@ -18,6 +18,7 @@ import { dispatcherState } from '../../recoilModel'; import { userSettingsState } from '../../recoilModel'; interface CodeEditorProps extends RouteComponentProps<{}> { dialogId: string; + containerId?: string; } const lspServerPath = '/lu-language-server'; @@ -30,7 +31,7 @@ const CodeEditor: React.FC = (props) => { const projectId = useRecoilValue(projectIdState); const userSettings = useRecoilValue(userSettingsState); const { dialogId } = props; - const targetFileId = dialogId.endsWith('.source') ? dialogId : `${dialogId}.${locale}`; + const targetFileId = props.containerId ? props.containerId : `${dialogId}.${locale}`; const file = qnaFiles.find(({ id }) => id === targetFileId); const hash = props.location?.hash ?? ''; const hashLine = querystring.parse(hash).L; diff --git a/Composer/packages/client/src/pages/knowledge-base/styles.ts b/Composer/packages/client/src/pages/knowledge-base/styles.ts index e85f004e35..ae8e3c5244 100644 --- a/Composer/packages/client/src/pages/knowledge-base/styles.ts +++ b/Composer/packages/client/src/pages/knowledge-base/styles.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { css } from '@emotion/core'; import { FontWeights } from '@uifabric/styling'; -import { NeutralColors, SharedColors } from '@uifabric/fluent-theme'; +import { NeutralColors, SharedColors, FontSizes } from '@uifabric/fluent-theme'; import { IIconStyles } from 'office-ui-fabric-react/lib/Icon'; export const content = css` min-height: 28px; @@ -152,7 +152,7 @@ export const addQnAPair = { export const addIcon = { root: { - fontSize: '16px', + fontSize: FontSizes.size12, color: SharedColors.cyanBlue10, }, }; diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index 9184cb867b..22bd31b697 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -28,6 +28,7 @@ import { QnAFile } from '@bfc/shared/src/types'; import { QnASection } from '@bfc/shared'; import { qnaUtil } from '@bfc/indexers'; +import { navigateTo } from '../../utils/navigation'; import { dialogsState, qnaFilesState, @@ -46,7 +47,7 @@ const noOp = () => undefined; interface QnASectionItem extends QnASection { fileId: string; - dialogId: string; + dialogId: string | undefined; used: boolean; expand: boolean; } @@ -89,10 +90,10 @@ const TableView: React.FC = (props) => { const qnaDialog = dialogs.find((dialog) => file.id === `${dialog.id}.${locale}`); return { fileId: file.id, - dialogId: qnaDialog?.id || '', + dialogId: qnaDialog?.id, used: !!qnaDialog, expand: false, - key: qnaSection.Body, + key: qnaSection.sectionId, ...qnaSection, }; }) @@ -100,6 +101,11 @@ const TableView: React.FC = (props) => { }; const [focusedIndex, setFocusedIndex] = useState(0); const [qnaSections, setQnASections] = useState([]); + + const importedFileIds = qnaFile?.imports.map(({ id }) => getBaseName(id)) || []; + const importedFiles = qnaFiles.filter(({ id }) => importedFileIds.includes(id)); + const importedSourceFiles = importedFiles.filter(({ id }) => id.endsWith('.source')); + useEffect(() => { if (isEmpty(qnaFiles)) return; @@ -111,30 +117,11 @@ const TableView: React.FC = (props) => { if (dialogId === 'all') { setQnASections(allSections); } else { - const dialogSections = allSections.filter((t) => t.dialogId === dialogId); + const dialogSections = allSections.filter((t) => t.dialogId === dialogId || importedFileIds.includes(t.fileId)); setQnASections(dialogSections); } }, [qnaFiles, dialogId, projectId]); - const importedFiles = useMemo(() => { - const results: QnAFile[] = []; - if (qnaFile && qnaFile.imports) { - qnaFile.imports.forEach(({ id }) => { - const fileId = getBaseName(id); - const target = qnaFiles.find(({ id }) => id === fileId); - if (target) results.push(target); - }); - } - - return results; - }, [qnaFile, qnaFiles]); - const importedSourceFiles = importedFiles.filter(({ id }) => id.endsWith('.source')); - - const importedSourceFileSections = importedSourceFiles.reduce((result: any[], qnaFile) => { - const res = generateQnASections(qnaFile); - return result.concat(res); - }, []); - const toggleExpandRow = (sectionId: string, expand?: boolean) => { const newSections = qnaSections.map((item) => { if (item.sectionId === sectionId) { @@ -159,7 +146,8 @@ const TableView: React.FC = (props) => { } }; - const onCreateNewQnAPairs = (fileId: string) => { + const onCreateNewQnAPairs = (fileId: string | undefined) => { + if (!fileId) return; const newQnAPair = qnaUtil.generateQnAPair(); addQnAPairs({ id: fileId, content: newQnAPair }); }; @@ -218,10 +206,20 @@ const TableView: React.FC = (props) => { }, ]} overflowItems={[ + { + key: 'edit', + // eslint-disable-next-line format-message/literal-pattern + name: formatMessage(`Show code`), + onClick: () => { + if (!qnaFile) return; + navigateTo(`/bot/${projectId}/knowledge-base/${dialogId}/${groupFileId}/edit`); + }, + }, { key: 'delete', + name: formatMessage('Delete knowledge base'), // eslint-disable-next-line format-message/literal-pattern - name: formatMessage(`Remove from ${dialogId}`), + // name: formatMessage(`Remove from ${dialogId}`), onClick: () => { if (!qnaFile) return; removeQnAImport({ id: qnaFile.id, sourceId: groupFileId }); @@ -242,14 +240,14 @@ const TableView: React.FC = (props) => { return ( -
+
{ if (!qnaFile) return; - onCreateNewQnAPairs(qnaFile.id); + onCreateNewQnAPairs(props.group?.key); actions.setMessage('item added'); }} > @@ -441,27 +439,35 @@ const TableView: React.FC = (props) => { }; const getGroups = (): IGroup[] | undefined => { - if (dialogId === 'all' || dialogId.endsWith('.source') || !qnaFile) { - return undefined; + if (dialogId === 'all') { + const groups: IGroup[] = []; + qnaFiles.forEach((currentFile) => { + const lastGroup = groups[groups.length - 1]; + const startIndex = lastGroup ? lastGroup.startIndex + lastGroup.count : 0; + const { id, qnaSections } = currentFile; + groups.push({ key: id, name: getBaseName(id), startIndex, count: qnaSections.length, level: 0 }); + }); + return groups; + } else { + if (!qnaFile) return undefined; + const groups: IGroup[] = [ + { + key: qnaFile.id, + name: activeDialog?.displayName || dialogId, + startIndex: 0, + count: qnaFile.qnaSections.length, + level: 0, + }, + ]; + importedSourceFiles.forEach((currentFile) => { + const lastGroup = groups[groups.length - 1]; + const startIndex = lastGroup.startIndex + lastGroup.count; + const { id, qnaSections } = currentFile; + const name = getBaseName(id); + groups.push({ key: id, name, startIndex, count: qnaSections.length, level: 0 }); + }); + return groups; } - const groups: IGroup[] = [ - { - key: dialogId, - name: activeDialog?.displayName || dialogId, - startIndex: 0, - count: qnaSections.length, - level: 0, - }, - ]; - importedSourceFiles.forEach((currentFile) => { - const lastGroup = groups[groups.length - 1]; - const startIndex = lastGroup.startIndex + lastGroup.count; - const { id, qnaSections } = currentFile; - const name = getBaseName(id); - groups.push({ key: id, name, startIndex, count: qnaSections.length, level: 0 }); - }); - - return groups; }; const onRenderDetailsHeader = useCallback( @@ -499,7 +505,7 @@ const TableView: React.FC = (props) => { }} groups={getGroups()} initialFocusedIndex={focusedIndex} - items={[...qnaSections, ...importedSourceFileSections]} + items={qnaSections} layoutMode={DetailsListLayoutMode.justified} selectionMode={SelectionMode.single} onRenderDetailsHeader={onRenderDetailsHeader} From 96aac3ff6bde7ff350c06fd22406f73ef61c6e46 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Thu, 10 Sep 2020 14:50:56 +0800 Subject: [PATCH 038/105] enable multiturn --- .../client/src/recoilModel/dispatchers/qna.ts | 3 ++- Composer/packages/lib/indexers/package.json | 2 +- Composer/packages/server/package.json | 1 + .../server/src/controllers/utilities.ts | 3 ++- .../server/src/models/utilities/parser.ts | 21 +++++++++++++------ Composer/yarn.lock | 16 +++++++------- 6 files changed, 29 insertions(+), 17 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index eb3a82c1b6..9e487a25f9 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -158,7 +158,8 @@ export const qnaDispatcher = () => { return; } - const contentForSourceQnA = `> !# @source.urls = ${urls} + const contentForSourceQnA = `> !# @source.urls=${urls} +> !# @source.multiTurn=${multiTurn} ${response.data} `; diff --git a/Composer/packages/lib/indexers/package.json b/Composer/packages/lib/indexers/package.json index 9616671130..27fed5652b 100644 --- a/Composer/packages/lib/indexers/package.json +++ b/Composer/packages/lib/indexers/package.json @@ -28,7 +28,7 @@ }, "dependencies": { "@bfc/shared": "*", - "@bfcomposer/bf-lu": "^1.4.7", + "@bfcomposer/bf-lu": "^1.4.8", "@microsoft/bf-lu": "^4.11.0-dev.20200824.de66343", "adaptive-expressions": "4.10.0-preview-147186", "botbuilder-lg": "^4.10.0-preview-150886", diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index bd86d0e8df..822424107e 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -61,6 +61,7 @@ "@bfc/lu-languageserver": "*", "@bfc/plugin-loader": "*", "@bfc/shared": "*", + "@bfcomposer/bf-lu": "^1.4.8", "@microsoft/bf-dispatcher": "^4.10.0-preview.141651", "@microsoft/bf-lu": "^4.11.0-dev.20200824.de66343", "archiver": "^3.0.0", diff --git a/Composer/packages/server/src/controllers/utilities.ts b/Composer/packages/server/src/controllers/utilities.ts index 5c59e1bac8..a32a5af76d 100644 --- a/Composer/packages/server/src/controllers/utilities.ts +++ b/Composer/packages/server/src/controllers/utilities.ts @@ -7,7 +7,8 @@ import { parseQnAContent } from '../models/utilities/parser'; async function getQnaContent(req: Request, res: Response) { try { const urls = decodeURIComponent(req.query.urls).split(','); - res.status(200).json(await parseQnAContent(urls)); + const multiTurn = req.query.multiTurn === 'true' || req.query.multiTurn === true ? true : false; + res.status(200).json(await parseQnAContent(urls, multiTurn)); } catch (e) { res.status(400).json({ message: e.message || e.text, diff --git a/Composer/packages/server/src/models/utilities/parser.ts b/Composer/packages/server/src/models/utilities/parser.ts index 47a9e1ca4d..7f4d1b33e5 100644 --- a/Composer/packages/server/src/models/utilities/parser.ts +++ b/Composer/packages/server/src/models/utilities/parser.ts @@ -11,7 +11,9 @@ import log from './../../logger'; import { DOC_EXTENSIONS, QNA_SUBSCRIPTION_KEY, COGNITIVE_SERVICES_ENDPOINTS } from './../../constants'; // eslint-disable-next-line @typescript-eslint/no-var-requires -const qnaBuild = require('@microsoft/bf-lu/lib/parser/qnabuild/builder.js'); +// const qnaBuild = require('@microsoft/bf-lu/lib/parser/qnabuild/builder.js'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const qnaBuild = require('@bfcomposer/bf-lu/lib/parser/qnabuild/builder.js'); const debug = log.extend('helper-parser'); @@ -25,7 +27,7 @@ function getBuildEnvironment() { return {}; } -async function importQnAFromUrl(builder: any, url: string, subscriptionKey: string) { +async function importQnAFromUrl(builder: any, url: string, subscriptionKey: string, multiTurn = false) { url = url.trim(); let onlineQnAContent = ''; if (DOC_EXTENSIONS.some((e) => url.endsWith(e))) { @@ -36,17 +38,24 @@ async function importQnAFromUrl(builder: any, url: string, subscriptionKey: stri url, subscriptionKey, COGNITIVE_SERVICES_ENDPOINTS, - uuid() + uuid(), + multiTurn ); } else { - onlineQnAContent = await builder.importUrlReference(url, subscriptionKey, COGNITIVE_SERVICES_ENDPOINTS, uuid()); + onlineQnAContent = await builder.importUrlReference( + url, + subscriptionKey, + COGNITIVE_SERVICES_ENDPOINTS, + uuid(), + multiTurn + ); } return onlineQnAContent; } //https://azure.microsoft.com/en-us/pricing/details/cognitive-services/qna-maker/ //limited to 3 transactions per second -export async function parseQnAContent(urls: string[]) { +export async function parseQnAContent(urls: string[], multiTurn: boolean) { const builder = new qnaBuild.Builder((message: string) => debug(message)); let qnaContent = ''; @@ -64,7 +73,7 @@ export async function parseQnAContent(urls: string[]) { const batchUrls = urls.slice(i, i + limitedNumInBatch); const contents = await Promise.all( batchUrls.map(async (url) => { - return await importQnAFromUrl(builder, url, subscriptionKey); + return await importQnAFromUrl(builder, url, subscriptionKey, multiTurn); }) ); contents.forEach((content) => { diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 84c0e0bcb8..54e513ad4b 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -2332,10 +2332,10 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@bfcomposer/bf-lu@^1.4.7": - version "1.4.7" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.7.tgz#27b0cf4bc1a56f369b5ec3b971c86e7badf45c74" - integrity sha1-J7DPS8GlbzabXsO5cchue630XHQ= +"@bfcomposer/bf-lu@^1.4.8": + version "1.4.8" + resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.8.tgz#dbb68cc54b2f70c558623952f38567e65d49b5de" + integrity sha1-27aMxUsvcMVYYjlS84Vn5l1Jtd4= dependencies: "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" "@azure/ms-rest-azure-js" "2.0.1" @@ -5465,10 +5465,10 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== -bl@^1.0.0: - version "1.2.3" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" - integrity sha1-Ho3YAULqyA1xWMnczAR/tiDgNec= +bl@^1.0.0, bl@^2.2.1: + version "2.2.1" + resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/bl/-/bl-2.2.1.tgz#8c11a7b730655c5d56898cdc871224f40fd901d5" + integrity sha1-jBGntzBlXF1WiYzchxIk9A/ZAdU= dependencies: readable-stream "^2.3.5" safe-buffer "^5.1.1" From 59e4f406e2a2a2e42bae723d02a0ce3cdebd484d Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Thu, 10 Sep 2020 16:32:50 +0800 Subject: [PATCH 039/105] remove file state --- .../client/src/recoilModel/dispatchers/lg.ts | 9 ++++++- .../client/src/recoilModel/dispatchers/lu.ts | 12 +++++++--- .../client/src/recoilModel/dispatchers/qna.ts | 24 ++++++++++++++++--- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts index 93afaaed1c..fd228068bf 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts @@ -108,7 +108,14 @@ export const createLgFileState = async ( export const removeLgFileState = async (callbackHelpers: CallbackInterface, { id }: { id: string }) => { const { set, snapshot } = callbackHelpers; let lgFiles = await snapshot.getPromise(lgFilesState); - lgFiles = lgFiles.filter((file) => getBaseName(file.id) !== id); + const locale = await snapshot.getPromise(localeState); + + const targetLgFile = lgFiles.find((item) => item.id === id) || lgFiles.find((item) => item.id === `${id}.${locale}`); + if (!targetLgFile) { + throw new Error(`remove lg file ${id} not exist`); + } + + lgFiles = lgFiles.filter((file) => file.id !== targetLgFile.id); set(lgFilesState, lgFiles); }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lu.ts b/Composer/packages/client/src/recoilModel/dispatchers/lu.ts index f85715e53f..415b30e447 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/lu.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/lu.ts @@ -102,14 +102,20 @@ export const removeLuFileState = async (callbackHelpers: CallbackInterface, { id const { set, snapshot } = callbackHelpers; let luFiles = await snapshot.getPromise(luFilesState); const projectId = await snapshot.getPromise(projectIdState); + const locale = await snapshot.getPromise(localeState); + + const targetLuFile = luFiles.find((item) => item.id === id) || luFiles.find((item) => item.id === `${id}.${locale}`); + if (!targetLuFile) { + throw new Error(`remove lu file ${id} not exist`); + } luFiles.forEach((file) => { - if (getBaseName(file.id) === getBaseName(id)) { - luFileStatusStorage.removeFileStatus(projectId, id); + if (file.id === targetLuFile.id) { + luFileStatusStorage.removeFileStatus(projectId, targetLuFile.id); } }); - luFiles = luFiles.filter((file) => getBaseName(file.id) !== id); + luFiles = luFiles.filter((file) => file.id !== targetLuFile.id); set(luFilesState, luFiles); }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index 9e487a25f9..b1fd89b128 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -60,18 +60,31 @@ export const createQnAFileState = async ( set(qnaFilesState, [...qnaFiles, ...changes]); }; +/** + * id can be + * 1. dialogId, no locale + * 2. qna file id, with locale + * 3. source qna file id, no locale + */ export const removeQnAFileState = async (callbackHelpers: CallbackInterface, { id }: { id: string }) => { const { set, snapshot } = callbackHelpers; let qnaFiles = await snapshot.getPromise(qnaFilesState); const projectId = await snapshot.getPromise(projectIdState); + const locale = await snapshot.getPromise(localeState); + + const targetQnAFile = + qnaFiles.find((item) => item.id === id) || qnaFiles.find((item) => item.id === `${id}.${locale}`); + if (!targetQnAFile) { + throw new Error(`remove qna file ${id} not exist`); + } qnaFiles.forEach((file) => { - if (getBaseName(file.id) === getBaseName(id)) { - qnaFileStatusStorage.removeFileStatus(projectId, id); + if (file.id === targetQnAFile.id) { + qnaFileStatusStorage.removeFileStatus(projectId, targetQnAFile.id); } }); - qnaFiles = qnaFiles.filter((file) => getBaseName(file.id) !== id); + qnaFiles = qnaFiles.filter((file) => file.id !== targetQnAFile.id); set(qnaFilesState, qnaFiles); }; @@ -131,6 +144,10 @@ export const qnaDispatcher = () => { } ); + const removeQnAFile = useRecoilCallback((callbackHelpers: CallbackInterface) => async ({ id }: { id: string }) => { + await removeQnAFileState(callbackHelpers, { id }); + }); + const importQnAFromUrls = useRecoilCallback( (callbackHelpers: CallbackInterface) => async ({ id, @@ -337,6 +354,7 @@ ${response.data} updateQnAQuestion, updateQnAAnswer, createQnAFile, + removeQnAFile, updateQnAFile, importQnAFromUrls, }; From d330c0f19eda53712091ff507c9e7b6866667be1 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Thu, 10 Sep 2020 16:35:21 +0800 Subject: [PATCH 040/105] update style --- .../components/AllupviewComponets/styles.ts | 10 +-- .../client/src/pages/knowledge-base/styles.ts | 2 +- .../src/pages/knowledge-base/table-view.tsx | 67 +++++++++---------- 3 files changed, 34 insertions(+), 45 deletions(-) diff --git a/Composer/packages/client/src/components/AllupviewComponets/styles.ts b/Composer/packages/client/src/components/AllupviewComponets/styles.ts index 0bc2d46a9d..b3291e50c1 100644 --- a/Composer/packages/client/src/components/AllupviewComponets/styles.ts +++ b/Composer/packages/client/src/components/AllupviewComponets/styles.ts @@ -3,7 +3,7 @@ // Licensed under the MIT License. import { mergeStyleSets, FontSizes, FontWeights } from 'office-ui-fabric-react/lib/Styling'; -import { NeutralColors, SharedColors } from '@uifabric/fluent-theme'; +import { SharedColors } from '@uifabric/fluent-theme'; // import { getTheme } from 'office-ui-fabric-react/lib/Styling'; // const theme = getTheme(); @@ -11,12 +11,9 @@ import { NeutralColors, SharedColors } from '@uifabric/fluent-theme'; export const AddTemplateButton = { root: { fontSize: FontSizes.smallPlus, - margin: '0 1em', + marginLeft: '28px', color: SharedColors.cyanBlue10, }, - icon: { - fontSize: FontSizes.xSmall, - }, }; export const classNames = mergeStyleSets({ @@ -27,7 +24,6 @@ export const classNames = mergeStyleSets({ alignItems: 'center', }, groupHeaderSourceName: { - display: 'flex', - margin: '0 1em', + marginLeft: '5px', }, }); diff --git a/Composer/packages/client/src/pages/knowledge-base/styles.ts b/Composer/packages/client/src/pages/knowledge-base/styles.ts index ae8e3c5244..8bbbde93e9 100644 --- a/Composer/packages/client/src/pages/knowledge-base/styles.ts +++ b/Composer/packages/client/src/pages/knowledge-base/styles.ts @@ -152,7 +152,7 @@ export const addQnAPair = { export const addIcon = { root: { - fontSize: FontSizes.size12, + fontSize: FontSizes.size10, color: SharedColors.cyanBlue10, }, }; diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index 22bd31b697..c9e7672cfe 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -14,7 +14,7 @@ import { IDetailsGroupRenderProps, IGroup, } from 'office-ui-fabric-react/lib/DetailsList'; -import { GroupHeader } from 'office-ui-fabric-react/lib/GroupedList'; +import { GroupHeader, CollapseAllVisibility } from 'office-ui-fabric-react/lib/GroupedList'; import { IOverflowSetItemProps, OverflowSet } from 'office-ui-fabric-react/lib/OverflowSet'; import { Link } from 'office-ui-fabric-react/lib/Link'; import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; @@ -66,6 +66,7 @@ const TableView: React.FC = (props) => { const { // addQnAImport, removeQnAImport, + removeQnAFile, // setMessage, addQnAPairs, removeQnAPairs, @@ -136,14 +137,14 @@ const TableView: React.FC = (props) => { setQnASections(newSections); }; - const deleteQnASection = (sectionId: string) => { + const deleteQnASection = (fileId: string, sectionId: string) => { + if (!fileId) return; actions.setMessage('item deleted'); - if (qnaFile) { - removeQnAPairs({ - id: qnaFile.id, - sectionId, - }); - } + + removeQnAPairs({ + id: fileId, + sectionId, + }); }; const onCreateNewQnAPairs = (fileId: string | undefined) => { @@ -152,25 +153,21 @@ const TableView: React.FC = (props) => { addQnAPairs({ id: fileId, content: newQnAPair }); }; - const onCreateNewQuestion = useCallback( - (fileId, sectionId) => { - if (qnaFile) { - const payload = { - id: fileId, - sectionId, - content: 'Add new question', - }; - addQnAQuestion(payload); - setFocusedIndex(qnaSections.length); - } - }, - [qnaFile] - ); + const onCreateNewQuestion = (fileId, sectionId) => { + if (qnaFile) { + const payload = { + id: fileId, + sectionId, + content: 'Add new question', + }; + addQnAQuestion(payload); + } + }; const onRenderGroupHeader: IDetailsGroupRenderProps['onRenderHeader'] = (props) => { const groupName = props?.group?.name || ''; const groupFileId = props?.group?.key || ''; - const isImportedSource = groupFileId !== dialogId; + const isImportedSource = groupFileId.endsWith('.source'); const onRenderItem = (item: IOverflowSetItemProps): JSX.Element => { return ; @@ -192,7 +189,7 @@ const TableView: React.FC = (props) => {
{isImportedSource && ( -
Source:
+ Source: {groupName} @@ -208,8 +205,7 @@ const TableView: React.FC = (props) => { overflowItems={[ { key: 'edit', - // eslint-disable-next-line format-message/literal-pattern - name: formatMessage(`Show code`), + name: formatMessage('Show code'), onClick: () => { if (!qnaFile) return; navigateTo(`/bot/${projectId}/knowledge-base/${dialogId}/${groupFileId}/edit`); @@ -218,11 +214,10 @@ const TableView: React.FC = (props) => { { key: 'delete', name: formatMessage('Delete knowledge base'), - // eslint-disable-next-line format-message/literal-pattern - // name: formatMessage(`Remove from ${dialogId}`), - onClick: () => { + onClick: async () => { if (!qnaFile) return; - removeQnAImport({ id: qnaFile.id, sourceId: groupFileId }); + await removeQnAImport({ id: qnaFile.id, sourceId: groupFileId }); + await removeQnAFile({ id: groupFileId }); }, }, ]} @@ -391,19 +386,14 @@ const TableView: React.FC = (props) => { fieldName: 'buttons', data: 'string', onRender: (item) => { - const isSourceSectionInDialog = item.fileId.endsWith('.source') && !dialogId.endsWith('.source'); - const isAllowEdit = dialogId !== 'all' && !isSourceSectionInDialog; - return (
); From dc497f3691bfc8f7b7e40f89ebcb451fa4c1b60a Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Fri, 11 Sep 2020 13:05:50 +0800 Subject: [PATCH 044/105] refactor create qna modal --- .../client/src/components/CreateQnAModal.tsx | 100 +++++------------- .../components/CreationFlow/CreationFlow.tsx | 10 +- .../client/src/pages/design/DesignPage.tsx | 10 +- .../src/pages/knowledge-base/QnAPage.tsx | 6 +- .../client/src/recoilModel/dispatchers/qna.ts | 12 +-- .../server/src/controllers/utilities.ts | 4 +- .../server/src/models/utilities/parser.ts | 21 +--- 7 files changed, 46 insertions(+), 117 deletions(-) diff --git a/Composer/packages/client/src/components/CreateQnAModal.tsx b/Composer/packages/client/src/components/CreateQnAModal.tsx index 1f0fc682d1..5b484edf7d 100644 --- a/Composer/packages/client/src/components/CreateQnAModal.tsx +++ b/Composer/packages/client/src/components/CreateQnAModal.tsx @@ -93,7 +93,7 @@ interface CreateQnAModalProps const DialogTitle = () => { return (
- {formatMessage('Populate your Knowledge Base')} + {formatMessage('Create new knowledge base')}

{formatMessage( @@ -115,31 +115,19 @@ const DialogTitle = () => { }; export interface CreateQnAFormData { - urls: string[]; + url: string; name: string; multiTurn: boolean; } -const validateUrls = (urls: string[]) => { - const errors = Array(urls.length).fill(''); +const validateUrl: FieldValidator = (url: string): string => { + let error = ''; - for (let i = 0; i < urls.length; i++) { - const baseUrl = urls[i].replace(/\/$/, ''); - for (let j = 0; j < urls.length; j++) { - const candidateUrl = urls[j].replace(/\/$/, ''); - if (baseUrl && candidateUrl && baseUrl === candidateUrl && i !== j) { - errors[i] = errors[j] = formatMessage('This url is duplicated'); - } - } - } - - for (let i = 0; i < urls.length; i++) { - if (urls[i] && !urls[i].startsWith('http://') && !urls[i].startsWith('https://')) { - errors[i] = formatMessage('A valid url should start with http:// or https://'); - } + if (url && !url.startsWith('http://') && !url.startsWith('https://')) { + error = formatMessage('A valid url should start with http:// or https://'); } - return errors; + return error; }; const QnANameRegex = /^\w[-\w]*$/; @@ -162,9 +150,9 @@ const validateName = (sources: QnAFile[]): FieldValidator => { }; const formConfig: FieldConfig = { - urls: { + url: { required: true, - defaultValue: [''], + defaultValue: '', }, name: { required: true, @@ -178,12 +166,12 @@ const formConfig: FieldConfig = { export const CreateQnAModal: React.FC = (props) => { const { onDismiss, onSubmit, dialogId, qnaFiles } = props; - const [urlErrors, setUrlErrors] = useState(['']); formConfig.name.validate = validateName(qnaFiles); + formConfig.url.validate = validateUrl; const { formData, updateField, hasErrors, formErrors } = useForm(formConfig); const isQnAFileselected = !(dialogId === 'all'); - const disabled = hasErrors || urlErrors.some((e) => !!e) || formData.urls.some((url) => !url); + const disabled = hasErrors; const updateName = (name = '') => { updateField('name', name); @@ -191,25 +179,8 @@ export const CreateQnAModal: React.FC = (props) => { const updateMultiTurn = (val) => { updateField('multiTurn', val); }; - - const addNewUrl = () => { - const urls = [...formData.urls, '']; - updateField('urls', urls); - setUrlErrors(validateUrls(urls)); - }; - - const updateUrl = (index: number, url = '') => { - const urls = [...formData.urls]; - urls[index] = url; - updateField('urls', urls); - setUrlErrors(validateUrls(urls)); - }; - - const removeUrl = (index: number) => { - const urls = [...formData.urls]; - urls.splice(index, 1); - updateField('urls', urls); - setUrlErrors(validateUrls(urls)); + const updateUrl = (url = '') => { + updateField('url', url); }; return ( @@ -231,45 +202,24 @@ export const CreateQnAModal: React.FC = (props) => { updateName(name)} /> - {formData.urls.map((l, index) => { - return ( -

- updateUrl(index, url)} - /> - {index !== 0 && ( -
- ); - })} - - {formatMessage('Add additional URL')} - + updateUrl(url)} + /> + {!isQnAFileselected && (
{formatMessage('Please select a specific qna file to import QnA')}
)} diff --git a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx index 8812e40d6f..f01a706415 100644 --- a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx +++ b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx @@ -43,7 +43,7 @@ const CreationFlow: React.FC = () => { updateCurrentPathForStorage, updateFolder, saveTemplateId, - importQnAFromUrls, + addQnAKBFromUrl, fetchProjectById, fetchRecentProjects, } = useRecoilValue(dispatcherState); @@ -112,14 +112,12 @@ const CreationFlow: React.FC = () => { saveProjectAs(projectId, formData.name, formData.description, formData.location); }; - const handleCreateQnA = async ({ name, urls, multiTurn }) => { + const handleCreateQnA = async ({ name, url, multiTurn }) => { saveTemplateId(QnABotTemplateId); handleDismiss(); await handleCreateNew(formData, QnABotTemplateId); - // import qna from urls - if (urls.length > 0) { - await importQnAFromUrls({ id: `${formData.name.toLocaleLowerCase()}.${locale}`, name, urls, multiTurn }); - } + // import qna from url + await addQnAKBFromUrl({ id: `${formData.name.toLocaleLowerCase()}.${locale}`, name, url, multiTurn }); }; const handleSubmitOrImportQnA = async (formData, templateId: string) => { diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index b8e6b0f312..84e1aa0ef1 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -143,7 +143,7 @@ const DesignPage: React.FC { + const handleCreateQnA = async ({ name, url, multiTurn }) => { cancelImportQnAModal(); const formData = { $kind: qnaMatcherKey, @@ -586,10 +586,8 @@ const DesignPage: React.FC 0) { - await importQnAFromUrls({ id: `${dialogId}.${locale}`, name, urls, multiTurn }); - } + // import qna from url + await addQnAKBFromUrl({ id: `${dialogId}.${locale}`, name, url, multiTurn }); } }; diff --git a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx index 56f40e6151..9c422ef59e 100644 --- a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx @@ -96,7 +96,7 @@ const QnAPage: React.FC = (props) => { items: [ { 'data-testid': 'FlyoutNewDialog', - key: 'importQnAFromUrls', + key: 'addQnAKBFromUrl', text: formatMessage('Import QnA From Url'), onClick: () => { setCreateQnAModalVisiability(true); @@ -133,9 +133,9 @@ const QnAPage: React.FC = (props) => { setCreateQnAModalVisiability(false); }; - const onSubmit = async ({ name, urls, multiTurn }: CreateQnAFormData) => { + const onSubmit = async ({ name, url, multiTurn }: CreateQnAFormData) => { onDismiss(); - await actions.importQnAFromUrls({ id: `${dialogId}.${locale}`, name, urls, multiTurn }); + await actions.addQnAKBFromUrl({ id: `${dialogId}.${locale}`, name, url, multiTurn }); }; return ( diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index b1fd89b128..e57a61af20 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -148,16 +148,16 @@ export const qnaDispatcher = () => { await removeQnAFileState(callbackHelpers, { id }); }); - const importQnAFromUrls = useRecoilCallback( + const addQnAKBFromUrl = useRecoilCallback( (callbackHelpers: CallbackInterface) => async ({ id, name, - urls, + url, multiTurn, }: { id: string; // dialogId.locale name: string; - urls: string[]; + url: string; multiTurn: boolean; }) => { const { set } = callbackHelpers; @@ -167,7 +167,7 @@ export const qnaDispatcher = () => { let response; try { response = await httpClient.get(`/utilities/qna/parse`, { - params: { urls: encodeURIComponent(urls.join(',')), multiTurn }, + params: { url: encodeURIComponent(url), multiTurn }, }); } catch (err) { setError(callbackHelpers, err); @@ -175,7 +175,7 @@ export const qnaDispatcher = () => { return; } - const contentForSourceQnA = `> !# @source.urls=${urls} + const contentForSourceQnA = `> !# @source.url=${url} > !# @source.multiTurn=${multiTurn} ${response.data} `; @@ -356,6 +356,6 @@ ${response.data} createQnAFile, removeQnAFile, updateQnAFile, - importQnAFromUrls, + addQnAKBFromUrl, }; }; diff --git a/Composer/packages/server/src/controllers/utilities.ts b/Composer/packages/server/src/controllers/utilities.ts index a32a5af76d..72c76c6187 100644 --- a/Composer/packages/server/src/controllers/utilities.ts +++ b/Composer/packages/server/src/controllers/utilities.ts @@ -6,9 +6,9 @@ import { parseQnAContent } from '../models/utilities/parser'; async function getQnaContent(req: Request, res: Response) { try { - const urls = decodeURIComponent(req.query.urls).split(','); + const url = decodeURIComponent(req.query.url); const multiTurn = req.query.multiTurn === 'true' || req.query.multiTurn === true ? true : false; - res.status(200).json(await parseQnAContent(urls, multiTurn)); + res.status(200).json(await parseQnAContent(url, multiTurn)); } catch (e) { res.status(400).json({ message: e.message || e.text, diff --git a/Composer/packages/server/src/models/utilities/parser.ts b/Composer/packages/server/src/models/utilities/parser.ts index 7f4d1b33e5..f28177fe20 100644 --- a/Composer/packages/server/src/models/utilities/parser.ts +++ b/Composer/packages/server/src/models/utilities/parser.ts @@ -55,31 +55,14 @@ async function importQnAFromUrl(builder: any, url: string, subscriptionKey: stri //https://azure.microsoft.com/en-us/pricing/details/cognitive-services/qna-maker/ //limited to 3 transactions per second -export async function parseQnAContent(urls: string[], multiTurn: boolean) { +export async function parseQnAContent(url: string, multiTurn: boolean) { const builder = new qnaBuild.Builder((message: string) => debug(message)); - let qnaContent = ''; - const subscriptionKey = QNA_SUBSCRIPTION_KEY || getBuildEnvironment()?.QNA_SUBSCRIPTION_KEY; if (!subscriptionKey) { throw new Error('Missing subscription key for QnAMaker'); } - const limitedNumInBatch = 3; - let i = 0; - - while (i < urls.length) { - const batchUrls = urls.slice(i, i + limitedNumInBatch); - const contents = await Promise.all( - batchUrls.map(async (url) => { - return await importQnAFromUrl(builder, url, subscriptionKey, multiTurn); - }) - ); - contents.forEach((content) => { - qnaContent += content; - }); - i = i + limitedNumInBatch; - } - return qnaContent; + return await importQnAFromUrl(builder, url, subscriptionKey, multiTurn); } From 31b11521d3a45528c312f7a6a1cdabbe620a5442 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Fri, 11 Sep 2020 16:12:06 +0800 Subject: [PATCH 045/105] ... menuItems for navTree --- .../client/src/components/NavTree.tsx | 62 ++++++++++++++----- .../src/pages/knowledge-base/QnAPage.tsx | 43 ++++++------- 2 files changed, 70 insertions(+), 35 deletions(-) diff --git a/Composer/packages/client/src/components/NavTree.tsx b/Composer/packages/client/src/components/NavTree.tsx index d23544cb33..cc49fd6b4b 100644 --- a/Composer/packages/client/src/components/NavTree.tsx +++ b/Composer/packages/client/src/components/NavTree.tsx @@ -4,11 +4,12 @@ /** @jsx jsx */ import { jsx, css } from '@emotion/core'; import { Resizable, ResizeCallback } from 're-resizable'; -import { DefaultButton } from 'office-ui-fabric-react/lib/Button'; +import { DefaultButton, CommandBarButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button'; import { FontWeights, FontSizes } from 'office-ui-fabric-react/lib/Styling'; -import { NeutralColors } from '@uifabric/fluent-theme'; -import { IButtonStyles } from 'office-ui-fabric-react/lib/Button'; +import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu'; +import { OverflowSet, IOverflowSetItemProps } from 'office-ui-fabric-react/lib/OverflowSet'; import { mergeStyleSets } from 'office-ui-fabric-react/lib/Styling'; +import { NeutralColors } from '@uifabric/fluent-theme'; import { useRecoilValue } from 'recoil'; import { navigateTo } from '../utils/navigation'; @@ -65,6 +66,7 @@ export interface INavTreeItem { name: string; ariaLabel?: string; url: string; + menuItems?: IContextualMenuItem[]; disabled?: boolean; } @@ -95,19 +97,51 @@ const NavTree: React.FC = (props) => {
{navLinks.map((item) => { const isSelected = location.pathname.includes(item.url); + const onRenderOverflowButton = (menuItems: IOverflowSetItemProps[] | undefined): JSX.Element => { + const buttonStyles: Partial = { + root: { + minWidth: 0, + padding: '0 4px', + alignSelf: 'stretch', + height: 'auto', + background: isSelected ? NeutralColors.gray20 : NeutralColors.white, + }, + }; + return ( + + ); + }; return ( - { - e.preventDefault(); - navigateTo(item.url); - }} - /> +
+ { + e.preventDefault(); + navigateTo(item.url); + }} + /> + {item.menuItems && !item.disabled && ( + undefined} + onRenderOverflowButton={onRenderOverflowButton} + /> + )} +
); })}
diff --git a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx index 9c422ef59e..334934e839 100644 --- a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx @@ -50,6 +50,28 @@ const QnAPage: React.FC = (props) => { name: dialog.displayName, ariaLabel: formatMessage('qna file'), url: `/bot/${projectId}/knowledge-base/${dialog.id}`, + menuItems: [ + { + name: formatMessage('Create KB from scratch'), + key: 'Create KB from scratch', + iconProps: { + iconName: 'Add', + }, + onClick: () => { + setCreateQnAModalVisiability(true); + }, + }, + { + name: formatMessage('Create KB from URL or file'), + key: 'Create KB from URL or file', + iconProps: { + iconName: 'Add', + }, + onClick: () => { + setCreateQnAModalVisiability(true); + }, + }, + ], }; }); const mainDialogIndex = newDialogLinks.findIndex((link) => link.id === 'Main'); @@ -84,27 +106,6 @@ const QnAPage: React.FC = (props) => { ); const toolbarItems = [ - { - type: 'dropdown', - text: formatMessage('Add'), - align: 'left', - dataTestid: 'AddFlyout', - buttonProps: { - iconProps: { iconName: 'Add' }, - }, - menuProps: { - items: [ - { - 'data-testid': 'FlyoutNewDialog', - key: 'addQnAKBFromUrl', - text: formatMessage('Import QnA From Url'), - onClick: () => { - setCreateQnAModalVisiability(true); - }, - }, - ], - }, - }, { type: 'element', element: , From 376ad5613b0699a490971609c32113de659eaf27 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Fri, 11 Sep 2020 16:29:11 +0800 Subject: [PATCH 046/105] rename --- .../components/CreationFlow/CreationFlow.tsx | 4 ++-- .../client/src/pages/design/DesignPage.tsx | 4 ++-- .../src/pages/knowledge-base/QnAPage.tsx | 8 +------- .../src/pages/knowledge-base/table-view.tsx | 10 +++++----- .../client/src/recoilModel/dispatchers/qna.ts | 18 +++++++++--------- .../packages/lib/indexers/src/utils/qnaUtil.ts | 2 +- 6 files changed, 20 insertions(+), 26 deletions(-) diff --git a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx index f01a706415..d641ac4ed1 100644 --- a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx +++ b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx @@ -43,7 +43,7 @@ const CreationFlow: React.FC = () => { updateCurrentPathForStorage, updateFolder, saveTemplateId, - addQnAKBFromUrl, + createQnAKBFromUrl, fetchProjectById, fetchRecentProjects, } = useRecoilValue(dispatcherState); @@ -117,7 +117,7 @@ const CreationFlow: React.FC = () => { handleDismiss(); await handleCreateNew(formData, QnABotTemplateId); // import qna from url - await addQnAKBFromUrl({ id: `${formData.name.toLocaleLowerCase()}.${locale}`, name, url, multiTurn }); + await createQnAKBFromUrl({ id: `${formData.name.toLocaleLowerCase()}.${locale}`, name, url, multiTurn }); }; const handleSubmitOrImportQnA = async (formData, templateId: string) => { diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 84e1aa0ef1..799659a48b 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -143,7 +143,7 @@ const DesignPage: React.FC = (props) => { { name: formatMessage('Create KB from scratch'), key: 'Create KB from scratch', - iconProps: { - iconName: 'Add', - }, onClick: () => { setCreateQnAModalVisiability(true); }, @@ -64,9 +61,6 @@ const QnAPage: React.FC = (props) => { { name: formatMessage('Create KB from URL or file'), key: 'Create KB from URL or file', - iconProps: { - iconName: 'Add', - }, onClick: () => { setCreateQnAModalVisiability(true); }, @@ -136,7 +130,7 @@ const QnAPage: React.FC = (props) => { const onSubmit = async ({ name, url, multiTurn }: CreateQnAFormData) => { onDismiss(); - await actions.addQnAKBFromUrl({ id: `${dialogId}.${locale}`, name, url, multiTurn }); + await actions.createQnAKBFromUrl({ id: `${dialogId}.${locale}`, name, url, multiTurn }); }; return ( diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index d09083f790..efbafd00d7 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -64,13 +64,13 @@ const TableView: React.FC = (props) => { const locale = useRecoilValue(localeState); const settings = useRecoilValue(settingsState); const { - // addQnAImport, + // createQnAImport, removeQnAImport, removeQnAFile, // setMessage, - addQnAPairs, + createQnAPairs, removeQnAPairs, - addQnAQuestion, + createQnAQuestion, // removeQnAQuestion, updateQnAAnswer, updateQnAQuestion, @@ -167,7 +167,7 @@ const TableView: React.FC = (props) => { const newQnAPair = qnaUtil.generateQnAPair(); const sectionIndex = qnaSections.findIndex((item) => item.fileId === fileId); setFocusedIndex(sectionIndex + 1); - addQnAPairs({ id: fileId, content: newQnAPair }); + createQnAPairs({ id: fileId, content: newQnAPair }); }; console.log(focusedIndex); @@ -181,7 +181,7 @@ const TableView: React.FC = (props) => { sectionId, content: 'Add new question', }; - addQnAQuestion(payload); + createQnAQuestion(payload); } }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index e57a61af20..9b4e5181b1 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -148,7 +148,7 @@ export const qnaDispatcher = () => { await removeQnAFileState(callbackHelpers, { id }); }); - const addQnAKBFromUrl = useRecoilCallback( + const createQnAKBFromUrl = useRecoilCallback( (callbackHelpers: CallbackInterface) => async ({ id, name, @@ -239,7 +239,7 @@ ${response.data} } ); - const addQnAQuestion = useRecoilCallback( + const createQnAQuestion = useRecoilCallback( ({ set, snapshot }: CallbackInterface) => async ({ id, sectionId, @@ -253,7 +253,7 @@ ${response.data} const qnaFile = qnaFiles.find((temp) => temp.id === id); if (!qnaFile) return qnaFiles; - const updatedFile = qnaUtil.addQnAQuestion(qnaFile, sectionId, content); + const updatedFile = qnaUtil.createQnAQuestion(qnaFile, sectionId, content); set(qnaFilesState, (qnaFiles) => { return qnaFiles.map((file) => { return file.id === id ? updatedFile : file; @@ -285,7 +285,7 @@ ${response.data} } ); - const addQnAPairs = useRecoilCallback( + const createQnAPairs = useRecoilCallback( ({ set, snapshot }: CallbackInterface) => async ({ id, content }: { id: string; content: string }) => { const qnaFiles = await snapshot.getPromise(qnaFilesState); const qnaFile = qnaFiles.find((temp) => temp.id === id); @@ -316,7 +316,7 @@ ${response.data} } ); - const addQnAImport = useRecoilCallback( + const createQnAImport = useRecoilCallback( ({ set, snapshot }: CallbackInterface) => async ({ id, sourceId }: { id: string; sourceId: string }) => { const qnaFiles = await snapshot.getPromise(qnaFilesState); const qnaFile = qnaFiles.find((temp) => temp.id === id); @@ -345,17 +345,17 @@ ${response.data} } ); return { - addQnAImport, + createQnAImport, removeQnAImport, - addQnAPairs, + createQnAPairs, removeQnAPairs, - addQnAQuestion, + createQnAQuestion, removeQnAQuestion, updateQnAQuestion, updateQnAAnswer, createQnAFile, removeQnAFile, updateQnAFile, - addQnAKBFromUrl, + createQnAKBFromUrl, }; }; diff --git a/Composer/packages/lib/indexers/src/utils/qnaUtil.ts b/Composer/packages/lib/indexers/src/utils/qnaUtil.ts index 20f79455a2..b03c473c68 100644 --- a/Composer/packages/lib/indexers/src/utils/qnaUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/qnaUtil.ts @@ -281,7 +281,7 @@ export function updateQnASection(qnaFile: QnAFile, sectionId: string, changes: Q return updateSection(qnaFile, sectionId, targetSectionContent); } -export function addQnAQuestion(qnaFile: QnAFile, sectionId: string, questionContent: string) { +export function createQnAQuestion(qnaFile: QnAFile, sectionId: string, questionContent: string) { const changes: QnASectionChanges = { Questions: [ { From fb1169c067122351011b1a124da776b757530145 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Fri, 11 Sep 2020 17:39:28 +0800 Subject: [PATCH 047/105] create from scratch & url --- .../components/createQnAModal.test.tsx | 8 +- .../components/CreationFlow/CreationFlow.tsx | 4 +- .../client/src/components/NavTree.tsx | 2 +- .../QnA/CreateQnAFromScratchModal.tsx | 111 ++++++++++++ .../CreateQnAFromUrlModal.tsx} | 159 ++++-------------- .../client/src/components/QnA/constants.ts | 41 +++++ .../client/src/components/QnA/index.ts | 5 + .../client/src/components/QnA/styles.ts | 49 ++++++ Composer/packages/client/src/constants.ts | 5 - .../client/src/pages/design/DesignPage.tsx | 4 +- .../src/pages/knowledge-base/QnAPage.tsx | 51 ++++-- .../src/pages/knowledge-base/table-view.tsx | 51 ++++-- 12 files changed, 319 insertions(+), 171 deletions(-) create mode 100644 Composer/packages/client/src/components/QnA/CreateQnAFromScratchModal.tsx rename Composer/packages/client/src/components/{CreateQnAModal.tsx => QnA/CreateQnAFromUrlModal.tsx} (64%) create mode 100644 Composer/packages/client/src/components/QnA/constants.ts create mode 100644 Composer/packages/client/src/components/QnA/index.ts create mode 100644 Composer/packages/client/src/components/QnA/styles.ts diff --git a/Composer/packages/client/__tests__/components/createQnAModal.test.tsx b/Composer/packages/client/__tests__/components/createQnAModal.test.tsx index b73c9dd275..c868addc52 100644 --- a/Composer/packages/client/__tests__/components/createQnAModal.test.tsx +++ b/Composer/packages/client/__tests__/components/createQnAModal.test.tsx @@ -5,20 +5,20 @@ import React from 'react'; import { fireEvent } from '@bfc/test-utils'; import { renderWithRecoil } from '../testUtils/renderWithRecoil'; -import CreateQnAModal from '../../src/components/CreateQnAModal'; +import CreateQnAFromUrlModal from '../../src/components/QnA/CreateQnAFromUrlModal'; -describe('', () => { +describe('', () => { const onDismiss = jest.fn(() => {}); const onSubmit = jest.fn(() => {}); let container; beforeEach(() => { container = renderWithRecoil( - , + , () => {} ); }); - it('renders and create from scratch', () => { + it('renders and create from scratch', () => { const { getByText } = container; expect(getByText('Populate your Knowledge Base')).not.toBeNull(); const createFromScratchButton = getByText('Create knowledge base from scratch'); diff --git a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx index d641ac4ed1..7d9e2a116c 100644 --- a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx +++ b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx @@ -22,7 +22,7 @@ import { import Home from '../../pages/home/Home'; import { QnABotTemplateId } from '../../constants'; import { useProjectIdCache } from '../../utils/hooks'; -import CreateQnAModal from '../CreateQnAModal'; +import CreateQnAFromUrlModal from '../QnA/CreateQnAFromUrlModal'; import { CreateOptions } from './CreateOptions'; import { OpenProject } from './OpenProject'; @@ -177,7 +177,7 @@ const CreationFlow: React.FC = () => { onDismiss={handleDismiss} onOpen={openBot} /> - = (props) => { /> {item.menuItems && !item.disabled && ( { + dialogId: string; + qnaFiles: QnAFile[]; + subscriptionKey?: string; + onDismiss: () => void; + onSubmit: (formData: CreateQnAFromScratchFormData) => void; +} + +export interface CreateQnAFromScratchFormData { + name: string; +} + +const formConfig: FieldConfig = { + name: { + required: true, + defaultValue: '', + }, +}; + +const DialogTitle = () => { + return ( +
+ {formatMessage('Create new knowledge base from scratch')} +

+ {formatMessage('Manually add question and answer pairs to create a KB')} +

+
+ ); +}; + +export const CreateQnAFromScratchModal: React.FC = (props) => { + const { onDismiss, onSubmit, qnaFiles } = props; + + formConfig.name.validate = validateName(qnaFiles); + const { formData, updateField, hasErrors, formErrors } = useForm(formConfig); + const disabled = hasErrors; + + const updateName = (name = '') => { + updateField('name', name); + }; + + return ( + , + styles: styles.dialog, + }} + hidden={false} + modalProps={{ + isBlocking: false, + styles: styles.modal, + }} + onDismiss={onDismiss} + > +
+ + updateName(name)} + /> + +
+ + + { + if (hasErrors) { + return; + } + onSubmit(formData); + }} + /> + +
+ ); +}; + +export default CreateQnAFromScratchModal; diff --git a/Composer/packages/client/src/components/CreateQnAModal.tsx b/Composer/packages/client/src/components/QnA/CreateQnAFromUrlModal.tsx similarity index 64% rename from Composer/packages/client/src/components/CreateQnAModal.tsx rename to Composer/packages/client/src/components/QnA/CreateQnAFromUrlModal.tsx index 5b484edf7d..64c0f8cb5d 100644 --- a/Composer/packages/client/src/components/CreateQnAModal.tsx +++ b/Composer/packages/client/src/components/QnA/CreateQnAFromUrlModal.tsx @@ -2,84 +2,24 @@ // Licensed under the MIT License. /** @jsx jsx */ -import { jsx, css } from '@emotion/core'; -import React, { useState } from 'react'; +import { jsx } from '@emotion/core'; +import React from 'react'; import formatMessage from 'format-message'; import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; import { Stack } from 'office-ui-fabric-react/lib/Stack'; import { TextField } from 'office-ui-fabric-react/lib/TextField'; import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; -import { PrimaryButton, DefaultButton, ActionButton } from 'office-ui-fabric-react/lib/Button'; -import { Link } from 'office-ui-fabric-react/lib/Link'; -import { FontWeights } from '@uifabric/styling'; -import { FontSizes, SharedColors, NeutralColors } from '@uifabric/fluent-theme'; +import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; import { RouteComponentProps } from '@reach/router'; import { QnAFile } from '@bfc/shared'; +import { Link } from 'office-ui-fabric-react/lib/Link'; -import { QnAMakerLearningUrl, knowledgeBaseSourceUrl } from '../constants'; -import { FieldConfig, useForm, FieldValidator } from '../hooks/useForm'; - -const styles = { - dialog: { - title: { - fontWeight: FontWeights.bold, - fontSize: FontSizes.size20, - paddingTop: '14px', - paddingBottom: '11px', - }, - subText: { - fontSize: FontSizes.size14, - }, - }, - modal: { - main: { - maxWidth: '800px !important', - }, - }, -}; - -const dialogWindow = css` - display: flex; - flex-direction: column; - width: 400px; - min-height: 200px; -`; - -const textField = { - root: { - width: '400px', - paddingBottom: '20px', - }, -}; - -const warning = { - color: SharedColors.red10, - fontSize: FontSizes.size10, -}; - -const actionButton = css` - font-size: 16px; - padding-left: 0px; - margin-left: -5px; -`; - -const urlContainer = css` - display: flex; - width: 444px; -`; - -const cancel = css` - margin-top: -3px; - margin-left: 10px; -`; +import { FieldConfig, useForm } from '../../hooks/useForm'; -const subText = css` - color: ${NeutralColors.gray130}; - font-size: 14px; - font-weight: 400; -`; +import { knowledgeBaseSourceUrl, QnAMakerLearningUrl, validateUrl, validateName } from './constants'; +import { subText, styles, dialogWindow, textField, warning } from './styles'; -interface CreateQnAModalProps +interface CreateQnAFromUrlModalProps extends RouteComponentProps<{ location: string; }> { @@ -87,9 +27,30 @@ interface CreateQnAModalProps qnaFiles: QnAFile[]; subscriptionKey?: string; onDismiss: () => void; - onSubmit: (formData: CreateQnAFormData) => void; + onSubmit: (formData: CreateQnAFromUrlFormData) => void; } +export interface CreateQnAFromUrlFormData { + url: string; + name: string; + multiTurn: boolean; +} + +const formConfig: FieldConfig = { + url: { + required: true, + defaultValue: '', + }, + name: { + required: true, + defaultValue: '', + }, + multiTurn: { + required: false, + defaultValue: false, + }, +}; + const DialogTitle = () => { return (
@@ -114,57 +75,7 @@ const DialogTitle = () => { ); }; -export interface CreateQnAFormData { - url: string; - name: string; - multiTurn: boolean; -} - -const validateUrl: FieldValidator = (url: string): string => { - let error = ''; - - if (url && !url.startsWith('http://') && !url.startsWith('https://')) { - error = formatMessage('A valid url should start with http:// or https://'); - } - - return error; -}; - -const QnANameRegex = /^\w[-\w]*$/; - -const validateName = (sources: QnAFile[]): FieldValidator => { - return (name: string) => { - let currentError = ''; - if (name) { - if (!QnANameRegex.test(name)) { - currentError = formatMessage('Name contains invalid charactors'); - } - - const duplicatedItemIndex = sources.findIndex((item) => item.name === name); - if (duplicatedItemIndex > -1) { - currentError = formatMessage('Duplicate imported QnA name'); - } - } - return currentError; - }; -}; - -const formConfig: FieldConfig = { - url: { - required: true, - defaultValue: '', - }, - name: { - required: true, - defaultValue: '', - }, - multiTurn: { - required: false, - defaultValue: false, - }, -}; - -export const CreateQnAModal: React.FC = (props) => { +export const CreateQnAFromUrlModal: React.FC = (props) => { const { onDismiss, onSubmit, dialogId, qnaFiles } = props; formConfig.name.validate = validateName(qnaFiles); @@ -235,13 +146,13 @@ export const CreateQnAModal: React.FC = (props) => { {window.location.href.indexOf('/knowledge-base/') == -1 && ( { if (hasErrors) { return; } - onSubmit({} as CreateQnAFormData); + onSubmit({} as CreateQnAFromUrlFormData); }} /> )} @@ -249,7 +160,7 @@ export const CreateQnAModal: React.FC = (props) => { { if (hasErrors) { return; @@ -262,4 +173,4 @@ export const CreateQnAModal: React.FC = (props) => { ); }; -export default CreateQnAModal; +export default CreateQnAFromUrlModal; diff --git a/Composer/packages/client/src/components/QnA/constants.ts b/Composer/packages/client/src/components/QnA/constants.ts new file mode 100644 index 0000000000..90e2943366 --- /dev/null +++ b/Composer/packages/client/src/components/QnA/constants.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { QnAFile } from '@bfc/shared'; +import formatMessage from 'format-message'; + +import { FieldValidator } from '../../hooks/useForm'; + +export const validateUrl: FieldValidator = (url: string): string => { + let error = ''; + + if (url && !url.startsWith('http://') && !url.startsWith('https://')) { + error = formatMessage('A valid url should start with http:// or https://'); + } + + return error; +}; + +export const QnANameRegex = /^\w[-\w]*$/; + +export const validateName = (sources: QnAFile[]): FieldValidator => { + return (name: string) => { + let currentError = ''; + if (name) { + if (!QnANameRegex.test(name)) { + currentError = formatMessage('Name contains invalid charactors'); + } + + const duplicatedItemIndex = sources.findIndex((item) => item.name === name); + if (duplicatedItemIndex > -1) { + currentError = formatMessage('Duplicate imported QnA name'); + } + } + return currentError; + }; +}; + +export const knowledgeBaseSourceUrl = + 'https://docs.microsoft.com/en-us/azure/cognitive-services/qnamaker/concepts/content-types'; + +export const QnAMakerLearningUrl = 'https://azure.microsoft.com/en-us/pricing/details/cognitive-services/qna-maker/'; diff --git a/Composer/packages/client/src/components/QnA/index.ts b/Composer/packages/client/src/components/QnA/index.ts new file mode 100644 index 0000000000..07c20e8bab --- /dev/null +++ b/Composer/packages/client/src/components/QnA/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export * from './CreateQnAFromScratchModal'; +export * from './CreateQnAFromUrlModal'; diff --git a/Composer/packages/client/src/components/QnA/styles.ts b/Composer/packages/client/src/components/QnA/styles.ts new file mode 100644 index 0000000000..f047c028af --- /dev/null +++ b/Composer/packages/client/src/components/QnA/styles.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { css } from '@emotion/core'; +import { FontWeights } from '@uifabric/styling'; +import { FontSizes, SharedColors, NeutralColors } from '@uifabric/fluent-theme'; + +export const styles = { + dialog: { + title: { + fontWeight: FontWeights.bold, + fontSize: FontSizes.size20, + paddingTop: '14px', + paddingBottom: '11px', + }, + subText: { + fontSize: FontSizes.size14, + }, + }, + modal: { + main: { + maxWidth: '800px !important', + }, + }, +}; + +export const dialogWindow = css` + display: flex; + flex-direction: column; + width: 400px; + min-height: 200px; +`; + +export const textField = { + root: { + width: '400px', + paddingBottom: '20px', + }, +}; + +export const warning = { + color: SharedColors.red10, + fontSize: FontSizes.size10, +}; + +export const subText = css` + color: ${NeutralColors.gray130}; + font-size: 14px; + font-weight: 400; +`; diff --git a/Composer/packages/client/src/constants.ts b/Composer/packages/client/src/constants.ts index 0b86f43e99..3b0a2d47a5 100644 --- a/Composer/packages/client/src/constants.ts +++ b/Composer/packages/client/src/constants.ts @@ -205,8 +205,3 @@ export const nameRegex = /^[a-zA-Z0-9-_]+$/; export const triggerNotSupportedWarning = formatMessage( 'This trigger type is not supported by the RegEx recognizer. To ensure this trigger is fired, change the recognizer type.' ); - -export const knowledgeBaseSourceUrl = - 'https://docs.microsoft.com/en-us/azure/cognitive-services/qnamaker/concepts/content-types'; - -export const QnAMakerLearningUrl = 'https://azure.microsoft.com/en-us/pricing/details/cognitive-services/qna-maker/'; diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 799659a48b..37e9dbca6e 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -54,7 +54,7 @@ import { getBaseName } from '../../utils/fileUtil'; import { validatedDialogsSelector } from '../../recoilModel/selectors/validatedDialogs'; import plugins, { mergePluginConfigs } from '../../plugins'; import { useElectronFeatures } from '../../hooks/useElectronFeatures'; -import CreateQnAModal from '../../components/CreateQnAModal'; +import CreateQnAFromUrlModal from '../../components/QnA/CreateQnAFromUrlModal'; import { triggerNotSupported } from '../../utils/dialogValidator'; import { WarningMessage } from './WarningMessage'; @@ -700,7 +700,7 @@ const DesignPage: React.FC )} {importQnAModalVisibility && ( - = (props) => { const locale = 'en-us'; //const locale = useRecoilValue(localeState); const qnaAllUpViewStatus = useRecoilValue(qnaAllUpViewStatusState); - const [createQnAModalVisiability, setCreateQnAModalVisiability] = useState(false); + const [createQnAModalFromUrlVisiability, setCreateQnAModalFromUrlVisiability] = useState(false); + const [createQnAModalFromScratchVisiability, setCreateQnAModalFromScratchVisiability] = useState(false); const path = props.location?.pathname ?? ''; const { dialogId = '' } = props; @@ -55,14 +61,14 @@ const QnAPage: React.FC = (props) => { name: formatMessage('Create KB from scratch'), key: 'Create KB from scratch', onClick: () => { - setCreateQnAModalVisiability(true); + setCreateQnAModalFromScratchVisiability(true); }, }, { name: formatMessage('Create KB from URL or file'), key: 'Create KB from URL or file', onClick: () => { - setCreateQnAModalVisiability(true); + setCreateQnAModalFromUrlVisiability(true); }, }, ], @@ -124,15 +130,6 @@ const QnAPage: React.FC = (props) => { return null; }; - const onDismiss = () => { - setCreateQnAModalVisiability(false); - }; - - const onSubmit = async ({ name, url, multiTurn }: CreateQnAFormData) => { - onDismiss(); - await actions.createQnAKBFromUrl({ id: `${dialogId}.${locale}`, name, url, multiTurn }); - }; - return ( = (props) => { {qnaAllUpViewStatus === QnAAllUpViewStatus.Loading && ( )} - {createQnAModalVisiability && ( - + {createQnAModalFromUrlVisiability && ( + { + setCreateQnAModalFromUrlVisiability(false); + }} + onSubmit={async ({ name, url, multiTurn }: CreateQnAFromUrlFormData) => { + setCreateQnAModalFromUrlVisiability(false); + await actions.createQnAKBFromUrl({ id: `${dialogId}.${locale}`, name, url, multiTurn }); + }} + /> + )} + + {createQnAModalFromScratchVisiability && ( + { + setCreateQnAModalFromScratchVisiability(false); + }} + onSubmit={async ({ name }: CreateQnAFromUrlFormData) => { + setCreateQnAModalFromScratchVisiability(false); + // await actions.createQnAKBFromUrl({ id: `${dialogId}.${locale}`, name, url, multiTurn }); + }} + /> )} diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index efbafd00d7..a3183b6531 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -4,7 +4,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import { useRecoilValue } from 'recoil'; -import React, { useEffect, useState, useCallback, useMemo, Fragment } from 'react'; +import React, { useEffect, useState, useCallback, Fragment } from 'react'; import { DetailsList, DetailsRow, @@ -150,7 +150,34 @@ const TableView: React.FC = (props) => { setQnASections(newSections); }; - const deleteQnASection = (fileId: string, sectionId: string) => { + const onUpdateQnAQuestion = (fileId: string, sectionId: string, questionId: string, content: string) => { + if (!fileId) return; + actions.setMessage('item deleted'); + const sectionIndex = qnaSections.findIndex((item) => item.fileId === fileId); + setFocusedIndex(sectionIndex); + + updateQnAQuestion({ + id: fileId, + sectionId, + questionId, + content, + }); + }; + + const onUpdateQnAAnswer = (fileId: string, sectionId: string, content: string) => { + if (!fileId) return; + actions.setMessage('item deleted'); + const sectionIndex = qnaSections.findIndex((item) => item.fileId === fileId); + setFocusedIndex(sectionIndex); + + updateQnAAnswer({ + id: fileId, + sectionId, + content, + }); + }; + + const onRemoveQnAPairs = (fileId: string, sectionId: string) => { if (!fileId) return; actions.setMessage('item deleted'); const sectionIndex = qnaSections.findIndex((item) => item.fileId === fileId); @@ -170,7 +197,6 @@ const TableView: React.FC = (props) => { createQnAPairs({ id: fileId, content: newQnAPair }); }; - console.log(focusedIndex); const onCreateNewQuestion = (fileId, sectionId) => { if (qnaFile) { const sectionIndex = qnaSections.findIndex((item) => item.sectionId === sectionId); @@ -278,8 +304,6 @@ const TableView: React.FC = (props) => { return null; }; - const getKeyCallback = useCallback((item) => item.uuid, []); - const getTableColums = () => { const tableColums = [ { @@ -343,12 +367,7 @@ const TableView: React.FC = (props) => { const newValue = value?.trim().replace(/^#/, ''); const isChanged = question.content !== newValue; if (newValue && isChanged) { - updateQnAQuestion({ - id: item.fileId, - sectionId: item.sectionId, - questionId: question.id, - content: newValue, - }); + onUpdateQnAQuestion(item.fileId, item.sectionId, question.id, newValue); } }} onChange={() => {}} @@ -386,11 +405,7 @@ const TableView: React.FC = (props) => { const newValue = value?.trim().replace(/^#/, ''); const isChanged = item.Answer !== newValue; if (newValue && isChanged) { - updateQnAAnswer({ - id: item.fileId, - sectionId: item.sectionId, - content: newValue, - }); + onUpdateQnAAnswer(item.fileId, item.sectionId, newValue); } }} onChange={() => {}} @@ -417,7 +432,7 @@ const TableView: React.FC = (props) => { styles={icon} title="Delete" onClick={() => { - deleteQnASection(item.fileId, item.sectionId); + onRemoveQnAPairs(item.fileId, item.sectionId); }} /> ); @@ -480,6 +495,7 @@ const TableView: React.FC = (props) => { const name = getBaseName(id); groups.push({ key: id, name, startIndex, count: qnaSections.length, level: 0 }); }); + return groups; } }; @@ -515,7 +531,6 @@ const TableView: React.FC = (props) => { Date: Mon, 14 Sep 2020 10:46:07 +0800 Subject: [PATCH 048/105] clean up --- .../client/__tests__/utils/qnaUtil.test.ts | 92 ------------------- .../QnA/CreateQnAFromScratchModal.tsx | 6 +- .../src/pages/knowledge-base/QnAPage.tsx | 3 +- .../src/pages/knowledge-base/table-view.tsx | 4 +- .../pages/notifications/useNotifications.tsx | 8 +- .../client/src/recoilModel/dispatchers/qna.ts | 17 ++++ .../parsers/workers/qnaParser.worker.ts | 2 +- 7 files changed, 27 insertions(+), 105 deletions(-) delete mode 100644 Composer/packages/client/__tests__/utils/qnaUtil.test.ts diff --git a/Composer/packages/client/__tests__/utils/qnaUtil.test.ts b/Composer/packages/client/__tests__/utils/qnaUtil.test.ts deleted file mode 100644 index db964cc45b..0000000000 --- a/Composer/packages/client/__tests__/utils/qnaUtil.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { qnaIndexer } from '@bfc/indexers/src/qnaIndexer'; - -import { - addSection, - updateSection, - removeSection, - insertSection, - addQuestion, - updateQuestion, - updateAnswer, -} from '../../src/utils/qnaUtil'; - -describe('qna utils', () => { - const qnaPair1 = ` -# ?Question1 -\`\`\` -Answer1 -\`\`\``; - const qnaPair2 = ` -# ?Question2 -\`\`\` -Answer2 -\`\`\``; - - it('should add QnA Section', () => { - const newContent = addSection(qnaPair1, qnaPair2); - const res = qnaIndexer.parse(newContent); - const qnaSections = res.qnaSections; - expect(qnaSections.length).toBe(2); - }); - - it('should update QnA Section', () => { - const newContent = updateSection(0, qnaPair1, qnaPair2); - const res = qnaIndexer.parse(newContent); - const qnaSections = res.qnaSections; - expect(qnaSections.length).toBe(1); - expect(qnaSections[0].Questions[0].content).toBe('Question2'); - expect(qnaSections[0].Answer).toBe('Answer2'); - }); - - it('should remove QnA Section', () => { - const content = qnaPair1 + '\n' + qnaPair2; - const newContent = removeSection(0, content); - const res = qnaIndexer.parse(newContent); - const qnaSections = res.qnaSections; - expect(qnaSections.length).toBe(1); - expect(qnaSections[0].Questions[0].content).toBe('Question2'); - expect(qnaSections[0].Answer).toBe('Answer2'); - }); - - it('should insert QnA Section at the beginning', () => { - const newContent = insertSection(0, qnaPair1, qnaPair2); - const res = qnaIndexer.parse(newContent); - const qnaSections = res.qnaSections; - expect(qnaSections.length).toBe(2); - expect(qnaSections[0].Questions[0].content).toBe('Question2'); - expect(qnaSections[0].Answer).toBe('Answer2'); - }); - - it('should add a new Question to QnA Sectionn', () => { - const qnaSections = qnaIndexer.parse(qnaPair1).qnaSections; - const newContent = addQuestion('Question2', qnaSections, 0); - const newQnaSections = qnaIndexer.parse(newContent).qnaSections; - expect(newQnaSections.length).toBe(1); - expect(newQnaSections[0].Questions[0].content).toBe('Question1'); - expect(newQnaSections[0].Questions[1].content).toBe('Question2'); - expect(newQnaSections[0].Answer).toBe('Answer1'); - }); - - it('should update Question to QnA Sectionn', () => { - const qnaSections = qnaIndexer.parse(qnaPair1).qnaSections; - const newContent = updateQuestion('Question2', 0, qnaSections, 0); - const newQnaSections = qnaIndexer.parse(newContent).qnaSections; - expect(newQnaSections.length).toBe(1); - expect(newQnaSections[0].Questions.length).toBe(1); - expect(newQnaSections[0].Questions[0].content).toBe('Question2'); - expect(newQnaSections[0].Answer).toBe('Answer1'); - }); - - it('should update Answer to QnA Sectionn', () => { - const qnaSections = qnaIndexer.parse(qnaPair1).qnaSections; - const newContent = updateAnswer('Answer2', qnaSections, 0); - const newQnaSections = qnaIndexer.parse(newContent).qnaSections; - expect(newQnaSections.length).toBe(1); - expect(newQnaSections[0].Questions.length).toBe(1); - expect(newQnaSections[0].Questions[0].content).toBe('Question1'); - expect(newQnaSections[0].Answer).toBe('Answer2'); - }); -}); diff --git a/Composer/packages/client/src/components/QnA/CreateQnAFromScratchModal.tsx b/Composer/packages/client/src/components/QnA/CreateQnAFromScratchModal.tsx index 8e6b55bac5..98b1bdf1c9 100644 --- a/Composer/packages/client/src/components/QnA/CreateQnAFromScratchModal.tsx +++ b/Composer/packages/client/src/components/QnA/CreateQnAFromScratchModal.tsx @@ -8,16 +8,14 @@ import formatMessage from 'format-message'; import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; import { Stack } from 'office-ui-fabric-react/lib/Stack'; import { TextField } from 'office-ui-fabric-react/lib/TextField'; -import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; import { RouteComponentProps } from '@reach/router'; import { QnAFile } from '@bfc/shared'; -import { Link } from 'office-ui-fabric-react/lib/Link'; import { FieldConfig, useForm } from '../../hooks/useForm'; -import { knowledgeBaseSourceUrl, QnAMakerLearningUrl, validateUrl, validateName } from './constants'; -import { subText, styles, dialogWindow, textField, warning } from './styles'; +import { validateName } from './constants'; +import { subText, styles, dialogWindow, textField } from './styles'; interface CreateQnAFromScratchModalProps extends RouteComponentProps<{ diff --git a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx index 7c6e5b3d77..980c6ad50b 100644 --- a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx @@ -171,8 +171,9 @@ const QnAPage: React.FC = (props) => { onDismiss={() => { setCreateQnAModalFromScratchVisiability(false); }} - onSubmit={async ({ name }: CreateQnAFromUrlFormData) => { + onSubmit={async ({ name }: CreateQnAFromScratchFormData) => { setCreateQnAModalFromScratchVisiability(false); + console.log(name); // await actions.createQnAKBFromUrl({ id: `${dialogId}.${locale}`, name, url, multiTurn }); }} /> diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index a3183b6531..4bc41e4552 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -254,7 +254,6 @@ const TableView: React.FC = (props) => { key: 'edit', name: formatMessage('Show code'), onClick: () => { - if (!qnaFile) return; navigateTo(`/bot/${projectId}/knowledge-base/${dialogId}/${groupFileId}/edit`); }, }, @@ -288,7 +287,6 @@ const TableView: React.FC = (props) => { iconProps={{ iconName: 'Add', styles: addIcon }} styles={AddTemplateButton} onClick={() => { - if (!qnaFile) return; onCreateNewQnAPairs(props.group?.key); actions.setMessage('item added'); }} @@ -337,7 +335,7 @@ const TableView: React.FC = (props) => { const questions = item.Questions; const showingQuestions = item.expand ? questions : questions.slice(0, limitedNumber); //This question content of this qna Section is '#?' - const isQuestionEmpty = showingQuestions.length === 1 && showingQuestions[0].content === ''; + // const isQuestionEmpty = showingQuestions.length === 1 && showingQuestions[0].content === ''; const isSourceSectionInDialog = item.fileId.endsWith('.source') && !dialogId.endsWith('.source'); const isAllowEdit = dialogId !== 'all' && !isSourceSectionInDialog; diff --git a/Composer/packages/client/src/pages/notifications/useNotifications.tsx b/Composer/packages/client/src/pages/notifications/useNotifications.tsx index 77d5383b26..26021e6d4a 100644 --- a/Composer/packages/client/src/pages/notifications/useNotifications.tsx +++ b/Composer/packages/client/src/pages/notifications/useNotifications.tsx @@ -70,25 +70,25 @@ export default function useNotifications(filter?: string) { }); dialogs.forEach((dialog) => { - dialog.diagnostics.map((diagnostic) => { + dialog.diagnostics.forEach((diagnostic) => { const location = `${dialog.id}.dialog`; notifications.push(new DialogNotification(projectId, dialog.id, location, diagnostic)); }); }); getReferredLuFiles(luFiles, dialogs).forEach((lufile) => { - lufile.diagnostics.map((diagnostic) => { + lufile.diagnostics.forEach((diagnostic) => { const location = `${lufile.id}.lu`; notifications.push(new LuNotification(projectId, lufile.id, location, diagnostic, lufile, dialogs)); }); }); lgFiles.forEach((lgFile) => { - lgFile.diagnostics.map((diagnostic) => { + lgFile.diagnostics.forEach((diagnostic) => { const location = `${lgFile.id}.lg`; notifications.push(new LgNotification(projectId, lgFile.id, location, diagnostic, lgFile, dialogs)); }); }); qnaFiles.forEach((qnaFile) => { - get(qnaFile, 'diagnostics', []).map((diagnostic) => { + get(qnaFile, 'diagnostics', []).forEach((diagnostic) => { const location = `${qnaFile.id}.qna`; notifications.push(new QnANotification(projectId, qnaFile.id, location, diagnostic)); }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index 9b4e5181b1..276826ee2b 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -190,6 +190,22 @@ ${response.data} } ); + const createQnAKBFromScratch = useRecoilCallback( + (callbackHelpers: CallbackInterface) => async ({ + id, + name, + }: { + id: string; // dialogId.locale + name: string; + }) => { + await createSourceQnAFileState(callbackHelpers, { + id, + name, + content: '', + }); + } + ); + const updateQnAQuestion = useRecoilCallback( ({ set, snapshot }: CallbackInterface) => async ({ id, @@ -357,5 +373,6 @@ ${response.data} removeQnAFile, updateQnAFile, createQnAKBFromUrl, + createQnAKBFromScratch, }; }; diff --git a/Composer/packages/client/src/recoilModel/parsers/workers/qnaParser.worker.ts b/Composer/packages/client/src/recoilModel/parsers/workers/qnaParser.worker.ts index 7dc3481f4a..2fb2e8db25 100644 --- a/Composer/packages/client/src/recoilModel/parsers/workers/qnaParser.worker.ts +++ b/Composer/packages/client/src/recoilModel/parsers/workers/qnaParser.worker.ts @@ -7,7 +7,7 @@ import { QnAActionType } from './../types'; const ctx: Worker = self as any; const parse = (content: string, id: string) => { - return { id, content, ...qnaIndexer.parse(content, id) }; + return qnaIndexer.parse(content, id); }; ctx.onmessage = function (msg) { From 17c3ee6ab07cf91a04a21a449f3561e235488457 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Mon, 14 Sep 2020 15:16:31 +0800 Subject: [PATCH 049/105] fix build --- Composer/packages/lib/indexers/package.json | 1 - Composer/plugins/azurePublish/yarn.lock | 39 ++++++++++++++++----- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/Composer/packages/lib/indexers/package.json b/Composer/packages/lib/indexers/package.json index 27fed5652b..2b188fc96c 100644 --- a/Composer/packages/lib/indexers/package.json +++ b/Composer/packages/lib/indexers/package.json @@ -27,7 +27,6 @@ "rimraf": "^2.6.3" }, "dependencies": { - "@bfc/shared": "*", "@bfcomposer/bf-lu": "^1.4.8", "@microsoft/bf-lu": "^4.11.0-dev.20200824.de66343", "adaptive-expressions": "4.10.0-preview-147186", diff --git a/Composer/plugins/azurePublish/yarn.lock b/Composer/plugins/azurePublish/yarn.lock index cc628d44bc..e82f264678 100644 --- a/Composer/plugins/azurePublish/yarn.lock +++ b/Composer/plugins/azurePublish/yarn.lock @@ -156,14 +156,6 @@ "@azure/ms-rest-js" "^2.0.4" adal-node "^0.1.28" -"@bfc/indexers@../../packages/lib/indexers": - version "0.0.0" - dependencies: - "@microsoft/bf-lu" "^4.10.0-dev.20200808.5a7c973" - adaptive-expressions "4.10.0-preview-147186" - botbuilder-lg "^4.10.0-preview-150886" - lodash "^4.17.19" - "@bfc/extension@../../packages/extension": version "1.0.0" dependencies: @@ -172,6 +164,15 @@ passport "^0.4.1" path-to-regexp "^6.1.0" +"@bfc/indexers@../../packages/lib/indexers": + version "0.0.0" + dependencies: + "@bfcomposer/bf-lu" "^1.4.8" + "@microsoft/bf-lu" "^4.11.0-dev.20200824.de66343" + adaptive-expressions "4.10.0-preview-147186" + botbuilder-lg "^4.10.0-preview-150886" + lodash "^4.17.19" + "@bfc/shared@../../packages/lib/shared": version "0.0.0" dependencies: @@ -180,6 +181,28 @@ nanoid "^3.1.3" nanoid-dictionary "^3.0.0" +"@bfcomposer/bf-lu@^1.4.8": + version "1.4.8" + resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.8.tgz#dbb68cc54b2f70c558623952f38567e65d49b5de" + integrity sha1-27aMxUsvcMVYYjlS84Vn5l1Jtd4= + dependencies: + "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" + "@azure/ms-rest-azure-js" "2.0.1" + "@types/node-fetch" "~2.5.5" + antlr4 "^4.7.2" + chalk "2.4.1" + console-stream "^0.1.1" + deep-equal "^1.0.1" + delay "^4.3.0" + fs-extra "^8.1.0" + get-stdin "^6.0.0" + globby "^10.0.1" + intercept-stdout "^0.1.2" + lodash "^4.17.19" + node-fetch "~2.6.0" + semver "^5.5.1" + tslib "^1.10.0" + "@microsoft/bf-cli-command@4.10.0-dev.20200721.8bb21ac": version "4.10.0-dev.20200721.8bb21ac" resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-cli-command/-/@microsoft/bf-cli-command-4.10.0-dev.20200721.8bb21ac.tgz#9c81b37bc10072ca6beec7d7f68aa309f8a552ec" From 4e60b2ceb7fc77ba9bd41026101afa014a935475 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Mon, 14 Sep 2020 15:44:10 +0800 Subject: [PATCH 050/105] update --- Composer/packages/client/src/pages/design/DesignPage.tsx | 4 ++-- .../packages/client/src/recoilModel/dispatchers/qna.ts | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 37e9dbca6e..1a75b04ffd 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -584,8 +584,8 @@ const DesignPage: React.FC { if (!file.id.endsWith('.source') && getBaseName(file.id) === getBaseName(updatedQnAId)) { - return { - ...file, - content: contentForDialogQnA + file.content, - }; + return qnaUtil.addImport(file, `${createdSourceQnAId}.qna`); } return file; }); From 87c3d8716fa69ecbba0a64a65ecf97aed05ca288 Mon Sep 17 00:00:00 2001 From: liweitian Date: Tue, 15 Sep 2020 10:07:48 +0800 Subject: [PATCH 051/105] update all up view UX --- .../client/src/components/EditableField.tsx | 75 ++++++++++--------- .../client/src/pages/knowledge-base/styles.ts | 49 +++++++++--- .../src/pages/knowledge-base/table-view.tsx | 51 +++++++++---- 3 files changed, 113 insertions(+), 62 deletions(-) diff --git a/Composer/packages/client/src/components/EditableField.tsx b/Composer/packages/client/src/components/EditableField.tsx index 4e449d53ae..2cef1579af 100644 --- a/Composer/packages/client/src/components/EditableField.tsx +++ b/Composer/packages/client/src/components/EditableField.tsx @@ -17,6 +17,7 @@ interface EditableFieldProps extends Omit = (props) => { multiline = false, onChange, onBlur, + resizable = true, value, id, error, @@ -75,48 +77,49 @@ const EditableField: React.FC = (props) => { } return ( -
setEditing(true)} - onMouseLeave={() => !hasFocus && setEditing(false)} - > - - } - value={localValue} - onBlur={handleCommit} - onChange={handleChange} - onFocus={() => setHasFocus(true)} - /> -
+ }, + styles + ) as Partial + } + value={localValue} + onBlur={handleCommit} + onChange={handleChange} + onFocus={() => setHasFocus(true)} + onMouseEnter={() => setEditing(true)} + onMouseLeave={() => !hasFocus && setEditing(false)} + /> ); }; diff --git a/Composer/packages/client/src/pages/knowledge-base/styles.ts b/Composer/packages/client/src/pages/knowledge-base/styles.ts index 8bbbde93e9..c3894c9a97 100644 --- a/Composer/packages/client/src/pages/knowledge-base/styles.ts +++ b/Composer/packages/client/src/pages/knowledge-base/styles.ts @@ -25,6 +25,7 @@ export const formCell = css` white-space: pre-wrap; font-size: 14px; line-height: 28px; + height: 100%; `; export const inlineContainer = (isBold) => css` @@ -102,18 +103,30 @@ export const rowDetails = { selectors: { '&:hover': { background: NeutralColors.gray30, + selectors: { + '.ms-TextField-fieldGroup': { + background: NeutralColors.gray30, + }, + '.ms-Button--icon': { + visibility: 'visible', + }, + '.ms-Button': { + visibility: 'visible', + }, + }, }, - '&:hover .ms-Button--icon': { - visibility: 'visible', - }, - '&.is-selected .ms-Button--icon': { - visibility: 'visible', - }, - '&:hover .ms-Button': { - visibility: 'visible', - }, - '&.is-selected .ms-Button': { - visibility: 'visible', + '&.is-selected': { + selectors: { + '.ms-Button--icon': { + visibility: 'visible', + }, + '.ms-Button': { + visibility: 'visible', + }, + '.ms-TextField-fieldGroup': { + background: NeutralColors.gray30, + }, + }, }, }, }, @@ -156,3 +169,17 @@ export const addIcon = { color: SharedColors.cyanBlue10, }, }; + +export const editableField = { + root: { + height: '100%', + selectors: { + '.ms-TextField-wrapper': { + height: '100%', + }, + }, + }, + fieldGroup: { + height: '100%', + }, +}; diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index 4bc41e4552..700a3f1b45 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -34,14 +34,14 @@ import { qnaFilesState, projectIdState, localeState, - settingsState, + //settingsState, } from '../../recoilModel/atoms/botState'; import { dispatcherState } from '../../recoilModel'; import { getBaseName } from '../../utils/fileUtil'; import { EditableField } from '../../components/EditableField'; import { classNames, AddTemplateButton } from '../../components/AllupviewComponets/styles'; -import { formCell, content, addIcon, divider, rowDetails, icon, addAlternative } from './styles'; +import { formCell, content, addIcon, divider, rowDetails, icon, addAlternative, editableField } from './styles'; const noOp = () => undefined; @@ -62,7 +62,7 @@ const TableView: React.FC = (props) => { const qnaFiles = useRecoilValue(qnaFilesState); const projectId = useRecoilValue(projectIdState); const locale = useRecoilValue(localeState); - const settings = useRecoilValue(settingsState); + //const settings = useRecoilValue(settingsState); const { // createQnAImport, removeQnAImport, @@ -78,7 +78,6 @@ const TableView: React.FC = (props) => { } = useRecoilValue(dispatcherState); // const { languages, defaultLanguage } = settings; - console.log(settings); const { dialogId } = props; const activeDialog = dialogs.find(({ id }) => id === dialogId); @@ -115,14 +114,13 @@ const TableView: React.FC = (props) => { return result.concat(res); }, []); if (dialogId === 'all') { - setQnASections( - allSections.map((item, index) => { - return { - ...item, - expand: index === focusedIndex, - }; - }) - ); + const sections = allSections.map((item, index) => { + return { + ...item, + expand: index === focusedIndex, + }; + }); + setQnASections(sections); } else { const dialogSections = allSections .filter((t) => t.dialogId === dialogId || importedFileIds.includes(t.fileId)) @@ -150,6 +148,20 @@ const TableView: React.FC = (props) => { setQnASections(newSections); }; + // const expandRow = (sectionId: string) => { + // const newSections = qnaSections.map((item) => { + // if (item.sectionId === sectionId) { + // return { + // ...item, + // expand: true, + // }; + // } else { + // return item; + // } + // }); + // setQnASections(newSections); + // }; + const onUpdateQnAQuestion = (fileId: string, sectionId: string, questionId: string, content: string) => { if (!fileId) return; actions.setMessage('item deleted'); @@ -398,6 +410,8 @@ const TableView: React.FC = (props) => { disabled={isAllowEdit} id={item.sectionId} name={item.Answer} + resizable={false} + styles={editableField} value={item.Answer} onBlur={(_id, value) => { const newValue = value?.trim().replace(/^#/, ''); @@ -518,7 +532,14 @@ const TableView: React.FC = (props) => { const onRenderRow = (props) => { if (props) { - return ; + return ( + expandRow(props.item.sectionId)} + /> + ); } return null; }; @@ -534,10 +555,10 @@ const TableView: React.FC = (props) => { collapseAllVisibility: CollapseAllVisibility.hidden, }} groups={getGroups()} - initialFocusedIndex={focusedIndex} + //initialFocusedIndex={focusedIndex} items={qnaSections} layoutMode={DetailsListLayoutMode.justified} - selectionMode={SelectionMode.none} + selectionMode={SelectionMode.single} onRenderDetailsHeader={onRenderDetailsHeader} onRenderRow={onRenderRow} /> From 4a47af1559d6f879266fafb1fcd2ed96339ca452 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Tue, 15 Sep 2020 16:36:21 +0800 Subject: [PATCH 052/105] creation flow from qna scratch --- .../components/AllupviewComponets/styles.ts | 14 + .../client/src/images/emptyQnAIcon.svg | 284 ++++++++++++++++++ .../src/pages/knowledge-base/QnAPage.tsx | 44 ++- .../src/pages/knowledge-base/code-editor.tsx | 7 +- .../src/pages/knowledge-base/table-view.tsx | 85 ++++-- .../client/src/recoilModel/atoms/botState.ts | 18 ++ .../client/src/recoilModel/dispatchers/qna.ts | 67 ++++- 7 files changed, 479 insertions(+), 40 deletions(-) create mode 100644 Composer/packages/client/src/images/emptyQnAIcon.svg diff --git a/Composer/packages/client/src/components/AllupviewComponets/styles.ts b/Composer/packages/client/src/components/AllupviewComponets/styles.ts index b3291e50c1..72a13509a8 100644 --- a/Composer/packages/client/src/components/AllupviewComponets/styles.ts +++ b/Composer/packages/client/src/components/AllupviewComponets/styles.ts @@ -26,4 +26,18 @@ export const classNames = mergeStyleSets({ groupHeaderSourceName: { marginLeft: '5px', }, + emptyTableList: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + }, + emptyTableListCenter: { + display: 'flex', + alignItems: 'center', + flexDirection: 'column', + width: '50%', + textAlign: 'center', + marginTop: '-20%', + }, }); diff --git a/Composer/packages/client/src/images/emptyQnAIcon.svg b/Composer/packages/client/src/images/emptyQnAIcon.svg new file mode 100644 index 0000000000..d70057b412 --- /dev/null +++ b/Composer/packages/client/src/images/emptyQnAIcon.svg @@ -0,0 +1,284 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx index 980c6ad50b..cbec4b64a6 100644 --- a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx @@ -15,7 +15,15 @@ import { navigateTo } from '../../utils/navigation'; import { TestController } from '../../components/TestController/TestController'; import { INavTreeItem } from '../../components/NavTree'; import { Page } from '../../components/Page'; -import { dialogsState, projectIdState, qnaAllUpViewStatusState, qnaFilesState } from '../../recoilModel/atoms/botState'; +import { + dialogsState, + projectIdState, + qnaAllUpViewStatusState, + qnaFilesState, + showCreateDialogModalState, + showCreateQnAFromScratchDialogState, + showCreateQnAFromUrlDialogState, +} from '../../recoilModel/atoms/botState'; import { dispatcherState } from '../../recoilModel'; import { QnAAllUpViewStatus } from '../../recoilModel/types'; import { @@ -42,8 +50,10 @@ const QnAPage: React.FC = (props) => { const locale = 'en-us'; //const locale = useRecoilValue(localeState); const qnaAllUpViewStatus = useRecoilValue(qnaAllUpViewStatusState); - const [createQnAModalFromUrlVisiability, setCreateQnAModalFromUrlVisiability] = useState(false); - const [createQnAModalFromScratchVisiability, setCreateQnAModalFromScratchVisiability] = useState(false); + const [createOnDialogId, setCreateOnDialogId] = useState(''); + + const createQnAModalFromUrlVisiability = useRecoilValue(showCreateQnAFromUrlDialogState); + const createQnAModalFromScratchVisiability = useRecoilValue(showCreateQnAFromScratchDialogState); const path = props.location?.pathname ?? ''; const { dialogId = '' } = props; @@ -61,14 +71,16 @@ const QnAPage: React.FC = (props) => { name: formatMessage('Create KB from scratch'), key: 'Create KB from scratch', onClick: () => { - setCreateQnAModalFromScratchVisiability(true); + setCreateOnDialogId(dialog.id); + actions.createQnAFromScratchDialogBegin(() => undefined); }, }, { name: formatMessage('Create KB from URL or file'), key: 'Create KB from URL or file', onClick: () => { - setCreateQnAModalFromUrlVisiability(true); + setCreateOnDialogId(dialog.id); + actions.createQnAFromUrlDialogBegin(() => {}); }, }, ], @@ -143,8 +155,6 @@ const QnAPage: React.FC = (props) => { }> - - {qnaAllUpViewStatus !== QnAAllUpViewStatus.Loading && } {qnaAllUpViewStatus === QnAAllUpViewStatus.Loading && ( @@ -152,29 +162,31 @@ const QnAPage: React.FC = (props) => { )} {createQnAModalFromUrlVisiability && ( { - setCreateQnAModalFromUrlVisiability(false); + actions.createQnAFromUrlDialogCancel(); }} onSubmit={async ({ name, url, multiTurn }: CreateQnAFromUrlFormData) => { - setCreateQnAModalFromUrlVisiability(false); - await actions.createQnAKBFromUrl({ id: `${dialogId}.${locale}`, name, url, multiTurn }); + await actions.createQnAKBFromUrl({ + id: `${createOnDialogId || dialogId}.${locale}`, + name, + url, + multiTurn, + }); }} /> )} {createQnAModalFromScratchVisiability && ( { - setCreateQnAModalFromScratchVisiability(false); + actions.createQnAFromScratchDialogCancel(); }} onSubmit={async ({ name }: CreateQnAFromScratchFormData) => { - setCreateQnAModalFromScratchVisiability(false); - console.log(name); - // await actions.createQnAKBFromUrl({ id: `${dialogId}.${locale}`, name, url, multiTurn }); + await actions.createQnAKBFromScratch({ id: `${createOnDialogId || dialogId}.${locale}`, name }); }} /> )} diff --git a/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx b/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx index 6fb8b333d2..defbf0aa88 100644 --- a/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/code-editor.tsx @@ -18,7 +18,6 @@ import { dispatcherState } from '../../recoilModel'; import { userSettingsState } from '../../recoilModel'; interface CodeEditorProps extends RouteComponentProps<{}> { dialogId: string; - containerId?: string; } const lspServerPath = '/lu-language-server'; @@ -31,7 +30,10 @@ const CodeEditor: React.FC = (props) => { const projectId = useRecoilValue(projectIdState); const userSettings = useRecoilValue(userSettingsState); const { dialogId } = props; - const targetFileId = props.containerId ? props.containerId : `${dialogId}.${locale}`; + const search = props.location?.search ?? ''; + const searchContainerId = querystring.parse(search).C; + + const targetFileId = searchContainerId ? searchContainerId : `${dialogId}.${locale}`; const file = qnaFiles.find(({ id }) => id === targetFileId); const hash = props.location?.hash ?? ''; const hashLine = querystring.parse(hash).L; @@ -39,6 +41,7 @@ const CodeEditor: React.FC = (props) => { const [content, setContent] = useState(file?.content); const currentDiagnostics = get(file, 'diagnostics', []); const [qnaEditor, setQnAEditor] = useState(null); + useEffect(() => { if (qnaEditor) { window.requestAnimationFrame(() => { diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index 4bc41e4552..45e9e10bc1 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -4,6 +4,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import { useRecoilValue } from 'recoil'; +import inRange from 'lodash/inRange'; import React, { useEffect, useState, useCallback, Fragment } from 'react'; import { DetailsList, @@ -18,7 +19,7 @@ import { GroupHeader, CollapseAllVisibility } from 'office-ui-fabric-react/lib/G import { IOverflowSetItemProps, OverflowSet } from 'office-ui-fabric-react/lib/OverflowSet'; import { Link } from 'office-ui-fabric-react/lib/Link'; import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; -import { IconButton, ActionButton } from 'office-ui-fabric-react/lib/Button'; +import { IconButton, ActionButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button'; import { ScrollablePane, ScrollbarVisibility } from 'office-ui-fabric-react/lib/ScrollablePane'; import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky'; import formatMessage from 'format-message'; @@ -27,7 +28,9 @@ import isEmpty from 'lodash/isEmpty'; import { QnAFile } from '@bfc/shared/src/types'; import { QnASection } from '@bfc/shared'; import { qnaUtil } from '@bfc/indexers'; +import querystring from 'query-string'; +import emptyQnAIcon from '../../images/emptyQnAIcon.svg'; import { navigateTo } from '../../utils/navigation'; import { dialogsState, @@ -81,6 +84,9 @@ const TableView: React.FC = (props) => { console.log(settings); const { dialogId } = props; + const search = props.location?.search ?? ''; + const searchContainerId = querystring.parse(search).C; + const activeDialog = dialogs.find(({ id }) => id === dialogId); const targetFileId = dialogId.endsWith('.source') ? dialogId : `${dialogId}.${locale}`; const qnaFile = qnaFiles.find(({ id }) => id === targetFileId); @@ -100,7 +106,7 @@ const TableView: React.FC = (props) => { }) : []; }; - const [focusedIndex, setFocusedIndex] = useState(0); + const [focusedIndex, setFocusedIndex] = useState(-1); const [qnaSections, setQnASections] = useState([]); const importedFileIds = qnaFile?.imports.map(({ id }) => getBaseName(id)) || []; @@ -124,15 +130,16 @@ const TableView: React.FC = (props) => { }) ); } else { - const dialogSections = allSections - .filter((t) => t.dialogId === dialogId || importedFileIds.includes(t.fileId)) - .map((item, index) => { + const dialogSections = allSections.filter((t) => t.dialogId === dialogId || importedFileIds.includes(t.fileId)); + + setQnASections( + dialogSections.map((item, index) => { return { ...item, expand: index === focusedIndex, }; - }); - setQnASections(dialogSections); + }) + ); } }, [qnaFiles, dialogId, projectId]); @@ -213,8 +220,8 @@ const TableView: React.FC = (props) => { const onRenderGroupHeader: IDetailsGroupRenderProps['onRenderHeader'] = (props) => { const groupName = props?.group?.name || ''; - const groupFileId = props?.group?.key || ''; - const isImportedSource = groupFileId.endsWith('.source'); + const containerId = props?.group?.key || ''; + const isImportedSource = containerId.endsWith('.source'); const onRenderItem = (item: IOverflowSetItemProps): JSX.Element => { return ; @@ -254,7 +261,7 @@ const TableView: React.FC = (props) => { key: 'edit', name: formatMessage('Show code'), onClick: () => { - navigateTo(`/bot/${projectId}/knowledge-base/${dialogId}/${groupFileId}/edit`); + navigateTo(`/bot/${projectId}/knowledge-base/${dialogId}/edit?C=${containerId}`); }, }, { @@ -262,8 +269,8 @@ const TableView: React.FC = (props) => { name: formatMessage('Delete knowledge base'), onClick: async () => { if (!qnaFile) return; - await removeQnAImport({ id: qnaFile.id, sourceId: groupFileId }); - await removeQnAFile({ id: groupFileId }); + await removeQnAImport({ id: qnaFile.id, sourceId: containerId }); + await removeQnAFile({ id: containerId }); }, }, ]} @@ -472,7 +479,16 @@ const TableView: React.FC = (props) => { const lastGroup = groups[groups.length - 1]; const startIndex = lastGroup ? lastGroup.startIndex + lastGroup.count : 0; const { id, qnaSections } = currentFile; - groups.push({ key: id, name: getBaseName(id), startIndex, count: qnaSections.length, level: 0 }); + const count = qnaSections.length; + const shouldExpand = inRange(focusedIndex, startIndex, startIndex + count) || searchContainerId === id; + groups.push({ + key: id, + name: getBaseName(id), + startIndex, + count, + level: 0, + isCollapsed: !shouldExpand, + }); }); return groups; } else { @@ -484,14 +500,17 @@ const TableView: React.FC = (props) => { startIndex: 0, count: qnaFile.qnaSections.length, level: 0, + isCollapsed: !inRange(focusedIndex, 0, qnaFile.qnaSections.length), }, ]; importedSourceFiles.forEach((currentFile) => { const lastGroup = groups[groups.length - 1]; const startIndex = lastGroup.startIndex + lastGroup.count; const { id, qnaSections } = currentFile; + const count = qnaSections.length; + const shouldExpand = inRange(focusedIndex, startIndex, startIndex + count) || searchContainerId === id; const name = getBaseName(id); - groups.push({ key: id, name, startIndex, count: qnaSections.length, level: 0 }); + groups.push({ key: id, name, startIndex, count, level: 0, isCollapsed: !shouldExpand }); }); return groups; @@ -516,12 +535,38 @@ const TableView: React.FC = (props) => { [dialogId] ); - const onRenderRow = (props) => { - if (props) { - return ; - } - return null; - }; + const onRenderRow = useCallback( + (props) => { + if (props) { + return ; + } + return null; + }, + [dialogId] + ); + + if (qnaFile?.empty) { + return ( +
+
+ {formatMessage('Empty +

{formatMessage('Create a knowledge base from scratch or import knowledge from a URL or PDF files')}

+ { + actions.createQnAFromScratchDialogBegin(() => undefined); + }} + /> +
+
+ ); + } return (
diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index cd35d8c701..9e4b886873 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -215,6 +215,24 @@ export const qnaAllUpViewStatusState = atom({ default: QnAAllUpViewStatus.Success, }); +export const showCreateQnAFromUrlDialogState = atom({ + key: getFullyQualifiedKey('showCreateQnAFromUrlDialog'), + default: false, +}); + +export const showCreateQnAFromScratchDialogState = atom({ + key: getFullyQualifiedKey('showCreateQnAFromScratchDialog'), + default: false, +}); +export const onCreateQnAFromUrlDialogCompleteState = atom({ + key: getFullyQualifiedKey('onCreateQnAFromUrlDialogCompleteState'), + default: { func: undefined }, +}); +export const onCreateQnAFromScratchDialogCompleteState = atom({ + key: getFullyQualifiedKey('onCreateQnAFromScratchDialogCompleteState'), + default: { func: undefined }, +}); + export const isEjectRuntimeExistState = atom({ key: getFullyQualifiedKey('isEjectRuntimeExist'), default: false, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index 6584c6cd60..61ac88aa5a 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -6,10 +6,21 @@ import { useRecoilCallback, CallbackInterface } from 'recoil'; import { qnaUtil } from '@bfc/indexers'; import qnaWorker from '../parsers/qnaWorker'; -import { qnaFilesState, qnaAllUpViewStatusState, projectIdState, localeState, settingsState } from '../atoms/botState'; +import { + qnaFilesState, + qnaAllUpViewStatusState, + projectIdState, + localeState, + settingsState, + showCreateQnAFromScratchDialogState, + showCreateQnAFromUrlDialogState, + onCreateQnAFromScratchDialogCompleteState, + onCreateQnAFromUrlDialogCompleteState, +} from '../atoms/botState'; import { QnAAllUpViewStatus } from '../types'; import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage'; import { getBaseName } from '../../utils/fileUtil'; +import { navigateTo } from '../../utils/navigation'; import httpClient from './../../utils/httpUtil'; import { setError } from './shared'; @@ -127,6 +138,45 @@ export const createSourceQnAFileState = async ( }; export const qnaDispatcher = () => { + const createQnAFromUrlDialogBegin = useRecoilCallback(({ set }: CallbackInterface) => (onComplete) => { + set(showCreateQnAFromUrlDialogState, true); + set(onCreateQnAFromUrlDialogCompleteState, { func: onComplete }); + }); + + const createQnAFromUrlDialogCancel = useRecoilCallback(({ set }: CallbackInterface) => () => { + set(showCreateQnAFromUrlDialogState, false); + set(onCreateQnAFromUrlDialogCompleteState, { func: undefined }); + }); + + const createQnAFromScratchDialogBegin = useRecoilCallback(({ set }: CallbackInterface) => (onComplete) => { + set(showCreateQnAFromScratchDialogState, true); + set(onCreateQnAFromScratchDialogCompleteState, { func: onComplete }); + }); + + const createQnAFromScratchDialogCancel = useRecoilCallback(({ set }: CallbackInterface) => () => { + set(showCreateQnAFromScratchDialogState, false); + set(onCreateQnAFromScratchDialogCompleteState, { func: undefined }); + }); + + const createQnAFromUrlDialogSuccess = useRecoilCallback(({ set, snapshot }: CallbackInterface) => async () => { + const onCreateQnAFromUrlDialogComplete = (await snapshot.getPromise(onCreateQnAFromUrlDialogCompleteState)).func; + if (typeof onCreateQnAFromUrlDialogComplete === 'function') { + onCreateQnAFromUrlDialogComplete(null); + } + set(showCreateQnAFromUrlDialogState, false); + set(onCreateQnAFromUrlDialogCompleteState, { func: undefined }); + }); + + const createQnAFromScratchDialogSuccess = useRecoilCallback(({ set, snapshot }: CallbackInterface) => async () => { + const onCreateQnAFromScratchDialogComplete = (await snapshot.getPromise(onCreateQnAFromScratchDialogCompleteState)) + .func; + if (typeof onCreateQnAFromScratchDialogComplete === 'function') { + onCreateQnAFromScratchDialogComplete(null); + } + set(showCreateQnAFromScratchDialogState, false); + set(onCreateQnAFromScratchDialogCompleteState, { func: undefined }); + }); + const updateQnAFile = useRecoilCallback( (callbackHelpers: CallbackInterface) => async ({ id, content }: { id: string; content: string }) => { await updateQnAFileState(callbackHelpers, { id, content }); @@ -181,6 +231,7 @@ ${response.data} content: contentForSourceQnA, }); + await createQnAFromUrlDialogSuccess(); set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Success); } ); @@ -193,11 +244,18 @@ ${response.data} id: string; // dialogId.locale name: string; }) => { + const projectId = await callbackHelpers.snapshot.getPromise(projectIdState); + // const dialogs = await callbackHelpers.snapshot.getPromise(dialogsState); + + const content = qnaUtil.generateQnAPair(); await createSourceQnAFileState(callbackHelpers, { id, name, - content: '', + content, }); + await createQnAFromScratchDialogSuccess(); + + navigateTo(`/bot/${projectId}/knowledge-base/${getBaseName(id)}?C=${encodeURIComponent(name)}.source`); } ); @@ -355,6 +413,7 @@ ${response.data} }); } ); + return { createQnAImport, removeQnAImport, @@ -369,5 +428,9 @@ ${response.data} updateQnAFile, createQnAKBFromUrl, createQnAKBFromScratch, + createQnAFromScratchDialogBegin, + createQnAFromScratchDialogCancel, + createQnAFromUrlDialogBegin, + createQnAFromUrlDialogCancel, }; }; From 68990791480fea2a911af90725a4f3669f47d3a1 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Tue, 15 Sep 2020 17:55:25 +0800 Subject: [PATCH 053/105] edit container name --- .../QnA/EditQnAFromScratchModal.tsx | 99 +++++++++++++++++++ .../client/src/components/QnA/index.ts | 1 + .../src/pages/knowledge-base/table-view.tsx | 24 ++++- .../client/src/recoilModel/dispatchers/qna.ts | 77 ++++++++++++++- 4 files changed, 195 insertions(+), 6 deletions(-) create mode 100644 Composer/packages/client/src/components/QnA/EditQnAFromScratchModal.tsx diff --git a/Composer/packages/client/src/components/QnA/EditQnAFromScratchModal.tsx b/Composer/packages/client/src/components/QnA/EditQnAFromScratchModal.tsx new file mode 100644 index 0000000000..34c97cf7b0 --- /dev/null +++ b/Composer/packages/client/src/components/QnA/EditQnAFromScratchModal.tsx @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import React from 'react'; +import formatMessage from 'format-message'; +import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; +import { QnAFile } from '@bfc/shared'; + +import { FieldConfig, useForm } from '../../hooks/useForm'; +import { getBaseName } from '../../utils/fileUtil'; + +import { validateName } from './constants'; +import { styles, dialogWindow, textField } from './styles'; + +interface EditQnAFromScratchModalProps { + qnaFiles: QnAFile[]; + qnaFile: QnAFile; + onDismiss: () => void; + onSubmit: (formData: EditQnAFromScratchFormData) => void; +} + +export interface EditQnAFromScratchFormData { + name: string; +} + +const formConfig: FieldConfig = { + name: { + required: true, + defaultValue: '', + }, +}; + +const DialogTitle = () => { + return
{formatMessage('Edit KB name')}
; +}; + +export const EditQnAFromScratchModal: React.FC = (props) => { + const { onDismiss, onSubmit, qnaFiles, qnaFile } = props; + + formConfig.name.validate = validateName(qnaFiles); + formConfig.name.defaultValue = getBaseName(qnaFile.id); + const { formData, updateField, hasErrors, formErrors } = useForm(formConfig); + const disabled = hasErrors; + + const updateName = (name = '') => { + updateField('name', name); + }; + + return ( + , + styles: styles.dialog, + }} + hidden={false} + modalProps={{ + isBlocking: false, + styles: styles.modal, + }} + onDismiss={onDismiss} + > +
+ + updateName(name)} + /> + +
+ + + { + if (hasErrors) { + return; + } + onSubmit(formData); + }} + /> + +
+ ); +}; + +export default EditQnAFromScratchModal; diff --git a/Composer/packages/client/src/components/QnA/index.ts b/Composer/packages/client/src/components/QnA/index.ts index 07c20e8bab..4f7785f7c1 100644 --- a/Composer/packages/client/src/components/QnA/index.ts +++ b/Composer/packages/client/src/components/QnA/index.ts @@ -3,3 +3,4 @@ export * from './CreateQnAFromScratchModal'; export * from './CreateQnAFromUrlModal'; +export * from './EditQnAFromScratchModal'; diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index 45e9e10bc1..d241803d4a 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -43,6 +43,7 @@ import { dispatcherState } from '../../recoilModel'; import { getBaseName } from '../../utils/fileUtil'; import { EditableField } from '../../components/EditableField'; import { classNames, AddTemplateButton } from '../../components/AllupviewComponets/styles'; +import { EditQnAFromScratchModal } from '../../components/QnA'; import { formCell, content, addIcon, divider, rowDetails, icon, addAlternative } from './styles'; @@ -106,6 +107,7 @@ const TableView: React.FC = (props) => { }) : []; }; + const [editQnAFile, setEditQnAFile] = useState(undefined); const [focusedIndex, setFocusedIndex] = useState(-1); const [qnaSections, setQnASections] = useState([]); @@ -221,6 +223,7 @@ const TableView: React.FC = (props) => { const onRenderGroupHeader: IDetailsGroupRenderProps['onRenderHeader'] = (props) => { const groupName = props?.group?.name || ''; const containerId = props?.group?.key || ''; + const containerQnAFile = qnaFiles.find(({ id }) => id === containerId); const isImportedSource = containerId.endsWith('.source'); const onRenderItem = (item: IOverflowSetItemProps): JSX.Element => { @@ -253,7 +256,9 @@ const TableView: React.FC = (props) => { { key: 'edit', name: 'edit', - onClick: noOp, + onClick: () => { + setEditQnAFile(containerQnAFile); + }, }, ]} overflowItems={[ @@ -587,6 +592,23 @@ const TableView: React.FC = (props) => { onRenderRow={onRenderRow} /> + {editQnAFile && ( + { + setEditQnAFile(undefined); + }} + onSubmit={async ({ name }) => { + const newId = `${name}.source`; + await actions.renameQnAKB({ id: editQnAFile.id, name: newId }); + if (!qnaFile) return; + await actions.removeQnAImport({ id: qnaFile.id, sourceId: editQnAFile.id }); + await actions.createQnAImport({ id: qnaFile.id, sourceId: newId }); + setEditQnAFile(undefined); + }} + > + )}
); }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index 61ac88aa5a..a44090063b 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -99,7 +99,7 @@ export const removeQnAFileState = async (callbackHelpers: CallbackInterface, { i set(qnaFilesState, qnaFiles); }; -export const createSourceQnAFileState = async ( +export const createKBFileState = async ( callbackHelpers: CallbackInterface, { id, name, content }: { id: string; name: string; content: string } ) => { @@ -137,6 +137,63 @@ export const createSourceQnAFileState = async ( set(qnaFilesState, [...newQnAFiles, createdQnAFile]); }; +export const removeKBFileState = async (callbackHelpers: CallbackInterface, { id }: { id: string }) => { + const { set, snapshot } = callbackHelpers; + let qnaFiles = await snapshot.getPromise(qnaFilesState); + const projectId = await snapshot.getPromise(projectIdState); + const locale = await snapshot.getPromise(localeState); + + const targetQnAFile = + qnaFiles.find((item) => item.id === id) || qnaFiles.find((item) => item.id === `${id}.${locale}`); + if (!targetQnAFile) { + throw new Error(`remove qna container file ${id} not exist`); + } + + qnaFiles.forEach((file) => { + if (file.id === targetQnAFile.id) { + qnaFileStatusStorage.removeFileStatus(projectId, targetQnAFile.id); + } + }); + + qnaFiles = qnaFiles.filter((file) => file.id !== targetQnAFile.id); + set(qnaFilesState, qnaFiles); +}; + +export const renameKBFileState = async ( + callbackHelpers: CallbackInterface, + { id, name }: { id: string; name: string } +) => { + const { set, snapshot } = callbackHelpers; + const qnaFiles = await snapshot.getPromise(qnaFilesState); + const projectId = await snapshot.getPromise(projectIdState); + const locale = await snapshot.getPromise(localeState); + + const targetQnAFile = + qnaFiles.find((item) => item.id === id) || qnaFiles.find((item) => item.id === `${id}.${locale}`); + if (!targetQnAFile) { + throw new Error(`rename qna container file ${id} not exist`); + } + + const existQnAFile = + qnaFiles.find((item) => item.id === name) || qnaFiles.find((item) => item.id === `${name}.${locale}`); + if (existQnAFile) { + throw new Error(`rename qna container file to ${name} already exist`); + } + qnaFileStatusStorage.removeFileStatus(projectId, targetQnAFile.id); + + const newQnAFiles = qnaFiles.map((file) => { + if (file.id === targetQnAFile.id) { + return { + ...file, + id: name, + }; + } + return file; + }); + + set(qnaFilesState, newQnAFiles); +}; + export const qnaDispatcher = () => { const createQnAFromUrlDialogBegin = useRecoilCallback(({ set }: CallbackInterface) => (onComplete) => { set(showCreateQnAFromUrlDialogState, true); @@ -225,7 +282,7 @@ export const qnaDispatcher = () => { ${response.data} `; - await createSourceQnAFileState(callbackHelpers, { + await createKBFileState(callbackHelpers, { id, name, content: contentForSourceQnA, @@ -248,7 +305,7 @@ ${response.data} // const dialogs = await callbackHelpers.snapshot.getPromise(dialogsState); const content = qnaUtil.generateQnAPair(); - await createSourceQnAFileState(callbackHelpers, { + await createKBFileState(callbackHelpers, { id, name, content, @@ -391,7 +448,7 @@ ${response.data} const qnaFile = qnaFiles.find((temp) => temp.id === id); if (!qnaFile) return qnaFiles; - const updatedFile = qnaUtil.addImport(qnaFile, sourceId); + const updatedFile = qnaUtil.addImport(qnaFile, `${sourceId}.qna`); set(qnaFilesState, (qnaFiles) => { return qnaFiles.map((file) => { return file.id === id ? updatedFile : file; @@ -405,7 +462,7 @@ ${response.data} const qnaFile = qnaFiles.find((temp) => temp.id === id); if (!qnaFile) return qnaFiles; - const updatedFile = qnaUtil.removeImport(qnaFile, sourceId); + const updatedFile = qnaUtil.removeImport(qnaFile, `${sourceId}.qna`); set(qnaFilesState, (qnaFiles) => { return qnaFiles.map((file) => { return file.id === id ? updatedFile : file; @@ -413,6 +470,14 @@ ${response.data} }); } ); + const removeQnAKB = useRecoilCallback((callbackHelpers: CallbackInterface) => async ({ id }: { id: string }) => { + await removeKBFileState(callbackHelpers, { id }); + }); + const renameQnAKB = useRecoilCallback( + (callbackHelpers: CallbackInterface) => async ({ id, name }: { id: string; name: string }) => { + await renameKBFileState(callbackHelpers, { id, name }); + } + ); return { createQnAImport, @@ -426,6 +491,8 @@ ${response.data} createQnAFile, removeQnAFile, updateQnAFile, + removeQnAKB, + renameQnAKB, createQnAKBFromUrl, createQnAKBFromScratch, createQnAFromScratchDialogBegin, From 349dab05d425c5c69bf93eb36e0379a8882cd7b3 Mon Sep 17 00:00:00 2001 From: liweitian Date: Tue, 15 Sep 2020 18:03:46 +0800 Subject: [PATCH 054/105] update css --- .../client/src/components/EditableField.tsx | 80 ++++++++++--------- .../client/src/pages/knowledge-base/styles.ts | 1 + .../src/pages/knowledge-base/table-view.tsx | 44 +++++++--- .../pages/language-generation/table-view.tsx | 10 +-- 4 files changed, 82 insertions(+), 53 deletions(-) diff --git a/Composer/packages/client/src/components/EditableField.tsx b/Composer/packages/client/src/components/EditableField.tsx index 2cef1579af..0eaa53ad90 100644 --- a/Composer/packages/client/src/components/EditableField.tsx +++ b/Composer/packages/client/src/components/EditableField.tsx @@ -36,6 +36,7 @@ const EditableField: React.FC = (props) => { styles = {}, placeholder, fontSize, + autoAdjustHeight = false, multiline = false, onChange, onBlur, @@ -77,49 +78,52 @@ const EditableField: React.FC = (props) => { } return ( - + - } - value={localValue} - onBlur={handleCommit} - onChange={handleChange} - onFocus={() => setHasFocus(true)} - onMouseEnter={() => setEditing(true)} - onMouseLeave={() => !hasFocus && setEditing(false)} - /> + styles + ) as Partial + } + value={localValue} + onBlur={handleCommit} + onChange={handleChange} + onFocus={() => setHasFocus(true)} + onMouseEnter={() => setEditing(true)} + onMouseLeave={() => !hasFocus && setEditing(false)} + /> + ); }; diff --git a/Composer/packages/client/src/pages/knowledge-base/styles.ts b/Composer/packages/client/src/pages/knowledge-base/styles.ts index c3894c9a97..3ab75f64f0 100644 --- a/Composer/packages/client/src/pages/knowledge-base/styles.ts +++ b/Composer/packages/client/src/pages/knowledge-base/styles.ts @@ -181,5 +181,6 @@ export const editableField = { }, fieldGroup: { height: '100%', + border: '0', }, }; diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index 700a3f1b45..0603c8ec63 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -101,7 +101,7 @@ const TableView: React.FC = (props) => { }; const [focusedIndex, setFocusedIndex] = useState(0); const [qnaSections, setQnASections] = useState([]); - + const [kthSectionIsCreatingQuestion, setCreatingQuestionInKthSection] = useState(-1); const importedFileIds = qnaFile?.imports.map(({ id }) => getBaseName(id)) || []; const importedFiles = qnaFiles.filter(({ id }) => importedFileIds.includes(id)); const importedSourceFiles = importedFiles.filter(({ id }) => id.endsWith('.source')); @@ -204,12 +204,12 @@ const TableView: React.FC = (props) => { const onCreateNewQnAPairs = (fileId: string | undefined) => { if (!fileId) return; const newQnAPair = qnaUtil.generateQnAPair(); - const sectionIndex = qnaSections.findIndex((item) => item.fileId === fileId); - setFocusedIndex(sectionIndex + 1); + //const sectionIndex = qnaSections.findIndex((item) => item.fileId === fileId); + //setFocusedIndex(sectionIndex + 1); createQnAPairs({ id: fileId, content: newQnAPair }); }; - const onCreateNewQuestion = (fileId, sectionId) => { + const onCreateNewQuestion = (fileId, sectionId, content?: string) => { if (qnaFile) { const sectionIndex = qnaSections.findIndex((item) => item.sectionId === sectionId); setFocusedIndex(sectionIndex); @@ -217,7 +217,7 @@ const TableView: React.FC = (props) => { const payload = { id: fileId, sectionId, - content: 'Add new question', + content: content || 'Add new question', }; createQnAQuestion(payload); } @@ -343,11 +343,11 @@ const TableView: React.FC = (props) => { maxWidth: 450, isResizable: true, data: 'string', - onRender: (item: QnASectionItem) => { + onRender: (item: QnASectionItem, index: number) => { const questions = item.Questions; const showingQuestions = item.expand ? questions : questions.slice(0, limitedNumber); //This question content of this qna Section is '#?' - // const isQuestionEmpty = showingQuestions.length === 1 && showingQuestions[0].content === ''; + const isQuestionEmpty = showingQuestions.length === 1 && showingQuestions[0].content === ''; const isSourceSectionInDialog = item.fileId.endsWith('.source') && !dialogId.endsWith('.source'); const isAllowEdit = dialogId !== 'all' && !isSourceSectionInDialog; @@ -355,7 +355,7 @@ const TableView: React.FC = (props) => { onCreateNewQuestion(item.fileId, item.sectionId)} + onClick={(e) => setCreatingQuestionInKthSection(index)} > {formatMessage('add alternative phrasing')} @@ -372,6 +372,7 @@ const TableView: React.FC = (props) => { disabled={isAllowEdit} id={question.id} name={question.content} + placeholder={'add new question'} value={question.content} onBlur={(_id, value) => { const newValue = value?.trim().replace(/^#/, ''); @@ -384,8 +385,30 @@ const TableView: React.FC = (props) => { /> ); })} - {!item.expand && ({questions.length})} - {addQuestionButton} + {!item.expand && !isQuestionEmpty && ({questions.length})} + {kthSectionIsCreatingQuestion === index ? ( + { + const newValue = value?.trim().replace(/^#/, ''); + if (newValue) { + onCreateNewQuestion(item.fileId, item.sectionId, newValue); + } + setCreatingQuestionInKthSection(-1); + }} + onChange={() => {}} + /> + ) : ( + addQuestionButton + )}
); }, @@ -410,6 +433,7 @@ const TableView: React.FC = (props) => { disabled={isAllowEdit} id={item.sectionId} name={item.Answer} + placeholder={'add new answer'} resizable={false} styles={editableField} value={item.Answer} diff --git a/Composer/packages/client/src/pages/language-generation/table-view.tsx b/Composer/packages/client/src/pages/language-generation/table-view.tsx index d7d37fae95..cf966dab3d 100644 --- a/Composer/packages/client/src/pages/language-generation/table-view.tsx +++ b/Composer/packages/client/src/pages/language-generation/table-view.tsx @@ -51,7 +51,7 @@ const TableView: React.FC = (props) => { const activeDialog = dialogs.find(({ id }) => id === dialogId); - const [focusedIndex, setFocusedIndex] = useState(0); + //const [focusedIndex, setFocusedIndex] = useState(0); useEffect(() => { if (!file || isEmpty(file)) return; @@ -79,7 +79,7 @@ const TableView: React.FC = (props) => { } as LgTemplate, }; createLgTemplate(payload); - setFocusedIndex(file.templates.length); + //setFocusedIndex(file.templates.length); } }, [file]); @@ -91,7 +91,7 @@ const TableView: React.FC = (props) => { templateName: name, }; removeLgTemplate(payload); - setFocusedIndex(file.templates.findIndex((item) => item.name === name)); + //setFocusedIndex(file.templates.findIndex((item) => item.name === name)); } }, [file] @@ -107,7 +107,7 @@ const TableView: React.FC = (props) => { toTemplateName: resolvedName, }; copyLgTemplate(payload); - setFocusedIndex(file.templates.length); + //setFocusedIndex(file.templates.length); } }, [file] @@ -425,7 +425,7 @@ const TableView: React.FC = (props) => { columns={getTableColums()} componentRef={listRef} getKey={getKeyCallback} - initialFocusedIndex={focusedIndex} + //initialFocusedIndex={focusedIndex} items={templatesToRender} // getKey={item => item.name} layoutMode={DetailsListLayoutMode.justified} From 9054f62de5368ae16d0180e4b17007cdad0a6768 Mon Sep 17 00:00:00 2001 From: liweitian Date: Tue, 15 Sep 2020 21:42:58 +0800 Subject: [PATCH 055/105] update css --- .../client/src/pages/knowledge-base/styles.ts | 58 ++++++------ .../src/pages/knowledge-base/table-view.tsx | 90 +++++++++++++------ 2 files changed, 94 insertions(+), 54 deletions(-) diff --git a/Composer/packages/client/src/pages/knowledge-base/styles.ts b/Composer/packages/client/src/pages/knowledge-base/styles.ts index 3ab75f64f0..3bf34cbee8 100644 --- a/Composer/packages/client/src/pages/knowledge-base/styles.ts +++ b/Composer/packages/client/src/pages/knowledge-base/styles.ts @@ -3,7 +3,7 @@ import { css } from '@emotion/core'; import { FontWeights } from '@uifabric/styling'; import { NeutralColors, SharedColors, FontSizes } from '@uifabric/fluent-theme'; -import { IIconStyles } from 'office-ui-fabric-react/lib/Icon'; + export const content = css` min-height: 28px; outline: none; @@ -72,26 +72,6 @@ export const addQnAPairLink = { }, }; -export const actionButton = css` - font-size: 16px; - margin: 0; - margin-left: 15px; -`; - -export const QnAIconStyle = { - root: { - padding: '8px', - boxSizing: 'border-box', - width: '40px', - height: '32px', - }, -} as IIconStyles; - -export const firstLine = css` - display: flex; - flex-direction: row; -`; - export const divider = css` height: 1px; background: ${NeutralColors.gray30}; @@ -113,6 +93,13 @@ export const rowDetails = { '.ms-Button': { visibility: 'visible', }, + // '&.is-selected': { + // selectors: { + // '.ms-DetailsRowy': { + // background: NeutralColors.gray30, + // }, + // }, + // }, }, }, '&.is-selected': { @@ -128,6 +115,9 @@ export const rowDetails = { }, }, }, + '&.is-selected:hover': { + background: NeutralColors.gray30, + }, }, }, }; @@ -139,11 +129,6 @@ export const icon = { }, }; -export const addButtonContainer = css` - z-index: 1; - background: ${NeutralColors.white}; -`; - export const addAlternative = { root: { fontSize: 16, @@ -170,7 +155,7 @@ export const addIcon = { }, }; -export const editableField = { +export const editableFieldAnswer = { root: { height: '100%', selectors: { @@ -184,3 +169,22 @@ export const editableField = { border: '0', }, }; + +export const editableFieldQuestion = { + root: { + width: '90%', + }, + fieldGroup: { + border: '0', + marginBottom: 1, + }, +}; + +export const questionNumber = { + height: 32, +}; + +export const firstQuestion = css` + display: flex; +} +`; diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index 0603c8ec63..7d193a4a9b 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -41,7 +41,19 @@ import { getBaseName } from '../../utils/fileUtil'; import { EditableField } from '../../components/EditableField'; import { classNames, AddTemplateButton } from '../../components/AllupviewComponets/styles'; -import { formCell, content, addIcon, divider, rowDetails, icon, addAlternative, editableField } from './styles'; +import { + formCell, + content, + addIcon, + divider, + rowDetails, + icon, + addAlternative, + editableFieldAnswer, + editableFieldQuestion, + questionNumber, + firstQuestion, +} from './styles'; const noOp = () => undefined; @@ -343,7 +355,7 @@ const TableView: React.FC = (props) => { maxWidth: 450, isResizable: true, data: 'string', - onRender: (item: QnASectionItem, index: number) => { + onRender: (item: QnASectionItem, index) => { const questions = item.Questions; const showingQuestions = item.expand ? questions : questions.slice(0, limitedNumber); //This question content of this qna Section is '#?' @@ -363,29 +375,53 @@ const TableView: React.FC = (props) => { return (
- {showingQuestions.map((question) => { - return ( - { - const newValue = value?.trim().replace(/^#/, ''); - const isChanged = question.content !== newValue; - if (newValue && isChanged) { - onUpdateQnAQuestion(item.fileId, item.sectionId, question.id, newValue); - } - }} - onChange={() => {}} - /> - ); - })} - {!item.expand && !isQuestionEmpty && ({questions.length})} +
+ { + const newValue = value?.trim().replace(/^#/, ''); + const isChanged = showingQuestions[0].content !== newValue; + if (newValue && isChanged) { + onUpdateQnAQuestion(item.fileId, item.sectionId, showingQuestions[0].id, newValue); + } + }} + onChange={() => {}} + /> + {!item.expand && !isQuestionEmpty &&
({questions.length})
} +
+ {showingQuestions.length > 1 && + showingQuestions.slice(1).map((question) => { + return ( + { + const newValue = value?.trim().replace(/^#/, ''); + const isChanged = question.content !== newValue; + if (newValue && isChanged) { + onUpdateQnAQuestion(item.fileId, item.sectionId, question.id, newValue); + } + }} + onChange={() => {}} + /> + ); + })} + {kthSectionIsCreatingQuestion === index ? ( = (props) => { id={'New Question'} name={'New Question'} placeholder={'add new question'} - styles={editableField} + styles={editableFieldQuestion} value={''} onBlur={(_id, value) => { const newValue = value?.trim().replace(/^#/, ''); @@ -435,7 +471,7 @@ const TableView: React.FC = (props) => { name={item.Answer} placeholder={'add new answer'} resizable={false} - styles={editableField} + styles={editableFieldAnswer} value={item.Answer} onBlur={(_id, value) => { const newValue = value?.trim().replace(/^#/, ''); From 45dfbcae23d2a7c9fb2cd577ba80eeaec9613d44 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Wed, 16 Sep 2020 17:36:48 +0800 Subject: [PATCH 056/105] migrate exist qna to container --- .../QnA/EditQnAFromScratchModal.tsx | 2 +- .../client/src/components/QnA/constants.ts | 6 +-- .../src/pages/knowledge-base/table-view.tsx | 33 ++++++------ .../src/recoilModel/dispatchers/project.ts | 4 +- Composer/packages/client/src/utils/qnaUtil.ts | 50 +++++++++++++++++++ .../server/src/models/bot/botProject.ts | 4 +- 6 files changed, 77 insertions(+), 22 deletions(-) diff --git a/Composer/packages/client/src/components/QnA/EditQnAFromScratchModal.tsx b/Composer/packages/client/src/components/QnA/EditQnAFromScratchModal.tsx index 34c97cf7b0..a53cb943c4 100644 --- a/Composer/packages/client/src/components/QnA/EditQnAFromScratchModal.tsx +++ b/Composer/packages/client/src/components/QnA/EditQnAFromScratchModal.tsx @@ -42,7 +42,7 @@ const DialogTitle = () => { export const EditQnAFromScratchModal: React.FC = (props) => { const { onDismiss, onSubmit, qnaFiles, qnaFile } = props; - formConfig.name.validate = validateName(qnaFiles); + formConfig.name.validate = validateName(qnaFiles.filter(({ id }) => qnaFile.id !== id)); formConfig.name.defaultValue = getBaseName(qnaFile.id); const { formData, updateField, hasErrors, formErrors } = useForm(formConfig); const disabled = hasErrors; diff --git a/Composer/packages/client/src/components/QnA/constants.ts b/Composer/packages/client/src/components/QnA/constants.ts index 90e2943366..9d94d7faf5 100644 --- a/Composer/packages/client/src/components/QnA/constants.ts +++ b/Composer/packages/client/src/components/QnA/constants.ts @@ -16,17 +16,17 @@ export const validateUrl: FieldValidator = (url: string): string => { return error; }; -export const QnANameRegex = /^\w[-\w]*$/; +export const FileNameRegex = /^[a-zA-Z0-9-_]+$/; export const validateName = (sources: QnAFile[]): FieldValidator => { return (name: string) => { let currentError = ''; if (name) { - if (!QnANameRegex.test(name)) { + if (!FileNameRegex.test(name)) { currentError = formatMessage('Name contains invalid charactors'); } - const duplicatedItemIndex = sources.findIndex((item) => item.name === name); + const duplicatedItemIndex = sources.findIndex((item) => item.id.toLowerCase() === `${name.toLowerCase()}.source`); if (duplicatedItemIndex > -1) { currentError = formatMessage('Duplicate imported QnA name'); } diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index d241803d4a..133ab5ca90 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -118,10 +118,12 @@ const TableView: React.FC = (props) => { useEffect(() => { if (isEmpty(qnaFiles)) return; - const allSections = qnaFiles.reduce((result: any[], qnaFile) => { - const res = generateQnASections(qnaFile); - return result.concat(res); - }, []); + const allSections = qnaFiles + .filter(({ id }) => id.endsWith('.source')) + .reduce((result: any[], qnaFile) => { + const res = generateQnASections(qnaFile); + return result.concat(res); + }, []); if (dialogId === 'all') { setQnASections( allSections.map((item, index) => { @@ -132,7 +134,7 @@ const TableView: React.FC = (props) => { }) ); } else { - const dialogSections = allSections.filter((t) => t.dialogId === dialogId || importedFileIds.includes(t.fileId)); + const dialogSections = allSections.filter((t) => importedFileIds.includes(t.fileId)); setQnASections( dialogSections.map((item, index) => { @@ -207,17 +209,16 @@ const TableView: React.FC = (props) => { }; const onCreateNewQuestion = (fileId, sectionId) => { - if (qnaFile) { - const sectionIndex = qnaSections.findIndex((item) => item.sectionId === sectionId); - setFocusedIndex(sectionIndex); - - const payload = { - id: fileId, - sectionId, - content: 'Add new question', - }; - createQnAQuestion(payload); - } + if (!fileId || !sectionId) return; + const sectionIndex = qnaSections.findIndex((item) => item.sectionId === sectionId); + setFocusedIndex(sectionIndex); + + const payload = { + id: fileId, + sectionId, + content: 'Add new question', + }; + createQnAQuestion(payload); }; const onRenderGroupHeader: IDetailsGroupRenderProps['onRenderHeader'] = (props) => { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 094aff011e..39b6e408d1 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -15,6 +15,7 @@ import qnaWorker from '../parsers/qnaWorker'; import httpClient from '../../utils/httpUtil'; import { BotStatus } from '../../constants'; import { getReferredLuFiles } from '../../utils/luUtil'; +import { reformQnAToContainerKB } from '../../utils/qnaUtil'; import luFileStatusStorage from '../../utils/luFileStatusStorage'; import { getReferredQnaFiles } from '../../utils/qnaUtil'; import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage'; @@ -170,12 +171,13 @@ export const projectDispatcher = () => { }); await lgWorker.addProject(projectId, lgFiles); + const updateQnAFiles = reformQnAToContainerKB(projectId, qnaFiles); // Important: gotoSnapshot will wipe all states. const newSnapshot = snapshot.map(({ set }) => { set(skillManifestsState, skillManifestFiles); set(luFilesState, initLuFilesStatus(botName, luFiles, dialogs)); - set(qnaFilesState, initQnaFilesStatus(botName, qnaFiles, dialogs)); + set(qnaFilesState, initQnaFilesStatus(botName, updateQnAFiles, dialogs)); set(lgFilesState, lgFiles); set(dialogsState, verifiedDialogs); set(dialogSchemasState, dialogSchemas); diff --git a/Composer/packages/client/src/utils/qnaUtil.ts b/Composer/packages/client/src/utils/qnaUtil.ts index 62529acaf3..ce65085e3c 100644 --- a/Composer/packages/client/src/utils/qnaUtil.ts +++ b/Composer/packages/client/src/utils/qnaUtil.ts @@ -7,6 +7,9 @@ * for more usage detail, please check client/__tests__/utils/luUtil.test.ts */ import { QnAFile, DialogInfo } from '@bfc/shared'; +import { qnaUtil } from '@bfc/indexers'; + +import { createFile, updateFile } from '../recoilModel/persistence/http'; import { getBaseName, getExtension } from './fileUtil'; @@ -20,3 +23,50 @@ export function getReferredQnaFiles(qnaFiles: QnAFile[], dialogs: DialogInfo[]) return dialogs.some((dialog) => dialog.qnaFile === idWithOutLocale && !!file.content); }); } +// substring text file by lines +export function substringTextByLine(text: string, start?: number, end?: number): string { + return text.split('\n').slice(start, end).join('\n'); +} +/** + * Move qna pair in *.qna to container KB *.source.qna file. + * @param qnaFiles + */ +export const reformQnAToContainerKB = (projectId: string, qnaFiles: QnAFile[]): QnAFile[] => { + const qnaFilesNeedMigrate = qnaFiles.filter((file) => { + return !file.id.endsWith('.source') && file.qnaSections.length; + }); + if (!qnaFilesNeedMigrate.length) return qnaFiles; + const updatedFiles: QnAFile[] = []; + const createdFiles: QnAFile[] = []; + qnaFilesNeedMigrate.forEach((file) => { + const { id, content } = file; + const qnaSectionStartLine = file.qnaSections[0].range?.start.line || 0; + const content1 = substringTextByLine(content, 0, qnaSectionStartLine - 1); + const content2 = substringTextByLine(content, qnaSectionStartLine - 1); + let file1 = qnaUtil.parse(id, content1); + const file2Id = `${getBaseName(id)}.source`; + file1 = qnaUtil.addImport(file1, `${file2Id}.qna`); + updateFile(projectId, `${file1.id}.qna`, file1.content); + updatedFiles.push(file1); + + const existedFile2 = qnaFiles.find((item) => item.id === file2Id); + if (existedFile2) { + const file2Content = existedFile2.content + '\n' + content2; + const file2 = qnaUtil.parse(file2Id, file2Content); + updateFile(projectId, `${file2.id}.qna`, file2.content); + updatedFiles.push(file2); + } else { + const file2Content = content2; + const file2 = qnaUtil.parse(file2Id, file2Content); + createFile(projectId, `${file2.id}.qna`, file2.content); + createdFiles.push(file2); + } + }); + + const newQnAfiles: QnAFile[] = qnaFiles.map((file) => { + const updated = updatedFiles.find((item) => item.id === file.id); + return updated || file; + }); + newQnAfiles.push(...createdFiles); + return newQnAfiles; +}; diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index c61b2e59fe..d2ff8503b4 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -341,7 +341,9 @@ export class BotProject implements IBotProject { } const file = this.files.get(name); if (file === undefined) { - throw new Error(`no such file ${name}`); + // throw new Error(`no such file ${name}`); + const { lastModified } = await this.createFile(name, content); + return lastModified; } const relativePath = file.relativePath; From a905b8a82e3d9dbf81f51739489a650380928745 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Wed, 16 Sep 2020 18:30:06 +0800 Subject: [PATCH 057/105] edit url source --- .../client/src/components/QnA/EditQnAFrom.tsx | 31 +++++ .../components/QnA/EditQnAFromUrlModal.tsx | 122 ++++++++++++++++++ .../src/pages/knowledge-base/table-view.tsx | 6 +- Composer/packages/client/src/utils/qnaUtil.ts | 4 + .../lib/indexers/__tests__/qnaUtil.test.ts | 5 +- .../lib/indexers/src/utils/qnaUtil.ts | 18 ++- .../packages/lib/shared/src/types/indexers.ts | 3 +- 7 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 Composer/packages/client/src/components/QnA/EditQnAFrom.tsx create mode 100644 Composer/packages/client/src/components/QnA/EditQnAFromUrlModal.tsx diff --git a/Composer/packages/client/src/components/QnA/EditQnAFrom.tsx b/Composer/packages/client/src/components/QnA/EditQnAFrom.tsx new file mode 100644 index 0000000000..4722a00f46 --- /dev/null +++ b/Composer/packages/client/src/components/QnA/EditQnAFrom.tsx @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import React from 'react'; +import { QnAFile } from '@bfc/shared'; + +import { getQnAFileUrlOption } from '../../utils/qnaUtil'; + +import EditQnAFromScratchModal, { EditQnAFromScratchFormData } from './EditQnAFromScratchModal'; +import EditQnAFromUrlModal, { EditQnAFromUrlFormData } from './EditQnAFromUrlModal'; + +interface EditQnAModalProps { + qnaFiles: QnAFile[]; + qnaFile: QnAFile; + onDismiss: () => void; + onSubmit: (formData: EditQnAFromScratchFormData | EditQnAFromUrlFormData) => void; +} + +export const EditQnAModal: React.FC = (props) => { + const url = getQnAFileUrlOption(props.qnaFile); + + if (url) { + return ; + } else { + return ; + } +}; + +export default EditQnAModal; diff --git a/Composer/packages/client/src/components/QnA/EditQnAFromUrlModal.tsx b/Composer/packages/client/src/components/QnA/EditQnAFromUrlModal.tsx new file mode 100644 index 0000000000..525c78e47d --- /dev/null +++ b/Composer/packages/client/src/components/QnA/EditQnAFromUrlModal.tsx @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import React from 'react'; +import formatMessage from 'format-message'; +import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; +import { QnAFile } from '@bfc/shared'; + +import { FieldConfig, useForm } from '../../hooks/useForm'; +import { getBaseName } from '../../utils/fileUtil'; +import { getQnAFileUrlOption } from '../../utils/qnaUtil'; + +import { validateName, validateUrl } from './constants'; +import { styles, dialogWindow, textField } from './styles'; + +interface EditQnAFromUrlModalProps { + qnaFiles: QnAFile[]; + qnaFile: QnAFile; + onDismiss: () => void; + onSubmit: (formData: EditQnAFromUrlFormData) => void; +} + +export interface EditQnAFromUrlFormData { + name: string; + url: string; +} + +const formConfig: FieldConfig = { + name: { + required: true, + defaultValue: '', + }, + url: { + required: true, + defaultValue: '', + }, +}; + +const DialogTitle = () => { + return
{formatMessage('Edit KB name')}
; +}; + +export const EditQnAFromUrlModal: React.FC = (props) => { + const { onDismiss, onSubmit, qnaFiles, qnaFile } = props; + + formConfig.name.validate = validateName(qnaFiles.filter(({ id }) => qnaFile.id !== id)); + formConfig.name.defaultValue = getBaseName(qnaFile.id); + formConfig.url.validate = validateUrl; + formConfig.url.defaultValue = getQnAFileUrlOption(qnaFile); + + const { formData, updateField, hasErrors, formErrors } = useForm(formConfig); + const disabled = hasErrors; + + const updateName = (name = '') => { + updateField('name', name); + }; + const updateUrl = (url = '') => { + updateField('url', url); + }; + + return ( + , + styles: styles.dialog, + }} + hidden={false} + modalProps={{ + isBlocking: false, + styles: styles.modal, + }} + onDismiss={onDismiss} + > +
+ + updateName(name)} + /> + + + updateUrl(url)} + /> + +
+ + + { + if (hasErrors) { + return; + } + onSubmit(formData); + }} + /> + +
+ ); +}; + +export default EditQnAFromUrlModal; diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index 133ab5ca90..26cca3dbb4 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -43,7 +43,7 @@ import { dispatcherState } from '../../recoilModel'; import { getBaseName } from '../../utils/fileUtil'; import { EditableField } from '../../components/EditableField'; import { classNames, AddTemplateButton } from '../../components/AllupviewComponets/styles'; -import { EditQnAFromScratchModal } from '../../components/QnA'; +import { EditQnAModal } from '../../components/QnA/EditQnAFrom'; import { formCell, content, addIcon, divider, rowDetails, icon, addAlternative } from './styles'; @@ -594,7 +594,7 @@ const TableView: React.FC = (props) => { /> {editQnAFile && ( - { @@ -608,7 +608,7 @@ const TableView: React.FC = (props) => { await actions.createQnAImport({ id: qnaFile.id, sourceId: newId }); setEditQnAFile(undefined); }} - > + > )}
); diff --git a/Composer/packages/client/src/utils/qnaUtil.ts b/Composer/packages/client/src/utils/qnaUtil.ts index ce65085e3c..8eb742c34d 100644 --- a/Composer/packages/client/src/utils/qnaUtil.ts +++ b/Composer/packages/client/src/utils/qnaUtil.ts @@ -70,3 +70,7 @@ export const reformQnAToContainerKB = (projectId: string, qnaFiles: QnAFile[]): newQnAfiles.push(...createdFiles); return newQnAfiles; }; + +export const getQnAFileUrlOption = (file: QnAFile): string | undefined => { + return file.options.find((opt) => opt.name === 'url')?.value; +}; diff --git a/Composer/packages/lib/indexers/__tests__/qnaUtil.test.ts b/Composer/packages/lib/indexers/__tests__/qnaUtil.test.ts index dcd9fa7840..f18f8a59c3 100644 --- a/Composer/packages/lib/indexers/__tests__/qnaUtil.test.ts +++ b/Composer/packages/lib/indexers/__tests__/qnaUtil.test.ts @@ -366,7 +366,7 @@ ${content1} describe('QnA File Options CRUD', () => { const fileId1 = 'a.qna'; - const content2 = `> !# @source.urls = https://download + const content2 = `> !# @source.url = https://download ${generateQnAPair()} ${content1} ${generateQnAPair()}`; @@ -376,10 +376,13 @@ describe('QnA File Options CRUD', () => { const { qnaSections, content, id, diagnostics, options } = qnaFile; expect(id).toEqual(fileId1); + expect(qnaFile.empty).toEqual(false); expect(content).toEqual(content2); expect(diagnostics.length).toEqual(0); expect(qnaSections.length).toEqual(4); expect(options.length).toEqual(1); + expect(options[0].name).toEqual('url'); + expect(options[0].value).toEqual('https://download'); }); it('update question in qna pair (with model info section in head)', () => { diff --git a/Composer/packages/lib/indexers/src/utils/qnaUtil.ts b/Composer/packages/lib/indexers/src/utils/qnaUtil.ts index b03c473c68..6fc8197a11 100644 --- a/Composer/packages/lib/indexers/src/utils/qnaUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/qnaUtil.ts @@ -121,14 +121,18 @@ export function convertQnAParseResultToQnAFile(id = '', resource: LuParseResourc }); // TODO: use regexp match "> !# @source.urls = https://download" - const options = Sections.filter(({ SectionType }) => SectionType === SectionTypes.LUModelInfo).map( - ({ ModelInfo, Id }) => { - return { - name: Id, - value: ModelInfo, - }; + const optionRegExp = new RegExp(/@source\.(\w+)\s*=\s*(.*)/); + const options: { id: string; name: string; value: string }[] = []; + Sections.filter(({ SectionType }) => SectionType === SectionTypes.LUModelInfo).forEach(({ ModelInfo, Id }) => { + const match = ModelInfo.match(optionRegExp); + if (match) { + options.push({ + id: Id, + name: match[1], + value: match[2], + }); } - ); + }); const diagnostics = Errors.map((e) => convertQnADiagnostic(e, id)); return { diff --git a/Composer/packages/lib/shared/src/types/indexers.ts b/Composer/packages/lib/shared/src/types/indexers.ts index 575373cc72..2d3b758566 100644 --- a/Composer/packages/lib/shared/src/types/indexers.ts +++ b/Composer/packages/lib/shared/src/types/indexers.ts @@ -128,7 +128,8 @@ export interface QnAFile { diagnostics: Diagnostic[]; qnaSections: QnASection[]; imports: { id: string; path: string }[]; - options: { name: string; value: string }[]; + options: { id: string; name: string; value: string }[]; + empty: boolean; resource: LuParseResource; [key: string]: any; } From 29831b5ce1b6c912aa2e6a3e76e4653015a45bfe Mon Sep 17 00:00:00 2001 From: liweitian Date: Wed, 16 Sep 2020 19:46:27 +0800 Subject: [PATCH 058/105] update css --- .../client/src/components/EditableField.tsx | 78 +++++++++++--- .../client/src/pages/knowledge-base/styles.ts | 74 ++++++++----- .../src/pages/knowledge-base/table-view.tsx | 100 +++++++----------- .../pages/language-generation/table-view.tsx | 6 +- .../pages/language-understanding/styles.ts | 4 + .../language-understanding/table-view.tsx | 6 +- 6 files changed, 163 insertions(+), 105 deletions(-) diff --git a/Composer/packages/client/src/components/EditableField.tsx b/Composer/packages/client/src/components/EditableField.tsx index 0eaa53ad90..a09a0d1e4f 100644 --- a/Composer/packages/client/src/components/EditableField.tsx +++ b/Composer/packages/client/src/components/EditableField.tsx @@ -1,18 +1,40 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - -import React, { useState, useEffect } from 'react'; -import { TextField, ITextFieldStyles, ITextFieldProps } from 'office-ui-fabric-react/lib/TextField'; -import { NeutralColors } from '@uifabric/fluent-theme'; +/** @jsx jsx */ +import { jsx, css } from '@emotion/core'; +import React, { useState, useEffect, useRef } from 'react'; +import { TextField, ITextFieldStyles, ITextFieldProps, ITextField } from 'office-ui-fabric-react/lib/TextField'; +import { NeutralColors, SharedColors } from '@uifabric/fluent-theme'; import { mergeStyleSets } from '@uifabric/styling'; +import { IconButton } from 'office-ui-fabric-react/lib/Button'; +import { IIconProps } from 'office-ui-fabric-react/lib/Icon'; +//------------------------ +const defaultContainerStyle = (isHovered) => css` + display: flex; + width: 100%; + outline: ${isHovered ? `2px solid ${SharedColors.cyanBlue10}` : undefined}; + margin-top: 2px; + :hover .ms-Button-icon { + visibility: visible; + } +`; + +//------------------------ +interface IconProps { + iconStyles?: Partial; + iconName: string; + onClick?: () => void; +} interface EditableFieldProps extends Omit { + autoAdjustHeight?: boolean; fontSize?: string; styles?: Partial; transparentBorder?: boolean; ariaLabel?: string; error?: string | JSX.Element; - + extraContent?: string; + containerStyles?: any; className?: string; depth: number; description?: string; @@ -24,16 +46,20 @@ interface EditableFieldProps extends Omit void; onChange: (newValue?: string) => void; onFocus?: (id: string, value?: string) => void; - onBlur?: (id: string, value?: string) => void; } const EditableField: React.FC = (props) => { const { + containerStyles, depth, + extraContent = '', styles = {}, + iconProps, placeholder, fontSize, autoAdjustHeight = false, @@ -47,18 +73,24 @@ const EditableField: React.FC = (props) => { className, transparentBorder, ariaLabel, + enableIcon = false, } = props; const [editing, setEditing] = useState(false); const [hasFocus, setHasFocus] = useState(false); const [localValue, setLocalValue] = useState(value); const [hasBeenEdited, setHasBeenEdited] = useState(false); - + const fieldRef = useRef(null); useEffect(() => { if (!hasBeenEdited || value !== localValue) { setLocalValue(value); } }, [value]); + const resetValue = () => { + setLocalValue(''); + fieldRef.current?.focus(); + }; + const handleChange = (_e: any, newValue?: string) => { setLocalValue(newValue); setHasBeenEdited(true); @@ -76,14 +108,14 @@ const EditableField: React.FC = (props) => { if (!editing && !error) { borderColor = localValue || transparentBorder || depth > 1 ? 'transparent' : NeutralColors.gray30; } - return ( - <> +
= (props) => { styles ) as Partial } - value={localValue} + value={hasFocus ? localValue : localValue + extraContent} onBlur={handleCommit} onChange={handleChange} onFocus={() => setHasFocus(true)} onMouseEnter={() => setEditing(true)} onMouseLeave={() => !hasFocus && setEditing(false)} /> - + {enableIcon && ( + + )} +
); }; diff --git a/Composer/packages/client/src/pages/knowledge-base/styles.ts b/Composer/packages/client/src/pages/knowledge-base/styles.ts index 3bf34cbee8..3e3addf46c 100644 --- a/Composer/packages/client/src/pages/knowledge-base/styles.ts +++ b/Composer/packages/client/src/pages/knowledge-base/styles.ts @@ -93,13 +93,6 @@ export const rowDetails = { '.ms-Button': { visibility: 'visible', }, - // '&.is-selected': { - // selectors: { - // '.ms-DetailsRowy': { - // background: NeutralColors.gray30, - // }, - // }, - // }, }, }, '&.is-selected': { @@ -155,29 +148,55 @@ export const addIcon = { }, }; -export const editableFieldAnswer = { - root: { - height: '100%', - selectors: { - '.ms-TextField-wrapper': { - height: '100%', +export const editableFieldAnswer = (isExpand) => { + return { + root: { + height: '100%', + selectors: { + '.ms-TextField-wrapper': { + height: '100%', + }, }, }, - }, - fieldGroup: { - height: '100%', - border: '0', - }, + fieldGroup: { + height: '100%', + border: '0', + selectors: { + '&.ms-TextField-fieldGroup': { + selectors: { + '::after': { + border: 'none !important', + }, + }, + }, + }, + }, + field: { + height: isExpand ? undefined : '80px !important', + overflowY: 'auto' as 'auto', + maxHeight: 500, + }, + }; }; -export const editableFieldQuestion = { - root: { - width: '90%', - }, - fieldGroup: { - border: '0', - marginBottom: 1, - }, +export const editableFieldQuestion = (index) => { + return { + fieldGroup: { + border: '0', + selectors: { + '&.ms-TextField-fieldGroup': { + selectors: { + '::after': { + border: 'none !important', + }, + }, + }, + }, + }, + field: { + fontWeight: index === 0 ? FontWeights.semibold : FontWeights.regular, + }, + }; }; export const questionNumber = { @@ -186,5 +205,4 @@ export const questionNumber = { export const firstQuestion = css` display: flex; -} -`; +}`; diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index 7d193a4a9b..b35aab5a58 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -160,20 +160,6 @@ const TableView: React.FC = (props) => { setQnASections(newSections); }; - // const expandRow = (sectionId: string) => { - // const newSections = qnaSections.map((item) => { - // if (item.sectionId === sectionId) { - // return { - // ...item, - // expand: true, - // }; - // } else { - // return item; - // } - // }); - // setQnASections(newSections); - // }; - const onUpdateQnAQuestion = (fileId: string, sectionId: string, questionId: string, content: string) => { if (!fileId) return; actions.setMessage('item deleted'); @@ -375,52 +361,35 @@ const TableView: React.FC = (props) => { return (
-
- { - const newValue = value?.trim().replace(/^#/, ''); - const isChanged = showingQuestions[0].content !== newValue; - if (newValue && isChanged) { - onUpdateQnAQuestion(item.fileId, item.sectionId, showingQuestions[0].id, newValue); - } - }} - onChange={() => {}} - /> - {!item.expand && !isQuestionEmpty &&
({questions.length})
} -
- {showingQuestions.length > 1 && - showingQuestions.slice(1).map((question) => { - return ( - { - const newValue = value?.trim().replace(/^#/, ''); - const isChanged = question.content !== newValue; - if (newValue && isChanged) { - onUpdateQnAQuestion(item.fileId, item.sectionId, question.id, newValue); - } - }} - onChange={() => {}} - /> - ); - })} + {showingQuestions.map((question, qIndex: number) => { + console.log(editableFieldQuestion(qIndex)); + return ( + { + const newValue = value?.trim().replace(/^#/, ''); + const isChanged = question.content !== newValue; + if (newValue && isChanged) { + onUpdateQnAQuestion(item.fileId, item.sectionId, question.id, newValue); + } + }} + onChange={() => {}} + /> + ); + })} {kthSectionIsCreatingQuestion === index ? ( = (props) => { key: 'Answer', name: formatMessage('Answer'), fieldName: 'answer', - minWidth: 250, - maxWidth: 450, + minWidth: 350, + maxWidth: 550, isResizable: true, data: 'string', onRender: (item) => { @@ -463,15 +432,20 @@ const TableView: React.FC = (props) => { return (
{ const newValue = value?.trim().replace(/^#/, ''); diff --git a/Composer/packages/client/src/pages/language-generation/table-view.tsx b/Composer/packages/client/src/pages/language-generation/table-view.tsx index cf966dab3d..c6e23e21e8 100644 --- a/Composer/packages/client/src/pages/language-generation/table-view.tsx +++ b/Composer/packages/client/src/pages/language-generation/table-view.tsx @@ -21,7 +21,7 @@ import { lgUtil } from '@bfc/indexers'; import { EditableField } from '../../components/EditableField'; import { navigateTo } from '../../utils/navigation'; -import { actionButton, formCell } from '../language-understanding/styles'; +import { actionButton, formCell, editableFieldContainer } from '../language-understanding/styles'; import { dispatcherState, lgFilesState, projectIdState, localeState, settingsState } from '../../recoilModel'; import { languageListTemplates } from '../../components/MultiLanguage'; import { validatedDialogsSelector } from '../../recoilModel/selectors/validatedDialogs'; @@ -198,6 +198,7 @@ const TableView: React.FC = (props) => {
= (props) => { = (props) => { = (props) => { { dialogId: string; } @@ -168,6 +168,7 @@ const TableView: React.FC = (props) => { = (props) => { = (props) => { = (props) => { Date: Wed, 16 Sep 2020 23:18:54 +0800 Subject: [PATCH 059/105] update css --- .../client/src/components/EditableField.tsx | 35 ++++++++- .../src/pages/knowledge-base/table-view.tsx | 73 ++++++++++++------- 2 files changed, 80 insertions(+), 28 deletions(-) diff --git a/Composer/packages/client/src/components/EditableField.tsx b/Composer/packages/client/src/components/EditableField.tsx index a09a0d1e4f..e0d59d2c12 100644 --- a/Composer/packages/client/src/components/EditableField.tsx +++ b/Composer/packages/client/src/components/EditableField.tsx @@ -50,7 +50,7 @@ interface EditableFieldProps extends Omit void; onChange: (newValue?: string) => void; - onFocus?: (id: string, value?: string) => void; + onFocus?: () => void; } const EditableField: React.FC = (props) => { @@ -65,6 +65,7 @@ const EditableField: React.FC = (props) => { autoAdjustHeight = false, multiline = false, onChange, + onFocus, onBlur, resizable = true, value, @@ -78,6 +79,7 @@ const EditableField: React.FC = (props) => { const [editing, setEditing] = useState(false); const [hasFocus, setHasFocus] = useState(false); const [localValue, setLocalValue] = useState(value); + const [initialValue, setInitialValue] = useState(''); const [hasBeenEdited, setHasBeenEdited] = useState(false); const fieldRef = useRef(null); useEffect(() => { @@ -86,6 +88,12 @@ const EditableField: React.FC = (props) => { } }, [value]); + useEffect(() => { + if (hasFocus) { + setInitialValue(localValue); + } + }, [hasFocus]); + const resetValue = () => { setLocalValue(''); fieldRef.current?.focus(); @@ -103,6 +111,27 @@ const EditableField: React.FC = (props) => { onBlur && onBlur(id, localValue); }; + const handleOnFocus = () => { + setHasFocus(true); + onFocus && onFocus(); + }; + + const cancel = () => { + setHasFocus(false); + setEditing(false); + setLocalValue(initialValue); + fieldRef.current?.blur(); + }; + + const handleOnKeyDown = (e) => { + if (e.key === 'Enter' && !multiline) { + handleCommit(); + } + if (e.key === 'Escape') { + cancel(); + } + }; + let borderColor: string | undefined = undefined; if (!editing && !error) { @@ -111,6 +140,7 @@ const EditableField: React.FC = (props) => { return (
= (props) => { value={hasFocus ? localValue : localValue + extraContent} onBlur={handleCommit} onChange={handleChange} - onFocus={() => setHasFocus(true)} + onFocus={handleOnFocus} + onKeyDown={handleOnKeyDown} onMouseEnter={() => setEditing(true)} onMouseLeave={() => !hasFocus && setEditing(false)} /> diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index b35aab5a58..e4717c4d57 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -51,8 +51,6 @@ import { addAlternative, editableFieldAnswer, editableFieldQuestion, - questionNumber, - firstQuestion, } from './styles'; const noOp = () => undefined; @@ -117,6 +115,21 @@ const TableView: React.FC = (props) => { const importedFileIds = qnaFile?.imports.map(({ id }) => getBaseName(id)) || []; const importedFiles = qnaFiles.filter(({ id }) => importedFileIds.includes(id)); const importedSourceFiles = importedFiles.filter(({ id }) => id.endsWith('.source')); + const [isQnASectionsExpanded, setIsQnASectionsExpanded] = useState([]); + + useEffect(() => { + if (isEmpty(qnaFiles)) return; + const allSections = qnaFiles.reduce((result: any[], qnaFile) => { + const res = generateQnASections(qnaFile); + return result.concat(res); + }, []); + if (dialogId === 'all') { + setIsQnASectionsExpanded(Array(allSections.length).fill(false)); + } else { + const dialogSections = allSections.filter((t) => t.dialogId === dialogId || importedFileIds.includes(t.fileId)); + setIsQnASectionsExpanded(Array(dialogSections.length).fill(false)); + } + }, [dialogId, projectId]); useEffect(() => { if (isEmpty(qnaFiles)) return; @@ -146,18 +159,18 @@ const TableView: React.FC = (props) => { } }, [qnaFiles, dialogId, projectId]); - const toggleExpandRow = (sectionId: string, expand?: boolean) => { - const newSections = qnaSections.map((item) => { - if (item.sectionId === sectionId) { - return { - ...item, - expand: expand === undefined ? !item.expand : expand, - }; - } else { - return item; - } - }); - setQnASections(newSections); + const toggleExpandRow = (index) => { + const newArray = [...isQnASectionsExpanded]; + newArray[index] = !newArray[index]; + setIsQnASectionsExpanded(newArray); + }; + + const expandRow = (index) => { + if (!isQnASectionsExpanded[index]) { + const newArray = [...isQnASectionsExpanded]; + newArray[index] = true; + setIsQnASectionsExpanded(newArray); + } }; const onUpdateQnAQuestion = (fileId: string, sectionId: string, questionId: string, content: string) => { @@ -192,11 +205,13 @@ const TableView: React.FC = (props) => { actions.setMessage('item deleted'); const sectionIndex = qnaSections.findIndex((item) => item.fileId === fileId); setFocusedIndex(sectionIndex); - removeQnAPairs({ id: fileId, sectionId, }); + const newArray = [...isQnASectionsExpanded]; + newArray.splice(sectionIndex, 1); + setIsQnASectionsExpanded(newArray); }; const onCreateNewQnAPairs = (fileId: string | undefined) => { @@ -205,6 +220,8 @@ const TableView: React.FC = (props) => { //const sectionIndex = qnaSections.findIndex((item) => item.fileId === fileId); //setFocusedIndex(sectionIndex + 1); createQnAPairs({ id: fileId, content: newQnAPair }); + const newArray = [false, ...isQnASectionsExpanded]; + setIsQnASectionsExpanded(newArray); }; const onCreateNewQuestion = (fileId, sectionId, content?: string) => { @@ -321,14 +338,14 @@ const TableView: React.FC = (props) => { minWidth: 40, maxWidth: 40, isResizable: true, - onRender: (item) => { + onRender: (item, index) => { return ( toggleExpandRow(item.sectionId)} + onClick={() => toggleExpandRow(index)} /> ); }, @@ -343,7 +360,7 @@ const TableView: React.FC = (props) => { data: 'string', onRender: (item: QnASectionItem, index) => { const questions = item.Questions; - const showingQuestions = item.expand ? questions : questions.slice(0, limitedNumber); + const showingQuestions = isQnASectionsExpanded[index] ? questions : questions.slice(0, limitedNumber); //This question content of this qna Section is '#?' const isQuestionEmpty = showingQuestions.length === 1 && showingQuestions[0].content === ''; const isSourceSectionInDialog = item.fileId.endsWith('.source') && !dialogId.endsWith('.source'); @@ -362,7 +379,6 @@ const TableView: React.FC = (props) => { return (
{showingQuestions.map((question, qIndex: number) => { - console.log(editableFieldQuestion(qIndex)); return ( = (props) => { ariaLabel={formatMessage(`Question is {content}`, { content: question.content })} depth={0} disabled={isAllowEdit} - extraContent={qIndex === 0 && !item.expand && !isQuestionEmpty ? ` (${questions.length})` : ''} + extraContent={ + qIndex === 0 && !isQnASectionsExpanded[index] && !isQuestionEmpty ? ` (${questions.length})` : '' + } iconProps={{ iconName: 'Cancel', }} @@ -387,6 +405,7 @@ const TableView: React.FC = (props) => { } }} onChange={() => {}} + onFocus={() => expandRow(index)} /> ); })} @@ -400,7 +419,7 @@ const TableView: React.FC = (props) => { id={'New Question'} name={'New Question'} placeholder={'add new question'} - styles={editableFieldQuestion} + styles={editableFieldQuestion(-1)} value={''} onBlur={(_id, value) => { const newValue = value?.trim().replace(/^#/, ''); @@ -410,6 +429,7 @@ const TableView: React.FC = (props) => { setCreatingQuestionInKthSection(-1); }} onChange={() => {}} + onFocus={() => expandRow(index)} /> ) : ( addQuestionButton @@ -426,7 +446,7 @@ const TableView: React.FC = (props) => { maxWidth: 550, isResizable: true, data: 'string', - onRender: (item) => { + onRender: (item, index) => { const isSourceSectionInDialog = item.fileId.endsWith('.source') && !dialogId.endsWith('.source'); const isAllowEdit = dialogId !== 'all' && !isSourceSectionInDialog; return ( @@ -435,7 +455,7 @@ const TableView: React.FC = (props) => { enableIcon multiline ariaLabel={formatMessage(`Answer is {content}`, { content: item.Answer })} - autoAdjustHeight={item.expand} + autoAdjustHeight={isQnASectionsExpanded[index]} depth={0} disabled={isAllowEdit} iconProps={{ @@ -445,7 +465,7 @@ const TableView: React.FC = (props) => { name={item.Answer} placeholder={'add new answer'} resizable={false} - styles={editableFieldAnswer(item.expand)} + styles={editableFieldAnswer(isQnASectionsExpanded[index])} value={item.Answer} onBlur={(_id, value) => { const newValue = value?.trim().replace(/^#/, ''); @@ -455,6 +475,7 @@ const TableView: React.FC = (props) => { } }} onChange={() => {}} + onFocus={() => expandRow(index)} />
); @@ -571,7 +592,7 @@ const TableView: React.FC = (props) => { {...props} styles={rowDetails} tabIndex={props.itemIndex} - //onClick={() => expandRow(props.item.sectionId)} + //onClick={() => toggleExpandRow(props.itemIndex)} /> ); } From 6620d0c24b1b9565e097bddcb41353086742bc3f Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Thu, 17 Sep 2020 10:25:04 +0800 Subject: [PATCH 060/105] clean up --- .../components/QnA/EditQnAFromUrlModal.tsx | 1 + .../src/pages/knowledge-base/QnAPage.tsx | 1 - .../src/pages/knowledge-base/code-editor.tsx | 3 +- .../src/pages/knowledge-base/table-view.tsx | 63 +++++++------------ 4 files changed, 24 insertions(+), 44 deletions(-) diff --git a/Composer/packages/client/src/components/QnA/EditQnAFromUrlModal.tsx b/Composer/packages/client/src/components/QnA/EditQnAFromUrlModal.tsx index 525c78e47d..1a7636a8bb 100644 --- a/Composer/packages/client/src/components/QnA/EditQnAFromUrlModal.tsx +++ b/Composer/packages/client/src/components/QnA/EditQnAFromUrlModal.tsx @@ -91,6 +91,7 @@ export const EditQnAFromUrlModal: React.FC = (props) = = (props) => { const search = props.location?.search ?? ''; const searchContainerId = querystring.parse(search).C; - const targetFileId = searchContainerId ? searchContainerId : `${dialogId}.${locale}`; + const targetFileId = + searchContainerId && typeof searchContainerId === 'string' ? searchContainerId : `${dialogId}.${locale}`; const file = qnaFiles.find(({ id }) => id === targetFileId); const hash = props.location?.hash ?? ''; const hashLine = querystring.parse(hash).L; diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index 3395c1395f..fa01184c34 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -117,7 +117,6 @@ const TableView: React.FC = (props) => { : []; }; const [editQnAFile, setEditQnAFile] = useState(undefined); - const [focusedIndex, setFocusedIndex] = useState(-1); const [qnaSections, setQnASections] = useState([]); const [kthSectionIsCreatingQuestion, setCreatingQuestionInKthSection] = useState(-1); const importedFileIds = qnaFile?.imports.map(({ id }) => getBaseName(id)) || []; @@ -149,24 +148,11 @@ const TableView: React.FC = (props) => { return result.concat(res); }, []); if (dialogId === 'all') { - const sections = allSections.map((item, index) => { - return { - ...item, - expand: index === focusedIndex, - }; - }); - setQnASections(sections); + setQnASections(allSections); } else { const dialogSections = allSections.filter((t) => importedFileIds.includes(t.fileId)); - setQnASections( - dialogSections.map((item, index) => { - return { - ...item, - expand: index === focusedIndex, - }; - }) - ); + setQnASections(dialogSections); } }, [qnaFiles, dialogId, projectId]); @@ -187,9 +173,6 @@ const TableView: React.FC = (props) => { const onUpdateQnAQuestion = (fileId: string, sectionId: string, questionId: string, content: string) => { if (!fileId) return; actions.setMessage('item deleted'); - const sectionIndex = qnaSections.findIndex((item) => item.fileId === fileId); - setFocusedIndex(sectionIndex); - updateQnAQuestion({ id: fileId, sectionId, @@ -201,9 +184,6 @@ const TableView: React.FC = (props) => { const onUpdateQnAAnswer = (fileId: string, sectionId: string, content: string) => { if (!fileId) return; actions.setMessage('item deleted'); - const sectionIndex = qnaSections.findIndex((item) => item.fileId === fileId); - setFocusedIndex(sectionIndex); - updateQnAAnswer({ id: fileId, sectionId, @@ -215,7 +195,6 @@ const TableView: React.FC = (props) => { if (!fileId) return; actions.setMessage('item deleted'); const sectionIndex = qnaSections.findIndex((item) => item.fileId === fileId); - setFocusedIndex(sectionIndex); removeQnAPairs({ id: fileId, sectionId, @@ -228,18 +207,15 @@ const TableView: React.FC = (props) => { const onCreateNewQnAPairs = (fileId: string | undefined) => { if (!fileId) return; const newQnAPair = qnaUtil.generateQnAPair(); - //const sectionIndex = qnaSections.findIndex((item) => item.fileId === fileId); - //setFocusedIndex(sectionIndex + 1); + const sectionIndex = qnaSections.findIndex((item) => item.fileId === fileId); createQnAPairs({ id: fileId, content: newQnAPair }); - const newArray = [false, ...isQnASectionsExpanded]; + const newArray = [...isQnASectionsExpanded]; + newArray.splice(sectionIndex, 0, false); setIsQnASectionsExpanded(newArray); }; const onCreateNewQuestion = (fileId, sectionId, content?: string) => { if (!fileId || !sectionId) return; - const sectionIndex = qnaSections.findIndex((item) => item.sectionId === sectionId); - setFocusedIndex(sectionIndex); - const payload = { id: fileId, sectionId, @@ -248,6 +224,16 @@ const TableView: React.FC = (props) => { createQnAQuestion(payload); }; + const onSubmitEditKB = async ({ name }: { name: string }) => { + if (!editQnAFile) return; + const newId = `${name}.source`; + await actions.renameQnAKB({ id: editQnAFile.id, name: newId }); + if (!qnaFile) return; + await actions.removeQnAImport({ id: qnaFile.id, sourceId: editQnAFile.id }); + await actions.createQnAImport({ id: qnaFile.id, sourceId: newId }); + setEditQnAFile(undefined); + }; + const onRenderGroupHeader: IDetailsGroupRenderProps['onRenderHeader'] = (props) => { const groupName = props?.group?.name || ''; const containerId = props?.group?.key || ''; @@ -555,14 +541,14 @@ const TableView: React.FC = (props) => { const startIndex = lastGroup ? lastGroup.startIndex + lastGroup.count : 0; const { id, qnaSections } = currentFile; const count = qnaSections.length; - const shouldExpand = inRange(focusedIndex, startIndex, startIndex + count) || searchContainerId === id; + // const shouldExpand = inRange(focusedIndex, startIndex, startIndex + count) || searchContainerId === id; groups.push({ key: id, name: getBaseName(id), startIndex, count, level: 0, - isCollapsed: !shouldExpand, + // isCollapsed: !shouldExpand, }); }); return groups; @@ -575,7 +561,7 @@ const TableView: React.FC = (props) => { startIndex: 0, count: qnaFile.qnaSections.length, level: 0, - isCollapsed: !inRange(focusedIndex, 0, qnaFile.qnaSections.length), + // isCollapsed: !inRange(focusedIndex, 0, qnaFile.qnaSections.length), }, ]; importedSourceFiles.forEach((currentFile) => { @@ -583,9 +569,9 @@ const TableView: React.FC = (props) => { const startIndex = lastGroup.startIndex + lastGroup.count; const { id, qnaSections } = currentFile; const count = qnaSections.length; - const shouldExpand = inRange(focusedIndex, startIndex, startIndex + count) || searchContainerId === id; + // const shouldExpand = inRange(focusedIndex, startIndex, startIndex + count) || searchContainerId === id; const name = getBaseName(id); - groups.push({ key: id, name, startIndex, count, level: 0, isCollapsed: !shouldExpand }); + groups.push({ key: id, name, startIndex, count, level: 0 }); }); return groups; @@ -669,14 +655,7 @@ const TableView: React.FC = (props) => { onDismiss={() => { setEditQnAFile(undefined); }} - onSubmit={async ({ name }) => { - const newId = `${name}.source`; - await actions.renameQnAKB({ id: editQnAFile.id, name: newId }); - if (!qnaFile) return; - await actions.removeQnAImport({ id: qnaFile.id, sourceId: editQnAFile.id }); - await actions.createQnAImport({ id: qnaFile.id, sourceId: newId }); - setEditQnAFile(undefined); - }} + onSubmit={onSubmitEditKB} > )}
From 57b027e5e3218f83921369c7f596f9f96c5c00a2 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Thu, 17 Sep 2020 10:41:13 +0800 Subject: [PATCH 061/105] update bf-lu package --- Composer/packages/lib/indexers/package.json | 3 +- .../lib/indexers/src/utils/qnaUtil.ts | 3 +- Composer/packages/server/package.json | 5 +- .../server/src/models/utilities/parser.ts | 4 +- Composer/plugins/azurePublish/yarn.lock | 47 +++-- Composer/yarn.lock | 184 +++++++++++++----- 6 files changed, 160 insertions(+), 86 deletions(-) diff --git a/Composer/packages/lib/indexers/package.json b/Composer/packages/lib/indexers/package.json index 2b188fc96c..30ad43f6f1 100644 --- a/Composer/packages/lib/indexers/package.json +++ b/Composer/packages/lib/indexers/package.json @@ -27,8 +27,7 @@ "rimraf": "^2.6.3" }, "dependencies": { - "@bfcomposer/bf-lu": "^1.4.8", - "@microsoft/bf-lu": "^4.11.0-dev.20200824.de66343", + "@microsoft/bf-lu": "^4.11.0-dev.20200916.4f7c793", "adaptive-expressions": "4.10.0-preview-147186", "botbuilder-lg": "^4.10.0-preview-150886", "lodash": "^4.17.19" diff --git a/Composer/packages/lib/indexers/src/utils/qnaUtil.ts b/Composer/packages/lib/indexers/src/utils/qnaUtil.ts index 6fc8197a11..38c35de039 100644 --- a/Composer/packages/lib/indexers/src/utils/qnaUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/qnaUtil.ts @@ -8,8 +8,7 @@ //import isEmpty from 'lodash/isEmpty'; import { QnAFile } from '@bfc/shared'; -// import { sectionHandler } from '@microsoft/bf-lu/lib/parser/composerindex'; -import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser/composerindex'; +import { sectionHandler } from '@microsoft/bf-lu/lib/parser/composerindex'; import isEmpty from 'lodash/isEmpty'; import { Diagnostic, Position, Range, DiagnosticSeverity, LuParseResource } from '@bfc/shared'; import { nanoid } from 'nanoid'; diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index 5363ebb123..3d9b60e9ea 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -55,16 +55,15 @@ "dependencies": { "@azure/ms-rest-js": "^2.0.6", "@bfc/client": "*", + "@bfc/extension": "*", "@bfc/indexers": "*", "@bfc/intellisense-languageserver": "*", "@bfc/lg-languageserver": "*", "@bfc/lu-languageserver": "*", - "@bfc/extension": "*", "@bfc/shared": "*", - "@bfcomposer/bf-lu": "^1.4.8", "@microsoft/bf-dispatcher": "^4.10.0-preview.141651", - "@microsoft/bf-lu": "^4.11.0-dev.20200824.de66343", "@microsoft/bf-generate-library": "^4.10.0-daily.20200827.161452", + "@microsoft/bf-lu": "^4.11.0-dev.20200916.4f7c793", "archiver": "^5.0.2", "axios": "^0.19.2", "azure-storage": "^2.10.3", diff --git a/Composer/packages/server/src/models/utilities/parser.ts b/Composer/packages/server/src/models/utilities/parser.ts index f28177fe20..95d9d1aecc 100644 --- a/Composer/packages/server/src/models/utilities/parser.ts +++ b/Composer/packages/server/src/models/utilities/parser.ts @@ -11,9 +11,7 @@ import log from './../../logger'; import { DOC_EXTENSIONS, QNA_SUBSCRIPTION_KEY, COGNITIVE_SERVICES_ENDPOINTS } from './../../constants'; // eslint-disable-next-line @typescript-eslint/no-var-requires -// const qnaBuild = require('@microsoft/bf-lu/lib/parser/qnabuild/builder.js'); -// eslint-disable-next-line @typescript-eslint/no-var-requires -const qnaBuild = require('@bfcomposer/bf-lu/lib/parser/qnabuild/builder.js'); +const qnaBuild = require('@microsoft/bf-lu/lib/parser/qnabuild/builder.js'); const debug = log.extend('helper-parser'); diff --git a/Composer/plugins/azurePublish/yarn.lock b/Composer/plugins/azurePublish/yarn.lock index 527c3cdb0b..2e44868bde 100644 --- a/Composer/plugins/azurePublish/yarn.lock +++ b/Composer/plugins/azurePublish/yarn.lock @@ -167,8 +167,7 @@ "@bfc/indexers@../../packages/lib/indexers": version "0.0.0" dependencies: - "@bfcomposer/bf-lu" "^1.4.8" - "@microsoft/bf-lu" "^4.11.0-dev.20200824.de66343" + "@microsoft/bf-lu" "^4.11.0-dev.20200916.4f7c793" adaptive-expressions "4.10.0-preview-147186" botbuilder-lg "^4.10.0-preview-150886" lodash "^4.17.19" @@ -181,28 +180,6 @@ nanoid "^3.1.3" nanoid-dictionary "^3.0.0" -"@bfcomposer/bf-lu@^1.4.8": - version "1.4.8" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.8.tgz#dbb68cc54b2f70c558623952f38567e65d49b5de" - integrity sha1-27aMxUsvcMVYYjlS84Vn5l1Jtd4= - dependencies: - "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" - "@azure/ms-rest-azure-js" "2.0.1" - "@types/node-fetch" "~2.5.5" - antlr4 "^4.7.2" - chalk "2.4.1" - console-stream "^0.1.1" - deep-equal "^1.0.1" - delay "^4.3.0" - fs-extra "^8.1.0" - get-stdin "^6.0.0" - globby "^10.0.1" - intercept-stdout "^0.1.2" - lodash "^4.17.19" - node-fetch "~2.6.0" - semver "^5.5.1" - tslib "^1.10.0" - "@microsoft/bf-cli-command@4.10.0-dev.20200721.8bb21ac": version "4.10.0-dev.20200721.8bb21ac" resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-cli-command/-/@microsoft/bf-cli-command-4.10.0-dev.20200721.8bb21ac.tgz#9c81b37bc10072ca6beec7d7f68aa309f8a552ec" @@ -266,6 +243,28 @@ semver "^5.5.1" tslib "^1.10.0" +"@microsoft/bf-lu@^4.11.0-dev.20200916.4f7c793": + version "4.11.0-dev.20200916.4f7c793" + resolved "https://registry.yarnpkg.com/@microsoft/bf-lu/-/bf-lu-4.11.0-dev.20200916.4f7c793.tgz#0b2a2c1cbed2e98a64c2ca379d306b6454ff1e16" + integrity sha512-IuiEjvV4/3S1tDFIPMQK9rIuUIEiAUPvtLrTaoMKEiEFTviPS0FdqwQJsorsHg3Vxy8h0GXsVFXJQiI1gro4GQ== + dependencies: + "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" + "@azure/ms-rest-azure-js" "2.0.1" + "@types/node-fetch" "~2.5.5" + antlr4 "^4.7.2" + chalk "2.4.1" + console-stream "^0.1.1" + deep-equal "^1.0.1" + delay "^4.3.0" + fs-extra "^8.1.0" + get-stdin "^6.0.0" + globby "^10.0.1" + intercept-stdout "^0.1.2" + lodash "^4.17.19" + node-fetch "~2.6.0" + semver "^5.5.1" + tslib "^1.10.0" + "@microsoft/bf-luis-cli@^4.10.0-dev.20200721.8bb21ac": version "4.10.0-dev.20200721.8bb21ac" resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-luis-cli/-/@microsoft/bf-luis-cli-4.10.0-dev.20200721.8bb21ac.tgz#b952806f8093dd1639e1141ede9d27ec74b6d379" diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 7bc06ecf72..2fe8e548b7 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -864,10 +864,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.10.5", "@babel/parser@^7.11.3", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4", "@babel/parser@^7.4.0", "@babel/parser@^7.7.0", "@babel/parser@^7.7.4", "@babel/parser@^7.7.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0", "@babel/parser@^7.9.6": - version "7.11.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.3.tgz#9e1eae46738bcd08e23e867bab43e7b95299a8f9" - integrity sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA== +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.10.5", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4", "@babel/parser@^7.4.0", "@babel/parser@^7.7.0", "@babel/parser@^7.7.4", "@babel/parser@^7.7.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0", "@babel/parser@^7.9.6": + version "7.11.5" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@babel/parser/-/@babel/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" + integrity sha1-x/9jA99xCA7HpPW4wAPFjxz1EDc= "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" @@ -2312,28 +2312,6 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@bfcomposer/bf-lu@^1.4.8": - version "1.4.8" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.4.8.tgz#dbb68cc54b2f70c558623952f38567e65d49b5de" - integrity sha1-27aMxUsvcMVYYjlS84Vn5l1Jtd4= - dependencies: - "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" - "@azure/ms-rest-azure-js" "2.0.1" - "@types/node-fetch" "~2.5.5" - antlr4 "^4.7.2" - chalk "2.4.1" - console-stream "^0.1.1" - deep-equal "^1.0.1" - delay "^4.3.0" - fs-extra "^8.1.0" - get-stdin "^6.0.0" - globby "^10.0.1" - intercept-stdout "^0.1.2" - lodash "^4.17.19" - node-fetch "~2.6.0" - semver "^5.5.1" - tslib "^1.10.0" - "@cnakazawa/watch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" @@ -3047,6 +3025,28 @@ semver "^5.5.1" tslib "^1.10.0" +"@microsoft/bf-lu@^4.11.0-dev.20200916.4f7c793": + version "4.11.0-dev.20200916.4f7c793" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-4.11.0-dev.20200916.4f7c793.tgz#0b2a2c1cbed2e98a64c2ca379d306b6454ff1e16" + integrity sha1-CyosHL7S6Ypkwso3nTBrZFT/HhY= + dependencies: + "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" + "@azure/ms-rest-azure-js" "2.0.1" + "@types/node-fetch" "~2.5.5" + antlr4 "^4.7.2" + chalk "2.4.1" + console-stream "^0.1.1" + deep-equal "^1.0.1" + delay "^4.3.0" + fs-extra "^8.1.0" + get-stdin "^6.0.0" + globby "^10.0.1" + intercept-stdout "^0.1.2" + lodash "^4.17.19" + node-fetch "~2.6.0" + semver "^5.5.1" + tslib "^1.10.0" + "@microsoft/load-themed-styles@^1.10.26": version "1.10.39" resolved "https://registry.yarnpkg.com/@microsoft/load-themed-styles/-/load-themed-styles-1.10.39.tgz#23024bfa264a01ab2f05a9fda9c97c1f90ef80d8" @@ -5504,13 +5504,14 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== -bl@^2.2.1, bl@^4.0.3: - version "2.2.1" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/bl/-/bl-2.2.1.tgz#8c11a7b730655c5d56898cdc871224f40fd901d5" - integrity sha1-jBGntzBlXF1WiYzchxIk9A/ZAdU= +bl@^4.0.3: + version "4.0.3" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" + integrity sha1-EtYoetwpCA4ipwXldksqlSLNxIk= dependencies: - readable-stream "^2.3.5" - safe-buffer "^5.1.1" + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" blob-util@2.0.2: version "2.0.2" @@ -5841,6 +5842,14 @@ buffer@^5.1.0: base64-js "^1.0.2" ieee754 "^1.1.4" +buffer@^5.5.0: + version "5.6.0" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" + integrity sha1-oxdJ3H2B2E2wir+Te2uMQDP2J4Y= + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + builder-util-runtime@8.6.2: version "8.6.2" resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.6.2.tgz#8270e15b012d8d3b110f3e327b0fd8b0e07b1686" @@ -8552,9 +8561,9 @@ elegant-spinner@^1.0.1: resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= -elliptic@^6.0.0, elliptic@^6.5.3: +elliptic@^6.0.0: version "6.5.3" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" integrity sha1-y1nrLv2vc6C9eMzXAVpirW4Pk9Y= dependencies: bn.js "^4.4.0" @@ -11138,6 +11147,11 @@ inherits@2.0.1: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= +inherits@^2.0.4: + version "2.0.4" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w= + ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" @@ -11322,6 +11336,11 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-buffer@^1.0.2, is-buffer@^1.1.5: + version "1.1.6" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha1-76ouqdqg16suoTqXsritUf776L4= + is-buffer@^2.0.0, is-buffer@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" @@ -11575,7 +11594,7 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= -is-plain-object@^2.0.1, is-plain-object@^2.0.4: +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== @@ -12533,9 +12552,35 @@ killable@^1.0.1: resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== -kind-of@^2.0.1, kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0, kind-of@^4.0.0, kind-of@^5.0.0, kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: +kind-of@^2.0.1: + version "2.0.1" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" + integrity sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU= + dependencies: + is-buffer "^1.0.2" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha1-cpyR4thXt6QZofmqZWhcTDP1hF0= + +kind-of@^6.0.0, kind-of@^6.0.2: version "6.0.3" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha1-B8BQNKbDSfoG4k+jWqdttFgM5N0= kleur@^3.0.2: @@ -12948,9 +12993,9 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: +"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: version "4.17.20" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI= log-driver@^1.2.7: @@ -13389,6 +13434,11 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: dependencies: brace-expansion "^1.1.7" +minimist@0.0.8: + version "0.0.8" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" @@ -13469,9 +13519,16 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" -mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.2, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1: +mkdirp@0.5.1: + version "0.5.1" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1: version "0.5.5" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha1-2Rzv1i0UNsoPQWIOJRKI1CAJne8= dependencies: minimist "^1.2.5" @@ -16087,7 +16144,7 @@ read-text-file@^1.1.0: iconv-lite "^0.4.17" jschardet "^1.4.2" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -16892,10 +16949,10 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -selfsigned@1.10.8, selfsigned@^1.10.7: +selfsigned@^1.10.7: version "1.10.8" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30" - integrity sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w== + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30" + integrity sha1-DRcgi30Swz+OrIXEGDXyf8PYGjA= dependencies: node-forge "^0.10.0" @@ -16991,7 +17048,17 @@ serialize-error@^5.0.0: dependencies: type-fest "^0.8.0" -serialize-javascript@^1.7.0, serialize-javascript@^2.1.2, serialize-javascript@^3.1.0: +serialize-javascript@^1.7.0: + version "1.9.1" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb" + integrity sha1-z8IArvd7YAxH2pu4FJyUPnmML9s= + +serialize-javascript@^2.1.2: + version "2.1.2" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" + integrity sha1-7OxTsOAxe9yV73arcHS3OEeF+mE= + +serialize-javascript@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.1.0.tgz#8bf3a9170712664ef2561b44b691eafe399214ea" integrity sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg== @@ -17036,12 +17103,25 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= -set-value@^0.4.3, set-value@^2.0.0, set-value@^3.0.2: - version "3.0.2" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90" - integrity sha1-dOjs0CPDPQ93GZ1BVAmkDyHmG5A= +set-value@^0.4.3: + version "0.4.3" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" + integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE= dependencies: - is-plain-object "^2.0.4" + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.1" + to-object-path "^0.3.0" + +set-value@^2.0.0: + version "2.0.1" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha1-oY1AUw5vB95CKMfe/kInr4ytAFs= + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" @@ -17446,7 +17526,7 @@ split-on-first@^1.0.0: resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== -split-string@^3.0.2: +split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== From 021cfbf91f91d9b77598a7c78962bb961879554a Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Thu, 17 Sep 2020 11:19:36 +0800 Subject: [PATCH 062/105] update --- .../client/src/components/QnA/EditQnAFrom.tsx | 6 +-- .../src/pages/knowledge-base/table-view.tsx | 42 ++++++++----------- Composer/packages/client/src/utils/qnaUtil.ts | 12 +++++- .../lib/indexers/src/utils/qnaUtil.ts | 1 - 4 files changed, 31 insertions(+), 30 deletions(-) diff --git a/Composer/packages/client/src/components/QnA/EditQnAFrom.tsx b/Composer/packages/client/src/components/QnA/EditQnAFrom.tsx index 4722a00f46..35fafe8fad 100644 --- a/Composer/packages/client/src/components/QnA/EditQnAFrom.tsx +++ b/Composer/packages/client/src/components/QnA/EditQnAFrom.tsx @@ -6,7 +6,7 @@ import { jsx } from '@emotion/core'; import React from 'react'; import { QnAFile } from '@bfc/shared'; -import { getQnAFileUrlOption } from '../../utils/qnaUtil'; +import { isQnAFileCreatedFromUrl } from '../../utils/qnaUtil'; import EditQnAFromScratchModal, { EditQnAFromScratchFormData } from './EditQnAFromScratchModal'; import EditQnAFromUrlModal, { EditQnAFromUrlFormData } from './EditQnAFromUrlModal'; @@ -19,9 +19,7 @@ interface EditQnAModalProps { } export const EditQnAModal: React.FC = (props) => { - const url = getQnAFileUrlOption(props.qnaFile); - - if (url) { + if (isQnAFileCreatedFromUrl(props.qnaFile)) { return ; } else { return ; diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index fa01184c34..cf9291f5f8 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -4,7 +4,6 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import { useRecoilValue } from 'recoil'; -import inRange from 'lodash/inRange'; import React, { useEffect, useState, useCallback, Fragment } from 'react'; import { DetailsList, @@ -28,7 +27,7 @@ import isEmpty from 'lodash/isEmpty'; import { QnAFile } from '@bfc/shared/src/types'; import { QnASection } from '@bfc/shared'; import { qnaUtil } from '@bfc/indexers'; -import querystring from 'query-string'; +// import querystring from 'query-string'; import emptyQnAIcon from '../../images/emptyQnAIcon.svg'; import { navigateTo } from '../../utils/navigation'; @@ -44,6 +43,7 @@ import { getBaseName } from '../../utils/fileUtil'; import { EditableField } from '../../components/EditableField'; import { classNames, AddTemplateButton } from '../../components/AllupviewComponets/styles'; import { EditQnAModal } from '../../components/QnA/EditQnAFrom'; +import { isQnAFileCreatedFromUrl } from '../../utils/qnaUtil'; import { formCell, @@ -63,7 +63,6 @@ interface QnASectionItem extends QnASection { fileId: string; dialogId: string | undefined; used: boolean; - expand: boolean; } interface TableViewProps extends RouteComponentProps<{}> { @@ -94,8 +93,8 @@ const TableView: React.FC = (props) => { // const { languages, defaultLanguage } = settings; const { dialogId } = props; - const search = props.location?.search ?? ''; - const searchContainerId = querystring.parse(search).C; + // const search = props.location?.search ?? ''; + // const searchContainerId = querystring.parse(search).C; const activeDialog = dialogs.find(({ id }) => id === dialogId); const targetFileId = dialogId.endsWith('.source') ? dialogId : `${dialogId}.${locale}`; @@ -109,7 +108,6 @@ const TableView: React.FC = (props) => { fileId: file.id, dialogId: qnaDialog?.id, used: !!qnaDialog, - expand: false, key: qnaSection.sectionId, ...qnaSection, }; @@ -169,7 +167,6 @@ const TableView: React.FC = (props) => { setIsQnASectionsExpanded(newArray); } }; - const onUpdateQnAQuestion = (fileId: string, sectionId: string, questionId: string, content: string) => { if (!fileId) return; actions.setMessage('item deleted'); @@ -239,6 +236,7 @@ const TableView: React.FC = (props) => { const containerId = props?.group?.key || ''; const containerQnAFile = qnaFiles.find(({ id }) => id === containerId); const isImportedSource = containerId.endsWith('.source'); + const isSourceFromUrl = isImportedSource && isQnAFileCreatedFromUrl(containerQnAFile); const onRenderItem = (item: IOverflowSetItemProps): JSX.Element => { return ; @@ -260,10 +258,16 @@ const TableView: React.FC = (props) => {
{isImportedSource && ( - Source: - - {groupName} - + {isSourceFromUrl && ( + + Source: + + {groupName} + + + )} + {!isSourceFromUrl &&
{groupName}
} + = (props) => { return groups; } else { if (!qnaFile) return undefined; - const groups: IGroup[] = [ - { - key: qnaFile.id, - name: activeDialog?.displayName || dialogId, - startIndex: 0, - count: qnaFile.qnaSections.length, - level: 0, - // isCollapsed: !inRange(focusedIndex, 0, qnaFile.qnaSections.length), - }, - ]; + const groups: IGroup[] = []; importedSourceFiles.forEach((currentFile) => { const lastGroup = groups[groups.length - 1]; - const startIndex = lastGroup.startIndex + lastGroup.count; + const startIndex = lastGroup ? lastGroup.startIndex + lastGroup.count : 0; const { id, qnaSections } = currentFile; const count = qnaSections.length; - // const shouldExpand = inRange(focusedIndex, startIndex, startIndex + count) || searchContainerId === id; const name = getBaseName(id); groups.push({ key: id, name, startIndex, count, level: 0 }); }); - return groups; } }; @@ -628,6 +621,7 @@ const TableView: React.FC = (props) => {
); } + console.log(qnaSections); return (
diff --git a/Composer/packages/client/src/utils/qnaUtil.ts b/Composer/packages/client/src/utils/qnaUtil.ts index 8eb742c34d..51ca9bb5f2 100644 --- a/Composer/packages/client/src/utils/qnaUtil.ts +++ b/Composer/packages/client/src/utils/qnaUtil.ts @@ -45,10 +45,16 @@ export const reformQnAToContainerKB = (projectId: string, qnaFiles: QnAFile[]): const content2 = substringTextByLine(content, qnaSectionStartLine - 1); let file1 = qnaUtil.parse(id, content1); const file2Id = `${getBaseName(id)}.source`; - file1 = qnaUtil.addImport(file1, `${file2Id}.qna`); + const file2FullId = `${file2Id}.qna`; + + // if container file not be imported, do import + if (!file1.imports.find(({ id }) => file2FullId === id)) { + file1 = qnaUtil.addImport(file1, file2FullId); + } updateFile(projectId, `${file1.id}.qna`, file1.content); updatedFiles.push(file1); + // if container file not exist, create it. if exist, update it const existedFile2 = qnaFiles.find((item) => item.id === file2Id); if (existedFile2) { const file2Content = existedFile2.content + '\n' + content2; @@ -74,3 +80,7 @@ export const reformQnAToContainerKB = (projectId: string, qnaFiles: QnAFile[]): export const getQnAFileUrlOption = (file: QnAFile): string | undefined => { return file.options.find((opt) => opt.name === 'url')?.value; }; + +export const isQnAFileCreatedFromUrl = (file: QnAFile): boolean => { + return getQnAFileUrlOption(file) ? true : false; +}; diff --git a/Composer/packages/lib/indexers/src/utils/qnaUtil.ts b/Composer/packages/lib/indexers/src/utils/qnaUtil.ts index 38c35de039..3be6785194 100644 --- a/Composer/packages/lib/indexers/src/utils/qnaUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/qnaUtil.ts @@ -119,7 +119,6 @@ export function convertQnAParseResultToQnAFile(id = '', resource: LuParseResourc }; }); - // TODO: use regexp match "> !# @source.urls = https://download" const optionRegExp = new RegExp(/@source\.(\w+)\s*=\s*(.*)/); const options: { id: string; name: string; value: string }[] = []; Sections.filter(({ SectionType }) => SectionType === SectionTypes.LUModelInfo).forEach(({ ModelInfo, Id }) => { From f2ee9c5b58fae372de2e6aa67611738701cc0fc3 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Thu, 17 Sep 2020 17:38:08 +0800 Subject: [PATCH 063/105] update ux --- .../client/src/components/NavTree.tsx | 4 +- .../components/QnA/CreateQnAFromUrlModal.tsx | 8 +- .../client/src/components/QnA/constants.ts | 4 +- .../src/pages/knowledge-base/QnAPage.tsx | 3 + .../src/pages/knowledge-base/table-view.tsx | 118 ++++++++++++------ 5 files changed, 90 insertions(+), 47 deletions(-) diff --git a/Composer/packages/client/src/components/NavTree.tsx b/Composer/packages/client/src/components/NavTree.tsx index e852f63bf7..310425269f 100644 --- a/Composer/packages/client/src/components/NavTree.tsx +++ b/Composer/packages/client/src/components/NavTree.tsx @@ -11,6 +11,7 @@ import { OverflowSet, IOverflowSetItemProps } from 'office-ui-fabric-react/lib/O import { mergeStyleSets } from 'office-ui-fabric-react/lib/Styling'; import { NeutralColors } from '@uifabric/fluent-theme'; import { useRecoilValue } from 'recoil'; +import { IIconProps } from 'office-ui-fabric-react/lib/Icon'; import { navigateTo } from '../utils/navigation'; import { dispatcherState, userSettingsState } from '../recoilModel'; @@ -67,6 +68,7 @@ export interface INavTreeItem { ariaLabel?: string; url: string; menuItems?: IContextualMenuItem[]; + menuIconProps?: IIconProps; disabled?: boolean; } @@ -110,7 +112,7 @@ const NavTree: React.FC = (props) => { return ( { {formatMessage('Learn more about knowledge base sources. ')} - {formatMessage( - 'Skip this step to add questions and answers manually after creation. The number of sources and file size you can add depends on the QnA service SKU you choose. ' - )} - - {formatMessage('Learn more about QnA Maker SKUs.')} -

diff --git a/Composer/packages/client/src/components/QnA/constants.ts b/Composer/packages/client/src/components/QnA/constants.ts index 9d94d7faf5..f070d3c448 100644 --- a/Composer/packages/client/src/components/QnA/constants.ts +++ b/Composer/packages/client/src/components/QnA/constants.ts @@ -23,12 +23,12 @@ export const validateName = (sources: QnAFile[]): FieldValidator => { let currentError = ''; if (name) { if (!FileNameRegex.test(name)) { - currentError = formatMessage('Name contains invalid charactors'); + currentError = formatMessage('KB name cannot contain speacial characters.'); } const duplicatedItemIndex = sources.findIndex((item) => item.id.toLowerCase() === `${name.toLowerCase()}.source`); if (duplicatedItemIndex > -1) { - currentError = formatMessage('Duplicate imported QnA name'); + currentError = formatMessage('You already have a KB with that name.Choose another name and try again.'); } } return currentError; diff --git a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx index 20d640fb10..bfc3b9b1e0 100644 --- a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx @@ -65,6 +65,9 @@ const QnAPage: React.FC = (props) => { name: dialog.displayName, ariaLabel: formatMessage('qna file'), url: `/bot/${projectId}/knowledge-base/${dialog.id}`, + menuIconProps: { + iconName: 'Add', + }, menuItems: [ { name: formatMessage('Create KB from scratch'), diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index cf9291f5f8..40bb9ea60e 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -28,6 +28,7 @@ import { QnAFile } from '@bfc/shared/src/types'; import { QnASection } from '@bfc/shared'; import { qnaUtil } from '@bfc/indexers'; // import querystring from 'query-string'; +import { NeutralColors } from '@uifabric/fluent-theme'; import emptyQnAIcon from '../../images/emptyQnAIcon.svg'; import { navigateTo } from '../../utils/navigation'; @@ -63,6 +64,7 @@ interface QnASectionItem extends QnASection { fileId: string; dialogId: string | undefined; used: boolean; + usedIn: { id: string; displayName: string }[]; } interface TableViewProps extends RouteComponentProps<{}> { @@ -96,23 +98,37 @@ const TableView: React.FC = (props) => { // const search = props.location?.search ?? ''; // const searchContainerId = querystring.parse(search).C; - const activeDialog = dialogs.find(({ id }) => id === dialogId); + // const activeDialog = dialogs.find(({ id }) => id === dialogId); const targetFileId = dialogId.endsWith('.source') ? dialogId : `${dialogId}.${locale}`; const qnaFile = qnaFiles.find(({ id }) => id === targetFileId); const limitedNumber = 1; const generateQnASections = (file: QnAFile): QnASectionItem[] => { - return file - ? file.qnaSections.map((qnaSection) => { - const qnaDialog = dialogs.find((dialog) => file.id === `${dialog.id}.${locale}`); - return { - fileId: file.id, - dialogId: qnaDialog?.id, - used: !!qnaDialog, - key: qnaSection.sectionId, - ...qnaSection, - }; - }) - : []; + if (!file) return []; + const usedInDialog: any[] = []; + dialogs.forEach((dialog) => { + const dialogQnAFile = + qnaFiles.find(({ id }) => id === dialog.qnaFile) || + qnaFiles.find(({ id }) => id === `${dialog.qnaFile}.${locale}`); + if (dialogQnAFile) { + dialogQnAFile.imports.forEach(({ id }) => { + if (id === `${file.id}.qna`) { + usedInDialog.push({ id: dialog.id, displayName: dialog.displayName }); + } + }); + } + }); + + return file.qnaSections.map((qnaSection) => { + const qnaDialog = dialogs.find((dialog) => file.id === `${dialog.id}.${locale}`); + return { + fileId: file.id, + dialogId: qnaDialog?.id, + used: !!qnaDialog, + usedIn: usedInDialog, + key: qnaSection.sectionId, + ...qnaSection, + }; + }); }; const [editQnAFile, setEditQnAFile] = useState(undefined); const [qnaSections, setQnASections] = useState([]); @@ -236,10 +252,16 @@ const TableView: React.FC = (props) => { const containerId = props?.group?.key || ''; const containerQnAFile = qnaFiles.find(({ id }) => id === containerId); const isImportedSource = containerId.endsWith('.source'); - const isSourceFromUrl = isImportedSource && isQnAFileCreatedFromUrl(containerQnAFile); + const isSourceFromUrl = isImportedSource && containerQnAFile && isQnAFileCreatedFromUrl(containerQnAFile); const onRenderItem = (item: IOverflowSetItemProps): JSX.Element => { - return ; + return ( + + ); }; const onRenderOverflowButton = (overflowItems: any[] | undefined): JSX.Element => { @@ -248,6 +270,7 @@ const TableView: React.FC = (props) => { menuIconProps={{ iconName: 'More' }} menuProps={{ items: overflowItems || [] }} role="menuitem" + styles={{ root: { color: NeutralColors.black } }} title="More options" /> ); @@ -279,24 +302,28 @@ const TableView: React.FC = (props) => { }, }, ]} - overflowItems={[ - { - key: 'edit', - name: formatMessage('Show code'), - onClick: () => { - navigateTo(`/bot/${projectId}/knowledge-base/${dialogId}/edit?C=${containerId}`); + overflowItems={ + [ + { + key: 'edit', + name: formatMessage('Show code'), + iconProps: { iconName: 'CodeEdit' }, + onClick: () => { + navigateTo(`/bot/${projectId}/knowledge-base/${dialogId}/edit?C=${containerId}`); + }, }, - }, - { - key: 'delete', - name: formatMessage('Delete knowledge base'), - onClick: async () => { - if (!qnaFile) return; - await removeQnAImport({ id: qnaFile.id, sourceId: containerId }); - await removeQnAFile({ id: containerId }); + { + key: 'delete', + iconProps: { iconName: 'Delete' }, + name: formatMessage('Delete knowledge base'), + onClick: async () => { + if (!qnaFile) return; + await removeQnAImport({ id: qnaFile.id, sourceId: containerId }); + await removeQnAFile({ id: containerId }); + }, }, - }, - ]} + ] as IOverflowSetItemProps[] + } role="menubar" onRenderItem={onRenderItem} onRenderOverflowButton={onRenderOverflowButton} @@ -346,7 +373,7 @@ const TableView: React.FC = (props) => { toggleExpandRow(index)} /> @@ -446,14 +473,13 @@ const TableView: React.FC = (props) => { name: formatMessage('Answer'), fieldName: 'answer', minWidth: 350, - maxWidth: 550, isResizable: true, data: 'string', onRender: (item, index) => { const isSourceSectionInDialog = item.fileId.endsWith('.source') && !dialogId.endsWith('.source'); const isAllowEdit = dialogId !== 'all' && !isSourceSectionInDialog; return ( -
+
= (props) => { ); }, }, + { + key: 'UsedIn', + name: formatMessage('Used In'), + fieldName: 'UsedIn', + minWidth: 100, + maxWidth: 100, + isResizable: false, + data: 'string', + onRender: (item) => { + return ( +
+ {item.usedIn.map(({ id, displayName }) => { + return {displayName}; + })} +
+ ); + }, + }, ]; if (dialogId !== 'all') { const extraOperations = { @@ -499,7 +543,7 @@ const TableView: React.FC = (props) => { { onRemoveQnAPairs(item.fileId, item.sectionId); @@ -508,7 +552,7 @@ const TableView: React.FC = (props) => { ); }, }; - tableColums.splice(3, 0, extraOperations); + tableColums.push(extraOperations); } // all view, show used in column @@ -524,7 +568,7 @@ const TableView: React.FC = (props) => { data: 'string', onRender: (item) => { return ( -
+
{item.dialogId}
From 533fc12574999c5c58104b8c176e4ff2c8a53a8d Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Fri, 18 Sep 2020 13:40:02 +0800 Subject: [PATCH 064/105] creation back --- .../src/components/QnA/CreateQnAFrom.tsx | 33 +++++++ .../QnA/CreateQnAFromScratchModal.tsx | 43 +++++---- .../components/QnA/CreateQnAFromUrlModal.tsx | 50 +++++----- .../client/src/components/QnA/constants.ts | 23 +++++ .../client/src/components/QnA/index.ts | 4 +- .../client/src/pages/design/DesignPage.tsx | 24 +---- .../src/pages/knowledge-base/QnAPage.tsx | 58 ++++------- .../src/pages/knowledge-base/table-view.tsx | 95 ++++++++++++------- .../client/src/recoilModel/dispatchers/qna.ts | 69 ++++++++++---- 9 files changed, 238 insertions(+), 161 deletions(-) create mode 100644 Composer/packages/client/src/components/QnA/CreateQnAFrom.tsx diff --git a/Composer/packages/client/src/components/QnA/CreateQnAFrom.tsx b/Composer/packages/client/src/components/QnA/CreateQnAFrom.tsx new file mode 100644 index 0000000000..664abe47dd --- /dev/null +++ b/Composer/packages/client/src/components/QnA/CreateQnAFrom.tsx @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import React from 'react'; +import { useRecoilValue } from 'recoil'; + +import { + dispatcherState, + showCreateQnAFromScratchDialogState, + showCreateQnAFromUrlDialogState, +} from '../../recoilModel'; + +import CreateQnAFromScratchModal from './CreateQnAFromScratchModal'; +import CreateQnAFromUrlModal from './CreateQnAFromUrlModal'; +import { CreateQnAFromModalProps } from './constants'; + +export const CreateQnAModal: React.FC = (props) => { + const { createQnAFromScratchDialogBegin } = useRecoilValue(dispatcherState); + const showCreateQnAFromScratchDialog = useRecoilValue(showCreateQnAFromScratchDialogState); + const showCreateQnAFromUrlDialog = useRecoilValue(showCreateQnAFromUrlDialogState); + + if (showCreateQnAFromScratchDialog) { + return ; + } else if (showCreateQnAFromUrlDialog) { + return ; + } else { + return null; + } +}; + +export default CreateQnAModal; diff --git a/Composer/packages/client/src/components/QnA/CreateQnAFromScratchModal.tsx b/Composer/packages/client/src/components/QnA/CreateQnAFromScratchModal.tsx index 98b1bdf1c9..4bfdc90230 100644 --- a/Composer/packages/client/src/components/QnA/CreateQnAFromScratchModal.tsx +++ b/Composer/packages/client/src/components/QnA/CreateQnAFromScratchModal.tsx @@ -4,34 +4,19 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import React from 'react'; +import { useRecoilValue } from 'recoil'; import formatMessage from 'format-message'; import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; import { Stack } from 'office-ui-fabric-react/lib/Stack'; import { TextField } from 'office-ui-fabric-react/lib/TextField'; import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; -import { RouteComponentProps } from '@reach/router'; -import { QnAFile } from '@bfc/shared'; import { FieldConfig, useForm } from '../../hooks/useForm'; +import { dispatcherState, showCreateQnAFromUrlDialogState } from '../../recoilModel'; -import { validateName } from './constants'; +import { validateName, CreateQnAFromModalProps, CreateQnAFromScratchFormData } from './constants'; import { subText, styles, dialogWindow, textField } from './styles'; -interface CreateQnAFromScratchModalProps - extends RouteComponentProps<{ - location: string; - }> { - dialogId: string; - qnaFiles: QnAFile[]; - subscriptionKey?: string; - onDismiss: () => void; - onSubmit: (formData: CreateQnAFromScratchFormData) => void; -} - -export interface CreateQnAFromScratchFormData { - name: string; -} - const formConfig: FieldConfig = { name: { required: true, @@ -50,8 +35,10 @@ const DialogTitle = () => { ); }; -export const CreateQnAFromScratchModal: React.FC = (props) => { - const { onDismiss, onSubmit, qnaFiles } = props; +export const CreateQnAFromScratchModal: React.FC = (props) => { + const { onDismiss, onSubmit, qnaFiles, dialogId } = props; + const actions = useRecoilValue(dispatcherState); + const showCreateQnAFromUrlDialog = useRecoilValue(showCreateQnAFromUrlDialogState); formConfig.name.validate = validateName(qnaFiles); const { formData, updateField, hasErrors, formErrors } = useForm(formConfig); @@ -89,7 +76,21 @@ export const CreateQnAFromScratchModal: React.FC
- + {showCreateQnAFromUrlDialog && ( + { + actions.createQnAFromScratchDialogCancel(dialogId); + }} + /> + )} + { + actions.createQnAFromScratchDialogCancel(); + onDismiss && onDismiss(); + }} + /> { - dialogId: string; - qnaFiles: QnAFile[]; - subscriptionKey?: string; - onDismiss: () => void; - onSubmit: (formData: CreateQnAFromUrlFormData) => void; -} - -export interface CreateQnAFromUrlFormData { - url: string; - name: string; - multiTurn: boolean; -} - const formConfig: FieldConfig = { url: { required: true, @@ -69,8 +63,9 @@ const DialogTitle = () => { ); }; -export const CreateQnAFromUrlModal: React.FC = (props) => { +export const CreateQnAFromUrlModal: React.FC = (props) => { const { onDismiss, onSubmit, dialogId, qnaFiles } = props; + const actions = useRecoilValue(dispatcherState); formConfig.name.validate = validateName(qnaFiles); formConfig.url.validate = validateUrl; @@ -143,14 +138,17 @@ export const CreateQnAFromUrlModal: React.FC = (prop styles={{ root: { float: 'left' } }} text={formatMessage('Create knowledge base from scratch')} onClick={() => { - if (hasErrors) { - return; - } - onSubmit({} as CreateQnAFromUrlFormData); + actions.createQnAFromScratchDialogBegin({ dialogId }); }} /> )} - + { + actions.createQnAFromUrlDialogCancel(); + onDismiss && onDismiss(); + }} + /> void; + onSubmit: (formData: CreateQnAFormData) => void; +} + export const validateUrl: FieldValidator = (url: string): string => { let error = ''; diff --git a/Composer/packages/client/src/components/QnA/index.ts b/Composer/packages/client/src/components/QnA/index.ts index 4f7785f7c1..90507f314d 100644 --- a/Composer/packages/client/src/components/QnA/index.ts +++ b/Composer/packages/client/src/components/QnA/index.ts @@ -3,4 +3,6 @@ export * from './CreateQnAFromScratchModal'; export * from './CreateQnAFromUrlModal'; -export * from './EditQnAFromScratchModal'; +export * from './CreateQnAFrom'; +export * from './EditQnAFrom'; +export * from './constants'; diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index d4673ff7ab..5aa2eba614 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -54,7 +54,7 @@ import { getBaseName } from '../../utils/fileUtil'; import { validatedDialogsSelector } from '../../recoilModel/selectors/validatedDialogs'; import plugins, { mergePluginConfigs } from '../../plugins'; import { useElectronFeatures } from '../../hooks/useElectronFeatures'; -import CreateQnAFromUrlModal from '../../components/QnA/CreateQnAFromUrlModal'; +import { CreateQnAModal } from '../../components/QnA'; import { triggerNotSupported } from '../../utils/dialogValidator'; import { WarningMessage } from './WarningMessage'; @@ -144,6 +144,7 @@ const DesignPage: React.FC(dialogs[0]); const [exportSkillModalVisible, setExportSkillModalVisible] = useState(false); const [warningIsVisible, setWarningIsVisible] = useState(true); @@ -243,10 +243,6 @@ const DesignPage: React.FC { - setImportQnAModalVisibility(true); - }; - const onTriggerCreationDismiss = () => { setTriggerModalVisibility(false); }; @@ -324,7 +320,7 @@ const DesignPage: React.FC { - openImportQnAModal(); + createQnAFromUrlDialogBegin({}); }, }, ], @@ -569,12 +565,7 @@ const DesignPage: React.FC { - setImportQnAModalVisibility(false); - }; - const handleCreateQnA = async ({ name, url, multiTurn }) => { - cancelImportQnAModal(); const formData = { $kind: qnaMatcherKey, errors: { $kind: '', intent: '', event: '', triggerPhrases: '', regEx: '', activity: '' }, @@ -699,14 +690,7 @@ const DesignPage: React.FC )} - {importQnAModalVisibility && ( - - )} + ) {displaySkillManifest && ( )} diff --git a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx index bfc3b9b1e0..bb7292da2b 100644 --- a/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/QnAPage.tsx @@ -15,22 +15,10 @@ import { navigateTo } from '../../utils/navigation'; import { TestController } from '../../components/TestController/TestController'; import { INavTreeItem } from '../../components/NavTree'; import { Page } from '../../components/Page'; -import { - dialogsState, - projectIdState, - qnaAllUpViewStatusState, - qnaFilesState, - showCreateQnAFromScratchDialogState, - showCreateQnAFromUrlDialogState, -} from '../../recoilModel/atoms/botState'; +import { dialogsState, projectIdState, qnaAllUpViewStatusState, qnaFilesState } from '../../recoilModel/atoms/botState'; import { dispatcherState } from '../../recoilModel'; import { QnAAllUpViewStatus } from '../../recoilModel/types'; -import { - CreateQnAFromUrlModal, - CreateQnAFromUrlFormData, - CreateQnAFromScratchModal, - CreateQnAFromScratchFormData, -} from '../../components/QnA'; +import { CreateQnAModal } from '../../components/QnA'; import TableView from './table-view'; @@ -51,9 +39,6 @@ const QnAPage: React.FC = (props) => { const qnaAllUpViewStatus = useRecoilValue(qnaAllUpViewStatusState); const [createOnDialogId, setCreateOnDialogId] = useState(''); - const createQnAModalFromUrlVisiability = useRecoilValue(showCreateQnAFromUrlDialogState); - const createQnAModalFromScratchVisiability = useRecoilValue(showCreateQnAFromScratchDialogState); - const path = props.location?.pathname ?? ''; const { dialogId = '' } = props; const edit = /\/edit(\/)?$/.test(path); @@ -74,7 +59,7 @@ const QnAPage: React.FC = (props) => { key: 'Create KB from scratch', onClick: () => { setCreateOnDialogId(dialog.id); - actions.createQnAFromScratchDialogBegin(() => undefined); + actions.createQnAFromScratchDialogBegin({ dialogId: dialog.id }); }, }, { @@ -82,7 +67,7 @@ const QnAPage: React.FC = (props) => { key: 'Create KB from URL or file', onClick: () => { setCreateOnDialogId(dialog.id); - actions.createQnAFromUrlDialogBegin(() => {}); + actions.createQnAFromUrlDialogBegin({ dialogId: dialog.id }); }, }, ], @@ -162,36 +147,25 @@ const QnAPage: React.FC = (props) => { {qnaAllUpViewStatus === QnAAllUpViewStatus.Loading && ( )} - {createQnAModalFromUrlVisiability && ( - { - actions.createQnAFromUrlDialogCancel(); - }} - onSubmit={async ({ name, url, multiTurn }: CreateQnAFromUrlFormData) => { + { + actions.createQnAFromUrlDialogCancel(); + }} + onSubmit={async ({ name, url, multiTurn = false }) => { + if (url) { await actions.createQnAKBFromUrl({ id: `${createOnDialogId || dialogId}.${locale}`, name, url, multiTurn, }); - }} - /> - )} - - {createQnAModalFromScratchVisiability && ( - { - actions.createQnAFromScratchDialogCancel(); - }} - onSubmit={async ({ name }: CreateQnAFromScratchFormData) => { + } else { await actions.createQnAKBFromScratch({ id: `${createOnDialogId || dialogId}.${locale}`, name }); - }} - /> - )} + } + }} + > ); diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index 40bb9ea60e..ebffb93099 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -4,7 +4,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import { useRecoilValue } from 'recoil'; -import React, { useEffect, useState, useCallback, Fragment } from 'react'; +import React, { useEffect, useState, useCallback, Fragment, useRef } from 'react'; import { DetailsList, DetailsRow, @@ -13,6 +13,7 @@ import { CheckboxVisibility, IDetailsGroupRenderProps, IGroup, + IDetailsList, } from 'office-ui-fabric-react/lib/DetailsList'; import { GroupHeader, CollapseAllVisibility } from 'office-ui-fabric-react/lib/GroupedList'; import { IOverflowSetItemProps, OverflowSet } from 'office-ui-fabric-react/lib/OverflowSet'; @@ -23,11 +24,13 @@ import { ScrollablePane, ScrollbarVisibility } from 'office-ui-fabric-react/lib/ import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky'; import formatMessage from 'format-message'; import { RouteComponentProps } from '@reach/router'; +import isEqual from 'lodash/isEqual'; import isEmpty from 'lodash/isEmpty'; +// import inRange from 'lodash/inRange'; import { QnAFile } from '@bfc/shared/src/types'; import { QnASection } from '@bfc/shared'; import { qnaUtil } from '@bfc/indexers'; -// import querystring from 'query-string'; +import querystring from 'query-string'; import { NeutralColors } from '@uifabric/fluent-theme'; import emptyQnAIcon from '../../images/emptyQnAIcon.svg'; @@ -95,8 +98,8 @@ const TableView: React.FC = (props) => { // const { languages, defaultLanguage } = settings; const { dialogId } = props; - // const search = props.location?.search ?? ''; - // const searchContainerId = querystring.parse(search).C; + const search = props.location?.search ?? ''; + const searchContainerId = querystring.parse(search).C; // const activeDialog = dialogs.find(({ id }) => id === dialogId); const targetFileId = dialogId.endsWith('.source') ? dialogId : `${dialogId}.${locale}`; @@ -130,6 +133,7 @@ const TableView: React.FC = (props) => { }; }); }; + const detailListRef = useRef(null); const [editQnAFile, setEditQnAFile] = useState(undefined); const [qnaSections, setQnASections] = useState([]); const [kthSectionIsCreatingQuestion, setCreatingQuestionInKthSection] = useState(-1); @@ -581,39 +585,60 @@ const TableView: React.FC = (props) => { return tableColums; }; + const [groups, setGroups] = useState(undefined); const getGroups = (): IGroup[] | undefined => { + let currentFiles = importedSourceFiles; if (dialogId === 'all') { - const groups: IGroup[] = []; - qnaFiles.forEach((currentFile) => { - const lastGroup = groups[groups.length - 1]; - const startIndex = lastGroup ? lastGroup.startIndex + lastGroup.count : 0; - const { id, qnaSections } = currentFile; - const count = qnaSections.length; - // const shouldExpand = inRange(focusedIndex, startIndex, startIndex + count) || searchContainerId === id; - groups.push({ - key: id, - name: getBaseName(id), - startIndex, - count, - level: 0, - // isCollapsed: !shouldExpand, - }); - }); - return groups; + currentFiles = qnaFiles; } else { if (!qnaFile) return undefined; - const groups: IGroup[] = []; - importedSourceFiles.forEach((currentFile) => { - const lastGroup = groups[groups.length - 1]; - const startIndex = lastGroup ? lastGroup.startIndex + lastGroup.count : 0; - const { id, qnaSections } = currentFile; - const count = qnaSections.length; - const name = getBaseName(id); - groups.push({ key: id, name, startIndex, count, level: 0 }); - }); - return groups; } + + const newGroups: IGroup[] = []; + currentFiles.forEach((currentFile) => { + const lastGroup = newGroups[newGroups.length - 1]; + const startIndex = lastGroup ? lastGroup.startIndex + lastGroup.count : 0; + const { id, qnaSections } = currentFile; + const count = qnaSections.length; + const name = getBaseName(id); + + // restore last group collapse state + const prevGroup = groups?.find(({ key }) => key === id); + const newGroup = prevGroup || {}; + newGroups.push({ ...newGroup, key: id, name, startIndex, count, level: 0 }); + }); + return newGroups; }; + useEffect(() => { + const newGroups = getGroups(); + const isChanged = !isEqual(groups, newGroups); + if (isChanged) setGroups(newGroups); + }, [dialogId, qnaFiles]); + + useEffect(() => { + // set focus to target container. + console.log(searchContainerId, groups); + if (searchContainerId && groups && detailListRef.current) { + const targetGroup = groups.find(({ key }) => key === searchContainerId); + if (targetGroup) { + detailListRef.current.focusIndex(targetGroup.startIndex); + } + // if (targetGroup) { + // const newGroups = groups.map((item) => { + // if (item.key === targetGroup.key) { + // return { + // ...item, + // isCollapsed: false, + // }; + // } + // return item; + // }); + // const isChanged = !isEqual(groups, newGroups); + // if (isChanged) setGroups(newGroups); + + // } + } + }, [searchContainerId, groups]); const onRenderDetailsHeader = useCallback( (props, defaultRender) => { @@ -658,14 +683,13 @@ const TableView: React.FC = (props) => { data-testid={'createKnowledgeBase'} text={formatMessage('Create new KB')} onClick={() => { - actions.createQnAFromScratchDialogBegin(() => undefined); + actions.createQnAFromScratchDialogBegin({}); }} />
); } - console.log(qnaSections); return (
@@ -673,12 +697,13 @@ const TableView: React.FC = (props) => { { - const createQnAFromUrlDialogBegin = useRecoilCallback(({ set }: CallbackInterface) => (onComplete) => { - set(showCreateQnAFromUrlDialogState, true); - set(onCreateQnAFromUrlDialogCompleteState, { func: onComplete }); - }); + const createQnAFromUrlDialogBegin = useRecoilCallback( + ({ set, snapshot }: CallbackInterface) => async ({ + dialogId, + onComplete, + }: { + dialogId?: string; + onComplete?: Function; + }) => { + if (dialogId) { + const projectId = await snapshot.getPromise(projectIdState); + navigateTo(`/bot/${projectId}/knowledge-base/${dialogId}`); + } + set(showCreateQnAFromUrlDialogState, true); + set(onCreateQnAFromUrlDialogCompleteState, { func: onComplete }); + } + ); const createQnAFromUrlDialogCancel = useRecoilCallback(({ set }: CallbackInterface) => () => { set(showCreateQnAFromUrlDialogState, false); set(onCreateQnAFromUrlDialogCompleteState, { func: undefined }); }); - const createQnAFromScratchDialogBegin = useRecoilCallback(({ set }: CallbackInterface) => (onComplete) => { - set(showCreateQnAFromScratchDialogState, true); - set(onCreateQnAFromScratchDialogCompleteState, { func: onComplete }); - }); + const createQnAFromScratchDialogBegin = useRecoilCallback( + ({ set, snapshot }: CallbackInterface) => async ({ + dialogId, + onComplete, + }: { + dialogId?: string; + onComplete?: Function; + }) => { + // navigate to all up view scratch. + if (dialogId) { + const projectId = await snapshot.getPromise(projectIdState); + navigateTo(`/bot/${projectId}/knowledge-base/${dialogId}`); + } - const createQnAFromScratchDialogCancel = useRecoilCallback(({ set }: CallbackInterface) => () => { - set(showCreateQnAFromScratchDialogState, false); - set(onCreateQnAFromScratchDialogCompleteState, { func: undefined }); - }); + set(showCreateQnAFromScratchDialogState, true); + set(onCreateQnAFromScratchDialogCompleteState, { func: onComplete }); + } + ); + + const createQnAFromScratchDialogCancel = useRecoilCallback( + ({ set, snapshot }: CallbackInterface) => async (dialogId?: string) => { + // navigate back to design page. if click `Back` + if (dialogId) { + const projectId = await snapshot.getPromise(projectIdState); + navigateTo(`/bot/${projectId}/dialogs/${dialogId}`); + } + set(showCreateQnAFromScratchDialogState, false); + set(onCreateQnAFromScratchDialogCompleteState, { func: undefined }); + } + ); const createQnAFromUrlDialogSuccess = useRecoilCallback(({ set, snapshot }: CallbackInterface) => async () => { const onCreateQnAFromUrlDialogComplete = (await snapshot.getPromise(onCreateQnAFromUrlDialogCompleteState)).func; @@ -250,6 +283,11 @@ export const qnaDispatcher = () => { await removeQnAFileState(callbackHelpers, { id }); }); + const dismissCreateQnAModal = useRecoilCallback(({ set }: CallbackInterface) => async () => { + set(showCreateQnAFromUrlDialogState, false); + set(showCreateQnAFromScratchDialogState, false); + }); + const createQnAKBFromUrl = useRecoilCallback( (callbackHelpers: CallbackInterface) => async ({ id, @@ -263,9 +301,8 @@ export const qnaDispatcher = () => { multiTurn: boolean; }) => { const { set } = callbackHelpers; - + await dismissCreateQnAModal(); set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Loading); - let response; try { response = await httpClient.get(`/utilities/qna/parse`, { @@ -301,9 +338,9 @@ ${response.data} id: string; // dialogId.locale name: string; }) => { - const projectId = await callbackHelpers.snapshot.getPromise(projectIdState); - // const dialogs = await callbackHelpers.snapshot.getPromise(dialogsState); + await dismissCreateQnAModal(); + const projectId = await callbackHelpers.snapshot.getPromise(projectIdState); const content = qnaUtil.generateQnAPair(); await createKBFileState(callbackHelpers, { id, From b9c50d512ae155f5b043424dd241841110724682 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Fri, 18 Sep 2020 15:40:44 +0800 Subject: [PATCH 065/105] update --- .../components/CreationFlow/CreationFlow.tsx | 4 +- .../client/src/components/EditableField.tsx | 2 +- .../src/components/QnA/CreateQnAFrom.tsx | 7 +-- .../components/QnA/CreateQnAFromUrlModal.tsx | 7 +-- .../client/src/pages/design/DesignPage.tsx | 6 +-- .../src/pages/knowledge-base/table-view.tsx | 50 ++++--------------- .../client/src/recoilModel/dispatchers/qna.ts | 2 +- 7 files changed, 20 insertions(+), 58 deletions(-) diff --git a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx index a4dd663f11..58d5511930 100644 --- a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx +++ b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx @@ -121,9 +121,10 @@ const CreationFlow: React.FC = () => { saveProjectAs(projectId, formData.name, formData.description, formData.location); }; - const handleCreateQnA = async ({ name, url, multiTurn }) => { + const handleCreateQnA = async (data) => { saveTemplateId(QnABotTemplateId); handleDismiss(); + const { name, url, multiTurn } = data; await handleCreateNew(formData, QnABotTemplateId); // import qna from url await createQnAKBFromUrl({ id: `${formData.name.toLocaleLowerCase()}.${locale}`, name, url, multiTurn }); @@ -188,7 +189,6 @@ const CreationFlow: React.FC = () => { /> = (props) => { return (
= (props) => { - const { createQnAFromScratchDialogBegin } = useRecoilValue(dispatcherState); const showCreateQnAFromScratchDialog = useRecoilValue(showCreateQnAFromScratchDialogState); const showCreateQnAFromUrlDialog = useRecoilValue(showCreateQnAFromUrlDialogState); diff --git a/Composer/packages/client/src/components/QnA/CreateQnAFromUrlModal.tsx b/Composer/packages/client/src/components/QnA/CreateQnAFromUrlModal.tsx index 87097e051f..222764bbcc 100644 --- a/Composer/packages/client/src/components/QnA/CreateQnAFromUrlModal.tsx +++ b/Composer/packages/client/src/components/QnA/CreateQnAFromUrlModal.tsx @@ -14,12 +14,7 @@ import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button' import { Link } from 'office-ui-fabric-react/lib/Link'; import { FieldConfig, useForm } from '../../hooks/useForm'; -import { - actionsSeedState, - dispatcherState, - showCreateQnAFromScratchDialogState, - showCreateQnAFromUrlDialogState, -} from '../../recoilModel'; +import { dispatcherState } from '../../recoilModel'; import { knowledgeBaseSourceUrl, diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 5aa2eba614..342ef0f50a 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -565,7 +565,7 @@ const DesignPage: React.FC { + const handleCreateQnA = async (data) => { const formData = { $kind: qnaMatcherKey, errors: { $kind: '', intent: '', event: '', triggerPhrases: '', regEx: '', activity: '' }, @@ -575,8 +575,8 @@ const DesignPage: React.FC = (props) => { const locale = useRecoilValue(localeState); //const settings = useRecoilValue(settingsState); const { - // createQnAImport, removeQnAImport, removeQnAFile, - // setMessage, createQnAPairs, removeQnAPairs, createQnAQuestion, - // removeQnAQuestion, updateQnAAnswer, updateQnAQuestion, - // updateQnAFile, } = useRecoilValue(dispatcherState); - // const { languages, defaultLanguage } = settings; - const { dialogId } = props; - const search = props.location?.search ?? ''; - const searchContainerId = querystring.parse(search).C; - - // const activeDialog = dialogs.find(({ id }) => id === dialogId); + // const { languages, defaultLanguage } = settings; const targetFileId = dialogId.endsWith('.source') ? dialogId : `${dialogId}.${locale}`; const qnaFile = qnaFiles.find(({ id }) => id === targetFileId); const limitedNumber = 1; @@ -604,8 +593,16 @@ const TableView: React.FC = (props) => { // restore last group collapse state const prevGroup = groups?.find(({ key }) => key === id); - const newGroup = prevGroup || {}; - newGroups.push({ ...newGroup, key: id, name, startIndex, count, level: 0 }); + const newGroup = prevGroup || { isCollapsed: true }; + newGroups.push({ + ...newGroup, + key: id, + name, + startIndex, + count, + level: 0, + // isCollapsed: prevGroup.isCollapsed !== undefined ? prevGroup.isCollapsed : true, + }); }); return newGroups; }; @@ -615,31 +612,6 @@ const TableView: React.FC = (props) => { if (isChanged) setGroups(newGroups); }, [dialogId, qnaFiles]); - useEffect(() => { - // set focus to target container. - console.log(searchContainerId, groups); - if (searchContainerId && groups && detailListRef.current) { - const targetGroup = groups.find(({ key }) => key === searchContainerId); - if (targetGroup) { - detailListRef.current.focusIndex(targetGroup.startIndex); - } - // if (targetGroup) { - // const newGroups = groups.map((item) => { - // if (item.key === targetGroup.key) { - // return { - // ...item, - // isCollapsed: false, - // }; - // } - // return item; - // }); - // const isChanged = !isEqual(groups, newGroups); - // if (isChanged) setGroups(newGroups); - - // } - } - }, [searchContainerId, groups]); - const onRenderDetailsHeader = useCallback( (props, defaultRender) => { return ( diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index f6aee8c803..71a8437c41 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -134,7 +134,7 @@ export const createKBFileState = async ( } qnaFileStatusStorage.updateFileStatus(projectId, createdSourceQnAId); - set(qnaFilesState, [...newQnAFiles, createdQnAFile]); + set(qnaFilesState, [createdQnAFile, ...newQnAFiles]); }; export const removeKBFileState = async (callbackHelpers: CallbackInterface, { id }: { id: string }) => { From ff7a31156228235e79e08c3f455409104b26b30a Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Fri, 18 Sep 2020 17:14:13 +0800 Subject: [PATCH 066/105] fix create qna bot --- .../components/CreationFlow/CreationFlow.tsx | 39 ++----------------- .../src/recoilModel/dispatchers/project.ts | 14 ++++--- .../client/src/recoilModel/dispatchers/qna.ts | 2 +- 3 files changed, 14 insertions(+), 41 deletions(-) diff --git a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx index 58d5511930..f6e27bef21 100644 --- a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx +++ b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx @@ -4,7 +4,7 @@ // TODO: Remove path module import Path from 'path'; -import React, { useEffect, useRef, Fragment, useState } from 'react'; +import React, { useEffect, useRef, Fragment } from 'react'; import { RouteComponentProps, Router, navigate } from '@reach/router'; import { useRecoilValue } from 'recoil'; @@ -17,13 +17,9 @@ import { storagesState, focusedStorageFolderState, userSettingsState, - localeState, - qnaFilesState, } from '../../recoilModel'; import Home from '../../pages/home/Home'; -import { QnABotTemplateId } from '../../constants'; import { useProjectIdCache } from '../../utils/hooks'; -import CreateQnAFromUrlModal from '../QnA/CreateQnAFromUrlModal'; import { CreateOptions } from './CreateOptions'; import { OpenProject } from './OpenProject'; @@ -44,7 +40,6 @@ const CreationFlow: React.FC = () => { updateCurrentPathForStorage, updateFolder, saveTemplateId, - createQnAKBFromUrl, fetchProjectById, fetchRecentProjects, } = useRecoilValue(dispatcherState); @@ -54,13 +49,11 @@ const CreationFlow: React.FC = () => { const storages = useRecoilValue(storagesState); const focusedStorageFolder = useRecoilValue(focusedStorageFolderState); const { appLocale } = useRecoilValue(userSettingsState); - const locale = useRecoilValue(localeState); - const qnaFiles = useRecoilValue(qnaFilesState); + const cachedProjectId = useProjectIdCache(); const currentStorageIndex = useRef(0); const storage = storages[currentStorageIndex.current]; const currentStorageId = storage ? storage.id : 'default'; - const [formData, setFormData] = useState({ name: '' }); useEffect(() => { if (storages && storages.length) { @@ -121,24 +114,6 @@ const CreationFlow: React.FC = () => { saveProjectAs(projectId, formData.name, formData.description, formData.location); }; - const handleCreateQnA = async (data) => { - saveTemplateId(QnABotTemplateId); - handleDismiss(); - const { name, url, multiTurn } = data; - await handleCreateNew(formData, QnABotTemplateId); - // import qna from url - await createQnAKBFromUrl({ id: `${formData.name.toLocaleLowerCase()}.${locale}`, name, url, multiTurn }); - }; - - const handleSubmitOrImportQnA = async (formData, templateId: string) => { - if (templateId === 'QnASample') { - setFormData(formData); - navigate(`./QnASample/importQnA`); - return; - } - handleSubmit(formData, templateId); - }; - const handleSubmit = async (formData, templateId: string) => { handleDismiss(); switch (creationFlowStatus) { @@ -168,7 +143,7 @@ const CreationFlow: React.FC = () => { updateFolder={updateFolder} onCurrentPathUpdate={updateCurrentPath} onDismiss={handleDismiss} - onSubmit={handleSubmitOrImportQnA} + onSubmit={handleSubmit} /> = () => { updateFolder={updateFolder} onCurrentPathUpdate={updateCurrentPath} onDismiss={handleDismiss} - onSubmit={handleSubmitOrImportQnA} + onSubmit={handleSubmit} /> = () => { onDismiss={handleDismiss} onOpen={openBot} /> - ); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index ad1fd910c0..f509edb1df 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -31,7 +31,7 @@ import filePersistence from '../persistence/FilePersistence'; import { navigateTo } from '../../utils/navigation'; import languageStorage from '../../utils/languageStorage'; import { projectIdCache } from '../../utils/projectCache'; -import { designPageLocationState } from '../atoms/botState'; +import { designPageLocationState, showCreateQnAFromUrlDialogState } from '../atoms/botState'; import { QnABotTemplateId } from '../../constants'; import { @@ -194,6 +194,10 @@ export const projectDispatcher = () => { set(botStatusState, BotStatus.unConnected); set(locationState, location); } + // if create from QnATemplate, continue creation flow. + if (templateId === QnABotTemplateId) { + set(showCreateQnAFromUrlDialogState, true); + } set(skillsState, skills); set(schemasState, schemas); set(localeState, locale); @@ -214,10 +218,10 @@ export const projectDispatcher = () => { }); gotoSnapshot(newSnapshot); if (jump && projectId) { - let url = `/bot/${projectId}/dialogs/${mainDialog}`; - if (templateId === QnABotTemplateId) { - url = `/bot/${projectId}/knowledge-base/${mainDialog}`; - } + const url = `/bot/${projectId}/dialogs/${mainDialog}`; + // if (templateId === QnABotTemplateId) { + // url = `/bot/${projectId}/knowledge-base/${mainDialog}`; + // } navigateTo(url); } } catch (err) { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index 71a8437c41..ad26c964d9 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -349,7 +349,7 @@ ${response.data} }); await createQnAFromScratchDialogSuccess(); - navigateTo(`/bot/${projectId}/knowledge-base/${getBaseName(id)}?C=${encodeURIComponent(name)}.source`); + navigateTo(`/bot/${projectId}/knowledge-base/${getBaseName(id)}`); } ); From 87e0e402b55fa2489d58406dd0ed39e47a3c39df Mon Sep 17 00:00:00 2001 From: liweitian Date: Fri, 18 Sep 2020 19:15:38 +0800 Subject: [PATCH 067/105] update css --- Composer/packages/client/src/components/EditableField.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Composer/packages/client/src/components/EditableField.tsx b/Composer/packages/client/src/components/EditableField.tsx index 13e64be0a5..7b1dac18c0 100644 --- a/Composer/packages/client/src/components/EditableField.tsx +++ b/Composer/packages/client/src/components/EditableField.tsx @@ -9,10 +9,11 @@ import { mergeStyleSets } from '@uifabric/styling'; import { IconButton } from 'office-ui-fabric-react/lib/Button'; import { IIconProps } from 'office-ui-fabric-react/lib/Icon'; //------------------------ -const defaultContainerStyle = (isHovered) => css` +const defaultContainerStyle = (hasFocus) => css` display: flex; width: 100%; - outline: ${isHovered ? `2px solid ${SharedColors.cyanBlue10}` : undefined}; + outline: ${hasFocus ? `2px solid ${SharedColors.cyanBlue10}` : undefined}; + background: ${hasFocus ? NeutralColors.white : 'inherit'}; margin-top: 2px; :hover .ms-Button-icon { visibility: visible; From a7b7e59c1e6058e5c7ae61d2aa199b8433454d66 Mon Sep 17 00:00:00 2001 From: zhixzhan Date: Mon, 21 Sep 2020 11:54:26 +0800 Subject: [PATCH 068/105] update container edit --- .../src/pages/knowledge-base/table-view.tsx | 271 +++++++++--------- 1 file changed, 132 insertions(+), 139 deletions(-) diff --git a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx index 5271dd4937..27127849a4 100644 --- a/Composer/packages/client/src/pages/knowledge-base/table-view.tsx +++ b/Composer/packages/client/src/pages/knowledge-base/table-view.tsx @@ -49,7 +49,6 @@ import { isQnAFileCreatedFromUrl } from '../../utils/qnaUtil'; import { formCell, - content, addIcon, divider, rowDetails, @@ -126,9 +125,10 @@ const TableView: React.FC = (props) => { const [editQnAFile, setEditQnAFile] = useState(undefined); const [qnaSections, setQnASections] = useState([]); const [kthSectionIsCreatingQuestion, setCreatingQuestionInKthSection] = useState(-1); - const importedFileIds = qnaFile?.imports.map(({ id }) => getBaseName(id)) || []; - const importedFiles = qnaFiles.filter(({ id }) => importedFileIds.includes(id)); - const importedSourceFiles = importedFiles.filter(({ id }) => id.endsWith('.source')); + const currentDialogImportedFileIds = qnaFile?.imports.map(({ id }) => getBaseName(id)) || []; + const currentDialogImportedFiles = qnaFiles.filter(({ id }) => currentDialogImportedFileIds.includes(id)); + const currentDialogImportedSourceFiles = currentDialogImportedFiles.filter(({ id }) => id.endsWith('.source')); + const allSourceFiles = qnaFiles.filter(({ id }) => id.endsWith('.source')); const [isQnASectionsExpanded, setIsQnASectionsExpanded] = useState([]); useEffect(() => { @@ -140,7 +140,9 @@ const TableView: React.FC = (props) => { if (dialogId === 'all') { setIsQnASectionsExpanded(Array(allSections.length).fill(false)); } else { - const dialogSections = allSections.filter((t) => t.dialogId === dialogId || importedFileIds.includes(t.fileId)); + const dialogSections = allSections.filter( + (t) => t.dialogId === dialogId || currentDialogImportedFileIds.includes(t.fileId) + ); setIsQnASectionsExpanded(Array(dialogSections.length).fill(false)); } }, [dialogId, projectId]); @@ -157,7 +159,7 @@ const TableView: React.FC = (props) => { if (dialogId === 'all') { setQnASections(allSections); } else { - const dialogSections = allSections.filter((t) => importedFileIds.includes(t.fileId)); + const dialogSections = allSections.filter((t) => currentDialogImportedFileIds.includes(t.fileId)); setQnASections(dialogSections); } @@ -240,117 +242,122 @@ const TableView: React.FC = (props) => { setEditQnAFile(undefined); }; - const onRenderGroupHeader: IDetailsGroupRenderProps['onRenderHeader'] = (props) => { - const groupName = props?.group?.name || ''; - const containerId = props?.group?.key || ''; - const containerQnAFile = qnaFiles.find(({ id }) => id === containerId); - const isImportedSource = containerId.endsWith('.source'); - const isSourceFromUrl = isImportedSource && containerQnAFile && isQnAFileCreatedFromUrl(containerQnAFile); - - const onRenderItem = (item: IOverflowSetItemProps): JSX.Element => { - return ( - - ); - }; + const onRenderGroupHeader: IDetailsGroupRenderProps['onRenderHeader'] = useCallback( + (props) => { + const groupName = props?.group?.name || ''; + const containerId = props?.group?.key || ''; + const containerQnAFile = qnaFiles.find(({ id }) => id === containerId); + const isImportedSource = containerId.endsWith('.source'); + const isSourceFromUrl = isImportedSource && containerQnAFile && isQnAFileCreatedFromUrl(containerQnAFile); + const isAllTab = dialogId === 'all'; + const onRenderItem = (item: IOverflowSetItemProps): JSX.Element => { + return ( + + ); + }; - const onRenderOverflowButton = (overflowItems: any[] | undefined): JSX.Element => { - return ( - - ); - }; + const onRenderOverflowButton = (overflowItems: any[] | undefined): JSX.Element => { + return ( +