Aktualisiere Version auf 1.1.2, füge Unterstützung für die Auflösung interner WSS-IDs über die TaxonomyHiddenList hinzu und verbessere die Handhabung von Kategoriewerten im Accordion-WebPart

This commit is contained in:
Torsten Brendgen
2026-08-26 23:41:37 +02:00
parent 3ee1247efe
commit b9c0f1de47
7 changed files with 126 additions and 34 deletions
+3 -2
View File
@@ -1,6 +1,6 @@
# Accordion # 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 sich funktional am PnP-Beispiel `react-accordion-dynamic-section`, verwendet aber die mit SharePoint SE
kompatible SPFx-1.4.1-/React-15-Toolchain. kompatible SPFx-1.4.1-/React-15-Toolchain.
@@ -11,6 +11,7 @@ kompatible SPFx-1.4.1-/React-15-Toolchain.
- optionales Gruppieren nach Kategorien - optionales Gruppieren nach Kategorien
- Filter auf eine konkrete, aktuell in der Liste verwendete Kategorie - Filter auf eine konkrete, aktuell in der Liste verwendete Kategorie
- ein- und mehrwertige Managed-Metadata-Spalten als Kategorien - ein- und mehrwertige Managed-Metadata-Spalten als Kategorien
- Auflösung interner WSS-IDs über die `TaxonomyHiddenList` der Site Collection
- auf- oder absteigende Sortierung - auf- oder absteigende Sortierung
- initial vollständig geschlossen, erster Eintrag geöffnet oder alle Einträge geöffnet - initial vollständig geschlossen, erster Eintrag geöffnet oder alle Einträge geöffnet
- wahlweise genau ein oder mehrere gleichzeitig geöffnete Bereiche - wahlweise genau ein oder mehrere gleichzeitig geöffnete Bereiche
@@ -39,7 +40,7 @@ nächsten Laden beziehungsweise Rendern der Seite automatisch.
| Eigenschaft | Wert | | Eigenschaft | Wert |
|---|---| |---|---|
| Version | 1.1.1 | | Version | 1.1.2 |
| SharePoint Framework | 1.4.1 | | SharePoint Framework | 1.4.1 |
| React | 15.6.2 | | React | 15.6.2 |
| Node.js | 8.17.0 | | Node.js | 8.17.0 |
+1 -1
View File
@@ -4,7 +4,7 @@
"name": "accordion-web-part", "name": "accordion-web-part",
"title": "accordion-web-part", "title": "accordion-web-part",
"id": "27a6fd90-42a1-4e39-8862-08ed0e7803bc", "id": "27a6fd90-42a1-4e39-8862-08ed0e7803bc",
"version": "1.1.1.0", "version": "1.1.2.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": true "skipFeatureDeployment": true
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "accordion-web-part", "name": "accordion-web-part",
"version": "1.1.1", "version": "1.1.2",
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
"engines": { "engines": {
@@ -44,3 +44,20 @@ function categoryEntryValues(entry: any, splitPlainSemicolon: boolean): string[]
} }
return values.filter((text: string): boolean => !!text); 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;
}
+83 -18
View File
@@ -1,9 +1,12 @@
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http'; import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import { WebPartContext } from '@microsoft/sp-webpart-base'; import { WebPartContext } from '@microsoft/sp-webpart-base';
import { getAccordionCategoryValues } from './AccordionCategoryValues'; import { getAccordionCategoryValues, replaceAccordionCategoryIds } from './AccordionCategoryValues';
import { IAccordionFieldOption, IAccordionItem, IAccordionListOption } from './AccordionTypes'; import { IAccordionFieldOption, IAccordionItem, IAccordionListOption } from './AccordionTypes';
export class AccordionDataService { export class AccordionDataService {
private taxonomyLabelsByWssId: { [wssId: string]: string } = {};
private missingTaxonomyWssIds: { [wssId: string]: boolean } = {};
public constructor(private context: WebPartContext) { } public constructor(private context: WebPartContext) { }
public getLists(): Promise<IAccordionListOption[]> { public getLists(): Promise<IAccordionListOption[]> {
@@ -55,20 +58,24 @@ export class AccordionDataService {
'&$orderby=' + orderField + (sortAscending ? ' asc' : ' desc') + '&$top=5000'; '&$orderby=' + orderField + (sortAscending ? ' asc' : ' desc') + '&$top=5000';
return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.headers }) return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.headers })
.then((response: SPHttpClientResponse): Promise<any> => this.ensureJson(response)) // tslint:disable-line:no-any .then((response: SPHttpClientResponse): Promise<any> => this.ensureJson(response)) // tslint:disable-line:no-any
.then((payload: any): IAccordionItem[] => this.results(payload) // tslint:disable-line:no-any .then((payload: any): Promise<IAccordionItem[]> => { // tslint:disable-line:no-any
.filter((item: any): boolean => !categoryValue || // tslint:disable-line:no-any const items: any[] = this.results(payload); // tslint:disable-line:no-any
this.itemCategoryValues(item, categoryField, taxonomyCategory).indexOf(categoryValue) >= 0) return this.resolveItemCategoryValues(items, categoryField, taxonomyCategory)
.map((item: any): IAccordionItem => { // tslint:disable-line:no-any .then((categoriesByItem: string[][]): IAccordionItem[] => {
const categories: string[] = categoryField const result: IAccordionItem[] = [];
? this.itemCategoryValues(item, categoryField, taxonomyCategory) for (let i: number = 0; i < items.length; i++) {
: []; const categories: string[] = categoriesByItem[i];
return { if (categoryValue && categories.indexOf(categoryValue) < 0) { continue; }
id: Number(item.Id), result.push({
title: this.toText(item[titleField]), id: Number(items[i].Id),
content: this.toText(item[contentField]), title: this.toText(items[i][titleField]),
content: this.toText(items[i][contentField]),
categories: categories categories: categories
}; });
})); }
return result;
});
});
} }
public getCategoryValues(listId: string, categoryField: string, categoryFieldType: string): Promise<string[]> { public getCategoryValues(listId: string, categoryField: string, categoryFieldType: string): Promise<string[]> {
@@ -80,12 +87,14 @@ export class AccordionDataService {
'?$select=' + categoryField + formattedSelect + formattedExpand + '&$top=5000'; '?$select=' + categoryField + formattedSelect + formattedExpand + '&$top=5000';
return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.headers }) return this.context.spHttpClient.get(url, SPHttpClient.configurations.v1, { headers: this.headers })
.then((response: SPHttpClientResponse): Promise<any> => this.ensureJson(response)) // tslint:disable-line:no-any .then((response: SPHttpClientResponse): Promise<any> => this.ensureJson(response)) // tslint:disable-line:no-any
.then((payload: any): string[] => { // tslint:disable-line:no-any .then((payload: any): Promise<string[]> => { // 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[][]): string[] => {
const values: string[] = []; const values: string[] = [];
const keys: { [key: string]: boolean } = {}; const keys: { [key: string]: boolean } = {};
const items: any[] = this.results(payload); // tslint:disable-line:no-any for (let i: number = 0; i < categoriesByItem.length; i++) {
for (let i: number = 0; i < items.length; i++) { const current: string[] = categoriesByItem[i];
const current: string[] = this.itemCategoryValues(items[i], categoryField, taxonomyCategory);
for (let valueIndex: number = 0; valueIndex < current.length; valueIndex++) { for (let valueIndex: number = 0; valueIndex < current.length; valueIndex++) {
const key: string = '$' + current[valueIndex].toLocaleLowerCase(); const key: string = '$' + current[valueIndex].toLocaleLowerCase();
if (!keys[key]) { keys[key] = true; values.push(current[valueIndex]); } if (!keys[key]) { keys[key] = true; values.push(current[valueIndex]); }
@@ -93,6 +102,7 @@ export class AccordionDataService {
} }
return values.sort((left: string, right: string): number => left.localeCompare(right)); return values.sort((left: string, right: string): number => left.localeCompare(right));
}); });
});
} }
private get webUrl(): string { return this.context.pageContext.web.absoluteUrl.replace(/\/$/, ''); } private get webUrl(): string { return this.context.pageContext.web.absoluteUrl.replace(/\/$/, ''); }
@@ -129,6 +139,61 @@ export class AccordionDataService {
return getAccordionCategoryValues(item ? item[categoryField] : undefined); return getAccordionCategoryValues(item ? item[categoryField] : undefined);
} }
private resolveItemCategoryValues(
items: any[], // tslint:disable-line:no-any
categoryField: string,
taxonomyCategory: boolean
): Promise<string[][]> {
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<Promise<any[]>> = []; // 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<any> => 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 private toText(value: any): string { // tslint:disable-line:no-any
if (value === undefined || value === null) { return ''; } if (value === undefined || value === null) { return ''; }
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return String(value); } if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return String(value); }
+6 -1
View File
@@ -1,4 +1,7 @@
import { getAccordionCategoryValues } from '../src/webparts/accordion/AccordionCategoryValues'; import {
getAccordionCategoryValues,
replaceAccordionCategoryIds
} from '../src/webparts/accordion/AccordionCategoryValues';
let passed: number = 0; let passed: number = 0;
function assertValues(actual: string[], expected: string[], message: string): void { 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.'); ['General', 'News'], 'Choice values were not retained and de-duplicated.');
assertValues(getAccordionCategoryValues('Policies; Benefits', true), assertValues(getAccordionCategoryValues('Policies; Benefits', true),
['Policies', 'Benefits'], 'FieldValuesAsText taxonomy labels were not split.'); ['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); console.log('Accordion category value tests passed:', passed);
+6 -2
View File
@@ -18,8 +18,8 @@ var component = read('src/webparts/accordion/components/Accordion.tsx');
var sanitizer = read('src/webparts/accordion/AccordionHtmlSanitizer.ts'); var sanitizer = read('src/webparts/accordion/AccordionHtmlSanitizer.ts');
var styles = read('src/webparts/accordion/components/Accordion.module.scss'); var styles = read('src/webparts/accordion/components/Accordion.module.scss');
assert(packageJson.version === '1.1.1', 'Package version must be 1.1.1.'); assert(packageJson.version === '1.1.2', 'Package version must be 1.1.2.');
assert(solution.version === '1.1.1.0', 'Solution version must be 1.1.1.0.'); 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.includeClientSideAssets === true, 'Client-side assets must be included.');
assert(solution.skipFeatureDeployment === true, 'Solution must support central deployment.'); assert(solution.skipFeatureDeployment === true, 'Solution must support central deployment.');
assert(packageJson.name === 'accordion-web-part' && solution.name === 'accordion-web-part' && 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 && assert(dataService.indexOf('FieldValuesAsText/') >= 0 &&
dataService.indexOf('itemCategoryValues') >= 0 && categoryValues.indexOf('splitPlainSemicolon') >= 0, dataService.indexOf('itemCategoryValues') >= 0 && categoryValues.indexOf('splitPlainSemicolon') >= 0,
'Managed Metadata labels must be resolved instead of displaying WSS IDs.'); '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, assert(component.indexOf('items[i].categories') >= 0 && component.indexOf('nameIndex') >= 0,
'Multi-value category grouping is missing.'); 'Multi-value category grouping is missing.');
assert(component.indexOf("aria-expanded={open}") >= 0 && component.indexOf("role='region'") >= 0, assert(component.indexOf("aria-expanded={open}") >= 0 && component.indexOf("role='region'") >= 0,