From 38325a3a0c6365d1c92fe04bb9c21ea27ea4b932 Mon Sep 17 00:00:00 2001 From: Ning Tang Date: Fri, 3 Nov 2023 16:35:20 +0800 Subject: [PATCH] refactor: rename filter options (#10276) * refactor: rename filter options * fix: sample config cache bug * fix: ut --- packages/cli/tests/unit/utils.tests.ts | 9 +- packages/fx-core/src/common/samples.ts | 102 ++++++------- .../tests/common/samples-config-v3.json | 11 +- packages/fx-core/tests/common/samples.test.ts | 12 +- .../src/controls/sampleGallery/ISamples.ts | 10 +- .../controls/sampleGallery/SampleGallery.tsx | 54 +++---- .../controls/sampleGallery/sampleFilter.tsx | 144 ++++++++++++------ .../sampleGallery/sampleListItem.scss | 2 + .../controls/sampleGallery/sampleListItem.tsx | 2 +- 9 files changed, 193 insertions(+), 153 deletions(-) diff --git a/packages/cli/tests/unit/utils.tests.ts b/packages/cli/tests/unit/utils.tests.ts index 3f424f8303..a607b45e4e 100644 --- a/packages/cli/tests/unit/utils.tests.ts +++ b/packages/cli/tests/unit/utils.tests.ts @@ -137,16 +137,15 @@ projectId: 00000000-0000-0000-0000-000000000000`; this.afterEach(() => { sandbox.restore(); - core.sampleProvider["sampleCollection"] = undefined; }); it("filters samples have maximum cli verion", async () => { sandbox.stub(core.sampleProvider, "fetchSampleConfig").callsFake(async () => { core.sampleProvider["samplesConfig"] = { filterOptions: { - types: ["Tab"], + capabilities: ["Tab"], languages: ["TS"], - techniques: ["Azure"], + technologies: ["Azure"], }, samples: [ { @@ -187,9 +186,9 @@ projectId: 00000000-0000-0000-0000-000000000000`; sandbox.stub(core.sampleProvider, "fetchSampleConfig").callsFake(async () => { core.sampleProvider["samplesConfig"] = { filterOptions: { - types: ["Tab"], + capabilities: ["Tab"], languages: ["TS"], - techniques: ["Azure"], + technologies: ["Azure"], }, samples: [ { diff --git a/packages/fx-core/src/common/samples.ts b/packages/fx-core/src/common/samples.ts index a6b948f63e..1aa4cd21be 100644 --- a/packages/fx-core/src/common/samples.ts +++ b/packages/fx-core/src/common/samples.ts @@ -9,7 +9,6 @@ import { SampleUrlInfo, sendRequestWithTimeout } from "../component/generator/ut import { ErrorContextMW } from "../core/globalVars"; import { AccessGithubError } from "../error/common"; import { FeatureFlagName } from "./constants"; -import { isVideoFilterEnabled } from "./featureFlags"; const packageJson = require("../../package.json"); @@ -47,9 +46,9 @@ export interface SampleConfig { interface SampleCollection { samples: SampleConfig[]; filterOptions: { - types: string[]; + capabilities: string[]; languages: string[]; - techniques: string[]; + technologies: string[]; }; } @@ -59,7 +58,6 @@ type SampleConfigType = { }; class SampleProvider { - private sampleCollection: SampleCollection | undefined; private samplesConfig: SampleConfigType | undefined; private branchOrTag = SampleConfigTag; @@ -93,58 +91,54 @@ class SampleProvider { } public get SampleCollection(): SampleCollection { - if (!this.sampleCollection) { - const samples = - this.samplesConfig?.samples.map((sample) => { - const isExternal = sample["downloadUrlInfo"] ? true : false; - let gifUrl = + const samples = + this.samplesConfig?.samples.map((sample) => { + const isExternal = sample["downloadUrlInfo"] ? true : false; + let gifUrl = + sample["gifPath"] !== undefined + ? `https://raw.githubusercontent.com/${SampleConfigOwner}/${SampleConfigRepo}/${ + this.branchOrTag + }/${sample["id"] as string}/${sample["gifPath"] as string}` + : undefined; + let thumbnailUrl = `https://raw.githubusercontent.com/${SampleConfigOwner}/${SampleConfigRepo}/${ + this.branchOrTag + }/${sample["id"] as string}/${sample["thumbnailPath"] as string}`; + if (isExternal) { + const info = sample["downloadUrlInfo"] as SampleUrlInfo; + gifUrl = sample["gifPath"] !== undefined - ? `https://raw.githubusercontent.com/${SampleConfigOwner}/${SampleConfigRepo}/${ - this.branchOrTag - }/${sample["id"] as string}/${sample["gifPath"] as string}` + ? `https://raw.githubusercontent.com/${info.owner}/${info.repository}/${info.ref}/${ + info.dir + }/${sample["gifPath"] as string}` : undefined; - let thumbnailUrl = `https://raw.githubusercontent.com/${SampleConfigOwner}/${SampleConfigRepo}/${ - this.branchOrTag - }/${sample["id"] as string}/${sample["thumbnailPath"] as string}`; - if (isExternal) { - const info = sample["downloadUrlInfo"] as SampleUrlInfo; - gifUrl = - sample["gifPath"] !== undefined - ? `https://raw.githubusercontent.com/${info.owner}/${info.repository}/${info.ref}/${ - info.dir - }/${sample["gifPath"] as string}` - : undefined; - thumbnailUrl = `https://raw.githubusercontent.com/${info.owner}/${info.repository}/${ - info.ref - }/${info.dir}/${sample["thumbnailPath"] as string}`; - } - return { - ...sample, - onboardDate: new Date(sample["onboardDate"] as string), - downloadUrlInfo: isExternal - ? sample["downloadUrlInfo"] - : { - owner: SampleConfigOwner, - repository: SampleConfigRepo, - ref: this.branchOrTag, - dir: sample["id"] as string, - }, - gifUrl: gifUrl, - thumbnailUrl: thumbnailUrl, - } as SampleConfig; - }) || []; - - this.sampleCollection = { - samples, - filterOptions: { - types: this.samplesConfig?.filterOptions["types"] || [], - languages: this.samplesConfig?.filterOptions["languages"] || [], - techniques: this.samplesConfig?.filterOptions["techniques"] || [], - }, - }; - } - - return this.sampleCollection; + thumbnailUrl = `https://raw.githubusercontent.com/${info.owner}/${info.repository}/${ + info.ref + }/${info.dir}/${sample["thumbnailPath"] as string}`; + } + return { + ...sample, + onboardDate: new Date(sample["onboardDate"] as string), + downloadUrlInfo: isExternal + ? sample["downloadUrlInfo"] + : { + owner: SampleConfigOwner, + repository: SampleConfigRepo, + ref: this.branchOrTag, + dir: sample["id"] as string, + }, + gifUrl: gifUrl, + thumbnailUrl: thumbnailUrl, + } as SampleConfig; + }) || []; + + return { + samples, + filterOptions: { + capabilities: this.samplesConfig?.filterOptions["capabilities"] || [], + languages: this.samplesConfig?.filterOptions["languages"] || [], + technologies: this.samplesConfig?.filterOptions["technologies"] || [], + }, + }; } private async fetchRawFileContent(branchOrTag: string): Promise { diff --git a/packages/fx-core/tests/common/samples-config-v3.json b/packages/fx-core/tests/common/samples-config-v3.json index 0005bb4ed2..4253c64a75 100644 --- a/packages/fx-core/tests/common/samples-config-v3.json +++ b/packages/fx-core/tests/common/samples-config-v3.json @@ -1,15 +1,16 @@ { "filterOptions": { - "types": [ + "capabilities": [ "Tab", "Bot", - "Message extension" + "Message Extension", + "Meeting" ], "languages": [ - "JS", - "TS" + "JavaScript", + "TypeScript" ], - "techniques": [ + "technologies": [ "Azure", "Adaptive Cards", "SSO", diff --git a/packages/fx-core/tests/common/samples.test.ts b/packages/fx-core/tests/common/samples.test.ts index 54ff14345a..e0895cd8f8 100644 --- a/packages/fx-core/tests/common/samples.test.ts +++ b/packages/fx-core/tests/common/samples.test.ts @@ -19,9 +19,9 @@ describe("Samples", () => { const sandbox = sinon.createSandbox(); const fakedSampleConfig = { filterOptions: { - types: ["Tab"], + capabilities: ["Tab"], languages: ["TS"], - techniques: ["Azure"], + technologies: ["Azure"], }, samples: [ { @@ -51,7 +51,6 @@ describe("Samples", () => { sandbox.restore(); sampleProvider["samplesConfig"] = undefined; process.env["TEAMSFX_SAMPLE_CONFIG_BRANCH"] = undefined; - (sampleProvider as any).sampleCollection = undefined; }); it("download sample config on 'dev' branch in alpha version", async () => { @@ -78,7 +77,7 @@ describe("Samples", () => { }); chai.expect(samples[0].gifUrl).equal(undefined); const filterOptions = sampleProvider.SampleCollection.filterOptions; - chai.expect(filterOptions.types).to.deep.equal(["Tab"]); + chai.expect(filterOptions.capabilities).to.deep.equal(["Tab"]); }); it("download sample config of prerelease branch in prerelease(beta) version", async () => { @@ -235,7 +234,6 @@ describe("Samples", () => { chai.expect(sampleConfigV3.samples.find((sampleInConfig) => sampleInConfig.id === sample.id)) .exist; } - (sampleProvider as any).sampleCollection = undefined; }); it("External sample url can be retrieved correctly in v3", () => { @@ -264,7 +262,6 @@ describe("Samples", () => { chai.expect(faked?.downloadUrlInfo).equals(fakedExternalSample.downloadUrlInfo); chai.expect(faked?.gifUrl).equals(undefined); - (sampleProvider as any).sampleCollection = undefined; sampleConfigV3.samples.splice(sampleConfigV3.samples.length - 1, 1); }); @@ -311,10 +308,9 @@ describe("Samples", () => { }); it("returns empty sample collection when sample config is undefined", () => { - (sampleProvider as any).sampleCollection = undefined; const sampleCollection = sampleProvider.SampleCollection; - chai.expect(sampleCollection.filterOptions.types).to.deep.equal([]); + chai.expect(sampleCollection.filterOptions.capabilities).to.deep.equal([]); chai.expect(sampleCollection.samples).to.deep.equal([]); }); }); diff --git a/packages/vscode-extension/src/controls/sampleGallery/ISamples.ts b/packages/vscode-extension/src/controls/sampleGallery/ISamples.ts index daf3b4f83a..9f75c458bc 100644 --- a/packages/vscode-extension/src/controls/sampleGallery/ISamples.ts +++ b/packages/vscode-extension/src/controls/sampleGallery/ISamples.ts @@ -12,7 +12,7 @@ export type SampleGalleryState = { // keep filtering state here to recover after navigating back from detail page layout: "grid" | "list"; query: string; - filterTags: Record; + filterTags: SampleFilterOptionType; }; export interface SampleInfo { @@ -47,9 +47,9 @@ export type SampleProps = { }; export type SampleFilterOptionType = { - types: string[]; + capabilities: string[]; languages: string[]; - techniques: string[]; + technologies: string[]; }; export type SampleFilterProps = { @@ -57,8 +57,8 @@ export type SampleFilterProps = { filterOptions: SampleFilterOptionType; layout: "grid" | "list"; query: string; - filterTags: Record; + filterTags: SampleFilterOptionType; onLayoutChanged: (layout: "grid" | "list") => void; - onFilterConditionChanged: (query: string, filterTags: Record) => void; + onFilterConditionChanged: (query: string, filterTags: SampleFilterOptionType) => void; }; diff --git a/packages/vscode-extension/src/controls/sampleGallery/SampleGallery.tsx b/packages/vscode-extension/src/controls/sampleGallery/SampleGallery.tsx index 1a0dbd6cc2..633626daa3 100644 --- a/packages/vscode-extension/src/controls/sampleGallery/SampleGallery.tsx +++ b/packages/vscode-extension/src/controls/sampleGallery/SampleGallery.tsx @@ -25,9 +25,9 @@ import SampleListItem from "./sampleListItem"; export default class SampleGallery extends React.Component { private samples: SampleInfo[] = []; private filterOptions: SampleFilterOptionType = { - types: [], + capabilities: [], languages: [], - techniques: [], + technologies: [], }; constructor(props: unknown) { @@ -36,7 +36,7 @@ export default class SampleGallery extends React.Component) => { - let filteredSamples = this.samples.filter((sample: SampleInfo) => { - for (const key in filterTags) { - if (filterTags[key].length === 0) { - continue; - } - let isMatch = false; - for (const tag of filterTags[key]) { - if (sample.tags.findIndex((value) => value.includes(tag)) >= 0) { - isMatch = true; - break; - } - } - if (!isMatch) { - return false; + private onFilterConditionChanged = (query: string, filterTags: SampleFilterOptionType) => { + const containsTag = (targets: string[], tags: string[]) => { + if (targets.length === 0) { + return true; + } + for (const target of targets) { + if (tags.findIndex((value) => value.toLowerCase().includes(target.toLowerCase())) >= 0) { + return true; } } - return true; + return false; + }; + let filteredSamples = this.samples.filter((sample: SampleInfo) => { + return ( + containsTag(filterTags.capabilities, sample.tags) && + containsTag(filterTags.languages, sample.tags) && + containsTag(filterTags.technologies, sample.tags) + ); }); if (query !== "") { const fuse = new Fuse(filteredSamples, { @@ -282,9 +282,9 @@ export default class SampleGallery extends React.Component { constructor(props: SampleFilterProps) { @@ -24,11 +24,11 @@ export default class SampleFilter extends React.Component { - const selected = this.props.filterTags.types.indexOf(type) >= 0; + const selected = this.props.filterTags.capabilities.indexOf(type) >= 0; return { key: type, text: type, selected }; }); const languageOptions: IDropdownOption[] = sampleLanguages.map((type) => { @@ -36,7 +36,7 @@ export default class SampleFilter extends React.Component { - const selected = this.props.filterTags.techniques.indexOf(type) >= 0; + const selected = this.props.filterTags.technologies.indexOf(type) >= 0; return { key: type, text: type, selected }; }); const dropdownStyles = this.getDropdownStyles(); @@ -52,35 +52,35 @@ export default class SampleFilter extends React.Component { - return this.props.filterTags["types"].indexOf(type) >= 0; + return this.props.filterTags.capabilities.indexOf(type) >= 0; })} dropdownWidth="auto" /> { - return this.props.filterTags["languages"].indexOf(type) >= 0; + return this.props.filterTags.languages.indexOf(type) >= 0; })} dropdownWidth="auto" /> { - return this.props.filterTags["techniques"].indexOf(type) >= 0; + return this.props.filterTags.technologies.indexOf(type) >= 0; })} dropdownWidth="auto" /> @@ -134,33 +134,80 @@ export default class SampleFilter extends React.Component (_event: React.FormEvent, option?: IDropdownOption) => void = ( - filterType: string + private onFilterTagChanged = ( + telemetryEvent: TelemetryEvent, + changedFilter: string, + newFilterTags: SampleFilterOptionType ) => { - return (_event: React.FormEvent, option?: IDropdownOption) => { - const choice = option?.key as string; - const event = option?.selected - ? TelemetryEvent.FilterSampleAdd - : TelemetryEvent.FilterSampleRemove; - vscode.postMessage({ - command: Commands.SendTelemetryEvent, - data: { - eventName: event, - properties: { - [TelemetryProperty.TriggerFrom]: TelemetryTriggerFrom.SampleGallery, - [TelemetryProperty.ChangedFilter]: choice, - [TelemetryProperty.SampleFilters]: this.getAllFilterTags().join(","), - }, + vscode.postMessage({ + command: Commands.SendTelemetryEvent, + data: { + eventName: telemetryEvent, + properties: { + [TelemetryProperty.TriggerFrom]: TelemetryTriggerFrom.SampleGallery, + [TelemetryProperty.ChangedFilter]: changedFilter, + [TelemetryProperty.SampleFilters]: this.getAllFilterTags().join(","), }, - }); - const newTags = option?.selected - ? [...this.props.filterTags[filterType], choice] - : this.props.filterTags[filterType].filter((tag) => tag !== choice); - const newFilterTags = { ...this.props.filterTags, [filterType]: newTags }; - this.props.onFilterConditionChanged(this.props.query, newFilterTags); - }; + }, + }); + this.props.onFilterConditionChanged(this.props.query, newFilterTags); + }; + + private onFilterCapabilityChanged = ( + _event: React.FormEvent, + option?: IDropdownOption + ) => { + const choice = option?.key as string; + let telemetryEvent = TelemetryEvent.FilterSampleAdd; + let newData: string[] = []; + if (option?.selected) { + newData = [...this.props.filterTags.capabilities, choice]; + } else { + telemetryEvent = TelemetryEvent.FilterSampleRemove; + newData = this.props.filterTags.capabilities.filter((tag) => tag !== choice); + } + this.onFilterTagChanged(telemetryEvent, choice, { + ...this.props.filterTags, + capabilities: newData, + }); + }; + + private onFilterLanguageChanged = ( + _event: React.FormEvent, + option?: IDropdownOption + ) => { + const choice = option?.key as string; + let telemetryEvent = TelemetryEvent.FilterSampleAdd; + let newData: string[] = []; + if (option?.selected) { + newData = [...this.props.filterTags.languages, choice]; + } else { + telemetryEvent = TelemetryEvent.FilterSampleRemove; + newData = this.props.filterTags.languages.filter((tag) => tag !== choice); + } + this.onFilterTagChanged(telemetryEvent, choice, { + ...this.props.filterTags, + languages: newData, + }); + }; + + private onFilterTechnologyChanged = ( + _event: React.FormEvent, + option?: IDropdownOption + ) => { + const choice = option?.key as string; + let telemetryEvent = TelemetryEvent.FilterSampleAdd; + let newData: string[] = []; + if (option?.selected) { + newData = [...this.props.filterTags.technologies, choice]; + } else { + telemetryEvent = TelemetryEvent.FilterSampleRemove; + newData = this.props.filterTags.technologies.filter((tag) => tag !== choice); + } + this.onFilterTagChanged(telemetryEvent, choice, { + ...this.props.filterTags, + technologies: newData, + }); }; private onTagRemoved = (removedTag: string) => { @@ -176,9 +223,9 @@ export default class SampleFilter extends React.Component tag !== removedTag); - } + newFilterTags.capabilities = newFilterTags.capabilities.filter((tag) => tag !== removedTag); + newFilterTags.languages = newFilterTags.languages.filter((tag) => tag !== removedTag); + newFilterTags.technologies = newFilterTags.technologies.filter((tag) => tag !== removedTag); this.props.onFilterConditionChanged(this.props.query, newFilterTags); }; @@ -194,10 +241,11 @@ export default class SampleFilter extends React.Component { - return this.props.filterTags.types + return this.props.filterTags.capabilities .concat(this.props.filterTags.languages) - .concat(this.props.filterTags.techniques); + .concat(this.props.filterTags.technologies); }; } diff --git a/packages/vscode-extension/src/controls/sampleGallery/sampleListItem.scss b/packages/vscode-extension/src/controls/sampleGallery/sampleListItem.scss index 3511853458..5fcff76eb6 100644 --- a/packages/vscode-extension/src/controls/sampleGallery/sampleListItem.scss +++ b/packages/vscode-extension/src/controls/sampleGallery/sampleListItem.scss @@ -56,6 +56,8 @@ flex-grow: 1; flex-shrink: 2; min-width: 4.5px; + height: 20px; + cursor:pointer; } .tagSection { diff --git a/packages/vscode-extension/src/controls/sampleGallery/sampleListItem.tsx b/packages/vscode-extension/src/controls/sampleGallery/sampleListItem.tsx index bcc8949823..ff89bb5f04 100644 --- a/packages/vscode-extension/src/controls/sampleGallery/sampleListItem.tsx +++ b/packages/vscode-extension/src/controls/sampleGallery/sampleListItem.tsx @@ -60,7 +60,7 @@ export default class SampleListItem extends React.Component -
+
{sample.versionComparisonResult != 0 && (