diff --git a/.gitignore b/.gitignore index 900af3ede..fa44b8540 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,11 @@ obj *.tgz # VSCode -.vscode +.vscode/* + +# Included VSCode files +!.vscode/example-tasks.json +!.vscode/example-launch.json # Documentation docs/documentation/site diff --git a/.vscode/example-launch.json b/.vscode/example-launch.json new file mode 100644 index 000000000..14a70321b --- /dev/null +++ b/.vscode/example-launch.json @@ -0,0 +1,44 @@ +{ + /** + * Populate and rename this file to launch.json to configure debugging + */ + "version": "0.2.0", + "configurations": [ + { + "name": "Hosted workbench (chrome)", + "type": "chrome", + "request": "launch", + "url": "https://enter-your-SharePoint-site.sharepoint.com/sites/mySite/_layouts/15/workbench.aspx", + "webRoot": "${workspaceRoot}", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "preLaunchTask": "npm: serve", + "runtimeArgs": [ + "--remote-debugging-port=9222", + ] + }, + { + "name": "Hosted workbench (edge)", + "type": "edge", + "request": "launch", + "url": "https://enter-your-SharePoint-site.sharepoint.com/sites/mySite/_layouts/15/workbench.aspx", + "webRoot": "${workspaceRoot}", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "preLaunchTask": "npm: serve", + "runtimeArgs": [ + "--remote-debugging-port=9222", + ] + }, + ] + } \ No newline at end of file diff --git a/.vscode/example-tasks.json b/.vscode/example-tasks.json new file mode 100644 index 000000000..bb633d756 --- /dev/null +++ b/.vscode/example-tasks.json @@ -0,0 +1,30 @@ +{ + /** + * Populate and rename this file to launch.json to configure debugging + */ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "serve", + "isBackground": true, + "problemMatcher": { + "owner": "custom", + "pattern": { + "regexp": "." + }, + "background": { + "activeOnStart": true, + "beginsPattern": "Starting 'bundle'", + "endsPattern": "\\[\\sFinished\\s\\]" + } + }, + "label": "npm: serve", + "detail": "gulp bundle --custom-serve --max_old_space_size=4096 && fast-serve", + "group": { + "kind": "build", + "isDefault": true + } + }, + ] +} \ No newline at end of file diff --git a/CHANGELOG.json b/CHANGELOG.json index b2aa8d808..b9e5f1fff 100644 --- a/CHANGELOG.json +++ b/CHANGELOG.json @@ -1,5 +1,33 @@ { "versions": [ + { + "version": "3.17.0", + "changes":{ + "new": [], + "enhancements": [ + "`DyanmicForm`: Added file handling [#1625](https://github.com/pnp/sp-dev-fx-controls-react/pull/1625)", + "`DynamicForm`: Custom Formatting and Validation, ControlsTestWebPart updates [#1672](https://github.com/pnp/sp-dev-fx-controls-react/pull/1672)", + "`PeoplePicker`: Added custom filter to PeoplePicker selection [#1657](https://github.com/pnp/sp-dev-fx-controls-react/issues/1657)", + "`RichText`: Align RichText heading styles and font sizes with OOB SharePoint text web part [#1706](https://github.com/pnp/sp-dev-fx-controls-react/pull/1706)" + ], + "fixes": [ + "Build fails due to missing @iconify/react dependency after upgrade to 3.16.0 [#1719](https://github.com/pnp/sp-dev-fx-controls-react/issues/1719)", + "`ModernTaxonomyPicker`: not displaying suggestions when typing in values - API not found error [#1688](https://github.com/pnp/sp-dev-fx-controls-react/issues/1688)", + "`DynamicForm`: Disable issue on fieldOverrides field control when onBeforeSubmit return true [#1715](https://github.com/pnp/sp-dev-fx-controls-react/issues/1715)", + "`PeoplePicker`: PeoplePicker returns no results with webAbsoluteUrl and ensureUser [#1669](https://github.com/pnp/sp-dev-fx-controls-react/issues/1669)", + "`DynamicForm`: [DynamicForm] Fixing multi taxonomy field (loading + saving existing item) [#1739](https://github.com/pnp/sp-dev-fx-controls-react/pull/1739)" + ] + }, + "contributions": [ + "[Guido Zambarda](https://github.com/GuidoZam)", + "[Lars Fernhomberg](https://github.com/lafe)", + "[Mark Bice](https://github.com/mbice)", + "[Michaël Maillot](https://github.com/michaelmaillot)", + "[Nishkalank Bezawada](https://github.com/NishkalankBezawada)", + "[Tom G](https://github.com/t0mgerman)", + "[wuxiaojun514](https://github.com/wuxiaojun514)" + ] + }, { "version": "3.16.2", "changes": { diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce6ea1dd..aab3d886a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Releases +## 3.17.0 + +### Enhancements + +- `DyanmicForm`: Added file handling [#1625](https://github.com/pnp/sp-dev-fx-controls-react/pull/1625) +- `DynamicForm`: Custom Formatting and Validation, ControlsTestWebPart updates [#1672](https://github.com/pnp/sp-dev-fx-controls-react/pull/1672) +- `PeoplePicker`: Added custom filter to PeoplePicker selection [#1657](https://github.com/pnp/sp-dev-fx-controls-react/issues/1657) +- `RichText`: Align RichText heading styles and font sizes with OOB SharePoint text web part [#1706](https://github.com/pnp/sp-dev-fx-controls-react/pull/1706) + +### Fixes + +- Build fails due to missing @iconify/react dependency after upgrade to 3.16.0 [#1719](https://github.com/pnp/sp-dev-fx-controls-react/issues/1719) +- `ModernTaxonomyPicker`: not displaying suggestions when typing in values - API not found error [#1688](https://github.com/pnp/sp-dev-fx-controls-react/issues/1688) +- `DynamicForm`: Disable issue on fieldOverrides field control when onBeforeSubmit return true [#1715](https://github.com/pnp/sp-dev-fx-controls-react/issues/1715) +- `PeoplePicker`: PeoplePicker returns no results with webAbsoluteUrl and ensureUser [#1669](https://github.com/pnp/sp-dev-fx-controls-react/issues/1669) +- `DynamicForm`: [DynamicForm] Fixing multi taxonomy field (loading + saving existing item) [#1739](https://github.com/pnp/sp-dev-fx-controls-react/pull/1739) + +### Contributors + +Special thanks to our contributors (in alphabetical order): [Guido Zambarda](https://github.com/GuidoZam), [Lars Fernhomberg](https://github.com/lafe), [Mark Bice](https://github.com/mbice), [Michaël Maillot](https://github.com/michaelmaillot), [Nishkalank Bezawada](https://github.com/NishkalankBezawada), [Tom G](https://github.com/t0mgerman), [wuxiaojun514](https://github.com/wuxiaojun514). + ## 3.16.2 ### Fixes diff --git a/docs/documentation/docs/about/release-notes.md b/docs/documentation/docs/about/release-notes.md index 0ce6ea1dd..aab3d886a 100644 --- a/docs/documentation/docs/about/release-notes.md +++ b/docs/documentation/docs/about/release-notes.md @@ -1,5 +1,26 @@ # Releases +## 3.17.0 + +### Enhancements + +- `DyanmicForm`: Added file handling [#1625](https://github.com/pnp/sp-dev-fx-controls-react/pull/1625) +- `DynamicForm`: Custom Formatting and Validation, ControlsTestWebPart updates [#1672](https://github.com/pnp/sp-dev-fx-controls-react/pull/1672) +- `PeoplePicker`: Added custom filter to PeoplePicker selection [#1657](https://github.com/pnp/sp-dev-fx-controls-react/issues/1657) +- `RichText`: Align RichText heading styles and font sizes with OOB SharePoint text web part [#1706](https://github.com/pnp/sp-dev-fx-controls-react/pull/1706) + +### Fixes + +- Build fails due to missing @iconify/react dependency after upgrade to 3.16.0 [#1719](https://github.com/pnp/sp-dev-fx-controls-react/issues/1719) +- `ModernTaxonomyPicker`: not displaying suggestions when typing in values - API not found error [#1688](https://github.com/pnp/sp-dev-fx-controls-react/issues/1688) +- `DynamicForm`: Disable issue on fieldOverrides field control when onBeforeSubmit return true [#1715](https://github.com/pnp/sp-dev-fx-controls-react/issues/1715) +- `PeoplePicker`: PeoplePicker returns no results with webAbsoluteUrl and ensureUser [#1669](https://github.com/pnp/sp-dev-fx-controls-react/issues/1669) +- `DynamicForm`: [DynamicForm] Fixing multi taxonomy field (loading + saving existing item) [#1739](https://github.com/pnp/sp-dev-fx-controls-react/pull/1739) + +### Contributors + +Special thanks to our contributors (in alphabetical order): [Guido Zambarda](https://github.com/GuidoZam), [Lars Fernhomberg](https://github.com/lafe), [Mark Bice](https://github.com/mbice), [Michaël Maillot](https://github.com/michaelmaillot), [Nishkalank Bezawada](https://github.com/NishkalankBezawada), [Tom G](https://github.com/t0mgerman), [wuxiaojun514](https://github.com/wuxiaojun514). + ## 3.16.2 ### Fixes diff --git a/docs/documentation/docs/assets/DynamicFormWithFileSelection.png b/docs/documentation/docs/assets/DynamicFormWithFileSelection.png new file mode 100644 index 000000000..548b28573 Binary files /dev/null and b/docs/documentation/docs/assets/DynamicFormWithFileSelection.png differ diff --git a/docs/documentation/docs/controls/DynamicForm.md b/docs/documentation/docs/controls/DynamicForm.md index 19954739b..ce2326d4d 100644 --- a/docs/documentation/docs/controls/DynamicForm.md +++ b/docs/documentation/docs/controls/DynamicForm.md @@ -26,6 +26,17 @@ import { DynamicForm } from "@pnp/spfx-controls-react/lib/DynamicForm"; ``` ![DynamicForm](../assets/DynamicForm.png) +## File selection + +To upload a file when creating a new document in a document library you need to specify: +- enableFileSelection: Set this parameter to true to enable file selection. +- contentTypeId: This parameter specifies the target content type ID of the document you are creating. +- supportedFileExtensions: This parameter is optional and is used to specify the supported file extensions if they are different from the default ones. + +Enabling the file selection will display a new button on top of the form that allow the user to select a file from the recent files, browsing OneDrive or select and upload a file from the computer. + +![DynamicFormWithFileSelection](../assets/DynamicFormWithFileSelection.png) + ## Implementation The `DynamicForm` can be configured with the following properties: @@ -38,6 +49,7 @@ The `DynamicForm` can be configured with the following properties: | contentTypeId | string | no | content type ID | | disabled | boolean | no | Allows form to be disabled. Default value is `false`| | disabledFields | string[] | no | InternalName of fields that should be disabled. Default value is `false`| +| enableFileSelection | boolean | no | Specify if the form should support the creation of a new list item in a document library attaching a file to it. This option is only available for document libraries and works only when the contentTypeId is specified and has a base type of type Document. Default value is `false`| | hiddenFields | string[] | no | InternalName of fields that should be hidden. Default value is `false`| | onListItemLoaded | (listItemData: any) => Promise<void> | no | List item loaded handler. Allows to access list item information after it's loaded.| | onBeforeSubmit | (listItemData: any) => Promise<boolean> | no | Before submit handler. Allows to modify the object to be submitted or cancel the submission. To cancel, return `true`.| @@ -45,6 +57,7 @@ The `DynamicForm` can be configured with the following properties: | onSubmitError | (listItemData: any, error: Error) => void | no | Handler of submission error. | | onCancelled | () => void | no | Handler when form has been cancelled. | | returnListItemInstanceOnSubmit | boolean | no | Specifies if `onSubmitted` event should pass PnPJS list item (`IItem`) as a second parameter. Default - `true` | +| supportedFileExtensions | string[] | no | Specify the supported file extensions for the file picker. Only used when enableFileSelection is `true`. Default value is `["docx", "doc", "pptx", "ppt", "xlsx", "xls", "pdf"]`. | | webAbsoluteUrl | string | no | Absolute Web Url of target site (user requires permissions). | | fieldOverrides | {[columnInternalName: string] : {(fieldProperties: IDynamicFieldProps): React.ReactElement\}} | no | Key value pair for fields you want to override. Key is the internal field name, value is the function to be called for the custom element to render. | | respectEtag | boolean | no | Specifies if the form should respect the ETag of the item. Default - `true` | diff --git a/docs/documentation/docs/index.md b/docs/documentation/docs/index.md index 439f67729..c9d2a6e14 100644 --- a/docs/documentation/docs/index.md +++ b/docs/documentation/docs/index.md @@ -112,6 +112,7 @@ The following controls are currently available: - [TreeView](./controls/TreeView) (Tree View) - [UploadFiles](./controls/UploadFiles) (Upload Files) - [VariantThemeProvider](./controls/VariantThemeProvider) (Variant Theme Provider) +- [ViewPicker](./controls/ViewPicker.md) (View Picker Control) - [WebPartTitle](./controls/WebPartTitle) (Customizable web part title control) diff --git a/docs/documentation/mkdocs.yml b/docs/documentation/mkdocs.yml index 8b6a95790..97d24f4a8 100644 --- a/docs/documentation/mkdocs.yml +++ b/docs/documentation/mkdocs.yml @@ -73,6 +73,7 @@ nav: - TreeView: 'controls/TreeView.md' - UploadFiles: 'controls/UploadFiles.md' - VariantThemeProvider: 'controls/VariantThemeProvider.md' + - ViewPicker: 'controls/ViewPicker.md' - WebPartTitle: 'controls/WebPartTitle.md' - 'Field Controls': - 'Getting started': 'controls/fields/main.md' diff --git a/package-lock.json b/package-lock.json index 226aba494..da33efb08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pnp/spfx-controls-react", - "version": "3.16.2", + "version": "3.17.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pnp/spfx-controls-react", - "version": "3.16.2", + "version": "3.17.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -23,6 +23,7 @@ "@fluentui/scheme-utilities": "^8.2.12", "@fluentui/styles": "0.66.5", "@fluentui/theme": "^2.6.6", + "@iconify/react": "^4.1.1", "@microsoft/decorators": "1.18.2", "@microsoft/mgt-react": "3.1.3", "@microsoft/mgt-spfx": "3.1.3", @@ -74,7 +75,6 @@ "swiper": "^8.2.6" }, "devDependencies": { - "@iconify/react": "^4.1.1", "@microsoft/eslint-config-spfx": "1.18.2", "@microsoft/eslint-plugin-spfx": "1.18.2", "@microsoft/microsoft-graph-types": "^2.1.0", @@ -87,7 +87,7 @@ "@types/es6-promise": "3.3.0", "@types/he": "^1.1.2", "@types/jest": "25.2.3", - "@types/lodash": "4.14.194", + "@types/lodash": "4.14.202", "@types/quill": "^1.3.10", "@types/react": "17.0.45", "@types/react-addons-shallow-compare": "0.14.17", @@ -117,7 +117,7 @@ "react-test-renderer": "17.0.1", "request-promise": "4.2.5", "sonarqube-scanner": "2.8.2", - "spfx-fast-serve-helpers": "1.17.3", + "spfx-fast-serve-helpers": "1.18.4", "ts-jest": "^29.1.1", "tslib": "2.3.1", "typescript": "4.7.4", @@ -3270,7 +3270,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/@iconify/react/-/react-4.1.1.tgz", "integrity": "sha512-jed14EjvKjee8mc0eoscGxlg7mSQRkwQG3iX3cPBCO7UlOjz0DtlvTqxqEcHUJGh+z1VJ31Yhu5B9PxfO0zbdg==", - "dev": true, "dependencies": { "@iconify/types": "^2.0.0" }, @@ -3284,8 +3283,7 @@ "node_modules/@iconify/types": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", - "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", - "dev": true + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==" }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -11477,6 +11475,15 @@ "@types/node": "*" } }, + "node_modules/@types/cross-spawn": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.3.tgz", + "integrity": "sha512-BDAkU7WHHRHnvBf5z89lcvACsvkz/n7Tv+HyD/uW76O29HoH1Tk/W6iQrepaZVbisvlEek4ygwT8IW7ow9XLAA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/enzyme": { "version": "2.8.12", "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-2.8.12.tgz", @@ -11990,9 +11997,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.14.194", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.194.tgz", - "integrity": "sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==" + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" }, "node_modules/@types/lodash.isequal": { "version": "4.5.8", @@ -13844,18 +13851,6 @@ "node": ">=0.10.0" } }, - "node_modules/ansi-html": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", - "integrity": "sha512-JoAxEa1DfP9m2xfB/y2r/aKcwXNlltr4+0QSBC4TrLfcxyvepX2Pv0t/xpgGV5bGsDzCYV8SzjWgyCW0T9yYbA==", - "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "bin": { - "ansi-html": "bin/ansi-html" - } - }, "node_modules/ansi-html-community": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", @@ -16588,9 +16583,9 @@ "dev": true }, "node_modules/colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, "node_modules/colors": { @@ -19949,6 +19944,7 @@ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, + "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -19962,6 +19958,7 @@ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, + "peer": true, "engines": { "node": ">=6" } @@ -38872,15 +38869,16 @@ "dev": true }, "node_modules/spfx-fast-serve-helpers": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/spfx-fast-serve-helpers/-/spfx-fast-serve-helpers-1.17.3.tgz", - "integrity": "sha512-5/U4Vmm1HWjfaDH+tPUyKJjruKFcI4hksrGgFGX2fx1fQSNaU3e6Ec3N16T7bpViUSXxWF4zZ9eVkPxjGjMH6w==", + "version": "1.18.4", + "resolved": "https://registry.npmjs.org/spfx-fast-serve-helpers/-/spfx-fast-serve-helpers-1.18.4.tgz", + "integrity": "sha512-gJvw5bbhDQth3khWEKldKmgFlb4VERHyozozKf0fcy2rh8ZK3EKu77Tw0yu8KLuZm6GFeFRSSu4uecnuUu8AEw==", "dev": true, "dependencies": { - "@microsoft/loader-load-themed-styles": "2.0.27", - "@microsoft/spfx-heft-plugins": "1.17.4", + "@microsoft/loader-load-themed-styles": "2.0.45", + "@microsoft/spfx-heft-plugins": "1.18.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.7", "@types/copy-webpack-plugin": "6.4.3", + "@types/cross-spawn": "6.0.3", "@types/loader-utils": "2.0.2", "@types/webpack-dev-server": "3.11.4", "@types/yargs": "6.6.0", @@ -38888,6 +38886,7 @@ "clean-css-loader": "3.0.0", "colors": "1.4.0", "copy-webpack-plugin": "6.4.0", + "cross-spawn": "7.0.3", "css-loader": "5.2.4", "del": "6.0.0", "eslint-webpack-plugin": "2.5.4", @@ -38909,9 +38908,9 @@ "ts-loader": "8.1.0", "tsconfig": "7.0.0", "tsconfig-paths-webpack-plugin": "3.5.2", - "webpack": "4.44.2", - "webpack-cli": "4.6.0", - "webpack-dev-server": "3.11.2", + "webpack": "4.47.0", + "webpack-cli": "4.10.0", + "webpack-dev-server": "3.11.3", "webpack-merge": "5.7.3", "yargs": "4.6.0" }, @@ -38976,15 +38975,15 @@ } }, "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/loader-load-themed-styles": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/@microsoft/loader-load-themed-styles/-/loader-load-themed-styles-2.0.27.tgz", - "integrity": "sha512-TVr737vb95u/d6F3D0k1IAh5VNkBY9VFfYsrV3zIH1HRYrD/D8CpEF9kV6yk5jwg6LgS2JrxhJtBKlIiTvA9Yg==", + "version": "2.0.45", + "resolved": "https://registry.npmjs.org/@microsoft/loader-load-themed-styles/-/loader-load-themed-styles-2.0.45.tgz", + "integrity": "sha512-04foUzzYBbKpBKj16N9pjyKJzt6jthyd2gMzg1fQJPfrIblsJanumlhiUitpZjzyhs/53qYzEsTo0PZCKRHUpQ==", "dev": true, "dependencies": { "loader-utils": "1.4.2" }, "peerDependencies": { - "@microsoft/load-themed-styles": "^2.0.29", + "@microsoft/load-themed-styles": "^2.0.47", "@types/webpack": "^4" }, "peerDependenciesMeta": { @@ -39020,21 +39019,23 @@ } }, "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/rush-lib": { - "version": "5.93.1", - "resolved": "https://registry.npmjs.org/@microsoft/rush-lib/-/rush-lib-5.93.1.tgz", - "integrity": "sha512-8ZCSW4He9VPAAsF2T/OxVaTN06wLbzeEveOvEuwNJ5h6AQYPTtlH0yv8cDDuZqSEVgOv/gK4D+kAExOszYm06A==", + "version": "5.100.2", + "resolved": "https://registry.npmjs.org/@microsoft/rush-lib/-/rush-lib-5.100.2.tgz", + "integrity": "sha512-wuyvYok7qEdADNeN98C+tO5lU23CH04kSYbJ/lz4CQfqVIviFLQQExDEPnvRxNP0I1XmuMdsaIVG28m1tLCMMA==", "dev": true, "dependencies": { + "@pnpm/dependency-path": "~2.1.2", "@pnpm/link-bins": "~5.3.7", - "@rushstack/heft-config-file": "0.11.9", - "@rushstack/node-core-library": "3.55.2", - "@rushstack/package-deps-hash": "4.0.8", - "@rushstack/rig-package": "0.3.18", - "@rushstack/rush-amazon-s3-build-cache-plugin": "5.93.1", - "@rushstack/rush-azure-storage-build-cache-plugin": "5.93.1", - "@rushstack/stream-collator": "4.0.227", - "@rushstack/terminal": "0.5.2", - "@rushstack/ts-command-line": "4.13.2", + "@rushstack/heft-config-file": "0.13.2", + "@rushstack/node-core-library": "3.59.6", + "@rushstack/package-deps-hash": "4.0.41", + "@rushstack/package-extractor": "0.3.11", + "@rushstack/rig-package": "0.4.0", + "@rushstack/rush-amazon-s3-build-cache-plugin": "5.100.2", + "@rushstack/rush-azure-storage-build-cache-plugin": "5.100.2", + "@rushstack/stream-collator": "4.0.259", + "@rushstack/terminal": "0.5.34", + "@rushstack/ts-command-line": "4.15.1", "@types/node-fetch": "2.6.2", "@yarnpkg/lockfile": "~1.0.2", "builtin-modules": "~3.1.0", @@ -39049,16 +39050,12 @@ "ignore": "~5.1.6", "inquirer": "~7.3.3", "js-yaml": "~3.13.1", - "jszip": "~3.8.0", - "lodash": "~4.17.15", "node-fetch": "2.6.7", "npm-check": "~6.0.1", "npm-package-arg": "~6.1.0", - "npm-packlist": "~2.1.2", "read-package-tree": "~5.1.5", - "resolve": "~1.22.1", "rxjs": "~6.6.7", - "semver": "~7.3.0", + "semver": "~7.5.4", "ssri": "~8.0.0", "strict-uri-encode": "~2.0.0", "tapable": "2.2.1", @@ -39069,6 +39066,24 @@ "node": ">=5.6.0" } }, + "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/rush-lib/node_modules/@rushstack/terminal": { + "version": "0.5.34", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.5.34.tgz", + "integrity": "sha512-Q7YDkPTsvJZpHapapo5sK2VCxW7byoqhK89tXMUiva6dNwelomgEe0S+njKw4vcmGde4hQD7LAqQPJPYFeU4mw==", + "dev": true, + "dependencies": { + "@rushstack/node-core-library": "3.59.6", + "wordwrap": "~1.0.0" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/rush-lib/node_modules/colors": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", @@ -39098,31 +39113,14 @@ } } }, - "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/rush-lib/node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/sp-css-loader": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/@microsoft/sp-css-loader/-/sp-css-loader-1.17.4.tgz", - "integrity": "sha512-HBzv+/cu1Mxc5j0LA04EhoXndaNhCGk4Xhqy1KZioNSZgz5DbrsEWtNklexy0wXoJP+dbro+mtZYb/B07EvV6Q==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@microsoft/sp-css-loader/-/sp-css-loader-1.18.0.tgz", + "integrity": "sha512-UFfmsN+3+WcEHx8fEWJoOMTP3pOTTkFAxwa9aEtKxnrT21wfqLnJfzll1ato2X0vT3eYzkCFtrspCeT1atLURw==", "dev": true, "dependencies": { "@microsoft/load-themed-styles": "1.10.292", - "@rushstack/node-core-library": "3.55.2", + "@rushstack/node-core-library": "3.59.6", "autoprefixer": "9.7.1", "css-loader": "3.4.2", "cssnano": "~5.1.14", @@ -39132,7 +39130,7 @@ "postcss-modules-local-by-default": "~4.0.0", "postcss-modules-scope": "~3.0.0", "postcss-modules-values": "~4.0.0", - "webpack": "~4.44.2" + "webpack": "~4.47.0" } }, "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/sp-css-loader/node_modules/@microsoft/load-themed-styles": { @@ -39384,38 +39382,38 @@ } }, "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/sp-module-interfaces": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/@microsoft/sp-module-interfaces/-/sp-module-interfaces-1.17.4.tgz", - "integrity": "sha512-+tVV2O9B5i2RXdziEvg9FnKTBc2FgFn1XxbCfpmUj+F/Gh3PMtG0XyquBFY12jjxObEIv78J0A0fK2x0shZMLw==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@microsoft/sp-module-interfaces/-/sp-module-interfaces-1.18.0.tgz", + "integrity": "sha512-fXLV70zP1S8z2FGYAf1iqfgIIC5rOfPQeeCh/qICFx+RuUFtvkbW+N5vr0ugFYaF6L0rfrYqspcllloHJPOVYQ==", "dev": true, "dependencies": { - "@rushstack/node-core-library": "3.55.2", + "@rushstack/node-core-library": "3.59.6", "z-schema": "4.2.4" }, "engines": { - "node": ">=12.13.0 <13.0.0 || >=14.15.0 <15.0.0 || >=16.13.0 <17.0.0" + "node": ">=16.13.0 <17.0.0 || >=18.17.1 <19.0.0" } }, "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/spfx-heft-plugins": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/@microsoft/spfx-heft-plugins/-/spfx-heft-plugins-1.17.4.tgz", - "integrity": "sha512-BOTYm5H1coXpgp529PbI1XtrNGSI42c2EwxuR48ZThM20N8OagQeto5wpQh4z2wqdUhDpFVLu5gFqAEmG5v1Bg==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@microsoft/spfx-heft-plugins/-/spfx-heft-plugins-1.18.0.tgz", + "integrity": "sha512-tWj8mtnz4+gi9LUV/XIIArHw53fPXOs1R9eLh2hm/FcB5d3AMsDObhLyna+XjTY2JpJtsvRjC4A1nypHlG2uVQ==", "dev": true, "dependencies": { "@azure/storage-blob": "~12.11.0", "@microsoft/load-themed-styles": "1.10.292", - "@microsoft/loader-load-themed-styles": "2.0.27", - "@microsoft/rush-lib": "5.93.1", - "@microsoft/sp-css-loader": "1.17.4", - "@microsoft/sp-module-interfaces": "1.17.4", - "@rushstack/heft-config-file": "0.11.9", - "@rushstack/localization-utilities": "0.8.46", - "@rushstack/node-core-library": "3.55.2", - "@rushstack/rig-package": "0.3.18", - "@rushstack/set-webpack-public-path-plugin": "3.3.91", - "@rushstack/terminal": "0.5.2", - "@rushstack/webpack4-localization-plugin": "0.17.2", - "@rushstack/webpack4-module-minifier-plugin": "0.9.40", + "@microsoft/loader-load-themed-styles": "2.0.68", + "@microsoft/rush-lib": "5.100.2", + "@microsoft/sp-css-loader": "1.18.0", + "@microsoft/sp-module-interfaces": "1.18.0", + "@rushstack/heft-config-file": "0.13.2", + "@rushstack/localization-utilities": "0.8.80", + "@rushstack/node-core-library": "3.59.6", + "@rushstack/rig-package": "0.4.0", + "@rushstack/set-webpack-public-path-plugin": "4.0.15", + "@rushstack/terminal": "0.5.36", + "@rushstack/webpack4-localization-plugin": "0.17.46", + "@rushstack/webpack4-module-minifier-plugin": "0.12.35", "@types/tapable": "1.0.6", "autoprefixer": "9.7.1", "colors": "~1.2.1", @@ -39427,19 +39425,18 @@ "git-repo-info": "~2.1.1", "glob": "~7.0.5", "html-loader": "~0.5.1", - "jszip": "3.5.0", + "jszip": "~3.8.0", "lodash": "4.17.21", "mime": "2.5.2", "postcss": "^8.4.19", "postcss-loader": "^4.2.0", "resolve": "~1.17.0", - "sass": "1.49.11", "source-map": "0.6.1", "source-map-loader": "1.1.3", "tapable": "1.1.3", "true-case-path": "~2.2.1", "uuid": "~3.1.0", - "webpack": "~4.44.2", + "webpack": "~4.47.0", "webpack-dev-server": "~4.9.3", "webpack-sources": "1.4.3", "xml": "~1.0.1" @@ -39451,15 +39448,46 @@ "integrity": "sha512-LQWGImtpv2zHKIPySLalR1aFXumXfOq8UuJvR15mIZRKXIoM+KuN9wZq+ved2FyeuePjQSJGOxYynxtCLLwDBA==", "dev": true }, + "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/spfx-heft-plugins/node_modules/@microsoft/loader-load-themed-styles": { + "version": "2.0.68", + "resolved": "https://registry.npmjs.org/@microsoft/loader-load-themed-styles/-/loader-load-themed-styles-2.0.68.tgz", + "integrity": "sha512-rScfOP4hEO+zZlhaf0vPzj1I4mVm4XJgACBJ4ym4Z/zT5kt7XkEvlcoCNqr4lbwBvNrafUL9b6GFOTGE6Y8fmg==", + "dev": true, + "dependencies": { + "loader-utils": "1.4.2" + }, + "peerDependencies": { + "@microsoft/load-themed-styles": "^2.0.70", + "@types/webpack": "^4" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + } + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/spfx-heft-plugins/node_modules/@microsoft/loader-load-themed-styles/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/spfx-heft-plugins/node_modules/@rushstack/webpack4-module-minifier-plugin": { - "version": "0.9.40", - "resolved": "https://registry.npmjs.org/@rushstack/webpack4-module-minifier-plugin/-/webpack4-module-minifier-plugin-0.9.40.tgz", - "integrity": "sha512-QOoeFPTPlKaIkMBTB/zqYZGbvVYAc07/xRQlGTSEwDY07IbIXy/HEq8vvMXAXGtofqBVP8s8wZtfR6z0kAt9Xw==", + "version": "0.12.35", + "resolved": "https://registry.npmjs.org/@rushstack/webpack4-module-minifier-plugin/-/webpack4-module-minifier-plugin-0.12.35.tgz", + "integrity": "sha512-/tHFN9iuKbsDt0GfSU/XQQEND9XkD1EkDkmQkSsc45YKnip7kCLRN8bpJL410MBiWIMOTWglkafVyiS9pyZ6bw==", "dev": true, "dependencies": { - "@rushstack/module-minifier": "0.1.41", - "@rushstack/worker-pool": "0.1.41", - "@types/node": "12.20.24", + "@rushstack/module-minifier": "0.3.38", + "@rushstack/worker-pool": "0.3.37", "@types/tapable": "1.0.6", "tapable": "1.1.3" }, @@ -39467,12 +39495,16 @@ "node": ">=10.17.1" }, "peerDependencies": { + "@types/node": "*", "@types/webpack": "*", "@types/webpack-sources": "*", "webpack": "^4.31.0", "webpack-sources": "~1.4.3" }, "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, "@types/webpack": { "optional": true }, @@ -39764,18 +39796,6 @@ "json5": "lib/cli.js" } }, - "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/spfx-heft-plugins/node_modules/jszip": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz", - "integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==", - "dev": true, - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "set-immediate-shim": "~1.0.1" - } - }, "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/spfx-heft-plugins/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -39928,23 +39948,6 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, - "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/spfx-heft-plugins/node_modules/sass": { - "version": "1.49.11", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.11.tgz", - "integrity": "sha512-wvS/geXgHUGs6A/4ud5BFIWKO1nKd7wYIGimDk4q4GFkJicILActpv9ueMT4eRGSsp1BdKHuw1WwAHXbhsJELQ==", - "dev": true, - "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/spfx-fast-serve-helpers/node_modules/@microsoft/spfx-heft-plugins/node_modules/serialize-javascript": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", @@ -40071,13 +40074,13 @@ } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/heft-config-file": { - "version": "0.11.9", - "resolved": "https://registry.npmjs.org/@rushstack/heft-config-file/-/heft-config-file-0.11.9.tgz", - "integrity": "sha512-01JFmD+G44v5btO0fVIbVBJCfGWLTN2l4Y/+IVU8D9eR14+wYJjV5CO25uxydDynMr334URFcITuzG21L9L0GA==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@rushstack/heft-config-file/-/heft-config-file-0.13.2.tgz", + "integrity": "sha512-eJCuVnKR+uSG7qyeyICA57IOBD3OoOlNTpsJgNjcZZiTj+ZlKPaGmJ8/mzXwNiEpTIlRsVvoQURYFz9QY9sfnQ==", "dev": true, "dependencies": { - "@rushstack/node-core-library": "3.55.2", - "@rushstack/rig-package": "0.3.18", + "@rushstack/node-core-library": "3.59.6", + "@rushstack/rig-package": "0.4.0", "jsonpath-plus": "~4.0.0" }, "engines": { @@ -40085,28 +40088,35 @@ } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/localization-utilities": { - "version": "0.8.46", - "resolved": "https://registry.npmjs.org/@rushstack/localization-utilities/-/localization-utilities-0.8.46.tgz", - "integrity": "sha512-CjSQ+gYSefFLOpulTeoeYfSTqZh84KiCQxcb5BeefChAdhcHpYMVxmLsWQrA0WX2Al1Tw/NZ/QahYytl4E6kXw==", + "version": "0.8.80", + "resolved": "https://registry.npmjs.org/@rushstack/localization-utilities/-/localization-utilities-0.8.80.tgz", + "integrity": "sha512-kEM8v6ULA3ReikAmdP4faFWMDG4WcATty3lDU2/XFKh2+oj6HLDtnyUgDpYBaASx2FQstu5f5J7QehTLcl21MA==", "dev": true, "dependencies": { - "@rushstack/node-core-library": "3.55.2", - "@rushstack/typings-generator": "0.10.2", + "@rushstack/node-core-library": "3.59.6", + "@rushstack/typings-generator": "0.10.36", "pseudolocale": "~1.1.0", "xmldoc": "~1.1.2" } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/module-minifier": { - "version": "0.1.41", - "resolved": "https://registry.npmjs.org/@rushstack/module-minifier/-/module-minifier-0.1.41.tgz", - "integrity": "sha512-dvj/QSknUY+Q54Vv398BbX/CynobE2h8V4F/mnWi3nXX848NFOcgGHSe8UhH1cMdsz7EGrBcUG4kJctMvsXJ3A==", + "version": "0.3.38", + "resolved": "https://registry.npmjs.org/@rushstack/module-minifier/-/module-minifier-0.3.38.tgz", + "integrity": "sha512-o0HzguvsC+VUbpg8gqNCsE9myZ4s6ZIGZggPTR26Qz33yIKvnBHVwHkDu191Y3N1cqMYgVwcZznSUSWifV3qOw==", "dev": true, "dependencies": { - "@rushstack/worker-pool": "0.1.41", - "@types/node": "12.20.24", + "@rushstack/worker-pool": "0.3.37", "serialize-javascript": "6.0.0", "source-map": "~0.7.3", - "terser": "5.9.0" + "terser": "^5.9.0" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/module-minifier/node_modules/serialize-javascript": { @@ -40128,9 +40138,9 @@ } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/node-core-library": { - "version": "3.55.2", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.55.2.tgz", - "integrity": "sha512-SaLe/x/Q/uBVdNFK5V1xXvsVps0y7h1sN7aSJllQyFbugyOaxhNRF25bwEDnicARNEjJw0pk0lYnJQ9Kr6ev0A==", + "version": "3.59.6", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.59.6.tgz", + "integrity": "sha512-bMYJwNFfWXRNUuHnsE9wMlW/mOB4jIwSUkRKtu02CwZhQdmzMsUbxE0s1xOLwTpNIwlzfW/YT7OnOHgDffLgYg==", "dev": true, "dependencies": { "colors": "~1.2.1", @@ -40138,7 +40148,7 @@ "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", - "semver": "~7.3.0", + "semver": "~7.5.4", "z-schema": "~5.0.2" }, "peerDependencies": { @@ -40207,18 +40217,51 @@ } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/package-deps-hash": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@rushstack/package-deps-hash/-/package-deps-hash-4.0.8.tgz", - "integrity": "sha512-ad2ZnGWLlcga4GVRVo3mibkTrrnDs8xvABTr79z7zwA43htaVbddwFs3Y+tyqaPo8s92Tqh47jzrGDJTqm6Vyg==", + "version": "4.0.41", + "resolved": "https://registry.npmjs.org/@rushstack/package-deps-hash/-/package-deps-hash-4.0.41.tgz", + "integrity": "sha512-bx1g0I54BidJuIqyQHY2Vr4Azn2ThLgrc6hHjEIBzIVmXeznZxJfYViAPNFAu7BV/TaLIU1BSYeRn/yObu9KZA==", "dev": true, "dependencies": { - "@rushstack/node-core-library": "3.55.2" + "@rushstack/node-core-library": "3.59.6" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/package-extractor": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@rushstack/package-extractor/-/package-extractor-0.3.11.tgz", + "integrity": "sha512-j5hRGB/ilCozT7qH5q3swM/xdf/TPFtolWkqciYCU8G8WFXxILbN2nwo4goWyWQaD9hFlCiw9S7z8LTEkSmapQ==", + "dev": true, + "dependencies": { + "@pnpm/link-bins": "~5.3.7", + "@rushstack/node-core-library": "3.59.6", + "@rushstack/terminal": "0.5.34", + "ignore": "~5.1.6", + "jszip": "~3.8.0", + "minimatch": "~3.0.3", + "npm-packlist": "~2.1.2" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/package-extractor/node_modules/@rushstack/terminal": { + "version": "0.5.34", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.5.34.tgz", + "integrity": "sha512-Q7YDkPTsvJZpHapapo5sK2VCxW7byoqhK89tXMUiva6dNwelomgEe0S+njKw4vcmGde4hQD7LAqQPJPYFeU4mw==", + "dev": true, + "dependencies": { + "@rushstack/node-core-library": "3.59.6", + "wordwrap": "~1.0.0" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/rig-package": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.18.tgz", - "integrity": "sha512-SGEwNTwNq9bI3pkdd01yCaH+gAsHqs0uxfGvtw9b0LJXH52qooWXnrFTRRLG1aL9pf+M2CARdrA9HLHJys3jiQ==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.4.0.tgz", + "integrity": "sha512-FnM1TQLJYwSiurP6aYSnansprK5l8WUK8VG38CmAaZs29ZeL1msjK0AP1VS4ejD33G0kE/2cpsPsS9jDenBMxw==", "dev": true, "dependencies": { "resolve": "~1.22.1", @@ -40243,13 +40286,13 @@ } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/rush-amazon-s3-build-cache-plugin": { - "version": "5.93.1", - "resolved": "https://registry.npmjs.org/@rushstack/rush-amazon-s3-build-cache-plugin/-/rush-amazon-s3-build-cache-plugin-5.93.1.tgz", - "integrity": "sha512-urEQ+u7oSdfQnbuuVURbTE3RaJVh7rOSyB8RN2xAYh88HveYMeduq3EU5/0afHKnRs/UJG/iwt6EqCbXRR0J+w==", + "version": "5.100.2", + "resolved": "https://registry.npmjs.org/@rushstack/rush-amazon-s3-build-cache-plugin/-/rush-amazon-s3-build-cache-plugin-5.100.2.tgz", + "integrity": "sha512-A49NzlRDcp0Hd5WZWN8jvnvI+0MoFOdRXL3iutVI12YAYBH6c7uSul+71MMY83x0yQqk4TcfGYVpFWx1j/n8/Q==", "dev": true, "dependencies": { - "@rushstack/node-core-library": "3.55.2", - "@rushstack/rush-sdk": "5.93.1", + "@rushstack/node-core-library": "3.59.6", + "@rushstack/rush-sdk": "5.100.2", "https-proxy-agent": "~5.0.0", "node-fetch": "2.6.7" } @@ -40275,55 +40318,74 @@ } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/rush-azure-storage-build-cache-plugin": { - "version": "5.93.1", - "resolved": "https://registry.npmjs.org/@rushstack/rush-azure-storage-build-cache-plugin/-/rush-azure-storage-build-cache-plugin-5.93.1.tgz", - "integrity": "sha512-urbl28yUit+GJ4cgU9iAfWEhu6bP0/kdBaQEsOTYoLYRGnF0uBJ6O+46aMOp4WsqxAk+K+xL6ixw1ZE1BTix6g==", + "version": "5.100.2", + "resolved": "https://registry.npmjs.org/@rushstack/rush-azure-storage-build-cache-plugin/-/rush-azure-storage-build-cache-plugin-5.100.2.tgz", + "integrity": "sha512-FIAvmIfYLWhnygDCyUWSZOuyTWVRLFHYeG9xPmUpwJSPqxUL3HG5cRGVYlyRgK9oSJSEq+g0mpbe7nE8WwJgtg==", "dev": true, "dependencies": { "@azure/identity": "~2.1.0", "@azure/storage-blob": "~12.11.0", - "@rushstack/node-core-library": "3.55.2", - "@rushstack/rush-sdk": "5.93.1", - "@rushstack/terminal": "0.5.2" + "@rushstack/node-core-library": "3.59.6", + "@rushstack/rush-sdk": "5.100.2", + "@rushstack/terminal": "0.5.34" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/rush-azure-storage-build-cache-plugin/node_modules/@rushstack/terminal": { + "version": "0.5.34", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.5.34.tgz", + "integrity": "sha512-Q7YDkPTsvJZpHapapo5sK2VCxW7byoqhK89tXMUiva6dNwelomgEe0S+njKw4vcmGde4hQD7LAqQPJPYFeU4mw==", + "dev": true, + "dependencies": { + "@rushstack/node-core-library": "3.59.6", + "wordwrap": "~1.0.0" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/rush-sdk": { - "version": "5.93.1", - "resolved": "https://registry.npmjs.org/@rushstack/rush-sdk/-/rush-sdk-5.93.1.tgz", - "integrity": "sha512-rHfGvxyiR6nO5nqruqz/0N3GpAIi4P565FYcadnHsK791ncoh60lBHvQU9b9oRdpZjl2dHjoAQrr+pgSgOY/vw==", + "version": "5.100.2", + "resolved": "https://registry.npmjs.org/@rushstack/rush-sdk/-/rush-sdk-5.100.2.tgz", + "integrity": "sha512-+4DKbXj6R8vilRYswH8Lb+WIuIoD29/ZjMmazKBKXJTm3x7sgGJy45ozAZbfeXvdOTzqsg11NzIbwaDm8rRhLQ==", "dev": true, "dependencies": { - "@rushstack/node-core-library": "3.55.2", + "@rushstack/node-core-library": "3.59.6", "@types/node-fetch": "2.6.2", "tapable": "2.2.1" } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/set-webpack-public-path-plugin": { - "version": "3.3.91", - "resolved": "https://registry.npmjs.org/@rushstack/set-webpack-public-path-plugin/-/set-webpack-public-path-plugin-3.3.91.tgz", - "integrity": "sha512-2Bvac24WHZagQC+zLk+eksqxfeX2OhMH+eLPXEWvpuYceWYbqphckGLotj0WeAmSvjNTrTxxDfhTCHTWLIOAjw==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@rushstack/set-webpack-public-path-plugin/-/set-webpack-public-path-plugin-4.0.15.tgz", + "integrity": "sha512-TwXZVRPV0wRrjDfAYGXU38FTFihHjUDIn5iRWtu6rn/MCXNR6y4OwPVg5MlSVbqn/hU8WnmML6/hT54XCdOfPQ==", "dev": true, "dependencies": { - "@rushstack/webpack-plugin-utilities": "0.1.56" + "@rushstack/node-core-library": "3.59.6", + "@rushstack/webpack-plugin-utilities": "0.2.36" }, "peerDependencies": { - "@types/webpack": "^4.39.8", - "webpack": "^5.35.1" + "@types/webpack": "^4.39.8" }, "peerDependenciesMeta": { "@types/webpack": { "optional": true - }, - "webpack": { - "optional": true } } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/set-webpack-public-path-plugin/node_modules/@rushstack/webpack-plugin-utilities": { - "version": "0.1.56", - "resolved": "https://registry.npmjs.org/@rushstack/webpack-plugin-utilities/-/webpack-plugin-utilities-0.1.56.tgz", - "integrity": "sha512-PaSnWl0rU0CqB8PYh6ATBkM94FlC37tDm904ywBADPeQj/ZiykaIHhRLeFz93vcUBsJvmofFScMYHuPhpNx2qA==", + "version": "0.2.36", + "resolved": "https://registry.npmjs.org/@rushstack/webpack-plugin-utilities/-/webpack-plugin-utilities-0.2.36.tgz", + "integrity": "sha512-LguxiG0b6AKSxUODKbmPqHr9Q08weilpK3qOiyzYMqIQ5nR3WOGoflaYbO/kDsKbjgLyxQWL2XPZdyyYke3gjg==", "dev": true, + "dependencies": { + "memfs": "3.4.3", + "webpack-merge": "~5.8.0" + }, "peerDependencies": { "@types/webpack": "^4.39.8", "webpack": "^5.35.1" @@ -40337,23 +40399,239 @@ } } }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/set-webpack-public-path-plugin/node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/set-webpack-public-path-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/set-webpack-public-path-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/set-webpack-public-path-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/set-webpack-public-path-plugin/node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/set-webpack-public-path-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/set-webpack-public-path-plugin/node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/set-webpack-public-path-plugin/node_modules/webpack": { + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/set-webpack-public-path-plugin/node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/set-webpack-public-path-plugin/node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/stream-collator": { - "version": "4.0.227", - "resolved": "https://registry.npmjs.org/@rushstack/stream-collator/-/stream-collator-4.0.227.tgz", - "integrity": "sha512-SLHwjWqUlEfqA6KfLkSmZSr28/2Z5BxWnqtXqtLDFndZUuHUiUDg85w8GtS9MyZXMOfAjj9pS7Xi764bjsOKBA==", + "version": "4.0.259", + "resolved": "https://registry.npmjs.org/@rushstack/stream-collator/-/stream-collator-4.0.259.tgz", + "integrity": "sha512-UfMRCp1avkUUs9pdtWQ8ZE8Nmuxeuw1a9bjLQ7cQJ3meuv8iDxKuxsyJRfrwIfCkVkNVw5OJ9eM6E/edUPP7qw==", "dev": true, "dependencies": { - "@rushstack/node-core-library": "3.55.2", - "@rushstack/terminal": "0.5.2" + "@rushstack/node-core-library": "3.59.6", + "@rushstack/terminal": "0.5.34" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/stream-collator/node_modules/@rushstack/terminal": { + "version": "0.5.34", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.5.34.tgz", + "integrity": "sha512-Q7YDkPTsvJZpHapapo5sK2VCxW7byoqhK89tXMUiva6dNwelomgEe0S+njKw4vcmGde4hQD7LAqQPJPYFeU4mw==", + "dev": true, + "dependencies": { + "@rushstack/node-core-library": "3.59.6", + "wordwrap": "~1.0.0" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/terminal": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.5.2.tgz", - "integrity": "sha512-zyzUQLUkDhRdKIvEk94WforJHCITedizbr1215pSONRwWS8MQEMTcDY+dBz+U8Ar4s/4oJAtFuT5cHP/uTYYdw==", + "version": "0.5.36", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.5.36.tgz", + "integrity": "sha512-PMigbJYHuiKYe4IxA9pInLSFjOAQI4NV7OmIhTuh8Jy+YYjSexmQfnYwBqsZrwah4k/apY7VZ7lQucHxhJFiiQ==", "dev": true, "dependencies": { - "@rushstack/node-core-library": "3.55.2", + "@rushstack/node-core-library": "3.59.6", "wordwrap": "~1.0.0" }, "peerDependencies": { @@ -40366,9 +40644,9 @@ } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/ts-command-line": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.13.2.tgz", - "integrity": "sha512-bCU8qoL9HyWiciltfzg7GqdfODUeda/JpI0602kbN5YH22rzTxyqYvv7aRLENCM7XCQ1VRs7nMkEqgJUOU8Sag==", + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.15.1.tgz", + "integrity": "sha512-EL4jxZe5fhb1uVL/P/wQO+Z8Rc8FMiWJ1G7VgnPDvdIt5GVjRfK7vwzder1CZQiX3x0PY6uxENYLNGTFd1InRQ==", "dev": true, "dependencies": { "@types/argparse": "1.0.38", @@ -40387,12 +40665,12 @@ } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/typings-generator": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@rushstack/typings-generator/-/typings-generator-0.10.2.tgz", - "integrity": "sha512-0T0dkv3QNnpGLHjdogn+7wTw+4aRJVlMPIpWIr30DlGQ62XxcbP5sVUN45JcWRtYXriUurXi9dgzSQZv94nJwg==", + "version": "0.10.36", + "resolved": "https://registry.npmjs.org/@rushstack/typings-generator/-/typings-generator-0.10.36.tgz", + "integrity": "sha512-9aB/D8lI+fbmM5LzPgGcUJzuw+Xg4FixGuQVnis70Bss+5SU6YzOk/bfN4/xhSghMzG+AI7S87368x37TgeQtA==", "dev": true, "dependencies": { - "@rushstack/node-core-library": "3.55.2", + "@rushstack/node-core-library": "3.59.6", "chokidar": "~3.4.0", "glob": "~7.0.5" }, @@ -40406,20 +40684,19 @@ } }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/webpack4-localization-plugin": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/@rushstack/webpack4-localization-plugin/-/webpack4-localization-plugin-0.17.2.tgz", - "integrity": "sha512-CRVWQUPqtXPvpGkCC5OYRVdDM9iVCK7NO294MNx0LG8P7+b24M0o9a8hvYAv8802gyONdkEDvObT29RvhwQYhA==", + "version": "0.17.46", + "resolved": "https://registry.npmjs.org/@rushstack/webpack4-localization-plugin/-/webpack4-localization-plugin-0.17.46.tgz", + "integrity": "sha512-wEEVp6oBp5/OIrRzwgkuuQlawUY6MfjaWsp2T9Zp4MkbqGVgF+gdKG+iKzWtBKW2YbZ9fnVZJH23FoWwh81w4w==", "dev": true, "dependencies": { - "@rushstack/localization-utilities": "0.8.46", - "@rushstack/node-core-library": "3.55.2", + "@rushstack/localization-utilities": "0.8.83", + "@rushstack/node-core-library": "3.59.7", "@types/tapable": "1.0.6", "loader-utils": "1.4.2", - "lodash": "~4.17.15", "minimatch": "~3.0.3" }, "peerDependencies": { - "@rushstack/set-webpack-public-path-plugin": "^3.3.91", + "@rushstack/set-webpack-public-path-plugin": "^4.0.16", "@types/node": "*", "@types/webpack": "^4.39.0", "webpack": "^4.31.0" @@ -40436,6 +40713,79 @@ } } }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/webpack4-localization-plugin/node_modules/@rushstack/localization-utilities": { + "version": "0.8.83", + "resolved": "https://registry.npmjs.org/@rushstack/localization-utilities/-/localization-utilities-0.8.83.tgz", + "integrity": "sha512-0Wjvg/3686xgLIjX4aCxNoOfWb1BOpuckzNMjEK5MZyCEFz4Ral+ln13zP+AMKGGWcdxsYdWs+n1yfkJKEX9fQ==", + "dev": true, + "dependencies": { + "@rushstack/node-core-library": "3.59.7", + "@rushstack/typings-generator": "0.11.1", + "pseudolocale": "~1.1.0", + "xmldoc": "~1.1.2" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/webpack4-localization-plugin/node_modules/@rushstack/node-core-library": { + "version": "3.59.7", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.59.7.tgz", + "integrity": "sha512-ln1Drq0h+Hwa1JVA65x5mlSgUrBa1uHL+V89FqVWQgXd1vVIMhrtqtWGQrhTnFHxru5ppX+FY39VWELF/FjQCw==", + "dev": true, + "dependencies": { + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "z-schema": "~5.0.2" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/webpack4-localization-plugin/node_modules/@rushstack/typings-generator": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@rushstack/typings-generator/-/typings-generator-0.11.1.tgz", + "integrity": "sha512-pcnA9r14xl1TE4QXW6+t6yGP/5JfGZEGixlL6NH6PHjQVXAFnw91EXvc2NteslePTNdjPuR/34uLqE0i57WNpw==", + "dev": true, + "dependencies": { + "@rushstack/node-core-library": "3.59.7", + "chokidar": "~3.4.0", + "fast-glob": "~3.2.4" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/webpack4-localization-plugin/node_modules/colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", + "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/webpack4-localization-plugin/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/webpack4-localization-plugin/node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -40462,13 +40812,55 @@ "node": ">=4.0.0" } }, - "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/worker-pool": { - "version": "0.1.41", - "resolved": "https://registry.npmjs.org/@rushstack/worker-pool/-/worker-pool-0.1.41.tgz", - "integrity": "sha512-n2NC9Pr/VLs2iYNA4oB+/usl5iBIu0n3s3Mf4DT4UHSREJz8NJuxtGgLxCsJgINkCGz2VSEImZniNeIkNF1jpQ==", + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/webpack4-localization-plugin/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "dependencies": { - "@types/node": "12.20.24" + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/webpack4-localization-plugin/node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dev": true, + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@rushstack/worker-pool": { + "version": "0.3.37", + "resolved": "https://registry.npmjs.org/@rushstack/worker-pool/-/worker-pool-0.3.37.tgz", + "integrity": "sha512-KVuklmysCkNdRxTcLb80MNEBG/KrDL74c+1XIYZlTvSlDnTs5j9gdjKIV73lZmYox+SWTpvUWrP6JhWb2noDJg==", + "dev": true, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/spfx-fast-serve-helpers/node_modules/@types/eslint": { @@ -40503,6 +40895,161 @@ "integrity": "sha512-021+XKlD4/hDZkkdgGhgtDGKlcTIXrII1lrCLp/ZNPoU0AHN9HmTNe+i1eKRxcZisFObX3ItTncemegEACgnsw==", "dev": true }, + "node_modules/spfx-fast-serve-helpers/node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, "node_modules/spfx-fast-serve-helpers/node_modules/acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -40673,12 +41220,6 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, - "node_modules/spfx-fast-serve-helpers/node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true - }, "node_modules/spfx-fast-serve-helpers/node_modules/colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", @@ -40688,12 +41229,6 @@ "node": ">=0.1.90" } }, - "node_modules/spfx-fast-serve-helpers/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, "node_modules/spfx-fast-serve-helpers/node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -40927,6 +41462,21 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, + "node_modules/spfx-fast-serve-helpers/node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/spfx-fast-serve-helpers/node_modules/eslint": { "version": "7.32.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", @@ -41099,15 +41649,6 @@ "node": ">=4" } }, - "node_modules/spfx-fast-serve-helpers/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/spfx-fast-serve-helpers/node_modules/express": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", @@ -41178,6 +41719,22 @@ "node": ">=0.10.0" } }, + "node_modules/spfx-fast-serve-helpers/node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/spfx-fast-serve-helpers/node_modules/file-loader": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", @@ -41297,6 +41854,12 @@ "node": ">=6" } }, + "node_modules/spfx-fast-serve-helpers/node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "dev": true + }, "node_modules/spfx-fast-serve-helpers/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -41619,6 +42182,17 @@ "node": ">=0.10.0" } }, + "node_modules/spfx-fast-serve-helpers/node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=6.11.5" + } + }, "node_modules/spfx-fast-serve-helpers/node_modules/loader-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", @@ -41646,6 +42220,30 @@ "node": ">=6" } }, + "node_modules/spfx-fast-serve-helpers/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/spfx-fast-serve-helpers/node_modules/memfs": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.3.tgz", + "integrity": "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg==", + "dev": true, + "dependencies": { + "fs-monkey": "1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/spfx-fast-serve-helpers/node_modules/memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -41918,6 +42516,21 @@ } ] }, + "node_modules/spfx-fast-serve-helpers/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/spfx-fast-serve-helpers/node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -42043,32 +42656,6 @@ "node": ">=6" } }, - "node_modules/spfx-fast-serve-helpers/node_modules/terser": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", - "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", - "dev": true, - "dependencies": { - "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/spfx-fast-serve-helpers/node_modules/terser/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/spfx-fast-serve-helpers/node_modules/to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", @@ -42120,68 +42707,34 @@ "node": ">= 0.10" } }, - "node_modules/spfx-fast-serve-helpers/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, - "node_modules/spfx-fast-serve-helpers/node_modules/webpack": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.2.tgz", - "integrity": "sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q==", + "node_modules/spfx-fast-serve-helpers/node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", "dev": true, + "optional": true, + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/wasm-edit": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "acorn": "^6.4.1", - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.3.0", - "eslint-scope": "^4.0.3", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.4.0", - "loader-utils": "^1.2.3", - "memory-fs": "^0.4.1", - "micromatch": "^3.1.10", - "mkdirp": "^0.5.3", - "neo-async": "^2.6.1", - "node-libs-browser": "^2.2.1", - "schema-utils": "^1.0.0", - "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.7.4", - "webpack-sources": "^1.4.1" - }, - "bin": { - "webpack": "bin/webpack.js" + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" }, "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - }, - "webpack-command": { - "optional": true - } + "node": ">=10.13.0" } }, + "node_modules/spfx-fast-serve-helpers/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, "node_modules/spfx-fast-serve-helpers/node_modules/webpack-dev-server": { - "version": "3.11.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz", - "integrity": "sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ==", + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.3.tgz", + "integrity": "sha512-3x31rjbEQWKMNzacUZRE6wXvUFuGpH7vr0lIEbYpMAG9BOxi0928QU1BBswOAP3kg3H1O4hiS+sq4YyAn6ANnA==", "dev": true, "dependencies": { - "ansi-html": "0.0.7", + "ansi-html-community": "0.0.8", "bonjour": "^3.5.0", "chokidar": "^2.1.8", "compression": "^1.7.4", @@ -42636,146 +43189,6 @@ "yargs-parser": "^13.1.2" } }, - "node_modules/spfx-fast-serve-helpers/node_modules/webpack/node_modules/acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/spfx-fast-serve-helpers/node_modules/webpack/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spfx-fast-serve-helpers/node_modules/webpack/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spfx-fast-serve-helpers/node_modules/webpack/node_modules/eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/spfx-fast-serve-helpers/node_modules/webpack/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spfx-fast-serve-helpers/node_modules/webpack/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/spfx-fast-serve-helpers/node_modules/webpack/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/spfx-fast-serve-helpers/node_modules/webpack/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spfx-fast-serve-helpers/node_modules/webpack/node_modules/schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "dependencies": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/spfx-fast-serve-helpers/node_modules/webpack/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/spfx-fast-serve-helpers/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -42854,6 +43267,12 @@ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, + "node_modules/spfx-fast-serve-helpers/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/spfx-fast-serve-helpers/node_modules/yargs-parser": { "version": "13.1.2", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", @@ -46326,24 +46745,22 @@ } }, "node_modules/webpack-cli": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.6.0.tgz", - "integrity": "sha512-9YV+qTcGMjQFiY7Nb1kmnupvb1x40lfpj8pwdO/bom+sQiP4OBMKjHq29YQrlDWDPZO9r/qWaRRywKaRDKqBTA==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", + "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "dev": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.0.2", - "@webpack-cli/info": "^1.2.3", - "@webpack-cli/serve": "^1.3.1", - "colorette": "^1.2.1", + "@webpack-cli/configtest": "^1.2.0", + "@webpack-cli/info": "^1.5.0", + "@webpack-cli/serve": "^1.7.0", + "colorette": "^2.0.14", "commander": "^7.0.0", - "enquirer": "^2.3.6", - "execa": "^5.0.0", + "cross-spawn": "^7.0.3", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", "interpret": "^2.2.0", "rechoir": "^0.7.0", - "v8-compile-cache": "^2.2.0", "webpack-merge": "^5.7.3" }, "bin": { @@ -46352,6 +46769,10 @@ "engines": { "node": ">=10.13.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, "peerDependencies": { "webpack": "4.x.x || 5.x.x" }, @@ -46379,50 +46800,6 @@ "node": ">= 10" } }, - "node_modules/webpack-cli/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/webpack-cli/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/webpack-cli/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, "node_modules/webpack-cli/node_modules/interpret": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", @@ -46495,12 +46872,6 @@ "ajv": "^8.8.2" } }, - "node_modules/webpack-dev-middleware/node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true - }, "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -46669,12 +47040,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/webpack-dev-server/node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true - }, "node_modules/webpack-dev-server/node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", diff --git a/package.json b/package.json index 8c9557521..5ea60c2c9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@pnp/spfx-controls-react", "description": "Reusable React controls for SharePoint Framework solutions", - "version": "3.16.2", + "version": "3.17.0", "node": ">=16.13.0 <17.0.0 || >=18.17.1 <19.0.0", "scripts": { "build": "gulp build", @@ -82,10 +82,10 @@ "react-quill": "2.0.0", "regexify-string": "^1.0.16", "spfx-uifabric-themes": "^0.9.0", - "swiper": "^8.2.6" + "swiper": "^8.2.6", + "@iconify/react": "^4.1.1" }, "devDependencies": { - "@iconify/react": "^4.1.1", "@microsoft/eslint-config-spfx": "1.18.2", "@microsoft/eslint-plugin-spfx": "1.18.2", "@microsoft/microsoft-graph-types": "^2.1.0", @@ -98,7 +98,7 @@ "@types/es6-promise": "3.3.0", "@types/he": "^1.1.2", "@types/jest": "25.2.3", - "@types/lodash": "4.14.194", + "@types/lodash": "4.14.202", "@types/quill": "^1.3.10", "@types/react": "17.0.45", "@types/react-addons-shallow-compare": "0.14.17", @@ -128,7 +128,7 @@ "react-test-renderer": "17.0.1", "request-promise": "4.2.5", "sonarqube-scanner": "2.8.2", - "spfx-fast-serve-helpers": "1.17.3", + "spfx-fast-serve-helpers": "1.18.4", "ts-jest": "^29.1.1", "tslib": "2.3.1", "typescript": "4.7.4", diff --git a/src/common/SPEntities.ts b/src/common/SPEntities.ts index 531c89acc..4d4734c02 100644 --- a/src/common/SPEntities.ts +++ b/src/common/SPEntities.ts @@ -56,6 +56,11 @@ export interface ISPField { LookupDisplayUrl?: string; TypeAsString?: string; ResultType?: string; + ValidationFormula?: string; + ValidationMessage?: string; + MinimumValue?: number; + MaximumValue?: number; + CurrencyLocaleId?: number; } /** diff --git a/src/common/telemetry/version.ts b/src/common/telemetry/version.ts index 9dfa023c5..d33abe852 100644 --- a/src/common/telemetry/version.ts +++ b/src/common/telemetry/version.ts @@ -1 +1 @@ -export const version: string = "3.16.2"; \ No newline at end of file +export const version: string = "3.17.0"; diff --git a/src/common/utilities/CustomFormatting.ts b/src/common/utilities/CustomFormatting.ts new file mode 100644 index 000000000..db3438dcc --- /dev/null +++ b/src/common/utilities/CustomFormatting.ts @@ -0,0 +1,163 @@ +import * as React from "react"; +import { Icon } from "@fluentui/react/lib/Icon"; +import { FormulaEvaluation } from "./FormulaEvaluation"; +import { ASTNode, Context } from "./FormulaEvaluation.types"; +import { ICustomFormattingExpressionNode, ICustomFormattingNode } from "./ICustomFormatting"; + +type CustomFormatResult = string | number | boolean | JSX.Element | ICustomFormattingNode; + +/** + * A class that provides helper methods for custom formatting + * See: https://learn.microsoft.com/en-us/sharepoint/dev/declarative-customization/formatting-syntax-reference + */ +export default class CustomFormattingHelper { + + private _formulaEvaluator: FormulaEvaluation; + + /** + * Custom Formatting Helper / Renderer + * @param formulaEvaluator An instance of FormulaEvaluation used for evaluating expressions in custom formatting + */ + constructor(formulaEvaluator: FormulaEvaluation) { + this._formulaEvaluator = formulaEvaluator; + } + + /** + * The Formula Evaluator expects an ASTNode to be passed to it for evaluation. This method converts expressions + * described by the interface ICustomFormattingExpressionNode to ASTNodes. + * @param node An ICustomFormattingExpressionNode to be converted to an ASTNode + */ + private convertCustomFormatExpressionNodes = (node: ICustomFormattingExpressionNode | string | number | boolean): ASTNode => { + if (typeof node !== "object") { + switch (typeof node) { + case "string": + return { type: "string", value: node }; + case "number": + return { type: "number", value: node }; + case "boolean": + return { type: "booelan", value: node ? 1 : 0 }; + } + } + const operator = node.operator; + const operands = node.operands.map(o => this.convertCustomFormatExpressionNodes(o)); + return { type: "operator", value: operator, operands }; + } + + /** + * Given a single custom formatting expression, node or element, this method evaluates the expression and returns the result + * @param content An object, expression or literal value to be evaluated + * @param context A context object containing values / variables to be used in the evaluation + * @returns + */ + private evaluateCustomFormatContent = (content: ICustomFormattingExpressionNode | ICustomFormattingNode | string | number | boolean, context: Context): CustomFormatResult => { + + // If content is a string or number, it is a literal value and should be returned as-is + if ((typeof content === "string" && content.charAt(0) !== "=") || typeof content === "number") return content; + + // If content is a string beginning with '=' it is a formula/expression, and should be evaluated + if (typeof content === "string" && content.charAt(0) === "=") { + const result = this._formulaEvaluator.evaluate(content.substring(1), context); + return result as CustomFormatResult; + } + + // If content is an object, it is either further custom formatting described by an ICustomFormattingNode, + // or an expression to be evaluated - as described by an ICustomFormattingExpressionNode + + if (typeof content === "object") { + + if (Object.prototype.hasOwnProperty.call(content, "elmType")) { + + // Custom Formatting Content + return this.renderCustomFormatContent(content as ICustomFormattingNode, context); + + } else if (Object.prototype.hasOwnProperty.call(content, "operator")) { + + // Expression to be evaluated + const astNode = this.convertCustomFormatExpressionNodes(content as ICustomFormattingExpressionNode); + const result = this._formulaEvaluator.evaluateASTNode(astNode, context); + if (typeof result === "object" && Object.prototype.hasOwnProperty.call(result, "elmType")) { + return this.renderCustomFormatContent(result as ICustomFormattingNode, context); + } + return result as CustomFormatResult; + + } + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public renderCustomFormatContent = (node: ICustomFormattingNode, context: Context, rootEl: boolean = false): JSX.Element | string | number => { + + // We don't want attempts to render custom format content to kill the component or web part, + // so we wrap the entire method in a try/catch block, log errors and return null if an error occurs + try { + + // If node is a string or number, it is a literal value and should be returned as-is + if (typeof node === "string" || typeof node === "number") return node; + + // Custom formatting nodes / elements may have a txtContent property, which represents the inner + // content of a HTML element. This can be a string literal, or another expression to be evaluated: + let textContent: CustomFormatResult | undefined; + if (node.txtContent) { + textContent = this.evaluateCustomFormatContent(node.txtContent, context); + } + + // Custom formatting nodes / elements may have a style property, which contains the style rules + // to be applied to the resulting HTML element. Rule values can be string literals or another expression + // to be evaluated: + const styleProperties: React.CSSProperties = {}; + if (node.style) { + for (const styleAttribute in node.style) { + if (node.style[styleAttribute]) { + styleProperties[styleAttribute] = this.evaluateCustomFormatContent(node.style[styleAttribute], context) as string; + } + } + } + + // Custom formatting nodes / elements may have an attributes property, which represents the HTML attributes + // to be applied to the resulting HTML element. Attribute values can be string literals or another expression + // to be evaluated: + const attributes = {} as Record; + if (node.attributes) { + for (const attribute in node.attributes) { + if (node.attributes[attribute]) { + let attributeName = attribute; + + // Because we're using React to render the HTML content, we need to rename the 'class' attribute + if (attributeName === "class") attributeName = "className"; + + // Evaluation + attributes[attributeName] = this.evaluateCustomFormatContent(node.attributes[attribute], context) as string; + + // Add the 'sp-field-customFormatter' class to the root element + if (attributeName === "className" && rootEl) { + attributes[attributeName] = `${attributes[attributeName]} sp-field-customFormatter`; + } + } + } + } + + // Custom formatting nodes / elements may have children. These are likely to be further custom formatting + let children: (CustomFormatResult)[] = []; + + // If the node has an iconName property, we'll render an Icon component as the first child. + // SharePoint uses CSS to apply the icon in a ::before rule, but we can't count on the global selector for iconName + // being present on the page, so we'll add it as a child instead: + if (attributes.iconName) { + const icon = React.createElement(Icon, { iconName: attributes.iconName }); + children.push(icon); + } + + // Each child object is evaluated recursively and added to the children array + if (node.children) { + children = node.children.map(c => this.evaluateCustomFormatContent(c, context)); + } + + // The resulting HTML element is returned to the callee using React.createElement + const el = React.createElement(node.elmType, { style: styleProperties, ...attributes }, textContent, ...children); + return el; + } catch (error) { + console.error('Unable to render custom formatted content', error); + return null; + } + } +} \ No newline at end of file diff --git a/src/common/utilities/FormulaEvaluation.ts b/src/common/utilities/FormulaEvaluation.ts new file mode 100644 index 000000000..3b9acaa48 --- /dev/null +++ b/src/common/utilities/FormulaEvaluation.ts @@ -0,0 +1,659 @@ +import { IContext } from "../Interfaces"; +import { ASTNode, ArrayLiteralNode, ArrayNodeValue, Context, Token, TokenType, ValidFuncNames } from "./FormulaEvaluation.types"; + +const operatorTypes = ["+", "-", "*", "/", "==", "!=", "<>", ">", "<", ">=", "<=", "&&", "||", "%", "&", "|", "?", ":"]; + +/** Each token pattern matches a particular token type. The tokenizer looks for matches in this order. */ +const patterns: [RegExp, TokenType][] = [ + [/^\[\$?[a-zA-Z_][a-zA-Z_0-9.]*\]/, "VARIABLE"], // [$variable] + [/^@[a-zA-Z_][a-zA-Z_0-9.]*/, "VARIABLE"], // @variable + [/^[0-9]+(?:\.[0-9]+)?/, "NUMBER"], // Numeric literals + [/^"([^"]*)"/, "STRING"], // Match double-quoted strings + [/^'([^']*)'/, "STRING"], // Match single-quoted strings + [/^\[[^\]]*\]/, "ARRAY"], // Array literals + [new RegExp(`^(${ValidFuncNames.join('|')})\\(`), "FUNCTION"], // Functions or other words + [/^(true|false)/, "BOOLEAN"], // Boolean literals + [/^\w+/, "WORD"], // Other words, checked against valid variables + [/^&&|^\|\||^==|^<>/, "OPERATOR"], // Operators and special characters (match double first) + [/^[+\-*/<>=%!&|?:,()[\]]/, "OPERATOR"], // Operators and special characters +]; + +export class FormulaEvaluation { + private webUrl: string; + private _meEmail: string; + + constructor(context?: IContext, webUrlOverride?: string) { + if (context) { + this._meEmail = context.pageContext.user.email; + } + this.webUrl = webUrlOverride || context?.pageContext.web.absoluteUrl || ''; + } + + /** Evaluates a formula expression and returns the result, with optional context object for variables */ + public evaluate(expression: string, context: Context = {}): boolean | string | number | ArrayNodeValue | object { + context.me = this._meEmail; + context.today = new Date(); + const tokens: Token[] = this.tokenize(expression, context); + const postfix: Token[] = this.shuntingYard(tokens); + const ast: ASTNode = this.buildAST(postfix); + return this.evaluateASTNode(ast, context); + } + + /** Tokenizes an expression into a list of tokens (primatives, operators, variables, function names, arrays etc) */ + public tokenize(expression: string, context: Context = {}): Token[] { + + const tokens: Token[] = []; + let i = 0; + + while (i < expression.length) { + let match: string | null = null; + + // For each pattern, try to match it from the current position in the expression + for (const [pattern, tokenType] of patterns) { + const regexResult = pattern.exec(expression.slice(i)); + + if (regexResult) { + match = regexResult[1] || regexResult[0]; + + // Unary minus is a special case that we need to + // capture in order to process negative numbers, or + // expressions such as 5 + - 3 + if (tokenType === "OPERATOR" && match === "-" && (tokens.length === 0 || tokens[tokens.length - 1].type === "OPERATOR")) { + tokens.push(new Token("UNARY_MINUS", match)); + i += match.length + expression.slice(i).indexOf(match); + } else if ( + // String literals, surrounded by single or double quotes + tokenType === "STRING" + ) { + tokens.push(new Token(tokenType, match)); + i += match.length + 2; + } else if (tokenType === "WORD") { + // We only match words if they are a valid variable name + if (context && context[match]) { + tokens.push(new Token("VARIABLE", match)); + } + i += match.length; + } else { + // Otherwise, just add the token to the list + tokens.push(new Token(tokenType, match)); + // console.log(`Added token: ${JSON.stringify({tokenType: tokenType, value: match})}`); + i += match.length + expression.slice(i).indexOf(match); + if (tokenType === "FUNCTION") i -= 1; + } + + break; + } + } + + if (!match) { + // If no patterns matched, move to the next character + // console.log(`No match found for character: ${expression[i]}`); + i++; + } + } + + return tokens; + } + + public shuntingYard(tokens: Token[]): Token[] { + + /** Stores re-ordered tokens to be returned by this algorithm / method */ + const output: Token[][] = [[]]; + + /** Stores tokens temporarily pushed to a stack to help with re-ordering */ + const stack: Token[][] = [[]]; + + /** Keeps track of parenthesis depth, important for nested expressions and method signatures */ + let parenDepth = 0; + + for (const token of tokens) { + // Numbers, strings, booleans, words, variables, and arrays are added to the output + if (token.type === "NUMBER" || token.type === "STRING" || token.type === "BOOLEAN" || token.type === "WORD" || token.type === "VARIABLE" || token.type === "ARRAY") { + output[parenDepth].push(token); + // Functions are pushed to the stack + } else if (token.type === "FUNCTION") { + stack[parenDepth].push(token); + } else if (token.type === "OPERATOR" || token.type === "UNARY_MINUS") { + // Left parenthesis are pushed to the stack + if (token.value === "(") { + parenDepth++; + stack[parenDepth] = []; + output[parenDepth] = []; + stack[parenDepth].push(token); + } else if (token.value === ")") { + // When Right parenthesis is found, items are popped from stack to output until left parenthesis is found + while (stack[parenDepth].length > 0 && stack[parenDepth][stack[parenDepth].length - 1].value !== "(") { + output[parenDepth].push(stack[parenDepth].pop() as Token); + } + stack[parenDepth].pop(); // pop the left parenthesis + // If the item on top of the stack is a function name, parens were part of method signature, + // pop it to the output + if (stack[parenDepth].length > 0 && stack[parenDepth][stack[parenDepth].length - 1].type === "FUNCTION") { + output[parenDepth].push(stack[parenDepth].pop() as Token); + } + // Combine outputs + output[parenDepth - 1] = output[parenDepth - 1].concat(output[parenDepth]); + parenDepth--; + } else if (token.value === ",") { + // When comma is found, items are popped from stack to output until left parenthesis is found + while (stack[parenDepth].length > 0 && stack[parenDepth][stack[parenDepth].length - 1].value !== "(") { + output[parenDepth].push(stack[parenDepth].pop() as Token); + } + // Combine outputs + output[parenDepth - 1] = output[parenDepth - 1].concat(output[parenDepth]); + output[parenDepth] = []; + } else if (token.value === "?") { + while (stack[parenDepth].length > 0 && stack[parenDepth][stack[parenDepth].length - 1].value !== "(") { + output[parenDepth].push(stack[parenDepth].pop() as Token); + } + stack[parenDepth].push(token); + } else { + // When an operator is found, items are popped from stack to output until an operator with lower precedence is found + const currentTokenPrecedence = this.getPrecedence(token.value.toString()); + + while (stack[parenDepth].length > 0) { + const topStackIsOperator = stack[parenDepth][stack[parenDepth].length - 1].type === "OPERATOR" || stack[parenDepth][stack[parenDepth].length - 1].type === "UNARY_MINUS"; + const topStackPrecedence = this.getPrecedence(stack[parenDepth][stack[parenDepth].length - 1].value.toString()); + + if (stack[parenDepth][stack[parenDepth].length - 1].value === "(") break; + if (topStackIsOperator && ( + currentTokenPrecedence.associativity === "left" ? + topStackPrecedence.precedence <= currentTokenPrecedence.precedence : + topStackPrecedence.precedence < currentTokenPrecedence.precedence + )) { + break; + } + + output[parenDepth].push(stack[parenDepth].pop() as Token); + } + + stack[parenDepth].push(token); + } + } + } + + // When there are no more tokens to read, pop any remaining tokens from the stack to the output + while (stack[parenDepth].length > 0) { + output[parenDepth].push(stack[parenDepth].pop() as Token); + } + + // Combine all outputs + let finalOutput: Token[] = []; + for (let i = 0; i <= parenDepth; i++) { + finalOutput = finalOutput.concat(output[i]); + } + + return finalOutput; + } + + public buildAST(postfixTokens: Token[]): ASTNode { + + // Tokens are arranged on a stack/array of node objects + const stack: (Token | ASTNode | ArrayLiteralNode)[] = []; + for (const token of postfixTokens) { + if (token.type === "STRING" || token.type === "NUMBER" || token.type === "BOOLEAN" || token.type === "VARIABLE") { + // Strings, numbers, booleans, function names, and variable names are pushed directly to the stack + stack.push(token); + } else if (token.type === "UNARY_MINUS") { + // Unary minus has a single operand, we discard the minus token and push an object representing a negative number + const operand = stack.pop() as ASTNode; + const numericValue = parseFloat(operand.value as string); + stack.push({ type: operand.type, value: -numericValue }); + } else if (token.type === "OPERATOR") { + // Operators have two operands, we pop them from the stack and push an object representing the operation + if (operatorTypes.includes(token.value as string)) { + if (token.value === "?") { + // Ternary operator has three operands, and left and right operators should be top of stack + const colonOperator = stack.pop() as ASTNode; + if (colonOperator.operands) { + const rightOperand = colonOperator.operands[1]; + const leftOperand = colonOperator.operands[0]; + const condition = stack.pop() as ASTNode; + stack.push({ type: "FUNCTION", value: "ternary", operands: [condition, leftOperand, rightOperand] }); + } + } else { + const rightOperand = stack.pop() as ASTNode; + const leftOperand = stack.pop() as ASTNode; + stack.push({ type: token.type, value: token.value, operands: [leftOperand, rightOperand] }); + } + } + } else if (token.type === "ARRAY") { + // At this stage, arrays are represented by a single token with a string value ie "[1,2,3]" + let arrayString = token.value as string; + // Remove leading and trailing square brackets + arrayString = arrayString.slice(1, -1); + // Split the string by commas and trim whitespace + const arrayElements = arrayString.split(',').map(element => element.trim()); + stack.push(new ArrayLiteralNode(arrayElements)); + } else if (token.type === "FUNCTION") { + const arity = this._getFnArity(token.value as string); + const operands: ASTNode[] = []; + while (operands.length < arity) { + operands.push(stack.pop() as ASTNode); + } + stack.push({ type: token.type, value: token.value, operands: operands.reverse() }); + } + } + + // At this stage, the stack should contain a single node representing the root of the AST + return stack[0] as ASTNode; + } + + public evaluateASTNode(node: ASTNode | ArrayLiteralNode | ArrayNodeValue | string | number, context: Context = {}): + boolean | number | string | ArrayNodeValue | object { + + if (!node) return 0; + + // If node is an object with a type and value property, it is an ASTNode and should be evaluated recursively + // otherwise it may actually be an object value that we need to return (as is the case with custom formatting) + if (typeof node === "object" && !(Object.prototype.hasOwnProperty.call(node, 'type') && Object.prototype.hasOwnProperty.call(node, 'value'))) { + return node; + } + + // Each element in an ArrayLiteralNode is evaluated recursively + if (node instanceof ArrayLiteralNode) { + const evaluatedElements = (node as ArrayLiteralNode).elements.map(element => this.evaluateASTNode(element, context)); + return evaluatedElements; + } + + // If node is an actual array, it has likely already been transformed above + if (Array.isArray(node)) { + return node; + } + + // Number and string literals are returned as-is + if (typeof node === "number" || typeof node === "string") { + return node; + } + + // Nodes with a type of NUMBER are parsed to a number + if (node.type === "NUMBER") { + const numVal = Number(node.value); + if (isNaN(numVal)) { + throw new Error(`Invalid number: ${node.value}`); + } + return numVal; + } + + // Nodes with a type of BOOLEAN are parsed to a boolean + if (node.type === "BOOLEAN") { + return node.value === "true" ? 1 : 0; + } + + // WORD and STRING nodes are returned as-is + if (node.type === "WORD" || node.type === "STRING") { + return node.value?.toString(); + } + + // VARIABLE nodes are looked up in the context object and returned + if (node.type === "VARIABLE") { + return context[(node.value as string).replace(/^[[@]?\$?([a-zA-Z_][a-zA-Z_0-9.]*)\]?/, '$1')] ?? null; + } + + // OPERATOR nodes have their OPERANDS evaluated recursively, with the operator applied to the results + if (node.type === "OPERATOR" && operatorTypes.includes(node.value as string) && node.operands) { + + const leftValue = this.evaluateASTNode(node.operands[0], context) as string | number; + const rightValue = this.evaluateASTNode(node.operands[1], context) as string | number; + + // These operators are valid for both string and number operands + switch (node.value) { + case "==": return leftValue === rightValue ? 1 : 0; + case "!=": return leftValue !== rightValue ? 1 : 0; + case "<>": return leftValue !== rightValue ? 1 : 0; + case ">": return leftValue > rightValue ? 1 : 0; + case "<": return leftValue < rightValue ? 1 : 0; + case ">=": return leftValue >= rightValue ? 1 : 0; + case "<=": return leftValue <= rightValue ? 1 : 0; + case "&&": return (leftValue !== 0 && rightValue !== 0) ? 1 : 0; + case "||": return (leftValue !== 0 || rightValue !== 0) ? 1 : 0; + } + + if (typeof leftValue === "string" || typeof rightValue === "string") { + // Concatenate strings if either operand is a string + if (node.value === "+") { + const concatString: string = (leftValue || "").toString() + (rightValue || "").toString(); + return concatString; + } else { + // Throw an error if the operator is not valid for strings + throw new Error(`Invalid operation ${node.value} with string operand.`); + } + } + + // Both operands will be numbers at this point + switch (node.value) { + case "+": return leftValue + rightValue; + case "-": return leftValue - rightValue; + case "*": return leftValue * rightValue; + case "/": return leftValue / rightValue; + + case "%": return leftValue % rightValue; + case "&": return leftValue & rightValue; + case "|": return leftValue | rightValue; + } + } + + // Evaluation of function nodes is handled here: + + if (node.type === "FUNCTION" && node.operands) { + + // Evaluate operands recursively - casting to any here to allow for any type of operand + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const funcArgs = node.operands.map(arg => this.evaluateASTNode(arg, context)) as any[]; + + switch (node.value) { + + /** + * Logical Functions + */ + + case 'if': + case 'ternary': { + const condition = funcArgs[0]; + if (condition !== 0) { + return funcArgs[1]; + } else { + return funcArgs[2]; + } + } + + /** + * Math Functions + */ + + case "Number": + return Number(funcArgs[0]); + case "abs": + return Math.abs(funcArgs[0]); + case 'floor': + return Math.floor(funcArgs[0]); + case 'ceiling': + return Math.ceil(funcArgs[0]); + case 'pow': { + const basePow = funcArgs[0]; + const exponentPow = funcArgs[1]; + return Math.pow(basePow, exponentPow); + } + case 'cos': { + const angleCos = funcArgs[0]; + return Math.cos(angleCos); + } + case 'sin': { + const angleSin = funcArgs[0]; + return Math.sin(angleSin); + } + + /** + * String Functions + */ + + case "toString": + return funcArgs[0].toString(); + case 'lastIndexOf': { + const mainStrLastIndexOf = funcArgs[0]; + const searchStrLastIndexOf = funcArgs[1]; + return mainStrLastIndexOf.lastIndexOf(searchStrLastIndexOf); + } + case 'join': { + const arrayToJoin = (node.operands[0] as ArrayLiteralNode).evaluate(); + const separator = funcArgs[1]; + return arrayToJoin.join(separator); + } + case 'substring': { + const mainStrSubstring = funcArgs[0] || ''; + const start = funcArgs[1] || 0; + const end = funcArgs[2] || mainStrSubstring.length; + return mainStrSubstring.substr(start, end); + } + case 'toUpperCase': { + const strToUpper = funcArgs[0] || ''; + return strToUpper.toUpperCase(); + } + case 'toLowerCase': { + const strToLower = funcArgs[0] || ''; + return strToLower.toLowerCase(); + } + case 'startsWith': { + const mainStrStartsWith = funcArgs[0]; + const searchStrStartsWith = funcArgs[1]; + return mainStrStartsWith.startsWith(searchStrStartsWith); + } + case 'endsWith': { + const mainStrEndsWith = funcArgs[0]; + const searchStrEndsWith = funcArgs[1]; + return mainStrEndsWith.endsWith(searchStrEndsWith); + } + case 'replace': { + const mainStrReplace = funcArgs[0]; + const searchStrReplace = funcArgs[1]; + const replaceStr = funcArgs[2]; + return mainStrReplace.replace(searchStrReplace, replaceStr); + } + case 'replaceAll': { + const mainStrReplaceAll = funcArgs[0]; + const searchStrReplaceAll = funcArgs[1]; + const replaceAllStr = funcArgs[2]; + // Using a global regex to simulate replaceAll behavior + const globalRegex = new RegExp(searchStrReplaceAll, 'g'); + return mainStrReplaceAll.replace(globalRegex, replaceAllStr); + } + case 'padStart': { + const mainStrPadStart = funcArgs[0]; + const lengthPadStart = funcArgs[1]; + const padStrStart = funcArgs[2]; + return mainStrPadStart.padStart(lengthPadStart, padStrStart); + } + case 'padEnd': { + const mainStrPadEnd = funcArgs[0]; + const lengthPadEnd = funcArgs[1]; + const padStrEnd = funcArgs[2]; + return mainStrPadEnd.padEnd(lengthPadEnd, padStrEnd); + } + case 'split': { + const mainStrSplit = funcArgs[0]; + const delimiterSplit = funcArgs[1]; + return mainStrSplit.split(delimiterSplit); + } + + /** + * Date Functions + */ + + case 'toDate': { + const dateStr = funcArgs[0]; + return new Date(dateStr); + } + case 'toDateString': { + const dateToDateString = new Date(funcArgs[0]); + return dateToDateString.toDateString(); + } + case "toLocaleString": { + const dateToLocaleString = new Date(funcArgs[0]); + return dateToLocaleString.toLocaleString(); + } + case "toLocaleDateString": { + const dateToLocaleDateString = new Date(funcArgs[0]); + return dateToLocaleDateString.toLocaleDateString(); + } + case "toLocaleTimeString": { + const dateToLocaleTimeString = new Date(funcArgs[0]); + return dateToLocaleTimeString.toLocaleTimeString(); + } + case 'getDate': { + const dateStrGetDate = funcArgs[0]; + return new Date(dateStrGetDate).getDate(); + } + case 'getMonth': { + const dateStrGetMonth = funcArgs[0]; + return new Date(dateStrGetMonth).getMonth(); + } + case 'getYear': { + const dateStrGetYear = funcArgs[0]; + return new Date(dateStrGetYear).getFullYear(); + } + case 'addDays': { + const dateStrAddDays = funcArgs[0]; + const daysToAdd = funcArgs[1]; + const dateAddDays = new Date(dateStrAddDays); + dateAddDays.setDate(dateAddDays.getDate() + daysToAdd); + return dateAddDays; + } + case 'addMinutes': { + const dateStrAddMinutes = funcArgs[0]; + const minutesToAdd = funcArgs[1]; + const dateAddMinutes = new Date(dateStrAddMinutes); + dateAddMinutes.setMinutes(dateAddMinutes.getMinutes() + minutesToAdd); + return dateAddMinutes; + } + + /** + * SharePoint Functions + */ + + case 'getUserImage': { + const userEmail = funcArgs[0]; + const userImage = this._getUserImageUrl(userEmail); + return userImage; + } + case 'getThumbnailImage': { + const imageUrl = funcArgs[0]; + const thumbnailImage = this._getSharePointThumbnailUrl(imageUrl); + return thumbnailImage; + } + + /** + * Array Functions + */ + + case "indexOf": { + const array = funcArgs[0]; + const operand = funcArgs[1]; + if (Array.isArray(array)) { + return array.indexOf(operand); + } else if (typeof array === 'string') { + return array.indexOf(operand); + } + return -1; // Default to -1 if not found. + } + case "length": { + const array = funcArgs[0]; + if (array instanceof ArrayLiteralNode) { + // treat as array literal + const value = array.evaluate(); + return value.length; + } + else { + // treat as char Array + const value = this.evaluateASTNode(array, context); + return value.toString().length; + } + } + case 'appendTo': { + const mainArrayAppend = (node.operands[0] as ArrayLiteralNode).evaluate(); + const elementToAppend = funcArgs[1]; + mainArrayAppend.push(elementToAppend); + return mainArrayAppend; + } + case 'removeFrom': { + const mainArrayRemove = (node.operands[0] as ArrayLiteralNode).evaluate(); + const elementToRemove = funcArgs[1]; + const indexToRemove = mainArrayRemove.indexOf(elementToRemove); + if (indexToRemove !== -1) { + mainArrayRemove.splice(indexToRemove, 1); + } + return mainArrayRemove; + } + case 'loopIndex': + return 0; // This should ideally return the current loop index in context but is not implemented yet + } + } + + return 0; // Default fallback + } + + public validate(expression: string): boolean { + const validFunctionRegex = `(${ValidFuncNames.map(fn => `${fn}\\(`).join('|')})`; + const pattern = new RegExp(`^(?:@\\w+|\\[\\$?[\\w+.]\\]|\\d+(?:\\.\\d+)?|"(?:[^"]*)"|'(?:[^']*)'|${validFunctionRegex}|[+\\-*/<>=%!&|?:,()\\[\\]]|\\?|:)`); + + /* Explanation - + /@\\w+/ matches variables specified by the form @variableName. + /\\[\\$?\\w+\\/] matches variables specified by the forms [variableName] and [$variableName]. + /\\d+(?:\\.\\d+)?/ matches numbers, including decimal numbers. + /"(?:[^"]*)"/ and /'(?:[^']*)'/ match string literals in double and single quotes, respectively. + /${validFunctionRegex}/ matches valid function names. + /\\?/ matches the ternary operator ?. + /:/ matches the colon :. + /[+\\-*///<>=%!&|?:,()\\[\\]]/ matches operators. + + return pattern.test(expression); + } + + /** Returns a precedence value for a token or operator */ + private getPrecedence(op: string): { precedence: number, associativity: "left" | "right" } { + + // If the operator is a valid function name, return a high precedence value + if (ValidFuncNames.indexOf(op) >= 0) return { precedence: 7, associativity: "left" }; + + // Otherwise, return the precedence value for the operator + const precedence: { [key: string]: { precedence: number, associativity: "left" | "right" } } = { + "+": { precedence: 4, associativity: "left" }, + "-": { precedence: 4, associativity: "left" }, + "*": { precedence: 5, associativity: "left" }, + "/": { precedence: 5, associativity: "left" }, + "%": { precedence: 5, associativity: "left" }, + ">": { precedence: 3, associativity: "left" }, + "<": { precedence: 3, associativity: "left" }, + "==": { precedence: 3, associativity: "left" }, + "!=": { precedence: 3, associativity: "left" }, + "<>": { precedence: 3, associativity: "left" }, + ">=": { precedence: 3, associativity: "left" }, + "<=": { precedence: 3, associativity: "left" }, + "&&": { precedence: 2, associativity: "left" }, + "||": { precedence: 1, associativity: "left" }, + "?": { precedence: 6, associativity: "left" }, + ":": { precedence: 6, associativity: "left" }, + ",": { precedence: 0, associativity: "left" }, + }; + + return precedence[op] ?? { precedence: 8, associativity: "left" }; + } + + private _getFnArity(fnName: string): number { + switch (fnName) { + case "if": + case "substring": + case "replace": + case "replaceAll": + case "padStart": + case "padEnd": + case "ternary": + return 3; + case "pow": + case "indexOf": + case "lastIndexOf": + case "join": + case "startsWith": + case "endsWith": + case "split": + case "addDays": + case "addMinutes": + case "appendTo": + case "removeFrom": + return 2; + default: + return 1; + } + } + + private _getSharePointThumbnailUrl(imageUrl: string): string { + const filename = imageUrl.split('/').pop(); + const url = imageUrl.replace(filename, ''); + const [filenameNoExt, ext] = filename.split('.'); + return `${url}_t/${filenameNoExt}_${ext}.jpg`; + } + private _getUserImageUrl(userEmail: string): string { + return `${this.webUrl}/_layouts/15/userphoto.aspx?size=L&accountname=${userEmail}` + } +} + + diff --git a/src/common/utilities/FormulaEvaluation.types.ts b/src/common/utilities/FormulaEvaluation.types.ts new file mode 100644 index 000000000..7af1c0555 --- /dev/null +++ b/src/common/utilities/FormulaEvaluation.types.ts @@ -0,0 +1,97 @@ +export type TokenType = "FUNCTION" | "STRING" | "NUMBER" | "UNARY_MINUS" | "BOOLEAN" | "WORD" | "OPERATOR" | "ARRAY" | "VARIABLE"; + +export class Token { + type: TokenType; + value: string | number; + + constructor(tokenType: TokenType, value: string | number) { + this.type = tokenType; + this.value = value; + } + + toString(): string { + return `${this.type}: ${this.value}`; + } +} + +export type ArrayNodeValue = (string | number | ArrayNodeValue)[]; +export class ArrayLiteralNode { + elements: ArrayNodeValue; + + constructor(elements: ArrayNodeValue) { + this.elements = elements; // Store array elements + } + + evaluate(): ArrayNodeValue { + // Evaluate array elements and return the array + const evaluatedElements = this.elements.map((element) => { + if (element instanceof ArrayLiteralNode) { + return element.evaluate(); + } else { + if ( + typeof element === "string" && + ( + (element.startsWith("'") && element.endsWith("'")) || + (element.startsWith('"') && element.endsWith('"')) + ) + ) { + return element.slice(1, -1); + } else { + return element; + } + } + }); + return evaluatedElements; + } +} + +export type ASTNode = { + type: string; + value?: string | number; + operands?: (ASTNode | ArrayLiteralNode)[]; +}; + +export type Context = { [key: string]: boolean | number | object | string | undefined }; + +export const ValidFuncNames = [ + "if", + "ternary", + "Number", + "abs", + "floor", + "ceiling", + "pow", + "cos", + "sin", + "indexOf", + "lastIndexOf", + "toString", + "join", + "substring", + "toUpperCase", + "toLowerCase", + "startsWith", + "endsWith", + "replaceAll", + "replace", + "padStart", + "padEnd", + "split", + "toDateString", + "toDate", + "toLocaleString", + "toLocaleDateString", + "toLocaleTimeString", + "getDate", + "getMonth", + "getYear", + "addDays", + "addMinutes", + "getUserImage", + "getThumbnailImage", + "indexOf", + "length", + "appendTo", + "removeFrom", + "loopIndex" +]; \ No newline at end of file diff --git a/src/common/utilities/ICustomFormatting.ts b/src/common/utilities/ICustomFormatting.ts new file mode 100644 index 000000000..119af53f9 --- /dev/null +++ b/src/common/utilities/ICustomFormatting.ts @@ -0,0 +1,30 @@ +import { CSSProperties } from "react"; + +export interface ICustomFormattingExpressionNode { + operator: string; + operands: (string | number | ICustomFormattingExpressionNode)[]; +} + +export interface ICustomFormattingNode { + elmType: keyof HTMLElementTagNameMap; + iconName: string; + style: CSSProperties; + attributes?: { + [key: string]: string; + }; + children?: ICustomFormattingNode[]; + txtContent?: string; +} + +export interface ICustomFormattingBodySection { + displayname: string; + fields: string[]; +} + +export interface ICustomFormatting { + headerJSONFormatter: ICustomFormattingNode; + bodyJSONFormatter: { + sections: ICustomFormattingBodySection[]; + }; + footerJSONFormatter: ICustomFormattingNode; +} \ No newline at end of file diff --git a/src/controls/dynamicForm/DynamicForm.module.scss b/src/controls/dynamicForm/DynamicForm.module.scss index d3fc6b123..f5f9a8020 100644 --- a/src/controls/dynamicForm/DynamicForm.module.scss +++ b/src/controls/dynamicForm/DynamicForm.module.scss @@ -147,3 +147,59 @@ } } } + +.selectedFileContainer { + display: flex; + margin: 10px 0px; +} + +h2.sectionTitle { + color: #000000; + font-weight: 600; + font-size: 16px; + margin-top: 6px; + margin-bottom: 12px; + clear: both; +} +.sectionFormFields { + display: flex; + flex-wrap: wrap; +} +.sectionFormField { + @media (max-width: 1920px) { + min-width: 244px; + max-width: 244px; + } + @media (max-width: 1366px) { + min-width: 248px; + max-width: 248px; + margin-right: 44px; + } + @media (max-width: 1024px) { + min-width: 272px; + max-width: 272px; + } + @media (max-width: 640px) { + min-width: 432px; + max-width: 432px; + } + @media (max-width: 480px) { + width: 90%; + } +} +.sectionLine { + width: 100%; + border-top: 1px solid #edebe9; + border-bottom-width: 0; + border-left-width: 0; + border-right-width: 0; + clear: both; +} + +:global { + .sp-field-customFormatter { + min-height: inherit; + display: flex; + align-items: center; + } +} \ No newline at end of file diff --git a/src/controls/dynamicForm/DynamicForm.tsx b/src/controls/dynamicForm/DynamicForm.tsx index 72d1bd9d9..eb15b71e5 100644 --- a/src/controls/dynamicForm/DynamicForm.tsx +++ b/src/controls/dynamicForm/DynamicForm.tsx @@ -1,37 +1,50 @@ /* eslint-disable @microsoft/spfx/no-async-await */ -import { SPHttpClient } from "@microsoft/sp-http"; -import { IInstalledLanguageInfo, sp } from "@pnp/sp/presets/all"; +import * as React from "react"; import * as strings from "ControlStrings"; +import styles from "./DynamicForm.module.scss"; + +// Controls import { DefaultButton, PrimaryButton, } from "@fluentui/react/lib/Button"; -import { IDropdownOption } from "@fluentui/react/lib/components/Dropdown"; +import { + Dialog, + DialogFooter, + DialogType, +} from "@fluentui/react/lib/Dialog"; +import { IDropdownOption } from "@fluentui/react/lib/Dropdown"; +import { MessageBar, MessageBarType } from "@fluentui/react/lib/MessageBar"; import { ProgressIndicator } from "@fluentui/react/lib/ProgressIndicator"; import { IStackTokens, Stack } from "@fluentui/react/lib/Stack"; -import * as React from "react"; -import { IUploadImageResult } from "../../common/SPEntities"; -import SPservice from "../../services/SPService"; -import { IFilePickerResult } from "../filePicker"; +import { Icon } from "@fluentui/react/lib/components/Icon/Icon"; import { DynamicField } from "./dynamicField"; import { DateFormat, FieldChangeAdditionalData, IDynamicFieldProps, } from "./dynamicField/IDynamicFieldProps"; -import styles from "./DynamicForm.module.scss"; -import { IDynamicFormProps } from "./IDynamicFormProps"; -import { IDynamicFormState } from "./IDynamicFormState"; -import { - Dialog, - DialogFooter, - DialogType, -} from "@fluentui/react/lib/Dialog"; +import { FilePicker, IFilePickerResult } from "../filePicker"; +// pnp/sp, helpers / utils +import { sp } from "@pnp/sp"; import "@pnp/sp/lists"; import "@pnp/sp/content-types"; import "@pnp/sp/folders"; import "@pnp/sp/items"; +import { IInstalledLanguageInfo } from "@pnp/sp/presets/all"; +import { cloneDeep, isEqual } from "lodash"; +import { ICustomFormatting, ICustomFormattingBodySection, ICustomFormattingNode } from "../../common/utilities/ICustomFormatting"; +import SPservice from "../../services/SPService"; +import { IRenderListDataAsStreamClientFormResult } from "../../services/ISPService"; +import { ISPField, IUploadImageResult } from "../../common/SPEntities"; +import { FormulaEvaluation } from "../../common/utilities/FormulaEvaluation"; +import { Context } from "../../common/utilities/FormulaEvaluation.types"; +import CustomFormattingHelper from "../../common/utilities/CustomFormatting"; + +// Dynamic Form Props / State +import { IDynamicFormProps } from "./IDynamicFormProps"; +import { IDynamicFormState } from "./IDynamicFormState"; const stackTokens: IStackTokens = { childrenGap: 20 }; @@ -43,14 +56,17 @@ export class DynamicForm extends React.Component< IDynamicFormState > { private _spService: SPservice; + private _formulaEvaluation: FormulaEvaluation; + private _customFormatter: CustomFormattingHelper; + private webURL = this.props.webAbsoluteUrl ? this.props.webAbsoluteUrl : this.props.context.pageContext.web.absoluteUrl; constructor(props: IDynamicFormProps) { super(props); - // Initialize pnp sp + // Initialize pnp sp if (this.props.webAbsoluteUrl) { sp.setup({ sp: { @@ -68,38 +84,108 @@ export class DynamicForm extends React.Component< // Initialize state this.state = { + infoErrorMessages: [], fieldCollection: [], + validationFormulas: {}, + clientValidationFormulas: {}, + validationErrors: {}, + hiddenByFormula: [], isValidationErrorDialogOpen: false, }; + // Get SPService Factory this._spService = this.props.webAbsoluteUrl ? new SPservice(this.props.context, this.props.webAbsoluteUrl) : new SPservice(this.props.context); + + // Setup Formula Validation utils + this._formulaEvaluation = new FormulaEvaluation(this.props.context, this.props.webAbsoluteUrl); + + // Setup Custom Formatting utils + this._customFormatter = new CustomFormattingHelper(this._formulaEvaluation); } /** * Lifecycle hook when component is mounted */ public componentDidMount(): void { - this.getFieldInformations() + this.getListInformation() .then(() => { /* no-op; */ }) - .catch(() => { + .catch((err) => { /* no-op; */ + console.error(err); + }); + } + + public componentDidUpdate(prevProps: IDynamicFormProps, prevState: IDynamicFormState): void { + if (!isEqual(prevProps, this.props)) { + // Props have changed due to parent component or workbench config, reset state + this.setState({ + infoErrorMessages: [], // Reset info/error messages + validationErrors: {} // Reset validation errors + }, () => { + // If listId or listItemId have changed, reload list information + if (prevProps.listId !== this.props.listId || prevProps.listItemId !== this.props.listItemId) { + this.getListInformation() + .then(() => { + /* no-op; */ + }) + .catch((err) => { + /* no-op; */ + console.error(err); + }); + } else { + this.performValidation(); + } }); + } } /** * Default React component render method */ public render(): JSX.Element { - const { fieldCollection, isSaving } = this.state; + const { customFormatting, fieldCollection, hiddenByFormula, infoErrorMessages, isSaving } = this.state; - const fieldOverrides = this.props.fieldOverrides; + const customFormattingDisabled = this.props.useCustomFormatting === false; + + // Custom Formatting - Header + let headerContent: JSX.Element; + if (!customFormattingDisabled && customFormatting?.header) { + headerContent = this._customFormatter.renderCustomFormatContent(customFormatting.header, this.getFormValuesForValidation(), true) as JSX.Element; + } + + // Custom Formatting - Body + const bodySections: ICustomFormattingBodySection[] = []; + if (!customFormattingDisabled && customFormatting?.body) { + bodySections.push(...customFormatting.body.slice()); + if (bodySections.length > 0) { + const specifiedFields: string[] = bodySections.reduce((prev, cur) => { + prev.push(...cur.fields); + return prev; + }, []); + const omittedFields = fieldCollection.filter(f => !specifiedFields.includes(f.label)).map(f => f.label); + bodySections[bodySections.length - 1].fields.push(...omittedFields); + } + } + + // Custom Formatting - Footer + let footerContent: JSX.Element; + if (!customFormattingDisabled && customFormatting?.footer) { + footerContent = this._customFormatter.renderCustomFormatContent(customFormatting.footer, this.getFormValuesForValidation(), true) as JSX.Element; + } + + // Content Type + let contentTypeId = this.props.contentTypeId; + if (this.state.contentTypeId !== undefined) contentTypeId = this.state.contentTypeId; return (
+ {infoErrorMessages.map((ie, i) => ( + {ie.message} + ))} {fieldCollection.length === 0 ? (
) : (
- {fieldCollection.map((v, i) => { - if ( - fieldOverrides && - Object.prototype.hasOwnProperty.call( - fieldOverrides, - v.columnInternalName - ) - ) { - v.disabled = v.disabled || isSaving; - return fieldOverrides[v.columnInternalName](v); - } - return ( - - ); - })} + {headerContent} + {this.props.enableFileSelection === true && + this.props.listItemId === undefined && + contentTypeId !== undefined && + contentTypeId.startsWith("0x0101") && + this.renderFileSelectionControl()} + {(bodySections.length > 0 && !customFormattingDisabled) && bodySections + .filter(bs => bs.fields.filter(bsf => hiddenByFormula.indexOf(bsf) < 0).length > 0) + .map((section, i) => ( + <> +

{section.displayname}

+
+ {section.fields.map((f, i) => ( +
+ {this.renderField(fieldCollection.find(fc => fc.label === f) as IDynamicFieldProps)} +
+ ))} +
+ {i < bodySections.length - 1 &&
} + + ))} + {(bodySections.length === 0 || customFormattingDisabled) && fieldCollection.map((f, i) => this.renderField(f))} + {footerContent} {!this.props.disabled && ( { + const { fieldOverrides } = this.props; + const { hiddenByFormula, isSaving, validationErrors } = this.state; + + // If the field is hidden by a formula, don't render it + if (hiddenByFormula.find(h => h === field.columnInternalName)) { + return null; + } + + // If validation error, show error message + let validationErrorMessage: string = ""; + if (validationErrors[field.columnInternalName]) { + validationErrorMessage = validationErrors[field.columnInternalName]; + } + + // If field override is provided, use it instead of the DynamicField component + if ( + fieldOverrides && + Object.prototype.hasOwnProperty.call( + fieldOverrides, + field.columnInternalName + ) + ) { + return fieldOverrides[field.columnInternalName]({ ...field,disabled: field.disabled || isSaving} ) + } + + // Default render + return ( + + ); + } + + private updateFormMessages(type: MessageBarType, message: string): void { + const { infoErrorMessages } = this.state; + const newMessages = infoErrorMessages.slice(); + newMessages.push({ type, message }); + this.setState({ infoErrorMessages: newMessages }); + } + + /** Triggered when the user submits the form. */ private onSubmitClick = async (): Promise => { const { listId, listItemId, - contentTypeId, onSubmitted, onBeforeSubmit, onSubmitError, + enableFileSelection, + validationErrorDialogProps, + returnListItemInstanceOnSubmit } = this.props; + let contentTypeId = this.props.contentTypeId; + if (this.state.contentTypeId !== undefined) contentTypeId = this.state.contentTypeId; + + const fileSelectRendered = !listItemId && contentTypeId.startsWith("0x0101") && enableFileSelection === true; + try { + + /** Set to true to cancel form submission */ let shouldBeReturnBack = false; + const fields = (this.state.fieldCollection || []).slice(); - fields.forEach((val) => { - if (val.required) { - if (val.newValue === null) { + fields.forEach((field) => { + + // When a field is required and has no value + if (field.required) { + if (field.newValue === null) { if ( - val.fieldDefaultValue === null || - val.fieldDefaultValue === "" || - val.fieldDefaultValue.length === 0 || - val.fieldDefaultValue === undefined + field.defaultValue === null || + field.defaultValue === "" || + field.defaultValue.length === 0 || + field.defaultValue === undefined ) { - if (val.fieldType === "DateTime") val.fieldDefaultValue = null; - else val.fieldDefaultValue = ""; + if (field.fieldType === "DateTime") field.defaultValue = null; + else field.defaultValue = ""; shouldBeReturnBack = true; } - } else if (val.newValue === "") { - val.fieldDefaultValue = ""; + } else if (field.newValue === "") { + field.defaultValue = ""; shouldBeReturnBack = true; - } else if (Array.isArray(val.newValue) && val.newValue.length === 0) { - val.fieldDefaultValue = null; + } else if (Array.isArray(field.newValue) && field.newValue.length === 0) { + field.defaultValue = null; shouldBeReturnBack = true; } } - if (val.fieldType === "Number") { - if (val.showAsPercentage) val.newValue /= 100; - if (this.isEmptyNumOrString(val.newValue) && (val.minimumValue !== null || val.maximumValue !== null)) { - val.newValue = val.fieldDefaultValue = null; - } - if (!this.isEmptyNumOrString(val.newValue) && (isNaN(Number(val.newValue)) || (val.newValue < val.minimumValue) || (val.newValue > val.maximumValue))) { + + // Check min and max values for number fields + if (field.fieldType === "Number" && field.newValue !== undefined && field.newValue.trim() !== "") { + if ((field.newValue < field.minimumValue) || (field.newValue > field.maximumValue)) { shouldBeReturnBack = true; } } + }); + + // Perform validation + const validationDisabled = this.props.useFieldValidation === false; + let validationErrors: Record = {}; + if (!validationDisabled) { + validationErrors = await this.evaluateFormulas(this.state.validationFormulas, true, true, this.state.hiddenByFormula) as Record; + if (Object.keys(validationErrors).length > 0) { + shouldBeReturnBack = true; + } + } + + // If validation failed, return without saving if (shouldBeReturnBack) { this.setState({ fieldCollection: fields, isValidationErrorDialogOpen: - this.props.validationErrorDialogProps + validationErrorDialogProps + ?.showDialogOnValidationError === true, + }); + return; + } + + if (fileSelectRendered === true && this.state.selectedFile === undefined && this.props.listItemId === undefined) { + this.setState({ + missingSelectedFile: true, + isValidationErrorDialogOpen: + validationErrorDialogProps ?.showDialogOnValidationError === true, + validationErrors }); return; } @@ -232,55 +400,85 @@ export class DynamicForm extends React.Component< isSaving: true, }); + /** Item values for save / update */ const objects = {}; + for (let i = 0, len = fields.length; i < len; i++) { - const val = fields[i]; + const field = fields[i]; const { fieldType, additionalData, columnInternalName, hiddenFieldName, - } = val; - if (val.newValue !== null && val.newValue !== undefined) { - let value = val.newValue; + } = field; + if (field.newValue !== null && field.newValue !== undefined) { + + let value = field.newValue; + if (["Lookup", "LookupMulti", "User", "UserMulti", "TaxonomyFieldTypeMulti"].indexOf(fieldType) < 0) { + objects[columnInternalName] = value; + } + + // Choice fields + + if (fieldType === "Choice") { + objects[columnInternalName] = field.newValue.key; + } + if (fieldType === "MultiChoice") { + objects[columnInternalName] = { results: field.newValue }; + } + + // Lookup fields + if (fieldType === "Lookup") { if (value && value.length > 0) { objects[`${columnInternalName}Id`] = value[0].key; } else { objects[`${columnInternalName}Id`] = null; } - } else if (fieldType === "LookupMulti") { + } + if (fieldType === "LookupMulti") { value = []; - val.newValue.forEach((element) => { + field.newValue.forEach((element) => { value.push(element.key); }); objects[`${columnInternalName}Id`] = { results: value.length === 0 ? null : value, }; - } else if (fieldType === "TaxonomyFieldType") { + } + + // User fields + + if (fieldType === "User") { + objects[`${columnInternalName}Id`] = field.newValue.length === 0 ? null : field.newValue; + } + if (fieldType === "UserMulti") { + objects[`${columnInternalName}Id`] = { + results: field.newValue.length === 0 ? null : field.newValue, + }; + } + + // Taxonomy / Managed Metadata fields + + if (fieldType === "TaxonomyFieldType") { objects[columnInternalName] = { __metadata: { type: "SP.Taxonomy.TaxonomyFieldValue" }, Label: value[0]?.name ?? "", TermGuid: value[0]?.key ?? "11111111-1111-1111-1111-111111111111", WssId: "-1", }; - } else if (fieldType === "TaxonomyFieldTypeMulti") { - objects[hiddenFieldName] = val.newValue + } + if (fieldType === "TaxonomyFieldTypeMulti") { + objects[hiddenFieldName] = field.newValue .map((term) => `-1#;${term.name}|${term.key};`) .join("#"); - } else if (fieldType === "User") { - objects[`${columnInternalName}Id`] = val.newValue.length === 0 ? null : val.newValue; - } else if (fieldType === "Choice") { - objects[columnInternalName] = val.newValue.key; - } else if (fieldType === "MultiChoice") { - objects[columnInternalName] = { results: val.newValue }; - } else if (fieldType === "Location") { - objects[columnInternalName] = JSON.stringify(val.newValue); - } else if (fieldType === "UserMulti") { - objects[`${columnInternalName}Id`] = { - results: val.newValue.length === 0 ? null : val.newValue, - }; - } else if (fieldType === "Thumbnail") { + } + + // Other fields + + if (fieldType === "Location") { + objects[columnInternalName] = JSON.stringify(field.newValue); + } + if (fieldType === "Thumbnail") { if (additionalData) { const uploadedImage = await this.uploadImage(additionalData); objects[columnInternalName] = JSON.stringify({ @@ -293,9 +491,6 @@ export class DynamicForm extends React.Component< objects[columnInternalName] = null; } } - else { - objects[columnInternalName] = val.newValue; - } } } @@ -310,6 +505,8 @@ export class DynamicForm extends React.Component< } } + let apiError: string; + // If we have the item ID, we simply need to update it let newETag: string | undefined = undefined; if (listItemId) { @@ -322,44 +519,52 @@ export class DynamicForm extends React.Component< if (onSubmitted) { onSubmitted( iur.data, - this.props.returnListItemInstanceOnSubmit !== false + returnListItemInstanceOnSubmit !== false ? iur.item : undefined ); } } catch (error) { + apiError = error.message; if (onSubmitError) { onSubmitError(objects, error); } console.log("Error", error); } } + // Otherwise, depending on the content type ID of the item, if any, we need to behave accordingly else if ( contentTypeId === undefined || contentTypeId === "" || - !contentTypeId.startsWith("0x0120")|| - contentTypeId.startsWith("0x01") + (!contentTypeId.startsWith("0x0120") && + contentTypeId.startsWith("0x01")) ) { - // We are adding a new list item - try { - const contentTypeIdField = "ContentTypeId"; - //check if item contenttype is passed, then update the object with content type id, else, pass the object - if (contentTypeId !== undefined && contentTypeId.startsWith("0x01")) objects[contentTypeIdField] = contentTypeId; - const iar = await sp.web.lists.getById(listId).items.add(objects); - if (onSubmitted) { - onSubmitted( - iar.data, - this.props.returnListItemInstanceOnSubmit !== false - ? iar.item - : undefined - ); - } - } catch (error) { - if (onSubmitError) { - onSubmitError(objects, error); + if (fileSelectRendered === true) { + await this.addFileToLibrary(objects); + } + else { + // We are adding a new list item + try { + const contentTypeIdField = "ContentTypeId"; + // check if item contenttype is passed, then update the object with content type id, else, pass the object + if (contentTypeId !== undefined && contentTypeId.startsWith("0x01")) objects[contentTypeIdField] = contentTypeId; + const iar = await sp.web.lists.getById(listId).items.add(objects); + if (onSubmitted) { + onSubmitted( + iar.data, + this.props.returnListItemInstanceOnSubmit !== false + ? iar.item + : undefined + ); + } + } catch (error) { + apiError = error.message; + if (onSubmitError) { + onSubmitError(objects, error); + } + console.log("Error", error); } - console.log("Error", error); } } else if (contentTypeId.startsWith("0x0120")) { @@ -403,6 +608,7 @@ export class DynamicForm extends React.Component< ); } } catch (error) { + apiError = error.message; if (onSubmitError) { onSubmitError(objects, error); } @@ -413,6 +619,7 @@ export class DynamicForm extends React.Component< this.setState({ isSaving: false, etag: newETag, + infoErrorMessages: apiError ? [{ type: MessageBarType.error, message: apiError }] : [], }); } catch (error) { if (onSubmitError) { @@ -422,21 +629,109 @@ export class DynamicForm extends React.Component< } }; - // trigger when the user change any value in the form + /** + * Adds selected file to the library + */ + private addFileToLibrary = async (objects: {}): Promise => { + const { + selectedFile + } = this.state; + + const { + listId, + contentTypeId, + onSubmitted, + onSubmitError, + returnListItemInstanceOnSubmit + } = this.props; + + + if (selectedFile !== undefined) { + try { + const idField = "ID"; + const contentTypeIdField = "ContentTypeId"; + + const library = await sp.web.lists.getById(listId); + const itemTitle = + selectedFile !== undefined && selectedFile.fileName !== undefined && selectedFile.fileName !== "" + ? (selectedFile.fileName as string).replace( + /["|*|:|<|>|?|/|\\||]/g, + "_" + ) // Replace not allowed chars in folder name + : ""; // Empty string will be replaced by SPO with Folder Item ID + + const fileCreatedResult = await library.rootFolder.files.addChunked(encodeURI(itemTitle), await selectedFile.downloadFileContent()); + const fields = await fileCreatedResult.file.listItemAllFields(); + + if (fields[idField]) { + // Read the ID of the just created folder or Document Set + const folderId = fields[idField]; + + // Set the content type ID for the target item + objects[contentTypeIdField] = contentTypeId; + // Update the just created folder or Document Set + const iur = await library.items.getById(folderId).update(objects); + if (onSubmitted) { + onSubmitted( + iur.data, + returnListItemInstanceOnSubmit !== false + ? iur.item + : undefined + ); + } + } else { + throw new Error( + "Unable to read the ID of the just created folder or Document Set" + ); + } + } catch (error) { + if (onSubmitError) { + onSubmitError(objects, error); + } + console.log("Error", error); + } + } + } + + /** + * Triggered when the user makes any field value change in the form + */ private onChange = async ( internalName: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any newValue: any, - additionalData?: FieldChangeAdditionalData + validate: boolean, + additionalData?: FieldChangeAdditionalData, ): Promise => { - // eslint-disable-line @typescript-eslint/no-explicit-any - // try { - const fieldCol = (this.state.fieldCollection || []).slice(); + + const fieldCol = cloneDeep(this.state.fieldCollection || []); const field = fieldCol.filter((element, i) => { return element.columnInternalName === internalName; })[0]; + + // Init new value(s) field.newValue = newValue; + field.stringValue = newValue.toString(); field.additionalData = additionalData; + field.subPropertyValues = {}; + + // Store string values for various field types + + if (field.fieldType === "Choice") { + field.stringValue = newValue.text; + } + if (field.fieldType === "MultiChoice") { + field.stringValue = newValue.join(';#'); + } + if (field.fieldType === "Lookup" || field.fieldType === "LookupMulti") { + field.stringValue = newValue.map(nv => nv.key + ';#' + nv.name).join(';#'); + } + if (field.fieldType === "TaxonomyFieldType" || field.fieldType === "TaxonomyFieldTypeMulti") { + field.stringValue = newValue.map(nv => nv.name).join(';'); + } + + // Capture additional property data for User fields + if (field.fieldType === "User" && newValue.length !== 0) { if ( newValue[0].id === undefined || @@ -448,11 +743,19 @@ export class DynamicForm extends React.Component< } const result = await sp.web.ensureUser(user); field.newValue = result.data.Id; // eslint-disable-line require-atomic-updates + field.stringValue = user; + field.subPropertyValues = { + id: result.data.Id, + title: result.data.Title, + email: result.data.Email, + }; } else { field.newValue = newValue[0].id; } - } else if (field.fieldType === "UserMulti" && newValue.length !== 0) { + } + if (field.fieldType === "UserMulti" && newValue.length !== 0) { field.newValue = []; + const emails: string[] = []; for (let index = 0; index < newValue.length; index++) { const element = newValue[index]; if ( @@ -465,18 +768,141 @@ export class DynamicForm extends React.Component< } const result = await sp.web.ensureUser(user); field.newValue.push(result.data.Id); + emails.push(user); } else { field.newValue.push(element.id); } } + field.stringValue = emails.join(";"); } + + const validationErrors = {...this.state.validationErrors}; + if (validationErrors[field.columnInternalName]) delete validationErrors[field.columnInternalName]; + this.setState({ fieldCollection: fieldCol, + validationErrors + }, () => { + if (validate) this.performValidation(); }); }; - //getting all the fields information as part of get ready process - private getFieldInformations = async (): Promise => { + /** Validation callback, used when form first loads (getListInformation) and following onChange */ + private performValidation = (skipFieldValueValidation?: boolean): void => { + const { useClientSideValidation, useFieldValidation } = this.props; + const { clientValidationFormulas, validationFormulas } = this.state; + if (Object.keys(clientValidationFormulas).length || Object.keys(validationFormulas).length) { + this.setState({ + isSaving: true, // Disable save btn and fields while validation in progress + }, () => { + const clientSideValidationDisabled = useClientSideValidation === false; + const fieldValidationDisabled = useFieldValidation === false; + const hiddenByFormula: string[] = !clientSideValidationDisabled ? this.evaluateColumnVisibilityFormulas() : []; + let validationErrors = { ...this.state.validationErrors }; + if (!skipFieldValueValidation && !fieldValidationDisabled) validationErrors = this.evaluateFieldValueFormulas(hiddenByFormula); + this.setState({ hiddenByFormula, isSaving: false, validationErrors }); + }); + } + } + + /** Determines visibility of fields that have show/hide formulas set in Edit Form > Edit Columns > Edit Conditional Formula */ + private evaluateColumnVisibilityFormulas = (): string[] => { + return this.evaluateFormulas(this.state.clientValidationFormulas, false) as string[]; + } + + /** Evaluates field validation formulas set in column settings and returns a Record of error messages */ + private evaluateFieldValueFormulas = (hiddenFields: string[]): Record => { + return this.evaluateFormulas(this.state.validationFormulas, true, true, hiddenFields) as Record; + } + + /** + * Evaluates formulas and returns a Record of error messages or an array of column names that have failed validation + * @param formulas A Record / dictionary-like object, where key is internal column name and value is an object with ValidationFormula and ValidationMessage properties + * @param returnMessages Determines whether a Record of error messages is returned or an array of column names that have failed validation + * @param requireValue Set to true if the formula should only be evaluated when the field has a value + * @returns + */ + private evaluateFormulas = ( + formulas: Record>, + returnMessages = true, + requireValue: boolean = false, + ignoreFields: string[] = [] + ): string[] | Record => { + const { fieldCollection } = this.state; + const results: Record = {}; + for (let i = 0; i < Object.keys(formulas).length; i++) { + const fieldName = Object.keys(formulas)[i]; + if (formulas[fieldName]) { + const field = fieldCollection.find(f => f.columnInternalName === fieldName); + if (!field) continue; + if (ignoreFields.indexOf(fieldName) > -1) continue; // Skip fields that are being ignored (e.g. hidden by formula) + const formula = formulas[fieldName].ValidationFormula; + const message = formulas[fieldName].ValidationMessage; + if (!formula) continue; + const context = this.getFormValuesForValidation(); + if (requireValue && !context[fieldName]) continue; + const result = this._formulaEvaluation.evaluate(formula, context); + if (Boolean(result) !== true) { + results[fieldName] = message; + } + } + } + if (!returnMessages) { return Object.keys(results); } + return results; + } + + /** + * Used for validation. Returns a Record of field values, where key is internal column name and value is the field value. + * Expands certain properties and stores many of them as primitives (strings, numbers or bools) so the expression evaluator + * can process them. For example: a User column named Person will have values stored as Person, Person.email, Person.title etc. + * This is so the expression evaluator can process expressions like '=[$Person.title] == "Contoso Employee 1138"' + * @param fieldCollection Optional. Could be used to compare field values in state with previous state. + * @returns + */ + private getFormValuesForValidation = (fieldCollection?: IDynamicFieldProps[]): Context => { + const { fieldCollection: fieldColFromState } = this.state; + if (!fieldCollection) fieldCollection = fieldColFromState; + return fieldCollection.reduce((prev, cur) => { + let value: boolean | number | object | string | undefined = cur.value; + switch (cur.fieldType) { + case "Lookup": + case "Choice": + case "TaxonomyFieldType": + case "LookupMulti": + case "MultiChoice": + case "TaxonomyFieldTypeMulti": + case "User": + case "UserMulti": + value = cur.stringValue; + break; + case "Currency": + case "Number": + if (cur.value !== undefined && cur.value !== null) value = Number(cur.value); + if (cur.newValue !== undefined && cur.newValue !== null) value = Number(cur.newValue); + break; + case "URL": + if (cur.value !== undefined && cur.value !== null) value = cur.value.Url; + if (cur.newValue !== undefined && cur.newValue !== null) value = cur.newValue.Url; + value = cur.newValue ? cur.newValue.Url : null; + break; + default: + value = cur.newValue || cur.value; + break; + } + prev[cur.columnInternalName] = value; + if (cur.subPropertyValues) { + Object.keys(cur.subPropertyValues).forEach((key) => { + prev[`${cur.columnInternalName}.${key}`] = cur.subPropertyValues[key]; + }); + } + return prev; + }, {} as Context); + } + + /** + * Invoked when component first mounts, loads information about the SharePoint list, fields and list item + */ + private getListInformation = async (): Promise => { const { listId, listItemId, @@ -485,12 +911,63 @@ export class DynamicForm extends React.Component< onListItemLoaded, } = this.props; let contentTypeId = this.props.contentTypeId; + try { - const spList = await sp.web.lists.getById(listId); + + // Fetch form rendering information from SharePoint + const listInfo = await this._spService.getListFormRenderInfo(listId); + + // Fetch additional information about fields from SharePoint + // (Number fields for min and max values, and fields with validation) + const additionalInfo = await this._spService.getAdditionalListFormFieldInfo(listId); + const numberFields = additionalInfo.filter((f) => f.TypeAsString === "Number" || f.TypeAsString === "Currency"); + + // Build a dictionary of validation formulas and messages + const validationFormulas: Record> = additionalInfo.reduce((prev, cur) => { + if (!prev[cur.InternalName] && cur.ValidationFormula) { + prev[cur.InternalName] = { + ValidationFormula: cur.ValidationFormula, + ValidationMessage: cur.ValidationMessage, + }; + } + return prev; + }, {}); + + // If no content type ID is provided, use the default (first one in the list) + if (contentTypeId === undefined || contentTypeId === "") { + contentTypeId = Object.keys(listInfo.ContentTypeIdToNameMap)[0]; + } + const contentTypeName: string = listInfo.ContentTypeIdToNameMap[contentTypeId]; + + // Build a dictionary of client validation formulas and messages + // These are formulas that are added in Edit Form > Edit Columns > Edit Conditional Formula + // They are evaluated on the client side, and determine whether a field should be hidden or shown + const clientValidationFormulas = listInfo.ClientForms.Edit[contentTypeName].reduce((prev, cur) => { + if (cur.ClientValidationFormula) { + prev[cur.InternalName] = { + ValidationFormula: cur.ClientValidationFormula, + ValidationMessage: cur.ClientValidationMessage, + }; + } + return prev; + }, {} as Record>); + + // Custom Formatting + let headerJSON: ICustomFormattingNode, footerJSON: ICustomFormattingNode; + let bodySections: ICustomFormattingBodySection[]; + if (listInfo.ClientFormCustomFormatter && listInfo.ClientFormCustomFormatter[contentTypeId]) { + const customFormatInfo = JSON.parse(listInfo.ClientFormCustomFormatter[contentTypeId]) as ICustomFormatting; + bodySections = customFormatInfo.bodyJSONFormatter.sections; + headerJSON = customFormatInfo.headerJSONFormatter; + footerJSON = customFormatInfo.footerJSONFormatter; + } + + // Load SharePoint list item + const spList = sp.web.lists.getById(listId); let item = null; let etag: string | undefined = undefined; if (listItemId !== undefined && listItemId !== null && listItemId !== 0) { - item = await spList.items.getById(listItemId).get(); + item = await spList.items.getById(listItemId).get().catch(err => this.updateFormMessages(MessageBarType.error, err.message)); if (onListItemLoaded) { await onListItemLoaded(item); @@ -501,263 +978,321 @@ export class DynamicForm extends React.Component< } } - if (contentTypeId === undefined || contentTypeId === "") { - const defaultContentType = await spList.contentTypes - .select("Id", "Name") - .get(); - contentTypeId = defaultContentType[0].Id.StringValue; - } - const listFields = await this.getFormFields( + // Build the field collection + const tempFields: IDynamicFieldProps[] = await this.buildFieldCollection( + listInfo, + contentTypeName, + item, + numberFields, listId, - contentTypeId, - this.webURL + listItemId, + disabledFields ); - const tempFields: IDynamicFieldProps[] = []; - let order: number = 0; - const responseValue = listFields.value; - const hiddenFields = - this.props.hiddenFields !== undefined ? this.props.hiddenFields : []; - let defaultDayOfWeek: number = 0; - for (let i = 0, len = responseValue.length; i < len; i++) { - const field = responseValue[i]; - - // Handle only fields that are not marked as hidden - if (hiddenFields.indexOf(field.EntityPropertyName) < 0) { - order++; - const fieldType = field.TypeAsString; - field.order = order; - let cultureName: string; - let hiddenName = ""; - let termSetId = ""; - let anchorId = ""; - let lookupListId = ""; - let lookupField = ""; - const choices: IDropdownOption[] = []; - let defaultValue = null; - const selectedTags: any = []; // eslint-disable-line @typescript-eslint/no-explicit-any - let richText = false; - let dateFormat: DateFormat | undefined; - let principalType = ""; - let minValue: number | undefined; - let maxValue: number | undefined; - let showAsPercentage: boolean | undefined; + + // Get installed languages for Currency fields + let installedLanguages: IInstalledLanguageInfo[]; + if (tempFields.filter(f => f.fieldType === "Currency").length > 0) { + installedLanguages = await sp.web.regionalSettings.getInstalledLanguages(); + } + + this.setState({ + contentTypeId, + clientValidationFormulas, + customFormatting: { + header: headerJSON, + body: bodySections, + footer: footerJSON + }, + etag, + fieldCollection: tempFields, + installedLanguages, + validationFormulas + }, () => this.performValidation(true)); + + } catch (error) { + this.updateFormMessages(MessageBarType.error, 'An error occurred while loading: ' + error.message); + console.error(`An error occurred while loading DynamicForm`, error); + return null; + } + } + + /** + * Builds a collection of fields to be rendered in the form + * @param listInfo Data returned by RenderListDataAsStream with RenderOptions = 64 (ClientFormSchema) + * @param contentTypeName SharePoint List Content Type + * @param item SharePoint List Item + * @param numberFields Additional information about Number fields (min and max values) + * @param listId SharePoint List ID + * @param listItemId SharePoint List Item ID + * @param disabledFields Fields that should be disabled due to configuration + * @returns + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private async buildFieldCollection(listInfo: IRenderListDataAsStreamClientFormResult, contentTypeName: string, item: any, numberFields: ISPField[], listId: string, listItemId: number, disabledFields: string[]): Promise { + const tempFields: IDynamicFieldProps[] = []; + let order: number = 0; + const hiddenFields = this.props.hiddenFields !== undefined ? this.props.hiddenFields : []; + let defaultDayOfWeek: number = 0; + + for (let i = 0, len = listInfo.ClientForms.Edit[contentTypeName].length; i < len; i++) { + const field = listInfo.ClientForms.Edit[contentTypeName][i]; + + // Process fields that are not marked as hidden + if (hiddenFields.indexOf(field.InternalName) < 0) { + order++; + let hiddenName = ""; + let termSetId = ""; + let anchorId = ""; + let lookupListId = ""; + let lookupField = ""; + const choices: IDropdownOption[] = []; + let defaultValue = null; + let value = undefined; + let stringValue = null; + const subPropertyValues: Record = {}; + let richText = false; + let dateFormat: DateFormat | undefined; + let principalType = ""; + let cultureName: string; + let minValue: number | undefined; + let maxValue: number | undefined; + let showAsPercentage: boolean | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const selectedTags: any = []; + + // If a SharePoint Item was loaded, get the field value from it + if (item !== null && item[field.InternalName]) { + value = item[field.InternalName]; + stringValue = value.toString(); + } else { + defaultValue = field.DefaultValue; + } + + // Store choices for Choice fields + if (field.FieldType === "Choice") { + field.Choices.forEach((element) => { + choices.push({ key: element, text: element }); + }); + } + if (field.FieldType === "MultiChoice") { + field.MultiChoices.forEach((element) => { + choices.push({ key: element, text: element }); + }); + } + + // Setup Note, Number and Currency fields + if (field.FieldType === "Note") { + richText = field.RichText; + } + if (field.FieldType === "Number" || field.FieldType === "Currency") { + const numberField = numberFields.find(f => f.InternalName === field.InternalName); + if (numberField) { + minValue = numberField.MinimumValue; + maxValue = numberField.MaximumValue; + } + showAsPercentage = field.ShowAsPercentage; + if (field.FieldType === "Currency") { + cultureName = this.cultureNameLookup(numberField.CurrencyLocaleId); + } + } + + // Setup Lookup fields + if (field.FieldType === "Lookup" || field.FieldType === "LookupMulti") { + lookupListId = field.LookupListId; + lookupField = field.LookupFieldName; if (item !== null) { - defaultValue = item[field.EntityPropertyName]; + value = await this._spService.getLookupValues( + listId, + listItemId, + field.InternalName, + lookupField, + this.webURL + ); + stringValue = value?.map(dv => dv.key + ';#' + dv.name).join(';#'); + if (item[field.InternalName + "Id"]) { + subPropertyValues.id = item[field.InternalName + "Id"]; + subPropertyValues.lookupId = subPropertyValues.id; + } + subPropertyValues.lookupValue = value?.map(dv => dv.name); } else { - defaultValue = field.DefaultValue; + value = []; } - if (fieldType === "Choice" || fieldType === "MultiChoice") { - field.Choices.forEach((element) => { - choices.push({ key: element, text: element }); - }); - } else if (fieldType === "Note") { - richText = field.RichText; - } else if (fieldType === "Number" || fieldType === "Currency") { - minValue = field.MinimumValue; - maxValue = field.MaximumValue; - if (fieldType === "Number") { - showAsPercentage = field.ShowAsPercentage; - } else { - cultureName = this.cultureNameLookup(field.CurrencyLocaleId); - } - } else if (fieldType === "Lookup") { - lookupListId = field.LookupList; - lookupField = field.LookupField; - if (item !== null) { - defaultValue = await this._spService.getLookupValue( - listId, - listItemId, - field.EntityPropertyName, - lookupField, - this.webURL - ); - } else { - defaultValue = []; - } - } else if (fieldType === "LookupMulti") { - lookupListId = field.LookupList; - lookupField = field.LookupField; - if (item !== null) { - defaultValue = await this._spService.getLookupValues( + } + + // Setup User fields + if (field.FieldType === "User") { + if (item !== null) { + const userEmails: string[] = []; + userEmails.push( + (await this._spService.getUserUPNFromFieldValue( listId, listItemId, - field.EntityPropertyName, - lookupField, + field.InternalName, this.webURL - ); - } else { - defaultValue = []; + )) + "" + ); + value = userEmails; + stringValue = userEmails?.map(dv => dv.split('/').shift()).join(';'); + if (item[field.InternalName + "Id"]) { + subPropertyValues.id = item[field.InternalName + "Id"]; } - } else if (fieldType === "TaxonomyFieldTypeMulti") { - const response = await this._spService.getTaxonomyFieldInternalName( - this.props.listId, - field.TextField, + subPropertyValues.title = userEmails?.map(dv => dv.split('/').pop())[0]; + subPropertyValues.email = userEmails[0]; + } else { + value = []; + } + principalType = field.PrincipalAccountType; + } + if (field.FieldType === "UserMulti") { + if (item !== null) { + value = await this._spService.getUsersUPNFromFieldValue( + listId, + listItemId, + field.InternalName, this.webURL ); - hiddenName = response.value; - termSetId = field.TermSetId; - anchorId = field.AnchorId; - if (item && item[field.InternalName]) { - item[field.InternalName].forEach((element) => { - selectedTags.push({ - key: element.TermGuid, - name: element.Label, - }); - }); + stringValue = value?.map(dv => dv.split('/').pop()).join(';'); + } else { + value = []; + } + principalType = field.PrincipalAccountType; + } - defaultValue = selectedTags; - } else { - if (defaultValue && defaultValue !== "") { - defaultValue.split(/#|;/).forEach((element) => { - if (element.indexOf("|") !== -1) - selectedTags.push({ - key: element.split("|")[1], - name: element.split("|")[0], - }); - }); - - defaultValue = selectedTags; - } - } - if (defaultValue === "") defaultValue = null; - } else if (fieldType === "TaxonomyFieldType") { - termSetId = field.TermSetId; - anchorId = field.AnchorId; - if (item !== null) { - const response = - await this._spService.getSingleManagedMetadataLabel( - listId, - listItemId, - field.InternalName - ); - if (response) { - selectedTags.push({ - key: response.TermID, - name: response.Label, - }); - defaultValue = selectedTags; - } - } else { - if (defaultValue !== "") { - selectedTags.push({ - key: defaultValue.split("|")[1], - name: defaultValue.split("|")[0].split("#")[1], - }); - defaultValue = selectedTags; - } + // Setup Taxonomy / Metadata fields + if (field.FieldType === "TaxonomyFieldType") { + termSetId = field.TermSetId; + anchorId = field.AnchorId; + if (item !== null) { + const response = await this._spService.getSingleManagedMetadataLabel( + listId, + listItemId, + field.InternalName + ); + if (response) { + selectedTags.push({ + key: response.TermID, + name: response.Label, + }); + value = selectedTags; + stringValue = selectedTags?.map(dv => dv.key + ';#' + dv.name).join(';#'); } - if (defaultValue === "") defaultValue = null; - } else if (fieldType === "DateTime") { - if (item !== null && item[field.InternalName]) - defaultValue = new Date(item[field.InternalName]); - else if (defaultValue === "[today]") { - defaultValue = new Date(); + } else { + if (defaultValue !== "") { + selectedTags.push({ + key: defaultValue.split("|")[1], + name: defaultValue.split("|")[0].split("#")[1], + }); + value = selectedTags; } + } + if (defaultValue === "") defaultValue = null; + } + if (field.FieldType === "TaxonomyFieldTypeMulti") { + hiddenName = field.HiddenListInternalName; + termSetId = field.TermSetId; + anchorId = field.AnchorId; + if (item && item[field.InternalName]) { + item[field.InternalName].forEach((element) => { + selectedTags.push({ + key: element.TermGuid, + name: element.Label, + }); + }); - const schemaXml = field.SchemaXml; - const dateFormatRegEx = /\s+Format="([^"]+)"/gim.exec(schemaXml); - dateFormat = - dateFormatRegEx && dateFormatRegEx.length - ? (dateFormatRegEx[1] as DateFormat) - : "DateOnly"; - defaultDayOfWeek = (await this._spService.getRegionalWebSettings()) - .FirstDayOfWeek; - } else if (fieldType === "UserMulti") { - if (item !== null) - defaultValue = await this._spService.getUsersUPNFromFieldValue( - listId, - listItemId, - field.InternalName, - this.webURL - ); - else { - defaultValue = []; - } - principalType = field.SchemaXml.split('UserSelectionMode="')[1]; - principalType = principalType.substring( - 0, - principalType.indexOf('"') - ); - } else if (fieldType === "Thumbnail") { - if (defaultValue) { - defaultValue = JSON.parse(defaultValue).serverRelativeUrl; - } - } else if (fieldType === "User") { - if (item !== null) { - const userEmails: string[] = []; - userEmails.push( - (await this._spService.getUserUPNFromFieldValue( - listId, - listItemId, - field.InternalName, - this.webURL - )) + "" - ); - defaultValue = userEmails; - } else { - defaultValue = []; + value = selectedTags; + } else { + if (defaultValue && defaultValue !== "") { + defaultValue.split(/#|;/).forEach((element) => { + if (element.indexOf("|") !== -1) + selectedTags.push({ + key: element.split("|")[1], + name: element.split("|")[0], + }); + }); + + value = selectedTags; + stringValue = selectedTags?.map(dv => dv.key + ';#' + dv.name).join(';#'); } - principalType = field.SchemaXml.split('UserSelectionMode="')[1]; - principalType = principalType.substring( - 0, - principalType.indexOf('"') - ); - } else if (fieldType === "Location") { - defaultValue = JSON.parse(defaultValue); - } else if (fieldType === "Boolean") { - defaultValue = Boolean(Number(defaultValue)); - } - - tempFields.push({ - newValue: null, - fieldTermSetId: termSetId, - fieldAnchorId: anchorId, - options: choices, - lookupListID: lookupListId, - lookupField: lookupField, - cultureName, - changedValue: defaultValue, - fieldType: field.TypeAsString, - fieldTitle: field.Title, - fieldDefaultValue: defaultValue, - context: this.props.context, - disabled: - this.props.disabled || - (disabledFields && - disabledFields.indexOf(field.InternalName) > -1), - listId: this.props.listId, - columnInternalName: field.EntityPropertyName, - label: field.Title, - onChanged: this.onChange, - required: field.Required, - hiddenFieldName: hiddenName, - Order: field.order, - isRichText: richText, - dateFormat: dateFormat, - firstDayOfWeek: defaultDayOfWeek, - listItemId: listItemId, - principalType: principalType, - description: field.Description, - minimumValue: minValue, - maximumValue: maxValue, - showAsPercentage: showAsPercentage - }); - tempFields.sort((a, b) => a.Order - b.Order); + } + if (defaultValue === "") defaultValue = null; } - } - let installedLanguages: IInstalledLanguageInfo[]; - if (tempFields.filter(f => f.fieldType === "Currency").length > 0) { - installedLanguages = await sp.web.regionalSettings.getInstalledLanguages(); - } + // Setup DateTime fields + if (field.FieldType === "DateTime") { + if (item !== null && item[field.InternalName]) { + value = new Date(item[field.InternalName]); + stringValue = value.toISOString(); + } else if (defaultValue === "[today]") { + defaultValue = new Date(); + } else if (defaultValue) { + defaultValue = new Date(defaultValue); + } - this.setState({ fieldCollection: tempFields, installedLanguages, etag }); - //return arrayItems; - } catch (error) { - console.log(`Error get field informations`, error); - return null; + dateFormat = field.DateFormat || "DateOnly"; + defaultDayOfWeek = (await this._spService.getRegionalWebSettings()).FirstDayOfWeek; + } + + // Setup Thumbnail, Location and Boolean fields + if (field.FieldType === "Thumbnail") { + if (defaultValue) { + defaultValue = JSON.parse(defaultValue).serverRelativeUrl; + } + if (value) { + value = JSON.parse(value).serverRelativeUrl; + } + } + if (field.FieldType === "Location") { + if (defaultValue) defaultValue = JSON.parse(defaultValue); + if (value) value = JSON.parse(value); + } + if (field.FieldType === "Boolean") { + if (defaultValue !== undefined && defaultValue !== null) defaultValue = Boolean(Number(defaultValue)); + if (value !== undefined && value !== null) value = Boolean(Number(value)); + } + + tempFields.push({ + value, + newValue: undefined, + stringValue, + subPropertyValues, + cultureName, + fieldTermSetId: termSetId, + fieldAnchorId: anchorId, + options: choices, + lookupListID: lookupListId, + lookupField: lookupField, + // changedValue: defaultValue, + fieldType: field.FieldType, + // fieldTitle: field.Title, + defaultValue: defaultValue, + context: this.props.context, + disabled: this.props.disabled || + (disabledFields && + disabledFields.indexOf(field.InternalName) > -1), + // listId: this.props.listId, + columnInternalName: field.InternalName, + label: field.Title, + onChanged: this.onChange, + required: field.Required, + hiddenFieldName: hiddenName, + Order: order, + isRichText: richText, + dateFormat: dateFormat, + firstDayOfWeek: defaultDayOfWeek, + listItemId: listItemId, + principalType: principalType, + description: field.Description, + minimumValue: minValue, + maximumValue: maxValue, + showAsPercentage: showAsPercentage, + }); + + // This may not be necessary now using RenderListDataAsStream + tempFields.sort((a, b) => a.Order - b.Order); + } } - }; + return tempFields; + } private cultureNameLookup(lcid: number): string { const pageCulture = this.props.context.pageContext.cultureInfo.currentCultureName; @@ -799,50 +1334,6 @@ export class DynamicForm extends React.Component< }); }; - private getFormFields = async ( - listId: string, - contentTypeId: string | undefined, - webUrl?: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): Promise => { - // eslint-disable-line @typescript-eslint/no-explicit-any - try { - const { context } = this.props; - const webAbsoluteUrl = !webUrl ? this.webURL : webUrl; - let apiUrl = ""; - if (contentTypeId !== undefined && contentTypeId !== "") { - if (contentTypeId.startsWith("0x0120")) { - apiUrl = `${webAbsoluteUrl}/_api/web/lists(@listId)/contenttypes('${contentTypeId}')/fields?@listId=guid'${encodeURIComponent( - listId - )}'&$filter=ReadOnlyField eq false and (Hidden eq false or StaticName eq 'Title') and (FromBaseType eq false or StaticName eq 'Title')`; - } else { - apiUrl = `${webAbsoluteUrl}/_api/web/lists(@listId)/contenttypes('${contentTypeId}')/fields?@listId=guid'${encodeURIComponent( - listId - )}'&$filter=ReadOnlyField eq false and Hidden eq false and (FromBaseType eq false or StaticName eq 'Title')`; - } - } else { - apiUrl = `${webAbsoluteUrl}/_api/web/lists(@listId)/fields?@listId=guid'${encodeURIComponent( - listId - )}'&$filter=ReadOnlyField eq false and Hidden eq false and (FromBaseType eq false or StaticName eq 'Title')`; - } - const data = await context.spHttpClient.get( - apiUrl, - SPHttpClient.configurations.v1 - ); - if (data.ok) { - const results = await data.json(); - if (results) { - return results; - } - } - - return null; - } catch (error) { - console.dir(error); - return Promise.reject(error); - } - }; - private closeValidationErrorDialog = (): void => { this.setState({ isValidationErrorDialogOpen: false, @@ -871,7 +1362,75 @@ export class DynamicForm extends React.Component< return errorMessage; }; - private isEmptyNumOrString(value: string | number): boolean { - if ((value?.toString().trim().length || 0) === 0) return true; + private renderFileSelectionControl = (): React.ReactElement => { + const { + selectedFile, + missingSelectedFile + } = this.state; + + const labelEl = ; + + return
+
+ + {labelEl} +
+ { + if (filePickerResult.length === 1) { + this.setState({ + selectedFile: filePickerResult[0], + missingSelectedFile: false + }); + } + else { + this.setState({ + missingSelectedFile: true + }); + } + }} + required={true} + context={this.props.context} + hideWebSearchTab={true} + hideStockImages={true} + hideLocalMultipleUploadTab={true} + hideLinkUploadTab={true} + hideSiteFilesTab={true} + checkIfFileExists={true} + /> + {selectedFile &&
+ + {selectedFile.fileName} +
} + {missingSelectedFile === true && +
{strings.DynamicFormRequiredFileMessage}
} +
; } + + private getFileIconFromExtension = (): string => { + const fileExtension = this.state.selectedFile.fileName.split('.').pop(); + switch (fileExtension) { + case 'pdf': + return 'PDF'; + case 'docx': + case 'doc': + return 'WordDocument'; + case 'pptx': + case 'ppt': + return 'PowerPointDocument'; + case 'xlsx': + case 'xls': + return 'ExcelDocument'; + case 'jpg': + case 'jpeg': + case 'png': + case 'gif': + return 'FileImage'; + default: + return 'Document'; + } + } + } diff --git a/src/controls/dynamicForm/IDynamicFormProps.ts b/src/controls/dynamicForm/IDynamicFormProps.ts index f62e44196..43da7003e 100644 --- a/src/controls/dynamicForm/IDynamicFormProps.ts +++ b/src/controls/dynamicForm/IDynamicFormProps.ts @@ -78,8 +78,36 @@ export interface IDynamicFormProps { */ respectETag?: boolean; + /** + * Specifies whether custom formatting (set when customizing the out of the box form) should be used. Default - true + */ + useCustomFormatting?: boolean; + + /** + * Specifies whether client side validation should be used. Default - true + */ + useClientSideValidation?: boolean; + + /** + * Specifies whether field validation (set in column settings) should be used. Default - true + */ + useFieldValidation?: boolean; + /** * Specify validation error dialog properties */ validationErrorDialogProps?: IValidationErrorDialogProps; + + /** + * Specify if the form should support the creation of a new list item in a document library attaching a file to it. + * This option is only available for document libraries and works only when the contentTypeId is specified and has a base type of type Document. + * Default - false + */ + enableFileSelection?: boolean; + + /** + * Specify the supported file extensions for the file picker. Default - "docx", "doc", "pptx", "ppt", "xlsx", "xls", "pdf" + * Only used when enableFileSelection is true + */ + supportedFileExtensions?: string[]; } diff --git a/src/controls/dynamicForm/IDynamicFormState.ts b/src/controls/dynamicForm/IDynamicFormState.ts index 2f3de2198..5818690dc 100644 --- a/src/controls/dynamicForm/IDynamicFormState.ts +++ b/src/controls/dynamicForm/IDynamicFormState.ts @@ -1,13 +1,37 @@ - import { IInstalledLanguageInfo } from '@pnp/sp/regional-settings'; +import { ISPField } from '../../common/SPEntities'; +import { MessageBarType } from '@fluentui/react/lib/MessageBar'; +import { ICustomFormattingBodySection, ICustomFormattingNode } from '../../common/utilities/ICustomFormatting'; import { IDynamicFieldProps } from './dynamicField/IDynamicFieldProps'; +import { IFilePickerResult } from "../filePicker"; + export interface IDynamicFormState { + infoErrorMessages: { + type: MessageBarType; + message: string; + }[]; + /** Form and List Item data */ fieldCollection: IDynamicFieldProps[]; installedLanguages?: IInstalledLanguageInfo[]; + /** Validation Formulas set in List Column settings */ + validationFormulas: Record>; + /** Field Show / Hide Validation Formulas, set in Edit Form > Edit Columns > Edit Conditional Formula */ + clientValidationFormulas: Record>; + /** Tracks fields hidden by ClientValidationFormula */ + hiddenByFormula: string[]; + /** Populated by evaluation of List Column Setting validation. Key is internal field name, value is the configured error message. */ + validationErrors: Record; + customFormatting?: { + header: ICustomFormattingNode; + body: ICustomFormattingBodySection[]; + footer: ICustomFormattingNode; + } + headerContent?: JSX.Element; + footerContent?: JSX.Element; isSaving?: boolean; etag?: string; isValidationErrorDialogOpen: boolean; + selectedFile?: IFilePickerResult; + missingSelectedFile?: boolean; + contentTypeId?: string; } - - - diff --git a/src/controls/dynamicForm/dynamicField/DynamicField.tsx b/src/controls/dynamicForm/dynamicField/DynamicField.tsx index b8641731a..dc4deb6f4 100644 --- a/src/controls/dynamicForm/dynamicField/DynamicField.tsx +++ b/src/controls/dynamicForm/dynamicField/DynamicField.tsx @@ -22,10 +22,8 @@ import { IPickerTerms, TaxonomyPicker } from '../../taxonomyPicker'; import styles from '../DynamicForm.module.scss'; import { IDynamicFieldProps } from './IDynamicFieldProps'; import { IDynamicFieldState } from './IDynamicFieldState'; -import { isArray } from 'lodash'; import CurrencyMap from "../CurrencyMap"; - export class DynamicField extends React.Component { constructor(props: IDynamicFieldProps) { @@ -34,12 +32,12 @@ export class DynamicField extends React.Component{labelText}; - const errorText = this.getRequiredErrorText(); + const labelEl = ; + const errorText = this.props.validationErrorMessage || this.getRequiredErrorText(); const errorTextEl = {errorText}; const descriptionEl = {description}; const hasImage = !!changedValue; + const valueToDisplay = newValue !== undefined ? newValue : value; + switch (fieldType) { case 'loading': return { this.onChange(newText); }} @@ -131,7 +131,7 @@ export class DynamicField extends React.Component
@@ -139,7 +139,7 @@ export class DynamicField extends React.Component { this.onChange(newText); return newText; }} isEditMode={!disabled} /> @@ -155,6 +155,7 @@ export class DynamicField extends React.Component { this.onChange(option); }} + defaultSelectedKey={valueToDisplay ? undefined : defaultValue} + selectedKey={typeof valueToDisplay === "object" ? valueToDisplay?.key : valueToDisplay} + onChange={(e, option) => { this.onChange(option, true); }} onBlur={this.onBlur} errorMessage={errorText} /> {descriptionEl} @@ -190,7 +192,8 @@ export class DynamicField extends React.Component { this.onChange(newValue); }} - defaultValue={defaultValue} + onChange={(newValue) => { this.onChange(newValue, true); }} + defaultValue={valueToDisplay !== undefined ? valueToDisplay : defaultValue} errorMessage={errorText} /> {descriptionEl} @@ -217,7 +220,7 @@ export class DynamicField extends React.Component
@@ -232,7 +235,7 @@ export class DynamicField extends React.Component { this.onChange(newValue); }} + onSelectedItem={(newValue) => { this.onChange(newValue, true); }} context={context} /> {descriptionEl} @@ -241,7 +244,7 @@ export class DynamicField extends React.Component
@@ -256,7 +259,7 @@ export class DynamicField extends React.Component { this.onChange(newValue); }} + onSelectedItem={(newValue) => { this.onChange(newValue, true); }} context={context} /> {descriptionEl} @@ -273,14 +276,15 @@ export class DynamicField extends React.Component { this.onChange(newText); }} disabled={disabled} onBlur={this.onBlur} - errorMessage={customNumberErrorMessage} - min={minimumValue} + errorMessage={errorText || customNumberErrorMessage} + min={minimumValue} max={maximumValue} /> {descriptionEl}
; @@ -295,14 +299,15 @@ export class DynamicField extends React.Component { this.onChange(newText); }} disabled={disabled} onBlur={this.onBlur} - errorMessage={customNumberErrorMessage} - min={minimumValue} + errorMessage={errorText || customNumberErrorMessage} + min={minimumValue} max={maximumValue} /> {descriptionEl}
; @@ -319,8 +324,8 @@ export class DynamicField extends React.Component { return date.toLocaleDateString(context.pageContext.cultureInfo.currentCultureName); }} - value={(changedValue !== null && changedValue !== "") ? changedValue : defaultValue} - onSelectDate={(newDate) => { this.onChange(newDate); }} + value={valueToDisplay !== undefined ? valueToDisplay : defaultValue} + onSelectDate={(newDate) => { this.onChange(newDate, true); }} disabled={disabled} firstDayOfWeek={firstDayOfWeek} />} @@ -330,8 +335,8 @@ export class DynamicField extends React.Component { return date.toLocaleDateString(context.pageContext.cultureInfo.currentCultureName); }} - value={(changedValue !== null && changedValue !== "") ? changedValue : defaultValue} - onChange={(newDate) => { this.onChange(newDate); }} + value={valueToDisplay !== undefined ? valueToDisplay : defaultValue} + onChange={(newDate) => { this.onChange(newDate, true); }} disabled={disabled} firstDayOfWeek={firstDayOfWeek} /> @@ -349,16 +354,18 @@ export class DynamicField extends React.Component { this.onChange(checkedvalue); }} + onChange={(e, checkedvalue) => { this.onChange(checkedvalue, true); }} disabled={disabled} /> {descriptionEl} {errorTextEl}
; - case 'User': + case 'User': { + const userValue = Boolean(changedValue) ? changedValue.map(cv => cv.secondaryText) : (value ? value : defaultValue); return
@@ -366,7 +373,7 @@ export class DynamicField extends React.Component { this.onChange(items); }} + onChange={(items) => { this.onChange(items, true); }} disabled={disabled} /> {descriptionEl} {errorTextEl}
; + } case 'UserMulti': return
@@ -389,7 +397,7 @@ export class DynamicField extends React.Component { this.onChange(items); }} + onChange={(items) => { this.onChange(items, true); }} disabled={disabled} /> {descriptionEl} @@ -414,13 +422,16 @@ export class DynamicField extends React.Component { this.onURLChange(newText, true); }} disabled={disabled} - onBlur={this.onBlur} /> + onBlur={this.onBlur} + /> { this.onURLChange(newText, false); }} @@ -487,14 +498,14 @@ export class DynamicField extends React.Component { this.onChange(newValue); }} + onChange={(newValue?: IPickerTerms) => { this.onChange(newValue, true); }} isTermSetSelectable={false} />
@@ -512,14 +523,14 @@ export class DynamicField extends React.Component { this.onChange(newValue); }} + onChange={(newValue?: IPickerTerms) => { this.onChange(newValue, true); }} isTermSetSelectable={false} />
{descriptionEl} @@ -548,12 +559,12 @@ export class DynamicField extends React.Component { const { - fieldDefaultValue, + defaultValue, onChanged, columnInternalName } = this.props; - let currValue = this.state.changedValue || fieldDefaultValue || { + let currValue = this.state.changedValue || defaultValue || { Url: '', Description: '' }; @@ -573,18 +584,18 @@ export class DynamicField extends React.Component { // eslint-disable-line @typescript-eslint/no-explicit-any + private onChange = (value: any, callValidation = false): void => { // eslint-disable-line @typescript-eslint/no-explicit-any const { onChanged, columnInternalName } = this.props; if (onChanged) { - onChanged(columnInternalName, value); + onChanged(columnInternalName, value, callValidation); } this.setState({ changedValue: value @@ -592,9 +603,10 @@ export class DynamicField extends React.Component { - if (this.state.changedValue === null && this.props.fieldDefaultValue === "") { + if (this.state.changedValue === null && this.props.defaultValue === "") { this.setState({ changedValue: "" }); } + this.props.onChanged(this.props.columnInternalName, this.state.changedValue, true); } private getRequiredErrorText = (): string => { @@ -666,15 +678,17 @@ export class DynamicField extends React.Component { + value.forEach(element => { selectedItemArr.push(element); }); } else { - selectedItemArr = !changedValue ? [] : isArray(changedValue) ? changedValue : [ changedValue ]; + // selectedItemArr = this.props.value; + selectedItemArr = !changedValue ? [] : + ( Array.isArray(changedValue) ? [ ...changedValue ] : [ changedValue] ); } if (item.selected) { @@ -688,7 +702,7 @@ export class DynamicField extends React.Component void; // eslint-disable-line @typescript-eslint/no-explicit-any - value?: any; // eslint-disable-line @typescript-eslint/no-explicit-any + + /** Specifies if a field should be filled in order to pass validation */ required: boolean; + + /** Specifies if a field should be disabled */ + disabled?: boolean; + + /** List Item Id, passed to various utility/helper functions to determine things like selected User UPN, Lookup text, Term labels etc. */ + listItemId?: number; + + /** The default value of the field. */ + defaultValue: any; // eslint-disable-line @typescript-eslint/no-explicit-any + + /** Holds a field value. Set on all fields in the form. */ + value?: any; // eslint-disable-line @typescript-eslint/no-explicit-any + + /** Fired by DynamicField when a field value is changed */ + onChanged?: (columnInternalName: string, newValue: any, validate: boolean, additionalData?: FieldChangeAdditionalData) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + + /** Represents the value of the field as updated by the user. Only updated by fields when changed. */ newValue?: any; // eslint-disable-line @typescript-eslint/no-explicit-any - fieldType: string; - fieldTitle: string; - fieldDefaultValue: any; // eslint-disable-line @typescript-eslint/no-explicit-any - options?: IDropdownOption[]; + + /** Represents a stringified value of the field. Used in custom formatting and validation. */ + stringValue: any; // eslint-disable-line @typescript-eslint/no-explicit-any + + /** Holds additional properties that can be queried in validation. For example a Person column may be reference by both [$Person] and [$Person.email] */ + subPropertyValues?: Record; // eslint-disable-line @typescript-eslint/no-explicit-any + + /** If validation raises an error message, it can be stored against the field here for display by DynamicField */ + validationErrorMessage?: string; + + /** Field Term Set ID, used in Taxonomy / Metadata fields */ fieldTermSetId?: string; + + /** Field Anchor ID, used in Taxonomy / Metadata fields */ fieldAnchorId?: string; + + /** Lookup List ID, used in Lookup and User fields */ lookupListID?: string; + + /** Lookup Field. Represents the field used for Lookup values. */ lookupField?: string; - changedValue: any; // eslint-disable-line @typescript-eslint/no-explicit-any + + // changedValue: any; // eslint-disable-line @typescript-eslint/no-explicit-any + + /** Equivalent to HiddenListInternalName, used for Taxonomy Metadata fields */ hiddenFieldName?: string; + + /** Order of the field in the form */ Order: number; + + /** Used for files / image uploads */ + additionalData?: FieldChangeAdditionalData; + + // Related to various field types + options?: IDropdownOption[]; isRichText?: boolean; dateFormat?: DateFormat; firstDayOfWeek: number; - additionalData?: FieldChangeAdditionalData; principalType?: string; description?: string; maximumValue?: number; diff --git a/src/controls/fieldCollectionData/collectionDataItem/CollectionDataItem.tsx b/src/controls/fieldCollectionData/collectionDataItem/CollectionDataItem.tsx index d29fdaf51..ba4bd76d6 100644 --- a/src/controls/fieldCollectionData/collectionDataItem/CollectionDataItem.tsx +++ b/src/controls/fieldCollectionData/collectionDataItem/CollectionDataItem.tsx @@ -57,7 +57,7 @@ export class CollectionDataItem extends React.Component { // eslint-disable-line @typescript-eslint/no-explicit-any - + this.setState((prevState: ICollectionDataItemState): ICollectionDataItemState => { const { crntItem } = prevState; // Update the changed field @@ -93,7 +93,7 @@ export class CollectionDataItem extends React.Component { this.onValueChanged(field.id, date) }} formatDate={(date) => { return date ? date?.toLocaleDateString() : ""; }} - />; + />; case CustomCollectionFieldType.custom: if (field.onCustomRender) { return field.onCustomRender(field, item[field.id], this.onValueChanged, item, item.uniqueId, this.onCustomFieldValidation); @@ -537,7 +537,7 @@ export class CollectionDataItem extends React.Component value.key === item[field.id][i].key) === "undefined" ) { + if (typeof _comboBoxOptions.find(value => value.key === item[field.id][i].key) === "undefined") { _comboBoxOptions.push(item[field.id][i]); } } @@ -548,7 +548,7 @@ export class CollectionDataItem extends React.Component value.key === item[field.id].key) === "undefined" ) { + if (typeof _comboBoxOptions.find(value => value.key === item[field.id].key) === "undefined") { _comboBoxOptions.push(item[field.id]); } @@ -563,33 +563,33 @@ export class CollectionDataItem extends React.Component{ + onChange={async (event, option, index, value) => { if (field.multiSelect) { - this.onValueChangedComboBoxMulti(field.id, option, value); + this.onValueChangedComboBoxMulti(field.id, option, value) } else { - this.onValueChangedComboBoxSingle(field.id, option, value); + this.onValueChangedComboBoxSingle(field.id, option, value) } - } } - + }} />; + case CustomCollectionFieldType.peoplepicker: - _selectedUsers = item[field.id] !== null ? item[field.id]: [] ; + _selectedUsers = item[field.id] !== null ? item[field.id] : []; return { - const _selected: string[] = items.length === 0 ? null : items.map(({secondaryText}) => secondaryText); - this.onValueChanged(field.id, _selected) - } + const _selected: string[] = items.length === 0 ? null : items.map(({ secondaryText }) => secondaryText); + this.onValueChanged(field.id, _selected) + } } onGetErrorMessage={async (items: IPersonaProps[]) => await this.peoplepickerValidation(field, items, item)} @@ -604,7 +604,7 @@ export class CollectionDataItem extends React.Component this.onValueChanged(field.id, value)} deferredValidationTime={field.deferredValidationTime || field.deferredValidationTime >= 0 ? field.deferredValidationTime : 200} - onGetErrorMessage={async (value: string) => await this.fieldValidation(field, value) } + onGetErrorMessage={async (value: string) => await this.fieldValidation(field, value)} inputClassName="PropertyFieldCollectionData__panel__string-field" />; } } @@ -716,10 +716,10 @@ export class CollectionDataItem extends React.Component ) : ( - - - - ) + + + + ) }
diff --git a/src/controls/peoplepicker/IPeoplePicker.ts b/src/controls/peoplepicker/IPeoplePicker.ts index 52ef61d68..2eeb091db 100644 --- a/src/controls/peoplepicker/IPeoplePicker.ts +++ b/src/controls/peoplepicker/IPeoplePicker.ts @@ -15,7 +15,7 @@ export interface IPeoplePickerProps { context: BaseComponentContext; /** * Text of the Control - */ + */ titleText?: string; /** * Web Absolute Url of source site. When this is provided, a search request is done to the local site. @@ -32,7 +32,7 @@ export interface IPeoplePickerProps { /** * Id of SharePoint Group (Number) or Office365 Group (String) */ - groupId?: number | string | (string|number)[]; + groupId?: number | string | (string | number)[]; /** * Maximum number of suggestions to show in the full suggestion list. (default: 5) */ @@ -79,7 +79,7 @@ export interface IPeoplePickerProps { /** * Prop to validate contents on blur */ - validateOnFocusOut?: boolean; + validateOnFocusOut?: boolean; /** * Method to check value of People Picker text */ @@ -93,8 +93,8 @@ export interface IPeoplePickerProps { */ tooltipDirectional?: DirectionalHint; /** - * Class Name for the whole People picker control - */ + * Class Name for the whole People picker control + */ peoplePickerWPclassName?: string; /** * Class Name for the People picker control @@ -129,10 +129,14 @@ export interface IPeoplePickerProps { * Placeholder to be displayed in an empty term picker */ placeholder?: string; - /** + /** * styles to apply on control */ - styles?: Partial; + styles?: Partial; + /** + * Define a filter to be applied to the search results, such as a filter to only show users from a specific domain + */ + resultFilter?: (result: IPersonaProps[]) => IPersonaProps[]; } export interface IPeoplePickerState { diff --git a/src/controls/peoplepicker/PeoplePickerComponent.tsx b/src/controls/peoplepicker/PeoplePickerComponent.tsx index ba7c675df..2fa52a8dd 100644 --- a/src/controls/peoplepicker/PeoplePickerComponent.tsx +++ b/src/controls/peoplepicker/PeoplePickerComponent.tsx @@ -142,7 +142,13 @@ export class PeoplePicker extends React.Component 0) { + filteredPersons = this.props.resultFilter(filteredPersons); + } + // Add the users to the most recently used ones let recentlyUsed = [...filteredPersons, ...mostRecentlyUsedPersons]; recentlyUsed = uniqBy(recentlyUsed, "text"); diff --git a/src/controls/richText/RichText.module.scss b/src/controls/richText/RichText.module.scss index e14d9dc47..748839ea9 100644 --- a/src/controls/richText/RichText.module.scss +++ b/src/controls/richText/RichText.module.scss @@ -65,24 +65,28 @@ .toolbarDropDownOption { &.toolbarButtonH2 { - font-size: 21px; - font-weight: 100; + font-size: 20px; + font-weight: 600; } &.toolbarButtonH3 { - font-size: 14px; - font-weight: 100 !important; + font-size: 18px; + font-weight: 600; } &.toolbarButtonH4 { - font-size: 14px; - font-weight: 100 !important; + font-size: 16px; + font-weight: 600; } &.toolbarButtonBlockQuote { - font-size: 14px; + font-size: 16px; font-style: italic; } + + &.toolbarButtonNormal { + font-size: 16px; + } } .toolbarSubmenuCaret { @@ -179,7 +183,7 @@ .ql-snow .ql-toolbar button { background-color: "[theme:neutralPrimary, default:#{$ms-color-neutralPrimary}]"; color: "[theme:neutralLighterAlt, default:#{$ms-color-neutralLighterAlt}]"; - font-size: 14px; + font-size: 16px; min-width: 34px; height: 34px; padding-top: 4px; @@ -250,21 +254,21 @@ min-height: 68px; font-family: "Segoe UI Web (West European)", Segoe UI, -apple-system, BlinkMacSystemFont, Roboto, Helvetica Neue, sans-serif; - font-size: 17px; + font-size: 18px; h2 { - font-weight: 100 !important; + font-weight: 600 !important; font-size: 28px; } h3 { font-size: 24px; - font-weight: 100 !important; + font-weight: 600 !important; } h4 { - font-size: 21px; - font-weight: 100 !important; + font-size: 20px; + font-weight: 600 !important; } blockquote, @@ -276,14 +280,13 @@ p, ul { -webkit-font-smoothing: antialiased; - color: "[theme:black, default:#{$ms-color-black}]"; line-height: 1.3; // margin: 0 0 16px; word-wrap: break-word; } blockquote { - border-bottom-color: "[theme:neutralTertiaryAlt, default:#{$ms-color-neutralTertiaryAlt}]"; + border-bottom-color: "[theme:neutralLighter, default:#{$ms-color-neutralLighter}]"; border-bottom-style: solid; border-bottom-width: 1px; border-left-style: none; @@ -292,14 +295,13 @@ border-right-style: none; border-right-width: 0; border-right-color: transparent; - border-top-color: "[theme:neutralTertiaryAlt, default:#{$ms-color-neutralTertiaryAlt}]"; + border-top-color: "[theme:neutralLighter, default:#{$ms-color-neutralLighter}]"; border-top-style: solid; border-top-width: 1px; - color: "[theme:neutralSecondaryAlt, default:#{$ms-color-neutralSecondaryAlt}]"; - font-size: 24px; + font-size: 20px; font-style: italic; - font-weight: 100; - line-height: 31.2px; + font-weight: 600; + line-height: 1.3; margin-bottom: 28px; padding-bottom: 32px; margin-top: 28px; @@ -311,6 +313,10 @@ text-align: center; } + .ql-size-xsmall { + font-size: 10px; + } + .ql-size-small { font-size: 12px; } @@ -320,15 +326,15 @@ } .ql-size-mediumplus { - font-size: 15px; + font-size: 16px; } .ql-size-large { - font-size: 17px; + font-size: 18px; } .ql-size-xlarge { - font-size: 21px; + font-size: 20px; } .ql-size-xlargeplus { @@ -350,6 +356,10 @@ .ql-size-super { font-size: 42px; } + + .ql-size-superlarge { + font-size: 68px; + } } @media screen and (min-width: 1024px) { @@ -357,8 +367,8 @@ .ql-editor ol, .ql-editor p, .ql-editor ul { - font-size: 17px; - font-weight: 300; + font-size: 18px; + font-weight: 400; line-height: 1.3; } } @@ -375,8 +385,8 @@ .ql-editor.ql-blank::before { font-style: normal; color: "[theme:neutralTertiary, default:#{$ms-color-neutralTertiary}]"; - font-size: 17px; - font-weight: 300; + font-size: 18px; + font-weight: 400; line-height: 1.3; } diff --git a/src/controls/richText/RichText.tsx b/src/controls/richText/RichText.tsx index 1ccbfd690..4a743b535 100644 --- a/src/controls/richText/RichText.tsx +++ b/src/controls/richText/RichText.tsx @@ -41,10 +41,6 @@ export class RichText extends React.Component { private _richTextId = undefined; private ddStyleOpts = [{ - key: 0, - text: strings.HeaderNormalText, - data: {} - }, { key: 2, text: strings.HeaderH2, data: @@ -59,6 +55,10 @@ export class RichText extends React.Component { text: strings.HeaderH4, data: { className: styles.toolbarButtonH4 } + }, { + key: 0, + text: strings.HeaderNormalText, + data: { className: styles.toolbarButtonNormal } }, { key: 7, text: strings.HeaderBlockQuote, @@ -511,7 +511,8 @@ export class RichText extends React.Component { 'xxlarge', 'xxxlarge', 'xxlargeplus', - 'super']; + 'super', + 'superlarge']; ReactQuillInstance.register(sizeClass, true); return ( diff --git a/src/controls/richText/RichText.types.ts b/src/controls/richText/RichText.types.ts index bc89355f7..8a908d416 100644 --- a/src/controls/richText/RichText.types.ts +++ b/src/controls/richText/RichText.types.ts @@ -105,7 +105,7 @@ export interface StyleOptions { showImage?: boolean; /** - * Indicates if we should show the Styles button (Heading 1, Heading 2, ..., Pull quote) + * Indicates if we should show the Styles button (Heading 2, Heading 3, ..., Pull quote) * @defaultvalue true */ showStyles?: boolean; diff --git a/src/controls/richText/RichTextPropertyPane.tsx b/src/controls/richText/RichTextPropertyPane.tsx index c814d7194..24a251b75 100644 --- a/src/controls/richText/RichTextPropertyPane.tsx +++ b/src/controls/richText/RichTextPropertyPane.tsx @@ -190,16 +190,18 @@ export default class RichTextPropertyPane extends React.Component diff --git a/src/controls/webPartTitle/WebPartTitle.tsx b/src/controls/webPartTitle/WebPartTitle.tsx index 5a30431d7..b4b57d0ae 100644 --- a/src/controls/webPartTitle/WebPartTitle.tsx +++ b/src/controls/webPartTitle/WebPartTitle.tsx @@ -54,7 +54,7 @@ export class WebPartTitle extends React.Component {
{ this.props.displayMode === DisplayMode.Edit && ( -