diff --git a/README.md b/README.md index 500df95..89665eb 100644 --- a/README.md +++ b/README.md @@ -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 - diff --git a/package.json b/package.json index cdf146a..2488b26 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "accordion", + "name": "accordion-web-part", "version": "1.0.0", "private": true, "main": "lib/index.js", diff --git a/src/webparts/accordion/AccordionDataService.ts b/src/webparts/accordion/AccordionDataService.ts index 697c9b1..0d05a4b 100644 --- a/src/webparts/accordion/AccordionDataService.ts +++ b/src/webparts/accordion/AccordionDataService.ts @@ -36,7 +36,8 @@ export class AccordionDataService { contentField: string, categoryField: string, sortField: string, - sortAscending: boolean + sortAscending: boolean, + categoryValue: string ): Promise { if (!this.isGuid(listId)) { return Promise.reject(new Error('Invalid list ID.')); } const fields: string[] = this.uniqueFields(['Id', titleField, contentField, categoryField]); @@ -48,12 +49,36 @@ 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 => this.ensureJson(response)) // tslint:disable-line:no-any - .then((payload: any): IAccordionItem[] => this.results(payload).map((item: any): IAccordionItem => ({ // tslint:disable-line:no-any - id: Number(item.Id), - title: this.toText(item[titleField]), - content: this.toText(item[contentField]), - category: categoryField ? this.toText(item[categoryField]) : '' - }))); + .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]), + category: categoryField ? this.toText(item[categoryField]) : '' + }))); + } + + public getCategoryValues(listId: string, categoryField: string): Promise { + 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 => 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(/\/$/, ''); } @@ -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(); } } - diff --git a/src/webparts/accordion/AccordionWebPart.manifest.json b/src/webparts/accordion/AccordionWebPart.manifest.json index 3dd3fad..dc6de07 100644 --- a/src/webparts/accordion/AccordionWebPart.manifest.json +++ b/src/webparts/accordion/AccordionWebPart.manifest.json @@ -20,6 +20,7 @@ "titleField": "Title", "contentField": "", "categoryField": "", + "selectedCategory": "", "sortField": "Title", "sortAscending": true, "allowMultiple": false, diff --git a/src/webparts/accordion/AccordionWebPart.ts b/src/webparts/accordion/AccordionWebPart.ts index 309be56..e018c1a 100644 --- a/src/webparts/accordion/AccordionWebPart.ts +++ b/src/webparts/accordion/AccordionWebPart.ts @@ -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 { this.service = new AccordionDataService(this.context); return super.onInit().then((): Promise => this.loadLists()) - .then((): Promise => this.properties.listId ? this.loadFields(this.properties.listId) : Promise.resolve()); + .then((): Promise => this.properties.listId ? this.loadFields(this.properties.listId) : Promise.resolve()) + .then((): Promise => 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 { 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 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 { + 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) { diff --git a/src/webparts/accordion/loc/de-de.js b/src/webparts/accordion/loc/de-de.js index b586049..4f3cdcb 100644 --- a/src/webparts/accordion/loc/de-de.js +++ b/src/webparts/accordion/loc/de-de.js @@ -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" }; }); - diff --git a/src/webparts/accordion/loc/en-us.js b/src/webparts/accordion/loc/en-us.js index 93ecad5..565638d 100644 --- a/src/webparts/accordion/loc/en-us.js +++ b/src/webparts/accordion/loc/en-us.js @@ -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" }; }); - diff --git a/src/webparts/accordion/loc/myStrings.d.ts b/src/webparts/accordion/loc/myStrings.d.ts index 4f7cf73..416e316 100644 --- a/src/webparts/accordion/loc/myStrings.d.ts +++ b/src/webparts/accordion/loc/myStrings.d.ts @@ -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; diff --git a/tests/validate-project.js b/tests/validate-project.js index 9db5870..9e44cbc 100644 --- a/tests/validate-project.js +++ b/tests/validate-project.js @@ -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.');