From c410c973ba406460152d1cf645bc78ef4e16f506 Mon Sep 17 00:00:00 2001 From: Peter Paul Kirschner Date: Mon, 3 Jun 2024 12:55:19 +0200 Subject: [PATCH] make working under node 18 --- samples/react-kanban-board/.eslintrc.js | 2 +- samples/react-kanban-board/.nvmrc | 1 + .../.vscode/extensions.json | 5 - .../react-kanban-board/.vscode/launch.json | 4 - samples/react-kanban-board/.yo-rc.json | 2 + samples/react-kanban-board/README.md | 2 +- samples/react-kanban-board/assets/sample.json | 8 +- .../config/package-solution.json | 7 +- samples/react-kanban-board/package-lock.json | 391 ++++++++++-------- .../src/kanban/KanbanBucketConfigurator.tsx | 15 +- .../src/kanban/KanbanTask.tsx | 1 + .../src/kanban/KanbanTaskManagedProp.tsx | 4 +- .../kanbanBoard/KanbanBoardWebPart.ts | 29 +- .../FieldErrorMessage.module.scss | 14 + .../PropertyListPicker/FieldErrorMessage.tsx | 27 ++ .../IPropertyFieldListMultiPickerHost.ts | 22 + .../IPropertyFieldListPicker.ts | 152 +++++++ .../IPropertyFieldListPickerHost.ts | 49 +++ .../PropertyFieldListPicker.ts | 160 +++++++ .../PropertyFieldListPickerHost.tsx | 251 +++++++++++ .../PropertyListPicker/SPListPickerService.ts | 118 ++++++ .../components/PropertyListPicker/index.ts | 4 + .../PropertyOrderField/IPropertyFieldOrder.ts | 77 ++++ .../IPropertyFieldOrderHost.ts | 23 ++ .../PropertyOrderField/PropertyFieldOrder.ts | 94 +++++ .../PropertyFieldOrderHost.module.scss | 65 +++ .../PropertyFieldOrderHost.tsx | 263 ++++++++++++ .../components/PropertyOrderField/helper.ts | 12 + 28 files changed, 1591 insertions(+), 211 deletions(-) create mode 100644 samples/react-kanban-board/.nvmrc delete mode 100644 samples/react-kanban-board/.vscode/extensions.json create mode 100644 samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/FieldErrorMessage.module.scss create mode 100644 samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/FieldErrorMessage.tsx create mode 100644 samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/IPropertyFieldListMultiPickerHost.ts create mode 100644 samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/IPropertyFieldListPicker.ts create mode 100644 samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/IPropertyFieldListPickerHost.ts create mode 100644 samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/PropertyFieldListPicker.ts create mode 100644 samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/PropertyFieldListPickerHost.tsx create mode 100644 samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/SPListPickerService.ts create mode 100644 samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/index.ts create mode 100644 samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/IPropertyFieldOrder.ts create mode 100644 samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/IPropertyFieldOrderHost.ts create mode 100644 samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/PropertyFieldOrder.ts create mode 100644 samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/PropertyFieldOrderHost.module.scss create mode 100644 samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/PropertyFieldOrderHost.tsx create mode 100644 samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/helper.ts diff --git a/samples/react-kanban-board/.eslintrc.js b/samples/react-kanban-board/.eslintrc.js index 473df80cd..4737121c7 100644 --- a/samples/react-kanban-board/.eslintrc.js +++ b/samples/react-kanban-board/.eslintrc.js @@ -79,7 +79,7 @@ module.exports = { // This rule should be suppressed only in very special cases such as JSON.stringify() // where the type really can be anything. Even if the type is flexible, another type // may be more appropriate such as "unknown", "{}", or "Record". - '@typescript-eslint/no-explicit-any': 1, + '@typescript-eslint/no-explicit-any': 0, // RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch() // handler. Thus wherever a Promise arises, the code must either append a catch handler, // or else return the object to a caller (who assumes this responsibility). Unterminated diff --git a/samples/react-kanban-board/.nvmrc b/samples/react-kanban-board/.nvmrc new file mode 100644 index 000000000..860cc5000 --- /dev/null +++ b/samples/react-kanban-board/.nvmrc @@ -0,0 +1 @@ +v18.17.1 diff --git a/samples/react-kanban-board/.vscode/extensions.json b/samples/react-kanban-board/.vscode/extensions.json deleted file mode 100644 index 97af1643c..000000000 --- a/samples/react-kanban-board/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "msjsdiag.debugger-for-chrome" - ] -} \ No newline at end of file diff --git a/samples/react-kanban-board/.vscode/launch.json b/samples/react-kanban-board/.vscode/launch.json index f54f01185..53ca22cd8 100644 --- a/samples/react-kanban-board/.vscode/launch.json +++ b/samples/react-kanban-board/.vscode/launch.json @@ -1,8 +1,4 @@ { - /** - * Install Chrome Debugger Extension for Visual Studio Code to debug your components with the - * Chrome browser: https://aka.ms/spfx-debugger-extensions - */ "version": "0.2.0", "configurations": [ { diff --git a/samples/react-kanban-board/.yo-rc.json b/samples/react-kanban-board/.yo-rc.json index bc1a6a2f0..d3b44080e 100644 --- a/samples/react-kanban-board/.yo-rc.json +++ b/samples/react-kanban-board/.yo-rc.json @@ -7,7 +7,9 @@ "libraryName": "react-kanban-board", "libraryId": "cccbd72b-7b89-4128-9348-0a4850ded8fd", "packageManager": "npm", + "skipFeatureDeployment": true, "isDomainIsolated": false, + "plusBeta": false, "componentType": "webpart", "sdkVersions": { "@microsoft/teams-js": "2.12.0", diff --git a/samples/react-kanban-board/README.md b/samples/react-kanban-board/README.md index 1a9c814cf..4c4d598f3 100644 --- a/samples/react-kanban-board/README.md +++ b/samples/react-kanban-board/README.md @@ -74,7 +74,7 @@ Version|Date|Comments 1.0.1.0|April 21, 2020|Added support for Teams hosts 2.0.0.0|July 10, 2020| jqwidgets replaced with a custom Kanban Board based on Office UI Component and IE11 Support 3.0.0.0|October 29, 2021| SPFx 1.13, PnPJS v2, PnP Controls v3 -4.0.0.0|Jun 1, 2024| SPFx 1.19, PnPJS v4, Node 18 +4.0.0.0|Jun 1, 2024| SPFx 1.19, PnPJS v4, Node 18 (Property-ListPicker and Property-Order not used from @pnp/spfx-property-controls because of an issue ) [Read More about the implementation of this Board](./src/kanban/README.md) diff --git a/samples/react-kanban-board/assets/sample.json b/samples/react-kanban-board/assets/sample.json index 8d6706cbb..b5d4d83e7 100644 --- a/samples/react-kanban-board/assets/sample.json +++ b/samples/react-kanban-board/assets/sample.json @@ -6,10 +6,10 @@ "shortDescription": "This solution contains an SPFx web part which shows a Kanban board using jqxKanban ReactJS component (from JQWidgets). The web part uses the default columns of the SharePoint Tasks list for showing the board\u0027s columns and the tasks.", "url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-kanban-board", "longDescription": [ - "This solution contains an SPFx web part which shows a Kanban board using jqxKanban ReactJS component (from JQWidgets). The web part uses the default columns of the SharePoint Tasks list for showing the board\u0027s columns and the tasks." + "This solution contains an SPFx web part which shows a Kanban board. The web part uses the default columns of the SharePoint Tasks list for showing the board\u0027s columns and the tasks." ], "creationDateTime": "2020-07-02", - "updateDateTime": "2020-07-02", + "updateDateTime": "2024-05-26", "products": [ "SharePoint" ], @@ -20,7 +20,7 @@ }, { "key": "SPFX-VERSION", - "value": "1.13.0" + "value": "1.19.0" }, { "key": "SPFX-TEAMSTAB", @@ -56,7 +56,7 @@ }, { "gitHubAccount": "petkir", - "company": "Cubido Business Solutions GmbH", + "company": "ACP CUBIDO Digital Solutions GmbH", "pictureUrl": "https://github.com/petkir.png", "name": "Peter Paul Kirschner", "twitter": "petkir_at" diff --git a/samples/react-kanban-board/config/package-solution.json b/samples/react-kanban-board/config/package-solution.json index 34c1edfb1..88a78594d 100644 --- a/samples/react-kanban-board/config/package-solution.json +++ b/samples/react-kanban-board/config/package-solution.json @@ -12,7 +12,7 @@ "privacyUrl": "", "termsOfUseUrl": "", "websiteUrl": "", - "mpnId": "" + "mpnId": "Undefined-1.19.0" }, "metadata": { "shortDescription": { @@ -30,10 +30,7 @@ "title": "react-kanban-board KanbanBoardWebPart Feature", "description": "The feature that activates KanbanBoardWebPart from the react-kanban-board solution.", "id": "67cd5938-806b-4c79-b589-501f7f26998e", - "version": "3.0.0.0", - "componentIds": [ - "67cd5938-806b-4c79-b589-501f7f26998e" - ] + "version": "4.0.0.0" } ] diff --git a/samples/react-kanban-board/package-lock.json b/samples/react-kanban-board/package-lock.json index 5f646d780..4ee0a789d 100644 --- a/samples/react-kanban-board/package-lock.json +++ b/samples/react-kanban-board/package-lock.json @@ -8389,6 +8389,23 @@ "node": ">=8.0.0" } }, + "node_modules/@pnp/common": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@pnp/common/-/common-2.5.0.tgz", + "integrity": "sha512-ea4zTNC3sjLolrHZXP+/2SrJM+yC8PygmPW/yRfgbErdvdwYMUSogT69dW+NUaqhkfYZfkkAoWn42irlLMSpdw==", + "dependencies": { + "tslib": "2.2.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/patrick-rodgers/" + } + }, + "node_modules/@pnp/common/node_modules/tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + }, "node_modules/@pnp/core": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@pnp/core/-/core-4.1.0.tgz", @@ -8409,6 +8426,42 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/@pnp/logging": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@pnp/logging/-/logging-2.5.0.tgz", + "integrity": "sha512-SnmMCN6oADjiHKAIR23FfTqXeQZeXPBnWeVfyZAgzJfRn9uEQoUlkyET3jHjl9kkrFOVkiOD1CRI7TWMIxURbA==", + "dependencies": { + "tslib": "2.2.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/patrick-rodgers/" + } + }, + "node_modules/@pnp/logging/node_modules/tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + }, + "node_modules/@pnp/odata": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@pnp/odata/-/odata-2.5.0.tgz", + "integrity": "sha512-AeP01jDvnkiUVn7V+4FT07chz+G/yzrJDH0Gk+qzujJ393ZO6FwJpJEiOCRh9cxF48gqSj/f7r/IIyDHe0+IpQ==", + "dependencies": { + "@pnp/common": "2.5.0", + "@pnp/logging": "2.5.0", + "tslib": "2.2.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/patrick-rodgers/" + } + }, + "node_modules/@pnp/odata/node_modules/tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + }, "node_modules/@pnp/queryable": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@pnp/queryable/-/queryable-4.1.0.tgz", @@ -9016,59 +9069,6 @@ "react-dom": ">=16.8.0 <19.0.0" } }, - "node_modules/@pnp/spfx-controls-react/node_modules/@pnp/common": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@pnp/common/-/common-2.5.0.tgz", - "integrity": "sha512-ea4zTNC3sjLolrHZXP+/2SrJM+yC8PygmPW/yRfgbErdvdwYMUSogT69dW+NUaqhkfYZfkkAoWn42irlLMSpdw==", - "dependencies": { - "tslib": "2.2.0" - }, - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/patrick-rodgers/" - } - }, - "node_modules/@pnp/spfx-controls-react/node_modules/@pnp/common/node_modules/tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - }, - "node_modules/@pnp/spfx-controls-react/node_modules/@pnp/logging": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@pnp/logging/-/logging-2.5.0.tgz", - "integrity": "sha512-SnmMCN6oADjiHKAIR23FfTqXeQZeXPBnWeVfyZAgzJfRn9uEQoUlkyET3jHjl9kkrFOVkiOD1CRI7TWMIxURbA==", - "dependencies": { - "tslib": "2.2.0" - }, - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/patrick-rodgers/" - } - }, - "node_modules/@pnp/spfx-controls-react/node_modules/@pnp/logging/node_modules/tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - }, - "node_modules/@pnp/spfx-controls-react/node_modules/@pnp/odata": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@pnp/odata/-/odata-2.5.0.tgz", - "integrity": "sha512-AeP01jDvnkiUVn7V+4FT07chz+G/yzrJDH0Gk+qzujJ393ZO6FwJpJEiOCRh9cxF48gqSj/f7r/IIyDHe0+IpQ==", - "dependencies": { - "@pnp/common": "2.5.0", - "@pnp/logging": "2.5.0", - "tslib": "2.2.0" - }, - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/patrick-rodgers/" - } - }, - "node_modules/@pnp/spfx-controls-react/node_modules/@pnp/odata/node_modules/tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - }, "node_modules/@pnp/spfx-controls-react/node_modules/@pnp/sp": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@pnp/sp/-/sp-2.5.0.tgz", @@ -10261,6 +10261,67 @@ "sudo": "~1.0.3" } }, + "node_modules/@rushstack/debug-certificate-manager/node_modules/@rushstack/node-core-library": { + "version": "3.53.2", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.53.2.tgz", + "integrity": "sha512-FggLe5DQs0X9MNFeJN3/EXwb+8hyZUTEp2i+V1e8r4Va4JgkjBNY0BuEaQI+3DW6S4apV3UtXU3im17MSY00DA==", + "dev": true, + "dependencies": { + "@types/node": "12.20.24", + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.17.0", + "semver": "~7.3.0", + "z-schema": "~5.0.2" + } + }, + "node_modules/@rushstack/debug-certificate-manager/node_modules/@types/node": { + "version": "12.20.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.24.tgz", + "integrity": "sha512-yxDeaQIAJlMav7fH5AQqPH1u8YIuhYJXYBzxaQ4PifsU0GDO38MSdmEDeRlIxrKbC6NbEaaEHDanWb+y30U8SQ==", + "dev": true + }, + "node_modules/@rushstack/debug-certificate-manager/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/@rushstack/debug-certificate-manager/node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@rushstack/debug-certificate-manager/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/@rushstack/eslint-config": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@rushstack/eslint-config/-/eslint-config-2.5.1.tgz", @@ -11086,9 +11147,9 @@ } }, "node_modules/@rushstack/node-core-library": { - "version": "3.53.2", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.53.2.tgz", - "integrity": "sha512-FggLe5DQs0X9MNFeJN3/EXwb+8hyZUTEp2i+V1e8r4Va4JgkjBNY0BuEaQI+3DW6S4apV3UtXU3im17MSY00DA==", + "version": "3.53.3", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.53.3.tgz", + "integrity": "sha512-H0+T5koi5MFhJUd5ND3dI3bwLhvlABetARl78L3lWftJVQEPyzcgTStvTTRiIM5mCltyTM8VYm6BuCtNUuxD0Q==", "dev": true, "dependencies": { "@types/node": "12.20.24", @@ -13159,9 +13220,9 @@ "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==" }, "node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", "dev": true }, "node_modules/@types/minimist": { @@ -15964,9 +16025,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001625", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz", - "integrity": "sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w==", + "version": "1.0.30001626", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001626.tgz", + "integrity": "sha512-JRW7kAH8PFJzoPCJhLSHgDgKg5348hsQ68aqb+slnzuB5QFERv846oA/mRChmlLAOdEDeOkRn3ynb1gSFnjt3w==", "dev": true, "funding": [ { @@ -16725,9 +16786,9 @@ } }, "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, "node_modules/cosmiconfig": { @@ -17069,6 +17130,58 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-select/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/css-select/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/css-selector-tokenizer": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz", @@ -18018,28 +18131,18 @@ } }, "node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" }, "funding": { "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/dom7": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.6.tgz", @@ -18070,12 +18173,11 @@ } }, "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dependencies": { - "domelementtype": "^2.2.0" + "domelementtype": "^2.3.0" }, "engines": { "node": ">= 4" @@ -18085,14 +18187,13 @@ } }, "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" }, "funding": { "url": "https://github.com/fb55/domutils?sponsor=1" @@ -18287,6 +18388,15 @@ "once": "~1.3.0" } }, + "node_modules/end-of-stream/node_modules/once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/enhanced-resolve": { "version": "5.16.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz", @@ -21799,20 +21909,6 @@ "htmlparser2": "9.1.0" } }, - "node_modules/html-dom-parser/node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, "node_modules/html-encoding-sniffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", @@ -21901,20 +21997,6 @@ } } }, - "node_modules/html-react-parser/node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, "node_modules/htmlparser2": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", @@ -21933,46 +22015,6 @@ "entities": "^4.5.0" } }, - "node_modules/htmlparser2/node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/htmlparser2/node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/htmlparser2/node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -25984,6 +26026,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/multimatch/node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, "node_modules/multimatch/node_modules/array-differ": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", @@ -26999,9 +27047,9 @@ } }, "node_modules/once": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", - "integrity": "sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "dependencies": { "wrappy": "1" @@ -27136,6 +27184,15 @@ "once": "~1.3.0" } }, + "node_modules/orchestrator/node_modules/once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", @@ -32803,6 +32860,12 @@ "extsprintf": "^1.2.0" } }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, "node_modules/vinyl": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", diff --git a/samples/react-kanban-board/src/kanban/KanbanBucketConfigurator.tsx b/samples/react-kanban-board/src/kanban/KanbanBucketConfigurator.tsx index bfd19e6ee..3d2d2c6a6 100644 --- a/samples/react-kanban-board/src/kanban/KanbanBucketConfigurator.tsx +++ b/samples/react-kanban-board/src/kanban/KanbanBucketConfigurator.tsx @@ -44,20 +44,7 @@ export class KanbanBucketConfigurator extends React.Component { - /* - const columnProps: Partial = { - gap: 15, - styles: { root: { width: 300 } }, - };*/ - /* - const colorPickerStyles: Partial = { - panel: { padding: 12 }, - root: { - maxWidth: 352, - minWidth: 352, - }, - colorRectangle: { height: 268 }, - };*/ + const statebucket = this.state.bucket; if (!statebucket) { return (
); diff --git a/samples/react-kanban-board/src/kanban/KanbanTask.tsx b/samples/react-kanban-board/src/kanban/KanbanTask.tsx index d56172c2d..43b79b596 100644 --- a/samples/react-kanban-board/src/kanban/KanbanTask.tsx +++ b/samples/react-kanban-board/src/kanban/KanbanTask.tsx @@ -83,6 +83,7 @@ export default class KanbanTask extends React.Component{`${(value as any) * 100}%`} ); + return (value?{`${(value as any) * 100}%`} :); //TODO maybe better Formater break; case KanbanTaskMamagedPropertyType.html: - return ({HTMLReactParser(value)}); + return (value?{HTMLReactParser(value)}:); break; case KanbanTaskMamagedPropertyType.person: diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/KanbanBoardWebPart.ts b/samples/react-kanban-board/src/webparts/kanbanBoard/KanbanBoardWebPart.ts index 109739d9c..fa72b1c9c 100644 --- a/samples/react-kanban-board/src/webparts/kanbanBoard/KanbanBoardWebPart.ts +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/KanbanBoardWebPart.ts @@ -9,14 +9,18 @@ import { } from '@microsoft/sp-property-pane'; import { cloneDeep } from '@microsoft/sp-lodash-subset'; -import { PropertyFieldListPicker, PropertyFieldListPickerOrderBy } from '@pnp/spfx-property-controls/lib/PropertyFieldListPicker'; -import { PropertyFieldOrder } from '@pnp/spfx-property-controls/lib/PropertyFieldOrder'; +//import { PropertyFieldListPicker, PropertyFieldListPickerOrderBy } from '@pnp/spfx-property-controls'; + import * as strings from 'KanbanBoardWebPartStrings'; -import { spfi, SPFx } from "@pnp/sp"; +import { spfi, SPFx } from "@pnp/sp"; import PropertyPaneBucketConfigComponent from './components/PropertyPaneBucketConfig'; import KanbanBoardV2, { IKanbanBoardV2Props } from './components/KanbanBoardV2'; + import { bucketOrder } from './components/bucketOrder'; +import { PropertyFieldOrder } from './components/PropertyOrderField/PropertyFieldOrder'; + + import { mergeBucketsWithChoices } from './components/helper'; import { IKanbanBucket } from '../../kanban'; @@ -24,6 +28,7 @@ import { IKanbanBucket } from '../../kanban'; import { ISPKanbanService } from './services/ISPKanbanService'; import SPKanbanService from './services/SPKanbanService'; import MockKanbanService from './services/MockKanbanService'; +import { PropertyFieldListPicker, PropertyFieldListPickerOrderBy } from './components/PropertyListPicker'; export interface IKanbanBoardWebPartProps { hideWPTitle: boolean; @@ -42,9 +47,9 @@ export default class KanbanBoardWebPart extends BaseClientSideWebPart { - const sp = spfi().using(SPFx( this.context)); + const sp = spfi().using(SPFx(this.context)); //.using(PnPLogging(LogLevel.Warning)); - if ( Environment.type === EnvironmentType.Test) { + if (Environment.type === EnvironmentType.Test) { this.dataService = new MockKanbanService(); } else { this.dataService = new SPKanbanService(sp); @@ -107,7 +112,7 @@ export default class KanbanBoardWebPart extends BaseClientSideWebPart'', //TODO + onGetErrorMessage: () => '', //TODO deferredValidationTime: 0, key: 'listPickerFieldId', onListsRetrieved: (lists) => { @@ -132,14 +137,14 @@ export default class KanbanBoardWebPart extends BaseClientSideWebPart { throw new Error('Error loading Buckets by refreshBucket') }); + .catch(error => { throw new Error('Error loading Buckets by refreshBucket') }); } } diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/FieldErrorMessage.module.scss b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/FieldErrorMessage.module.scss new file mode 100644 index 000000000..ab8391f80 --- /dev/null +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/FieldErrorMessage.module.scss @@ -0,0 +1,14 @@ +.errorMessage { + font-size: 12px; + font-weight: 400; + color: #a80000; + margin: 0; + padding-top: 5px; + display: flex; + align-items: center; +} + +.errorIcon { + font-size: 14px; + margin-right: 5px; +} diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/FieldErrorMessage.tsx b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/FieldErrorMessage.tsx new file mode 100644 index 000000000..5383db071 --- /dev/null +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/FieldErrorMessage.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import styles from './FieldErrorMessage.module.scss'; +import { Icon } from '@fluentui/react/lib/Icon'; + +export interface IFieldErrorMessageProps { + errorMessage: string; +} + +/** + * Component that shows an error message when something went wront with the property control + */ +export default class FieldErrorMessage extends React.Component { + public render(): JSX.Element { + if (this.props.errorMessage !== 'undefined' && this.props.errorMessage !== null && this.props.errorMessage !== '') { + return ( +
+

