import * as React from 'react'; import * as ReactDom from 'react-dom'; import { Version } from '@microsoft/sp-core-library'; import { BaseClientSideWebPart, IPropertyPaneConfiguration, IPropertyPaneDropdownOption, PropertyPaneDropdown, PropertyPaneTextField, PropertyPaneToggle } from '@microsoft/sp-webpart-base'; import * as strings from 'AccordionWebPartStrings'; import Accordion from './components/Accordion'; import { IAccordionProps } from './components/IAccordionProps'; import { AccordionDataService } from './AccordionDataService'; import { AccordionInitialState, IAccordionFieldOption, IAccordionItem, IAccordionListOption } from './AccordionTypes'; import styles from './components/Accordion.module.scss'; export interface IAccordionWebPartProps { title: string; listId: string; titleField: string; contentField: string; categoryField: string; selectedCategory: string; sortField: string; sortAscending: boolean; allowMultiple: boolean; initialState: AccordionInitialState; showCategories: boolean; } export default class AccordionWebPart extends BaseClientSideWebPart { 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 { 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.properties.categoryField ? this.loadCategories(this.properties.listId, this.properties.categoryField) : Promise.resolve()); } public render(): void { const version: number = ++this.requestVersion; if (!this.isConfigured()) { this.renderStatus(strings.ConfigureMessage, false); return; } this.renderStatus(strings.LoadingMessage, false); this.service.getItems( this.properties.listId, this.properties.titleField, this.properties.contentField, this.properties.categoryField || '', this.properties.sortField || this.properties.titleField, this.properties.sortAscending !== false, this.properties.selectedCategory || '', this.getFieldType(this.properties.categoryField) ).then((items: IAccordionItem[]): void => { if (this.disposed || version !== this.requestVersion) { return; } if (!items.length) { this.renderStatus(strings.EmptyMessage, false); return; } const element: React.ReactElement = React.createElement(Accordion, { items: items, title: String(this.properties.title || '').trim(), allowMultiple: this.properties.allowMultiple === true, initialState: this.normalizeInitialState(this.properties.initialState), showCategories: this.properties.showCategories !== false && !!this.properties.categoryField, expandLabel: strings.ExpandLabel, collapseLabel: strings.CollapseLabel, defaultCategory: strings.DefaultCategory }); ReactDom.render(element, this.domElement); }).catch((error: Error): void => { if (this.disposed || version !== this.requestVersion) { return; } if (window.console) { console.error('[Accordion] ' + strings.LoadErrorMessage, error); } this.renderStatus(strings.LoadErrorMessage, true); }); } protected onDispose(): void { this.disposed = true; this.requestVersion++; ReactDom.unmountComponentAtNode(this.domElement); } protected get dataVersion(): Version { return Version.parse('1.0'); } protected onPropertyPaneConfigurationStart(): void { if (!this.lists.length && !this.loadingLists) { this.loadLists(); } 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 super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue); if (propertyPath === 'listId' && newValue !== oldValue) { 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()); } } protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { 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 < 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] }); } for (let i: number = 0; i < this.lists.length; i++) { listOptions.push({ key: this.lists[i].id, text: this.lists[i].title }); } return { pages: [{ header: { description: strings.PropertyPaneDescription }, groups: [{ groupName: strings.DataGroupName, groupFields: [ PropertyPaneTextField('title', { label: strings.WebPartTitleFieldLabel }), PropertyPaneDropdown('listId', { label: strings.ListFieldLabel, options: listOptions, disabled: this.loadingLists }), PropertyPaneDropdown('titleField', { label: strings.TitleFieldLabel, options: fieldOptions, disabled: !this.properties.listId || this.loadingFields }), PropertyPaneDropdown('contentField', { label: strings.ContentFieldLabel, options: fieldOptions, disabled: !this.properties.listId || this.loadingFields }), PropertyPaneDropdown('categoryField', { 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 }), PropertyPaneToggle('sortAscending', { label: strings.SortAscendingLabel }) ] }, { groupName: strings.BehaviorGroupName, groupFields: [ PropertyPaneDropdown('initialState', { label: strings.InitialStateFieldLabel, options: [ { key: 'collapsed', text: strings.InitialCollapsed }, { key: 'first', text: strings.InitialFirst }, { key: 'all', text: strings.InitialAll } ] }), PropertyPaneToggle('allowMultiple', { label: strings.AllowMultipleLabel }), PropertyPaneToggle('showCategories', { label: strings.ShowCategoriesLabel, disabled: !this.properties.categoryField }) ] }] }] }; } private loadLists(): Promise { this.loadingLists = true; this.refreshPropertyPane(); return this.service.getLists().then((lists: IAccordionListOption[]): void => { this.lists = lists; }).catch((error: Error): void => { this.lists = []; if (window.console) { console.error('[Accordion] Lists could not be loaded.', error); } }).then((): void => { this.loadingLists = false; this.refreshPropertyPane(); }); } private loadFields(listId: string): Promise { if (!listId) { this.fields = []; return Promise.resolve(); } this.loadingFields = true; this.refreshPropertyPane(); return this.service.getFields(listId).then((fields: IAccordionFieldOption[]): void => { this.fields = fields; this.applyFieldDefaults(); }).catch((error: Error): void => { this.fields = []; if (window.console) { console.error('[Accordion] Fields could not be loaded.', error); } }).then((): void => { this.loadingFields = false; this.refreshPropertyPane(); }); } private loadCategories(listId: string, categoryField: string): Promise { if (!listId || !categoryField) { this.categories = []; this.loadingCategories = false; this.refreshPropertyPane(); return Promise.resolve(); } this.loadingCategories = true; this.refreshPropertyPane(); return this.service.getCategoryValues(listId, categoryField, this.getFieldType(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 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 = 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 (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 && !this.isTaxonomyField(this.fields[i])) { return this.fields[i].internalName; } } return ''; } private isTaxonomyField(field: IAccordionFieldOption): boolean { return field.type === 'TaxonomyFieldType' || field.type === 'TaxonomyFieldTypeMulti'; } private getFieldType(internalName: string): string { for (let i: number = 0; i < this.fields.length; i++) { if (this.fields[i].internalName === internalName) { return this.fields[i].type; } } return ''; } private isConfigured(): boolean { return !!(this.service && this.properties.listId && this.properties.titleField && this.properties.contentField); } private normalizeInitialState(value: string): AccordionInitialState { return value === 'first' || value === 'all' ? value : 'collapsed'; } private refreshPropertyPane(): void { if (this.context && this.context.propertyPane) { this.context.propertyPane.refresh(); } } private renderStatus(message: string, error: boolean): void { ReactDom.unmountComponentAtNode(this.domElement); const status: HTMLDivElement = document.createElement('div'); status.className = error ? styles.error : styles.status; status.setAttribute('role', error ? 'alert' : 'status'); status.textContent = message; this.domElement.innerHTML = ''; this.domElement.appendChild(status); } }