diff --git a/docs/documentation/docs/assets/ListItemComments.gif b/docs/documentation/docs/assets/ListItemComments.gif new file mode 100644 index 000000000..b1347d9db Binary files /dev/null and b/docs/documentation/docs/assets/ListItemComments.gif differ diff --git a/docs/documentation/docs/assets/ListItemComments01.png b/docs/documentation/docs/assets/ListItemComments01.png new file mode 100644 index 000000000..a61343a71 Binary files /dev/null and b/docs/documentation/docs/assets/ListItemComments01.png differ diff --git a/docs/documentation/docs/assets/ListItemComments02.png b/docs/documentation/docs/assets/ListItemComments02.png new file mode 100644 index 000000000..30595097e Binary files /dev/null and b/docs/documentation/docs/assets/ListItemComments02.png differ diff --git a/docs/documentation/docs/assets/ListItemComments03.png b/docs/documentation/docs/assets/ListItemComments03.png new file mode 100644 index 000000000..fa4ca653e Binary files /dev/null and b/docs/documentation/docs/assets/ListItemComments03.png differ diff --git a/docs/documentation/docs/assets/ListItemComments04.png b/docs/documentation/docs/assets/ListItemComments04.png new file mode 100644 index 000000000..351fde302 Binary files /dev/null and b/docs/documentation/docs/assets/ListItemComments04.png differ diff --git a/docs/documentation/docs/controls/ListItemComments.md b/docs/documentation/docs/controls/ListItemComments.md new file mode 100644 index 000000000..7eca4726b --- /dev/null +++ b/docs/documentation/docs/controls/ListItemComments.md @@ -0,0 +1,59 @@ +# ListItemComments control + +This control allows you to manage list item comments, you can add or delete comments to an item. The comments are listed in tile view. +user can scroll to load more comments if they exist (infinite scroll); + + +Here is an example of the control: + +![ListItemComments](../assets/ListItemComments.gif) + +![ListItemComments](../assets/ListItemComments01.png) + +![ListItemComments](../assets/ListItemComments02.png) + +![ListItemComments](../assets/ListItemComments03.png) + +![ListItemComments](../assets/ListItemComments04.png) + +## How to use this control in your solutions + +- Check that you installed the `@pnp/spfx-controls-react` dependency. Check out the [getting started](../../#getting-started) page for more information about installing the dependency. +- Import the control into your component: + +```TypeScript +import { ListItemComments } from '@pnp/spfx-controls-react/lib/ListItemComments'; +``` +- Use the `ListItemComments` control in your code as follows: + +```TypeScript + +``` + + +## Implementation + +The `ListItemComments` control can be configured with the following properties: + + +| Property | Type | Required | Description | +| ---- | ---- | ---- | ---- | +| serviceScope | ServiceScope | yes | SPFx Service Scope | +| itemId | number | yes | List Item Id | +| listId | string | yes | Guid of the list. | +| webUrl | string | no | URL of the site. By default it uses the current site URL. | +| label | string | no | Label for control | +| numberCommentsPerPage | number | no | number of comments per page possible values 5 | 10 | 15 | 20 default 10 | + +## MSGraph Permissions required + +This control required the flowing scopes: +at least : People.Read + +![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/ListItemComments) diff --git a/package-lock.json b/package-lock.json index 16f48cc98..04cd69ef3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -787,6 +787,42 @@ } } }, + "@fluentui/font-icons-mdl2": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.1.6.tgz", + "integrity": "sha512-tNAaX72NQYbvR9zeiOiVQBQhYtVgPUgh68LKTGywuYGc2WffBu20Xk9wII8iLGmAijLI1QClaCQxaRAL0IkfBA==", + "requires": { + "@fluentui/set-version": "^8.1.3", + "@fluentui/style-utilities": "^8.2.0", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, + "@fluentui/foundation-legacy": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.1.6.tgz", + "integrity": "sha512-TJzUFcpfcJefXNmTOAJBKgIlQXDPw/dIcpO9l2nBfORvy4RnrJK4QjpdJPp5XOhDPtDVjlKPB1WvavoRkPRrbg==", + "requires": { + "@fluentui/merge-styles": "^8.1.3", + "@fluentui/set-version": "^8.1.3", + "@fluentui/style-utilities": "^8.2.0", + "@fluentui/utilities": "^8.2.1", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, "@fluentui/keyboard-key": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.2.17.tgz", @@ -802,6 +838,99 @@ } } }, + "@fluentui/merge-styles": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.1.3.tgz", + "integrity": "sha512-5vZUyXnbOb9M1rMLzQ7Kj5uadHgSTsp3gv0xDv6bfPvzB9RgQa3dEuJ6TA34tLezw8sFYuA6NnKd57nlb4aXMA==", + "requires": { + "@fluentui/set-version": "^8.1.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, + "@fluentui/react": { + "version": "8.23.7", + "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.23.7.tgz", + "integrity": "sha512-140AHdJlWpM/JViiqRBRIP1QqRiDoRilMkqsmhfNdx306SqE8cjSMdp56UvYWU1/xcsEDBKYqJKBadHUoB2GOA==", + "requires": { + "@fluentui/date-time-utilities": "^8.2.1", + "@fluentui/font-icons-mdl2": "^8.1.6", + "@fluentui/foundation-legacy": "^8.1.6", + "@fluentui/merge-styles": "^8.1.3", + "@fluentui/react-focus": "^8.1.8", + "@fluentui/react-hooks": "^8.2.4", + "@fluentui/react-window-provider": "^2.1.3", + "@fluentui/set-version": "^8.1.3", + "@fluentui/style-utilities": "^8.2.0", + "@fluentui/theme": "^2.1.4", + "@fluentui/utilities": "^8.2.1", + "@microsoft/load-themed-styles": "^1.10.26", + "tslib": "^2.1.0" + }, + "dependencies": { + "@fluentui/date-time-utilities": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@fluentui/date-time-utilities/-/date-time-utilities-8.2.1.tgz", + "integrity": "sha512-0AYXaXFQ3bPsOtiOi3bJSihzf+w3L44iQK38EiQgp3uAXei/i36VtDCToHZehYV+eS4s1qb/QGksoL0F4G6WCQ==", + "requires": { + "@fluentui/set-version": "^8.1.3", + "tslib": "^2.1.0" + } + }, + "@fluentui/keyboard-key": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.3.3.tgz", + "integrity": "sha512-3qX1WNCgJlKq7uGH76rLC4cdESgwdLhMH9WjcQUkQNJKtBpL4vs5O99M1keEhd3pfooW7zasr6AcYcWq4BRB4g==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@fluentui/react-focus": { + "version": "8.1.8", + "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.1.8.tgz", + "integrity": "sha512-EUI1TZwM7T2keNEjqIAkeV9ALMlLjz7abqHk0AypKJG3v4YPQHycal37KAHQb+gdVJX2hjVQLxynI1yrquKFUw==", + "requires": { + "@fluentui/keyboard-key": "^0.3.3", + "@fluentui/merge-styles": "^8.1.3", + "@fluentui/set-version": "^8.1.3", + "@fluentui/style-utilities": "^8.2.0", + "@fluentui/utilities": "^8.2.1", + "tslib": "^2.1.0" + } + }, + "@fluentui/react-window-provider": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.1.3.tgz", + "integrity": "sha512-3NWL3Kkqp3elD/aTrUaEufRUrN7K7hYsXEsOY2bDCDjPadLGtZlTGWNYFbUYNsaL/v79gZHhH+voCECP85HqRg==", + "requires": { + "@fluentui/set-version": "^8.1.3", + "tslib": "^2.1.0" + } + }, + "@fluentui/theme": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.1.4.tgz", + "integrity": "sha512-Y4FWgnYldvAFOo24tfsREMb8/3Tn5uDoYgGO7AAdrksP7VAaavaEVQCOgvHWy3l89Bsxf00/fE+QJ/AHv5Z4CA==", + "requires": { + "@fluentui/merge-styles": "^8.1.3", + "@fluentui/set-version": "^8.1.3", + "@fluentui/utilities": "^8.2.1", + "tslib": "^2.1.0" + } + }, + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, "@fluentui/react-bindings": { "version": "0.51.7", "resolved": "https://registry.npmjs.org/@fluentui/react-bindings/-/react-bindings-0.51.7.tgz", @@ -901,6 +1030,33 @@ } } }, + "@fluentui/react-hooks": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.2.4.tgz", + "integrity": "sha512-qc/j0YdxC0zAWVqh8BJppZuK3o9/rfyu5psY4N/AL9dmKrTFWszRgTSB5uiRShN99L88UUEV9RtlfknnLDGrUg==", + "requires": { + "@fluentui/react-window-provider": "^2.1.3", + "@fluentui/set-version": "^8.1.3", + "@fluentui/utilities": "^8.2.1", + "tslib": "^2.1.0" + }, + "dependencies": { + "@fluentui/react-window-provider": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.1.3.tgz", + "integrity": "sha512-3NWL3Kkqp3elD/aTrUaEufRUrN7K7hYsXEsOY2bDCDjPadLGtZlTGWNYFbUYNsaL/v79gZHhH+voCECP85HqRg==", + "requires": { + "@fluentui/set-version": "^8.1.3", + "tslib": "^2.1.0" + } + }, + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, "@fluentui/react-icons-northstar": { "version": "0.51.7", "resolved": "https://registry.npmjs.org/@fluentui/react-icons-northstar/-/react-icons-northstar-0.51.7.tgz", @@ -999,6 +1155,21 @@ } } }, + "@fluentui/set-version": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@fluentui/set-version/-/set-version-8.1.3.tgz", + "integrity": "sha512-QYLFBnwa6xJj0phEy6r+iO5bXWXxkhS1uNngUwfWsHaTskDa4PXDDdjZAXjTVV935xqmoM4GZhZk+Tcoe/OaXA==", + "requires": { + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, "@fluentui/state": { "version": "0.51.7", "resolved": "https://registry.npmjs.org/@fluentui/state/-/state-0.51.7.tgz", @@ -1007,6 +1178,37 @@ "@babel/runtime": "^7.10.4" } }, + "@fluentui/style-utilities": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.2.0.tgz", + "integrity": "sha512-gdAgBnevDOHbgqAKCaQG4CXN6dONMg8BRSZNqha0I9WdgLJy7F7t4xVo8elPjlDUP72ciYT8J9Z/YNljZzbE0w==", + "requires": { + "@fluentui/merge-styles": "^8.1.3", + "@fluentui/set-version": "^8.1.3", + "@fluentui/theme": "^2.1.4", + "@fluentui/utilities": "^8.2.1", + "@microsoft/load-themed-styles": "^1.10.26", + "tslib": "^2.1.0" + }, + "dependencies": { + "@fluentui/theme": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.1.4.tgz", + "integrity": "sha512-Y4FWgnYldvAFOo24tfsREMb8/3Tn5uDoYgGO7AAdrksP7VAaavaEVQCOgvHWy3l89Bsxf00/fE+QJ/AHv5Z4CA==", + "requires": { + "@fluentui/merge-styles": "^8.1.3", + "@fluentui/set-version": "^8.1.3", + "@fluentui/utilities": "^8.2.1", + "tslib": "^2.1.0" + } + }, + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, "@fluentui/styles": { "version": "0.51.7", "resolved": "https://registry.npmjs.org/@fluentui/styles/-/styles-0.51.7.tgz", @@ -1035,6 +1237,33 @@ } } }, + "@fluentui/utilities": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.2.1.tgz", + "integrity": "sha512-ezRkBUDhHQjrqAWA6H1TwWU3STauW/EjthDAe/upJbmXeP3ynn7tTpx1gpNi/vHjJlRkO1JjNoiSc6P4MZWkYw==", + "requires": { + "@fluentui/dom-utilities": "^2.1.3", + "@fluentui/merge-styles": "^8.1.3", + "@fluentui/set-version": "^8.1.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "@fluentui/dom-utilities": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@fluentui/dom-utilities/-/dom-utilities-2.1.3.tgz", + "integrity": "sha512-i2YECSldnkzPAhVmrxksuKSbqBKfMbHrexGqDJm7dy6KoIx+JSilbA5Lz0YNhA7VEgCy1X01GHlWBvqhCNQW8g==", + "requires": { + "@fluentui/set-version": "^8.1.3", + "tslib": "^2.1.0" + } + }, + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2211,6 +2440,13 @@ "@microsoft/microsoft-graph-types": "^1.36.0", "@microsoft/microsoft-graph-types-beta": "^0.7.0-preview", "office-ui-fabric-core": "11.0.0" + }, + "dependencies": { + "@microsoft/microsoft-graph-types": { + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-1.41.0.tgz", + "integrity": "sha512-GNGqnN3k4wo6zlxszBUdrzEXMQMeTRXRO2OP3h63LNbqRrRHUe6WY2EbGwL7gUhap7pOoyziZ+eG5YUvUNdBSA==" + } } }, "@microsoft/mgt-element": { @@ -2233,6 +2469,13 @@ "@microsoft/microsoft-graph-types": "^1.36.0", "@microsoft/microsoft-graph-types-beta": "^0.7.0-preview", "wc-react": "^0.5.0" + }, + "dependencies": { + "@microsoft/microsoft-graph-types": { + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-1.41.0.tgz", + "integrity": "sha512-GNGqnN3k4wo6zlxszBUdrzEXMQMeTRXRO2OP3h63LNbqRrRHUe6WY2EbGwL7gUhap7pOoyziZ+eG5YUvUNdBSA==" + } } }, "@microsoft/mgt-sharepoint-provider": { @@ -2272,9 +2515,10 @@ } }, "@microsoft/microsoft-graph-types": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-1.41.0.tgz", - "integrity": "sha512-GNGqnN3k4wo6zlxszBUdrzEXMQMeTRXRO2OP3h63LNbqRrRHUe6WY2EbGwL7gUhap7pOoyziZ+eG5YUvUNdBSA==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-2.1.0.tgz", + "integrity": "sha512-IwSgVVgLPKZmM9icGsSKY7DSj3qhma5UD2cHAFsuOq9+1As7rXoQqUX3G2s5B0EWz4k5oCgKULcRfEH+GuUaQA==", + "dev": true }, "@microsoft/microsoft-graph-types-beta": { "version": "0.7.0-preview", @@ -4888,6 +5132,12 @@ "chokidar": "^2.1.2" } }, + "@types/he": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/he/-/he-1.1.2.tgz", + "integrity": "sha512-kSJPcLO1x+oolc0R89pUl2kozldQ/fVQ1C1p5mp8fPoLdF/ZcBvckaTC2M8xXh3GYendXvCpy5m/a2eSbfgNgw==", + "dev": true + }, "@types/http-proxy": { "version": "1.17.7", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz", @@ -5039,8 +5289,7 @@ "@types/prop-types": { "version": "15.7.4", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", - "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", - "dev": true + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" }, "@types/q": { "version": "1.5.5", @@ -5072,7 +5321,6 @@ "version": "16.9.36", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.36.tgz", "integrity": "sha512-mGgUb/Rk/vGx4NCvquRuSH0GHBQKb1OqpGS9cT9lFxlTLHZgkksgI60TuIxubmn7JuCb+sENHhQciqa0npm0AQ==", - "dev": true, "requires": { "@types/prop-types": "*", "csstype": "^2.2.0" @@ -5114,6 +5362,14 @@ "@types/react": "*" } }, + "@types/react-mentions": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@types/react-mentions/-/react-mentions-4.1.3.tgz", + "integrity": "sha512-Kfmw8OBydSHEWmPwBxHwM2CrN9DNvJJ4DhJ6DTC1p4kDGxs3BCLf0uVMpLBM1kpLglQhZxaI3B1lryss74I6aw==", + "requires": { + "@types/react": "*" + } + }, "@types/requirejs": { "version": "2.1.29", "resolved": "https://registry.npmjs.org/@types/requirejs/-/requirejs-2.1.29.tgz", @@ -9270,6 +9526,11 @@ } } }, + "date-fns": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.22.1.tgz", + "integrity": "sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==" + }, "dateformat": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", @@ -13146,8 +13407,7 @@ "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, "hex-color-regex": { "version": "1.1.0", @@ -13908,6 +14168,14 @@ "p-is-promise": "^1.1.0" } }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", @@ -20769,6 +21037,27 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-mentions": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/react-mentions/-/react-mentions-4.3.0.tgz", + "integrity": "sha512-qr60FhczwzDX/0ZHWKaV8nriDx0a/+totqa7z1jGiz8YipNMp3ReAJMfA+UUlIVj1lnbCzpFUAo/bTQBW0IVlg==", + "requires": { + "@babel/runtime": "7.4.5", + "invariant": "^2.2.4", + "prop-types": "^15.5.8", + "substyle": "^9.1.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz", + "integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==", + "requires": { + "regenerator-runtime": "^0.13.2" + } + } + } + }, "react-quill": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/react-quill/-/react-quill-1.3.5.tgz", @@ -21182,6 +21471,11 @@ } } }, + "regexify-string": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/regexify-string/-/regexify-string-1.0.16.tgz", + "integrity": "sha512-bR1smB6mGdHzjDyRTOA4TKXGr4VLGGjmArk1nMt37Ii6TjeOOg+WA0UBKIwk4y9UiHzS3JhlSnzyU2m+vvrM6g==" + }, "regexp.prototype.flags": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", @@ -23842,6 +24136,15 @@ "cssjanus": "^1.3.0" } }, + "substyle": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/substyle/-/substyle-9.4.1.tgz", + "integrity": "sha512-VOngeq/W1/UkxiGzeqVvDbGDPM8XgUyJVWjrqeh+GgKqspEPiLYndK+XRcsKUHM5Muz/++1ctJ1QCF/OqRiKWA==", + "requires": { + "@babel/runtime": "^7.3.4", + "invariant": "^2.2.4" + } + }, "sudo": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sudo/-/sudo-1.0.3.tgz", diff --git a/package.json b/package.json index d1b1c0fc1..8d3be6d39 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "prepare": "husky install" }, "dependencies": { + "@fluentui/react": "^8.23.7", "@fluentui/react-northstar": "0.51.3", "@microsoft/mgt-react": "2.2.0", "@microsoft/mgt-spfx": "2.2.0", @@ -32,19 +33,25 @@ "@pnp/sp": "2.5.0", "@pnp/telemetry-js": "2.0.0", "@popperjs/core": "2.5.4", + "@types/react-mentions": "^4.1.3", "@uifabric/icons": "7.3.0", "animate.css": "^4.1.1", "chart.js": "2.9.4", "color": "3.1.2", + "date-fns": "^2.22.1", + "he": "^1.2.0", "lodash": "4.17.21", "office-ui-fabric-react": "7.156.0", "react": "16.9.0", "react-accessible-accordion": "^3.3.3", "react-dom": "16.9.0", + "react-mentions": "^4.3.0", "react-quill": "1.3.5", + "regexify-string": "^1.0.16", "spfx-uifabric-themes": "^0.8.5" }, "devDependencies": { + "@microsoft/microsoft-graph-types": "^2.1.0", "@microsoft/rush-stack-compiler-3.7": "0.2.3", "@microsoft/sp-build-web": "1.12.1", "@microsoft/sp-module-interfaces": "1.12.1", @@ -53,6 +60,7 @@ "@types/chart.js": "2.7.40", "@types/enzyme": "^2.8.12", "@types/es6-promise": "0.0.33", + "@types/he": "^1.1.2", "@types/jest": "25.2.3", "@types/lodash": "4.14.149", "@types/react": "16.9.36", diff --git a/src/ListItemComments.ts b/src/ListItemComments.ts new file mode 100755 index 000000000..6197b732f --- /dev/null +++ b/src/ListItemComments.ts @@ -0,0 +1 @@ +export * from './controls/listItemComments'; diff --git a/src/controls/LivePersona/ILivePersonaProps.ts b/src/controls/LivePersona/ILivePersonaProps.ts index b4e42fe79..15ba0db1d 100644 --- a/src/controls/LivePersona/ILivePersonaProps.ts +++ b/src/controls/LivePersona/ILivePersonaProps.ts @@ -1,11 +1,15 @@ import { BaseComponentContext} from "@microsoft/sp-component-base"; +import { ServiceScope } from "@microsoft/sp-core-library"; export interface ILivePersonatProps { /** * The Web Part context */ - context: BaseComponentContext; - + context?: BaseComponentContext; +/** + * The Web Part context + */ + serviceScope: ServiceScope; /** * The user UPN to use for the live information */ diff --git a/src/controls/LivePersona/LivePersona.tsx b/src/controls/LivePersona/LivePersona.tsx index 6794ff611..eb5c811de 100644 --- a/src/controls/LivePersona/LivePersona.tsx +++ b/src/controls/LivePersona/LivePersona.tsx @@ -4,15 +4,17 @@ import { useState } from "react"; import { Log } from "@microsoft/sp-core-library"; import { SPComponentLoader } from "@microsoft/sp-loader"; import { ILivePersonatProps} from '.'; +import { mergeStyles, mergeStyleSets } from "@fluentui/react"; const LIVE_PERSONA_COMPONENT_ID: string = "914330ee-2df2-4f6e-a858-30c23a812408"; + export const LivePersona: React.FunctionComponent = ( props: React.PropsWithChildren ) => { const [isComponentLoaded, setIsComponentLoaded] = useState(false); const sharedLibrary = useRef(); - const { upn, template, disableHover, context } = props; + const { upn, template, disableHover, context, serviceScope } = props; useEffect(() => { (async () => { @@ -21,24 +23,24 @@ export const LivePersona: React.FunctionComponent = ( sharedLibrary.current = await SPComponentLoader.loadComponentById(LIVE_PERSONA_COMPONENT_ID); setIsComponentLoaded(true); } catch (error) { - Log.error(`[LivePersona]`, error, context.serviceScope); + Log.error(`[LivePersona]`, error, serviceScope ?? context.serviceScope); } } })(); }, []); - let renderPersona: JSX.Element = null; - if (isComponentLoaded) { - renderPersona = createElement(sharedLibrary.current.LivePersonaCard, { - className: 'livePersonaCard', - clientScenario: 'livePersonaCard', - disableHover: disableHover, - hostAppPersonaInfo: { - PersonaType: 'User' - }, - upn: upn, - serviceScope: context.serviceScope, - }, createElement("div",{},template)); - } - return renderPersona; +let renderPersona: JSX.Element = null; +if (isComponentLoaded) { + renderPersona = createElement(sharedLibrary.current.LivePersonaCard, { + className: 'livePersonaCard', + clientScenario: 'livePersonaCard', + disableHover: disableHover, + hostAppPersonaInfo: { + PersonaType: 'User' + }, + upn: upn, + serviceScope: serviceScope ?? context.serviceScope, + }, createElement("div",{},template)); +} +return renderPersona; }; diff --git a/src/controls/listItemComments/ListItemComments.tsx b/src/controls/listItemComments/ListItemComments.tsx new file mode 100644 index 000000000..e4e655a70 --- /dev/null +++ b/src/controls/listItemComments/ListItemComments.tsx @@ -0,0 +1,47 @@ +import * as React from "react"; +import { ListItemCommentsStateProvider } from "./components/ListItemCommentsStateProvider"; +import { ServiceScope } from "@microsoft/sp-core-library"; +import { AppContext } from "./common"; +import { Theme } from "spfx-uifabric-themes"; // Don't remove this import is need to theme load form global var from window object +import { CommentsList } from "./components/Comments/CommentsList"; +import { Stack } from "@fluentui/react/lib/Stack"; +import { Text } from "@fluentui/react/lib/Text"; +export interface IListItemCommentsProps { + webUrl?: string; + listId: string; + itemId: string; + serviceScope: ServiceScope; + numberCommentsPerPage?: 5 | 10 | 15 | 20; + label?: string; +} +const theme = window.__themeState__.theme; +export const ListItemComments: React.FunctionComponent = ( + props: React.PropsWithChildren +) => { + const { webUrl, listId, itemId, serviceScope, numberCommentsPerPage, label } = props; + + if (!listId && !itemId && !serviceScope) return; + + return ( + <> + + + + {label} + + + + + + ); +}; diff --git a/src/controls/listItemComments/common/AppContext.ts b/src/controls/listItemComments/common/AppContext.ts new file mode 100644 index 000000000..6e6f1ad2a --- /dev/null +++ b/src/controls/listItemComments/common/AppContext.ts @@ -0,0 +1,5 @@ +import * as React from "react"; + +import { IAppContext } from "."; + +export const AppContext = React.createContext(undefined); diff --git a/src/controls/listItemComments/common/ECommentAction.ts b/src/controls/listItemComments/common/ECommentAction.ts new file mode 100644 index 000000000..1fd9bde23 --- /dev/null +++ b/src/controls/listItemComments/common/ECommentAction.ts @@ -0,0 +1,4 @@ +export enum ECommentAction { + "ADD" = "ADD", + "DELETE" = "DELETE" +} diff --git a/src/controls/listItemComments/common/IAppContext.ts b/src/controls/listItemComments/common/IAppContext.ts new file mode 100644 index 000000000..bdecc0d37 --- /dev/null +++ b/src/controls/listItemComments/common/IAppContext.ts @@ -0,0 +1,12 @@ +import { Theme } from "spfx-uifabric-themes"; +import { ServiceScope } from "@microsoft/sp-core-library"; + +export interface IAppContext { + theme: Theme; + serviceScope: ServiceScope; + webUrl: string; + listId: string; + itemId: string; + numberCommentsPerPage?: number; + label?:string; +} diff --git a/src/controls/listItemComments/common/constants.ts b/src/controls/listItemComments/common/constants.ts new file mode 100644 index 000000000..c7eacd957 --- /dev/null +++ b/src/controls/listItemComments/common/constants.ts @@ -0,0 +1,2 @@ +export const PHOTO_URL = "/_layouts/15/userphoto.aspx?size=M&accountname="; +export const TILE_HEIGHT: number = 70; diff --git a/src/controls/listItemComments/common/index.ts b/src/controls/listItemComments/common/index.ts new file mode 100644 index 000000000..fbef6e55c --- /dev/null +++ b/src/controls/listItemComments/common/index.ts @@ -0,0 +1,4 @@ +export * from './AppContext'; +export * from './ECommentAction'; +export * from './IAppContext'; +export * from './constants'; diff --git a/src/controls/listItemComments/components/AddComment/AddComment.tsx b/src/controls/listItemComments/components/AddComment/AddComment.tsx new file mode 100644 index 000000000..462f25009 --- /dev/null +++ b/src/controls/listItemComments/components/AddComment/AddComment.tsx @@ -0,0 +1,131 @@ +import { Stack } from "@fluentui/react/lib/Stack"; +import * as React from "react"; +import { useContext, useRef, useState } from "react"; +import { EListItemCommentsStateTypes, ListItemCommentsStateContext } from "./../ListItemCommentsStateProvider"; +import { IUserInfo } from "../../models/IUsersResults"; +import { MentionsInput, Mention, SuggestionDataItem, MentionItem } from "react-mentions"; +import { useCallback } from "react"; +import { useAddCommentStyles } from "./useAddCommentStyles"; +import { PHOTO_URL } from "../../common/constants"; +import { IconButton } from "@fluentui/react/lib/Button"; +import { Text} from "@fluentui/react/lib/Text"; +import { ECommentAction } from "../../common/ECommentAction"; +import { IAddCommentPayload } from "../../models/IAddCommentPayload"; +import { useMsGraphAPI } from "../.."; + +export interface IAddCommentProps {} + +export const AddComment: React.FunctionComponent = (props: IAddCommentProps) => { + const [commentText, setCommentText] = useState(""); + const { getUsers, getSuggestions } = useMsGraphAPI(); + const { reactMentionStyles, mentionsClasses, componentClasses } = useAddCommentStyles(); + const [singleLine, setSingleLine] = useState(true); + const { listItemCommentsState, setlistItemCommentsState } = useContext(ListItemCommentsStateContext); + let _addCommentText = useRef({ mentions: [], text: "" }); + + let sugestionsContainer = useRef(); + let _reactMentionStyles = reactMentionStyles; + + const _onChange = useCallback((event, newValue: string, newPlainTextValue: string, mentions: MentionItem[]) => { + let _reactMentionStyles = reactMentionStyles; + if (newValue) { + setSingleLine(false); + _reactMentionStyles["&multiLine"].control = { height: 63 }; + _addCommentText.current.text = newPlainTextValue; + _addCommentText.current.mentions = []; + for (let index = 0; index < mentions.length; index++) { + const mention = mentions[index]; + _addCommentText.current.text = _addCommentText.current.text.replace(mention.display, `@mention{${index}}`); + _addCommentText.current.mentions.push({ email: mention.id, name: mention.display.replace("@", "") }); + } + } else { + setSingleLine(true); + _reactMentionStyles["&multiLine"].control = { height: 35 }; + _addCommentText.current = { mentions: [], text: "" }; + } + setCommentText(newValue); + }, []); + + const _addComment = useCallback(() => { + setlistItemCommentsState({ type: EListItemCommentsStateTypes.SET_COMMENT_ACTION, payload: ECommentAction.ADD }); + setlistItemCommentsState({ type: EListItemCommentsStateTypes.SET_ADD_COMMENT, payload: _addCommentText.current }); + setSingleLine(true); + setCommentText(""); + }, []); + + const _searchData = (search: string, callback: (users: SuggestionDataItem[]) => void) => { + // Try to get sugested users when user type '@' + if (!search) { + getSuggestions() + .then((res) => res.users.map((user) => ({ display: user.displayName, id: user.mail }))) + .then(callback); + } else { + getUsers(search) + .then((res) => res.users.map((user) => ({ display: user.displayName, id: user.mail }))) + .then(callback); + } + }; + + const renderSugestion = useCallback((suggestion: SuggestionDataItem): React.ReactNode => { + const _user: IUserInfo = { + id: suggestion.id as string, + displayName: suggestion.display, + mail: suggestion.id as string, + }; + return ( + <> + + + + + + {_user.displayName} + + + {_user.mail} + + + + + + ); + }, []); + + return ( + <> + {/** Render Sugestions in the host element */} +
{ + sugestionsContainer.current = el; + }} + >
+
+ + `@${display}`} + className={mentionsClasses.mention} + /> + + + { + _addComment(); + }} + /> + +
+ + ); +}; diff --git a/src/controls/listItemComments/components/AddComment/index.ts b/src/controls/listItemComments/components/AddComment/index.ts new file mode 100644 index 000000000..231ba2cf9 --- /dev/null +++ b/src/controls/listItemComments/components/AddComment/index.ts @@ -0,0 +1,2 @@ +export * from './AddComment'; +export * from './useAddCommentStyles'; diff --git a/src/controls/listItemComments/components/AddComment/useAddCommentStyles.ts b/src/controls/listItemComments/components/AddComment/useAddCommentStyles.ts new file mode 100644 index 000000000..ab02ed852 --- /dev/null +++ b/src/controls/listItemComments/components/AddComment/useAddCommentStyles.ts @@ -0,0 +1,150 @@ +import * as React from "react"; +import { IDocumentCardStyles, IStackStyles, IStyle, mergeStyleSets } from "@fluentui/react"; +import { AppContext } from "../../common"; + +export const useAddCommentStyles = () => { + const { theme } = React.useContext(AppContext); + const itemContainerStyles: IStackStyles = { + root: { paddingTop: 0, paddingLeft: 20, paddingRight: 20, paddingBottom: 20 } as IStyle, + }; + + const deleteButtonContainerStyles: Partial = { + root: { + position: "absolute", + top: 0, + right: 0, + }, + }; + + const searchMentionContainerStyles: Partial = { + root: { + borderWidth: 1, + borderStyle: "solid", + borderColor: "silver", + width: 322, + ":focus": { + borderColor: theme.themePrimary, + }, + ":hover": { + borderColor: theme.themePrimary, + }, + }, + }; + + const documentCardUserStyles: Partial = { + root: { + marginTop: 2, + backgroundColor: theme?.white, + boxShadow: "0 5px 15px rgba(50, 50, 90, .1)", + ":hover": { + borderColor: theme.themePrimary, + backgroundColor: theme.neutralLighterAlt, + borderWidth: 1, + } as IStyle, + } as IStyle, + }; + + const componentClasses = mergeStyleSets({ + container: { + borderWidth: 1, + borderStyle: "solid", + display: "block", + borderColor: "silver", + overflow: "hidden", + width: 320, + ":focus": { + borderWidth: 2, + borderColor: theme.themePrimary, + }, + ":hover": { + borderWidth: 2, + borderColor: theme.themePrimary, + }, + } as IStyle, + }); + + const mentionsClasses = mergeStyleSets({ + mention: { + position: "relative", + zIndex: 9999, + color: theme.themePrimary, + pointerEvents: "none", + } as IStyle, + }); + + const reactMentionStyles = { + control: { + backgroundColor: "#fff", + fontSize: 12, + border: "none", + fontWeight: "normal", + outlineColor: theme.themePrimary, + borderRadius: 0, + } as IStyle, + "&multiLine": { + control: { + border: "none", + fontFamily: + '"Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue"', + minHeight: 35, + fontSize: 14, + fontWeight: 400, + borderRadius: 0, + } as IStyle, + highlighter: { + padding: 9, + border: "none", + borderWidth: 0, + borderRadius: 0, + } as IStyle, + input: { + padding: 9, + border: "none", + outline: "none", + } as IStyle, + }, + + "&singleLine": { + display: "inline-block", + height: 50, + outlineColor: theme.themePrimary, + border: "none", + highlighter: { + padding: 1, + border: "1px inset transparent", + }, + input: { + padding: 1, + width: "100%", + borderRadius: 0, + border: "none", + }, + }, + + suggestions: { + list: { + backgroundColor: "white", + border: "1px solid rgba(0,0,0,0.15)", + fontSize: 14, + }, + item: { + padding: "5px 15px", + borderBottom: "1px solid", + borderBottomColor: theme.themeLight, + "&focused": { + backgroundColor: theme.neutralLighterAlt, + }, + }, + }, + }; + + return { + documentCardUserStyles, + deleteButtonContainerStyles, + reactMentionStyles, + itemContainerStyles, + searchMentionContainerStyles, + mentionsClasses, + componentClasses, + }; +}; diff --git a/src/controls/listItemComments/components/Comments/CommentItem.tsx b/src/controls/listItemComments/components/Comments/CommentItem.tsx new file mode 100644 index 000000000..9cfb36620 --- /dev/null +++ b/src/controls/listItemComments/components/Comments/CommentItem.tsx @@ -0,0 +1,45 @@ +import * as React from "react"; +import { ReactNode, useMemo } from "react"; +import format from "date-fns/format"; +import parseISO from "date-fns/parseISO"; +import { ActivityItem, Link, Text } from "@fluentui/react"; +import { Stack } from "@fluentui/react/lib/Stack"; +import { IComment } from "./IComment"; +import { CommentText } from "./CommentText"; +import { isEmpty } from "lodash"; + +const PHOTO_URL = "/_layouts/15/userphoto.aspx?size=M&accountname="; + +export interface IRenderNotificationItemProps { + comment: IComment; +} + +export const CommentItem: React.FunctionComponent = ( + props: React.PropsWithChildren +) => { + if (isEmpty(props.comment)) return null; + const { author, createdDate, text, mentions } = props.comment; + + const activityDescription = useMemo((): ReactNode => { + const _activity: JSX.Element[] = []; + _activity.push( + + {author.name} + + ); + _activity.push(); + return _activity; + }, [mentions, text]); + + return ( + <> + + + + + ); +}; diff --git a/src/controls/listItemComments/components/Comments/CommentText.tsx b/src/controls/listItemComments/components/Comments/CommentText.tsx new file mode 100644 index 000000000..bbeb30880 --- /dev/null +++ b/src/controls/listItemComments/components/Comments/CommentText.tsx @@ -0,0 +1,73 @@ +import * as React from "react"; +import { useContext, useEffect, useState } from "react"; +import { Mention } from "./IComment"; +import { Text } from "@fluentui/react/lib/Text"; +import { LivePersona } from "../../../LivePersona"; +import { AppContext } from "../../common"; +import regexifyString from "regexify-string"; +import { Stack } from "@fluentui/react/lib/Stack"; +import { isArray, isObject } from "lodash"; +import he from 'he'; +export interface ICommentTextProps { + text: string; + mentions: Mention[]; +} + +export const CommentText: React.FunctionComponent = ( + props: React.PropsWithChildren +) => { + const [commentText, setCommentText] = useState(""); + const { theme, serviceScope } = useContext(AppContext); + const { text, mentions } = props; + const mentionsResults: Mention[] = mentions; + + useEffect(() => { + const hasMentions = mentions?.length ? true : false; + let result: string | JSX.Element[] = text; + if (hasMentions) { + result = regexifyString({ + pattern: /@mention{\d+}/g, + decorator: (match, index) => { + const mention = mentionsResults[index]; + const _name = `@${mention.name}`; + return ( + <> + {_name}} + /> + + ); + }, + input: text, + }) as JSX.Element[]; + } + setCommentText(result); + }, []); + + return ( + <> + + {isArray(commentText) ? ( + (commentText as any[]).map((el, i) => { + if (isObject(el)) { + return {el}; + } else { + const _el: string = el.trim(); + if (_el.length) { + return ( + + {he.decode(_el)} + + ); + } + } + }) + ) : ( + {he.decode(commentText)} + )} + + + ); +}; diff --git a/src/controls/listItemComments/components/Comments/CommentsList.tsx b/src/controls/listItemComments/components/Comments/CommentsList.tsx new file mode 100644 index 000000000..176d2c9f8 --- /dev/null +++ b/src/controls/listItemComments/components/Comments/CommentsList.tsx @@ -0,0 +1,169 @@ +import * as React from "react"; +import { useContext, useEffect, useRef } from "react"; +import { Stack } from "@fluentui/react/lib/Stack"; +import { useSpAPI } from "../../hooks"; +import { EListItemCommentsStateTypes, ListItemCommentsStateContext } from "../ListItemCommentsStateProvider"; +import { useListItemCommentsStyles } from "./useListItemCommentsStyles"; +import { IlistItemCommentsResults, IPageInfo } from "../../models"; +import { getScrollPosition } from "../../utils/utils"; +import { IErrorInfo } from "../ErrorInfo/IErrorInfo"; +import { RenderError } from "./RenderError"; +import { RenderSpinner } from "./RenderSpinner"; +import { Text } from "@fluentui/react/lib/Text"; +import { AddComment } from "../AddComment/AddComment"; +import { ECommentAction } from "../../common/ECommentAction"; +import { IAddCommentPayload } from "../../models/IAddCommentPayload"; +import { useCallback } from "react"; +import strings from "ControlStrings"; +import { RenderComments } from "./RenderComments"; + +export const CommentsList: React.FunctionComponent = () => { + const { listItemCommentsState, setlistItemCommentsState } = useContext(ListItemCommentsStateContext); + const { configurationListClasses } = useListItemCommentsStyles(); + const { getListItemComments, getNextPageOfComments, addComment, deleteComment } = useSpAPI(); + const { comments, isScrolling, pageInfo, commentAction, commentToAdd, selectedComment } = listItemCommentsState; + const { hasMore, nextLink } = pageInfo; + const scrollPanelRef = useRef(); + const { errorInfo } = listItemCommentsState; + + const _loadComments = useCallback(async () => { + try { + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_IS_LOADING, + payload: true, + }); + const _commentsResults: IlistItemCommentsResults = await getListItemComments(); + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_LIST_ITEM_COMMENTS, + payload: _commentsResults.comments, + }); + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_DATA_PAGE_INFO, + payload: { hasMore: _commentsResults.hasMore, nextLink: _commentsResults.nextLink } as IPageInfo, + }); + setlistItemCommentsState({ type: EListItemCommentsStateTypes.SET_COMMENT_ACTION, payload: undefined }); + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_IS_LOADING, + payload: false, + }); + } catch (error) { + const _errorInfo: IErrorInfo = { showError: true, error: error.message }; + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_ERROR_INFO, + payload: _errorInfo, + }); + } + }, [setlistItemCommentsState]); + + const _onAddComment = useCallback( + async (commentText: IAddCommentPayload) => { + try { + const _errorInfo: IErrorInfo = { showError: false, error: undefined }; + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_ERROR_INFO, + payload: _errorInfo, + }); + await addComment(commentText); + await _loadComments(); + } catch (error) { + const _errorInfo: IErrorInfo = { showError: true, error: error }; + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_ERROR_INFO, + payload: _errorInfo, + }); + } + }, + [setlistItemCommentsState, addComment, _loadComments] + ); + + const _onADeleteComment = useCallback( + async (commentId: number) => { + if (!commentId) return; + try { + const _errorInfo: IErrorInfo = { showError: false, error: undefined }; + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_ERROR_INFO, + payload: _errorInfo, + }); + + await deleteComment(commentId); + await _loadComments(); + } catch (error) { + const _errorInfo: IErrorInfo = { showError: true, error: error }; + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_ERROR_INFO, + payload: _errorInfo, + }); + } + }, + [setlistItemCommentsState, _loadComments] + ); + + useEffect(() => { + switch (commentAction) { + case ECommentAction.ADD: + (async () => { + // Add new comment + await _onAddComment(commentToAdd); + })(); + break; + case ECommentAction.DELETE: + (async () => { + // delete comment + const commentId = Number(selectedComment.id); + await _onADeleteComment(commentId); + })(); + break; + default: + break; + } + }, [commentAction, selectedComment, commentToAdd, _onAddComment, _onADeleteComment]); + + useEffect(() => { + (async () => { + await _loadComments(); + })(); + }, [_loadComments]); + + const handleScroll = React.useCallback(async () => { + const _scrollPosition = getScrollPosition(scrollPanelRef.current); + if (isScrolling) return; + if (hasMore && _scrollPosition > 90) { + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_IS_SCROLLING, + payload: true, + }); + const _commentsResults: IlistItemCommentsResults = await getNextPageOfComments(nextLink); + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_LIST_ITEM_COMMENTS, + payload: [...comments, ..._commentsResults.comments], + }); + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_DATA_PAGE_INFO, + payload: { hasMore: _commentsResults.hasMore, nextLink: _commentsResults.nextLink } as IPageInfo, + }); + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_IS_SCROLLING, + payload: false, + }); + } + }, [hasMore, nextLink, isScrolling, setlistItemCommentsState]); + + return ( + <> + + + + + {strings.ListItemCommentsLabel} + +
+ + + +
+
+ {} + + ); +}; diff --git a/src/controls/listItemComments/components/Comments/IComment.ts b/src/controls/listItemComments/components/Comments/IComment.ts new file mode 100644 index 000000000..b58ed53f5 --- /dev/null +++ b/src/controls/listItemComments/components/Comments/IComment.ts @@ -0,0 +1,63 @@ + +export interface IComment { + __metadata: Metadata; + likedBy: LikedBy; + replies: Replies; + author: Author; + createdDate: string; + id: string; + isLikedByUser: boolean; + isReply: boolean; + itemId: number; + likeCount: number; + listId: string; + mentions: Mention[]; + parentId: string; + replyCount: number; + text: string; +} + +export interface Mentions { + __metadata: string; + results: Mention[]; +} + +export interface Mention { + email: string; + id: number; + loginName: string; + name: string; +} + +export interface Author { + __metadata: string; + email: string; + expiration?: string; + id: number; + isActive: boolean; + isExternal: boolean; + jobTitle?: string; + loginName: string; + name: string; + principalType: number; + userId?: string; + userPrincipalName?: string; +} + +interface Replies { + results: any[]; +} + +interface LikedBy { + __deferred: Deferred; +} + +interface Deferred { + uri: string; +} + +interface Metadata { + id: string; + uri: string; + type: string; +} diff --git a/src/controls/listItemComments/components/Comments/RenderComments.tsx b/src/controls/listItemComments/components/Comments/RenderComments.tsx new file mode 100644 index 000000000..ad19485ae --- /dev/null +++ b/src/controls/listItemComments/components/Comments/RenderComments.tsx @@ -0,0 +1,78 @@ +import { IconButton } from "office-ui-fabric-react/lib/Button"; +import { DocumentCard, DocumentCardDetails } from "office-ui-fabric-react/lib/DocumentCard"; +import { Stack } from "office-ui-fabric-react/lib/Stack"; +import * as React from "react"; +import { useCallback, useState } from "react"; +import { useContext } from "react"; +import { ConfirmDelete } from "../ConfirmDelete/ConfirmDelete"; +import { EListItemCommentsStateTypes, ListItemCommentsStateContext } from "../ListItemCommentsStateProvider"; +import { CommentItem } from "./CommentItem"; +import { IComment } from "./IComment"; +import { RenderNoComments } from "./RenderNoComments"; +import { RenderSpinner } from "./RenderSpinner"; +import { useListItemCommentsStyles } from "./useListItemCommentsStyles"; +import { useBoolean } from "@fluentui/react-hooks"; +import { List } from "@fluentui/react/lib/List"; +import { ECommentAction } from "../.."; + +export interface IRenderCommentsProps {} + +export const RenderComments: React.FunctionComponent = () => { + const { listItemCommentsState, setlistItemCommentsState } = useContext(ListItemCommentsStateContext); + const { documentCardStyles, itemContainerStyles, deleteButtonContainerStyles } = useListItemCommentsStyles(); + const { comments, isLoading, selectedComment } = listItemCommentsState; + + const [hideDialog, { toggle: setHideDialog }] = useBoolean(true); + + const onRenderCell = useCallback( + (comment: IComment, index: number): JSX.Element => { + return ( + + + { + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_SELECTED_COMMENT, + payload: comment, + }); + setHideDialog(); + }} + > + + + + + + + + ); + }, + [comments] + ); + + return ( + <> + {isLoading ? : } + { + if (deleteComment) { + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_COMMENT_ACTION, + payload: ECommentAction.DELETE, + }); + } + setHideDialog(); + }} + /> + + ); +}; diff --git a/src/controls/listItemComments/components/Comments/RenderError.tsx b/src/controls/listItemComments/components/Comments/RenderError.tsx new file mode 100644 index 000000000..8150e281d --- /dev/null +++ b/src/controls/listItemComments/components/Comments/RenderError.tsx @@ -0,0 +1,33 @@ +import { Guid } from "@microsoft/sp-core-library"; +import { DocumentCard, DocumentCardDetails } from "office-ui-fabric-react/lib/DocumentCard"; +import { Stack } from "office-ui-fabric-react/lib/Stack"; +import * as React from "react"; +import { ErrorInfo } from "../ErrorInfo"; +import { IErrorInfo } from "../ErrorInfo/IErrorInfo"; +import { useListItemCommentsStyles } from "./useListItemCommentsStyles"; +export interface IRenderErrorProps { + errorInfo: IErrorInfo; +} +export const RenderError: React.FunctionComponent = ( + props: React.PropsWithChildren +) => { + const { showError, error } = props.errorInfo || ({} as IErrorInfo); + const { documentCardStyles } = useListItemCommentsStyles(); + + if (!showError) return null; + return ( + + + + + + + + ); +}; diff --git a/src/controls/listItemComments/components/Comments/RenderNoComments.tsx b/src/controls/listItemComments/components/Comments/RenderNoComments.tsx new file mode 100644 index 000000000..11dfdbfc0 --- /dev/null +++ b/src/controls/listItemComments/components/Comments/RenderNoComments.tsx @@ -0,0 +1,28 @@ +import { Guid } from "@microsoft/sp-core-library"; +import { DocumentCard, DocumentCardDetails } from "office-ui-fabric-react/lib/DocumentCard"; +import { Stack } from "office-ui-fabric-react/lib/Stack"; +import * as React from "react"; +import { Text } from "office-ui-fabric-react/lib/Text"; +import { useListItemCommentsStyles } from "./useListItemCommentsStyles"; +import strings from "ControlStrings"; + +export const RenderNoComments: React.FunctionComponent = () => { + const { documentCardStyles } = useListItemCommentsStyles(); + return ( + <> + + + + {strings.ListItemCommentsNoCommentsLabel} + + + + + ); +}; diff --git a/src/controls/listItemComments/components/Comments/RenderSpinner.tsx b/src/controls/listItemComments/components/Comments/RenderSpinner.tsx new file mode 100644 index 000000000..78020509c --- /dev/null +++ b/src/controls/listItemComments/components/Comments/RenderSpinner.tsx @@ -0,0 +1,31 @@ +import { Spinner } from "@fluentui/react/lib/Spinner"; +import { Guid } from "@microsoft/sp-core-library"; +import { DocumentCard, DocumentCardDetails } from "office-ui-fabric-react/lib/DocumentCard"; +import { SpinnerSize } from "office-ui-fabric-react/lib/Spinner"; +import { Stack } from "office-ui-fabric-react/lib/Stack"; +import * as React from "react"; +import { useContext } from "react"; +import { ListItemCommentsStateContext } from "../ListItemCommentsStateProvider"; +import { useListItemCommentsStyles } from "./useListItemCommentsStyles"; + +export const RenderSpinner: React.FunctionComponent = () => { + const { documentCardStyles } = useListItemCommentsStyles(); + const { listItemCommentsState } = useContext(ListItemCommentsStateContext); + const { isScrolling , isLoading} = listItemCommentsState; + if (!isScrolling && !isLoading) return null; + return ( + + + + + + + + ); +}; diff --git a/src/controls/listItemComments/components/Comments/RenderUser/RenderUser.tsx b/src/controls/listItemComments/components/Comments/RenderUser/RenderUser.tsx new file mode 100644 index 000000000..95e9e1bf0 --- /dev/null +++ b/src/controls/listItemComments/components/Comments/RenderUser/RenderUser.tsx @@ -0,0 +1,41 @@ +import { Guid } from "@microsoft/sp-core-library"; +import { DocumentCard, DocumentCardDetails } from "office-ui-fabric-react/lib/DocumentCard"; +import { Persona } from "office-ui-fabric-react/lib/Persona"; +import { Stack } from "office-ui-fabric-react/lib/Stack"; +import * as React from "react"; +import { IUserInfo } from "../../../models/IUsersResults"; +import { useListItemCommentsStyles } from "../useListItemCommentsStyles"; +import { PHOTO_URL } from "./../../../common/constants"; + +export interface IRenderUserProps { + user: IUserInfo; +} +export const RenderUser: React.FunctionComponent = ( + props: React.PropsWithChildren +) => { + const { user } = props; + const { documentCardUserStyles, renderUserContainerStyles } = useListItemCommentsStyles(); + + return ( + <> + + + + + + + + + ); +}; diff --git a/src/controls/listItemComments/components/Comments/RenderUser/index.ts b/src/controls/listItemComments/components/Comments/RenderUser/index.ts new file mode 100644 index 000000000..59e2c1aa7 --- /dev/null +++ b/src/controls/listItemComments/components/Comments/RenderUser/index.ts @@ -0,0 +1 @@ +export * from './RenderUser'; diff --git a/src/controls/listItemComments/components/Comments/useListItemCommentsStyles.ts b/src/controls/listItemComments/components/Comments/useListItemCommentsStyles.ts new file mode 100644 index 000000000..5b1547c45 --- /dev/null +++ b/src/controls/listItemComments/components/Comments/useListItemCommentsStyles.ts @@ -0,0 +1,129 @@ +import * as React from "react"; +import { + IDocumentCardStyles, + IProcessedStyleSet, + IStackStyles, + IStyle, + mergeStyles, + mergeStyleSets, +} from "@fluentui/react"; +import { AppContext } from "../../common"; +import { IScrollablePaneStyles } from "office-ui-fabric-react/lib/ScrollablePane"; +import { TILE_HEIGHT } from "../../common/constants"; + +interface returnObjectStyles { + itemContainerStyles: IStackStyles; + deleteButtonContainerStyles: Partial; + userListContainerStyles: Partial; + renderUserContainerStyles: Partial; + documentCardStyles: Partial; + documentCardDeleteStyles: Partial; + documentCardUserStyles: Partial; + configurationListClasses: any; +} + +export const useListItemCommentsStyles = (): returnObjectStyles => { + const { theme, numberCommentsPerPage } = React.useContext(AppContext); + // Calc Height List tiles Container Based on number Items per Page + const tilesHeight: number = numberCommentsPerPage + ? (numberCommentsPerPage < 5 ? 5 : numberCommentsPerPage) * TILE_HEIGHT + 35 + : 7 * TILE_HEIGHT; + + const itemContainerStyles: IStackStyles = { + root: { paddingTop: 0, paddingLeft: 20, paddingRight: 20, paddingBottom: 20 } as IStyle, + }; + + const deleteButtonContainerStyles: Partial = { + root: { + position: "absolute", + top: 0, + right: 0, + }, + }; + + const userListContainerStyles: Partial = { + root: { paddingLeft: 2, paddingRight: 2, paddingBottom: 2, minWidth: 206 }, + }; + + const renderUserContainerStyles: Partial = { + root: { paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }, + }; + const documentCardStyles: Partial = { + root: { + marginBottom: 7, + width: 322, + backgroundColor: theme.neutralLighterAlt, + ":hover": { + borderColor: theme.themePrimary, + borderWidth: 1, + } as IStyle, + } as IStyle, + }; + + const documentCardDeleteStyles: Partial = { + root: { + marginBottom: 5, + backgroundColor: theme.neutralLighterAlt, + ":hover": { + borderColor: theme.themePrimary, + borderWidth: 1, + } as IStyle, + } as IStyle, + }; + + const documentCardUserStyles: Partial = { + root: { + marginTop: 2, + backgroundColor: theme?.white, + boxShadow: "0 5px 15px rgba(50, 50, 90, .1)", + + ":hover": { + borderColor: theme.themePrimary, + backgroundColor: theme.neutralLighterAlt, + borderWidth: 1, + } as IStyle, + } as IStyle, + }; + + const configurationListClasses = mergeStyleSets({ + listIcon: mergeStyles({ + fontSize: 18, + width: 18, + height: 18, + color: theme.themePrimary, + }), + nolistItemIcon: mergeStyles({ + fontSize: 28, + width: 28, + height: 28, + color: theme.themePrimary, + }), + divContainer: { + display: "block", + } as IStyle, + titlesContainer: { + height: tilesHeight, + marginBottom: 10, + display: "flex", + marginTop: 15, + overflow: "auto", + "&::-webkit-scrollbar-thumb": { + backgroundColor: theme.neutralLighter, + }, + "&::-webkit-scrollbar": { + width: 5, + }, + } as IStyle, + }); + + return { + itemContainerStyles, + deleteButtonContainerStyles, + userListContainerStyles, + renderUserContainerStyles, + documentCardStyles, + documentCardDeleteStyles, + documentCardUserStyles, + configurationListClasses, + }; +}; diff --git a/src/controls/listItemComments/components/ConfirmDelete/ConfirmDelete.tsx b/src/controls/listItemComments/components/ConfirmDelete/ConfirmDelete.tsx new file mode 100644 index 000000000..212020066 --- /dev/null +++ b/src/controls/listItemComments/components/ConfirmDelete/ConfirmDelete.tsx @@ -0,0 +1,68 @@ +import * as React from "react"; +import { useContext } from "react"; +import { ListItemCommentsStateContext } from "../ListItemCommentsStateProvider"; +import { Dialog, DialogType, DialogFooter } from "@fluentui/react/lib/Dialog"; +import { PrimaryButton, DefaultButton } from "@fluentui/react/lib/Button"; +import { CommentItem } from "../Comments/CommentItem"; +import { DocumentCard } from "office-ui-fabric-react/lib/components/DocumentCard"; +import { DocumentCardDetails } from "@fluentui/react/lib/DocumentCard"; +import { Stack } from "@fluentui/react/lib/Stack"; +import { useListItemCommentsStyles } from "../Comments/useListItemCommentsStyles"; +import { IDialogContentStyles } from "office-ui-fabric-react"; +import strings from "ControlStrings"; +export interface IConfirmDeleteProps { + hideDialog: boolean; + onDismiss: (deleteComment: boolean) => void; +} + +export const ConfirmDelete: React.FunctionComponent = ( + props: React.PropsWithChildren +) => { + const { listItemCommentsState, setlistItemCommentsState } = useContext(ListItemCommentsStateContext); + const { documentCardDeleteStyles, itemContainerStyles } = useListItemCommentsStyles(); + const { hideDialog, onDismiss } = props; + const { selectedComment } = listItemCommentsState; + const stylesSubText: Partial = { + subText: { fontWeight: 600 }, + }; + + const modelProps = { + isBlocking: false, + styles: { main: { maxWidth: 450 } }, + }; + const dialogContentProps = { + type: DialogType.largeHeader, + title: strings.ListItemCommentsDialogDeleteTitle, + styles: stylesSubText, + subText: strings.ListItemCommentDIalogDeleteSubText, + }; + return ( + <> + + + ); +}; diff --git a/src/controls/listItemComments/components/ConfirmDelete/index.ts b/src/controls/listItemComments/components/ConfirmDelete/index.ts new file mode 100644 index 000000000..86c8276ff --- /dev/null +++ b/src/controls/listItemComments/components/ConfirmDelete/index.ts @@ -0,0 +1 @@ +export * from './ConfirmDelete'; diff --git a/src/controls/listItemComments/components/ErrorInfo/ErrorInfo.tsx b/src/controls/listItemComments/components/ErrorInfo/ErrorInfo.tsx new file mode 100644 index 000000000..f6a14cfaf --- /dev/null +++ b/src/controls/listItemComments/components/ErrorInfo/ErrorInfo.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import { MessageBar, MessageBarType } from "@fluentui/react/lib/MessageBar"; +import { Stack } from "@fluentui/react/lib/Stack"; +export interface IErrorInfoProps { + error: Error; + showError: boolean; + showStack?: boolean; +} + +export const ErrorInfo: React.FunctionComponent = ( + props: React.PropsWithChildren +) => { + const { error, showStack, showError } = props; + return ( + <> + {showError ? ( + + + {error.message} + {showStack ? error.stack : ""} + + + ) : null} + + ); +}; diff --git a/src/controls/listItemComments/components/ErrorInfo/IErrorInfo.ts b/src/controls/listItemComments/components/ErrorInfo/IErrorInfo.ts new file mode 100644 index 000000000..575256246 --- /dev/null +++ b/src/controls/listItemComments/components/ErrorInfo/IErrorInfo.ts @@ -0,0 +1,4 @@ +export interface IErrorInfo { + error:Error; + showError:boolean; +} diff --git a/src/controls/listItemComments/components/ErrorInfo/index.ts b/src/controls/listItemComments/components/ErrorInfo/index.ts new file mode 100644 index 000000000..39346c88c --- /dev/null +++ b/src/controls/listItemComments/components/ErrorInfo/index.ts @@ -0,0 +1,2 @@ +export * from './ErrorInfo'; +export * from './IErrorInfo'; diff --git a/src/controls/listItemComments/components/ListItemCommentsStateProvider/EListItemCommentsStateTypes.ts b/src/controls/listItemComments/components/ListItemCommentsStateProvider/EListItemCommentsStateTypes.ts new file mode 100644 index 000000000..f2927e492 --- /dev/null +++ b/src/controls/listItemComments/components/ListItemCommentsStateProvider/EListItemCommentsStateTypes.ts @@ -0,0 +1,10 @@ +export enum EListItemCommentsStateTypes { + "SET_ERROR_INFO" = "SET_ERROR_INFO", + "SET_LIST_ITEM_COMMENTS" = "SET_LIST_ITEM_COMMENTS", + "SET_IS_LOADING" = "SET_IS_LOADING", + "SET_IS_SCROLLING" = "SET_IS_SCROLLING", + "SET_DATA_PAGE_INFO" = "SET_DATA_PAGE_INFO", + "SET_COMMENT_ACTION" = "SET_COMMENT_ACTION", + "SET_ADD_COMMENT" = "SET_ADD_COMMENT", + "SET_SELECTED_COMMENT" = "SET_SELECTED_COMMENT", +} diff --git a/src/controls/listItemComments/components/ListItemCommentsStateProvider/IListItemCommentsState.ts b/src/controls/listItemComments/components/ListItemCommentsStateProvider/IListItemCommentsState.ts new file mode 100644 index 000000000..c10affe41 --- /dev/null +++ b/src/controls/listItemComments/components/ListItemCommentsStateProvider/IListItemCommentsState.ts @@ -0,0 +1,19 @@ + + +import { IErrorInfo } from "../ErrorInfo/IErrorInfo"; +import { IComment } from "../Comments/IComment"; +import { IPageInfo } from "../../models"; +import { ECommentAction } from "../../common/ECommentAction"; +import { IAddCommentPayload } from "../../models/IAddCommentPayload"; + +// Global State (Store) +export interface IListItemCommentsState { + errorInfo: IErrorInfo | undefined; + comments: IComment[]; + isLoading: boolean; + isScrolling: boolean; + pageInfo: IPageInfo; + commentAction: ECommentAction; + commentToAdd: IAddCommentPayload; + selectedComment: IComment; +} diff --git a/src/controls/listItemComments/components/ListItemCommentsStateProvider/IListItemCommentsStateContext.ts b/src/controls/listItemComments/components/ListItemCommentsStateProvider/IListItemCommentsStateContext.ts new file mode 100644 index 000000000..1f2a4f0ff --- /dev/null +++ b/src/controls/listItemComments/components/ListItemCommentsStateProvider/IListItemCommentsStateContext.ts @@ -0,0 +1,6 @@ +import { EListItemCommentsStateTypes } from "./EListItemCommentsStateTypes"; +import { IListItemCommentsState } from "./IListItemCommentsState"; +export interface IListItemCommentsStateContext { + listItemCommentsState: IListItemCommentsState; + setlistItemCommentsState: React.Dispatch<{type:EListItemCommentsStateTypes, payload: unknown}>; +} diff --git a/src/controls/listItemComments/components/ListItemCommentsStateProvider/ListItemCommentsStateProvider.tsx b/src/controls/listItemComments/components/ListItemCommentsStateProvider/ListItemCommentsStateProvider.tsx new file mode 100644 index 000000000..21fcdd9b5 --- /dev/null +++ b/src/controls/listItemComments/components/ListItemCommentsStateProvider/ListItemCommentsStateProvider.tsx @@ -0,0 +1,36 @@ +import React, { createContext, useReducer } from "react"; +import { ListItemCommentsStateReducer } from "./ListItemCommentsStateReducer"; +import { IListItemCommentsState } from "./IListItemCommentsState"; +import { IListItemCommentsStateContext } from "./IListItemCommentsStateContext"; +import { IPageInfo } from "../../models/IPageInfo"; +import { IAddCommentPayload } from "../../models/IAddCommentPayload"; +import { IComment } from "../Comments/IComment"; +// Reducer +// Initial State (Store ) +const initialState: IListItemCommentsState = { + errorInfo: undefined, + comments: [], + isLoading: false, + isScrolling: false, + pageInfo: {} as IPageInfo, + commentAction: undefined, + commentToAdd: {} as IAddCommentPayload, + selectedComment: {} as IComment, +}; + +const stateInit: IListItemCommentsStateContext = { + listItemCommentsState: initialState, + setlistItemCommentsState: ({}) => {return;}, +}; + +// (store) +export const ListItemCommentsStateContext = createContext(stateInit); +export const ListItemCommentsStateProvider = (props: { children: React.ReactNode }): JSX.Element => { + const [listItemCommentsState, setlistItemCommentsState] = useReducer(ListItemCommentsStateReducer, initialState); + + return ( + + {props.children} + + ); +}; diff --git a/src/controls/listItemComments/components/ListItemCommentsStateProvider/ListItemCommentsStateReducer.ts b/src/controls/listItemComments/components/ListItemCommentsStateProvider/ListItemCommentsStateReducer.ts new file mode 100644 index 000000000..58ba4456d --- /dev/null +++ b/src/controls/listItemComments/components/ListItemCommentsStateProvider/ListItemCommentsStateReducer.ts @@ -0,0 +1,34 @@ +import { IErrorInfo } from "../ErrorInfo/IErrorInfo"; +import { IComment } from "../Comments/IComment"; +import { EListItemCommentsStateTypes } from "./EListItemCommentsStateTypes"; +import { IListItemCommentsState } from "./IListItemCommentsState"; +import { IPageInfo } from "../../models/IPageInfo"; +import { ECommentAction } from "../../common/ECommentAction"; +import { IAddCommentPayload } from "../../models/IAddCommentPayload"; + +// Reducer +export const ListItemCommentsStateReducer = ( + state: IListItemCommentsState, + action: { type: EListItemCommentsStateTypes; payload: unknown } +): IListItemCommentsState => { + switch (action.type) { + case EListItemCommentsStateTypes.SET_ERROR_INFO: + return { ...state, errorInfo: action.payload as IErrorInfo }; + case EListItemCommentsStateTypes.SET_LIST_ITEM_COMMENTS: + return { ...state, comments: action.payload as IComment[] }; + case EListItemCommentsStateTypes.SET_IS_LOADING: + return { ...state, isLoading: action.payload as boolean }; + case EListItemCommentsStateTypes.SET_IS_SCROLLING: + return { ...state, isScrolling: action.payload as boolean }; + case EListItemCommentsStateTypes.SET_DATA_PAGE_INFO: + return { ...state, pageInfo: action.payload as IPageInfo }; + case EListItemCommentsStateTypes.SET_COMMENT_ACTION: + return { ...state, commentAction: action.payload as ECommentAction }; + case EListItemCommentsStateTypes.SET_ADD_COMMENT: + return { ...state, commentToAdd: action.payload as IAddCommentPayload }; + case EListItemCommentsStateTypes.SET_SELECTED_COMMENT: + return { ...state, selectedComment: action.payload as IComment }; + default: + return state; + } +}; diff --git a/src/controls/listItemComments/components/ListItemCommentsStateProvider/index.ts b/src/controls/listItemComments/components/ListItemCommentsStateProvider/index.ts new file mode 100644 index 000000000..bac3ec4a4 --- /dev/null +++ b/src/controls/listItemComments/components/ListItemCommentsStateProvider/index.ts @@ -0,0 +1,5 @@ +export * from './EListItemCommentsStateTypes'; +export * from './IListItemCommentsState'; +export * from './IListItemCommentsStateContext'; +export * from './ListItemCommentsStateProvider'; +export * from './ListItemCommentsStateReducer'; diff --git a/src/controls/listItemComments/components/index.ts b/src/controls/listItemComments/components/index.ts new file mode 100644 index 000000000..e9d903390 --- /dev/null +++ b/src/controls/listItemComments/components/index.ts @@ -0,0 +1,4 @@ +export * from './AddComment'; +export * from './ConfirmDelete'; +export * from './ErrorInfo'; +export * from './ListItemCommentsStateProvider'; diff --git a/src/controls/listItemComments/hooks/index.ts b/src/controls/listItemComments/hooks/index.ts new file mode 100644 index 000000000..bf68cd8ba --- /dev/null +++ b/src/controls/listItemComments/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useMsGraphAPI'; +export * from './useSpAPI'; diff --git a/src/controls/listItemComments/hooks/useMsGraphAPI.ts b/src/controls/listItemComments/hooks/useMsGraphAPI.ts new file mode 100644 index 000000000..0992933fd --- /dev/null +++ b/src/controls/listItemComments/hooks/useMsGraphAPI.ts @@ -0,0 +1,91 @@ +import { AppContext } from "../common"; +import { useContext, useCallback } from "react"; +import { MSGraphClientFactory, MSGraphClient } from "@microsoft/sp-http"; +import { Person } from "@microsoft/microsoft-graph-types"; +import { IUserInfo, IUsersResults } from "../models/IUsersResults"; + +interface returnObject { + getUsers: (search: string) => Promise; + getUsersNextPage: (nextLink: string) => Promise; + getSuggestions: () => Promise; +} + +export const useMsGraphAPI = (): returnObject => { + const { serviceScope } = useContext(AppContext); + let _msGraphClient: MSGraphClient = undefined; + serviceScope.whenFinished(async () => { + _msGraphClient = await serviceScope.consume(MSGraphClientFactory.serviceKey).getClient(); + }); + const getSuggestions = useCallback(async (): Promise => { + if (!_msGraphClient) return; + const _users: IUserInfo[] = []; + + const suggestedUsersResults = (await _msGraphClient + .api(`me/people`) + .header("ConsistencyLevel", "eventual") + .filter(`personType/class eq 'Person' and personType/subclass eq 'OrganizationUser'`) + .orderby(`displayName`) + .get()) as any; + console.log("rs", suggestedUsersResults); + const _sugestions: Person[] = suggestedUsersResults.value as Person[]; + for (const sugestion of _sugestions) { + _users.push({ + displayName: sugestion.displayName, + givenName: sugestion.givenName, + id: sugestion.id, + mail: sugestion.scoredEmailAddresses[0].address, + }); + } + + const returnInfo: IUsersResults = { + users: _users, + hasMore: false, + nextLink: undefined, + }; + return returnInfo; + }, [serviceScope, MSGraphClientFactory]); + + const getUsers = useCallback( + async (search: string): Promise => { + if (!_msGraphClient || !search) return; + let _filter = ""; + + if (search.length) { + _filter = `mail ne null AND (startswith(mail,'${search}') OR startswith(displayName,'${search}'))`; + } + + const usersResults = await _msGraphClient + .api(`/users`) + .header("ConsistencyLevel", "eventual") + .filter(_filter) + .orderby(`displayName`) + .count(true) + .top(25) + .get(); + + const returnInfo: IUsersResults = { + users: usersResults.value, + hasMore: usersResults["@odata.nextLink"] ? true : false, + nextLink: usersResults["@odata.nextLink"] ?? undefined, + }; + return returnInfo; + }, + [serviceScope, MSGraphClientFactory] + ); + + const getUsersNextPage = useCallback( + async (nextLink: string): Promise => { + if (!_msGraphClient) return; + const usersResults = await _msGraphClient.api(`${nextLink}`).get(); + const returnInfo: IUsersResults = { + users: usersResults.value, + hasMore: usersResults["@odata.nextLink"] ? true : false, + nextLink: usersResults["@odata.nextLink"] ?? undefined, + }; + return returnInfo; + }, + [serviceScope, MSGraphClientFactory] + ); + + return { getUsers, getUsersNextPage, getSuggestions }; +}; diff --git a/src/controls/listItemComments/hooks/useSpAPI.ts b/src/controls/listItemComments/hooks/useSpAPI.ts new file mode 100644 index 000000000..fffb47961 --- /dev/null +++ b/src/controls/listItemComments/hooks/useSpAPI.ts @@ -0,0 +1,106 @@ +import { AppContext } from "../common"; +import { useContext, useCallback } from "react"; +import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from "@microsoft/sp-http"; +import { IlistItemCommentsResults } from "./../models"; +import { IAddCommentPayload } from "../models/IAddCommentPayload"; +import { IComment } from "../components/Comments/IComment"; +import { PageContext } from "@microsoft/sp-page-context"; +interface returnObject { + getListItemComments: () => Promise; + getNextPageOfComments: (nextLink: string) => Promise; + addComment: (comment: IAddCommentPayload) => Promise; + deleteComment: (commentId: number) => Promise; +} + +export const useSpAPI = (): returnObject => { + const { serviceScope, webUrl, listId, itemId, numberCommentsPerPage } = useContext(AppContext); + let _webUrl: string = ""; + serviceScope.whenFinished(async () => { + _webUrl = serviceScope.consume(PageContext.serviceKey).web.absoluteUrl; + }); + //https://contoso.sharepoint.com/sites/ThePerspective/_api/web/lists(@a1)/GetItemById(@a2)/Comments(@a3)?@a1=%27%7BE738C4B3%2D6CFF%2D493A%2DA8DA%2DDBBF4732E3BF%7D%27&@a2=%2729%27&@a3=%273%27 + + const deleteComment = useCallback( + async (commentId: number): Promise => { + const spHttpClient = serviceScope.consume(SPHttpClient.serviceKey); + if (!spHttpClient) return; + const _endPointUrl = `${ + webUrl ?? _webUrl + }/_api/web/lists(@a1)/GetItemById(@a2)/Comments(@a3)?@a1='${listId}'&@a2='${itemId}'&@a3='${commentId}'`; + const spOpts: ISPHttpClientOptions = { + method: "DELETE", + }; + const _deleteResults: SPHttpClientResponse = await spHttpClient.fetch( + `${_endPointUrl}`, + SPHttpClient.configurations.v1, + spOpts + ); + return; + }, + [serviceScope] + ); + + const addComment = useCallback( + async (comment: IAddCommentPayload): Promise => { + const spHttpClient = serviceScope.consume(SPHttpClient.serviceKey); + if (!spHttpClient) return; + const _endPointUrl = `${ + webUrl ?? _webUrl + }/_api/web/lists(@a1)/GetItemById(@a2)/Comments()?@a1='${listId}'&@a2='${itemId}'`; + const spOpts: ISPHttpClientOptions = { + body: `{ "text": "${comment.text}", "mentions": ${JSON.stringify(comment.mentions)}}`, + }; + const _listResults: SPHttpClientResponse = await spHttpClient.post( + `${_endPointUrl}`, + SPHttpClient.configurations.v1, + spOpts + ); + const _commentResults: IComment = (await _listResults.json()) as IComment; + return _commentResults; + }, + [serviceScope] + ); + + const getListItemComments = useCallback(async (): Promise => { + const spHttpClient = serviceScope.consume(SPHttpClient.serviceKey); + if (!spHttpClient) return; + const _endPointUrl = `${ + webUrl ?? _webUrl + }/_api/web/lists(@a1)/GetItemById(@a2)/GetComments()?@a1='${listId}'&@a2='${itemId}'&$top=${ + numberCommentsPerPage ?? 10 + }`; + const _listResults: SPHttpClientResponse = await spHttpClient.get( + `${_endPointUrl}`, + SPHttpClient.configurations.v1 + ); + const _commentsResults = (await _listResults.json()) as any; + const _returnComments: IlistItemCommentsResults = { + comments: _commentsResults.value, + hasMore: _commentsResults["@odata.nextLink"] ? true : false, + nextLink: _commentsResults["@odata.nextLink"] ?? undefined, + }; + return _returnComments; + }, [serviceScope]); + + const getNextPageOfComments = useCallback( + async (nextLink: string): Promise => { + const spHttpClient = serviceScope.consume(SPHttpClient.serviceKey); + if (!spHttpClient || !nextLink) return; + const _endPointUrl = nextLink; + const _listResults: SPHttpClientResponse = await spHttpClient.get( + `${_endPointUrl}`, + SPHttpClient.configurations.v1 + ); + const _commentsResults = (await _listResults.json()) as any; + const _returnComments: IlistItemCommentsResults = { + comments: _commentsResults.value, + hasMore: _commentsResults["@odata.nextLink"] ? true : false, + nextLink: _commentsResults["@odata.nextLink"] ?? undefined, + }; + return _returnComments; + }, + [serviceScope] + ); + + return { getListItemComments, getNextPageOfComments, addComment, deleteComment }; +}; diff --git a/src/controls/listItemComments/index.ts b/src/controls/listItemComments/index.ts new file mode 100644 index 000000000..9e69126d4 --- /dev/null +++ b/src/controls/listItemComments/index.ts @@ -0,0 +1,5 @@ +export * from './ListItemComments'; +export * from './common'; +export * from './components'; +export * from './hooks'; +export * from './models'; diff --git a/src/controls/listItemComments/models/IAddCommentPayload.ts b/src/controls/listItemComments/models/IAddCommentPayload.ts new file mode 100644 index 000000000..ab4de1b23 --- /dev/null +++ b/src/controls/listItemComments/models/IAddCommentPayload.ts @@ -0,0 +1,8 @@ +export interface IAddCommentPayload { + text: string; + mentions: IAddMention[]; +} +interface IAddMention { + email: string; + name: string; +} diff --git a/src/controls/listItemComments/models/IPageInfo.ts b/src/controls/listItemComments/models/IPageInfo.ts new file mode 100644 index 000000000..766af35e1 --- /dev/null +++ b/src/controls/listItemComments/models/IPageInfo.ts @@ -0,0 +1,4 @@ +export interface IPageInfo { + hasMore: boolean; + nextLink: string; +} diff --git a/src/controls/listItemComments/models/IUsersResults.ts b/src/controls/listItemComments/models/IUsersResults.ts new file mode 100644 index 000000000..6fe5d10b2 --- /dev/null +++ b/src/controls/listItemComments/models/IUsersResults.ts @@ -0,0 +1,12 @@ +export interface IUsersResults { + users: IUserInfo[]; + hasMore?: boolean; + nextLink?: string; +} +export interface IUserInfo { + displayName: string; + givenName?: string; + mail: string; + userPrincipalName?: string; + id: string; +} diff --git a/src/controls/listItemComments/models/IlistItemCommentsResults.ts b/src/controls/listItemComments/models/IlistItemCommentsResults.ts new file mode 100644 index 000000000..9ec6992df --- /dev/null +++ b/src/controls/listItemComments/models/IlistItemCommentsResults.ts @@ -0,0 +1,6 @@ +import { IComment } from "../components/Comments/IComment"; +export interface IlistItemCommentsResults { + comments: IComment[]; + hasMore?: boolean; + nextLink?: string; +} diff --git a/src/controls/listItemComments/models/index.ts b/src/controls/listItemComments/models/index.ts new file mode 100644 index 000000000..5bd12155a --- /dev/null +++ b/src/controls/listItemComments/models/index.ts @@ -0,0 +1,4 @@ +export * from './IAddCommentPayload'; +export * from './IPageInfo'; +export * from './IUsersResults'; +export * from './IlistItemCommentsResults'; diff --git a/src/controls/listItemComments/utils/utils.ts b/src/controls/listItemComments/utils/utils.ts new file mode 100644 index 000000000..ea9ce4b1e --- /dev/null +++ b/src/controls/listItemComments/utils/utils.ts @@ -0,0 +1,118 @@ +import { SPComponentLoader } from "@microsoft/sp-loader"; +const DEFAULT_PERSONA_IMG_HASH: string = "7ad602295f8386b7615b582d87bcc294"; +const DEFAULT_IMAGE_PLACEHOLDER_HASH: string = "4a48f26592f4e1498d7a478a4c48609c"; +const MD5_MODULE_ID: string = "8494e7d7-6b99-47b2-a741-59873e42f16f"; +const PROFILE_IMAGE_URL: string = "/_layouts/15/userphoto.aspx?size=M&accountname="; + +export const getScrollPosition = (_dataListContainerRef: any) => { + const { scrollTop, scrollHeight, clientHeight } = _dataListContainerRef; + const percentNow = (scrollTop / (scrollHeight - clientHeight)) * 100; + return percentNow; +}; + +export const b64toBlob = async (b64Data: any, contentType: string, sliceSize?: number): Promise => { + contentType = contentType || "image/png"; + sliceSize = sliceSize || 512; + + let byteCharacters: string = atob(b64Data); + let byteArrays = []; + + for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { + let slice = byteCharacters.slice(offset, offset + sliceSize); + + let byteNumbers = new Array(slice.length); + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + let byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + } + + let blob = new Blob(byteArrays, { type: contentType }); + return blob; +}; +export const blobToBase64 = (blob: Blob): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = (_) => { + resolve(reader.result as string); + }; + reader.readAsDataURL(blob); + }); +}; + +export const getImageBase64 = async (pictureUrl: string): Promise => { + console.log(pictureUrl); + return new Promise((resolve, reject) => { + let image = new Image(); + image.addEventListener("load", () => { + let tempCanvas = document.createElement("canvas"); + (tempCanvas.width = image.width), + (tempCanvas.height = image.height), + tempCanvas.getContext("2d").drawImage(image, 0, 0); + let base64Str; + try { + base64Str = tempCanvas.toDataURL("image/png"); + } catch (e) { + return ""; + } + base64Str = base64Str.replace(/^data:image\/png;base64,/, ""); + resolve(base64Str); + }); + image.src = pictureUrl; + }); +}; +/** + * Get MD5Hash for the image url to verify whether user has default image or custom image + * @param url + */ +export const getMd5HashForUrl = async (url: string) => { + return new Promise(async (resolve, reject) => { + // tslint:disable-next-line: no-use-before-declare + const library: any = await loadSPComponentById(MD5_MODULE_ID); + try { + const md5Hash = library.Md5Hash; + if (md5Hash) { + const convertedHash = md5Hash(url); + resolve(convertedHash); + } + } catch (error) { + resolve(url); + } + }); +}; + +/** + * Load SPFx component by id, SPComponentLoader is used to load the SPFx components + * @param componentId - componentId, guid of the component library + */ +export const loadSPComponentById = async (componentId: string) => { + return new Promise((resolve, reject) => { + SPComponentLoader.loadComponentById(componentId) + .then((component: any) => { + resolve(component); + }) + .catch((error) => {}); + }); +}; +/** + * Gets user photo + * @param userId + * @returns user photo + */ +export const getUserPhoto = async (userId): Promise => { + const personaImgUrl = PROFILE_IMAGE_URL + userId; + + // tslint:disable-next-line: no-use-before-declare + const url: string = await getImageBase64(personaImgUrl); + // tslint:disable-next-line: no-use-before-declare + const newHash = await getMd5HashForUrl(url); + + if (newHash !== DEFAULT_PERSONA_IMG_HASH && newHash !== DEFAULT_IMAGE_PLACEHOLDER_HASH) { + return "data:image/png;base64," + url; + } else { + return "undefined"; + } +}; diff --git a/src/loc/bg-bg.ts b/src/loc/bg-bg.ts index f7932872e..91c042b69 100644 --- a/src/loc/bg-bg.ts +++ b/src/loc/bg-bg.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "No comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/ca-es.ts b/src/loc/ca-es.ts index 9834fbedf..e0a105953 100644 --- a/src/loc/ca-es.ts +++ b/src/loc/ca-es.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "No comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/da-dk.ts b/src/loc/da-dk.ts index 76cb143f2..828c3650e 100644 --- a/src/loc/da-dk.ts +++ b/src/loc/da-dk.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "No comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/de-de.ts b/src/loc/de-de.ts index b6fa5c679..05d433d99 100644 --- a/src/loc/de-de.ts +++ b/src/loc/de-de.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "No comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/el-gr.ts b/src/loc/el-gr.ts index 690ff9604..6d3b3ce8a 100644 --- a/src/loc/el-gr.ts +++ b/src/loc/el-gr.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "No comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/en-us.ts b/src/loc/en-us.ts index e0b6a9c67..509a4fce4 100644 --- a/src/loc/en-us.ts +++ b/src/loc/en-us.ts @@ -2,6 +2,8 @@ declare var define: any; define([], () => { return { + + "Save": "Save", "Cancel": "Cancel", @@ -380,8 +382,11 @@ define([], () => { DynamicFormTermPanelTitle: "Select Term", DynamicFormEnterURLPlaceholder: "Enter a URL", DynamicFormEnterDescriptionPlaceholder: "Alternative text", - customDisplayName: "Use this location:", + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", OrgAssetsLinkLabel: "Your organisation" }; }); diff --git a/src/loc/es-es.ts b/src/loc/es-es.ts index c8fa58fd8..23d4b34c5 100644 --- a/src/loc/es-es.ts +++ b/src/loc/es-es.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/et-ee.ts b/src/loc/et-ee.ts index 9d8b950f9..702d3bbc4 100644 --- a/src/loc/et-ee.ts +++ b/src/loc/et-ee.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/eu-es.ts b/src/loc/eu-es.ts index b5078a9e5..a0af3969c 100644 --- a/src/loc/eu-es.ts +++ b/src/loc/eu-es.ts @@ -253,6 +253,10 @@ define([], () => { LinkFileInstructions: "Paste a link to a file in OneDrive for Business or SharePoint Online", LinkHeader: "From a link", LinkImageInstructions: "Paste a link to an image in OneDrive for Business or SharePoint Online", + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", ListLayoutAriaLabel: "View options. {0} {1} .", ListLayoutCompact: "Compact view", ListLayoutCompactDescription: "View items and details in a compact list", diff --git a/src/loc/fi-fi.ts b/src/loc/fi-fi.ts index 726a2ca57..478c98eec 100644 --- a/src/loc/fi-fi.ts +++ b/src/loc/fi-fi.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/fr-ca.ts b/src/loc/fr-ca.ts index 7f79fe882..efef030f6 100644 --- a/src/loc/fr-ca.ts +++ b/src/loc/fr-ca.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/fr-fr.ts b/src/loc/fr-fr.ts index 7f79fe882..efef030f6 100644 --- a/src/loc/fr-fr.ts +++ b/src/loc/fr-fr.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/it-it.ts b/src/loc/it-it.ts index e835aae26..7f80c8f29 100644 --- a/src/loc/it-it.ts +++ b/src/loc/it-it.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/ja-jp.ts b/src/loc/ja-jp.ts index 53ae4fac4..0ca8ff3f3 100644 --- a/src/loc/ja-jp.ts +++ b/src/loc/ja-jp.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/lt-lt.ts b/src/loc/lt-lt.ts index 127b0fc47..2d9d5bbcb 100644 --- a/src/loc/lt-lt.ts +++ b/src/loc/lt-lt.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/lv-lv.ts b/src/loc/lv-lv.ts index a3f5b8bf4..4f83bd1b0 100644 --- a/src/loc/lv-lv.ts +++ b/src/loc/lv-lv.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/mystrings.d.ts b/src/loc/mystrings.d.ts index 5af694d63..90271ff6f 100644 --- a/src/loc/mystrings.d.ts +++ b/src/loc/mystrings.d.ts @@ -1,4 +1,9 @@ declare interface IControlStrings { + ListItemCommentsLabel: string; + ListItemCommentsNoCommentsLabel: string; + ListItemCommentDIalogDeleteSubText: string; + ListItemCommentsDialogDeleteTitle: string; + MyTeamsMessageError:string; MyTeamsNoTeamsMessage:string; MyTeamsLoadingMessage:string; diff --git a/src/loc/nb-no.ts b/src/loc/nb-no.ts index a54b424dc..dc7e71558 100644 --- a/src/loc/nb-no.ts +++ b/src/loc/nb-no.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/nl-nl.ts b/src/loc/nl-nl.ts index 63421b18a..5633c3529 100644 --- a/src/loc/nl-nl.ts +++ b/src/loc/nl-nl.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/pl-pl.ts b/src/loc/pl-pl.ts index c656a78cb..5e2485490 100644 --- a/src/loc/pl-pl.ts +++ b/src/loc/pl-pl.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/pt-pt.ts b/src/loc/pt-pt.ts index 2e773a560..b2d55e2d5 100644 --- a/src/loc/pt-pt.ts +++ b/src/loc/pt-pt.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Tem a certeza que quer eliminar este comentário?", + ListItemCommentsDialogDeleteTitle: "Confirmar Eliminar Comentário", + ListItemCommentsLabel: "Comentários", + ListItemCommentsNoCommentsLabel: "Sem Comentários", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", MyTeamsNoTeamsMessage: "Neste momento não tens nenhum Team", diff --git a/src/loc/ro-ro.ts b/src/loc/ro-ro.ts index edd5c5976..2e4ad30a6 100644 --- a/src/loc/ro-ro.ts +++ b/src/loc/ro-ro.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/ru-ru.ts b/src/loc/ru-ru.ts index 850346c37..3c8db88bc 100644 --- a/src/loc/ru-ru.ts +++ b/src/loc/ru-ru.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/sk-sk.ts b/src/loc/sk-sk.ts index fd9181457..f1efcc483 100644 --- a/src/loc/sk-sk.ts +++ b/src/loc/sk-sk.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/sr-latn-rs.ts b/src/loc/sr-latn-rs.ts index e0924bf6a..356857a4c 100644 --- a/src/loc/sr-latn-rs.ts +++ b/src/loc/sr-latn-rs.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/sv-se.ts b/src/loc/sv-se.ts index 0ea7a7dfe..2432de18b 100644 --- a/src/loc/sv-se.ts +++ b/src/loc/sv-se.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/tr-tr.ts b/src/loc/tr-tr.ts index 5b47cd3ec..6dde89a0e 100644 --- a/src/loc/tr-tr.ts +++ b/src/loc/tr-tr.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/vi-vn.ts b/src/loc/vi-vn.ts index c71bbbb21..f543479f2 100644 --- a/src/loc/vi-vn.ts +++ b/src/loc/vi-vn.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/zh-cn.ts b/src/loc/zh-cn.ts index 11354e9b4..478716630 100644 --- a/src/loc/zh-cn.ts +++ b/src/loc/zh-cn.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/loc/zh-tw.ts b/src/loc/zh-tw.ts index d08d79928..0624dae45 100644 --- a/src/loc/zh-tw.ts +++ b/src/loc/zh-tw.ts @@ -2,6 +2,10 @@ declare var define: any; define([], () => { return { + ListItemCommentDIalogDeleteSubText: "Are you sure that you want to delete this comment?", + ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", + ListItemCommentsLabel: "Comments", + ListItemCommentsNoCommentsLabel: "There is no Comments", MyTeamsLoadingMessage: "loading your teams", MyTeamsMessageDontHaveTeams: "You don't have any teams", MyTeamsMessageError: "Something went wrong while loading your teams, please try later or refresh browser", diff --git a/src/webparts/controlsTest/ControlsTestWebPart.ts b/src/webparts/controlsTest/ControlsTestWebPart.ts index 4741f51b4..148cca944 100644 --- a/src/webparts/controlsTest/ControlsTestWebPart.ts +++ b/src/webparts/controlsTest/ControlsTestWebPart.ts @@ -10,7 +10,7 @@ import { } from '@microsoft/sp-property-pane'; import * as strings from 'ControlsTestWebPartStrings'; -import ControlsTest from './components/ControlsTest'; + import { TestControl, ITestControlProps } from './components/TestControl'; import { IControlsTestProps } from './components/IControlsTestProps'; import { IControlsTestWebPartProps } from './IControlsTestWebPartProps'; import { @@ -18,6 +18,7 @@ import { ThemeChangedEventArgs, ThemeProvider, } from "@microsoft/sp-component-base"; +import ControlsTest from './components/ControlsTest'; /** * Web part to test the React controls */ @@ -66,11 +67,18 @@ export default class ControlsTestWebPart extends BaseClientSideWebPart = React.createElement( + TestControl, + { + context: this.context, + } + ); */ - public render(): void { - const element: React.ReactElement = React.createElement( - ControlsTest, + const element: React.ReactElement = React.createElement( + + ControlsTest, { themeVariant: this._themeVariant, diff --git a/src/webparts/controlsTest/components/TestControl.tsx b/src/webparts/controlsTest/components/TestControl.tsx new file mode 100644 index 000000000..5cc4735e2 --- /dev/null +++ b/src/webparts/controlsTest/components/TestControl.tsx @@ -0,0 +1,25 @@ +import { WebPartContext } from "@microsoft/sp-webpart-base"; +import { Stack } from "@fluentui/react/lib/Stack"; +import * as React from "react"; +import { ListItemComments } from "../../../controls/listItemComments"; +export interface ITestControlProps { + context: WebPartContext; +} + +export const TestControl: React.FunctionComponent = ( + props: React.PropsWithChildren +) => { + return ( + <> + + + + + ); +};