+ + {this.props.errorMessage} +

+
+ ); + } else { + return
; + } + } +} diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/IPropertyFieldListMultiPickerHost.ts b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/IPropertyFieldListMultiPickerHost.ts new file mode 100644 index 000000000..d3c8b6c79 --- /dev/null +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/IPropertyFieldListMultiPickerHost.ts @@ -0,0 +1,22 @@ +import { IChoiceGroupOption } from '@fluentui/react/lib/ChoiceGroup'; +import { IPropertyFieldListPickerPropsInternal } from './IPropertyFieldListPicker'; +import { ISPLists } from './IPropertyFieldListPickerHost'; + +/** + * PropertyFieldListPickerHost properties interface + */ +export interface IPropertyFieldListMultiPickerHostProps extends IPropertyFieldListPickerPropsInternal { + + onChange: (targetProperty?: string, newValue?: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +/** + * PropertyFieldSPListMultiplePickerHost state interface + */ +export interface IPropertyFieldListMultiPickerHostState { + loadedLists: ISPLists; + results: IChoiceGroupOption[]; + selectedKeys: string[]; + loaded: boolean; + errorMessage?: string; +} diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/IPropertyFieldListPicker.ts b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/IPropertyFieldListPicker.ts new file mode 100644 index 000000000..02ad3f8c0 --- /dev/null +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/IPropertyFieldListPicker.ts @@ -0,0 +1,152 @@ +import { BaseComponentContext } from '@microsoft/sp-component-base'; +import { ISPList } from './IPropertyFieldListPickerHost'; +import { IPropertyPaneCustomFieldProps } from '@microsoft/sp-property-pane'; + +/** + * Detailed list information + */ +export interface IPropertyFieldList { + /** + * List ID + */ + id: string; + /** + * List Title + */ + title?: string; + /** + * List server relative URL + */ + url?: string; +} + +/** + * Enum for specifying how the lists should be sorted + */ +export enum PropertyFieldListPickerOrderBy { + Id = 1, + Title +} + +/** + * Public properties of the PropertyFieldListPicker custom field + */ +export interface IPropertyFieldListPickerProps { + + /** + * Property field label displayed on top + */ + label: string; + /** + * Context of the current web part + */ + context: BaseComponentContext; + /** + * Absolute Web Url of target site (user requires permissions) + */ + webAbsoluteUrl?: string; + /** + * Initial selected list set of the control + */ + selectedList?: string | string[] | IPropertyFieldList | IPropertyFieldList[]; + /** + * BaseTemplate ID(s) of the lists or libraries you want to return. + */ + baseTemplate?: number | number[]; + /** + * Specify if you want to include or exclude hidden lists. By default this is true. + */ + includeHidden?: boolean; + /** + * Specify the property on which you want to order the retrieve set of lists. + */ + orderBy?: PropertyFieldListPickerOrderBy; + + + /** + * Defines a onPropertyChange function to raise when the selected value changed. + * Normally this function must be always defined with the 'this.onPropertyChange' + * method of the web part object. + */ + onPropertyChange(propertyPath: string, oldValue: any, newValue: any): void; // eslint-disable-line @typescript-eslint/no-explicit-any + /** + * Parent Web Part properties + */ + properties: any; // eslint-disable-line @typescript-eslint/no-explicit-any + /** + * An UNIQUE key indicates the identity of this control + */ + key?: string; + /** + * Whether the property pane field is enabled or not. + */ + disabled?: boolean; + /** + * The method is used to get the validation error message and determine whether the input value is valid or not. + * + * When it returns string: + * - If valid, it returns empty string. + * - If invalid, it returns the error message string and the text field will + * show a red border and show an error message below the text field. + * + * When it returns Promise: + * - The resolved value is display as error message. + * - The rejected, the value is thrown away. + * + */ + onGetErrorMessage?: (value: string) => string | Promise; + /** + * Custom Field will start to validate after users stop typing for `deferredValidationTime` milliseconds. + * Default value is 200. + */ + deferredValidationTime?: number; + /** + * Defines list titles which should be excluded from the list picker control + */ + listsToExclude?: string[]; + /** + * Filter list from Odata query (takes precendents over Hidden and BaseTemplate Filters) + */ + filter?: string; + /** + * Callback that is called before the dropdown is populated + */ + onListsRetrieved?: (lists: ISPList[]) => PromiseLike | ISPList[]; + + /** + * Specifies if the picker returns list id, title and url as an object instead on id. + */ + includeListTitleAndUrl?: boolean; + /** + * Content type id which, if present, must be on the list + */ + contentTypeId?: string; +} + +/** + * Private properties of the PropertyFieldListPicker custom field. + * We separate public & private properties to include onRender & onDispose method waited + * by the PropertyFieldCustom, witout asking to the developer to add it when he's using + * the PropertyFieldListPicker. + * + */ +export interface IPropertyFieldListPickerPropsInternal extends IPropertyFieldListPickerProps, IPropertyPaneCustomFieldProps { + + label: string; + targetProperty: string; + context: BaseComponentContext; + webAbsoluteUrl?: string; + selectedList?: string | IPropertyFieldList; + baseTemplate?: number | number[]; + orderBy?: PropertyFieldListPickerOrderBy; + includeHidden?: boolean; + onPropertyChange(propertyPath: string, oldValue: any, newValue: any): void; // eslint-disable-line @typescript-eslint/no-explicit-any + properties: any; // eslint-disable-line @typescript-eslint/no-explicit-any + key: string; + disabled?: boolean; + onGetErrorMessage?: (value: string | string[]) => string | Promise; + deferredValidationTime?: number; + listsToExclude?: string[]; + filter?: string; + onListsRetrieved?: (lists: ISPList[]) => PromiseLike | ISPList[]; +} diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/IPropertyFieldListPickerHost.ts b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/IPropertyFieldListPickerHost.ts new file mode 100644 index 000000000..47f127f70 --- /dev/null +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/IPropertyFieldListPickerHost.ts @@ -0,0 +1,49 @@ +import { IPropertyFieldListPickerPropsInternal } from './IPropertyFieldListPicker'; +import { IDropdownOption } from '@fluentui/react/lib/Dropdown'; + +/** + * PropertyFieldListPickerHost properties interface + */ +export interface IPropertyFieldListPickerHostProps extends IPropertyFieldListPickerPropsInternal { + + onChange: (targetProperty?: string, newValue?: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +/** + * PropertyFieldListPickerHost state interface + */ +export interface IPropertyFieldListPickerHostState { + loadedLists: ISPLists; + results: IDropdownOption[]; + selectedKey?: string; + errorMessage?: string; +} + +/** + * Defines a collection of SharePoint lists + */ +export interface ISPLists { + + value: ISPList[]; +} + +/** + * Defines a Content Type + */ + export interface ISPContentType{ + StringId:string; + } + +/** + * Defines a SharePoint list + */ +export interface ISPList { + + Title: string; + Id: string; + BaseTemplate: string; + RootFolder: { + ServerRelativeUrl: string; + }; + ContentTypes: Array; +} diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/PropertyFieldListPicker.ts b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/PropertyFieldListPicker.ts new file mode 100644 index 000000000..a63bfeca8 --- /dev/null +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/PropertyFieldListPicker.ts @@ -0,0 +1,160 @@ +import * as React from 'react'; +import * as ReactDom from 'react-dom'; +import { + IPropertyPaneField, + PropertyPaneFieldType +} from '@microsoft/sp-property-pane'; +import { BaseComponentContext } from '@microsoft/sp-component-base'; +import PropertyFieldListPickerHost from './PropertyFieldListPickerHost'; +import { IPropertyFieldListPickerHostProps, ISPList } from './IPropertyFieldListPickerHost'; +import { PropertyFieldListPickerOrderBy, IPropertyFieldListPickerProps, IPropertyFieldListPickerPropsInternal, IPropertyFieldList } from './IPropertyFieldListPicker'; + +/** + * Represents a PropertyFieldListPicker object + */ +class PropertyFieldListPickerBuilder implements IPropertyPaneField { + + //Properties defined by IPropertyPaneField + public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom; + public targetProperty: string; + public properties: IPropertyFieldListPickerPropsInternal; + + //Custom properties label: string; + private label: string; + private context: BaseComponentContext; + private webAbsoluteUrl?: string; + private selectedList: string | IPropertyFieldList|undefined; + private baseTemplate: number | number[]|undefined; + private orderBy: PropertyFieldListPickerOrderBy; + private includeHidden: boolean; + private listsToExclude: string[]; + private includeListTitleAndUrl: boolean; + + public onPropertyChange(propertyPath: string, oldValue: any, newValue: any): void { /* no-op; */ } // eslint-disable-line @typescript-eslint/no-explicit-any + private customProperties: any; // eslint-disable-line @typescript-eslint/no-explicit-any + private key: string; + private disabled: boolean = false; + private onGetErrorMessage: (value: string) => string | Promise; + private deferredValidationTime: number = 200; + private filter: string|undefined; + private contentTypeId: string|undefined; + private onListsRetrieved?: (lists: ISPList[]) => PromiseLike | ISPList[]; + /** + * Constructor method + */ + public constructor(_targetProperty: string, _properties: IPropertyFieldListPickerPropsInternal) { + this.render = this.render.bind(this); + this.targetProperty = _targetProperty; + this.properties = _properties; + this.properties.onDispose = this.dispose; + this.properties.onRender = this.render; + this.label = _properties.label; + this.context = _properties.context; + this.webAbsoluteUrl = _properties.webAbsoluteUrl; + this.selectedList = _properties.selectedList; + this.baseTemplate = _properties.baseTemplate; + this.orderBy = _properties.orderBy?_properties.orderBy:PropertyFieldListPickerOrderBy.Title; + + this.includeHidden = _properties.includeHidden?_properties.includeHidden:false; + this.onPropertyChange = _properties.onPropertyChange; + this.customProperties = _properties.properties; + this.key = _properties.key; + this.onGetErrorMessage = _properties.onGetErrorMessage?_properties.onGetErrorMessage:() => ''; + this.listsToExclude = _properties.listsToExclude?_properties.listsToExclude:[]; + this.filter = _properties.filter; + this.onListsRetrieved = _properties.onListsRetrieved; + this.includeListTitleAndUrl = _properties.includeListTitleAndUrl?_properties.includeListTitleAndUrl:false; + this.contentTypeId=_properties.contentTypeId; + + if (_properties.disabled === true) { + this.disabled = _properties.disabled; + } + if (_properties.deferredValidationTime) { + this.deferredValidationTime = _properties.deferredValidationTime; + } + } + + /** + * Renders the SPListPicker field content + */ + private render(elem: HTMLElement, ctx?: any, changeCallback?: (targetProperty?: string, newValue?: any) => void): void { // eslint-disable-line @typescript-eslint/no-explicit-any + const componentProps: IPropertyFieldListPickerHostProps = { + label: this.label, + targetProperty: this.targetProperty, + context: this.context, + webAbsoluteUrl: this.webAbsoluteUrl, + baseTemplate: this.baseTemplate, + orderBy: this.orderBy, + + includeHidden: this.includeHidden, + onDispose: this.dispose, + onRender: this.render, + onChange: (target,value) => ( changeCallback && changeCallback(target,value)), + onPropertyChange: this.onPropertyChange, + properties: this.customProperties, + key: this.key, + disabled: this.disabled, + onGetErrorMessage: this.onGetErrorMessage, + deferredValidationTime: this.deferredValidationTime, + listsToExclude: this.listsToExclude, + filter: this.filter, + onListsRetrieved: this.onListsRetrieved, + includeListTitleAndUrl: this.includeListTitleAndUrl, + contentTypeId:this.contentTypeId + + }; + + + // Single selector + componentProps.selectedList = this.selectedList; + const element: React.ReactElement = React.createElement(PropertyFieldListPickerHost, componentProps); + // Calls the REACT content generator + ReactDom.render(element, elem); + + } + + /** + * Disposes the current object + */ + private dispose(elem: HTMLElement): void { + ReactDom.unmountComponentAtNode(elem); + } + +} + +/** + * Helper method to create a SPList Picker on the PropertyPane. + * @param targetProperty - Target property the SharePoint list picker is associated to. + * @param properties - Strongly typed SPList Picker properties. + */ +export function PropertyFieldListPicker(targetProperty: string, properties: IPropertyFieldListPickerProps): IPropertyPaneField { + + //Create an internal properties object from the given properties + const newProperties: IPropertyFieldListPickerPropsInternal = { + label: properties.label, + targetProperty: targetProperty, + context: properties.context, + webAbsoluteUrl: properties.webAbsoluteUrl, + selectedList: !Array.isArray(properties.selectedList) ? properties.selectedList : undefined, + baseTemplate: properties.baseTemplate, + orderBy: properties.orderBy, + + includeHidden: properties.includeHidden, + onPropertyChange: properties.onPropertyChange, + properties: properties.properties, + onDispose: undefined, + onRender: (elem: HTMLElement, ctx?: any, changeCallback?: (targetProperty?: string, newValue?: any) => void): void => { /* no-op; */ }, // eslint-disable-line @typescript-eslint/no-explicit-any + key: "key"+properties.key, + disabled: properties.disabled, + onGetErrorMessage: properties.onGetErrorMessage, + deferredValidationTime: properties.deferredValidationTime, + listsToExclude: properties.listsToExclude, + filter: properties.filter, + onListsRetrieved: properties.onListsRetrieved, + includeListTitleAndUrl: properties.includeListTitleAndUrl, + contentTypeId:properties.contentTypeId + }; + //Calls the PropertyFieldListPicker builder object + //This object will simulate a PropertyFieldCustom to manage his rendering process + return new PropertyFieldListPickerBuilder(targetProperty, newProperties); +} diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/PropertyFieldListPickerHost.tsx b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/PropertyFieldListPickerHost.tsx new file mode 100644 index 000000000..a884e08c9 --- /dev/null +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/PropertyFieldListPickerHost.tsx @@ -0,0 +1,251 @@ +import * as React from 'react'; +import { Dropdown, IDropdownOption } from '@fluentui/react/lib/Dropdown'; +import { Async } from '@fluentui/react/lib/Utilities'; +import { Label } from '@fluentui/react/lib/Label'; +import { IPropertyFieldListPickerHostProps, IPropertyFieldListPickerHostState, ISPList } from './IPropertyFieldListPickerHost'; + +import { IPropertyFieldList } from './IPropertyFieldListPicker'; +import SPListPickerService from './SPListPickerService'; +import { setPropertyValue } from '../PropertyOrderField/helper'; +import FieldErrorMessage from './FieldErrorMessage'; + +// Empty list value, to be checked for single list selection +const EMPTY_LIST_KEY = 'NO_LIST_SELECTED'; + +/** + * Renders the controls for PropertyFieldListPicker component + */ +export default class PropertyFieldListPickerHost extends React.Component { + + private latestValidateValue: string; + private async: Async; + private delayedValidate: (value: string) => void; + + /** + * Constructor method + */ + constructor(props: IPropertyFieldListPickerHostProps) { + super(props); + + + this.state = { + loadedLists: { + value: [] + }, + results: [], + errorMessage: '' + }; + + this.async = new Async(this); + this.validate = this.validate.bind(this); + this.onChanged = this.onChanged.bind(this); + this.notifyAfterValidate = this.notifyAfterValidate.bind(this); + this.delayedValidate = this.async.debounce(this.validate, this.props.deferredValidationTime); + } + + public componentDidMount(): void { + // Start retrieving the SharePoint lists + this.loadLists().then(() => { /* no-op; */ }).catch(() => { /* no-op; */ }); + } + + public componentDidUpdate(prevProps: IPropertyFieldListPickerHostProps, prevState: IPropertyFieldListPickerHostState): void { + if (this.props.baseTemplate !== prevProps.baseTemplate || + this.props.webAbsoluteUrl !== prevProps.webAbsoluteUrl) { + this.loadLists().then(() => { /* no-op; */ }).catch(() => { /* no-op; */ }); + } + } + + /** + * Loads the list from SharePoint current web site, or target site if specified by webRelativeUrl + */ + private async loadLists(): Promise { + + const { + context, + selectedList + } = this.props; + + const listService: SPListPickerService = new SPListPickerService(this.props, context); + const listsToExclude: string[] = this.props.listsToExclude || []; + const options = []; + let selectedListKey: string = ''; + if (selectedList) { + selectedListKey = typeof selectedList === 'string' ? selectedList : selectedList.id; + } + let selectedKey: string | undefined; + const response = await listService.getLibs(); + // Start mapping the list that are selected + response.value.forEach((list: ISPList) => { + if (selectedListKey === list.Id) { + selectedKey = list.Id; + } + + // Make sure that the current list is NOT in the 'listsToExclude' array + if (listsToExclude.indexOf(list.Title) === -1 && listsToExclude.indexOf(list.Id) === -1) { + options.push({ + key: list.Id, + text: list.Title + }); + } + }); + + // Option to unselect the list + options.unshift({ + key: EMPTY_LIST_KEY, + text: '' + }); + + // Update the current component state + this.setState({ + loadedLists: response, + results: options, + selectedKey: selectedKey + }); + } + + /** + * Raises when a list has been selected + */ + private onChanged(option: IDropdownOption, index?: number): void { + const newValue: string = option.key as string; + this.delayedValidate(newValue); + } + + /** + * Validates the new custom field value + */ + private validate(value: string): void { + if (this.props.onGetErrorMessage === null || this.props.onGetErrorMessage === undefined) { + this.notifyAfterValidate(value); + return; + } + + if (this.latestValidateValue === value) { + return; + } + + this.latestValidateValue = value; + + const errResult: string | Promise = this.props.onGetErrorMessage(value || ''); + if (typeof errResult !== 'undefined') { + if (typeof errResult === 'string') { + if (errResult === '') { + this.notifyAfterValidate(value); + } + this.setState({ + errorMessage: errResult + }); + } else { + errResult.then((errorMessage: string) => { + if (!errorMessage) { + this.notifyAfterValidate(value); + } + this.setState({ + errorMessage: errorMessage + }); + }).catch(() => { /* no-op; */ }); + } + } else { + this.notifyAfterValidate(value); + } + } + + /** + * Notifies the parent Web Part of a property value change + */ + private notifyAfterValidate(newValue: string): void { + const { + onPropertyChange, + targetProperty, + selectedList, + includeListTitleAndUrl, + properties, + onChange + } = this.props; + + const { + loadedLists + } = this.state; + + // Check if the user wanted to unselect the list + let propValue: string | IPropertyFieldList | undefined; + + if (includeListTitleAndUrl) { + if (newValue === EMPTY_LIST_KEY) { + propValue = undefined; + } + else { + const spList = loadedLists.value.filter(l => l.Id === newValue)[0]; + propValue = { + id: newValue, + title: spList.Title, + url: spList.RootFolder.ServerRelativeUrl + }; + } + } + else { + propValue = newValue === EMPTY_LIST_KEY ? '' : newValue; + } + + + // Deselect all options + const options = this.state.results.map(option => { + if (option.selected) { + option.selected = false; + } + return option; + }); + // Set the current selected key + const selectedKey = newValue; + // Update the state + this.setState({ + selectedKey: selectedKey, + results: options + }); + + if (onPropertyChange && propValue !== null) { + // Store the new property value + setPropertyValue(properties, targetProperty, propValue); + // Trigger the default onPrpertyChange event + onPropertyChange(targetProperty, selectedList, propValue); + // Trigger the apply button + if (typeof onChange !== 'undefined' && onChange !== null) { + onChange(targetProperty, propValue); + } + } + } + + /** + * Called when the component will unmount + */ + public componentWillUnmount(): void { + if (typeof this.async !== 'undefined') { + this.async.dispose(); + } + } + + /** + * Renders the SPListpicker controls with Office UI Fabric + */ + public render(): JSX.Element { + // Renders content + return ( +
+ {this.props.label && } + { + this.onChanged(options as IDropdownOption); + } + } + options={this.state.results} + selectedKey={this.state.selectedKey} + /> + + +
+ ); + } +} diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/SPListPickerService.ts b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/SPListPickerService.ts new file mode 100644 index 000000000..09893a256 --- /dev/null +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/SPListPickerService.ts @@ -0,0 +1,118 @@ +import { SPHttpClient } from '@microsoft/sp-http'; +import { BaseComponentContext } from '@microsoft/sp-component-base'; +import { IPropertyFieldListPickerHostProps, ISPList, ISPLists } from './IPropertyFieldListPickerHost'; +import { PropertyFieldListPickerOrderBy } from './IPropertyFieldListPicker'; + + +/** + * Service implementation to get list & list items from current SharePoint site + */ +export default class SPListPickerService { + private context: BaseComponentContext; + private props: IPropertyFieldListPickerHostProps; + + /** + * Service constructor + */ + constructor( + _props: IPropertyFieldListPickerHostProps, + pageContext: BaseComponentContext + ) { + this.props = _props; + this.context = pageContext; + } + + /** + * Gets the collection of libs in the current SharePoint site, or target site if specified by webRelativeUrl + */ + public async getLibs(): Promise { + // use the web relative url if provided, otherwise default to current SharePoint site + const webAbsoluteUrl = this.props.webAbsoluteUrl + ? this.props.webAbsoluteUrl + : this.context.pageContext.web.absoluteUrl; + // If the running environment is SharePoint, request the lists REST service + let queryUrl: string; + if (this.props.contentTypeId) { + queryUrl = `${webAbsoluteUrl}/_api/lists?$select=Title,id,BaseTemplate,RootFolder/ServerRelativeUrl,ContentTypes/StringId,ContentTypes/Name&$expand=RootFolder&$expand=ContentTypes`; + } else { + queryUrl = `${webAbsoluteUrl}/_api/lists?$select=Title,id,BaseTemplate,RootFolder/ServerRelativeUrl&$expand=RootFolder`; + } + // Check if the orderBy property is provided + if (this.props.orderBy !== null) { + queryUrl += '&$orderby='; + switch (this.props.orderBy) { + case PropertyFieldListPickerOrderBy.Id: + queryUrl += 'Id'; + break; + case PropertyFieldListPickerOrderBy.Title: + queryUrl += 'Title'; + break; + } + } + + // Adds an OData Filter to the list + if (this.props.filter) { + queryUrl += `&$filter=${encodeURIComponent(this.props.filter)}`; + } + // Check if the list have get filtered based on the list base template type + else if ((this.props.baseTemplate !== null && this.props.baseTemplate) || Array.isArray(this.props.baseTemplate)) { + if (Array.isArray(this.props.baseTemplate)) { + queryUrl += '&$filter=('; + queryUrl += this.props.baseTemplate.map(temp => `(BaseTemplate%20eq%20${temp})`).join('%20or%20'); + queryUrl += ')'; + } else { + queryUrl += '&$filter=BaseTemplate%20eq%20'; + queryUrl += this.props.baseTemplate; + } + + // Check if you also want to exclude hidden list in the list + if (this.props.includeHidden === false) { + queryUrl += '%20and%20Hidden%20eq%20false'; + } + } else { + if (this.props.includeHidden === false) { + queryUrl += '&$filter=Hidden%20eq%20false'; + } + } + const response = await this.context.spHttpClient.get( + queryUrl, + SPHttpClient.configurations.v1 + ); + + let lists = (await response.json()) as ISPLists; + //remove unwanted contenttypes + + + if (this.props.contentTypeId) { + const testct = this.props.contentTypeId.toUpperCase(); + lists.value = lists.value.filter((l) => { + for (const ct of l.ContentTypes) { + const ctid: string = ct.StringId.toUpperCase(); + if (ctid.substring(0, testct.length) === testct) { + return true; + } + } + return false; + }); + } + + // Check if onListsRetrieved callback is defined + if (this.props.onListsRetrieved) { + //Call onListsRetrieved + const lr = this.props.onListsRetrieved(lists.value); + let output: ISPList[]; + + //Conditional checking to see of PromiseLike object or array + if (lr instanceof Array) { + output = lr; + } else { + output = await lr; + } + + lists = { + value: output, + }; + } + return lists; + } +} diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/index.ts b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/index.ts new file mode 100644 index 000000000..59ddfa5c0 --- /dev/null +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyListPicker/index.ts @@ -0,0 +1,4 @@ +export * from './PropertyFieldListPicker'; +export * from './IPropertyFieldListPicker'; +export * from './PropertyFieldListPickerHost'; +export * from './IPropertyFieldListPickerHost'; diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/IPropertyFieldOrder.ts b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/IPropertyFieldOrder.ts new file mode 100644 index 000000000..98d20ac8e --- /dev/null +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/IPropertyFieldOrder.ts @@ -0,0 +1,77 @@ +import { IPropertyPaneCustomFieldProps } from '@microsoft/sp-property-pane'; + +/** + * Public properties of the PropertyFieldOrder custom field + */ +export interface IPropertyFieldOrderProps { + + /** + * Property field label displayed on top + */ + label: string; + + /** + * Defines an onPropertyChange function to raise when the items order changes. + * Normally this function must be defined with the 'this.onPropertyChange' + * method of the web part object. + */ + onPropertyChange(propertyPath: string, oldValue: any, newValue: any): void; // eslint-disable-line @typescript-eslint/no-explicit-any + + /** + * An array of values to reorder + */ + items: Array; // eslint-disable-line @typescript-eslint/no-explicit-any + + /** + * The property to use for display, when undefined, the toString() method of the object is used (ignored when the onRenderItem function is specified) + */ + textProperty?: string; + + /** + * When true, drag and drop reordering is disabled (defaults to false) + */ + disableDragAndDrop?: boolean; + + /** + * When true, arrow buttons are not displayed (defaults to false) + */ + removeArrows?: boolean; + + /** + * The maximun height for the items in px (when not set, the control expands as necessary) + */ + maxHeight?: number; + + /** + * Whether the property pane field is enabled or not. + */ + disabled?: boolean; + + /** + * Optional callback to provide custom rendering of the item (default is simple text based on either item or the property identified in the textProperty) + */ + onRenderItem?: (item: any, index: number) => JSX.Element; // eslint-disable-line @typescript-eslint/no-explicit-any + + /** + * An UNIQUE key indicates the identity of this control + */ + key: string; + + /** + * Parent Web Part properties + */ + properties: any; // eslint-disable-line @typescript-eslint/no-explicit-any + + /** + * The name of the UI Fabric Font Icon to use for the move up button (defaults to ChevronUpSmall) + */ + moveUpIconName?: string; + + /** + * The name of the UI Fabric Font Icon to use for the move down button (defaults to ChevronDownSmall) + */ + moveDownIconName?: string; +} + +export interface IPropertyFieldOrderPropsInternal extends IPropertyFieldOrderProps, IPropertyPaneCustomFieldProps { +} \ No newline at end of file diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/IPropertyFieldOrderHost.ts b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/IPropertyFieldOrderHost.ts new file mode 100644 index 000000000..94ac65b2d --- /dev/null +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/IPropertyFieldOrderHost.ts @@ -0,0 +1,23 @@ +/** + * PropertyFieldOrderHost properties interface + */ +export interface IPropertyFieldOrderHostProps { + label: string; + disabled: boolean; + items: Array; // eslint-disable-line @typescript-eslint/no-explicit-any + textProperty?: string; + moveUpIconName: string; + moveDownIconName: string; + disableDragAndDrop: boolean; + removeArrows: boolean; + maxHeight?: number; + valueChanged: (newValue: Array) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + onRenderItem?: (item: any, index: number) => JSX.Element; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +/** + * PropertyFieldOrderHost state interface + */ +export interface IPropertyFieldOrderHostState { + items: Array; // eslint-disable-line @typescript-eslint/no-explicit-any +} \ No newline at end of file diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/PropertyFieldOrder.ts b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/PropertyFieldOrder.ts new file mode 100644 index 000000000..f7393aaf6 --- /dev/null +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/PropertyFieldOrder.ts @@ -0,0 +1,94 @@ +import { IPropertyPaneField, PropertyPaneFieldType } from '@microsoft/sp-property-pane'; +import * as React from 'react'; +import * as ReactDom from 'react-dom'; + + + +import { IPropertyFieldOrderHostProps } from './IPropertyFieldOrderHost'; +import PropertyFieldOrderHost from './PropertyFieldOrderHost'; +import { IPropertyFieldOrderProps, IPropertyFieldOrderPropsInternal } from './IPropertyFieldOrder'; +import { setPropertyValue } from './helper'; + + +class PropertyFieldOrderBuilder implements IPropertyPaneField { + + //Properties defined by IPropertyPaneField + public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom; + public targetProperty: string; + public properties: IPropertyFieldOrderPropsInternal; + private elem: HTMLElement; + private items: Array; // eslint-disable-line @typescript-eslint/no-explicit-any + private changeCB?: (targetProperty?: string, newValue?: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + + public constructor(_targetProperty: string, _properties: IPropertyFieldOrderProps) { + this.targetProperty = _targetProperty; + this.properties = { + key: _properties.key, + label: _properties.label, + onPropertyChange: _properties.onPropertyChange, + disabled: _properties.disabled, + properties: _properties.properties, + items: _properties.items, + textProperty: _properties.textProperty, + moveUpIconName: _properties.moveUpIconName, + moveDownIconName: _properties.moveDownIconName, + disableDragAndDrop: _properties.disableDragAndDrop, + removeArrows: _properties.removeArrows, + maxHeight: _properties.maxHeight, + onRenderItem: _properties.onRenderItem, + onRender: this.onRender.bind(this) + }; + this.items = _properties.items; + } + + public render(): void { + if (!this.elem) { + return; + } + + this.onRender(this.elem); + } + + public onDispose(element: HTMLElement): void { + ReactDom.unmountComponentAtNode(element); + } + + private onRender(elem: HTMLElement, ctx?: any, changeCallback?: (targetProperty?: string, newValue?: any) => void): void { // eslint-disable-line @typescript-eslint/no-explicit-any + if (!this.elem) { + this.elem = elem; + } + this.changeCB = changeCallback; + + const element: React.ReactElement = React.createElement(PropertyFieldOrderHost, { + label: this.properties.label, + disabled: this.properties.disabled ? this.properties.disabled : false, + items: this.items, + textProperty: this.properties.textProperty, + moveUpIconName: this.properties.moveUpIconName || 'ChevronUpSmall', + moveDownIconName: this.properties.moveDownIconName || 'ChevronDownSmall', + disableDragAndDrop: this.properties.disableDragAndDrop ? this.properties.disableDragAndDrop : false, + removeArrows: this.properties.removeArrows ? this.properties.removeArrows : false, + maxHeight: this.properties.maxHeight, + onRenderItem: this.properties.onRenderItem, + valueChanged: this.onValueChanged.bind(this) + }); + ReactDom.render(element, elem); + } + + private onValueChanged(newValue: Array): void { // eslint-disable-line @typescript-eslint/no-explicit-any + if (this.properties.onPropertyChange && newValue !== null) { + this.properties.onPropertyChange(this.targetProperty, this.items, newValue); + this.items = newValue; + setPropertyValue(this.properties.properties, this.targetProperty, newValue); + if (typeof this.changeCB !== 'undefined' && this.changeCB !== null) { + this.changeCB(this.targetProperty, newValue); + } + } + } + +} + +export function PropertyFieldOrder(targetProperty: string, properties: IPropertyFieldOrderProps): IPropertyPaneField { + return new PropertyFieldOrderBuilder(targetProperty, properties); +} + diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/PropertyFieldOrderHost.module.scss b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/PropertyFieldOrderHost.module.scss new file mode 100644 index 000000000..4bb46114d --- /dev/null +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/PropertyFieldOrderHost.module.scss @@ -0,0 +1,65 @@ +$ms-color-themePrimary: '[theme:themePrimary, default:#0078d7]'; +$ms-color-neutralLight: '[theme:neutralLight, default:#eaeaea]'; +$ms-color-neutralLighter: '[theme:neutralLighter, default:#f4f4f4]'; +$ms-color-neutralTertiary: '[theme:neutralTertiary, default:#a6a6a6]'; +$ms-color-white: '[theme:white, default:#ffffff]'; + + +.propertyFieldOrder { + margin-bottom: 2px; + + ul { + padding: 0.5px; + margin: 0; + overflow-y: auto; + } + + .disabled { + + li { + color: $ms-color-neutralTertiary; + } + + } + + li { + list-style: none; + background-color: $ms-color-white; + border: 0.5px solid; + border-color: $ms-color-neutralLight; + outline: 0.5px solid; + outline-color: $ms-color-neutralLight; + + .enabled & :hover { + background-color: $ms-color-neutralLighter; + } + + & > div { + padding: 3px 6px; + display: flex; + flex-direction: row; + } + + } + + .itemBox { + flex-grow: 1; + } + + .dragEnter { + background-color: $ms-color-neutralLight; + border-top: 2px dashed; + border-top-color: $ms-color-themePrimary; + } + + .dragLast { + background-color: $ms-color-neutralLight; + border-bottom: 2px dashed; + border-bottom-color: $ms-color-themePrimary; + } + + .lastBox { + height: 8px; + } + +} \ No newline at end of file diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/PropertyFieldOrderHost.tsx b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/PropertyFieldOrderHost.tsx new file mode 100644 index 000000000..42b3f9d92 --- /dev/null +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/PropertyFieldOrderHost.tsx @@ -0,0 +1,263 @@ +import { IButtonStyles, IconButton } from '@fluentui/react/lib/Button'; +//import { Selection } from '@fluentui/react/lib/DetailsList'; +import { Label } from '@fluentui/react/lib/Label'; +//import { DragDropHelper } from '@fluentui/react/lib/utilities/dragdrop'; +//import { IDragDropContext } from '@fluentui/react/lib/utilities/dragdrop/interfaces'; +import * as React from 'react'; + + +import { IPropertyFieldOrderHostProps, IPropertyFieldOrderHostState } from './IPropertyFieldOrderHost'; +import styles from './PropertyFieldOrderHost.module.scss'; +import { isEqual } from '@microsoft/sp-lodash-subset'; +//import { EventGroup } from '@fluentui/react/lib/Utilities'; //'@uifabric/utilities/lib/EventGroup'; + +export default class PropertyFieldOrderHost extends React.Component { + + private _draggedItem: any; // eslint-disable-line @typescript-eslint/no-explicit-any + //private _selection: Selection; + //private _ddHelper: DragDropHelper; + private _refs: Array; + private _ddSubs: Array; // eslint-disable-line @typescript-eslint/no-explicit-any + private _lastBox: HTMLDivElement; + + constructor(props: IPropertyFieldOrderHostProps, state: IPropertyFieldOrderHostState) { + super(props); + // this._selection = new Selection(); + /*this._ddHelper = new DragDropHelper({ + selection: this._selection + }); +*/ + this._refs = new Array(); + this._ddSubs = new Array(); // eslint-disable-line @typescript-eslint/no-explicit-any + + this._draggedItem = null; + + this.state = { + items: [] + }; + } + + public render(): JSX.Element { + const { + items + } = this.state; + return ( +
+ {this.props.label && } +
    + { + (items && items.length > 0) && ( + items.map((value: any, index: number) => { // eslint-disable-line @typescript-eslint/no-explicit-any + return ( +
  • {this.renderItem(value, index)}
  • + ); + }) + ) + } + { + (items && items.length > 0) &&
    { this._lastBox = ref; }} /> + } +
