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(', ')}