Füge Filter für die angezeigte Kategorie hinzu und aktualisiere die WebPart-Einstellungen

This commit is contained in:
Torsten Brendgen
2026-08-23 22:51:28 +02:00
parent bf9781d043
commit 6cc80b7d72
9 changed files with 103 additions and 16 deletions
+5 -3
View File
@@ -9,6 +9,7 @@ kompatible SPFx-1.4.1-/React-15-Toolchain.
- Auswahl einer nicht ausgeblendeten Liste aus dem aktuellen Web
- dynamische Auswahl von Überschrift-, Inhalt-, Kategorie- und Sortierspalte
- optionales Gruppieren nach Kategorien
- Filter auf eine konkrete, aktuell in der Liste verwendete Kategorie
- 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
@@ -28,8 +29,10 @@ Es wird keine fachliche Liste provisioniert. Eine einfache FAQ-Liste kann beispi
| `Category` | Auswahl oder Text | optionale Gruppierung |
| `SortOrder` | Zahl | optionale Sortierung |
Nach dem Hinzufügen des WebParts werden Liste und Spalten im Property Pane ausgewählt. Änderungen an den
Listeneinträgen erscheinen beim nächsten Laden beziehungsweise Rendern der Seite automatisch.
Nach dem Hinzufügen des WebParts werden Liste und Spalten im Property Pane ausgewählt. Sobald eine
Kategorie-Spalte festgelegt ist, kann unter **Angezeigte Kategorie** entweder eine der aktuell verwendeten
Kategorien oder **Alle Kategorien** ausgewählt werden. Änderungen an den Listeneinträgen erscheinen beim
nächsten Laden beziehungsweise Rendern der Seite automatisch.
## Version und Build
@@ -59,4 +62,3 @@ werden entfernt.
Die fachliche Vorlage ist das PnP Community Sample:
https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-accordion-dynamic-section
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "accordion",
"name": "accordion-web-part",
"version": "1.0.0",
"private": true,
"main": "lib/index.js",
+34 -3
View File
@@ -36,7 +36,8 @@ export class AccordionDataService {
contentField: string,
categoryField: string,
sortField: string,
sortAscending: boolean
sortAscending: boolean,
categoryValue: string
): Promise<IAccordionItem[]> {
if (!this.isGuid(listId)) { return Promise.reject(new Error('Invalid list ID.')); }
const fields: string[] = this.uniqueFields(['Id', titleField, contentField, categoryField]);
@@ -48,7 +49,10 @@ export class AccordionDataService {
'?$select=' + fields.join(',') + '&$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).map((item: any): IAccordionItem => ({ // 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.toValues(item[categoryField]).indexOf(categoryValue) >= 0)
.map((item: any): IAccordionItem => ({ // tslint:disable-line:no-any
id: Number(item.Id),
title: this.toText(item[titleField]),
content: this.toText(item[contentField]),
@@ -56,6 +60,27 @@ export class AccordionDataService {
})));
}
public getCategoryValues(listId: string, categoryField: string): Promise<string[]> {
if (!this.isGuid(listId) || !this.isFieldName(categoryField)) { return Promise.resolve([]); }
const url: string = this.webUrl + "/_api/web/lists(guid'" + this.cleanGuid(listId) + "')/items" +
'?$select=' + categoryField + '&$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 } = {};
const items: any[] = this.results(payload); // tslint:disable-line:no-any
for (let i: number = 0; i < items.length; i++) {
const current: string[] = this.toValues(items[i][categoryField]);
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));
});
}
private get webUrl(): string { return this.context.pageContext.web.absoluteUrl.replace(/\/$/, ''); }
private get headers(): any { return { 'Accept': 'application/json;odata=verbose', 'OData-Version': '3.0' }; } // tslint:disable-line:no-any
@@ -88,6 +113,13 @@ export class AccordionDataService {
return '';
}
private toValues(value: any): string[] { // tslint:disable-line:no-any
if (value === undefined || value === null) { return []; }
const source: any[] = value && Array.isArray(value.results) ? value.results : [value]; // tslint:disable-line:no-any
return source.map((entry: any): string => this.toText(entry).trim()) // tslint:disable-line:no-any
.filter((entry: string): boolean => !!entry);
}
private isSupportedField(type: string): boolean {
return ['Text', 'Note', 'Choice', 'MultiChoice', 'Number', 'Integer', 'Boolean', 'DateTime', 'Computed'].indexOf(String(type)) >= 0;
}
@@ -95,4 +127,3 @@ export class AccordionDataService {
private isGuid(value: string): boolean { return /^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(this.cleanGuid(value)); }
private cleanGuid(value: string): string { return String(value || '').replace(/[{}]/g, '').toLowerCase(); }
}
@@ -20,6 +20,7 @@
"titleField": "Title",
"contentField": "",
"categoryField": "",
"selectedCategory": "",
"sortField": "Title",
"sortAscending": true,
"allowMultiple": false,
+48 -2
View File
@@ -25,6 +25,7 @@ export interface IAccordionWebPartProps {
titleField: string;
contentField: string;
categoryField: string;
selectedCategory: string;
sortField: string;
sortAscending: boolean;
allowMultiple: boolean;
@@ -36,15 +37,19 @@ export default class AccordionWebPart extends BaseClientSideWebPart<IAccordionWe
private service: AccordionDataService;
private lists: IAccordionListOption[] = [];
private fields: IAccordionFieldOption[] = [];
private categories: string[] = [];
private loadingLists: boolean = false;
private loadingFields: boolean = false;
private loadingCategories: boolean = false;
private requestVersion: number = 0;
private disposed: boolean = false;
protected onInit(): Promise<void> {
this.service = new AccordionDataService(this.context);
return super.onInit().then((): Promise<void> => this.loadLists())
.then((): Promise<void> => this.properties.listId ? this.loadFields(this.properties.listId) : Promise.resolve());
.then((): Promise<void> => this.properties.listId ? this.loadFields(this.properties.listId) : Promise.resolve())
.then((): Promise<void> => this.properties.listId && this.properties.categoryField
? this.loadCategories(this.properties.listId, this.properties.categoryField) : Promise.resolve());
}
public render(): void {
@@ -60,7 +65,8 @@ export default class AccordionWebPart extends BaseClientSideWebPart<IAccordionWe
this.properties.contentField,
this.properties.categoryField || '',
this.properties.sortField || this.properties.titleField,
this.properties.sortAscending !== false
this.properties.sortAscending !== false,
this.properties.selectedCategory || ''
).then((items: IAccordionItem[]): void => {
if (this.disposed || version !== this.requestVersion) { return; }
if (!items.length) { this.renderStatus(strings.EmptyMessage, false); return; }
@@ -94,6 +100,9 @@ export default class AccordionWebPart extends BaseClientSideWebPart<IAccordionWe
if (this.properties.listId && !this.fields.length && !this.loadingFields) {
this.loadFields(this.properties.listId);
}
if (this.properties.listId && this.properties.categoryField && !this.categories.length && !this.loadingCategories) {
this.loadCategories(this.properties.listId, this.properties.categoryField);
}
}
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void { // tslint:disable-line:no-any
@@ -102,10 +111,17 @@ export default class AccordionWebPart extends BaseClientSideWebPart<IAccordionWe
this.properties.titleField = '';
this.properties.contentField = '';
this.properties.categoryField = '';
this.properties.selectedCategory = '';
this.properties.sortField = '';
this.fields = [];
this.categories = [];
this.context.propertyPane.refresh();
this.loadFields(String(newValue || '')).then((): void => this.render());
} else if (propertyPath === 'categoryField' && newValue !== oldValue) {
this.properties.selectedCategory = '';
this.categories = [];
this.context.propertyPane.refresh();
this.loadCategories(this.properties.listId, String(newValue || '')).then((): void => this.render());
}
}
@@ -114,8 +130,12 @@ export default class AccordionWebPart extends BaseClientSideWebPart<IAccordionWe
key: field.internalName, text: field.title + ' (' + field.internalName + ')'
}));
const categoryOptions: IPropertyPaneDropdownOption[] = [{ key: '', text: strings.NoCategoryOption }];
const categoryValueOptions: IPropertyPaneDropdownOption[] = [{ key: '', text: strings.AllCategoriesOption }];
const listOptions: IPropertyPaneDropdownOption[] = [{ key: '', text: strings.SelectOption }];
for (let i: number = 0; i < fieldOptions.length; i++) { categoryOptions.push(fieldOptions[i]); }
for (let i: number = 0; i < this.categories.length; i++) {
categoryValueOptions.push({ key: this.categories[i], text: this.categories[i] });
}
for (let i: number = 0; i < this.lists.length; i++) {
listOptions.push({ key: this.lists[i].id, text: this.lists[i].title });
}
@@ -142,6 +162,10 @@ export default class AccordionWebPart extends BaseClientSideWebPart<IAccordionWe
label: strings.CategoryFieldLabel, options: categoryOptions,
disabled: !this.properties.listId || this.loadingFields
}),
PropertyPaneDropdown('selectedCategory', {
label: strings.CategoryFilterLabel, options: categoryValueOptions,
disabled: !this.properties.categoryField || this.loadingCategories
}),
PropertyPaneDropdown('sortField', {
label: strings.SortFieldLabel, options: fieldOptions,
disabled: !this.properties.listId || this.loadingFields
@@ -200,6 +224,28 @@ export default class AccordionWebPart extends BaseClientSideWebPart<IAccordionWe
});
}
private loadCategories(listId: string, categoryField: string): Promise<void> {
if (!listId || !categoryField) {
this.categories = [];
this.loadingCategories = false;
this.refreshPropertyPane();
return Promise.resolve();
}
this.loadingCategories = true;
this.refreshPropertyPane();
return this.service.getCategoryValues(listId, categoryField).then((categories: string[]): void => {
this.categories = categories;
if (categories.indexOf(this.properties.selectedCategory) < 0) { this.properties.selectedCategory = ''; }
}).catch((error: Error): void => {
this.categories = [];
this.properties.selectedCategory = '';
if (window.console) { console.error('[Accordion] Categories could not be loaded.', error); }
}).then((): void => {
this.loadingCategories = false;
this.refreshPropertyPane();
});
}
private applyFieldDefaults(): void {
const names: string[] = this.fields.map((field: IAccordionFieldOption): string => field.internalName);
if (names.indexOf(this.properties.titleField) < 0) {
+2 -1
View File
@@ -7,6 +7,7 @@ define([], function() {
"TitleFieldLabel": "Spalte für Überschrift",
"ContentFieldLabel": "Spalte für Inhalt",
"CategoryFieldLabel": "Spalte für Kategorie (optional)",
"CategoryFilterLabel": "Angezeigte Kategorie",
"SortFieldLabel": "Sortierspalte",
"SortAscendingLabel": "Aufsteigend sortieren",
"AllowMultipleLabel": "Mehrere geöffnete Bereiche erlauben",
@@ -17,6 +18,7 @@ define([], function() {
"ShowCategoriesLabel": "Kategorieüberschriften anzeigen",
"SelectOption": "Auswählen...",
"NoCategoryOption": "Keine Kategorien",
"AllCategoriesOption": "Alle Kategorien",
"ConfigureMessage": "Konfigurieren Sie in den WebPart-Einstellungen eine Liste sowie Überschrift- und Inhaltsspalte.",
"LoadingMessage": "Akkordeon wird geladen...",
"EmptyMessage": "Es wurden keine Einträge gefunden.",
@@ -26,4 +28,3 @@ define([], function() {
"DefaultCategory": "Weitere"
};
});
+2 -1
View File
@@ -7,6 +7,7 @@ define([], function() {
"TitleFieldLabel": "Heading column",
"ContentFieldLabel": "Content column",
"CategoryFieldLabel": "Category column (optional)",
"CategoryFilterLabel": "Displayed category",
"SortFieldLabel": "Sort column",
"SortAscendingLabel": "Sort ascending",
"AllowMultipleLabel": "Allow multiple open sections",
@@ -17,6 +18,7 @@ define([], function() {
"ShowCategoriesLabel": "Show category headings",
"SelectOption": "Select...",
"NoCategoryOption": "No categories",
"AllCategoriesOption": "All categories",
"ConfigureMessage": "Configure a list, heading column and content column in the web part settings.",
"LoadingMessage": "Loading accordion...",
"EmptyMessage": "No entries were found.",
@@ -26,4 +28,3 @@ define([], function() {
"DefaultCategory": "Other"
};
});
+2
View File
@@ -6,6 +6,7 @@ declare interface IAccordionWebPartStrings {
TitleFieldLabel: string;
ContentFieldLabel: string;
CategoryFieldLabel: string;
CategoryFilterLabel: string;
SortFieldLabel: string;
SortAscendingLabel: string;
AllowMultipleLabel: string;
@@ -16,6 +17,7 @@ declare interface IAccordionWebPartStrings {
ShowCategoriesLabel: string;
SelectOption: string;
NoCategoryOption: string;
AllCategoriesOption: string;
ConfigureMessage: string;
LoadingMessage: string;
EmptyMessage: string;
+3
View File
@@ -27,6 +27,9 @@ assert(!!config.bundles['accordion-web-part'], 'Accordion bundle is missing.');
assert(webPart.indexOf("PropertyPaneDropdown('listId'") >= 0, 'Dynamic list selection is missing.');
assert(webPart.indexOf("PropertyPaneDropdown('contentField'") >= 0, 'Dynamic content field selection is missing.');
assert(webPart.indexOf("PropertyPaneDropdown('categoryField'") >= 0, 'Category field selection is missing.');
assert(webPart.indexOf("PropertyPaneDropdown('selectedCategory'") >= 0, 'Category value filter is missing.');
assert(read('src/webparts/accordion/AccordionDataService.ts').indexOf('getCategoryValues') >= 0,
'Dynamic category values are missing.');
assert(component.indexOf("aria-expanded={open}") >= 0 && component.indexOf("role='region'") >= 0,
'Accessible accordion semantics are missing.');
assert(component.indexOf('sanitizeAccordionHtml(item.content)') >= 0, 'Rich text is not sanitized before rendering.');