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 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 |
+1 -1
View File
@@ -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
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "accordion-web-part",
"version": "1.1.1",
"version": "1.1.2",
"private": true,
"main": "lib/index.js",
"engines": {
@@ -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;
}
+91 -26
View File
@@ -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<IAccordionListOption[]> {
@@ -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<any> => 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<IAccordionItem[]> => { // 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<string[]> {
@@ -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<any> => 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<string[]> => { // 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<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
if (value === undefined || value === null) { return ''; }
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;
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);
+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 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,