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 - Auswahl einer nicht ausgeblendeten Liste aus dem aktuellen Web
- dynamische Auswahl von Überschrift-, Inhalt-, Kategorie- und Sortierspalte - dynamische Auswahl von Überschrift-, Inhalt-, Kategorie- und Sortierspalte
- optionales Gruppieren nach Kategorien - optionales Gruppieren nach Kategorien
- Filter auf eine konkrete, aktuell in der Liste verwendete Kategorie
- 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
@@ -28,8 +29,10 @@ Es wird keine fachliche Liste provisioniert. Eine einfache FAQ-Liste kann beispi
| `Category` | Auswahl oder Text | optionale Gruppierung | | `Category` | Auswahl oder Text | optionale Gruppierung |
| `SortOrder` | Zahl | optionale Sortierung | | `SortOrder` | Zahl | optionale Sortierung |
Nach dem Hinzufügen des WebParts werden Liste und Spalten im Property Pane ausgewählt. Änderungen an den Nach dem Hinzufügen des WebParts werden Liste und Spalten im Property Pane ausgewählt. Sobald eine
Listeneinträgen erscheinen beim nächsten Laden beziehungsweise Rendern der Seite automatisch. 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 ## Version und Build
@@ -59,4 +62,3 @@ werden entfernt.
Die fachliche Vorlage ist das PnP Community Sample: Die fachliche Vorlage ist das PnP Community Sample:
https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-accordion-dynamic-section 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", "version": "1.0.0",
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
+34 -3
View File
@@ -36,7 +36,8 @@ export class AccordionDataService {
contentField: string, contentField: string,
categoryField: string, categoryField: string,
sortField: string, sortField: string,
sortAscending: boolean sortAscending: boolean,
categoryValue: string
): Promise<IAccordionItem[]> { ): Promise<IAccordionItem[]> {
if (!this.isGuid(listId)) { return Promise.reject(new Error('Invalid list ID.')); } if (!this.isGuid(listId)) { return Promise.reject(new Error('Invalid list ID.')); }
const fields: string[] = this.uniqueFields(['Id', titleField, contentField, categoryField]); 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'; '?$select=' + fields.join(',') + '&$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).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), id: Number(item.Id),
title: this.toText(item[titleField]), title: this.toText(item[titleField]),
content: this.toText(item[contentField]), 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 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 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 ''; 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 { private isSupportedField(type: string): boolean {
return ['Text', 'Note', 'Choice', 'MultiChoice', 'Number', 'Integer', 'Boolean', 'DateTime', 'Computed'].indexOf(String(type)) >= 0; 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 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(); } private cleanGuid(value: string): string { return String(value || '').replace(/[{}]/g, '').toLowerCase(); }
} }
@@ -20,6 +20,7 @@
"titleField": "Title", "titleField": "Title",
"contentField": "", "contentField": "",
"categoryField": "", "categoryField": "",
"selectedCategory": "",
"sortField": "Title", "sortField": "Title",
"sortAscending": true, "sortAscending": true,
"allowMultiple": false, "allowMultiple": false,
+48 -2
View File
@@ -25,6 +25,7 @@ export interface IAccordionWebPartProps {
titleField: string; titleField: string;
contentField: string; contentField: string;
categoryField: string; categoryField: string;
selectedCategory: string;
sortField: string; sortField: string;
sortAscending: boolean; sortAscending: boolean;
allowMultiple: boolean; allowMultiple: boolean;
@@ -36,15 +37,19 @@ export default class AccordionWebPart extends BaseClientSideWebPart<IAccordionWe
private service: AccordionDataService; private service: AccordionDataService;
private lists: IAccordionListOption[] = []; private lists: IAccordionListOption[] = [];
private fields: IAccordionFieldOption[] = []; private fields: IAccordionFieldOption[] = [];
private categories: string[] = [];
private loadingLists: boolean = false; private loadingLists: boolean = false;
private loadingFields: boolean = false; private loadingFields: boolean = false;
private loadingCategories: boolean = false;
private requestVersion: number = 0; private requestVersion: number = 0;
private disposed: boolean = false; private disposed: boolean = false;
protected onInit(): Promise<void> { protected onInit(): Promise<void> {
this.service = new AccordionDataService(this.context); this.service = new AccordionDataService(this.context);
return super.onInit().then((): Promise<void> => this.loadLists()) 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 { public render(): void {
@@ -60,7 +65,8 @@ export default class AccordionWebPart extends BaseClientSideWebPart<IAccordionWe
this.properties.contentField, this.properties.contentField,
this.properties.categoryField || '', this.properties.categoryField || '',
this.properties.sortField || this.properties.titleField, this.properties.sortField || this.properties.titleField,
this.properties.sortAscending !== false this.properties.sortAscending !== false,
this.properties.selectedCategory || ''
).then((items: IAccordionItem[]): void => { ).then((items: IAccordionItem[]): void => {
if (this.disposed || version !== this.requestVersion) { return; } if (this.disposed || version !== this.requestVersion) { return; }
if (!items.length) { this.renderStatus(strings.EmptyMessage, false); 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) { if (this.properties.listId && !this.fields.length && !this.loadingFields) {
this.loadFields(this.properties.listId); 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 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.titleField = '';
this.properties.contentField = ''; this.properties.contentField = '';
this.properties.categoryField = ''; this.properties.categoryField = '';
this.properties.selectedCategory = '';
this.properties.sortField = ''; this.properties.sortField = '';
this.fields = []; this.fields = [];
this.categories = [];
this.context.propertyPane.refresh(); this.context.propertyPane.refresh();
this.loadFields(String(newValue || '')).then((): void => this.render()); 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 + ')' key: field.internalName, text: field.title + ' (' + field.internalName + ')'
})); }));
const categoryOptions: IPropertyPaneDropdownOption[] = [{ key: '', text: strings.NoCategoryOption }]; const categoryOptions: IPropertyPaneDropdownOption[] = [{ key: '', text: strings.NoCategoryOption }];
const categoryValueOptions: IPropertyPaneDropdownOption[] = [{ key: '', text: strings.AllCategoriesOption }];
const listOptions: IPropertyPaneDropdownOption[] = [{ key: '', text: strings.SelectOption }]; 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 < 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++) { for (let i: number = 0; i < this.lists.length; i++) {
listOptions.push({ key: this.lists[i].id, text: this.lists[i].title }); 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, label: strings.CategoryFieldLabel, options: categoryOptions,
disabled: !this.properties.listId || this.loadingFields disabled: !this.properties.listId || this.loadingFields
}), }),
PropertyPaneDropdown('selectedCategory', {
label: strings.CategoryFilterLabel, options: categoryValueOptions,
disabled: !this.properties.categoryField || this.loadingCategories
}),
PropertyPaneDropdown('sortField', { PropertyPaneDropdown('sortField', {
label: strings.SortFieldLabel, options: fieldOptions, label: strings.SortFieldLabel, options: fieldOptions,
disabled: !this.properties.listId || this.loadingFields 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 { private applyFieldDefaults(): void {
const names: string[] = this.fields.map((field: IAccordionFieldOption): string => field.internalName); const names: string[] = this.fields.map((field: IAccordionFieldOption): string => field.internalName);
if (names.indexOf(this.properties.titleField) < 0) { if (names.indexOf(this.properties.titleField) < 0) {
+2 -1
View File
@@ -7,6 +7,7 @@ define([], function() {
"TitleFieldLabel": "Spalte für Überschrift", "TitleFieldLabel": "Spalte für Überschrift",
"ContentFieldLabel": "Spalte für Inhalt", "ContentFieldLabel": "Spalte für Inhalt",
"CategoryFieldLabel": "Spalte für Kategorie (optional)", "CategoryFieldLabel": "Spalte für Kategorie (optional)",
"CategoryFilterLabel": "Angezeigte Kategorie",
"SortFieldLabel": "Sortierspalte", "SortFieldLabel": "Sortierspalte",
"SortAscendingLabel": "Aufsteigend sortieren", "SortAscendingLabel": "Aufsteigend sortieren",
"AllowMultipleLabel": "Mehrere geöffnete Bereiche erlauben", "AllowMultipleLabel": "Mehrere geöffnete Bereiche erlauben",
@@ -17,6 +18,7 @@ define([], function() {
"ShowCategoriesLabel": "Kategorieüberschriften anzeigen", "ShowCategoriesLabel": "Kategorieüberschriften anzeigen",
"SelectOption": "Auswählen...", "SelectOption": "Auswählen...",
"NoCategoryOption": "Keine Kategorien", "NoCategoryOption": "Keine Kategorien",
"AllCategoriesOption": "Alle Kategorien",
"ConfigureMessage": "Konfigurieren Sie in den WebPart-Einstellungen eine Liste sowie Überschrift- und Inhaltsspalte.", "ConfigureMessage": "Konfigurieren Sie in den WebPart-Einstellungen eine Liste sowie Überschrift- und Inhaltsspalte.",
"LoadingMessage": "Akkordeon wird geladen...", "LoadingMessage": "Akkordeon wird geladen...",
"EmptyMessage": "Es wurden keine Einträge gefunden.", "EmptyMessage": "Es wurden keine Einträge gefunden.",
@@ -26,4 +28,3 @@ define([], function() {
"DefaultCategory": "Weitere" "DefaultCategory": "Weitere"
}; };
}); });
+2 -1
View File
@@ -7,6 +7,7 @@ define([], function() {
"TitleFieldLabel": "Heading column", "TitleFieldLabel": "Heading column",
"ContentFieldLabel": "Content column", "ContentFieldLabel": "Content column",
"CategoryFieldLabel": "Category column (optional)", "CategoryFieldLabel": "Category column (optional)",
"CategoryFilterLabel": "Displayed category",
"SortFieldLabel": "Sort column", "SortFieldLabel": "Sort column",
"SortAscendingLabel": "Sort ascending", "SortAscendingLabel": "Sort ascending",
"AllowMultipleLabel": "Allow multiple open sections", "AllowMultipleLabel": "Allow multiple open sections",
@@ -17,6 +18,7 @@ define([], function() {
"ShowCategoriesLabel": "Show category headings", "ShowCategoriesLabel": "Show category headings",
"SelectOption": "Select...", "SelectOption": "Select...",
"NoCategoryOption": "No categories", "NoCategoryOption": "No categories",
"AllCategoriesOption": "All categories",
"ConfigureMessage": "Configure a list, heading column and content column in the web part settings.", "ConfigureMessage": "Configure a list, heading column and content column in the web part settings.",
"LoadingMessage": "Loading accordion...", "LoadingMessage": "Loading accordion...",
"EmptyMessage": "No entries were found.", "EmptyMessage": "No entries were found.",
@@ -26,4 +28,3 @@ define([], function() {
"DefaultCategory": "Other" "DefaultCategory": "Other"
}; };
}); });
+2
View File
@@ -6,6 +6,7 @@ declare interface IAccordionWebPartStrings {
TitleFieldLabel: string; TitleFieldLabel: string;
ContentFieldLabel: string; ContentFieldLabel: string;
CategoryFieldLabel: string; CategoryFieldLabel: string;
CategoryFilterLabel: string;
SortFieldLabel: string; SortFieldLabel: string;
SortAscendingLabel: string; SortAscendingLabel: string;
AllowMultipleLabel: string; AllowMultipleLabel: string;
@@ -16,6 +17,7 @@ declare interface IAccordionWebPartStrings {
ShowCategoriesLabel: string; ShowCategoriesLabel: string;
SelectOption: string; SelectOption: string;
NoCategoryOption: string; NoCategoryOption: string;
AllCategoriesOption: string;
ConfigureMessage: string; ConfigureMessage: string;
LoadingMessage: string; LoadingMessage: string;
EmptyMessage: 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('listId'") >= 0, 'Dynamic list selection is missing.');
assert(webPart.indexOf("PropertyPaneDropdown('contentField'") >= 0, 'Dynamic content field 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('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, assert(component.indexOf("aria-expanded={open}") >= 0 && component.indexOf("role='region'") >= 0,
'Accessible accordion semantics are missing.'); 'Accessible accordion semantics are missing.');
assert(component.indexOf('sanitizeAccordionHtml(item.content)') >= 0, 'Rich text is not sanitized before rendering.'); assert(component.indexOf('sanitizeAccordionHtml(item.content)') >= 0, 'Rich text is not sanitized before rendering.');