diff --git a/README.md b/README.md index a7d3fac..fe1f46e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Accordion -Accordion 1.0.0 ist ein listenbasiertes SPFx-WebPart für SharePoint Server Subscription Edition. Es orientiert +Accordion 1.1.0 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. @@ -10,6 +10,7 @@ kompatible SPFx-1.4.1-/React-15-Toolchain. - dynamische Auswahl von Überschrift-, Inhalt-, Kategorie- und Sortierspalte - optionales Gruppieren nach Kategorien - Filter auf eine konkrete, aktuell in der Liste verwendete Kategorie +- ein- und mehrwertige Managed-Metadata-Spalten als Kategorien - 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 @@ -26,7 +27,7 @@ Es wird keine fachliche Liste provisioniert. Eine einfache FAQ-Liste kann beispi |---|---|---| | `Title` | Eine Textzeile | Überschrift | | `Content` | Mehrere Textzeilen mit Rich Text | Inhalt | -| `Category` | Auswahl oder Text | optionale Gruppierung | +| `Category` | Auswahl, Text oder Managed Metadata | optionale Gruppierung | | `SortOrder` | Zahl | optionale Sortierung | Nach dem Hinzufügen des WebParts werden Liste und Spalten im Property Pane ausgewählt. Sobald eine @@ -38,7 +39,7 @@ nächsten Laden beziehungsweise Rendern der Seite automatisch. | Eigenschaft | Wert | |---|---| -| Version | 1.0.0 | +| Version | 1.1.0 | | SharePoint Framework | 1.4.1 | | React | 15.6.2 | | Node.js | 8.17.0 | diff --git a/config/package-solution.json b/config/package-solution.json index 35711e1..dc1838a 100644 --- a/config/package-solution.json +++ b/config/package-solution.json @@ -4,7 +4,7 @@ "name": "accordion-web-part", "title": "accordion-web-part", "id": "27a6fd90-42a1-4e39-8862-08ed0e7803bc", - "version": "1.0.0.0", + "version": "1.1.0.0", "includeClientSideAssets": true, "skipFeatureDeployment": true }, diff --git a/package.json b/package.json index 2488b26..475b05d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "accordion-web-part", - "version": "1.0.0", + "version": "1.1.0", "private": true, "main": "lib/index.js", "engines": { @@ -9,7 +9,7 @@ "scripts": { "build": "gulp bundle", "clean": "gulp clean", - "test": "tsc --noEmit -p . && node tests/validate-project.js", + "test": "tsc -p tests/tsconfig.json && node temp/tests/tests/AccordionCategoryValues.test.js && tsc --noEmit -p . && node tests/validate-project.js", "package": "npm test && gulp clean && gulp bundle --ship && gulp package-solution --ship" }, "dependencies": { diff --git a/src/webparts/accordion/AccordionCategoryValues.ts b/src/webparts/accordion/AccordionCategoryValues.ts new file mode 100644 index 0000000..3b7d15a --- /dev/null +++ b/src/webparts/accordion/AccordionCategoryValues.ts @@ -0,0 +1,46 @@ +export function getAccordionCategoryValues(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 + const values: string[] = []; + const keys: { [key: string]: boolean } = {}; + for (let i: number = 0; i < source.length; i++) { + const current: string[] = categoryEntryValues(source[i]); + for (let valueIndex: number = 0; valueIndex < current.length; valueIndex++) { + const text: string = current[valueIndex].trim(); + const key: string = '$' + text.toLocaleLowerCase(); + if (text && !keys[key]) { keys[key] = true; values.push(text); } + } + } + return values; +} + +function categoryEntryValues(entry: any): string[] { // tslint:disable-line:no-any + if (entry === undefined || entry === null) { return []; } + if (entry.Label) { return [String(entry.Label)]; } + if (typeof entry !== 'string') { + if (typeof entry === 'number' || typeof entry === 'boolean') { return [String(entry)]; } + if (entry.Description || entry.Url) { return [String(entry.Description || entry.Url)]; } + return []; + } + + const candidate: string = entry.trim(); + if (!candidate) { return []; } + let parts: string[] = [candidate]; + if (candidate.indexOf(';#') >= 0) { + parts = candidate.split(';#'); + } else if (candidate.indexOf(';') >= 0) { + 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) { parts = semicolonParts; } + } + + const values: string[] = []; + for (let i: number = 0; i < parts.length; i++) { + const part: string = parts[i].trim(); + if (!part || /^-?\d+$/.test(part)) { continue; } + const taxonomyMatch: RegExpMatchArray = part.match(/^(.*)\|[0-9a-f]{8}-[0-9a-f-]{27}$/i); + values.push(taxonomyMatch ? taxonomyMatch[1].trim() : part); + } + return values.filter((text: string): boolean => !!text); +} diff --git a/src/webparts/accordion/AccordionDataService.ts b/src/webparts/accordion/AccordionDataService.ts index 0d05a4b..fe61bce 100644 --- a/src/webparts/accordion/AccordionDataService.ts +++ b/src/webparts/accordion/AccordionDataService.ts @@ -1,5 +1,6 @@ import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http'; import { WebPartContext } from '@microsoft/sp-webpart-base'; +import { getAccordionCategoryValues } from './AccordionCategoryValues'; import { IAccordionFieldOption, IAccordionItem, IAccordionListOption } from './AccordionTypes'; export class AccordionDataService { @@ -51,13 +52,16 @@ export class AccordionDataService { .then((response: SPHttpClientResponse): Promise => 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.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]) : '' - }))); + getAccordionCategoryValues(item[categoryField]).indexOf(categoryValue) >= 0) + .map((item: any): IAccordionItem => { // tslint:disable-line:no-any + const categories: string[] = categoryField ? getAccordionCategoryValues(item[categoryField]) : []; + return { + id: Number(item.Id), + title: this.toText(item[titleField]), + content: this.toText(item[contentField]), + categories: categories + }; + })); } public getCategoryValues(listId: string, categoryField: string): Promise { @@ -71,9 +75,9 @@ export class AccordionDataService { 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]); + const current: string[] = getAccordionCategoryValues(items[i][categoryField]); 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]); } } } @@ -113,15 +117,9 @@ 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; + return ['Text', 'Note', 'Choice', 'MultiChoice', 'Number', 'Integer', 'Boolean', 'DateTime', 'Computed', + 'TaxonomyFieldType', 'TaxonomyFieldTypeMulti'].indexOf(String(type)) >= 0; } private isFieldName(value: string): boolean { return /^[A-Za-z_][A-Za-z0-9_]*$/.test(String(value || '')); } private isGuid(value: string): boolean { return /^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(this.cleanGuid(value)); } diff --git a/src/webparts/accordion/AccordionTypes.ts b/src/webparts/accordion/AccordionTypes.ts index 944a83f..2d03edd 100644 --- a/src/webparts/accordion/AccordionTypes.ts +++ b/src/webparts/accordion/AccordionTypes.ts @@ -15,6 +15,5 @@ export interface IAccordionItem { id: number; title: string; content: string; - category: string; + categories: string[]; } - diff --git a/src/webparts/accordion/AccordionWebPart.ts b/src/webparts/accordion/AccordionWebPart.ts index e018c1a..8fad6cc 100644 --- a/src/webparts/accordion/AccordionWebPart.ts +++ b/src/webparts/accordion/AccordionWebPart.ts @@ -126,13 +126,18 @@ export default class AccordionWebPart extends BaseClientSideWebPart ({ + const standardFields: IAccordionFieldOption[] = this.fields.filter((field: IAccordionFieldOption): boolean => + !this.isTaxonomyField(field)); + const fieldOptions: IPropertyPaneDropdownOption[] = standardFields.map((field: IAccordionFieldOption): IPropertyPaneDropdownOption => ({ + key: field.internalName, text: field.title + ' (' + field.internalName + ')' + })); + const categoryFieldOptions: IPropertyPaneDropdownOption[] = this.fields.map((field: IAccordionFieldOption): IPropertyPaneDropdownOption => ({ 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 < categoryFieldOptions.length; i++) { categoryOptions.push(categoryFieldOptions[i]); } for (let i: number = 0; i < this.categories.length; i++) { categoryValueOptions.push({ key: this.categories[i], text: this.categories[i] }); } @@ -247,26 +252,35 @@ export default class AccordionWebPart extends BaseClientSideWebPart field.internalName); + const standardFields: IAccordionFieldOption[] = this.fields.filter((field: IAccordionFieldOption): boolean => + !this.isTaxonomyField(field)); + const names: string[] = standardFields.map((field: IAccordionFieldOption): string => field.internalName); + const categoryNames: string[] = this.fields.map((field: IAccordionFieldOption): string => field.internalName); if (names.indexOf(this.properties.titleField) < 0) { this.properties.titleField = names.indexOf('Title') >= 0 ? 'Title' : (names[0] || ''); } if (names.indexOf(this.properties.contentField) < 0) { - const note: IAccordionFieldOption = this.fields.filter((field: IAccordionFieldOption): boolean => + const note: IAccordionFieldOption = standardFields.filter((field: IAccordionFieldOption): boolean => field.type === 'Note' && field.internalName !== this.properties.titleField)[0]; this.properties.contentField = note ? note.internalName : this.firstDifferentField(this.properties.titleField); } - if (names.indexOf(this.properties.categoryField) < 0) { this.properties.categoryField = ''; } + if (categoryNames.indexOf(this.properties.categoryField) < 0) { this.properties.categoryField = ''; } if (names.indexOf(this.properties.sortField) < 0) { this.properties.sortField = this.properties.titleField; } } private firstDifferentField(excluded: string): string { for (let i: number = 0; i < this.fields.length; i++) { - if (this.fields[i].internalName !== excluded) { return this.fields[i].internalName; } + if (this.fields[i].internalName !== excluded && !this.isTaxonomyField(this.fields[i])) { + return this.fields[i].internalName; + } } return ''; } + private isTaxonomyField(field: IAccordionFieldOption): boolean { + return field.type === 'TaxonomyFieldType' || field.type === 'TaxonomyFieldTypeMulti'; + } + private isConfigured(): boolean { return !!(this.service && this.properties.listId && this.properties.titleField && this.properties.contentField); } diff --git a/src/webparts/accordion/components/Accordion.tsx b/src/webparts/accordion/components/Accordion.tsx index cc088de..f094b6b 100644 --- a/src/webparts/accordion/components/Accordion.tsx +++ b/src/webparts/accordion/components/Accordion.tsx @@ -28,17 +28,17 @@ export default class Accordion extends React.Component{group.name} : undefined} -
{group.items.map((item: IAccordionItem) => this.renderItem(item))}
+
{group.items.map((item: IAccordionItem) => this.renderItem(item, groupIndex))}
)} ; } - private renderItem(item: IAccordionItem): React.ReactElement { // tslint:disable-line:no-any + private renderItem(item: IAccordionItem, groupIndex: number): React.ReactElement { // tslint:disable-line:no-any const open: boolean = this.state.openIds.indexOf(item.id) >= 0; - const buttonId: string = this.instanceId + '-button-' + item.id; - const panelId: string = this.instanceId + '-panel-' + item.id; - return
+ const buttonId: string = this.instanceId + '-button-' + groupIndex + '-' + item.id; + const panelId: string = this.instanceId + '-panel-' + groupIndex + '-' + item.id; + return