diff --git a/CHANGELOG.md b/CHANGELOG.md index 841b24c9..50a9e3a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Releases +## 3.17.0 + +### Enhancements + +- `fast-serve`: ast-serve update to match the most recent changes. [#606](https://github.com/pnp/sp-dev-fx-property-controls/pull/606) + +### Contributors + +Special thanks to our contributor: [Sergei Sergeev](https://github.com/s-KaiNet). + ## 3.16.0 ### Enhancements diff --git a/docs/documentation/docs/about/release-notes.md b/docs/documentation/docs/about/release-notes.md index 841b24c9..50a9e3a1 100644 --- a/docs/documentation/docs/about/release-notes.md +++ b/docs/documentation/docs/about/release-notes.md @@ -1,5 +1,15 @@ # Releases +## 3.17.0 + +### Enhancements + +- `fast-serve`: ast-serve update to match the most recent changes. [#606](https://github.com/pnp/sp-dev-fx-property-controls/pull/606) + +### Contributors + +Special thanks to our contributor: [Sergei Sergeev](https://github.com/s-KaiNet). + ## 3.16.0 ### Enhancements diff --git a/docs/documentation/docs/assets/contentTypePicker-Error.png b/docs/documentation/docs/assets/contentTypePicker-Error.png new file mode 100644 index 00000000..dd0cd5ad Binary files /dev/null and b/docs/documentation/docs/assets/contentTypePicker-Error.png differ diff --git a/docs/documentation/docs/assets/contentTypePicker.png b/docs/documentation/docs/assets/contentTypePicker.png new file mode 100644 index 00000000..21c02c88 Binary files /dev/null and b/docs/documentation/docs/assets/contentTypePicker.png differ diff --git a/docs/documentation/docs/assets/contentTypePicker1.png b/docs/documentation/docs/assets/contentTypePicker1.png new file mode 100644 index 00000000..762a8eb6 Binary files /dev/null and b/docs/documentation/docs/assets/contentTypePicker1.png differ diff --git a/docs/documentation/docs/assets/contentTypes-for-Site.gif b/docs/documentation/docs/assets/contentTypes-for-Site.gif new file mode 100644 index 00000000..3439afad Binary files /dev/null and b/docs/documentation/docs/assets/contentTypes-for-Site.gif differ diff --git a/docs/documentation/docs/controls/PropertyFieldContentTypePicker.md b/docs/documentation/docs/controls/PropertyFieldContentTypePicker.md new file mode 100644 index 00000000..d70afd03 --- /dev/null +++ b/docs/documentation/docs/controls/PropertyFieldContentTypePicker.md @@ -0,0 +1,146 @@ +# PropertyFieldContentTypePicker control + +This control generates a ContentType picker field that can be used in the property pane of your SharePoint Framework web parts. + +The control automatically retrieves the ContentType for a given SharePoint Site or selected SharePoint list: + +![ContentType picker](../assets/contentTypePicker.png) + +## How to use this control in your solutions + +1. Check that you installed the `@pnp/spfx-property-controls` dependency. Check out The [getting started](../../#getting-started) page for more information about installing the dependency. +2. Import the following modules to your component: + +```TypeScript +import { PropertyFieldContentTypePicker, PropertyFieldContentTypePickerOrderBy } from '@pnp/spfx-property-controls/lib/PropertyFieldContentTypePicker'; +``` + +3. You'll probably want to use this control in combination with the [PropertyFieldListPicker](./PropertyFieldListPicker.md). Make sure to select the `multiSelect` prop to `false`, as this control is designed to work with a single list. Store the list id in your web part properties, as follows: +```TypeScript +export interface IPropertyControlsTestWebPartProps { + list: string; // Stores the list ID +} +``` + +3. Create a new property for your web part, as indicated between the `BEGIN:` and `END:` comments below: + + +```TypeScript +export interface IPropertyControlsTestWebPartProps { + list: string; // Stores the list ID + + // BEGIN: Added + view: string; // Stores the view ID + contentType : string // stores the contenttype ID + // END: Added +} +``` + +4. Add the custom property control to the `groupFields` of the web part property pane configuration: + +```TypeScript +PropertyFieldContentTypePicker('contentType', { + label: 'Select a Content Type', + context: this.context, + selectedContentType: this.properties.contentType, + disabled: false, + orderBy: PropertyFieldContentTypeOrderBy.Name, + onPropertyChange: this.onPropertyPaneFieldChanged.bind(this), + properties: this.properties, + onGetErrorMessage: null, + deferredValidationTime: 0, + key: 'contentTypePickerFieldId' +}) + +``` + +5. To fetch the contentTypes of a particular site, change the property pane configuration as follows: + +```TypeScript +PropertyFieldContentTypePicker('contentType', { + label: 'Select a Content Type', + context: this.context, + selectedContentType: this.properties.contentType, + disabled: false, + webAbsoluteUrl:"https://****.sharepoint.com/sites/*****", + orderBy: PropertyFieldContentTypeOrderBy.Name, + onPropertyChange: this.onPropertyPaneFieldChanged.bind(this), + properties: this.properties, + onGetErrorMessage: null, + deferredValidationTime: 0, + key: 'contentTypePickerFieldId' +}) +``` +![ContentType picker for site ](../assets/contentTypes-for-Site.gif) + +6. To fetch the contentTypes of selected list, change the property pane configuration as follows: + +```TypeScript +PropertyFieldContentTypePicker('contentType', { + label: 'Select a Content Type', + context: this.context, + selectedContentType: this.properties.contentType, + listId: {list-guid} //"0da3b4b7-8ebd-4f15-87ee-afae5cacadad" + disabled: false, + orderBy: PropertyFieldContentTypeOrderBy.Name, + onPropertyChange: this.onPropertyPaneFieldChanged.bind(this), + properties: this.properties, + onGetErrorMessage: null, + deferredValidationTime: 0, + key: 'contentTypePickerFieldId' +}) + +``` + +![ContentType picker for selected list ](../assets/contentTypePicker1.png) + +7. If ListID specified in the propertiesc is not available in the selected site, the control will error out as follows +```TypeScript +PropertyFieldContentTypePicker('contentType', { + label: 'Select a Content Type', + context: this.context, + selectedContentType: this.properties.contentType, + listId: {list-guid} //"0da3b4b7-8ebd-4f15-87ee-afae5cacadad" + disabled: false, + orderBy: PropertyFieldContentTypeOrderBy.Name, + onPropertyChange: this.onPropertyPaneFieldChanged.bind(this), + properties: this.properties, + onGetErrorMessage: null, + deferredValidationTime: 0, + key: 'contentTypePickerFieldId' +}) + +``` +![ContentType picker Error ](../assets/contentTypePicker-Error.png) + +## Implementation + +The `PropertyFieldContentTypePicker` control can be configured with the following properties: + +| Property | Type | Required | Description | +| ---- | ---- | ---- | ---- | +| label | string | yes | Property field label displayed on top. | +| listId | string | no | The ID of the list or library you wish to select a contentType from. | +| disabled | boolean | no | Specify if the control needs to be disabled. | +| context | BaseComponentContext | yes | Context of the current web part. | +| selectedContentType | string | string[] | no | IDefines ContentType titles which should be excluded from the ContentType picker control. | +| orderBy | PropertyFieldContentTypeOrderBy | no | Specify the property on which you want to order the retrieve set of ContentTypes. | +| webAbsoluteUrl | string | no | Absolute Web Url of target site (user requires permissions) | +| onPropertyChange | function | yes | Defines a onPropertyChange function to raise when the date gets changed. | +| properties | any | yes | Parent web part properties, this object is use to update the property value. | +| key | string | yes | An unique key that indicates the identity of this control. | +| onGetErrorMessage | function | no | The method is used to get the validation error message and determine whether the input value is valid or not. See [this documentation](https://dev.office.com/sharepoint/docs/spfx/web-parts/guidance/validate-web-part-property-values) to learn how to use it. | +| deferredValidationTime | number | no | Control will start to validate after users stop typing for `deferredValidationTime` milliseconds. Default value is 200. | +| contentTypesToExclude | string[] | no | Defines contentTypes by which should be excluded from the contentType picker control. You can specify contentType titles or IDs | +| filter | string | no | Filter contentTypes from OData query. | +| onContentTypesRetrieved | (contentType: ISPContentType[]) => PromiseLike \| ISPContentType[] | no | Callback that is called before the dropdown is populated. | + + +Enum `PropertyFieldContentTypePickerOrderBy` + +| Name | Description | +| ---- | ---- | +| Id | Sort by contentType ID | +| Title | Sort by contentType title | + +![](https://telemetry.sharepointpnp.com/sp-dev-fx-property-controls/wiki/PropertyFieldContentTypePicker) diff --git a/fast-serve/config.json b/fast-serve/config.json new file mode 100644 index 00000000..fbb6384c --- /dev/null +++ b/fast-serve/config.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/s-KaiNet/spfx-fast-serve/master/schema/config.latest.schema.json", + "cli": { + "isLibraryComponent": false + } +} \ No newline at end of file diff --git a/fast-serve/webpack.extend.js b/fast-serve/webpack.extend.js new file mode 100644 index 00000000..cf1ebcad --- /dev/null +++ b/fast-serve/webpack.extend.js @@ -0,0 +1,31 @@ +/* +* User webpack settings file. You can add your own settings here. +* Changes from this file will be merged into the base webpack configuration file. +* This file will not be overwritten by the subsequent spfx-fast-serve calls. +*/ + +/** + * you can add your project related webpack configuration here, it will be merged using webpack-merge module + * i.e. plugins: [new webpack.Plugin()] + */ +const webpackConfig = { + +} + +/** + * For even more fine-grained control, you can apply custom webpack settings using below function + * @param {object} initialWebpackConfig - initial webpack config object + * @param {object} webpack - webpack object, used by SPFx pipeline + * @returns webpack config object + */ +const transformConfig = function (initialWebpackConfig, webpack) { + // transform the initial webpack config here, i.e. + // initialWebpackConfig.plugins.push(new webpack.Plugin()); etc. + + return initialWebpackConfig; +} + +module.exports = { + webpackConfig, + transformConfig +} diff --git a/package-lock.json b/package-lock.json index c40ae65c..2778832a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "husky": "^6.0.0", "request-promise": "4.2.6", "sonarqube-scanner": "2.8.2", - "spfx-fast-serve-helpers": "1.18.11", + "spfx-fast-serve-helpers": "~1.18.0", "typescript": "4.7.4", "uuid": "^9.0.1" }, diff --git a/package.json b/package.json index 8bc7dea5..fbebd410 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build": "gulp build", "clean": "gulp clean", "test": "gulp test", - "serve": "fast-serve", + "serve": "gulp bundle --custom-serve --max_old_space_size=4096 && fast-serve", "versionUpdater": "gulp versionUpdater", "prepublishOnly": "gulp", "changelog": "node scripts/create-changelog.js && node scripts/sync-changelogs.js && gulp versionUpdater", @@ -69,7 +69,7 @@ "husky": "^6.0.0", "request-promise": "4.2.6", "sonarqube-scanner": "2.8.2", - "spfx-fast-serve-helpers": "1.18.11", + "spfx-fast-serve-helpers": "~1.18.0", "typescript": "4.7.4", "uuid": "^9.0.1" }, @@ -94,4 +94,4 @@ "main": "lib/index.js", "maintainers": [], "contributors": [] -} +} \ No newline at end of file diff --git a/src/PropertyFieldContentTypePicker.ts b/src/PropertyFieldContentTypePicker.ts new file mode 100644 index 00000000..3eb4dd95 --- /dev/null +++ b/src/PropertyFieldContentTypePicker.ts @@ -0,0 +1,2 @@ +export * from './propertyFields/contentTypePicker/index'; +export * from './propertyFields/contentTypePicker/index'; \ No newline at end of file diff --git a/src/propertyFields/contentTypePicker/IPropertyFieldContentTypePicker.ts b/src/propertyFields/contentTypePicker/IPropertyFieldContentTypePicker.ts new file mode 100644 index 00000000..7870edb5 --- /dev/null +++ b/src/propertyFields/contentTypePicker/IPropertyFieldContentTypePicker.ts @@ -0,0 +1,127 @@ +import { IPropertyPaneCustomFieldProps } from '@microsoft/sp-property-pane'; +import { BaseComponentContext } from '@microsoft/sp-component-base'; +import { ISPContentType } from './ISPContentType'; + + +/** + * Enum for specifying how the ContentTypes should be sorted + */ +export enum PropertyFieldContentTypeOrderBy { + Id = 1, + Name +} + +/** + * Public properties of the PropertyFieldContentTypePicker custom field + */ +export interface IPropertyFieldContentTypePickerProps { + /** + * Context of the current web part + */ + context: BaseComponentContext; + + /** + * 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 ContentTypes from Odata query + */ + filter?: string; + + /** + * An UNIQUE key indicates the identity of this control + */ + key?: string; + + /** + * Property field label displayed on top + */ + label: string; + /** + * The List Id of the list where you want to get the ContentTypes + */ + listId?: string; + + /** + * Specify the property on which you want to order the retrieve set of ContentTypes. + */ + orderBy?: PropertyFieldContentTypeOrderBy; + + /** + * Parent Web Part properties + */ + properties: any; // eslint-disable-line @typescript-eslint/no-explicit-any + + /** + * Initial selected ContentType of the control + */ + selectedContentType?: string | string[]; + + /** + * Defines ContentType titles which should be excluded from the ContentType picker control + */ + contentTypesToExclude?: string[]; + + /** + * Absolute Web Url of target site (user requires permissions) + */ + webAbsoluteUrl?: string; + + /** + * The method is used to get the validation error message and determine whether the input value is valid or not. + * + * When it returns string: + * - If valid, it returns empty string. + * - If invalid, it returns the error message string and the text field will + * show a red border and show an error message below the text field. + * + * When it returns Promise: + * - The resolved value is display as error message. + * - The rejected, the value is thrown away. + * + */ + onGetErrorMessage?: (value: string) => string | Promise; + /** + * 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(propertyPath: string, oldValue: any, newValue: any): void; // eslint-disable-line @typescript-eslint/no-explicit-any + /** + * Callback that is called before the dropdown is populated + */ + onContentTypesRetrieved?: (contentTypes: ISPContentType[]) => PromiseLike | ISPContentType[]; +} + +/** + * Private properties of the PropertyFieldContentTypePicker custom field. + * We separate public & private properties to include onRender & onDispose method waited + * by the PropertyFieldCustom, without asking to the developer to add it when he's using + * the PropertyFieldContentTypePicker. + */ +export interface IPropertyFieldContentTypePickerPropsInternal extends IPropertyFieldContentTypePickerProps, IPropertyPaneCustomFieldProps { + context: BaseComponentContext; + deferredValidationTime?: number; + disabled?: boolean; + filter?: string; + orderBy?: PropertyFieldContentTypeOrderBy; + key: string; + label: string; + listId?: string; + properties: any; // eslint-disable-line @typescript-eslint/no-explicit-any + selectedContentType?: string; + targetProperty: string; + contentTypesToExclude?: string[]; + webAbsoluteUrl?: string; + onGetErrorMessage?: (value: string | string[]) => string | Promise; + onPropertyChange(propertyPath: string, oldValue: any, newValue: any): void; // eslint-disable-line @typescript-eslint/no-explicit-any + onContentTypesRetrieved?: (contentTypes: ISPContentType[]) => PromiseLike | ISPContentType[]; +} diff --git a/src/propertyFields/contentTypePicker/IPropertyFieldContentTypePickerHost.ts b/src/propertyFields/contentTypePicker/IPropertyFieldContentTypePickerHost.ts new file mode 100644 index 00000000..e2e45b3a --- /dev/null +++ b/src/propertyFields/contentTypePicker/IPropertyFieldContentTypePickerHost.ts @@ -0,0 +1,19 @@ +import { IPropertyFieldContentTypePickerPropsInternal } from './IPropertyFieldContentTypePicker'; +import { IDropdownOption } from '@fluentui/react/lib/Dropdown'; + +/** + * PropertyFieldContentTypePickerHost properties interface + */ +export interface IPropertyFieldContentTypePickerHostProps extends IPropertyFieldContentTypePickerPropsInternal { + onChange: (targetProperty?: string, newValue?: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +/** + * PropertyFieldContentTypePickerHost state interface + */ +export interface IPropertyFieldContentTypePickerHostState { + + results: IDropdownOption[]; + selectedKey?: string; + errorMessage?: string; +} diff --git a/src/propertyFields/contentTypePicker/ISPContentType.ts b/src/propertyFields/contentTypePicker/ISPContentType.ts new file mode 100644 index 00000000..d997976c --- /dev/null +++ b/src/propertyFields/contentTypePicker/ISPContentType.ts @@ -0,0 +1,6 @@ +export interface ISPContentType { + Id: { + StringValue: string; + }; + Name: string; +} diff --git a/src/propertyFields/contentTypePicker/ISPContentTypes.ts b/src/propertyFields/contentTypePicker/ISPContentTypes.ts new file mode 100644 index 00000000..861a4edb --- /dev/null +++ b/src/propertyFields/contentTypePicker/ISPContentTypes.ts @@ -0,0 +1,9 @@ +import { ISPContentType } from "."; + +/** + * Defines a collection of SharePoint list ContentTypes + */ +export interface ISPContentTypes { + + value: ISPContentType[]; +} diff --git a/src/propertyFields/contentTypePicker/PropertyFieldContentTypePicker.ts b/src/propertyFields/contentTypePicker/PropertyFieldContentTypePicker.ts new file mode 100644 index 00000000..b0447d0e --- /dev/null +++ b/src/propertyFields/contentTypePicker/PropertyFieldContentTypePicker.ts @@ -0,0 +1,145 @@ +import * as React from 'react'; +import * as ReactDom from 'react-dom'; +import { + IPropertyPaneField, + PropertyPaneFieldType +} from '@microsoft/sp-property-pane'; +import { BaseComponentContext } from '@microsoft/sp-component-base'; +import PropertyFieldContentTypePickerHost from './PropertyFieldContentTypePickerHost'; +import { IPropertyFieldContentTypePickerHostProps } from './IPropertyFieldContentTypePickerHost'; +import { PropertyFieldContentTypeOrderBy, IPropertyFieldContentTypePickerProps, IPropertyFieldContentTypePickerPropsInternal } from './IPropertyFieldContentTypePicker'; +import { ISPContentType } from '.'; + +/** + * Represents a PropertyFieldContentTypePicker object + */ +class PropertyFieldContentTypePickerBuilder implements IPropertyPaneField { + + //Properties defined by IPropertyPaneField + public properties: IPropertyFieldContentTypePickerPropsInternal; + public targetProperty: string; + public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom; + + //Custom properties label: string; + private context: BaseComponentContext; + private label: string; + private listId?: string; + private selectedContentType: string; + private orderBy: PropertyFieldContentTypeOrderBy; + private contentTypesToExclude: string[]; + + private customProperties: any; // eslint-disable-line @typescript-eslint/no-explicit-any + private deferredValidationTime: number = 200; + private disabled: boolean = false; + private disableReactivePropertyChanges: boolean = false; + private filter: string; + private key: string; + private webAbsoluteUrl?: string; + private onGetErrorMessage: (value: string) => string | Promise; + private onContentTypesRetrieved?: (contentTypes: ISPContentType[]) => PromiseLike | ISPContentType[]; + public onPropertyChange(propertyPath: string, oldValue: any, newValue: any): void { /* no-op; */ } // eslint-disable-line @typescript-eslint/no-explicit-any + private renderWebPart: () => void; + + /** + * Constructor method + */ + public constructor(_targetProperty: string, _properties: IPropertyFieldContentTypePickerPropsInternal) { + this.render = this.render.bind(this); + this.targetProperty = _targetProperty; + this.properties = _properties; + this.properties.onDispose = this.dispose; + this.properties.onRender = this.render; + this.label = _properties.label; + this.context = _properties.context; + this.webAbsoluteUrl = _properties.webAbsoluteUrl; + this.listId = _properties.listId; + this.selectedContentType = _properties.selectedContentType; + this.onPropertyChange = _properties.onPropertyChange; + this.customProperties = _properties.properties; + this.key = _properties.key; + this.orderBy = _properties.orderBy; + this.contentTypesToExclude = _properties.contentTypesToExclude; + this.filter = _properties.filter; + this.onGetErrorMessage = _properties.onGetErrorMessage; + this.onContentTypesRetrieved = _properties.onContentTypesRetrieved; + + if (_properties.disabled === true) { + this.disabled = _properties.disabled; + } + if (_properties.deferredValidationTime) { + this.deferredValidationTime = _properties.deferredValidationTime; + } + } + + /** + * Renders the SPContentTypePicker field content + */ + private render(elem: HTMLElement, ctx?: any, changeCallback?: (targetProperty?: string, newValue?: any) => void): void { // eslint-disable-line @typescript-eslint/no-explicit-any + const componentProps: IPropertyFieldContentTypePickerHostProps = { + label: this.label, + targetProperty: this.targetProperty, + context: this.context, + webAbsoluteUrl: this.webAbsoluteUrl, + listId: this.listId, + orderBy: this.orderBy, + onDispose: this.dispose, + onRender: this.render, + onChange: changeCallback, + onPropertyChange: this.onPropertyChange, + properties: this.customProperties, + key: this.key, + disabled: this.disabled, + onGetErrorMessage: this.onGetErrorMessage, + deferredValidationTime: this.deferredValidationTime, + contentTypesToExclude: this.contentTypesToExclude, + filter: this.filter, + onContentTypesRetrieved: this.onContentTypesRetrieved + }; + + // Single selector + componentProps.selectedContentType = this.selectedContentType; + const element: React.ReactElement = React.createElement(PropertyFieldContentTypePickerHost, componentProps); + // Calls the REACT content generator + ReactDom.render(element, elem); + } + + /** + * Disposes the current object + */ + private dispose(_elem: HTMLElement): void { + // no-op; + } + +} + +/** + * Helper method to create a SPContentType Picker on the PropertyPane. + * @param targetProperty - Target property the SharePoint ContentType picker is associated to. + * @param properties - Strongly typed SPContentType Picker properties. + */ +export function PropertyFieldContentTypePicker(targetProperty: string, properties: IPropertyFieldContentTypePickerProps): IPropertyPaneField { + + //Create an internal properties object from the given properties + const newProperties: IPropertyFieldContentTypePickerPropsInternal = { + label: properties.label, + targetProperty: targetProperty, + context: properties.context, + listId: properties.listId, + selectedContentType: typeof properties.selectedContentType === 'string' ? properties.selectedContentType : null, + onPropertyChange: properties.onPropertyChange, + properties: properties.properties, + onDispose: null, + onRender: null, + key: properties.key, + disabled: properties.disabled, + contentTypesToExclude: properties.contentTypesToExclude, + webAbsoluteUrl: properties.webAbsoluteUrl, + filter: properties.filter, + onGetErrorMessage: properties.onGetErrorMessage, + deferredValidationTime: properties.deferredValidationTime, + onContentTypesRetrieved: properties.onContentTypesRetrieved + }; + //Calls the PropertyFieldContentTypePicker builder object + //This object will simulate a PropertyFieldCustom to manage his rendering process + return new PropertyFieldContentTypePickerBuilder(targetProperty, newProperties); +} diff --git a/src/propertyFields/contentTypePicker/PropertyFieldContentTypePickerHost.tsx b/src/propertyFields/contentTypePicker/PropertyFieldContentTypePickerHost.tsx new file mode 100644 index 00000000..f68ab807 --- /dev/null +++ b/src/propertyFields/contentTypePicker/PropertyFieldContentTypePickerHost.tsx @@ -0,0 +1,216 @@ +import * as React from 'react'; +import { Dropdown, IDropdownOption } from '@fluentui/react/lib/Dropdown'; +import { Async } from '@fluentui/react/lib/Utilities'; +import { Label } from '@fluentui/react/lib/Label'; +import { IPropertyFieldContentTypePickerHostProps, IPropertyFieldContentTypePickerHostState } from './IPropertyFieldContentTypePickerHost'; +import { SPContentTypePickerService } from '../../services/SPContentTypePickerService'; +import FieldErrorMessage from '../errorMessage/FieldErrorMessage'; +import { ISPContentType } from '.'; +import { ISPContentTypes } from './ISPContentTypes'; +import * as telemetry from '../../common/telemetry'; +import { setPropertyValue } from '../../helpers/GeneralHelper'; + +// Empty contentType value +const EMPTY_CONTENT_TYPE_KEY = 'NO_CONTENT_TYPE_SELECTED'; + +/** + * Renders the controls for PropertyFieldContentTypePicker component + */ +export default class PropertyFieldContentTypePickerHost extends React.Component { + private options: IDropdownOption[] = []; + private selectedKey: string; + private latestValidateValue: string; + private async: Async; + private delayedValidate: (value: string) => void; + + /** + * Constructor method + */ + constructor(props: IPropertyFieldContentTypePickerHostProps) { + super(props); + + telemetry.track('PropertyFieldContentTypePicker', { + disabled: props.disabled + }); + + this.state = { + results: this.options, + errorMessage: '' + }; + + 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 content types + this.loadContentTypes(); + } + + public componentDidUpdate(prevProps: IPropertyFieldContentTypePickerHostProps, _prevState: IPropertyFieldContentTypePickerHostState): void { + if (this.props.listId !== prevProps.listId || this.props.webAbsoluteUrl !== prevProps.webAbsoluteUrl) { + this.loadContentTypes(); + } + } + + /** + * Loads the loadContentTypes from a selected SharePoint list or SharePoint site + */ + private loadContentTypes(): void { + const contentTypeService: SPContentTypePickerService = new SPContentTypePickerService(this.props, this.props.context); + const contentTypesToExclude: string[] = this.props.contentTypesToExclude || []; + this.options = []; + contentTypeService.getContentTypes().then((response: ISPContentTypes) => { + console.log(response); + // Start mapping the contentTypes that are selected + response.value.forEach((contentType: ISPContentType) => { + if (this.props.selectedContentType === contentType.Id.StringValue) { + this.selectedKey = contentType.Id.StringValue; + } + + // Make sure that the current contentType is NOT in the 'contentTypesToExclude' array + if (contentTypesToExclude.indexOf(contentType.Name) === -1 && contentTypesToExclude.indexOf(contentType.Id.StringValue) === -1) { + this.options.push({ + key: contentType.Id.StringValue, + text: contentType.Name + }); + } + }); + + // Option to unselect the contentType + this.options.unshift({ + key: EMPTY_CONTENT_TYPE_KEY, + text: '' + }); + + // Update the current component state + this.setState({ + results: this.options, + selectedKey: this.selectedKey + }); + }).catch((error) => { + console.error('Error loading content types:', error); + // Handle the error appropriately, e.g., display an error message to the user + this.setState({ + errorMessage: 'Error : List does not exist.\n\nThe page you selected contains a list that does not exist. It may have been deleted by another user.' + }); + }); + } + + + /** + * Raises when a contentType has been selected + */ + + private onChanged(element: 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 { + if (this.props.onGetErrorMessage === null || this.props.onGetErrorMessage === undefined) { + this.notifyAfterValidate(this.props.selectedContentType, value); + return; + } + + if (this.latestValidateValue === value) { + return; + } + + this.latestValidateValue = value; + + const errResult: string | Promise = this.props.onGetErrorMessage(value || ''); + if (typeof errResult !== 'undefined') { + if (typeof errResult === 'string') { + if (errResult === '') { + this.notifyAfterValidate(this.props.selectedContentType, value); + } + this.setState({ + errorMessage: errResult + }); + } else { + errResult.then((errorMessage: string) => { + if (!errorMessage) { + this.notifyAfterValidate(this.props.selectedContentType, value); + } + this.setState({ + errorMessage: errorMessage + }); + }).catch(() => { /* no-op; */ }); + } + } else { + this.notifyAfterValidate(this.props.selectedContentType, value); + } + } + + /** + * 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 contentType + const propValue = newValue === EMPTY_CONTENT_TYPE_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; + // Update the state + this.setState({ + selectedKey: this.selectedKey, + results: this.options + }); + + if (this.props.onPropertyChange && propValue !== null) { + // Store the new property value + setPropertyValue(this.props.properties, this.props.targetProperty, propValue); + + // Trigger the default onPropertyChange event + this.props.onPropertyChange(this.props.targetProperty, oldValue, propValue); + + // Trigger the apply button + if (typeof this.props.onChange !== 'undefined' && this.props.onChange !== null) { + this.props.onChange(this.props.targetProperty, propValue); + } + } + } + + /** + * Called when the component will unmount + */ + public componentWillUnmount(): void { + if (typeof this.async !== 'undefined') { + this.async.dispose(); + } + } + + /** + * Renders the SPContentTypePicker controls with Office UI Fabric + */ + public render(): JSX.Element { + // Renders content + return ( +
+ {this.props.label && } + + + +
+ ); + } +} diff --git a/src/propertyFields/contentTypePicker/index.ts b/src/propertyFields/contentTypePicker/index.ts new file mode 100644 index 00000000..18b712d5 --- /dev/null +++ b/src/propertyFields/contentTypePicker/index.ts @@ -0,0 +1,6 @@ +export * from './PropertyFieldContentTypePicker'; +export * from './IPropertyFieldContentTypePicker'; +export * from './PropertyFieldContentTypePickerHost'; +export * from './IPropertyFieldContentTypePickerHost'; +export * from './ISPContentType'; +export * from './ISPContentTypes'; diff --git a/src/services/ISPContentTypePickerService.ts b/src/services/ISPContentTypePickerService.ts new file mode 100644 index 00000000..383ceab8 --- /dev/null +++ b/src/services/ISPContentTypePickerService.ts @@ -0,0 +1,6 @@ +import { ISPContentTypes } from "../propertyFields/contentTypePicker"; + +export interface ISPContentTypePickerService { + getContentTypes(): Promise; +} + diff --git a/src/services/SPContentTypePickerService.ts b/src/services/SPContentTypePickerService.ts new file mode 100644 index 00000000..f3893418 --- /dev/null +++ b/src/services/SPContentTypePickerService.ts @@ -0,0 +1,88 @@ +import { SPHttpClient } from '@microsoft/sp-http'; +import { BaseComponentContext } from '@microsoft/sp-component-base'; +import { ISPContentType, IPropertyFieldContentTypePickerHostProps, PropertyFieldContentTypeOrderBy } from '../propertyFields/contentTypePicker'; +import { ISPContentTypePickerService } from './ISPContentTypePickerService'; +import { ISPContentTypes } from '../propertyFields/contentTypePicker'; + +/** + * Service implementation to get Content Types from SharePoint site or selected SharePoint List + */ +export class SPContentTypePickerService implements ISPContentTypePickerService { + private context: BaseComponentContext; + private props: IPropertyFieldContentTypePickerHostProps; + + /** + * Service constructor + */ + constructor(_props: IPropertyFieldContentTypePickerHostProps, pageContext: BaseComponentContext) { + this.props = _props; + this.context = pageContext; + } + + /** + * Gets the collection of ContentTypes from SharePoint site or selected SharePoint List + */ + public async getContentTypes(): Promise { + if (this.context.pageContext.web.absoluteUrl === undefined || this.context.pageContext.web.absoluteUrl === "") { + return this.getEmptycontentTypes(); + } + + const webAbsoluteUrl = this.props.webAbsoluteUrl ? this.props.webAbsoluteUrl : this.context.pageContext.web.absoluteUrl; + + // If the listId is selected, then get the contentTypes from the list or get the contentTypes from site level + let queryUrl: string = this.props.listId ? `${webAbsoluteUrl}/_api/lists(guid'${this.props.listId}')/ContentTypes?$select=Name,Id` : `${webAbsoluteUrl}/_api/web/ContentTypes?$select=Name,Id`; + + // Check if the orderBy property is provided + if (this.props.orderBy !== null || this.props.orderBy !== undefined){ + queryUrl += '&$orderby='; + switch (this.props.orderBy) { + case PropertyFieldContentTypeOrderBy.Id: + queryUrl += 'Id'; + break; + case PropertyFieldContentTypeOrderBy.Name: + queryUrl += 'Name'; + 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 ISPContentTypes; + + // Check if onContentTypesRetrieved callback is defined + if (this.props.onContentTypesRetrieved) { + //Call onContentTypesRetrieved + const lr = this.props.onContentTypesRetrieved(views.value); + let output: ISPContentType[]; + + //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 contentType for when no selection is done + */ + private getEmptycontentTypes(): Promise { + return new Promise((resolve) => { + const listData: ISPContentTypes = { + value: [ + ] + }; + + resolve(listData); + }); + } +} diff --git a/src/webparts/propertyControlsTest/IPropertyControlsTestWebPartProps.ts b/src/webparts/propertyControlsTest/IPropertyControlsTestWebPartProps.ts index 71d1b2dd..2ac99433 100644 --- a/src/webparts/propertyControlsTest/IPropertyControlsTestWebPartProps.ts +++ b/src/webparts/propertyControlsTest/IPropertyControlsTestWebPartProps.ts @@ -56,4 +56,5 @@ export interface IPropertyControlsTestWebPartProps { iconPicker: string; editableComboBox: string; monacoEditor:string; + contentType:string; } diff --git a/src/webparts/propertyControlsTest/PropertyControlsTestWebPart.ts b/src/webparts/propertyControlsTest/PropertyControlsTestWebPart.ts index d3cb525f..77a3f300 100644 --- a/src/webparts/propertyControlsTest/PropertyControlsTestWebPart.ts +++ b/src/webparts/propertyControlsTest/PropertyControlsTestWebPart.ts @@ -132,6 +132,10 @@ import PropertyControlsTest from './components/PropertyControlsTest'; import { IPropertyControlsTestWebPartProps, } from './IPropertyControlsTestWebPartProps'; +import { + PropertyFieldContentTypePicker, + PropertyFieldContentTypeOrderBy +} from '../../PropertyFieldContentTypePicker'; /** * Web part that can be used to test out the various property controls @@ -189,6 +193,7 @@ export default class PropertyControlsTestWebPart extends BaseClientSideWebPartList Filtered: {this.props.listFiltered}

List (Multi baseTemplate): {this.props.singleListMultipleBaseTemplate}

View: {this.props.view}

+

ContentType: {this.props.contentType}

Column: {this.props.column}

Multi Column: {this.props.multiColumn? this.props.multiColumn.join(', '):''}

Multi List: {this.props.multiList.join(', ')}