diff --git a/README.md b/README.md index 0c60964..2114762 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Accordion -Accordion 1.1.1 ist ein listenbasiertes SPFx-WebPart für SharePoint Server Subscription Edition. Es orientiert +Accordion 1.1.2 ist ein listenbasiertes SPFx-WebPart für SharePoint Server Subscription Edition. Es orientiert sich funktional am PnP-Beispiel `react-accordion-dynamic-section`, verwendet aber die mit SharePoint SE kompatible SPFx-1.4.1-/React-15-Toolchain. @@ -11,6 +11,7 @@ kompatible SPFx-1.4.1-/React-15-Toolchain. - optionales Gruppieren nach Kategorien - Filter auf eine konkrete, aktuell in der Liste verwendete Kategorie - ein- und mehrwertige Managed-Metadata-Spalten als Kategorien +- Auflösung interner WSS-IDs über die `TaxonomyHiddenList` der Site Collection - auf- oder absteigende Sortierung - initial vollständig geschlossen, erster Eintrag geöffnet oder alle Einträge geöffnet - wahlweise genau ein oder mehrere gleichzeitig geöffnete Bereiche @@ -39,7 +40,7 @@ nächsten Laden beziehungsweise Rendern der Seite automatisch. | Eigenschaft | Wert | |---|---| -| Version | 1.1.1 | +| Version | 1.1.2 | | SharePoint Framework | 1.4.1 | | React | 15.6.2 | | Node.js | 8.17.0 | diff --git a/config/package-solution.json b/config/package-solution.json index 45e57ec..df01f56 100644 --- a/config/package-solution.json +++ b/config/package-solution.json @@ -4,7 +4,7 @@ "name": "accordion-web-part", "title": "accordion-web-part", "id": "27a6fd90-42a1-4e39-8862-08ed0e7803bc", - "version": "1.1.1.0", + "version": "1.1.2.0", "includeClientSideAssets": true, "skipFeatureDeployment": true }, diff --git a/package.json b/package.json index 87f5d7a..fcdbb6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "accordion-web-part", - "version": "1.1.1", + "version": "1.1.2", "private": true, "main": "lib/index.js", "engines": { diff --git a/src/webparts/accordion/AccordionCategoryValues.ts b/src/webparts/accordion/AccordionCategoryValues.ts index f2ca7cf..559675a 100644 --- a/src/webparts/accordion/AccordionCategoryValues.ts +++ b/src/webparts/accordion/AccordionCategoryValues.ts @@ -32,7 +32,7 @@ function categoryEntryValues(entry: any, splitPlainSemicolon: boolean): string[] const semicolonParts: string[] = candidate.split(';'); const allTaxonomyValues: boolean = semicolonParts.every((part: string): boolean => /\|[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(part.trim())); - if (allTaxonomyValues || splitPlainSemicolon) { parts = semicolonParts; } + if (allTaxonomyValues || splitPlainSemicolon) { parts = semicolonParts; } } const values: string[] = []; @@ -44,3 +44,20 @@ function categoryEntryValues(entry: any, splitPlainSemicolon: boolean): string[] } return values.filter((text: string): boolean => !!text); } + +export function replaceAccordionCategoryIds( + values: string[], + labelsByWssId: { [wssId: string]: string } +): string[] { + const result: string[] = []; + const keys: { [key: string]: boolean } = {}; + for (let i: number = 0; i < values.length; i++) { + const value: string = values[i]; + const label: string = /^\d+$/.test(value) && labelsByWssId[value] + ? labelsByWssId[value] + : value; + const key: string = '$' + label.toLocaleLowerCase(); + if (label && !keys[key]) { keys[key] = true; result.push(label); } + } + return result; +} diff --git a/src/webparts/accordion/AccordionDataService.ts b/src/webparts/accordion/AccordionDataService.ts index 86b33f6..5c7d6e3 100644 --- a/src/webparts/accordion/AccordionDataService.ts +++ b/src/webparts/accordion/AccordionDataService.ts @@ -1,9 +1,12 @@ import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http'; import { WebPartContext } from '@microsoft/sp-webpart-base'; -import { getAccordionCategoryValues } from './AccordionCategoryValues'; +import { getAccordionCategoryValues, replaceAccordionCategoryIds } from './AccordionCategoryValues'; import { IAccordionFieldOption, IAccordionItem, IAccordionListOption } from './AccordionTypes'; export class AccordionDataService { + private taxonomyLabelsByWssId: { [wssId: string]: string } = {}; + private missingTaxonomyWssIds: { [wssId: string]: boolean } = {}; + public constructor(private context: WebPartContext) { } public getLists(): Promise { @@ -55,20 +58,24 @@ export class AccordionDataService { '&$orderby=' + orderField + (sortAscending ? ' asc' : ' desc') + '&$top=5000'; return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.headers }) .then((response: SPHttpClientResponse): Promise => this.ensureJson(response)) // tslint:disable-line:no-any - .then((payload: any): IAccordionItem[] => this.results(payload) // tslint:disable-line:no-any - .filter((item: any): boolean => !categoryValue || // tslint:disable-line:no-any - this.itemCategoryValues(item, categoryField, taxonomyCategory).indexOf(categoryValue) >= 0) - .map((item: any): IAccordionItem => { // tslint:disable-line:no-any - const categories: string[] = categoryField - ? this.itemCategoryValues(item, categoryField, taxonomyCategory) - : []; - return { - id: Number(item.Id), - title: this.toText(item[titleField]), - content: this.toText(item[contentField]), - categories: categories - }; - })); + .then((payload: any): Promise => { // tslint:disable-line:no-any + const items: any[] = this.results(payload); // tslint:disable-line:no-any + return this.resolveItemCategoryValues(items, categoryField, taxonomyCategory) + .then((categoriesByItem: string[][]): IAccordionItem[] => { + const result: IAccordionItem[] = []; + for (let i: number = 0; i < items.length; i++) { + const categories: string[] = categoriesByItem[i]; + if (categoryValue && categories.indexOf(categoryValue) < 0) { continue; } + result.push({ + id: Number(items[i].Id), + title: this.toText(items[i][titleField]), + content: this.toText(items[i][contentField]), + categories: categories + }); + } + return result; + }); + }); } public getCategoryValues(listId: string, categoryField: string, categoryFieldType: string): Promise { @@ -80,18 +87,21 @@ export class AccordionDataService { '?$select=' + categoryField + formattedSelect + formattedExpand + '&$top=5000'; return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.headers }) .then((response: SPHttpClientResponse): Promise => this.ensureJson(response)) // tslint:disable-line:no-any - .then((payload: any): string[] => { // tslint:disable-line:no-any - const values: string[] = []; - const keys: { [key: string]: boolean } = {}; + .then((payload: any): Promise => { // tslint:disable-line:no-any const items: any[] = this.results(payload); // tslint:disable-line:no-any - for (let i: number = 0; i < items.length; i++) { - const current: string[] = this.itemCategoryValues(items[i], categoryField, taxonomyCategory); - for (let valueIndex: number = 0; valueIndex < current.length; valueIndex++) { - const key: string = '$' + current[valueIndex].toLocaleLowerCase(); - if (!keys[key]) { keys[key] = true; values.push(current[valueIndex]); } - } - } - return values.sort((left: string, right: string): number => left.localeCompare(right)); + return this.resolveItemCategoryValues(items, categoryField, taxonomyCategory) + .then((categoriesByItem: string[][]): string[] => { + const values: string[] = []; + const keys: { [key: string]: boolean } = {}; + for (let i: number = 0; i < categoriesByItem.length; i++) { + const current: string[] = categoriesByItem[i]; + for (let valueIndex: number = 0; valueIndex < current.length; valueIndex++) { + const key: string = '$' + current[valueIndex].toLocaleLowerCase(); + if (!keys[key]) { keys[key] = true; values.push(current[valueIndex]); } + } + } + return values.sort((left: string, right: string): number => left.localeCompare(right)); + }); }); } @@ -129,6 +139,61 @@ export class AccordionDataService { return getAccordionCategoryValues(item ? item[categoryField] : undefined); } + private resolveItemCategoryValues( + items: any[], // tslint:disable-line:no-any + categoryField: string, + taxonomyCategory: boolean + ): Promise { + const categoriesByItem: string[][] = items.map((item: any): string[] => // tslint:disable-line:no-any + categoryField ? this.itemCategoryValues(item, categoryField, taxonomyCategory) : []); + if (!taxonomyCategory) { return Promise.resolve(categoriesByItem); } + + const wssIds: string[] = []; + const seen: { [wssId: string]: boolean } = {}; + for (let itemIndex: number = 0; itemIndex < categoriesByItem.length; itemIndex++) { + for (let valueIndex: number = 0; valueIndex < categoriesByItem[itemIndex].length; valueIndex++) { + const value: string = categoriesByItem[itemIndex][valueIndex]; + if (/^\d+$/.test(value) && !seen[value]) { seen[value] = true; wssIds.push(value); } + } + } + if (!wssIds.length) { return Promise.resolve(categoriesByItem); } + + return this.loadTaxonomyLabels(wssIds).then((labelsByWssId: { [wssId: string]: string }): string[][] => + categoriesByItem.map((values: string[]): string[] => replaceAccordionCategoryIds(values, labelsByWssId))); + } + + private loadTaxonomyLabels(wssIds: string[]): Promise<{ [wssId: string]: string }> { + const missingIds: string[] = wssIds.filter((wssId: string): boolean => + !this.taxonomyLabelsByWssId[wssId] && !this.missingTaxonomyWssIds[wssId]); + if (!missingIds.length) { return Promise.resolve(this.taxonomyLabelsByWssId); } + + const requests: Array> = []; // tslint:disable-line:no-any + for (let offset: number = 0; offset < missingIds.length; offset += 40) { + const chunk: string[] = missingIds.slice(offset, offset + 40); + const filter: string = chunk.map((wssId: string): string => 'Id eq ' + wssId).join(' or '); + const url: string = this.webUrl + "/_api/site/rootweb/lists/getbytitle('TaxonomyHiddenList')/items" + + '?$select=Id,Term&$filter=' + filter + '&$top=40'; + requests.push(this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.headers }) + .then((response: SPHttpClientResponse): Promise => this.ensureJson(response)) // tslint:disable-line:no-any + .then((payload: any): any[] => this.results(payload))); // tslint:disable-line:no-any + } + + return Promise.all(requests).then((resultSets: any[][]): { [wssId: string]: string } => { // tslint:disable-line:no-any + for (let resultIndex: number = 0; resultIndex < resultSets.length; resultIndex++) { + const items: any[] = resultSets[resultIndex]; // tslint:disable-line:no-any + for (let itemIndex: number = 0; itemIndex < items.length; itemIndex++) { + const id: string = String(items[itemIndex].Id || items[itemIndex].ID || ''); + const term: string = String(items[itemIndex].Term || '').trim(); + if (id && term) { this.taxonomyLabelsByWssId[id] = term; } + } + } + for (let i: number = 0; i < missingIds.length; i++) { + if (!this.taxonomyLabelsByWssId[missingIds[i]]) { this.missingTaxonomyWssIds[missingIds[i]] = true; } + } + return this.taxonomyLabelsByWssId; + }); + } + private toText(value: any): string { // tslint:disable-line:no-any if (value === undefined || value === null) { return ''; } if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return String(value); } diff --git a/tests/AccordionCategoryValues.test.ts b/tests/AccordionCategoryValues.test.ts index 7ac443d..86fe626 100644 --- a/tests/AccordionCategoryValues.test.ts +++ b/tests/AccordionCategoryValues.test.ts @@ -1,4 +1,7 @@ -import { getAccordionCategoryValues } from '../src/webparts/accordion/AccordionCategoryValues'; +import { + getAccordionCategoryValues, + replaceAccordionCategoryIds +} from '../src/webparts/accordion/AccordionCategoryValues'; let passed: number = 0; function assertValues(actual: string[], expected: string[], message: string): void { @@ -25,5 +28,7 @@ assertValues(getAccordionCategoryValues({ results: ['General', 'General', 'News' ['General', 'News'], 'Choice values were not retained and de-duplicated.'); assertValues(getAccordionCategoryValues('Policies; Benefits', true), ['Policies', 'Benefits'], 'FieldValuesAsText taxonomy labels were not split.'); +assertValues(replaceAccordionCategoryIds(['1', '2'], { '1': 'Policies', '2': 'Benefits' }), + ['Policies', 'Benefits'], 'WSS IDs were not replaced with TaxonomyHiddenList labels.'); console.log('Accordion category value tests passed:', passed); diff --git a/tests/validate-project.js b/tests/validate-project.js index 142f3a8..98dde06 100644 --- a/tests/validate-project.js +++ b/tests/validate-project.js @@ -18,8 +18,8 @@ var component = read('src/webparts/accordion/components/Accordion.tsx'); var sanitizer = read('src/webparts/accordion/AccordionHtmlSanitizer.ts'); var styles = read('src/webparts/accordion/components/Accordion.module.scss'); -assert(packageJson.version === '1.1.1', 'Package version must be 1.1.1.'); -assert(solution.version === '1.1.1.0', 'Solution version must be 1.1.1.0.'); +assert(packageJson.version === '1.1.2', 'Package version must be 1.1.2.'); +assert(solution.version === '1.1.2.0', 'Solution version must be 1.1.2.0.'); assert(solution.includeClientSideAssets === true, 'Client-side assets must be included.'); assert(solution.skipFeatureDeployment === true, 'Solution must support central deployment.'); assert(packageJson.name === 'accordion-web-part' && solution.name === 'accordion-web-part' && @@ -44,6 +44,10 @@ assert(dataService.indexOf("'TaxonomyFieldType'") >= 0 && assert(dataService.indexOf('FieldValuesAsText/') >= 0 && dataService.indexOf('itemCategoryValues') >= 0 && categoryValues.indexOf('splitPlainSemicolon') >= 0, 'Managed Metadata labels must be resolved instead of displaying WSS IDs.'); +assert(dataService.indexOf("getbytitle('TaxonomyHiddenList')") >= 0 && + dataService.indexOf('loadTaxonomyLabels') >= 0 && + categoryValues.indexOf('replaceAccordionCategoryIds') >= 0, + 'Numeric WSS IDs must be resolved through the site collection TaxonomyHiddenList.'); assert(component.indexOf('items[i].categories') >= 0 && component.indexOf('nameIndex') >= 0, 'Multi-value category grouping is missing.'); assert(component.indexOf("aria-expanded={open}") >= 0 && component.indexOf("role='region'") >= 0,