+
+ ); + } + + private renderItem(item: any, index: number): JSX.Element { // eslint-disable-line @typescript-eslint/no-explicit-any + return ( +
+
+ {this.renderDisplayValue(item, index)} +
+ {!this.props.removeArrows && +
{this.renderArrows(index)}
+ } +
+ ); + } + + private renderDisplayValue(item: any, index: number): JSX.Element { // eslint-disable-line @typescript-eslint/no-explicit-any + if (typeof this.props.onRenderItem === "function") { + return this.props.onRenderItem(item, index); + } else { + return ( + {this.props.textProperty ? item[this.props.textProperty] : item.toString()} + ); + } + } + + private renderArrows(index: number): JSX.Element { + const arrowButtonStyles: Partial = { + root: { + width: '14px', + height: '100%', + display: 'inline-block' + }, + rootDisabled: { + backgroundColor: 'transparent' + }, + icon: { + fontSize: "10px" + } + }; + + return ( +
+ { this.onMoveUpClick(index); }} + styles={arrowButtonStyles} + /> + { this.onMoveDownClick(index); }} + styles={arrowButtonStyles} + /> +
+ ); + } + + public UNSAFE_componentWillMount(): void { + this.setState({ + items: this.props.items || [] + }); + } + + public componentDidMount(): void { + this.setupSubscriptions(); + } + + public UNSAFE_componentWillUpdate(nextProps: IPropertyFieldOrderHostProps): void { + // Check if the provided items are still the same + if (!isEqual(nextProps.items, this.state.items)) { + this.setState({ + items: this.props.items || [] + }); + } + } + + public componentDidUpdate(): void { + this.cleanupSubscriptions(); + this.setupSubscriptions(); + } + + public componentWillUnmount(): void { + this.cleanupSubscriptions(); + } + + private registerRef = (ref: HTMLLIElement): void => { + this._refs.push(ref); + } + + private setupSubscriptions = (): void => { + if (!this.props.disableDragAndDrop && !this.props.disabled) { + this._refs.forEach((value: HTMLElement, index: number) => { + /* this._ddSubs.push(this._ddHelper.subscribe(value, new EventGroup(value), { + eventMap: [ + { + callback: (context: IDragDropContext) => { + this._draggedItem = context.data; + }, + eventName: 'dragstart' + } + ], + selectionIndex: index, + context: { data: this.state.items[index], index: index }, + updateDropState: (isDropping: boolean, event: DragEvent) => { + if (isDropping) { + value.classList.add(styles.dragEnter); + } else { + value.classList.remove(styles.dragEnter); + } + }, + canDrop: (dropContext?: IDragDropContext, dragContext?: IDragDropContext) => { + return true; + }, + canDrag: (item?: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any + return true; + }, + onDrop: (item?: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any + if (this._draggedItem) { + this.insertBeforeItem(item); + } + }, + onDragEnd: () => { + this._draggedItem = null; + } + }));*/ + }); + + //Create dropable area below list to allow items to be dragged to the bottom + if (this._refs.length && typeof this._lastBox !== "undefined") { + /*this._ddSubs.push(this._ddHelper.subscribe(this._lastBox, new EventGroup(this._lastBox), { + selectionIndex: this._refs.length, + context: { data: {}, index: this._refs.length }, + updateDropState: (isDropping: boolean, event: DragEvent) => { + if (isDropping) { + this._refs[this._refs.length - 1].classList.add(styles.dragLast); + } else { + this._refs[this._refs.length - 1].classList.remove(styles.dragLast); + } + }, + canDrop: (dropContext?: IDragDropContext, dragContext?: IDragDropContext) => { + return true; + }, + onDrop: (item?: any, event?: DragEvent) => { // eslint-disable-line @typescript-eslint/no-explicit-any + if (this._draggedItem) { + const itemIndex: number = this.state.items.indexOf(this._draggedItem); + this.moveItemAtIndexToTargetIndex(itemIndex, this.state.items.length - 1); + } + } + }));*/ + } + } + } + + private cleanupSubscriptions = (): void => { + while (this._ddSubs.length) { + const sub: any = this._ddSubs.pop(); // eslint-disable-line @typescript-eslint/no-explicit-any + sub.dispose(); + } + } + + public insertBeforeItem = (item: any): void => { // eslint-disable-line @typescript-eslint/no-explicit-any + const itemIndex: number = this.state.items.indexOf(this._draggedItem); + let targetIndex: number = this.state.items.indexOf(item); + if (itemIndex < targetIndex) { + targetIndex -= 1; + } + this.moveItemAtIndexToTargetIndex(itemIndex, targetIndex); + } + + + private onMoveUpClick = (itemIndex: number): void => { + if (itemIndex > 0) { + this.moveItemAtIndexToTargetIndex(itemIndex, itemIndex - 1); + } + } + + private onMoveDownClick = (itemIndex: number): void => { + if (itemIndex < this.state.items.length - 1) { + this.moveItemAtIndexToTargetIndex(itemIndex, itemIndex + 1); + } + } + + private moveItemAtIndexToTargetIndex = (itemIndex: number, targetIndex: number): void => { + if (itemIndex !== targetIndex && itemIndex > -1 && targetIndex > -1 && itemIndex < this.state.items.length && targetIndex < this.state.items.length) { + const items: Array = this.state.items; // eslint-disable-line @typescript-eslint/no-explicit-any + items.splice(targetIndex, 0, ...items.splice(itemIndex, 1)); + + this.setState({ + items: items + }); + + this.props.valueChanged(items); + } + } +} \ No newline at end of file diff --git a/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/helper.ts b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/helper.ts new file mode 100644 index 000000000..73c955160 --- /dev/null +++ b/samples/react-kanban-board/src/webparts/kanbanBoard/components/PropertyOrderField/helper.ts @@ -0,0 +1,12 @@ +export const setPropertyValue = (properties: any, targetProperty: string, value: any): void => { // eslint-disable-line @typescript-eslint/no-explicit-any + if (!properties) { + return; + } + if (targetProperty.indexOf('.') === -1) { // simple prop + properties[targetProperty] = value; + } + else { + throw new Error('Nested properties are not supported'); + // .set(properties, targetProperty, value); + } + }; \ No newline at end of file