feat: Add pagination to posts' comments
This change adds pagination to posts' comments. When viewing a post, if there are more than one page of comments, initially users will only see the first page of the comments, and can load more comments by clicking "load more comments" button. This change only affects comments to posts. For comments that are responses to other comments the pagination has not been implemented.
This commit is contained in:
417
package-lock.json
generated
417
package-lock.json
generated
@@ -5170,6 +5170,243 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@testing-library/dom": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.5.0.tgz",
|
||||
"integrity": "sha512-O0fmHFaPlqaYCpa/cBL0cvroMridb9vZsMLacgIqrlxj+fd+bGF8UfAgwsLCHRF84KLBafWlm9CuOvxeNTlodw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/aria-query": "^4.2.0",
|
||||
"aria-query": "^4.2.2",
|
||||
"chalk": "^4.1.0",
|
||||
"dom-accessibility-api": "^0.5.6",
|
||||
"lz-string": "^1.4.4",
|
||||
"pretty-format": "^27.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz",
|
||||
"integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"@jest/types": {
|
||||
"version": "27.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.1.1.tgz",
|
||||
"integrity": "sha512-yqJPDDseb0mXgKqmNqypCsb85C22K1aY5+LUxh7syIM9n/b0AsaltxNy+o6tt29VcfGDpYEve175bm3uOhcehA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
"@types/node": "*",
|
||||
"@types/yargs": "^16.0.0",
|
||||
"chalk": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "16.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
|
||||
"integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"aria-query": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz",
|
||||
"integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.10.2",
|
||||
"@babel/runtime-corejs3": "^7.10.2"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "27.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.2.0.tgz",
|
||||
"integrity": "sha512-KyJdmgBkMscLqo8A7K77omgLx5PWPiXJswtTtFV7XgVZv2+qPk6UivpXXO+5k6ZEbWIbLoKdx1pZ6ldINzbwTA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/types": "^27.1.1",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@testing-library/jest-dom": {
|
||||
"version": "5.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.14.1.tgz",
|
||||
"integrity": "sha512-dfB7HVIgTNCxH22M1+KU6viG5of2ldoA5ly8Ar8xkezKHKXjRvznCdbMbqjYGgO2xjRbwnR+rR8MLUIqF3kKbQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@types/testing-library__jest-dom": "^5.9.1",
|
||||
"aria-query": "^4.2.2",
|
||||
"chalk": "^3.0.0",
|
||||
"css": "^3.0.0",
|
||||
"css.escape": "^1.5.1",
|
||||
"dom-accessibility-api": "^0.5.6",
|
||||
"lodash": "^4.17.15",
|
||||
"redent": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz",
|
||||
"integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"aria-query": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz",
|
||||
"integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.10.2",
|
||||
"@babel/runtime-corejs3": "^7.10.2"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
|
||||
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"indent-string": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||
"dev": true
|
||||
},
|
||||
"redent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"indent-string": "^4.0.0",
|
||||
"strip-indent": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"strip-indent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"min-indent": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@testing-library/react": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.0.tgz",
|
||||
"integrity": "sha512-Ge3Ht3qXE82Yv9lyPpQ7ZWgzo/HgOcHu569Y4ZGWcZME38iOFiOg87qnu6hTEa8jTJVL7zYovnvD3GE2nsNIoQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@testing-library/dom": "^8.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz",
|
||||
"integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tinymce/tinymce-react": {
|
||||
"version": "3.12.6",
|
||||
"resolved": "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-3.12.6.tgz",
|
||||
@@ -5191,6 +5428,12 @@
|
||||
"integrity": "sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/aria-query": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
|
||||
"integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/babel__core": {
|
||||
"version": "7.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz",
|
||||
@@ -5344,6 +5587,124 @@
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"@types/jest": {
|
||||
"version": "27.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.0.1.tgz",
|
||||
"integrity": "sha512-HTLpVXHrY69556ozYkcq47TtQJXpcWAWfkoqz+ZGz2JnmZhzlRjprCIyFnetSy8gpDWwTTGBcRVv1J1I1vBrHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"jest-diff": "^27.0.0",
|
||||
"pretty-format": "^27.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jest/types": {
|
||||
"version": "27.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.1.1.tgz",
|
||||
"integrity": "sha512-yqJPDDseb0mXgKqmNqypCsb85C22K1aY5+LUxh7syIM9n/b0AsaltxNy+o6tt29VcfGDpYEve175bm3uOhcehA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
"@types/node": "*",
|
||||
"@types/yargs": "^16.0.0",
|
||||
"chalk": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "16.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
|
||||
"integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"diff-sequences": {
|
||||
"version": "27.0.6",
|
||||
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz",
|
||||
"integrity": "sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==",
|
||||
"dev": true
|
||||
},
|
||||
"jest-diff": {
|
||||
"version": "27.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.2.0.tgz",
|
||||
"integrity": "sha512-QSO9WC6btFYWtRJ3Hac0sRrkspf7B01mGrrQEiCW6TobtViJ9RWL0EmOs/WnBsZDsI/Y2IoSHZA2x6offu0sYw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^4.0.0",
|
||||
"diff-sequences": "^27.0.6",
|
||||
"jest-get-type": "^27.0.6",
|
||||
"pretty-format": "^27.2.0"
|
||||
}
|
||||
},
|
||||
"jest-get-type": {
|
||||
"version": "27.0.6",
|
||||
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz",
|
||||
"integrity": "sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "27.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.2.0.tgz",
|
||||
"integrity": "sha512-KyJdmgBkMscLqo8A7K77omgLx5PWPiXJswtTtFV7XgVZv2+qPk6UivpXXO+5k6ZEbWIbLoKdx1pZ6ldINzbwTA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/types": "^27.1.1",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
|
||||
@@ -5464,6 +5825,15 @@
|
||||
"integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/testing-library__jest-dom": {
|
||||
"version": "5.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.1.tgz",
|
||||
"integrity": "sha512-Gk9vaXfbzc5zCXI9eYE9BI5BNHEp4D3FWjgqBE/ePGYElLAP+KvxBcsdkwfIVvezs605oiyd/VrpiHe3Oeg+Aw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/jest": "*"
|
||||
}
|
||||
},
|
||||
"@types/uglify-js": {
|
||||
"version": "3.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz",
|
||||
@@ -8104,6 +8474,29 @@
|
||||
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
|
||||
},
|
||||
"css": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz",
|
||||
"integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inherits": "^2.0.4",
|
||||
"source-map": "^0.6.1",
|
||||
"source-map-resolve": "^0.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map-resolve": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz",
|
||||
"integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"atob": "^2.1.2",
|
||||
"decode-uri-component": "^0.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"css-color-names": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-1.0.1.tgz",
|
||||
@@ -8187,6 +8580,12 @@
|
||||
"integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==",
|
||||
"dev": true
|
||||
},
|
||||
"css.escape": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||
"integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=",
|
||||
"dev": true
|
||||
},
|
||||
"cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@@ -8836,6 +9235,12 @@
|
||||
"esutils": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"dom-accessibility-api": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.7.tgz",
|
||||
"integrity": "sha512-ml3lJIq9YjUfM9TUnEPvEYWFSwivwIGBPKpewX7tii7fwCazA8yCioGdqQcNsItPpfFvSJ3VIdMQPj60LJhcQA==",
|
||||
"dev": true
|
||||
},
|
||||
"dom-converter": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
|
||||
@@ -16149,6 +16554,12 @@
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"lz-string": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
|
||||
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=",
|
||||
"dev": true
|
||||
},
|
||||
"mailto-link": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mailto-link/-/mailto-link-1.0.0.tgz",
|
||||
@@ -16479,6 +16890,12 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"min-indent": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
|
||||
"dev": true
|
||||
},
|
||||
"mini-create-react-context": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "8.0.4",
|
||||
"@testing-library/jest-dom": "5.14.1",
|
||||
"@testing-library/react": "12.1.0",
|
||||
"axios-mock-adapter": "1.20.0",
|
||||
"codecov": "3.8.3",
|
||||
"es-check": "5.2.3",
|
||||
|
||||
@@ -4,25 +4,35 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { ensureConfig, getConfig } from '@edx/frontend-platform';
|
||||
import { Spinner } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Spinner } from '@edx/paragon';
|
||||
|
||||
import { Post } from '../posts';
|
||||
import { selectThread } from '../posts/data/selectors';
|
||||
import { markThreadAsRead } from '../posts/data/thunks';
|
||||
import { selectThreadComments } from './data/selectors';
|
||||
import {
|
||||
selectThreadComments,
|
||||
selectThreadCurrentPage,
|
||||
selectThreadHasMorePages,
|
||||
} from './data/selectors';
|
||||
import { fetchThreadComments } from './data/thunks';
|
||||
import { Comment, ResponseEditor } from './comment';
|
||||
import messages from './messages';
|
||||
|
||||
ensureConfig(['POST_MARK_AS_READ_DELAY'], 'Comment thread view');
|
||||
|
||||
function CommentsView() {
|
||||
function CommentsView({ intl }) {
|
||||
const { postId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const thread = useSelector(selectThread(postId));
|
||||
const comments = useSelector(selectThreadComments(postId));
|
||||
|
||||
const hasMorePages = useSelector(selectThreadHasMorePages(postId));
|
||||
const currentPage = useSelector(selectThreadCurrentPage(postId));
|
||||
const handleLoadMoreComments = () => dispatch(fetchThreadComments(postId, { page: currentPage + 1 }));
|
||||
useEffect(() => {
|
||||
dispatch(fetchThreadComments(postId));
|
||||
if (!currentPage) {
|
||||
dispatch(fetchThreadComments(postId, { page: 1 }));
|
||||
}
|
||||
const markReadTimer = setTimeout(() => {
|
||||
if (thread && !thread.read) {
|
||||
dispatch(markThreadAsRead(postId));
|
||||
@@ -53,6 +63,17 @@ function CommentsView() {
|
||||
<Comment comment={comment} />
|
||||
</div>
|
||||
))}
|
||||
{hasMorePages && (
|
||||
<div className="list-group-item list-group-item-action">
|
||||
<Button
|
||||
onClick={handleLoadMoreComments}
|
||||
variant="link"
|
||||
block="true"
|
||||
>
|
||||
{intl.formatMessage(messages.loadMoreComments)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ResponseEditor postId={postId} />
|
||||
@@ -60,6 +81,8 @@ function CommentsView() {
|
||||
);
|
||||
}
|
||||
|
||||
CommentsView.propTypes = {};
|
||||
CommentsView.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default CommentsView;
|
||||
export default injectIntl(CommentsView);
|
||||
|
||||
199
src/discussions/comments/CommentsView.test.jsx
Normal file
199
src/discussions/comments/CommentsView.test.jsx
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { initializeStore } from '../../store';
|
||||
import { commentsApiUrl } from './data/api';
|
||||
import CommentsView from './CommentsView';
|
||||
|
||||
const postId = '1';
|
||||
let store;
|
||||
let axiosMock;
|
||||
|
||||
const mockCommentsPaged = [
|
||||
[
|
||||
{
|
||||
threadId: postId,
|
||||
id: '1',
|
||||
renderedBody: 'test comment 1',
|
||||
voteCount: 0,
|
||||
author: 'testauthor',
|
||||
users: {
|
||||
testauthor: {
|
||||
profile: {
|
||||
image: {
|
||||
image_url_small: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
editableFields: [],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
threadId: postId,
|
||||
id: '2',
|
||||
renderedBody: 'test comment 2',
|
||||
voteCount: 0,
|
||||
author: 'testauthor',
|
||||
users: {
|
||||
testauthor: {
|
||||
profile: {
|
||||
image: {
|
||||
image_url_small: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
editableFields: [],
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
function mockAxiosReturnPagedComments() {
|
||||
const paramsTemplate = {
|
||||
thread_id: postId,
|
||||
page: undefined,
|
||||
page_size: undefined,
|
||||
requested_fields: 'profile_image',
|
||||
};
|
||||
|
||||
const numPages = mockCommentsPaged.length;
|
||||
for (let page = 1; page <= numPages; page++) {
|
||||
const comments = mockCommentsPaged[page - 1];
|
||||
axiosMock
|
||||
.onGet(commentsApiUrl, { params: { ...paramsTemplate, page } })
|
||||
.reply(200, {
|
||||
results: comments,
|
||||
pagination: {
|
||||
page,
|
||||
numPages,
|
||||
next: page < numPages ? page + 1 : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderComponent() {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<MemoryRouter initialEntries={['comments/1']}>
|
||||
<Route path="comments/:postId">
|
||||
<CommentsView />
|
||||
</Route>
|
||||
</MemoryRouter>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('CommentsView', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
adminsitrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore({
|
||||
threads: {
|
||||
threadsById: {
|
||||
[postId]: {
|
||||
id: postId.toString(),
|
||||
author: 'testauthor',
|
||||
title: 'test thread',
|
||||
voteCount: 0,
|
||||
type: 'discussion',
|
||||
pinned: false,
|
||||
abuseFlagged: false,
|
||||
commentCount: mockCommentsPaged.reduce((acc, cur) => acc + cur.length, 0),
|
||||
courseId: 'course_id',
|
||||
following: false,
|
||||
rawBody: '',
|
||||
read: true,
|
||||
topicId: '',
|
||||
updatedAt: '',
|
||||
editableFields: [],
|
||||
},
|
||||
},
|
||||
avatars: {
|
||||
testauthor: {
|
||||
profile: {
|
||||
image: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
// TODO: use test id to prevent breaking from text changes
|
||||
const findLoadMoreCommentsButton = () => screen.findByRole('button', { name: /load more comments/i });
|
||||
|
||||
it('initially loads only the first page', async () => {
|
||||
const firstPageComment = mockCommentsPaged[0][0];
|
||||
const secondPageComment = mockCommentsPaged[1][0];
|
||||
mockAxiosReturnPagedComments();
|
||||
renderComponent();
|
||||
|
||||
await screen.findByText(firstPageComment.renderedBody);
|
||||
expect(screen.queryByText(secondPageComment.renderedBody)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pressing load more button will load next page of comments', async () => {
|
||||
const secondPageComment = mockCommentsPaged[1][0];
|
||||
mockAxiosReturnPagedComments();
|
||||
renderComponent();
|
||||
|
||||
const loadMoreButton = await findLoadMoreCommentsButton();
|
||||
fireEvent.click(loadMoreButton);
|
||||
|
||||
await screen.findByText(secondPageComment.renderedBody);
|
||||
});
|
||||
|
||||
it('newly loaded comments are appended to the old ones', async () => {
|
||||
const firstPageComment = mockCommentsPaged[0][0];
|
||||
const secondPageComment = mockCommentsPaged[1][0];
|
||||
mockAxiosReturnPagedComments();
|
||||
renderComponent();
|
||||
|
||||
const loadMoreButton = await findLoadMoreCommentsButton();
|
||||
fireEvent.click(loadMoreButton);
|
||||
|
||||
await screen.findByText(secondPageComment.renderedBody);
|
||||
// check that comments from the first pages are also displayed
|
||||
expect(screen.queryByText(firstPageComment.renderedBody)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('load more button is hidden when no more comments pages to load', async () => {
|
||||
const totalePages = mockCommentsPaged.length;
|
||||
const lastPageComment = mockCommentsPaged[totalePages - 1][0];
|
||||
mockAxiosReturnPagedComments();
|
||||
renderComponent();
|
||||
|
||||
const loadMoreButton = await findLoadMoreCommentsButton();
|
||||
for (let page = 1; page < totalePages; page++) {
|
||||
fireEvent.click(loadMoreButton);
|
||||
}
|
||||
|
||||
await screen.findByText(lastPageComment.renderedBody);
|
||||
await expect(findLoadMoreCommentsButton()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -45,8 +45,8 @@ describe('Comments/Responses data layer tests', () => {
|
||||
|
||||
expect(store.getState().comments.commentsInThreads)
|
||||
.toEqual({ 'test-thread': ['comment-1', 'comment-2', 'comment-3'] });
|
||||
expect(store.getState().comments.pages)
|
||||
.toEqual([['comment-1', 'comment-2', 'comment-3']]);
|
||||
expect(store.getState().comments.pagination)
|
||||
.toEqual({ 'test-thread': { currentPage: 1, totalPages: 1, hasMorePages: false } });
|
||||
expect(Object.keys(store.getState().comments.commentsById))
|
||||
.toEqual(['comment-1', 'comment-2', 'comment-3']);
|
||||
expect(store.getState().comments.commentsById['comment-1'])
|
||||
@@ -137,8 +137,6 @@ describe('Comments/Responses data layer tests', () => {
|
||||
|
||||
expect(store.getState().comments.commentsById)
|
||||
.toHaveProperty(commentId);
|
||||
expect(store.getState().comments.pages[0])
|
||||
.toContain(commentId);
|
||||
|
||||
axiosMock.onDelete(`${commentsApiUrl}${commentId}/`)
|
||||
.reply(201);
|
||||
@@ -148,9 +146,6 @@ describe('Comments/Responses data layer tests', () => {
|
||||
expect(store.getState().comments.commentsById)
|
||||
.not
|
||||
.toHaveProperty(commentId);
|
||||
expect(store.getState().comments.pages[0])
|
||||
.not
|
||||
.toContain(commentId);
|
||||
expect(store.getState().comments.commentsInThreads[threadId])
|
||||
.not
|
||||
.toContain(commentId);
|
||||
|
||||
@@ -20,4 +20,12 @@ export const selectCommentResponses = commentId => createSelector(
|
||||
mapIdToComment,
|
||||
);
|
||||
|
||||
export const selectThreadHasMorePages = threadId => (
|
||||
store => store.comments.pagination[threadId]?.hasMorePages || false
|
||||
);
|
||||
|
||||
export const selectThreadCurrentPage = threadId => (
|
||||
store => store.comments.pagination[threadId]?.currentPage || null
|
||||
);
|
||||
|
||||
export const commentsStatus = state => state.comments.status;
|
||||
|
||||
@@ -16,13 +16,12 @@ const commentsSlice = createSlice({
|
||||
commentsById: {
|
||||
// Map comment ids to comments.
|
||||
},
|
||||
pages: [],
|
||||
// Stores the comment being posted in case it needs to be reposted due to network failure.
|
||||
// TODO: save in localstorage so user can continue editing?
|
||||
commentDraft: null,
|
||||
totalPages: null,
|
||||
totalThreads: null,
|
||||
postStatus: RequestStatus.SUCCESSFUL,
|
||||
pagination: {
|
||||
},
|
||||
},
|
||||
reducers: {
|
||||
fetchCommentsRequest: (state) => {
|
||||
@@ -30,12 +29,17 @@ const commentsSlice = createSlice({
|
||||
},
|
||||
fetchCommentsSuccess: (state, { payload }) => {
|
||||
state.status = RequestStatus.SUCCESSFUL;
|
||||
state.pages[payload.page - 1] = payload.ids;
|
||||
state.commentsInThreads = { ...state.commentsInThreads, ...payload.commentsInThreads };
|
||||
state.commentsInThreads[payload.threadId] = [
|
||||
...(state.commentsInThreads[payload.threadId] || []),
|
||||
...(payload.commentsInThreads[payload.threadId] || []),
|
||||
];
|
||||
state.commentsInComments = { ...state.commentsInComments, ...payload.commentsInComments };
|
||||
state.commentsById = { ...state.commentsById, ...payload.commentsById };
|
||||
state.totalPages = payload.pagination.numPages;
|
||||
state.totalThreads = payload.pagination.count;
|
||||
state.pagination[payload.threadId] = {
|
||||
currentPage: payload.page,
|
||||
totalPages: payload.pagination.numPages,
|
||||
hasMorePages: Boolean(payload.pagination.next),
|
||||
};
|
||||
},
|
||||
fetchCommentsFailed: (state) => {
|
||||
state.status = RequestStatus.FAILED;
|
||||
@@ -109,7 +113,6 @@ const commentsSlice = createSlice({
|
||||
if (parentId) {
|
||||
state.commentsInComments[parentId] = state.commentsInComments[parentId].filter(item => item !== commentId);
|
||||
}
|
||||
state.pages = state.pages.map(page => page?.filter(item => item !== commentId));
|
||||
delete state.commentsById[commentId];
|
||||
},
|
||||
},
|
||||
|
||||
@@ -80,6 +80,7 @@ export function fetchThreadComments(threadId, { page = 1 } = {}) {
|
||||
dispatch(fetchCommentsSuccess({
|
||||
...normaliseComments(camelCaseObject(data)),
|
||||
page,
|
||||
threadId,
|
||||
}));
|
||||
} catch (error) {
|
||||
if (getHttpErrorStatus(error) === 403) {
|
||||
|
||||
@@ -11,6 +11,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Add a comment',
|
||||
description: 'Button to add a comment to a response',
|
||||
},
|
||||
loadMoreComments: {
|
||||
id: 'discussions.comments.comment.loadMoreComments',
|
||||
defaultMessage: 'Load more comments',
|
||||
description: 'Button to load more comments of forum posts',
|
||||
},
|
||||
postVisibility: {
|
||||
id: 'discussions.comments.comment.visibility',
|
||||
defaultMessage: `This post is visible to {group, select,
|
||||
|
||||
@@ -1 +1,18 @@
|
||||
import 'babel-polyfill';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
|
||||
// mock methods which are not implemented in JSDOM:
|
||||
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // deprecated
|
||||
removeListener: jest.fn(), // deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
@@ -4,13 +4,14 @@ import { commentsReducer } from './discussions/comments/data';
|
||||
import { threadsReducer } from './discussions/posts/data';
|
||||
import { topicsReducer } from './discussions/topics/data';
|
||||
|
||||
export function initializeStore() {
|
||||
export function initializeStore(preloadedState = undefined) {
|
||||
return configureStore({
|
||||
reducer: {
|
||||
topics: topicsReducer,
|
||||
threads: threadsReducer,
|
||||
comments: commentsReducer,
|
||||
},
|
||||
preloadedState,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user