diff --git a/src/ViewPicker.ts b/src/ViewPicker.ts new file mode 100644 index 000000000..408513b95 --- /dev/null +++ b/src/ViewPicker.ts @@ -0,0 +1 @@ +export * from './controls/viewPicker/index'; \ No newline at end of file diff --git a/src/common/SPEntities.ts b/src/common/SPEntities.ts index 5d5bac307..531c89acc 100644 --- a/src/common/SPEntities.ts +++ b/src/common/SPEntities.ts @@ -206,3 +206,15 @@ export interface IUploadImageResult { ServerRelativeUrl: string; UniqueId: string; } + +export interface ISPView { + Id: string; + Title: string; +} + + /** + * Defines a collection of SharePoint list views + */ +export interface ISPViews { + value: ISPView[]; +} \ No newline at end of file diff --git a/src/common/utilities/GeneralHelper.ts b/src/common/utilities/GeneralHelper.ts index ed359782d..91f4b1067 100644 --- a/src/common/utilities/GeneralHelper.ts +++ b/src/common/utilities/GeneralHelper.ts @@ -4,6 +4,7 @@ import * as strings from 'ControlStrings'; export const IMG_SUPPORTED_EXTENSIONS = ".gif,.jpg,.jpeg,.bmp,.dib,.tif,.tiff,.ico,.png,.jxr,.svg"; +import * as _ from '@microsoft/sp-lodash-subset'; /** * Helper with general methods to simplify some routines */ @@ -405,3 +406,15 @@ export function dateToNumber(date: string | number | Date): number { return dateObj.getTime(); } + +export const setPropertyValue = (properties: any, targetProperty: string, value: any): void => { // eslint-disable-line @typescript-eslint/no-explicit-any + if (!properties) { + return; + } + if (targetProperty.indexOf('.') === -1) { // simple prop + properties[targetProperty] = value; + } + else { + _.set(properties, targetProperty, value); + } +}; \ No newline at end of file diff --git a/src/controls/viewPicker/IViewPicker.ts b/src/controls/viewPicker/IViewPicker.ts new file mode 100644 index 000000000..5cd6a8b6b --- /dev/null +++ b/src/controls/viewPicker/IViewPicker.ts @@ -0,0 +1,100 @@ +import { BaseComponentContext } from '@microsoft/sp-component-base'; +import { ISPView } from "../../../src/common/SPEntities"; +import { IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; + + +/** + * Enum for specifying how the views should be sorted + */ +export enum PropertyFieldViewPickerOrderBy { + Id = 1, + Title +} + +export interface IViewPickerProps { + /** + * Context of the current web part + */ + context: BaseComponentContext; + + /** + * If provided, additional class name to provide on the dropdown element. + */ + className?: string; + + /** + * Custom Field will start to validate after users stop typing for `deferredValidationTime` milliseconds. + * Default value is 200. + */ + deferredValidationTime?: number; + + /** + * Whether the property pane field is enabled or not. + */ + disabled?: boolean; + + /** + * Filter views from Odata query + */ + filter?: string; + + /** + * Property field label displayed on top + */ + label: string; + + /** + * The List Id of the list where you want to get the views + */ + listId?: string; + + /** + * Input placeholder text. Displayed until option is selected. + */ + placeholder?: string; + + /** + * Specify the property on which you want to order the retrieve set of views. + */ + orderBy?: PropertyFieldViewPickerOrderBy; + + /** + * Initial selected view of the control + */ + selectedView?: string; + + /** + * Whether or not to show a blank option. Default false. + */ + showBlankOption?: boolean; + + /** + * Defines view titles which should be excluded from the view picker control + */ + viewsToExclude?: string[]; + + /** + * Absolute Web Url of target site (user requires permissions) + */ + webAbsoluteUrl?: string; + + + + /** + * Defines a onPropertyChange function to raise when the selected value changed. + * Normally this function must be always defined with the 'this.onPropertyChange' + * method of the web part object. + */ + onPropertyChange? : (newValue: string | string[]) => void; + /** + * Callback that is called before the dropdown is populated + */ + onViewsRetrieved?: (views: ISPView[]) => PromiseLike | ISPView[]; + +} + +export interface IViewPickerState { + results: IDropdownOption[]; + selectedKey?: string | string[]; + errorMessage?: string | string[]; +} \ No newline at end of file diff --git a/src/controls/viewPicker/ViewPicker.tsx b/src/controls/viewPicker/ViewPicker.tsx new file mode 100644 index 000000000..bcd6e3a1f --- /dev/null +++ b/src/controls/viewPicker/ViewPicker.tsx @@ -0,0 +1,177 @@ +import * as React from 'react'; +import cloneDeep from 'lodash/cloneDeep'; +import { Dropdown, IDropdownOption, IDropdownProps } from 'office-ui-fabric-react/lib/Dropdown'; +import { Async } from 'office-ui-fabric-react/lib/Utilities'; +import { Label } from 'office-ui-fabric-react/lib/Label'; +import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner'; +import * as telemetry from '../../common/telemetry'; +import { ISPService } from '../../services/ISPService'; +import { SPViewPickerService } from '../../services/SPViewPickerService'; +import { IViewPickerProps, IViewPickerState } from './IViewPicker'; +import { ISPView, ISPViews } from "../../common/SPEntities"; + + +// Empty view value +const EMPTY_VIEW_KEY = 'NO_VIEW_SELECTED'; + +export class ViewPicker extends React.Component { + private options: IDropdownOption[] = []; + private selectedKey: string| string[] = null; + private latestValidateValue: string; + private async: Async; + private delayedValidate: (value: string) => void; + + + constructor(props: IViewPickerProps){ + super(props); + + telemetry.track('ViewPicker'); + this.state = { + results: this.options + } + + this.async = new Async(this); + this.validate = this.validate.bind(this); + this.onChanged = this.onChanged.bind(this); + this.notifyAfterValidate = this.notifyAfterValidate.bind(this); + this.delayedValidate = this.async.debounce(this.validate, this.props.deferredValidationTime); + } + + + public componentDidMount(): void { + // Start retrieving the list views + this.loadViews(); + } + + public componentDidUpdate(prevProps: IViewPickerProps, _prevState: IViewPickerState): void { + if ( + this.props.listId !== prevProps.listId || + this.props.webAbsoluteUrl !== prevProps.webAbsoluteUrl || + this.props.orderBy !== prevProps.orderBy + ) { + this.loadViews(); + } + } + + /** + * Called when the component will unmount + */ + public componentWillUnmount(): void { + if (typeof this.async !== 'undefined') { + this.async.dispose(); + } + } + + private loadViews(): void { + + const viewService: SPViewPickerService = new SPViewPickerService(this.props, this.props.context); + const viewsToExclude: string[] = this.props.viewsToExclude || []; + this.options = []; + viewService.getViews().then((response: ISPViews) => { + // Start mapping the views that are selected + response.value.forEach((view: ISPView) => { + if (this.props.selectedView === view.Id) { + this.selectedKey = view.Id; + } + + // Make sure that the current view is NOT in the 'viewsToExclude' array + if (viewsToExclude.indexOf(view.Title) === -1 && viewsToExclude.indexOf(view.Id) === -1) { + this.options.push({ + key: view.Id, + text: view.Title + }); + } + }); + + // Option to unselect the view + this.options.unshift({ + key: EMPTY_VIEW_KEY, + text: EMPTY_VIEW_KEY + }); + + // Update the current component state + this.setState({ + results: this.options, + selectedKey: this.selectedKey + }); + }).catch(() => { /* no-op; */ }); + } + + private onChanged(event: React.FormEvent,option: IDropdownOption, _index?: number): void { + const newValue: string = option.key as string; + this.delayedValidate(newValue); + } + + /** + * Validates the new custom field value + */ + private validate(value: string): void { + this.notifyAfterValidate(this.props.selectedView, value); + if (this.latestValidateValue === value) { + return; + } + } + + + /** + * Notifies the parent Web Part of a property value change + */ + private notifyAfterValidate(oldValue: string, newValue: string): void { + // Check if the user wanted to unselect the view + const propValue = newValue === EMPTY_VIEW_KEY ? '' : newValue; + + // Deselect all options + this.options = this.state.results.map(option => { + if (option.selected) { + option.selected = false; + } + return option; + }); + // Set the current selected key + this.selectedKey = newValue; + console.log('Selected View key :'+this.selectedKey); + // Update the state + this.setState({ + selectedKey: this.selectedKey, + results: this.options + }); + } + + /** + * Renders the SPViewPicker controls with Office UI Fabric + */ + public render(): JSX.Element { + const { results, selectedKey } = this.state; + const {className, disabled, label, placeholder, showBlankOption} = this.props; + + const options : IDropdownOption[] = results.map(v => ({ + key : v.key, + text: v.text + })); + + if (showBlankOption) { + // Provide empty option + options.unshift({ + key: EMPTY_VIEW_KEY, + text: '', + }); + } + + const dropdownProps: IDropdownProps = { + className, + options, + disabled: disabled, + label, + placeholder, + onChange: this.onChanged, + }; + + // Renders content + return ( + <> + + + ); + } + +} \ No newline at end of file diff --git a/src/controls/viewPicker/index.ts b/src/controls/viewPicker/index.ts new file mode 100644 index 000000000..374101160 --- /dev/null +++ b/src/controls/viewPicker/index.ts @@ -0,0 +1,2 @@ +export * from './IViewPicker'; +export * from './ViewPicker'; \ No newline at end of file diff --git a/src/services/ISPViewPickerService.ts b/src/services/ISPViewPickerService.ts new file mode 100644 index 000000000..1f36c97e6 --- /dev/null +++ b/src/services/ISPViewPickerService.ts @@ -0,0 +1,5 @@ +import { ISPViews } from "../../src/common/SPEntities"; + +export interface ISPViewPickerService { + getViews(): Promise; +} \ No newline at end of file diff --git a/src/services/SPViewPickerService.ts b/src/services/SPViewPickerService.ts new file mode 100644 index 000000000..69199c761 --- /dev/null +++ b/src/services/SPViewPickerService.ts @@ -0,0 +1,88 @@ +import { SPHttpClient } from '@microsoft/sp-http'; +import { BaseComponentContext } from '@microsoft/sp-component-base'; +import {IViewPickerProps, PropertyFieldViewPickerOrderBy } from '../controls/viewPicker/IViewPicker'; +import { ISPViewPickerService } from './ISPViewPickerService'; +import { ISPView, ISPViews } from "../../src/common/SPEntities"; + +/** + * Service implementation to get list & list items from current SharePoint site + */ +export class SPViewPickerService implements ISPViewPickerService { + private context: BaseComponentContext; + private props: IViewPickerProps; + + /** + * Service constructor + */ + constructor(_props: IViewPickerProps, pageContext: BaseComponentContext) { + this.props = _props; + this.context = pageContext; + } + + /** + * Gets the collection of view for a selected list + */ + public async getViews(): Promise { + if (this.props.listId === undefined || this.props.listId === "") { + return this.getEmptyViews(); + } + + const webAbsoluteUrl = this.props.webAbsoluteUrl ? this.props.webAbsoluteUrl : this.context.pageContext.web.absoluteUrl; + + // If the running environment is SharePoint, request the lists REST service + let queryUrl: string = `${webAbsoluteUrl}/_api/lists(guid'${this.props.listId}')/Views?$select=Title,Id`; + + // Check if the orderBy property is provided + if (this.props.orderBy !== null) { + queryUrl += '&$orderby='; + switch (this.props.orderBy) { + case PropertyFieldViewPickerOrderBy.Id: + queryUrl += 'Id'; + break; + case PropertyFieldViewPickerOrderBy.Title: + queryUrl += 'Title'; + break; + } + + // Adds an OData Filter to the list + if (this.props.filter) { + queryUrl += `&$filter=${encodeURIComponent(this.props.filter)}`; + } + + const response = await this.context.spHttpClient.get(queryUrl, SPHttpClient.configurations.v1); + const views = (await response.json()) as ISPViews; + + // Check if onViewsRetrieved callback is defined + if (this.props.onViewsRetrieved) { + //Call onViewsRetrieved + const lr = this.props.onViewsRetrieved(views.value); + let output: ISPView[]; + + //Conditional checking to see of PromiseLike object or array + if (lr instanceof Array) { + output = lr; + } else { + output = await lr; + } + + views.value = output; + } + + return views; + } + } + + /** + * Returns an empty view for when a list isn't selected + */ + private getEmptyViews(): Promise { + return new Promise((resolve) => { + const listData: ISPViews = { + value: [ + ] + }; + + resolve(listData); + }); + } +} diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index d23d5184a..084190917 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -192,6 +192,7 @@ import { ModernAudio, ModernAudioLabelPosition } from "../../../ModernAudio"; import { SPTaxonomyService, TaxonomyTree } from "../../../ModernTaxonomyPicker"; import { TestControl } from "./TestControl"; import { UploadFiles } from "../../../controls/uploadFiles"; +import { ViewPicker } from "../../../controls/viewPicker"; // Used to render document card /** @@ -748,6 +749,14 @@ export default class ControlsTest extends React.Component { + console.log("newView:", newValue); + } + private _onRenderGridItem = (item: any, _finalSize: ISize, isCompact: boolean): JSX.Element => { const previewProps: IDocumentCardPreviewProps = { previewImages: [ @@ -1515,6 +1524,16 @@ export default class ControlsTest extends React.Component + +
View picker tester: + +
+
Icon Picker