Compare commits
23 Commits
ahtisham/I
...
inf-706
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71d04a5353 | ||
|
|
d2b2a2aff9 | ||
|
|
569ce49801 | ||
|
|
c67bc3e080 | ||
|
|
bcde4f5f87 | ||
|
|
eaa3ce16ea | ||
|
|
af5bc1a664 | ||
|
|
2fa0900a65 | ||
|
|
afbd894154 | ||
|
|
bfcb1282f0 | ||
|
|
f081e8dc77 | ||
|
|
2a187ca1df | ||
|
|
7de274a73e | ||
|
|
ad640611a1 | ||
|
|
42fa6a62c6 | ||
|
|
4a49c13f14 | ||
|
|
19e775737c | ||
|
|
b7e5dd0e28 | ||
|
|
f33e3c3e3d | ||
|
|
4e659f8293 | ||
|
|
02cf34a467 | ||
|
|
722da9616f | ||
|
|
6948a9fa5e |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -34,4 +34,4 @@ jobs:
|
||||
- name: i18n_extract
|
||||
run: npm run i18n_extract
|
||||
- name: Coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
46
README.rst
46
README.rst
@@ -1,16 +1,15 @@
|
||||
|Build Status| |Codecov| |license|
|
||||
|
||||
frontend-app-discussions
|
||||
========================
|
||||
|
||||
Please tag **@edx/fedx-team** on any PRs or issues. Thanks.
|
||||
|Build Status| |Codecov| |license|
|
||||
|
||||
Introduction
|
||||
------------
|
||||
Purpose
|
||||
-------
|
||||
|
||||
This repository is a React-based micro frontend for the Open edX discussion forums.
|
||||
|
||||
**Installation and Startup**
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
1. Clone your new repo:
|
||||
|
||||
@@ -26,6 +25,39 @@ This repository is a React-based micro frontend for the Open edX discussion foru
|
||||
|
||||
The dev server is running at `http://localhost:2002 <http://localhost:2002>`_.
|
||||
|
||||
Getting Help
|
||||
------------
|
||||
Please tag **@edx/fedx-team** on any PRs or issues. Thanks.
|
||||
If you're having trouble, we have discussion forums at https://discuss.openedx.org where you can connect with others in the community.
|
||||
For anything non-trivial, the best path is to open an issue in this repository with as many details about the issue you are facing as you can provide.
|
||||
|
||||
https://github.com/openedx/frontend-app-discussions/issues
|
||||
|
||||
For more information about these options, see the `Getting Help`_ page.
|
||||
|
||||
.. _Getting Help: https://openedx.org/getting-help
|
||||
|
||||
How to Contribute
|
||||
-----------------
|
||||
Details about how to become a contributor to the Open edX project may be found in the wiki at `How to contribute`_
|
||||
|
||||
.. _How to contribute: https://openedx.org/r/how-to-contribute
|
||||
|
||||
|
||||
The Open edX Code of Conduct
|
||||
----------------------------
|
||||
All community members should familarize themselves with the `Open edX Code of Conduct`_.
|
||||
|
||||
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
|
||||
|
||||
People
|
||||
------
|
||||
The assigned maintainers for this component and other project details may be found in Backstage or groked from inspecting catalog-info.yaml.
|
||||
|
||||
Reporting Security Issues
|
||||
-------------------------
|
||||
Please do not report security issues in public. Please email security@edx.org.
|
||||
|
||||
Project Structure
|
||||
-----------------
|
||||
|
||||
@@ -48,4 +80,4 @@ Please see `edx/frontend-platform's i18n module <https://edx.github.io/frontend-
|
||||
.. |Codecov| image:: https://codecov.io/gh/edx/frontend-app-discussions/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/edx/frontend-app-discussions
|
||||
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-discussions.svg
|
||||
:target: @edx/frontend-app-discussions
|
||||
:target: @edx/frontend-app-discussions
|
||||
38
catalog-info.yaml
Normal file
38
catalog-info.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
# This file records information about this repo. Its use is described in OEP-55:
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
|
||||
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
# (Required) Acceptable Values: Component, Resource, System
|
||||
# A repo will almost certainly be a Component.
|
||||
kind: Component
|
||||
metadata:
|
||||
name: 'frontend-app-discussions'
|
||||
description: "The discussion forum for openEdx discussions"
|
||||
links:
|
||||
- url: "https://github.com/openedx/frontend-app-discussions"
|
||||
title: "Frontend app discussions"
|
||||
# Backstage uses the MaterialUI Icon Set.
|
||||
# https://mui.com/material-ui/material-icons/
|
||||
# The value will be the name of the icon.
|
||||
icon: "Web"
|
||||
annotations:
|
||||
# (Optional) Annotation keys and values can be whatever you want.
|
||||
# We use it in Open edX repos to have a comma-separated list of GitHub user
|
||||
# names that might be interested in changes to the architecture of this
|
||||
# component.
|
||||
openedx.org/arch-interest-groups: ""
|
||||
spec:
|
||||
|
||||
# (Required) This can be a group (`group:<github_group_name>`) or a user (`user:<github_username>`).
|
||||
# Don't forget the "user:" or "group:" prefix. Groups must be GitHub team
|
||||
# names in the openedx GitHub organization: https://github.com/orgs/openedx/teams
|
||||
#
|
||||
# If you need a new team created, create an issue with tCRIL engineering:
|
||||
# https://github.com/openedx/tcril-engineering/issues/new/choose
|
||||
owner: group:infinity
|
||||
|
||||
# (Required) Acceptable Type Values: service, website, library
|
||||
type: 'website'
|
||||
|
||||
# (Required) Acceptable Lifecycle Values: experimental, production, deprecated
|
||||
lifecycle: 'production'
|
||||
1261
package-lock.json
generated
1261
package-lock.json
generated
@@ -19,6 +19,7 @@
|
||||
"babel-polyfill": "6.26.0",
|
||||
"classnames": "2.3.1",
|
||||
"core-js": "3.21.1",
|
||||
"dompurify": "^2.4.3",
|
||||
"formik": "2.2.9",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"prop-types": "15.8.1",
|
||||
@@ -44,8 +45,6 @@
|
||||
"@testing-library/user-event": "13.5.0",
|
||||
"axios-mock-adapter": "1.20.0",
|
||||
"babel-plugin-react-intl": "8.2.25",
|
||||
"codecov": "3.8.3",
|
||||
"es-check": "6.2.1",
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"glob": "7.2.0",
|
||||
"husky": "7.0.4",
|
||||
@@ -1830,109 +1829,6 @@
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@caporal/core": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@caporal/core/-/core-2.0.2.tgz",
|
||||
"integrity": "sha512-o3J5aZINFWkkL+sL0DUs1dPHJjaetAAdwMRLbJ4U8aJW3K81E323IFMkFNYcOwTiPVhNzllC3USxZbU6xWFjFg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/lodash": "4.14.149",
|
||||
"@types/node": "13.9.3",
|
||||
"@types/table": "5.0.0",
|
||||
"@types/tabtab": "^3.0.1",
|
||||
"@types/wrap-ansi": "^3.0.0",
|
||||
"chalk": "3.0.0",
|
||||
"glob": "^7.1.6",
|
||||
"lodash": "4.17.15",
|
||||
"table": "5.4.6",
|
||||
"tabtab": "^3.0.2",
|
||||
"winston": "3.2.1",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@caporal/core/node_modules/@types/node": {
|
||||
"version": "13.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.3.tgz",
|
||||
"integrity": "sha512-01s+ac4qerwd6RHD+mVbOEsraDHSgUaefQlEdBbUolnQFjKwCr7luvAlEwW1RFojh67u0z4OUTjPn9LEl4zIkA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@caporal/core/node_modules/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,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@caporal/core/node_modules/chalk": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
|
||||
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@caporal/core/node_modules/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,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@caporal/core/node_modules/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
|
||||
},
|
||||
"node_modules/@caporal/core/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@caporal/core/node_modules/lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@caporal/core/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@cnakazawa/watch": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz",
|
||||
@@ -1949,15 +1845,6 @@
|
||||
"node": ">=0.1.95"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/@cospired/i18n-iso-languages": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@cospired/i18n-iso-languages/-/i18n-iso-languages-2.2.0.tgz",
|
||||
@@ -6549,12 +6436,6 @@
|
||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.14.149",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz",
|
||||
"integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
|
||||
@@ -6706,21 +6587,6 @@
|
||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/table": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/table/-/table-5.0.0.tgz",
|
||||
"integrity": "sha512-fQLtGLZXor264zUPWI95WNDsZ3QV43/c0lJpR/h1hhLJumXRmHNsrvBfEzW2YMhb0EWCsn4U6h82IgwsajAuTA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/tabtab": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/tabtab/-/tabtab-3.0.2.tgz",
|
||||
"integrity": "sha512-d8aOSJPS3SEGZevyr7vbAVUNPWGFmdFlk13vbPPK87vz+gYGM57L8T11k4wK2mOgQYZjEVYQEqmCTvupPoQBWw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tapable": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz",
|
||||
@@ -6802,12 +6668,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/wrap-ansi": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz",
|
||||
"integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
|
||||
@@ -7310,15 +7170,6 @@
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/argv": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz",
|
||||
"integrity": "sha512-dEamhpPEwRUBpLNHeuCm/v+g0anFByHahxodVO/BbAarHVBBg2MccCwf9K+o1Pof+2btdnkJelYVUWjW/VrATw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.6.10"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.1.3.tgz",
|
||||
@@ -9298,26 +9149,6 @@
|
||||
"node": ">= 0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/codecov": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/codecov/-/codecov-3.8.3.tgz",
|
||||
"integrity": "sha512-Y8Hw+V3HgR7V71xWH2vQ9lyS358CbGCldWlJFR0JirqoGtOoas3R3/OclRTvgUYFK29mmJICDPauVKmpqbwhOA==",
|
||||
"deprecated": "https://about.codecov.io/blog/codecov-uploader-deprecation-plan/",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"argv": "0.0.2",
|
||||
"ignore-walk": "3.0.4",
|
||||
"js-yaml": "3.14.1",
|
||||
"teeny-request": "7.1.1",
|
||||
"urlgrey": "1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"codecov": "bin/codecov"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/collect-v8-coverage": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
||||
@@ -9337,16 +9168,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
|
||||
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.3",
|
||||
"color-string": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
@@ -9362,16 +9183,6 @@
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/colord": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz",
|
||||
@@ -9384,22 +9195,6 @@
|
||||
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/colornames": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz",
|
||||
"integrity": "sha512-/pyV40IrsdulWv+wFPmERh9k/mjsPZ64yUMDmWrtj/k1nmgrzzIENWKdaVKyBbvFdQWqkcaRxr+polCo3VMe7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/colorspace": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
|
||||
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color": "^3.1.3",
|
||||
"text-hex": "1.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -10595,17 +10390,6 @@
|
||||
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
|
||||
"integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA=="
|
||||
},
|
||||
"node_modules/diagnostics": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz",
|
||||
"integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"colorspace": "1.1.x",
|
||||
"enabled": "1.0.x",
|
||||
"kuler": "1.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/diff-sequences": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz",
|
||||
@@ -10760,9 +10544,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz",
|
||||
"integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw=="
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz",
|
||||
"integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ=="
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "2.8.0",
|
||||
@@ -10969,15 +10753,6 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/enabled": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz",
|
||||
"integrity": "sha512-nnzgVSpB35qKrUN8358SjO1bYAmxoThECTWw9s3J0x5G8A9hokKHVDFzBjVpCoSryo6MhN8woVyascN5jheaNA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"env-variable": "0.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
@@ -11020,12 +10795,6 @@
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/env-variable": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz",
|
||||
"integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/envinfo": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz",
|
||||
@@ -11093,23 +10862,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/es-check": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/es-check/-/es-check-6.2.1.tgz",
|
||||
"integrity": "sha512-IPiRXUlwSTd2yMklIf9yEGe6GK5wCS8Sz1aTNHm1QSiYzI4aiq19giYbLi95tb+e0JJVKmcU0iQXQWW60a8V9A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@caporal/core": "^2.0.2",
|
||||
"acorn": "^8.7.0",
|
||||
"fast-glob": "^3.2.11"
|
||||
},
|
||||
"bin": {
|
||||
"es-check": "index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz",
|
||||
@@ -11142,12 +10894,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-promisify": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-6.1.1.tgz",
|
||||
"integrity": "sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
@@ -12376,21 +12122,6 @@
|
||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-url-parser": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz",
|
||||
"integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"punycode": "^1.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-url-parser/node_modules/punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "3.21.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz",
|
||||
@@ -12454,12 +12185,6 @@
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fecha": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/figures": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
||||
@@ -14125,15 +13850,6 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore-walk": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz",
|
||||
"integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/image-webpack-loader": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/image-webpack-loader/-/image-webpack-loader-8.1.0.tgz",
|
||||
@@ -19362,15 +19078,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/kuler": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz",
|
||||
"integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"colornames": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
@@ -19560,19 +19267,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz",
|
||||
"integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww=="
|
||||
},
|
||||
"node_modules/logform": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.4.0.tgz",
|
||||
"integrity": "sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.5.0",
|
||||
"fecha": "^4.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"triple-beam": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -20053,48 +19747,6 @@
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
@@ -20550,12 +20202,6 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/one-time": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz",
|
||||
"integrity": "sha512-qAMrwuk2xLEutlASoiPiAMW3EN3K96Ka/ilSXYr6qR1zSVXw2j7+yDSqGTC4T9apfLYxM3tLLjKvgPdAUK7kYQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
@@ -23613,15 +23259,6 @@
|
||||
"ret": "~0.1.10"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz",
|
||||
"integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@@ -24249,21 +23886,6 @@
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle/node_modules/is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/sisteransi": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||
@@ -24759,15 +24381,6 @@
|
||||
"integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/stack-trace": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-utils": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz",
|
||||
@@ -24906,15 +24519,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-events": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
|
||||
"integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"stubs": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strict-uri-encode": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
|
||||
@@ -25140,12 +24744,6 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/stubs": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
|
||||
"integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/style-loader": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz",
|
||||
@@ -25370,166 +24968,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tabtab": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tabtab/-/tabtab-3.0.2.tgz",
|
||||
"integrity": "sha512-jANKmUe0sIQc/zTALTBy186PoM/k6aPrh3A7p6AaAfF6WPSbTx1JYeGIGH162btpH+mmVEXln+UxwViZHO2Jhg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.0.1",
|
||||
"es6-promisify": "^6.0.0",
|
||||
"inquirer": "^6.0.0",
|
||||
"minimist": "^1.2.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"untildify": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tabtab/node_modules/ansi-escapes": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
|
||||
"integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tabtab/node_modules/ansi-regex": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
|
||||
"integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tabtab/node_modules/cli-cursor": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
|
||||
"integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"restore-cursor": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tabtab/node_modules/cli-width": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz",
|
||||
"integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tabtab/node_modules/figures": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
|
||||
"integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tabtab/node_modules/inquirer": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz",
|
||||
"integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-escapes": "^3.2.0",
|
||||
"chalk": "^2.4.2",
|
||||
"cli-cursor": "^2.1.0",
|
||||
"cli-width": "^2.0.0",
|
||||
"external-editor": "^3.0.3",
|
||||
"figures": "^2.0.0",
|
||||
"lodash": "^4.17.12",
|
||||
"mute-stream": "0.0.7",
|
||||
"run-async": "^2.2.0",
|
||||
"rxjs": "^6.4.0",
|
||||
"string-width": "^2.1.0",
|
||||
"strip-ansi": "^5.1.0",
|
||||
"through": "^2.3.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tabtab/node_modules/is-fullwidth-code-point": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
|
||||
"integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tabtab/node_modules/mimic-fn": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
|
||||
"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tabtab/node_modules/mute-stream": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
|
||||
"integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tabtab/node_modules/onetime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
|
||||
"integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mimic-fn": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tabtab/node_modules/restore-cursor": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
|
||||
"integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"onetime": "^2.0.0",
|
||||
"signal-exit": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tabtab/node_modules/string-width": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
||||
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-fullwidth-code-point": "^2.0.0",
|
||||
"strip-ansi": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tabtab/node_modules/string-width/node_modules/strip-ansi": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
|
||||
"integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||
@@ -25557,22 +24995,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/teeny-request": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.1.1.tgz",
|
||||
"integrity": "sha512-iwY6rkW5DDGq8hE2YgNQlKbptYpY5Nn2xecjQiNjOXWbKzPGUfmeUBCSQbbr306d7Z7U2N0TPl+/SwYRfua1Dg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"http-proxy-agent": "^4.0.0",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"stream-events": "^1.0.5",
|
||||
"uuid": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/temp-dir": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz",
|
||||
@@ -25693,12 +25115,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/text-hex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@@ -25906,12 +25322,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/triple-beam": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
|
||||
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tryer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",
|
||||
@@ -26222,15 +25632,6 @@
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/untildify": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.3.tgz",
|
||||
"integrity": "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
@@ -26296,15 +25697,6 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/urlgrey": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-1.0.0.tgz",
|
||||
"integrity": "sha512-hJfIzMPJmI9IlLkby8QrsCykQ+SXDeO2W5Q9QTW3QpqZVTx4a/K7p8/5q+/isD8vsbVaFgql/gvAoQCRQ2Cb5w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-url-parser": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/use": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||
@@ -27263,68 +26655,6 @@
|
||||
"integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/winston": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz",
|
||||
"integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"async": "^2.6.1",
|
||||
"diagnostics": "^1.1.1",
|
||||
"is-stream": "^1.1.0",
|
||||
"logform": "^2.1.1",
|
||||
"one-time": "0.0.4",
|
||||
"readable-stream": "^3.1.1",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
|
||||
"integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"logform": "^2.3.2",
|
||||
"readable-stream": "^3.6.0",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport/node_modules/readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/winston/node_modules/readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
@@ -28790,90 +28120,6 @@
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true
|
||||
},
|
||||
"@caporal/core": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@caporal/core/-/core-2.0.2.tgz",
|
||||
"integrity": "sha512-o3J5aZINFWkkL+sL0DUs1dPHJjaetAAdwMRLbJ4U8aJW3K81E323IFMkFNYcOwTiPVhNzllC3USxZbU6xWFjFg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/lodash": "4.14.149",
|
||||
"@types/node": "13.9.3",
|
||||
"@types/table": "5.0.0",
|
||||
"@types/tabtab": "^3.0.1",
|
||||
"@types/wrap-ansi": "^3.0.0",
|
||||
"chalk": "3.0.0",
|
||||
"glob": "^7.1.6",
|
||||
"lodash": "4.17.15",
|
||||
"table": "5.4.6",
|
||||
"tabtab": "^3.0.2",
|
||||
"winston": "3.2.1",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "13.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.3.tgz",
|
||||
"integrity": "sha512-01s+ac4qerwd6RHD+mVbOEsraDHSgUaefQlEdBbUolnQFjKwCr7luvAlEwW1RFojh67u0z4OUTjPn9LEl4zIkA==",
|
||||
"dev": true
|
||||
},
|
||||
"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": "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
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@cnakazawa/watch": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz",
|
||||
@@ -28884,12 +28130,6 @@
|
||||
"minimist": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"@colors/colors": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@cospired/i18n-iso-languages": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@cospired/i18n-iso-languages/-/i18n-iso-languages-2.2.0.tgz",
|
||||
@@ -32471,12 +31711,6 @@
|
||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.149",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz",
|
||||
"integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
|
||||
@@ -32627,21 +31861,6 @@
|
||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/table": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/table/-/table-5.0.0.tgz",
|
||||
"integrity": "sha512-fQLtGLZXor264zUPWI95WNDsZ3QV43/c0lJpR/h1hhLJumXRmHNsrvBfEzW2YMhb0EWCsn4U6h82IgwsajAuTA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/tabtab": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/tabtab/-/tabtab-3.0.2.tgz",
|
||||
"integrity": "sha512-d8aOSJPS3SEGZevyr7vbAVUNPWGFmdFlk13vbPPK87vz+gYGM57L8T11k4wK2mOgQYZjEVYQEqmCTvupPoQBWw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/tapable": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz",
|
||||
@@ -32720,12 +31939,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/wrap-ansi": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz",
|
||||
"integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/ws": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
|
||||
@@ -33128,12 +32341,6 @@
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"argv": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz",
|
||||
"integrity": "sha512-dEamhpPEwRUBpLNHeuCm/v+g0anFByHahxodVO/BbAarHVBBg2MccCwf9K+o1Pof+2btdnkJelYVUWjW/VrATw==",
|
||||
"dev": true
|
||||
},
|
||||
"aria-hidden": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.1.3.tgz",
|
||||
@@ -34682,19 +33889,6 @@
|
||||
"integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
|
||||
"dev": true
|
||||
},
|
||||
"codecov": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/codecov/-/codecov-3.8.3.tgz",
|
||||
"integrity": "sha512-Y8Hw+V3HgR7V71xWH2vQ9lyS358CbGCldWlJFR0JirqoGtOoas3R3/OclRTvgUYFK29mmJICDPauVKmpqbwhOA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"argv": "0.0.2",
|
||||
"ignore-walk": "3.0.4",
|
||||
"js-yaml": "3.14.1",
|
||||
"teeny-request": "7.1.1",
|
||||
"urlgrey": "1.0.0"
|
||||
}
|
||||
},
|
||||
"collect-v8-coverage": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
||||
@@ -34711,16 +33905,6 @@
|
||||
"object-visit": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
|
||||
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^1.9.3",
|
||||
"color-string": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
@@ -34736,16 +33920,6 @@
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||
"dev": true
|
||||
},
|
||||
"color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"colord": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz",
|
||||
@@ -34758,22 +33932,6 @@
|
||||
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
|
||||
"dev": true
|
||||
},
|
||||
"colornames": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz",
|
||||
"integrity": "sha512-/pyV40IrsdulWv+wFPmERh9k/mjsPZ64yUMDmWrtj/k1nmgrzzIENWKdaVKyBbvFdQWqkcaRxr+polCo3VMe7A==",
|
||||
"dev": true
|
||||
},
|
||||
"colorspace": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
|
||||
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color": "^3.1.3",
|
||||
"text-hex": "1.0.x"
|
||||
}
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -35689,17 +34847,6 @@
|
||||
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
|
||||
"integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA=="
|
||||
},
|
||||
"diagnostics": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz",
|
||||
"integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"colorspace": "1.1.x",
|
||||
"enabled": "1.0.x",
|
||||
"kuler": "1.0.x"
|
||||
}
|
||||
},
|
||||
"diff-sequences": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz",
|
||||
@@ -35825,9 +34972,9 @@
|
||||
}
|
||||
},
|
||||
"dompurify": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz",
|
||||
"integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw=="
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz",
|
||||
"integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ=="
|
||||
},
|
||||
"domutils": {
|
||||
"version": "2.8.0",
|
||||
@@ -35995,15 +35142,6 @@
|
||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
|
||||
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="
|
||||
},
|
||||
"enabled": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz",
|
||||
"integrity": "sha512-nnzgVSpB35qKrUN8358SjO1bYAmxoThECTWw9s3J0x5G8A9hokKHVDFzBjVpCoSryo6MhN8woVyascN5jheaNA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"env-variable": "0.0.x"
|
||||
}
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
@@ -36034,12 +35172,6 @@
|
||||
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
|
||||
"dev": true
|
||||
},
|
||||
"env-variable": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz",
|
||||
"integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==",
|
||||
"dev": true
|
||||
},
|
||||
"envinfo": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz",
|
||||
@@ -36095,17 +35227,6 @@
|
||||
"unbox-primitive": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"es-check": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/es-check/-/es-check-6.2.1.tgz",
|
||||
"integrity": "sha512-IPiRXUlwSTd2yMklIf9yEGe6GK5wCS8Sz1aTNHm1QSiYzI4aiq19giYbLi95tb+e0JJVKmcU0iQXQWW60a8V9A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@caporal/core": "^2.0.2",
|
||||
"acorn": "^8.7.0",
|
||||
"fast-glob": "^3.2.11"
|
||||
}
|
||||
},
|
||||
"es-module-lexer": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz",
|
||||
@@ -36132,12 +35253,6 @@
|
||||
"is-symbol": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"es6-promisify": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-6.1.1.tgz",
|
||||
"integrity": "sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg==",
|
||||
"dev": true
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
@@ -37111,23 +36226,6 @@
|
||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||
"dev": true
|
||||
},
|
||||
"fast-url-parser": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz",
|
||||
"integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"punycode": "^1.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"fast-xml-parser": {
|
||||
"version": "3.21.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz",
|
||||
@@ -37181,12 +36279,6 @@
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"fecha": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||
"dev": true
|
||||
},
|
||||
"figures": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
||||
@@ -38401,15 +37493,6 @@
|
||||
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
|
||||
"dev": true
|
||||
},
|
||||
"ignore-walk": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz",
|
||||
"integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"image-webpack-loader": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/image-webpack-loader/-/image-webpack-loader-8.1.0.tgz",
|
||||
@@ -42369,15 +41452,6 @@
|
||||
"integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"kuler": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz",
|
||||
"integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"colornames": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
@@ -42536,19 +41610,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz",
|
||||
"integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww=="
|
||||
},
|
||||
"logform": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.4.0.tgz",
|
||||
"integrity": "sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@colors/colors": "1.5.0",
|
||||
"fecha": "^4.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"triple-beam": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -42932,39 +41993,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"dev": true
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"dev": true
|
||||
},
|
||||
"whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
@@ -43316,12 +42344,6 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"one-time": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz",
|
||||
"integrity": "sha512-qAMrwuk2xLEutlASoiPiAMW3EN3K96Ka/ilSXYr6qR1zSVXw2j7+yDSqGTC4T9apfLYxM3tLLjKvgPdAUK7kYQ==",
|
||||
"dev": true
|
||||
},
|
||||
"onetime": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
@@ -45537,12 +44559,6 @@
|
||||
"ret": "~0.1.10"
|
||||
}
|
||||
},
|
||||
"safe-stable-stringify": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz",
|
||||
"integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==",
|
||||
"dev": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@@ -46051,23 +45067,6 @@
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"dev": true
|
||||
},
|
||||
"simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"sisteransi": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||
@@ -46491,12 +45490,6 @@
|
||||
"integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
|
||||
"dev": true
|
||||
},
|
||||
"stack-trace": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
|
||||
"dev": true
|
||||
},
|
||||
"stack-utils": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz",
|
||||
@@ -46610,15 +45603,6 @@
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"dev": true
|
||||
},
|
||||
"stream-events": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
|
||||
"integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"stubs": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"strict-uri-encode": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
|
||||
@@ -46799,12 +45783,6 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"stubs": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
|
||||
"integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
|
||||
"dev": true
|
||||
},
|
||||
"style-loader": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz",
|
||||
@@ -46978,137 +45956,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"tabtab": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tabtab/-/tabtab-3.0.2.tgz",
|
||||
"integrity": "sha512-jANKmUe0sIQc/zTALTBy186PoM/k6aPrh3A7p6AaAfF6WPSbTx1JYeGIGH162btpH+mmVEXln+UxwViZHO2Jhg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^4.0.1",
|
||||
"es6-promisify": "^6.0.0",
|
||||
"inquirer": "^6.0.0",
|
||||
"minimist": "^1.2.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"untildify": "^3.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-escapes": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
|
||||
"integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
|
||||
"integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==",
|
||||
"dev": true
|
||||
},
|
||||
"cli-cursor": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
|
||||
"integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"restore-cursor": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"cli-width": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz",
|
||||
"integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==",
|
||||
"dev": true
|
||||
},
|
||||
"figures": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
|
||||
"integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"escape-string-regexp": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"inquirer": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz",
|
||||
"integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-escapes": "^3.2.0",
|
||||
"chalk": "^2.4.2",
|
||||
"cli-cursor": "^2.1.0",
|
||||
"cli-width": "^2.0.0",
|
||||
"external-editor": "^3.0.3",
|
||||
"figures": "^2.0.0",
|
||||
"lodash": "^4.17.12",
|
||||
"mute-stream": "0.0.7",
|
||||
"run-async": "^2.2.0",
|
||||
"rxjs": "^6.4.0",
|
||||
"string-width": "^2.1.0",
|
||||
"strip-ansi": "^5.1.0",
|
||||
"through": "^2.3.6"
|
||||
}
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
|
||||
"integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
|
||||
"dev": true
|
||||
},
|
||||
"mimic-fn": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
|
||||
"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
|
||||
"dev": true
|
||||
},
|
||||
"mute-stream": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
|
||||
"integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==",
|
||||
"dev": true
|
||||
},
|
||||
"onetime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
|
||||
"integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mimic-fn": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"restore-cursor": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
|
||||
"integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"onetime": "^2.0.0",
|
||||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"string-width": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
||||
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-fullwidth-code-point": "^2.0.0",
|
||||
"strip-ansi": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"strip-ansi": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
|
||||
"integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tapable": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||
@@ -47130,19 +45977,6 @@
|
||||
"xtend": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"teeny-request": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.1.1.tgz",
|
||||
"integrity": "sha512-iwY6rkW5DDGq8hE2YgNQlKbptYpY5Nn2xecjQiNjOXWbKzPGUfmeUBCSQbbr306d7Z7U2N0TPl+/SwYRfua1Dg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"http-proxy-agent": "^4.0.0",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"stream-events": "^1.0.5",
|
||||
"uuid": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"temp-dir": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz",
|
||||
@@ -47221,12 +46055,6 @@
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"text-hex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
|
||||
"dev": true
|
||||
},
|
||||
"text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@@ -47402,12 +46230,6 @@
|
||||
"escape-string-regexp": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"triple-beam": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
|
||||
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==",
|
||||
"dev": true
|
||||
},
|
||||
"tryer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",
|
||||
@@ -47653,12 +46475,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"untildify": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.3.tgz",
|
||||
"integrity": "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==",
|
||||
"dev": true
|
||||
},
|
||||
"uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
@@ -47701,15 +46517,6 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"urlgrey": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-1.0.0.tgz",
|
||||
"integrity": "sha512-hJfIzMPJmI9IlLkby8QrsCykQ+SXDeO2W5Q9QTW3QpqZVTx4a/K7p8/5q+/isD8vsbVaFgql/gvAoQCRQ2Cb5w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-url-parser": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"use": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||
@@ -48410,60 +47217,6 @@
|
||||
"integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==",
|
||||
"dev": true
|
||||
},
|
||||
"winston": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz",
|
||||
"integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"async": "^2.6.1",
|
||||
"diagnostics": "^1.1.1",
|
||||
"is-stream": "^1.1.0",
|
||||
"logform": "^2.1.1",
|
||||
"one-time": "0.0.4",
|
||||
"readable-stream": "^3.1.1",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"winston-transport": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
|
||||
"integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"logform": "^2.3.2",
|
||||
"readable-stream": "^3.6.0",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
|
||||
@@ -43,13 +43,13 @@
|
||||
"babel-polyfill": "6.26.0",
|
||||
"classnames": "2.3.1",
|
||||
"core-js": "3.21.1",
|
||||
"dompurify": "^2.4.3",
|
||||
"formik": "2.2.9",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"prop-types": "15.8.1",
|
||||
"raw-loader": "4.0.2",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-mathjax-preview": "2.2.6",
|
||||
"react-redux": "7.2.6",
|
||||
"react-router": "5.2.1",
|
||||
"react-router-dom": "5.3.0",
|
||||
@@ -68,8 +68,6 @@
|
||||
"@testing-library/user-event": "13.5.0",
|
||||
"axios-mock-adapter": "1.20.0",
|
||||
"babel-plugin-react-intl": "8.2.25",
|
||||
"codecov": "3.8.3",
|
||||
"es-check": "6.2.1",
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"glob": "7.2.0",
|
||||
"husky": "7.0.4",
|
||||
|
||||
@@ -9,6 +9,43 @@
|
||||
href="<%=htmlWebpackPlugin.options.FAVICON_URL%>"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<script>
|
||||
window.MathJax = {
|
||||
tex: {
|
||||
inlineMath: [
|
||||
['$', '$'],
|
||||
['\\\\(', '\\\\)'],
|
||||
['\\(', '\\)'],
|
||||
['[mathjaxinline]', '[/mathjaxinline]'],
|
||||
['\\begin{math}', '\\end{math}'],
|
||||
],
|
||||
displayMath: [
|
||||
['[mathjax]', '[/mathjax]'],
|
||||
['$$', '$$'],
|
||||
['\\\\[', '\\\\]'],
|
||||
['\\[', '\\]'],
|
||||
['\\begin{displaymath}', '\\end{displaymath}'],
|
||||
['\\begin{equation}', '\\end{equation}'],
|
||||
],
|
||||
processEscapes: true,
|
||||
processEnvironments: true,
|
||||
autoload: {
|
||||
color: [],
|
||||
colorv2: ['color']
|
||||
},
|
||||
packages: {'[+]': ['noerrors']}
|
||||
},
|
||||
options: {
|
||||
ignoreHtmlClass: 'tex2jax_ignore',
|
||||
processHtmlClass: 'tex2jax_process'
|
||||
},
|
||||
loader: {
|
||||
load: ['input/asciimath', '[tex]/noerrors']
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" class="small"></div>
|
||||
|
||||
@@ -1,53 +1,38 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import MathJax from 'react-mathjax-preview';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
const baseConfig = {
|
||||
showMathMenu: true,
|
||||
tex2jax: {
|
||||
inlineMath: [
|
||||
['$', '$'],
|
||||
['\\\\(', '\\\\)'],
|
||||
['\\(', '\\)'],
|
||||
['[mathjaxinline]', '[/mathjaxinline]'],
|
||||
['\\begin{math}', '\\end{math}'],
|
||||
],
|
||||
displayMath: [
|
||||
['[mathjax]', '[/mathjax]'],
|
||||
['$$', '$$'],
|
||||
['\\\\[', '\\\\]'],
|
||||
['\\[', '\\]'],
|
||||
['\\begin{displaymath}', '\\end{displaymath}'],
|
||||
['\\begin{equation}', '\\end{equation}'],
|
||||
],
|
||||
},
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
skipStartupTypeset: true,
|
||||
const defaultSanitizeOptions = {
|
||||
USE_PROFILES: { html: true },
|
||||
ADD_ATTR: ['columnalign'],
|
||||
};
|
||||
|
||||
function HTMLLoader({ htmlNode, componentId, cssClassName }) {
|
||||
const isLatex = htmlNode.match(/(\${1,2})((?:\\.|.)*)\1/)
|
||||
|| htmlNode.match(/(\[mathjax](.+?)\[\/mathjax])+/)
|
||||
|| htmlNode.match(/(\[mathjaxinline](.+?)\[\/mathjaxinline])+/)
|
||||
|| htmlNode.match(/(\\begin\{math}(.+?)\\end\{math})+/)
|
||||
|| htmlNode.match(/(\\begin\{displaymath}(.+?)\\end\{displaymath})+/)
|
||||
|| htmlNode.match(/(\\begin\{equation}(.+?)\\end\{equation})+/)
|
||||
|| htmlNode.match(/(\\\[(.+?)\\\])+/)
|
||||
|| htmlNode.match(/(\\\((.+?)\\\))+/);
|
||||
function HTMLLoader({
|
||||
htmlNode, componentId, cssClassName, testId,
|
||||
}) {
|
||||
const sanitizedMath = DOMPurify.sanitize(htmlNode, { ...defaultSanitizeOptions });
|
||||
const previewRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
let promise = Promise.resolve(); // Used to hold chain of typesetting calls
|
||||
|
||||
function typeset(code) {
|
||||
promise = promise.then(() => window.MathJax?.typesetPromise(code()))
|
||||
.catch((err) => logError(`Typeset failed: ${err.message}`));
|
||||
return promise;
|
||||
}
|
||||
|
||||
typeset(() => {
|
||||
previewRef.current.innerHTML = sanitizedMath;
|
||||
});
|
||||
}, [htmlNode]);
|
||||
|
||||
return (
|
||||
isLatex ? (
|
||||
<MathJax
|
||||
math={htmlNode}
|
||||
id={componentId}
|
||||
className={cssClassName}
|
||||
sanitizeOptions={{ USE_PROFILES: { html: true } }}
|
||||
config={baseConfig}
|
||||
/>
|
||||
)
|
||||
// eslint-disable-next-line react/no-danger
|
||||
: <div className={cssClassName} id={componentId} dangerouslySetInnerHTML={{ __html: htmlNode }} />
|
||||
<div ref={previewRef} className={cssClassName} id={componentId} data-testid={testId} />
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,12 +40,14 @@ HTMLLoader.propTypes = {
|
||||
htmlNode: PropTypes.node,
|
||||
componentId: PropTypes.string,
|
||||
cssClassName: PropTypes.string,
|
||||
testId: PropTypes.string,
|
||||
};
|
||||
|
||||
HTMLLoader.defaultProps = {
|
||||
htmlNode: '',
|
||||
componentId: null,
|
||||
cssClassName: '',
|
||||
testId: '',
|
||||
};
|
||||
|
||||
export default HTMLLoader;
|
||||
|
||||
@@ -29,7 +29,7 @@ function PostPreviewPane({
|
||||
className="float-right p-3"
|
||||
iconClassNames="icon-size"
|
||||
/>
|
||||
<HTMLLoader htmlNode={htmlNode} cssClassName="text-primary" />
|
||||
<HTMLLoader htmlNode={htmlNode} cssClassName="text-primary" componentId="post-preview" testId="post-preview" />
|
||||
</div>
|
||||
)}
|
||||
<div className="d-flex justify-content-end">
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React, { useContext, useEffect, useMemo } from 'react';
|
||||
import React, {
|
||||
useContext, useEffect, useMemo, useState,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
@@ -8,7 +10,8 @@ import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Icon, IconButton, Spinner,
|
||||
Button, Icon, IconButton,
|
||||
Spinner,
|
||||
} from '@edx/paragon';
|
||||
import { ArrowBack } from '@edx/paragon/icons';
|
||||
|
||||
@@ -17,12 +20,12 @@ import {
|
||||
} from '../../data/constants';
|
||||
import { useDispatchWithState } from '../../data/hooks';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { useIsOnDesktop } from '../data/hooks';
|
||||
import { useIsOnDesktop, useUserCanAddThreadInBlackoutDate } from '../data/hooks';
|
||||
import { EmptyPage } from '../empty-posts';
|
||||
import { Post } from '../posts';
|
||||
import { selectThread } from '../posts/data/selectors';
|
||||
import { fetchThread, markThreadAsRead } from '../posts/data/thunks';
|
||||
import { discussionsPath, filterPosts } from '../utils';
|
||||
import { discussionsPath, filterPosts, isLastElementOfList } from '../utils';
|
||||
import { selectThreadComments, selectThreadCurrentPage, selectThreadHasMorePages } from './data/selectors';
|
||||
import { fetchThreadComments } from './data/thunks';
|
||||
import { Comment, ResponseEditor } from './comment';
|
||||
@@ -80,26 +83,41 @@ function DiscussionCommentsView({
|
||||
|
||||
const endorsedComments = useMemo(() => [...filterPosts(comments, 'endorsed')], [comments]);
|
||||
const unEndorsedComments = useMemo(() => [...filterPosts(comments, 'unendorsed')], [comments]);
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const [addingResponse, setAddingResponse] = useState(false);
|
||||
|
||||
const handleDefinition = (message, commentsLength) => (
|
||||
<div className="mx-4 text-primary-700" role="heading" aria-level="2" style={{ lineHeight: '28px' }}>
|
||||
<div
|
||||
className="mx-4 my-14px text-gray-700 font-style-normal font-family-inter"
|
||||
role="heading"
|
||||
aria-level="2"
|
||||
style={{ lineHeight: '24px' }}
|
||||
>
|
||||
{intl.formatMessage(message, { num: commentsLength })}
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleComments = (postComments, showAddResponse = false, showLoadMoreResponses = false) => (
|
||||
const handleComments = (postComments, showLoadMoreResponses = false) => (
|
||||
<div className="mx-4" role="list">
|
||||
{postComments.map(comment => (
|
||||
<Comment comment={comment} key={comment.id} postType={postType} isClosedPost={isClosed} />
|
||||
{postComments.map((comment) => (
|
||||
<Comment
|
||||
comment={comment}
|
||||
key={comment.id}
|
||||
postType={postType}
|
||||
isClosedPost={isClosed}
|
||||
marginBottom={isLastElementOfList(postComments, comment)}
|
||||
/>
|
||||
|
||||
))}
|
||||
{hasMorePages && !isLoading && !showLoadMoreResponses && (
|
||||
<Button
|
||||
onClick={handleLoadMoreResponses}
|
||||
variant="link"
|
||||
block="true"
|
||||
className="card p-4 mb-4 font-weight-500 font-size-14"
|
||||
className="px-4 mt-3 py-0 mb-2 font-style-normal font-family-inter font-weight-500 font-size-14"
|
||||
style={{
|
||||
lineHeight: '20px',
|
||||
lineHeight: '24px',
|
||||
border: '0px',
|
||||
}}
|
||||
data-testid="load-more-comments"
|
||||
>
|
||||
@@ -107,12 +125,10 @@ function DiscussionCommentsView({
|
||||
</Button>
|
||||
)}
|
||||
{isLoading && !showLoadMoreResponses && (
|
||||
<div className="card my-4 p-4 d-flex align-items-center">
|
||||
<Spinner animation="border" variant="primary" />
|
||||
<div className="mb-2 mt-3 d-flex justify-content-center">
|
||||
<Spinner animation="border" variant="primary" className="spinner-dimentions" />
|
||||
</div>
|
||||
)}
|
||||
{!!postComments.length && !isClosed && showAddResponse
|
||||
&& <ResponseEditor postId={postId} addWrappingDiv />}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
@@ -123,15 +139,40 @@ function DiscussionCommentsView({
|
||||
<>
|
||||
{handleDefinition(messages.endorsedResponseCount, endorsedComments.length)}
|
||||
{endorsed === EndorsementStatus.DISCUSSION
|
||||
? handleComments(endorsedComments, false, true)
|
||||
: handleComments(endorsedComments)}
|
||||
? handleComments(endorsedComments, true)
|
||||
: handleComments(endorsedComments, false)}
|
||||
</>
|
||||
)}
|
||||
{endorsed !== EndorsementStatus.ENDORSED && (
|
||||
<>
|
||||
{handleDefinition(messages.responseCount, unEndorsedComments.length)}
|
||||
{unEndorsedComments.length === 0 && <br />}
|
||||
{handleComments(unEndorsedComments, true)}
|
||||
{handleComments(unEndorsedComments, false)}
|
||||
{(userCanAddThreadInBlackoutDate && !!unEndorsedComments.length && !isClosed) && (
|
||||
<div className="mx-4">
|
||||
{!addingResponse && (
|
||||
<Button
|
||||
variant="plain"
|
||||
block="true"
|
||||
className="card mb-4 px-0 py-10px mt-2 font-style-normal font-family-inter font-weight-500 font-size-14 text-primary-500"
|
||||
style={{
|
||||
lineHeight: '24px',
|
||||
border: '0px',
|
||||
}}
|
||||
onClick={() => setAddingResponse(true)}
|
||||
data-testid="add-response"
|
||||
>
|
||||
{intl.formatMessage(messages.addResponse)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<ResponseEditor
|
||||
postId={postId}
|
||||
handleCloseEditor={() => setAddingResponse(false)}
|
||||
addingResponse={addingResponse}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@@ -158,12 +199,14 @@ function CommentsView({ intl }) {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const isOnDesktop = useIsOnDesktop();
|
||||
const [addingResponse, setAddingResponse] = useState(false);
|
||||
const {
|
||||
courseId, learnerUsername, category, topicId, page, enableInContextSidebar,
|
||||
} = useContext(DiscussionContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!thread) { submitDispatch(fetchThread(postId, courseId, true)); }
|
||||
setAddingResponse(false);
|
||||
}, [postId]);
|
||||
|
||||
if (!thread) {
|
||||
@@ -173,7 +216,13 @@ function CommentsView({ intl }) {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Spinner animation="border" variant="primary" data-testid="loading-indicator" />
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
}}
|
||||
>
|
||||
<Spinner animation="border" variant="primary" data-testid="loading-indicator" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -211,41 +260,52 @@ function CommentsView({ intl }) {
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div className={classNames('discussion-comments d-flex flex-column card', {
|
||||
'm-4 p-4.5': !enableInContextSidebar,
|
||||
'p-4 rounded-0 border-0 mb-4': enableInContextSidebar,
|
||||
})}
|
||||
<div
|
||||
className={classNames('discussion-comments d-flex flex-column card border-0', {
|
||||
'post-card-margin post-card-padding': !enableInContextSidebar,
|
||||
'post-card-padding rounded-0 border-0 mb-4': enableInContextSidebar,
|
||||
})}
|
||||
>
|
||||
<Post post={thread} />
|
||||
{!thread.closed && <ResponseEditor postId={postId} />}
|
||||
<Post post={thread} handleAddResponseButton={() => setAddingResponse(true)} />
|
||||
{!thread.closed && (
|
||||
<ResponseEditor
|
||||
postId={postId}
|
||||
handleCloseEditor={() => setAddingResponse(false)}
|
||||
addingResponse={addingResponse}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{thread.type === ThreadType.DISCUSSION && (
|
||||
<DiscussionCommentsView
|
||||
postId={postId}
|
||||
intl={intl}
|
||||
postType={thread.type}
|
||||
endorsed={EndorsementStatus.DISCUSSION}
|
||||
isClosed={thread.closed}
|
||||
/>
|
||||
)}
|
||||
{thread.type === ThreadType.QUESTION && (
|
||||
<>
|
||||
{
|
||||
thread.type === ThreadType.DISCUSSION && (
|
||||
<DiscussionCommentsView
|
||||
postId={postId}
|
||||
intl={intl}
|
||||
postType={thread.type}
|
||||
endorsed={EndorsementStatus.ENDORSED}
|
||||
endorsed={EndorsementStatus.DISCUSSION}
|
||||
isClosed={thread.closed}
|
||||
/>
|
||||
<DiscussionCommentsView
|
||||
postId={postId}
|
||||
intl={intl}
|
||||
postType={thread.type}
|
||||
endorsed={EndorsementStatus.UNENDORSED}
|
||||
isClosed={thread.closed}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
)
|
||||
}
|
||||
{
|
||||
thread.type === ThreadType.QUESTION && (
|
||||
<>
|
||||
<DiscussionCommentsView
|
||||
postId={postId}
|
||||
intl={intl}
|
||||
postType={thread.type}
|
||||
endorsed={EndorsementStatus.ENDORSED}
|
||||
isClosed={thread.closed}
|
||||
/>
|
||||
<DiscussionCommentsView
|
||||
postId={postId}
|
||||
intl={intl}
|
||||
postType={thread.type}
|
||||
endorsed={EndorsementStatus.UNENDORSED}
|
||||
isClosed={thread.closed}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ describe('CommentsView', () => {
|
||||
|
||||
it('should show and hide the editor', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
await waitFor(() => screen.findByText('comment number 1', { exact: false }));
|
||||
await screen.findByTestId('thread-1');
|
||||
const addResponseButtons = screen.getAllByRole('button', { name: /add a response/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
@@ -178,7 +178,7 @@ describe('CommentsView', () => {
|
||||
|
||||
it('should allow posting a response', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
await waitFor(() => screen.findByText('comment number 1', { exact: false }));
|
||||
await screen.findByTestId('thread-1');
|
||||
const responseButtons = screen.getAllByRole('button', { name: /add a response/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
@@ -195,21 +195,25 @@ describe('CommentsView', () => {
|
||||
);
|
||||
});
|
||||
expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument();
|
||||
await waitFor(async () => expect(await screen.findByText('testing123', { exact: false })).toBeInTheDocument());
|
||||
await waitFor(async () => expect(await screen.findByTestId('comment-1')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should not allow posting a response on a closed post', async () => {
|
||||
renderComponent(closedPostId);
|
||||
await waitFor(() => screen.findByText('Thread-2', { exact: false }));
|
||||
expect(screen.queryByRole('button', { name: /add a response/i })).not.toBeInTheDocument();
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByText('Thread-2', { exact: false })));
|
||||
});
|
||||
expect(screen.queryByRole('button', { name: /add response/i }, { hidden: false })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should allow posting a comment', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
await waitFor(() => screen.findByText('comment number 1', { exact: false }));
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
screen.getAllByRole('button', { name: /add a comment/i })[0],
|
||||
screen.getAllByRole('button', { name: /add comment/i })[0],
|
||||
);
|
||||
});
|
||||
act(() => {
|
||||
@@ -222,22 +226,26 @@ describe('CommentsView', () => {
|
||||
);
|
||||
});
|
||||
expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument();
|
||||
await waitFor(async () => expect(await screen.findByText('testing123', { exact: false })).toBeInTheDocument());
|
||||
await waitFor(async () => expect(await screen.findByTestId('comment-comment-1')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should not allow posting a comment on a closed post', async () => {
|
||||
renderComponent(closedPostId);
|
||||
await waitFor(() => screen.findByText('thread-2', { exact: false }));
|
||||
await screen.findByTestId('thread-2');
|
||||
|
||||
await act(async () => {
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /add a comment/i }),
|
||||
).not.toBeInTheDocument();
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-3')));
|
||||
});
|
||||
|
||||
const addCommentButton = screen.getAllByRole('button', { name: /add comment/i }, { hidden: false })[0];
|
||||
expect(addCommentButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should allow editing an existing comment', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
await waitFor(() => screen.findByText('comment number 1', { exact: false }));
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
// The first edit menu is for the post, the second will be for the first comment.
|
||||
@@ -254,7 +262,7 @@ describe('CommentsView', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
|
||||
});
|
||||
await waitFor(async () => {
|
||||
expect(await screen.findByText('testing123', { exact: false })).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('comment-1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -278,7 +286,9 @@ describe('CommentsView', () => {
|
||||
it('should show reason codes when editing an existing comment', async () => {
|
||||
setupCourseConfig();
|
||||
renderComponent(discussionPostId);
|
||||
await waitFor(() => screen.findByText('comment number 1', { exact: false }));
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
// The first edit menu is for the post, the second will be for the first comment.
|
||||
@@ -291,10 +301,12 @@ describe('CommentsView', () => {
|
||||
expect(screen.queryByRole('combobox', { name: /reason for editing/i })).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('option', { name: /reason \d/i })).toHaveLength(2);
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.queryByRole('combobox', { name: /reason for editing/i }), { target: { value: null } });
|
||||
fireEvent.change(screen.queryByRole('combobox', { name: /reason for editing/i }),
|
||||
{ target: { value: null } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.queryByRole('combobox', { name: /reason for editing/i }), { target: { value: 'reason-1' } });
|
||||
fireEvent.change(screen.queryByRole('combobox',
|
||||
{ name: /reason for editing/i }), { target: { value: 'reason-1' } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } });
|
||||
@@ -308,6 +320,9 @@ describe('CommentsView', () => {
|
||||
it('should show reason codes when closing a post', async () => {
|
||||
setupCourseConfig();
|
||||
renderComponent(discussionPostId);
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('post-thread-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
// The first edit menu is for the post
|
||||
@@ -336,6 +351,9 @@ describe('CommentsView', () => {
|
||||
it('should close the post directly if reason codes are not enabled', async () => {
|
||||
setupCourseConfig(false);
|
||||
renderComponent(discussionPostId);
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('post-thread-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
// The first edit menu is for the post
|
||||
@@ -355,6 +373,9 @@ describe('CommentsView', () => {
|
||||
async (reasonCodesEnabled) => {
|
||||
setupCourseConfig(reasonCodesEnabled);
|
||||
renderComponent(closedPostId);
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByText('Thread-2', { exact: false })));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
// The first edit menu is for the post
|
||||
@@ -373,6 +394,9 @@ describe('CommentsView', () => {
|
||||
it('should show the editor if the post is edited', async () => {
|
||||
setupCourseConfig(false);
|
||||
renderComponent(discussionPostId);
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('post-thread-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
// The first edit menu is for the post
|
||||
@@ -387,6 +411,9 @@ describe('CommentsView', () => {
|
||||
|
||||
it('should allow pinning the post', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('post-thread-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
// The first edit menu is for the post
|
||||
@@ -401,6 +428,9 @@ describe('CommentsView', () => {
|
||||
|
||||
it('should allow reporting the post', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('post-thread-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
// The first edit menu is for the post
|
||||
@@ -422,7 +452,9 @@ describe('CommentsView', () => {
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
// Wait for the content to load
|
||||
await screen.findByText('comment number 7', { exact: false });
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-7')));
|
||||
});
|
||||
const view = screen.getByTestId('comment-comment-1');
|
||||
|
||||
const likeButton = within(view).getByRole('button', { name: /like/i });
|
||||
@@ -436,13 +468,8 @@ describe('CommentsView', () => {
|
||||
it('handles endorsing comments', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
// Wait for the content to load
|
||||
await screen.findByText('comment number 7', { exact: false });
|
||||
|
||||
// There should be three buttons, one for the post, the second for the
|
||||
// comment and the third for a response to that comment
|
||||
const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(actionButtons[1]);
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-7')));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -455,13 +482,12 @@ describe('CommentsView', () => {
|
||||
it('handles reporting comments', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
// Wait for the content to load
|
||||
await screen.findByText('comment number 7', { exact: false });
|
||||
|
||||
// There should be three buttons, one for the post, the second for the
|
||||
// comment and the third for a response to that comment
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-7')));
|
||||
});
|
||||
const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(actionButtons[1]);
|
||||
fireEvent.click(actionButtons[0]);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -488,9 +514,9 @@ describe('CommentsView', () => {
|
||||
|
||||
it('initially loads only the first page', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
expect(await screen.findByText('comment number 1', { exact: false }))
|
||||
expect(await screen.findByTestId('comment-1'))
|
||||
.toBeInTheDocument();
|
||||
expect(screen.queryByText('comment number 2', { exact: false }))
|
||||
expect(screen.queryByTestId('comment-2'))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
@@ -501,8 +527,8 @@ describe('CommentsView', () => {
|
||||
const loadMoreButton = await findLoadMoreCommentsButton();
|
||||
fireEvent.click(loadMoreButton);
|
||||
|
||||
await screen.findByText('comment number 1', { exact: false });
|
||||
await screen.findByText('comment number 2', { exact: false });
|
||||
await screen.findByTestId('comment-1');
|
||||
await screen.findByTestId('comment-2');
|
||||
});
|
||||
|
||||
it('newly loaded comments are appended to the old ones', async () => {
|
||||
@@ -511,9 +537,9 @@ describe('CommentsView', () => {
|
||||
const loadMoreButton = await findLoadMoreCommentsButton();
|
||||
fireEvent.click(loadMoreButton);
|
||||
|
||||
await screen.findByText('comment number 1', { exact: false });
|
||||
await screen.findByTestId('comment-1');
|
||||
// check that comments from the first page are also displayed
|
||||
expect(screen.queryByText('comment number 2', { exact: false }))
|
||||
expect(screen.queryByTestId('comment-2'))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -526,7 +552,7 @@ describe('CommentsView', () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
}
|
||||
|
||||
await screen.findByText('comment number 2', { exact: false });
|
||||
await screen.findByTestId('comment-2');
|
||||
await expect(findLoadMoreCommentsButton())
|
||||
.rejects
|
||||
.toThrow();
|
||||
@@ -538,11 +564,11 @@ describe('CommentsView', () => {
|
||||
|
||||
it('initially loads only the first page', async () => {
|
||||
act(() => renderComponent(questionPostId));
|
||||
expect(await screen.findByText('comment number 3', { exact: false }))
|
||||
expect(await screen.findByTestId('comment-3'))
|
||||
.toBeInTheDocument();
|
||||
expect(await screen.findByText('endorsed comment number 5', { exact: false }))
|
||||
expect(await screen.findByTestId('comment-5'))
|
||||
.toBeInTheDocument();
|
||||
expect(screen.queryByText('comment number 4', { exact: false }))
|
||||
expect(screen.queryByTestId('comment-4'))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
@@ -555,15 +581,15 @@ describe('CommentsView', () => {
|
||||
const [loadMoreButtonEndorsed, loadMoreButtonUnendorsed] = await findLoadMoreCommentsButtons();
|
||||
// Both load more buttons should show
|
||||
expect(await findLoadMoreCommentsButtons()).toHaveLength(2);
|
||||
expect(await screen.findByText('unendorsed comment number 3', { exact: false }))
|
||||
expect(await screen.findByTestId('comment-3'))
|
||||
.toBeInTheDocument();
|
||||
expect(await screen.findByText('endorsed comment number 5', { exact: false }))
|
||||
expect(await screen.findByTestId('comment-5'))
|
||||
.toBeInTheDocument();
|
||||
// Comments from next page should not be loaded yet.
|
||||
expect(await screen.queryByText('endorsed comment number 6', { exact: false }))
|
||||
expect(await screen.queryByTestId('comment-6'))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
expect(await screen.queryByText('unendorsed comment number 4', { exact: false }))
|
||||
expect(await screen.queryByTestId('comment-4'))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
|
||||
@@ -571,10 +597,10 @@ describe('CommentsView', () => {
|
||||
fireEvent.click(loadMoreButtonEndorsed);
|
||||
});
|
||||
// Endorsed comment from next page should be loaded now.
|
||||
await waitFor(() => expect(screen.queryByText('endorsed comment number 6', { exact: false }))
|
||||
await waitFor(() => expect(screen.queryByTestId('comment-6'))
|
||||
.toBeInTheDocument());
|
||||
// Unendorsed comment from next page should not be loaded yet.
|
||||
expect(await screen.queryByText('unendorsed comment number 4', { exact: false }))
|
||||
expect(await screen.queryByTestId('comment-4'))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
// Now only one load more buttons should show, for unendorsed comments
|
||||
@@ -583,7 +609,7 @@ describe('CommentsView', () => {
|
||||
fireEvent.click(loadMoreButtonUnendorsed);
|
||||
});
|
||||
// Unendorsed comment from next page should be loaded now.
|
||||
await waitFor(() => expect(screen.queryByText('unendorsed comment number 4', { exact: false }))
|
||||
await waitFor(() => expect(screen.queryByTestId('comment-4'))
|
||||
.toBeInTheDocument());
|
||||
await expect(findLoadMoreCommentsButtons()).rejects.toThrow();
|
||||
});
|
||||
@@ -595,8 +621,8 @@ describe('CommentsView', () => {
|
||||
it('initially loads only the first page', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
await waitFor(() => screen.findByText('comment number 7', { exact: false }));
|
||||
expect(screen.queryByText('comment number 8', { exact: false })).not.toBeInTheDocument();
|
||||
await waitFor(() => screen.findByTestId('comment-7'));
|
||||
expect(screen.queryByTestId('comment-8')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pressing load more button will load next page of responses', async () => {
|
||||
@@ -607,7 +633,7 @@ describe('CommentsView', () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
});
|
||||
|
||||
await screen.findByText('comment number 8', { exact: false });
|
||||
await screen.findByTestId('comment-8');
|
||||
});
|
||||
|
||||
it('newly loaded responses are appended to the old ones', async () => {
|
||||
@@ -618,9 +644,9 @@ describe('CommentsView', () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
});
|
||||
|
||||
await screen.findByText('comment number 8', { exact: false });
|
||||
await screen.findByTestId('comment-8');
|
||||
// check that comments from the first page are also displayed
|
||||
expect(screen.queryByText('comment number 7', { exact: false })).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('comment-7')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('load more button is hidden when no more responses pages to load', async () => {
|
||||
@@ -634,7 +660,7 @@ describe('CommentsView', () => {
|
||||
});
|
||||
}
|
||||
|
||||
await screen.findByText('comment number 8', { exact: false });
|
||||
await screen.findByTestId('comment-8');
|
||||
await expect(findLoadMoreCommentsResponsesButton())
|
||||
.rejects
|
||||
.toThrow();
|
||||
@@ -644,7 +670,9 @@ describe('CommentsView', () => {
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
// Wait for the content to load
|
||||
await screen.findByText('comment number 7', { exact: false });
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-7')));
|
||||
});
|
||||
const view = screen.getByTestId('comment-comment-1');
|
||||
|
||||
const likeButton = within(view).getByRole('button', { name: /like/i });
|
||||
@@ -658,13 +686,8 @@ describe('CommentsView', () => {
|
||||
it('handles endorsing comments', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
// Wait for the content to load
|
||||
await screen.findByText('comment number 7', { exact: false });
|
||||
|
||||
// There should be three buttons, one for the post, the second for the
|
||||
// comment and the third for a response to that comment
|
||||
const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(actionButtons[1]);
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-7')));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -677,7 +700,9 @@ describe('CommentsView', () => {
|
||||
it('handles reporting comments', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
// Wait for the content to load
|
||||
await screen.findByText('comment number 7', { exact: false });
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-7')));
|
||||
});
|
||||
|
||||
// There should be three buttons, one for the post, the second for the
|
||||
// comment and the third for a response to that comment
|
||||
@@ -710,8 +735,12 @@ describe('CommentsView', () => {
|
||||
test(`for ${component}`, async () => {
|
||||
renderComponent(discussionPostId);
|
||||
// Wait for the content to load
|
||||
await waitFor(() => expect(screen.queryByText('comment number 7', { exact: false })).toBeInTheDocument());
|
||||
// await waitFor(() => expect(screen.findByTestId('post-thread-1')).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.queryByText('This is Thread-1', { exact: false })).toBeInTheDocument());
|
||||
const content = screen.getByTestId(testId);
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(content);
|
||||
});
|
||||
const actionsButton = within(content).getAllByRole('button', { name: /actions menu/i })[0];
|
||||
await act(async () => {
|
||||
fireEvent.click(actionsButton);
|
||||
|
||||
@@ -17,16 +17,16 @@ function CommentIcons({
|
||||
timeago.register('time-locale', timeLocale);
|
||||
|
||||
const handleLike = () => dispatch(editComment(comment.id, { voted: !comment.voted }));
|
||||
if (comment.voteCount === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="d-flex flex-row align-items-center">
|
||||
<div className="ml-n1.5 mt-10px">
|
||||
<LikeButton
|
||||
count={comment.voteCount}
|
||||
onClick={handleLike}
|
||||
voted={comment.voted}
|
||||
/>
|
||||
<div className="d-flex flex-fill text-gray-500 justify-content-end" title={comment.createdAt}>
|
||||
{timeago.format(comment.createdAt, 'time-locale')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, useToggle } from '@edx/paragon';
|
||||
|
||||
import HTMLLoader from '../../../components/HTMLLoader';
|
||||
import { ContentActions } from '../../../data/constants';
|
||||
import { ContentActions, EndorsementStatus } from '../../../data/constants';
|
||||
import { AlertBanner, Confirmation, EndorsedAlertBanner } from '../../common';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import HoverCard from '../../common/HoverCard';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../../data/hooks';
|
||||
import { fetchThread } from '../../posts/data/thunks';
|
||||
import { useActions } from '../../utils';
|
||||
import CommentIcons from '../comment-icons/CommentIcons';
|
||||
import { selectCommentCurrentPage, selectCommentHasMorePages, selectCommentResponses } from '../data/selectors';
|
||||
import { editComment, fetchCommentResponses, removeComment } from '../data/thunks';
|
||||
@@ -28,6 +30,7 @@ function Comment({
|
||||
showFullThread = true,
|
||||
isClosedPost,
|
||||
intl,
|
||||
marginBottom,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const hasChildren = comment.childCount > 0;
|
||||
@@ -40,6 +43,7 @@ function Comment({
|
||||
const hasMorePages = useSelector(selectCommentHasMorePages(comment.id));
|
||||
const currentPage = useSelector(selectCommentCurrentPage(comment.id));
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const [showHoverCard, setShowHoverCard] = useState(false);
|
||||
const {
|
||||
courseId,
|
||||
} = useContext(DiscussionContext);
|
||||
@@ -49,6 +53,11 @@ function Comment({
|
||||
dispatch(fetchCommentResponses(comment.id, { page: 1 }));
|
||||
}
|
||||
}, [comment.id]);
|
||||
const actions = useActions({
|
||||
...comment,
|
||||
postType,
|
||||
});
|
||||
const endorseIcons = actions.find(({ action }) => action === EndorsementStatus.ENDORSED);
|
||||
|
||||
const handleAbusedFlag = () => {
|
||||
if (comment.abuseFlagged) {
|
||||
@@ -81,10 +90,15 @@ function Comment({
|
||||
const handleLoadMoreComments = () => (
|
||||
dispatch(fetchCommentResponses(comment.id, { page: currentPage + 1 }))
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames({ 'py-2 my-3': showFullThread })}>
|
||||
<div className="d-flex flex-column card" data-testid={`comment-${comment.id}`} role="listitem">
|
||||
<div className={classNames({ 'mb-3': (showFullThread && !marginBottom) })}>
|
||||
{/* eslint-disable jsx-a11y/no-noninteractive-tabindex */}
|
||||
<div
|
||||
tabIndex="0"
|
||||
className="d-flex flex-column card on-focus"
|
||||
data-testid={`comment-${comment.id}`}
|
||||
role="listitem"
|
||||
>
|
||||
<Confirmation
|
||||
isOpen={isDeleting}
|
||||
title={intl.formatMessage(messages.deleteResponseTitle)}
|
||||
@@ -105,73 +119,101 @@ function Comment({
|
||||
/>
|
||||
)}
|
||||
<EndorsedAlertBanner postType={postType} content={comment} />
|
||||
<div className="d-flex flex-column p-4.5">
|
||||
<div
|
||||
className="d-flex flex-column post-card-comment px-4 pt-3.5 pb-10px"
|
||||
aria-level={5}
|
||||
onMouseEnter={() => setShowHoverCard(true)}
|
||||
onMouseLeave={() => setShowHoverCard(false)}
|
||||
onFocus={() => setShowHoverCard(true)}
|
||||
onBlur={() => setShowHoverCard(false)}
|
||||
>
|
||||
{showHoverCard && (
|
||||
<HoverCard
|
||||
commentOrPost={comment}
|
||||
actionHandlers={actionHandlers}
|
||||
handleResponseCommentButton={() => setReplying(true)}
|
||||
onLike={() => dispatch(editComment(comment.id, { voted: !comment.voted }))}
|
||||
addResponseCommentButtonMessage={intl.formatMessage(messages.addComment)}
|
||||
isClosedPost={isClosedPost}
|
||||
endorseIcons={endorseIcons}
|
||||
/>
|
||||
)}
|
||||
<AlertBanner content={comment} />
|
||||
<CommentHeader comment={comment} actionHandlers={actionHandlers} postType={postType} />
|
||||
<CommentHeader comment={comment} />
|
||||
{isEditing
|
||||
? (
|
||||
<CommentEditor comment={comment} onCloseEditor={() => setEditing(false)} formClasses="pt-3" />
|
||||
)
|
||||
: <HTMLLoader cssClassName="comment-body pt-4 text-primary-500" componentId="comment" htmlNode={comment.renderedBody} />}
|
||||
: (
|
||||
<HTMLLoader
|
||||
cssClassName="comment-body html-loader text-break mt-14px font-style-normal font-family-inter text-primary-500"
|
||||
componentId="comment"
|
||||
htmlNode={comment.renderedBody}
|
||||
testId={comment.id}
|
||||
/>
|
||||
)}
|
||||
<CommentIcons
|
||||
comment={comment}
|
||||
following={comment.following}
|
||||
onLike={() => dispatch(editComment(comment.id, { voted: !comment.voted }))}
|
||||
createdAt={comment.createdAt}
|
||||
/>
|
||||
<div className="sr-only" role="heading" aria-level="3"> {intl.formatMessage(messages.replies, { count: inlineReplies.length })}</div>
|
||||
<div className="d-flex flex-column" role="list">
|
||||
{/* Pass along intl since component used here is the one before it's injected with `injectIntl` */}
|
||||
{inlineReplies.map(inlineReply => (
|
||||
<Reply
|
||||
reply={inlineReply}
|
||||
postType={postType}
|
||||
key={inlineReply.id}
|
||||
intl={intl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{inlineReplies.length > 0 && (
|
||||
<div className="d-flex flex-column mt-0.5" role="list">
|
||||
{/* Pass along intl since component used here is the one before it's injected with `injectIntl` */}
|
||||
{inlineReplies.map(inlineReply => (
|
||||
<Reply
|
||||
reply={inlineReply}
|
||||
postType={postType}
|
||||
key={inlineReply.id}
|
||||
intl={intl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{hasMorePages && (
|
||||
<Button
|
||||
onClick={handleLoadMoreComments}
|
||||
variant="link"
|
||||
block="true"
|
||||
className="mt-4.5 font-size-14 font-style-normal font-family-inter font-weight-500 px-2.5 py-2"
|
||||
data-testid="load-more-comments-responses"
|
||||
style={{
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage(messages.loadMoreComments)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleLoadMoreComments}
|
||||
variant="link"
|
||||
block="true"
|
||||
className="font-size-14 font-style-normal font-family-inter pt-10px border-0 font-weight-500 pb-0"
|
||||
data-testid="load-more-comments-responses"
|
||||
style={{
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage(messages.loadMoreComments)}
|
||||
</Button>
|
||||
)}
|
||||
{!isNested && showFullThread && (
|
||||
isReplying ? (
|
||||
<CommentEditor
|
||||
comment={{
|
||||
threadId: comment.threadId,
|
||||
parentId: comment.id,
|
||||
}}
|
||||
edit={false}
|
||||
onCloseEditor={() => setReplying(false)}
|
||||
/>
|
||||
<div className="mt-2.5">
|
||||
<CommentEditor
|
||||
comment={{
|
||||
threadId: comment.threadId,
|
||||
parentId: comment.id,
|
||||
}}
|
||||
edit={false}
|
||||
onCloseEditor={() => setReplying(false)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!isClosedPost && userCanAddThreadInBlackoutDate
|
||||
{!isClosedPost && userCanAddThreadInBlackoutDate && (inlineReplies.length >= 5)
|
||||
&& (
|
||||
<Button
|
||||
className="d-flex flex-grow mt-3 py-2 font-size-14"
|
||||
variant="outline-primary"
|
||||
style={{
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
onClick={() => setReplying(true)}
|
||||
>
|
||||
{intl.formatMessage(messages.addComment)}
|
||||
</Button>
|
||||
<Button
|
||||
className="d-flex flex-grow mt-2 font-size-14 font-style-normal font-family-inter font-weight-500 text-primary-500"
|
||||
variant="plain"
|
||||
style={{
|
||||
lineHeight: '24px',
|
||||
height: '36px',
|
||||
}}
|
||||
onClick={() => setReplying(true)}
|
||||
>
|
||||
{intl.formatMessage(messages.addComment)}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
@@ -186,11 +228,13 @@ Comment.propTypes = {
|
||||
showFullThread: PropTypes.bool,
|
||||
isClosedPost: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
marginBottom: PropTypes.bool,
|
||||
};
|
||||
|
||||
Comment.defaultProps = {
|
||||
showFullThread: true,
|
||||
isClosedPost: false,
|
||||
marginBottom: true,
|
||||
};
|
||||
|
||||
export default injectIntl(Comment);
|
||||
|
||||
@@ -1,46 +1,24 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import {
|
||||
Avatar, Icon,
|
||||
} from '@edx/paragon';
|
||||
import { Avatar } from '@edx/paragon';
|
||||
|
||||
import { AvatarOutlineAndLabelColors, EndorsementStatus, ThreadType } from '../../../data/constants';
|
||||
import { AvatarOutlineAndLabelColors } from '../../../data/constants';
|
||||
import { AuthorLabel } from '../../common';
|
||||
import ActionsDropdown from '../../common/ActionsDropdown';
|
||||
import { useAlertBannerVisible } from '../../data/hooks';
|
||||
import { selectAuthorAvatars } from '../../posts/data/selectors';
|
||||
import { useActions } from '../../utils';
|
||||
import { commentShape } from './proptypes';
|
||||
|
||||
function CommentHeader({
|
||||
comment,
|
||||
postType,
|
||||
actionHandlers,
|
||||
}) {
|
||||
const authorAvatars = useSelector(selectAuthorAvatars(comment.author));
|
||||
const colorClass = AvatarOutlineAndLabelColors[comment.authorLabel];
|
||||
const hasAnyAlert = useAlertBannerVisible(comment);
|
||||
|
||||
const actions = useActions({
|
||||
...comment,
|
||||
postType,
|
||||
});
|
||||
const actionIcons = actions.find(({ action }) => action === EndorsementStatus.ENDORSED);
|
||||
|
||||
const handleIcons = (action) => {
|
||||
const actionFunction = actionHandlers[action];
|
||||
if (actionFunction) {
|
||||
actionFunction();
|
||||
} else {
|
||||
logError(`Unknown or unimplemented action ${action}`);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className={classNames('d-flex flex-row justify-content-between', {
|
||||
'mt-2': hasAnyAlert,
|
||||
@@ -61,28 +39,8 @@ function CommentHeader({
|
||||
authorLabel={comment.authorLabel}
|
||||
labelColor={colorClass && `text-${colorClass}`}
|
||||
linkToProfile
|
||||
/>
|
||||
</div>
|
||||
<div className="d-flex align-items-center">
|
||||
|
||||
{actionIcons && (
|
||||
<span className="btn-icon btn-icon-sm mr-1 align-items-center pointer-cursor-hover">
|
||||
<Icon
|
||||
data-testid="check-icon"
|
||||
onClick={() => handleIcons(actionIcons.action)}
|
||||
src={actionIcons.icon}
|
||||
className={['endorse', 'unendorse'].includes(actionIcons.id) ? 'text-dark-500' : 'text-success-500'}
|
||||
size="sm"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<ActionsDropdown
|
||||
commentOrPost={{
|
||||
...comment,
|
||||
postType,
|
||||
}}
|
||||
actionHandlers={actionHandlers}
|
||||
postCreatedAt={comment.createdAt}
|
||||
postOrComment
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,8 +49,6 @@ function CommentHeader({
|
||||
|
||||
CommentHeader.propTypes = {
|
||||
comment: commentShape.isRequired,
|
||||
actionHandlers: PropTypes.objectOf(PropTypes.func).isRequired,
|
||||
postType: PropTypes.oneOf([ThreadType.QUESTION, ThreadType.DISCUSSION]).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CommentHeader);
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { initializeStore } from '../../../store';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import CommentHeader from './CommentHeader';
|
||||
|
||||
let store;
|
||||
|
||||
function renderComponent(comment, postType, actionHandlers) {
|
||||
return render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider
|
||||
value={{ courseId: 'course-v1:edX+TestX+Test_Course' }}
|
||||
>
|
||||
<CommentHeader comment={comment} postType={postType} actionHandlers={actionHandlers} />
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
const mockComment = {
|
||||
author: 'abc123',
|
||||
authorLabel: 'ABC 123',
|
||||
endorsed: true,
|
||||
editableFields: ['endorsed'],
|
||||
};
|
||||
|
||||
describe('Comment Header', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
it('should render verified icon for endorsed discussion posts', () => {
|
||||
renderComponent(mockComment, 'discussion', {});
|
||||
expect(screen.queryAllByTestId('check-icon')).toHaveLength(1);
|
||||
});
|
||||
it('should render check icon for endorsed question posts', () => {
|
||||
renderComponent(mockComment, 'question', {});
|
||||
expect(screen.queryAllByTestId('check-icon')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -64,7 +64,7 @@ function Reply({
|
||||
const hasAnyAlert = useAlertBannerVisible(reply);
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column mt-4.5" data-testid={`reply-${reply.id}`} role="listitem">
|
||||
<div className="d-flex flex-column mt-2.5 " data-testid={`reply-${reply.id}`} role="listitem">
|
||||
<Confirmation
|
||||
isOpen={isDeleting}
|
||||
title={intl.formatMessage(messages.deleteCommentTitle)}
|
||||
@@ -108,29 +108,41 @@ function Reply({
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="bg-light-300 px-4 pb-2 pt-2.5 flex-fill"
|
||||
className="bg-light-300 pl-4 pt-2.5 pr-2.5 pb-10px flex-fill"
|
||||
style={{ borderRadius: '0rem 0.375rem 0.375rem' }}
|
||||
>
|
||||
<div className="d-flex flex-row justify-content-between align-items-center mb-0.5">
|
||||
<AuthorLabel author={reply.author} authorLabel={reply.authorLabel} labelColor={colorClass && `text-${colorClass}`} linkToProfile />
|
||||
<div className="ml-auto d-flex">
|
||||
<div className="d-flex flex-row justify-content-between" style={{ height: '24px' }}>
|
||||
<AuthorLabel
|
||||
author={reply.author}
|
||||
authorLabel={reply.authorLabel}
|
||||
labelColor={colorClass && `text-${colorClass}`}
|
||||
linkToProfile
|
||||
postCreatedAt={reply.createdAt}
|
||||
postOrComment
|
||||
/>
|
||||
<div className="ml-auto d-flex" style={{ lineHeight: '24px' }}>
|
||||
<ActionsDropdown
|
||||
commentOrPost={{
|
||||
...reply,
|
||||
postType,
|
||||
}}
|
||||
actionHandlers={actionHandlers}
|
||||
iconSize="inline"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isEditing
|
||||
? <CommentEditor comment={reply} onCloseEditor={() => setEditing(false)} />
|
||||
: <HTMLLoader componentId="reply" htmlNode={reply.renderedBody} cssClassName="text-primary-500" />}
|
||||
: (
|
||||
<HTMLLoader
|
||||
componentId="reply"
|
||||
htmlNode={reply.renderedBody}
|
||||
cssClassName="html-loader text-break font-style-normal pb-1 font-family-inter text-primary-500"
|
||||
testId={reply.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-gray-500 align-self-end mt-2" title={reply.createdAt}>
|
||||
{timeago.format(reply.createdAt, 'time-locale')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,59 +1,39 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../../data/hooks';
|
||||
import messages from '../messages';
|
||||
import CommentEditor from './CommentEditor';
|
||||
|
||||
function ResponseEditor({
|
||||
postId,
|
||||
intl,
|
||||
addWrappingDiv,
|
||||
handleCloseEditor,
|
||||
addingResponse,
|
||||
}) {
|
||||
const { enableInContextSidebar } = useContext(DiscussionContext);
|
||||
const [addingResponse, setAddingResponse] = useState(false);
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
|
||||
useEffect(() => {
|
||||
setAddingResponse(false);
|
||||
handleCloseEditor();
|
||||
}, [postId]);
|
||||
|
||||
return addingResponse
|
||||
? (
|
||||
&& (
|
||||
<div className={classNames({ 'bg-white p-4 mb-4 rounded': addWrappingDiv })}>
|
||||
<CommentEditor
|
||||
comment={{ threadId: postId }}
|
||||
edit={false}
|
||||
onCloseEditor={() => setAddingResponse(false)}
|
||||
onCloseEditor={handleCloseEditor}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: userCanAddThreadInBlackoutDate && (
|
||||
<div className={classNames({ 'mb-4': addWrappingDiv }, 'actions d-flex')}>
|
||||
<Button
|
||||
variant="primary"
|
||||
className={classNames('px-2.5 py-2 font-size-14', { 'w-100': enableInContextSidebar })}
|
||||
onClick={() => setAddingResponse(true)}
|
||||
style={{
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage(messages.addResponse)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ResponseEditor.propTypes = {
|
||||
postId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
addWrappingDiv: PropTypes.bool,
|
||||
handleCloseEditor: PropTypes.func.isRequired,
|
||||
addingResponse: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
ResponseEditor.defaultProps = {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
addComment: {
|
||||
id: 'discussions.comments.comment.addComment',
|
||||
defaultMessage: 'Add comment',
|
||||
description: 'Button to add a comment to a response',
|
||||
},
|
||||
addResponse: {
|
||||
id: 'discussions.comments.comment.addResponse',
|
||||
defaultMessage: 'Add a response',
|
||||
description: 'Button to add a response in a thread of forum posts',
|
||||
},
|
||||
addComment: {
|
||||
id: 'discussions.comments.comment.addComment',
|
||||
defaultMessage: 'Add a comment',
|
||||
description: 'Button to add a comment to a response',
|
||||
description: 'Button to add a response to a response',
|
||||
},
|
||||
abuseFlaggedMessage: {
|
||||
id: 'discussions.comments.comment.abuseFlaggedMessage',
|
||||
@@ -188,6 +188,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Edited by',
|
||||
description: 'Text shown to users to indicate who edited a post. Followed by the username of editor.',
|
||||
},
|
||||
fullStop: {
|
||||
id: 'discussions.comment.comments.fullStop',
|
||||
defaultMessage: '•',
|
||||
description: 'Fullstop shown to users to indicate who edited a post. Followed by a reason.',
|
||||
},
|
||||
reason: {
|
||||
id: 'discussions.comment.comments.reason',
|
||||
defaultMessage: 'Reason',
|
||||
@@ -197,11 +202,6 @@ const messages = defineMessages({
|
||||
id: 'discussions.post.closedBy',
|
||||
defaultMessage: 'Post closed by',
|
||||
},
|
||||
replies: {
|
||||
id: 'discussion.comment.repliesHeading',
|
||||
defaultMessage: '{count} replies for the response added',
|
||||
description: 'Text added for screen reader to understand nesting replies.',
|
||||
},
|
||||
time: {
|
||||
id: 'discussion.comment.time',
|
||||
defaultMessage: '{time} ago',
|
||||
|
||||
@@ -23,6 +23,8 @@ function ActionsDropdown({
|
||||
commentOrPost,
|
||||
disabled,
|
||||
actionHandlers,
|
||||
iconSize,
|
||||
dropDownIconSize,
|
||||
}) {
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const [target, setTarget] = useState(null);
|
||||
@@ -49,8 +51,9 @@ function ActionsDropdown({
|
||||
src={MoreHoriz}
|
||||
iconAs={Icon}
|
||||
disabled={disabled}
|
||||
size="sm"
|
||||
size={iconSize}
|
||||
ref={setTarget}
|
||||
iconClassNames={dropDownIconSize ? 'dropdown-icon-dimentions' : ''}
|
||||
/>
|
||||
<div className="actions-dropdown">
|
||||
<ModalPopup
|
||||
@@ -66,7 +69,7 @@ function ActionsDropdown({
|
||||
{actions.map(action => (
|
||||
<React.Fragment key={action.id}>
|
||||
{(action.action === ContentActions.DELETE)
|
||||
&& <Dropdown.Divider />}
|
||||
&& <Dropdown.Divider />}
|
||||
|
||||
<Dropdown.Item
|
||||
as={Button}
|
||||
@@ -94,10 +97,14 @@ ActionsDropdown.propTypes = {
|
||||
commentOrPost: PropTypes.oneOfType([commentShape, postShape]).isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
actionHandlers: PropTypes.objectOf(PropTypes.func).isRequired,
|
||||
iconSize: PropTypes.string,
|
||||
dropDownIconSize: PropTypes.bool,
|
||||
};
|
||||
|
||||
ActionsDropdown.defaultProps = {
|
||||
disabled: false,
|
||||
iconSize: 'sm',
|
||||
dropDownIconSize: false,
|
||||
};
|
||||
|
||||
export default injectIntl(ActionsDropdown);
|
||||
|
||||
@@ -33,32 +33,45 @@ function AlertBanner({
|
||||
return (
|
||||
<>
|
||||
{canSeeReportedBanner && (
|
||||
<Alert icon={Report} variant="danger" className="px-3 mb-2 py-10px shadow-none flex-fill">
|
||||
<Alert icon={Report} variant="danger" className="px-3 mb-1 py-10px shadow-none flex-fill">
|
||||
{intl.formatMessage(messages.abuseFlaggedMessage)}
|
||||
</Alert>
|
||||
)}
|
||||
{reasonCodesEnabled && canSeeLastEditOrClosedAlert && (
|
||||
<>
|
||||
{content.lastEdit?.reason && (
|
||||
<Alert variant="info" className="px-3 shadow-none mb-2 py-10px bg-light-200">
|
||||
<div className="d-flex align-items-center flex-wrap">
|
||||
<Alert variant="info" className="px-3 shadow-none mb-1 py-10px bg-light-200">
|
||||
<div className="d-flex align-items-center flex-wrap text-gray-700 font-family-inter">
|
||||
{intl.formatMessage(messages.editedBy)}
|
||||
<span className="ml-1 mr-3">
|
||||
<AuthorLabel author={content.lastEdit.editorUsername} linkToProfile />
|
||||
<AuthorLabel author={content.lastEdit.editorUsername} linkToProfile postOrComment />
|
||||
</span>
|
||||
<span
|
||||
className="mx-1.5 font-family-inter font-size-8 font-style-normal text-light-700"
|
||||
style={{ lineHeight: '15px' }}
|
||||
>
|
||||
{intl.formatMessage(messages.fullStop)}
|
||||
</span>
|
||||
{intl.formatMessage(messages.reason)}: {content.lastEdit.reason}
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
{content.closed && (
|
||||
<Alert variant="info" className="px-3 shadow-none mb-2 py-10px bg-light-200">
|
||||
<div className="d-flex align-items-center flex-wrap">
|
||||
<Alert variant="info" className="px-3 shadow-none mb-1 py-10px bg-light-200">
|
||||
<div className="d-flex align-items-center flex-wrap text-gray-700 font-family-inter">
|
||||
{intl.formatMessage(messages.closedBy)}
|
||||
<span className="ml-1 ">
|
||||
<AuthorLabel author={content.closedBy} linkToProfile />
|
||||
<AuthorLabel author={content.closedBy} linkToProfile postOrComment />
|
||||
</span>
|
||||
<span className="mx-1" />
|
||||
<span
|
||||
className="mx-1.5 font-family-inter font-size-8 font-style-normal text-light-700"
|
||||
style={{ lineHeight: '15px' }}
|
||||
>
|
||||
{intl.formatMessage(messages.fullStop)}
|
||||
</span>
|
||||
|
||||
{content.closeReason && (`${intl.formatMessage(messages.reason)}: ${content.closeReason}`)}
|
||||
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -3,9 +3,10 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import * as timeago from 'timeago.js';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
|
||||
import { Institution, School } from '@edx/paragon/icons';
|
||||
|
||||
import { Routes } from '../../data/constants';
|
||||
@@ -13,6 +14,7 @@ import { useShowLearnersTab } from '../data/hooks';
|
||||
import messages from '../messages';
|
||||
import { discussionsPath } from '../utils';
|
||||
import { DiscussionContext } from './context';
|
||||
import timeLocale from './time-locale';
|
||||
|
||||
function AuthorLabel({
|
||||
intl,
|
||||
@@ -21,11 +23,15 @@ function AuthorLabel({
|
||||
linkToProfile,
|
||||
labelColor,
|
||||
alert,
|
||||
postCreatedAt,
|
||||
authorToolTip,
|
||||
postOrComment,
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const { courseId } = useContext(DiscussionContext);
|
||||
let icon = null;
|
||||
let authorLabelMessage = null;
|
||||
timeago.register('time-locale', timeLocale);
|
||||
|
||||
if (authorLabel === 'Staff') {
|
||||
icon = Institution;
|
||||
@@ -37,37 +43,56 @@ function AuthorLabel({
|
||||
}
|
||||
|
||||
const isRetiredUser = author ? author.startsWith('retired__user') : false;
|
||||
const showTextPrimary = !authorLabelMessage && !isRetiredUser && !alert;
|
||||
|
||||
const className = classNames('d-flex align-items-center mb-0.5', labelColor);
|
||||
const className = classNames('d-flex align-items-center', { 'mb-0.5': !postOrComment }, labelColor);
|
||||
|
||||
const showUserNameAsLink = useShowLearnersTab()
|
||||
&& linkToProfile && author && author !== intl.formatMessage(messages.anonymous);
|
||||
&& linkToProfile && author && author !== intl.formatMessage(messages.anonymous);
|
||||
|
||||
const labelContents = (
|
||||
<div className={className}>
|
||||
<span
|
||||
className={classNames('mr-1 font-size-14 font-style-normal font-family-inter font-weight-500', {
|
||||
'text-gray-700': isRetiredUser,
|
||||
'text-primary-500': !authorLabelMessage && !isRetiredUser && !alert,
|
||||
})}
|
||||
role="heading"
|
||||
aria-level="2"
|
||||
>
|
||||
{isRetiredUser ? '[Deactivated]' : author }
|
||||
</span>
|
||||
{icon && (
|
||||
<Icon
|
||||
style={{
|
||||
width: '1rem',
|
||||
height: '1rem',
|
||||
}}
|
||||
src={icon}
|
||||
/>
|
||||
<div className={className} style={{ lineHeight: '24px' }}>
|
||||
{!alert && (
|
||||
<span
|
||||
className={classNames('mr-1.5 font-size-14 font-style-normal font-family-inter font-weight-500', {
|
||||
'text-gray-700': isRetiredUser,
|
||||
'text-primary-500': !authorLabelMessage && !isRetiredUser,
|
||||
})}
|
||||
role="heading"
|
||||
aria-level="2"
|
||||
>
|
||||
{isRetiredUser ? '[Deactivated]' : author}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip id={`endorsed-by-${author}-tooltip`}>
|
||||
{author}
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
trigger={['hover', 'focus']}
|
||||
>
|
||||
<div className={classNames('d-flex flex-row align-items-center', {
|
||||
'disable-div': !authorToolTip,
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
style={{
|
||||
width: '1rem',
|
||||
height: '1rem',
|
||||
}}
|
||||
src={icon}
|
||||
data-testid="author-icon"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
{authorLabelMessage && (
|
||||
<span
|
||||
className={classNames('mr-1 font-size-14 font-style-normal font-family-inter font-weight-500', {
|
||||
'text-primary-500': !authorLabelMessage && !isRetiredUser && !alert,
|
||||
className={classNames('mr-1.5 font-size-14 font-style-normal font-family-inter font-weight-500', {
|
||||
'text-primary-500': showTextPrimary,
|
||||
'text-gray-700': isRetiredUser,
|
||||
})}
|
||||
style={{ marginLeft: '2px' }}
|
||||
@@ -75,6 +100,19 @@ function AuthorLabel({
|
||||
{authorLabelMessage}
|
||||
</span>
|
||||
)}
|
||||
{postCreatedAt && (
|
||||
<span
|
||||
title={postCreatedAt}
|
||||
className={classNames('font-family-inter align-content-center', {
|
||||
'text-white': alert,
|
||||
'text-gray-500': !alert,
|
||||
})}
|
||||
style={{ lineHeight: '20px', fontSize: '12px', marginBottom: '-2.3px' }}
|
||||
>
|
||||
{timeago.format(postCreatedAt, 'time-locale')}
|
||||
</span>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -100,6 +138,9 @@ AuthorLabel.propTypes = {
|
||||
linkToProfile: PropTypes.bool,
|
||||
labelColor: PropTypes.string,
|
||||
alert: PropTypes.bool,
|
||||
postCreatedAt: PropTypes.string,
|
||||
authorToolTip: PropTypes.bool,
|
||||
postOrComment: PropTypes.bool,
|
||||
};
|
||||
|
||||
AuthorLabel.defaultProps = {
|
||||
@@ -107,6 +148,9 @@ AuthorLabel.defaultProps = {
|
||||
authorLabel: null,
|
||||
labelColor: '',
|
||||
alert: false,
|
||||
postCreatedAt: null,
|
||||
authorToolTip: false,
|
||||
postOrComment: false,
|
||||
};
|
||||
|
||||
export default injectIntl(AuthorLabel);
|
||||
|
||||
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import * as timeago from 'timeago.js';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Alert, Icon } from '@edx/paragon';
|
||||
import { CheckCircle, Verified } from '@edx/paragon/icons';
|
||||
|
||||
import { ThreadType } from '../../data/constants';
|
||||
@@ -27,32 +27,35 @@ function EndorsedAlertBanner({
|
||||
content.endorsed && (
|
||||
<Alert
|
||||
variant="plain"
|
||||
className={`px-3 mb-0 py-10px align-items-center shadow-none ${classes}`}
|
||||
className={`px-2.5 mb-0 py-8px align-items-center shadow-none ${classes}`}
|
||||
style={{ borderRadius: '0.375rem 0.375rem 0 0' }}
|
||||
icon={iconClass}
|
||||
>
|
||||
<div className="d-flex justify-content-between flex-wrap">
|
||||
<strong className="lead">{intl.formatMessage(
|
||||
isQuestion
|
||||
? messages.answer
|
||||
: messages.endorsed,
|
||||
)}
|
||||
</strong>
|
||||
<span className="d-flex align-items-center mr-1 flex-wrap">
|
||||
<span className="mr-1">
|
||||
{intl.formatMessage(
|
||||
isQuestion
|
||||
? messages.answeredLabel
|
||||
: messages.endorsedLabel,
|
||||
)}
|
||||
</span>
|
||||
<div className="d-flex align-items-center">
|
||||
<Icon
|
||||
src={iconClass}
|
||||
style={{
|
||||
width: '21px',
|
||||
height: '20px',
|
||||
}}
|
||||
/>
|
||||
<strong className="ml-2 font-family-inter">{intl.formatMessage(
|
||||
isQuestion
|
||||
? messages.answer
|
||||
: messages.endorsed,
|
||||
)}
|
||||
</strong>
|
||||
</div>
|
||||
<span className="d-flex align-items-center align-items-center flex-wrap" style={{ marginRight: '-1px' }}>
|
||||
<AuthorLabel
|
||||
author={content.endorsedBy}
|
||||
authorLabel={content.endorsedByLabel}
|
||||
linkToProfile
|
||||
alert={content.endorsed}
|
||||
postCreatedAt={content.endorsedAt}
|
||||
authorToolTip
|
||||
postOrComment
|
||||
/>
|
||||
{intl.formatMessage(messages.time, { time: timeago.format(content.endorsedAt, 'time-locale') })}
|
||||
</span>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
@@ -46,21 +46,21 @@ describe.each([
|
||||
type: 'comment',
|
||||
postType: ThreadType.QUESTION,
|
||||
props: { endorsed: true, endorsedBy: 'test-user', endorsedByLabel: 'Staff' },
|
||||
expectText: [messages.answer.defaultMessage, messages.answeredLabel.defaultMessage, 'test-user', 'Staff'],
|
||||
expectText: [messages.answer.defaultMessage, 'Staff'],
|
||||
},
|
||||
{
|
||||
label: 'TA endorsed comment in a question thread',
|
||||
type: 'comment',
|
||||
postType: ThreadType.QUESTION,
|
||||
props: { endorsed: true, endorsedBy: 'test-user', endorsedByLabel: 'Community TA' },
|
||||
expectText: [messages.answer.defaultMessage, messages.answeredLabel.defaultMessage, 'test-user', 'TA'],
|
||||
expectText: [messages.answer.defaultMessage, 'TA'],
|
||||
},
|
||||
{
|
||||
label: 'endorsed comment in a discussion thread',
|
||||
type: 'comment',
|
||||
postType: ThreadType.DISCUSSION,
|
||||
props: { endorsed: true, endorsedBy: 'test-user' },
|
||||
expectText: [messages.endorsed.defaultMessage, messages.endorsedLabel.defaultMessage, 'test-user'],
|
||||
expectText: [messages.endorsed.defaultMessage],
|
||||
},
|
||||
])('EndorsedAlertBanner', ({
|
||||
label, type, postType, props, expectText,
|
||||
|
||||
123
src/discussions/common/HoverCard.jsx
Normal file
123
src/discussions/common/HoverCard.jsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button,
|
||||
Icon, IconButton,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import {
|
||||
StarFilled, StarOutline, ThumbUpFilled, ThumbUpOutline,
|
||||
} from '../../components/icons';
|
||||
import { commentShape } from '../comments/comment/proptypes';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../data/hooks';
|
||||
import { postShape } from '../posts/post/proptypes';
|
||||
import ActionsDropdown from './ActionsDropdown';
|
||||
import { DiscussionContext } from './context';
|
||||
|
||||
function HoverCard({
|
||||
commentOrPost,
|
||||
actionHandlers,
|
||||
handleResponseCommentButton,
|
||||
addResponseCommentButtonMessage,
|
||||
onLike,
|
||||
onFollow,
|
||||
isClosedPost,
|
||||
endorseIcons,
|
||||
}) {
|
||||
const { enableInContextSidebar } = useContext(DiscussionContext);
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="d-flex flex-fill justify-content-end align-items-center hover-card mr-n4 position-absolute"
|
||||
data-testid="hover-card"
|
||||
>
|
||||
{userCanAddThreadInBlackoutDate && (
|
||||
<div className="d-flex">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
className={classNames('px-2.5 py-2 border-0 font-style-normal font-family-inter text-gray-700 font-size-12',
|
||||
{ 'w-100': enableInContextSidebar })}
|
||||
onClick={() => handleResponseCommentButton()}
|
||||
disabled={isClosedPost}
|
||||
style={{
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
>
|
||||
{addResponseCommentButtonMessage}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{endorseIcons && (
|
||||
<div className="hover-button">
|
||||
<IconButton
|
||||
src={endorseIcons.icon}
|
||||
iconAs={Icon}
|
||||
onClick={() => {
|
||||
const actionFunction = actionHandlers[endorseIcons.action];
|
||||
actionFunction();
|
||||
}}
|
||||
className={['endorse', 'unendorse'].includes(endorseIcons.id) ? 'text-dark-500' : 'text-success-500'}
|
||||
size="sm"
|
||||
alt="Endorse"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="hover-button">
|
||||
<IconButton
|
||||
src={commentOrPost.voted ? ThumbUpFilled : ThumbUpOutline}
|
||||
iconAs={Icon}
|
||||
size="sm"
|
||||
alt="Like"
|
||||
iconClassNames="like-icon-dimentions"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onLike();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{commentOrPost.following !== undefined && (
|
||||
<div className="hover-button">
|
||||
<IconButton
|
||||
src={commentOrPost.following ? StarFilled : StarOutline}
|
||||
iconAs={Icon}
|
||||
size="sm"
|
||||
alt="Follow"
|
||||
iconClassNames="follow-icon-dimentions"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onFollow();
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="hover-button ml-auto">
|
||||
<ActionsDropdown commentOrPost={commentOrPost} actionHandlers={actionHandlers} dropDownIconSize />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
HoverCard.propTypes = {
|
||||
commentOrPost: PropTypes.oneOfType([commentShape, postShape]).isRequired,
|
||||
actionHandlers: PropTypes.objectOf(PropTypes.func).isRequired,
|
||||
handleResponseCommentButton: PropTypes.func.isRequired,
|
||||
onLike: PropTypes.func.isRequired,
|
||||
onFollow: PropTypes.func,
|
||||
addResponseCommentButtonMessage: PropTypes.string.isRequired,
|
||||
isClosedPost: PropTypes.bool.isRequired,
|
||||
endorseIcons: PropTypes.objectOf(PropTypes.any),
|
||||
};
|
||||
|
||||
HoverCard.defaultProps = {
|
||||
onFollow: () => null,
|
||||
endorseIcons: null,
|
||||
};
|
||||
|
||||
export default injectIntl(HoverCard);
|
||||
194
src/discussions/common/HoverCard.test.jsx
Normal file
194
src/discussions/common/HoverCard.test.jsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import {
|
||||
render, screen, waitFor, within,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { getCommentsApiUrl } from '../comments/data/api';
|
||||
import DiscussionContent from '../discussions-home/DiscussionContent';
|
||||
import { getThreadsApiUrl } from '../posts/data/api';
|
||||
import { fetchThreads } from '../posts/data/thunks';
|
||||
import { DiscussionContext } from './context';
|
||||
|
||||
import '../posts/data/__factories__';
|
||||
import '../comments/data/__factories__';
|
||||
|
||||
const commentsApiUrl = getCommentsApiUrl();
|
||||
const threadsApiUrl = getThreadsApiUrl();
|
||||
const discussionPostId = 'thread-1';
|
||||
const questionPostId = 'thread-2';
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
let store;
|
||||
let axiosMock;
|
||||
let container;
|
||||
|
||||
function mockAxiosReturnPagedComments() {
|
||||
[null, false, true].forEach(endorsed => {
|
||||
const postId = endorsed === null ? discussionPostId : questionPostId;
|
||||
[1, 2].forEach(page => {
|
||||
axiosMock
|
||||
.onGet(commentsApiUrl, {
|
||||
params: {
|
||||
thread_id: postId,
|
||||
page,
|
||||
page_size: undefined,
|
||||
requested_fields: 'profile_image',
|
||||
endorsed,
|
||||
},
|
||||
})
|
||||
.reply(200, Factory.build('commentsResult', { can_delete: true }, {
|
||||
threadId: postId,
|
||||
page,
|
||||
pageSize: 1,
|
||||
count: 2,
|
||||
endorsed,
|
||||
childCount: page === 1 ? 2 : 0,
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function mockAxiosReturnPagedCommentsResponses() {
|
||||
const parentId = 'comment-1';
|
||||
const commentsResponsesApiUrl = `${commentsApiUrl}${parentId}/`;
|
||||
const paramsTemplate = {
|
||||
page: undefined,
|
||||
page_size: undefined,
|
||||
requested_fields: 'profile_image',
|
||||
};
|
||||
|
||||
for (let page = 1; page <= 2; page++) {
|
||||
axiosMock
|
||||
.onGet(commentsResponsesApiUrl, { params: { ...paramsTemplate, page } })
|
||||
.reply(200, Factory.build('commentsResult', null, {
|
||||
parentId,
|
||||
page,
|
||||
pageSize: 1,
|
||||
count: 2,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function renderComponent(postId) {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider
|
||||
value={{ courseId }}
|
||||
>
|
||||
<MemoryRouter initialEntries={[`/${courseId}/posts/${postId}`]}>
|
||||
<DiscussionContent />
|
||||
<Route
|
||||
path="*"
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
container = wrapper.container;
|
||||
return container;
|
||||
}
|
||||
|
||||
describe('HoverCard', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
Factory.resetAll();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(threadsApiUrl)
|
||||
.reply(200, Factory.build('threadsResult'));
|
||||
axiosMock.onPatch(new RegExp(`${commentsApiUrl}*`)).reply(({
|
||||
url,
|
||||
data,
|
||||
}) => {
|
||||
const commentId = url.match(/comments\/(?<id>[a-z1-9-]+)\//).groups.id;
|
||||
const {
|
||||
rawBody,
|
||||
} = camelCaseObject(JSON.parse(data));
|
||||
return [200, Factory.build('comment', {
|
||||
id: commentId,
|
||||
rendered_body: rawBody,
|
||||
raw_body: rawBody,
|
||||
})];
|
||||
});
|
||||
axiosMock.onPost(commentsApiUrl)
|
||||
.reply(({ data }) => {
|
||||
const {
|
||||
rawBody,
|
||||
threadId,
|
||||
} = camelCaseObject(JSON.parse(data));
|
||||
return [200, Factory.build(
|
||||
'comment',
|
||||
{
|
||||
rendered_body: rawBody,
|
||||
raw_body: rawBody,
|
||||
thread_id: threadId,
|
||||
},
|
||||
)];
|
||||
});
|
||||
|
||||
executeThunk(fetchThreads(courseId), store.dispatch, store.getState);
|
||||
mockAxiosReturnPagedComments();
|
||||
mockAxiosReturnPagedCommentsResponses();
|
||||
});
|
||||
|
||||
test('it should show hover card when hovered on post', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
const post = screen.getByTestId('post-thread-1');
|
||||
userEvent.hover(post);
|
||||
expect(screen.getByTestId('hover-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it should show hover card when hovered on comment', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-1'));
|
||||
userEvent.hover(comment);
|
||||
expect(screen.getByTestId('hover-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it should not show hover card when post and comment not hovered', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
expect(screen.queryByTestId('hover-card')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it should show add response, like, follow and actions menu for hovered post', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
const post = screen.getByTestId('post-thread-1');
|
||||
userEvent.hover(post);
|
||||
const view = screen.getByTestId('hover-card');
|
||||
expect(within(view).queryByRole('button', { name: /Add response/i })).toBeInTheDocument();
|
||||
expect(within(view).getByRole('button', { name: /like/i })).toBeInTheDocument();
|
||||
expect(within(view).queryByRole('button', { name: /follow/i })).toBeInTheDocument();
|
||||
expect(within(view).queryByRole('button', { name: /actions menu/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it should show add comment, Endorse, like and actions menu Buttons for hovered comment', async () => {
|
||||
renderComponent(questionPostId);
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-3'));
|
||||
userEvent.hover(comment);
|
||||
const view = screen.getByTestId('hover-card');
|
||||
expect(screen.getByTestId('hover-card')).toBeInTheDocument();
|
||||
expect(within(view).queryByRole('button', { name: /Add comment/i })).toBeInTheDocument();
|
||||
expect(within(view).getByRole('button', { name: /Endorse/i })).toBeInTheDocument();
|
||||
expect(within(view).queryByRole('button', { name: /like/i })).toBeInTheDocument();
|
||||
expect(within(view).queryByRole('button', { name: /actions menu/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,9 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import {
|
||||
useContext, useEffect, useRef, useState,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@@ -17,6 +20,10 @@ import { DiscussionContext } from '../common/context';
|
||||
import { clearRedirect } from '../posts/data';
|
||||
import { threadsLoadingStatus } from '../posts/data/selectors';
|
||||
import { selectTopics } from '../topics/data/selectors';
|
||||
import tourCheckpoints from '../tours/constants';
|
||||
import { selectTours } from '../tours/data/selectors';
|
||||
import { updateTourShowStatus } from '../tours/data/thunks';
|
||||
import messages from '../tours/messages';
|
||||
import { discussionsPath, inBlackoutDateRange } from '../utils';
|
||||
import {
|
||||
selectAreThreadsFiltered,
|
||||
@@ -40,10 +47,11 @@ export function useTotalTopicThreadCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Object.keys(topics).reduce((total, topicId) => {
|
||||
const topic = topics[topicId];
|
||||
return total + topic.threadCounts.discussion + topic.threadCounts.question;
|
||||
}, 0);
|
||||
return Object.keys(topics)
|
||||
.reduce((total, topicId) => {
|
||||
const topic = topics[topicId];
|
||||
return total + topic.threadCounts.discussion + topic.threadCounts.question;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export const useSidebarVisible = () => {
|
||||
@@ -192,3 +200,27 @@ export const useUserCanAddThreadInBlackoutDate = () => {
|
||||
return (!(isInBlackoutDateRange)
|
||||
|| (isUserAdmin || userHasModerationPrivilages || isUserGroupTA || isCourseAdmin || isCourseStaff));
|
||||
};
|
||||
|
||||
function camelToConstant(string) {
|
||||
return string.replace(/[A-Z]/g, (match) => `_${match}`).toUpperCase();
|
||||
}
|
||||
|
||||
export const useTourConfiguration = (intl) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { enableInContextSidebar } = useContext(DiscussionContext);
|
||||
const tours = useSelector(selectTours);
|
||||
|
||||
return tours.map((tour) => (
|
||||
{
|
||||
tourId: tour.tourName,
|
||||
advanceButtonText: intl.formatMessage(messages.advanceButtonText),
|
||||
dismissButtonText: intl.formatMessage(messages.dismissButtonText),
|
||||
endButtonText: intl.formatMessage(messages.endButtonText),
|
||||
enabled: tour && Boolean(tour.showTour && !enableInContextSidebar),
|
||||
onDismiss: () => dispatch(updateTourShowStatus(tour.id)),
|
||||
onEnd: () => dispatch(updateTourShowStatus(tour.id)),
|
||||
checkpoints: tourCheckpoints(intl)[camelToConstant(tour.tourName)],
|
||||
}
|
||||
));
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ import { EmptyTopic as InContextEmptyTopics } from '../in-context-topics/compone
|
||||
import messages from '../messages';
|
||||
import { LegacyBreadcrumbMenu, NavigationBar } from '../navigation';
|
||||
import { selectPostEditorVisible } from '../posts/data/selectors';
|
||||
import DiscussionsProductTour from '../tours/DiscussionsProductTour';
|
||||
import { postMessageToParent } from '../utils';
|
||||
import BlackoutInformationBanner from './BlackoutInformationBanner';
|
||||
import DiscussionContent from './DiscussionContent';
|
||||
@@ -101,6 +102,7 @@ export default function DiscussionsHome() {
|
||||
component={LegacyBreadcrumbMenu}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="d-flex flex-row">
|
||||
<DiscussionSidebar displaySidebar={displaySidebar} postActionBarRef={postActionBarRef} />
|
||||
{displayContentArea && <DiscussionContent />}
|
||||
@@ -122,6 +124,7 @@ export default function DiscussionsHome() {
|
||||
</Switch>
|
||||
)}
|
||||
</div>
|
||||
<DiscussionsProductTour />
|
||||
</main>
|
||||
{!enableInContextSidebar && <Footer />}
|
||||
</DiscussionContext.Provider>
|
||||
|
||||
@@ -113,7 +113,7 @@ const messages = defineMessages({
|
||||
},
|
||||
showPreviewButton: {
|
||||
id: 'discussions.editor.posts.showPreview.button',
|
||||
defaultMessage: 'Show Preview',
|
||||
defaultMessage: 'Show preview',
|
||||
description: 'show preview button text to allow user to see their post content.',
|
||||
},
|
||||
actionsAlt: {
|
||||
|
||||
@@ -148,12 +148,15 @@ function PostFilterBar({
|
||||
cohort: capitalize(selectedCohort?.name),
|
||||
})}
|
||||
</span>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon src={Tune} />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<Icon src={Tune} />
|
||||
</Collapsible.Visible>
|
||||
<span id="icon-tune">
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon src={Tune} />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<Icon src={Tune} />
|
||||
</Collapsible.Visible>
|
||||
</span>
|
||||
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body className="collapsible-body px-4 pb-3 pt-0">
|
||||
<Form>
|
||||
|
||||
@@ -2,7 +2,9 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, IconButtonWithTooltip } from '@edx/paragon';
|
||||
import {
|
||||
Icon, IconButton, OverlayTrigger, Tooltip,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import { ThumbUpFilled, ThumbUpOutline } from '../../../components/icons';
|
||||
import messages from './messages';
|
||||
@@ -12,7 +14,6 @@ function LikeButton({
|
||||
intl,
|
||||
onClick,
|
||||
voted,
|
||||
preview,
|
||||
}) {
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
@@ -23,20 +24,27 @@ function LikeButton({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="d-flex align-items-center mr-4 text-primary-500">
|
||||
<IconButtonWithTooltip
|
||||
id={`like-${count}-tooltip`}
|
||||
tooltipPlacement="top"
|
||||
tooltipContent={intl.formatMessage(voted ? messages.removeLike : messages.like)}
|
||||
src={voted ? ThumbUpFilled : ThumbUpOutline}
|
||||
iconAs={Icon}
|
||||
alt="Like"
|
||||
onClick={handleClick}
|
||||
size={preview ? 'inline' : 'sm'}
|
||||
className={`mr-0.5 ${preview && 'p-3'}`}
|
||||
iconClassNames={preview && 'icon-size'}
|
||||
/>
|
||||
{(count && count > 0) ? count : null}
|
||||
<div className="d-flex align-items-center mr-36px text-primary-500">
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip id={`liked-${count}-tooltip`}>
|
||||
{intl.formatMessage(voted ? messages.removeLike : messages.like)}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
src={voted ? ThumbUpFilled : ThumbUpOutline}
|
||||
onClick={handleClick}
|
||||
className="post-footer-icon-dimentions"
|
||||
alt="Like"
|
||||
iconAs={Icon}
|
||||
iconClassNames="like-icon-dimentions"
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
<div className="font-family-inter font-style-normal">
|
||||
{(count && count > 0) ? count : null}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -46,13 +54,11 @@ LikeButton.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
voted: PropTypes.bool,
|
||||
preview: PropTypes.bool,
|
||||
};
|
||||
|
||||
LikeButton.defaultProps = {
|
||||
voted: false,
|
||||
onClick: undefined,
|
||||
preview: false,
|
||||
};
|
||||
|
||||
export default injectIntl(LikeButton);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext } from 'react';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
@@ -13,6 +13,7 @@ import { ContentActions } from '../../../data/constants';
|
||||
import { selectorForUnitSubsection, selectTopicContext } from '../../../data/selectors';
|
||||
import { AlertBanner, Confirmation } from '../../common';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import HoverCard from '../../common/HoverCard';
|
||||
import { selectModerationSettings } from '../../data/selectors';
|
||||
import { selectTopic } from '../../topics/data/selectors';
|
||||
import { removeThread, updateExistingThread } from '../data/thunks';
|
||||
@@ -26,6 +27,7 @@ function Post({
|
||||
post,
|
||||
preview,
|
||||
intl,
|
||||
handleAddResponseButton,
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
@@ -39,7 +41,7 @@ function Post({
|
||||
const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false);
|
||||
const [isReporting, showReportConfirmation, hideReportConfirmation] = useToggle(false);
|
||||
const [isClosing, showClosePostModal, hideClosePostModal] = useToggle(false);
|
||||
|
||||
const [showHoverCard, setShowHoverCard] = useState(false);
|
||||
const handleAbusedFlag = () => {
|
||||
if (post.abuseFlagged) {
|
||||
dispatch(updateExistingThread(post.id, { flagged: !post.abuseFlagged }));
|
||||
@@ -62,6 +64,12 @@ function Post({
|
||||
hideReportConfirmation();
|
||||
};
|
||||
|
||||
const handleBlurEvent = (e) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget)) {
|
||||
setShowHoverCard(false);
|
||||
}
|
||||
};
|
||||
|
||||
const actionHandlers = {
|
||||
[ContentActions.EDIT_CONTENT]: () => history.push({
|
||||
...location,
|
||||
@@ -87,7 +95,15 @@ function Post({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column w-100 mw-100" data-testid={`post-${post.id}`}>
|
||||
<div
|
||||
className="d-flex flex-column w-100 mw-100 post-card-comment"
|
||||
aria-level={5}
|
||||
data-testid={`post-${post.id}`}
|
||||
onMouseEnter={() => setShowHoverCard(true)}
|
||||
onMouseLeave={() => setShowHoverCard(false)}
|
||||
onFocus={() => setShowHoverCard(true)}
|
||||
onBlur={(e) => handleBlurEvent(e)}
|
||||
>
|
||||
<Confirmation
|
||||
isOpen={isDeleting}
|
||||
title={intl.formatMessage(messages.deletePostTitle)}
|
||||
@@ -107,16 +123,29 @@ function Post({
|
||||
confirmButtonVariant="danger"
|
||||
/>
|
||||
)}
|
||||
{showHoverCard && (
|
||||
<HoverCard
|
||||
commentOrPost={post}
|
||||
actionHandlers={actionHandlers}
|
||||
handleResponseCommentButton={handleAddResponseButton}
|
||||
addResponseCommentButtonMessage={intl.formatMessage(messages.addResponse)}
|
||||
onLike={() => dispatch(updateExistingThread(post.id, { voted: !post.voted }))}
|
||||
onFollow={() => dispatch(updateExistingThread(post.id, { following: !post.following }))}
|
||||
isClosedPost={post.closed}
|
||||
/>
|
||||
)}
|
||||
<AlertBanner content={post} />
|
||||
<PostHeader post={post} actionHandlers={actionHandlers} />
|
||||
<div className="d-flex mt-4 mb-2 text-break font-style-normal text-primary-500">
|
||||
<HTMLLoader htmlNode={post.renderedBody} id="post" />
|
||||
<PostHeader post={post} />
|
||||
<div className="d-flex mt-14px text-break font-style-normal font-family-inter text-primary-500">
|
||||
<HTMLLoader htmlNode={post.renderedBody} componentId="post" cssClassName="html-loader" testId={post.id} />
|
||||
</div>
|
||||
{topicContext && topic && (
|
||||
<div className={classNames('border px-3 rounded mb-4 border-light-400 align-self-start py-2.5',
|
||||
{ 'w-100': enableInContextSidebar })}
|
||||
<div
|
||||
className={classNames('mt-14px mb-1 font-style-normal font-family-inter font-size-12',
|
||||
{ 'w-100': enableInContextSidebar })}
|
||||
style={{ lineHeight: '20px' }}
|
||||
>
|
||||
<span className="text-gray-500">{intl.formatMessage(messages.relatedTo)}{' '}</span>
|
||||
<span className="text-gray-500" style={{ lineHeight: '20px' }}>{intl.formatMessage(messages.relatedTo)}{' '}</span>
|
||||
<Hyperlink
|
||||
destination={topicContext.unitLink}
|
||||
target="_top"
|
||||
@@ -135,9 +164,7 @@ function Post({
|
||||
</Hyperlink>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3">
|
||||
<PostFooter post={post} preview={preview} />
|
||||
</div>
|
||||
<PostFooter post={post} preview={preview} />
|
||||
<ClosePostReasonModal
|
||||
isOpen={isClosing}
|
||||
onCancel={hideClosePostModal}
|
||||
@@ -154,6 +181,7 @@ Post.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
post: postShape.isRequired,
|
||||
preview: PropTypes.bool,
|
||||
handleAddResponseButton: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Post.defaultProps = {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import * as timeago from 'timeago.js';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Badge, Icon, IconButtonWithTooltip, OverlayTrigger, Tooltip,
|
||||
Icon, IconButton, OverlayTrigger, Tooltip,
|
||||
} from '@edx/paragon';
|
||||
import {
|
||||
Locked,
|
||||
@@ -14,12 +12,9 @@ import {
|
||||
|
||||
import {
|
||||
People,
|
||||
QuestionAnswer,
|
||||
QuestionAnswerOutline,
|
||||
StarFilled,
|
||||
StarOutline,
|
||||
} from '../../../components/icons';
|
||||
import timeLocale from '../../common/time-locale';
|
||||
import { selectUserHasModerationPrivileges } from '../../data/selectors';
|
||||
import { updateExistingThread } from '../data/thunks';
|
||||
import LikeButton from './LikeButton';
|
||||
@@ -29,56 +24,39 @@ import { postShape } from './proptypes';
|
||||
function PostFooter({
|
||||
post,
|
||||
intl,
|
||||
preview,
|
||||
showNewCountLabel,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
timeago.register('time-locale', timeLocale);
|
||||
|
||||
return (
|
||||
<div className="d-flex align-items-center">
|
||||
<LikeButton
|
||||
count={post.voteCount}
|
||||
onClick={() => dispatch(updateExistingThread(post.id, { voted: !post.voted }))}
|
||||
voted={post.voted}
|
||||
preview={preview}
|
||||
/>
|
||||
<IconButtonWithTooltip
|
||||
id={`follow-${post.id}-tooltip`}
|
||||
tooltipPlacement="top"
|
||||
tooltipContent={intl.formatMessage(post.following ? messages.unFollow : messages.follow)}
|
||||
src={post.following ? StarFilled : StarOutline}
|
||||
iconAs={Icon}
|
||||
alt="Follow"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
dispatch(updateExistingThread(post.id, { following: !post.following }));
|
||||
return true;
|
||||
}}
|
||||
size={preview ? 'inline' : 'sm'}
|
||||
className={preview && 'p-3'}
|
||||
iconClassNames={preview && 'icon-size'}
|
||||
/>
|
||||
{preview && post.commentCount > 1 && (
|
||||
<div className="d-flex align-items-center ml-4">
|
||||
<IconButtonWithTooltip
|
||||
tooltipPlacement="top"
|
||||
tooltipContent={intl.formatMessage(messages.viewActivity)}
|
||||
src={post.unreadCommentCount ? QuestionAnswer : QuestionAnswerOutline}
|
||||
iconAs={Icon}
|
||||
alt="Comment Count"
|
||||
size="inline"
|
||||
className="p-3 mr-0.5"
|
||||
iconClassNames="icon-size"
|
||||
/>
|
||||
{post.commentCount}
|
||||
</div>
|
||||
<div className="d-flex align-items-center ml-n1.5 mt-10px" style={{ lineHeight: '32px' }}>
|
||||
{post.voteCount !== 0 && (
|
||||
<LikeButton
|
||||
count={post.voteCount}
|
||||
onClick={() => dispatch(updateExistingThread(post.id, { voted: !post.voted }))}
|
||||
voted={post.voted}
|
||||
/>
|
||||
)}
|
||||
{showNewCountLabel && preview && post?.unreadCommentCount > 0 && post.commentCount > 1 && (
|
||||
<Badge variant="light" className="ml-2">
|
||||
{intl.formatMessage(messages.newLabel, { count: post.unreadCommentCount })}
|
||||
</Badge>
|
||||
{post.following && (
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip id={`follow-${post.id}-tooltip`}>
|
||||
{intl.formatMessage(post.following ? messages.unFollow : messages.follow)}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
src={post.following ? StarFilled : StarOutline}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
dispatch(updateExistingThread(post.id, { following: !post.following }));
|
||||
return true;
|
||||
}}
|
||||
iconAs={Icon}
|
||||
iconClassNames="follow-icon-dimentions"
|
||||
className="post-footer-icon-dimentions"
|
||||
alt="Follow"
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
<div className="d-flex flex-fill justify-content-end align-items-center">
|
||||
{post.groupId && userHasModerationPrivileges && (
|
||||
@@ -100,10 +78,8 @@ function PostFooter({
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span title={post.createdAt} className="text-gray-700">
|
||||
{timeago.format(post.createdAt, 'time-locale')}
|
||||
</span>
|
||||
{!preview && post.closed
|
||||
|
||||
{post.closed
|
||||
&& (
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
@@ -130,13 +106,7 @@ function PostFooter({
|
||||
PostFooter.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
post: postShape.isRequired,
|
||||
preview: PropTypes.bool,
|
||||
showNewCountLabel: PropTypes.bool,
|
||||
};
|
||||
|
||||
PostFooter.defaultProps = {
|
||||
preview: false,
|
||||
showNewCountLabel: false,
|
||||
};
|
||||
|
||||
export default injectIntl(PostFooter);
|
||||
|
||||
@@ -64,11 +64,6 @@ describe('PostFooter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("shows 'x new' badge for new comments in case of read post only", () => {
|
||||
renderComponent(mockPost, true, true);
|
||||
expect(screen.getByText('2 New')).toBeTruthy();
|
||||
});
|
||||
|
||||
it("doesn't have 'new' badge when there are 0 new comments", () => {
|
||||
renderComponent({ ...mockPost, unreadCommentCount: 0 });
|
||||
expect(screen.queryByText('2 New')).toBeFalsy();
|
||||
@@ -89,18 +84,24 @@ describe('PostFooter', () => {
|
||||
expect(screen.getByTestId('cohort-icon')).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each([[true, /unfollow/i], [false, /follow/i]])('test follow button when following=%s', async (following, message) => {
|
||||
renderComponent({ ...mockPost, following });
|
||||
it('test follow button when following=true', async () => {
|
||||
renderComponent({ ...mockPost, following: true });
|
||||
const followButton = screen.getByRole('button', { name: /follow/i });
|
||||
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
||||
await act(async () => {
|
||||
fireEvent.mouseEnter(followButton);
|
||||
});
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent(message);
|
||||
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent(/unfollow/i);
|
||||
await act(async () => {
|
||||
fireEvent.click(followButton);
|
||||
});
|
||||
// clicking on the button triggers thread update.
|
||||
expect(store.getState().threads.status === RequestStatus.IN_PROGRESS).toBeTruthy();
|
||||
});
|
||||
|
||||
it('test follow button when following=false', async () => {
|
||||
renderComponent({ ...mockPost, following: false });
|
||||
expect(screen.queryByRole('button', { name: /follow/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Avatar, Badge, Icon } from '@edx/paragon';
|
||||
|
||||
import { Issue, Question } from '../../../components/icons';
|
||||
import { AvatarOutlineAndLabelColors, ThreadType } from '../../../data/constants';
|
||||
import { ActionsDropdown, AuthorLabel } from '../../common';
|
||||
import { AuthorLabel } from '../../common';
|
||||
import { useAlertBannerVisible } from '../../data/hooks';
|
||||
import { selectAuthorAvatars } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
@@ -24,7 +24,7 @@ export function PostAvatar({
|
||||
const avatarSize = useMemo(() => {
|
||||
let size = '2rem';
|
||||
if (post.type === ThreadType.DISCUSSION && !fromPostLink) {
|
||||
size = '2.375rem';
|
||||
size = '2rem';
|
||||
} else if (post.type === ThreadType.QUESTION) {
|
||||
size = '1.5rem';
|
||||
}
|
||||
@@ -52,11 +52,11 @@ export function PostAvatar({
|
||||
/>
|
||||
)}
|
||||
<Avatar
|
||||
className={classNames('border-0', {
|
||||
className={classNames('border-0 mt-1', {
|
||||
[`outline-${outlineColor}`]: outlineColor,
|
||||
'outline-anonymous': !outlineColor,
|
||||
'mt-3 ml-2': post.type === ThreadType.QUESTION && fromPostLink,
|
||||
'avarat-img-position': post.type === ThreadType.QUESTION,
|
||||
'avarat-img-position mt-17px': post.type === ThreadType.QUESTION,
|
||||
})}
|
||||
style={{
|
||||
height: avatarSize,
|
||||
@@ -86,14 +86,13 @@ function PostHeader({
|
||||
intl,
|
||||
post,
|
||||
preview,
|
||||
actionHandlers,
|
||||
}) {
|
||||
const showAnsweredBadge = preview && post.hasEndorsed && post.type === ThreadType.QUESTION;
|
||||
const authorLabelColor = AvatarOutlineAndLabelColors[post.authorLabel];
|
||||
const hasAnyAlert = useAlertBannerVisible(post);
|
||||
|
||||
return (
|
||||
<div className={classNames('d-flex flex-fill mw-100', { 'mt-2': hasAnyAlert && !preview })}>
|
||||
<div className={classNames('d-flex flex-fill mw-100', { 'mt-10px': hasAnyAlert && !preview })}>
|
||||
<div className="flex-shrink-0">
|
||||
<PostAvatar post={post} authorLabel={post.authorLabel} />
|
||||
</div>
|
||||
@@ -109,21 +108,17 @@ function PostHeader({
|
||||
&& <Badge variant="success">{intl.formatMessage(messages.answered)}</Badge>}
|
||||
</div>
|
||||
)
|
||||
: <h4 className="mb-0" style={{ lineHeight: '28px' }} aria-level="1" tabIndex="-1" accessKey="h">{post.title}</h4>}
|
||||
: <h5 className="mb-0 font-style-normal font-family-inter text-primary-500" style={{ lineHeight: '21px' }} aria-level="1" tabIndex="-1" accessKey="h">{post.title}</h5>}
|
||||
<AuthorLabel
|
||||
author={post.author || intl.formatMessage(messages.anonymous)}
|
||||
authorLabel={post.authorLabel}
|
||||
labelColor={authorLabelColor && `text-${authorLabelColor}`}
|
||||
linkToProfile
|
||||
postCreatedAt={post.createdAt}
|
||||
postOrComment
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!preview
|
||||
&& (
|
||||
<div className="ml-auto d-flex">
|
||||
<ActionsDropdown commentOrPost={post} actionHandlers={actionHandlers} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -132,7 +127,6 @@ PostHeader.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
post: postShape.isRequired,
|
||||
preview: PropTypes.bool,
|
||||
actionHandlers: PropTypes.objectOf(PropTypes.func).isRequired,
|
||||
};
|
||||
|
||||
PostHeader.defaultProps = {
|
||||
|
||||
@@ -6,6 +6,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'anonymous',
|
||||
description: 'Author name displayed when a post is anonymous',
|
||||
},
|
||||
addResponse: {
|
||||
id: 'discussions.post.addResponse',
|
||||
defaultMessage: 'Add response',
|
||||
description: 'Button to add a response in a thread of forum posts',
|
||||
},
|
||||
lastResponse: {
|
||||
id: 'discussions.post.lastResponse',
|
||||
defaultMessage: 'Last response {time}',
|
||||
|
||||
34
src/discussions/tours/DiscussionsProductTour.jsx
Normal file
34
src/discussions/tours/DiscussionsProductTour.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { ProductTour } from '@edx/paragon';
|
||||
|
||||
import { useTourConfiguration } from '../data/hooks';
|
||||
import { fetchDiscussionTours } from './data/thunks';
|
||||
|
||||
function DiscussionsProductTour({ intl }) {
|
||||
const dispatch = useDispatch();
|
||||
const config = useTourConfiguration(intl);
|
||||
useEffect(() => {
|
||||
dispatch(fetchDiscussionTours());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isEmpty(config) && (
|
||||
<ProductTour
|
||||
tours={config}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
DiscussionsProductTour.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(DiscussionsProductTour);
|
||||
14
src/discussions/tours/constants.js
Normal file
14
src/discussions/tours/constants.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import messages from './messages';
|
||||
|
||||
export default function tourCheckpoints(intl) {
|
||||
return {
|
||||
NOT_RESPONDED_FILTER: [
|
||||
{
|
||||
body: intl.formatMessage(messages.notRespondedFilterTourBody),
|
||||
placement: 'right',
|
||||
target: '#icon-tune',
|
||||
title: intl.formatMessage(messages.notRespondedFilterTourTitle),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
30
src/discussions/tours/data/api.js
Normal file
30
src/discussions/tours/data/api.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
// create constant for the API URL
|
||||
export const getDiscussionTourUrl = () => `${getConfig().LMS_BASE_URL}/api/user_tours/discussion_tours/`;
|
||||
|
||||
/**
|
||||
* getDiscussionTours
|
||||
* This function makes an HTTP GET request to the API to retrieve a list of tours for the authenticated user.
|
||||
* @returns {Promise} - A promise that resolves to the API response data.
|
||||
*/
|
||||
export async function getDiscssionTours() {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getDiscussionTourUrl());
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* updateDiscussionTour
|
||||
* This function makes an HTTP PUT request to the API to update a specific tour for the authenticated user.
|
||||
* @param {number} tourId - The ID of the tour to be updated.
|
||||
* @returns {Promise} - A promise that resolves to the API response data.
|
||||
*/
|
||||
export async function updateDiscussionTour(tourId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.put(`${getDiscussionTourUrl()}${tourId}`, {
|
||||
show_tour: false,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
1
src/discussions/tours/data/index.js
Normal file
1
src/discussions/tours/data/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './slices';
|
||||
213
src/discussions/tours/data/redux.test.js
Normal file
213
src/discussions/tours/data/redux.test.js
Normal file
@@ -0,0 +1,213 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
|
||||
import { RequestStatus } from '../../../data/constants';
|
||||
import { initializeStore } from '../../../store';
|
||||
import { getDiscussionTourUrl } from './api';
|
||||
import { selectTours } from './selectors';
|
||||
import {
|
||||
discussionsTourRequest,
|
||||
discussionsToursRequestError,
|
||||
fetchUserDiscussionsToursSuccess,
|
||||
toursReducer,
|
||||
updateUserDiscussionsTourSuccess,
|
||||
} from './slices';
|
||||
import { fetchDiscussionTours, updateTourShowStatus } from './thunks';
|
||||
import discussionTourFactory from './tours.factory';
|
||||
|
||||
let mockAxios;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let store;
|
||||
const url = getDiscussionTourUrl();
|
||||
describe('DiscussionToursThunk', () => {
|
||||
let actualActions;
|
||||
|
||||
const dispatch = (action) => {
|
||||
actualActions.push(action);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
mockAxios = new MockAdapter(getAuthenticatedHttpClient());
|
||||
store = initializeStore();
|
||||
actualActions = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockAxios.reset();
|
||||
});
|
||||
|
||||
it('dispatches get request, success actions', async () => {
|
||||
const mockData = discussionTourFactory.buildList(2);
|
||||
mockAxios.onGet(url)
|
||||
.reply(200, mockData);
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
payload: undefined,
|
||||
type: 'userDiscussionsTours/discussionsTourRequest',
|
||||
},
|
||||
{
|
||||
type: 'userDiscussionsTours/fetchUserDiscussionsToursSuccess',
|
||||
payload: mockData,
|
||||
},
|
||||
];
|
||||
await fetchDiscussionTours()(dispatch);
|
||||
expect(actualActions)
|
||||
.toEqual(expectedActions);
|
||||
});
|
||||
|
||||
it('dispatches request, and error actions', async () => {
|
||||
mockAxios.onGet('/api/discussion-tours/')
|
||||
.reply(500);
|
||||
const errorAction = [{
|
||||
payload: undefined,
|
||||
type: 'userDiscussionsTours/discussionsTourRequest',
|
||||
}, {
|
||||
payload: undefined,
|
||||
type: 'userDiscussionsTours/discussionsToursRequestError',
|
||||
}];
|
||||
|
||||
await fetchDiscussionTours()(dispatch);
|
||||
expect(actualActions)
|
||||
.toEqual(errorAction);
|
||||
});
|
||||
|
||||
it('dispatches put request, success actions', async () => {
|
||||
const mockData = discussionTourFactory.build();
|
||||
mockAxios.onPut(`${url}${1}`)
|
||||
.reply(200, mockData);
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
payload: undefined,
|
||||
type: 'userDiscussionsTours/discussionsTourRequest',
|
||||
},
|
||||
{
|
||||
type: 'userDiscussionsTours/updateUserDiscussionsTourSuccess',
|
||||
payload: mockData,
|
||||
},
|
||||
];
|
||||
await updateTourShowStatus(1)(dispatch);
|
||||
expect(actualActions)
|
||||
.toEqual(expectedActions);
|
||||
});
|
||||
|
||||
it('dispatches update request, and error actions', async () => {
|
||||
mockAxios.onPut(`${url}${1}`)
|
||||
.reply(500);
|
||||
const errorAction = [{
|
||||
payload: undefined,
|
||||
type: 'userDiscussionsTours/discussionsTourRequest',
|
||||
}, {
|
||||
payload: undefined,
|
||||
type: 'userDiscussionsTours/discussionsToursRequestError',
|
||||
}];
|
||||
|
||||
await updateTourShowStatus(1)(dispatch);
|
||||
expect(actualActions)
|
||||
.toEqual(errorAction);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toursReducer', () => {
|
||||
it('handles the discussionsToursRequest action', () => {
|
||||
const initialState = {
|
||||
tours: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
const state = toursReducer(initialState, discussionsTourRequest());
|
||||
expect(state)
|
||||
.toEqual({
|
||||
tours: [],
|
||||
loading: RequestStatus.IN_PROGRESS,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the fetchUserDiscussionsToursSuccess action', () => {
|
||||
const initialState = {
|
||||
tours: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
};
|
||||
const mockData = [{ id: 1 }, { id: 2 }];
|
||||
const state = toursReducer(initialState, fetchUserDiscussionsToursSuccess(mockData));
|
||||
expect(state)
|
||||
.toEqual({
|
||||
tours: mockData,
|
||||
loading: RequestStatus.SUCCESSFUL,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the updateUserDiscussionsTourSuccess action', () => {
|
||||
const initialState = {
|
||||
tours: [
|
||||
{ id: 1 },
|
||||
{ id: 2 },
|
||||
],
|
||||
};
|
||||
const updatedTour = {
|
||||
id: 2,
|
||||
name: 'Updated Tour',
|
||||
};
|
||||
const state = toursReducer(initialState, updateUserDiscussionsTourSuccess(updatedTour));
|
||||
expect(state.tours)
|
||||
.toEqual([{ id: 1 }, updatedTour]);
|
||||
});
|
||||
|
||||
it('handles the discussionsToursRequestError action', () => {
|
||||
const initialState = {
|
||||
tours: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
};
|
||||
const mockError = new Error('Something went wrong');
|
||||
const state = toursReducer(initialState, discussionsToursRequestError(mockError));
|
||||
expect(state)
|
||||
.toEqual({
|
||||
tours: [],
|
||||
loading: RequestStatus.FAILED,
|
||||
error: mockError,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tourSelector', () => {
|
||||
it('returns the tours list from state', () => {
|
||||
const state = {
|
||||
tours: {
|
||||
tours: [
|
||||
{ id: 1, tourName: 'not_responded_filter' },
|
||||
{ id: 2, tourName: 'other_filter' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const expectedResult = [
|
||||
{ id: 1, tourName: 'not_responded_filter' },
|
||||
{ id: 2, tourName: 'other_filter' },
|
||||
];
|
||||
expect(selectTours(state)).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('returns an empty list if the tours state is not defined', () => {
|
||||
const state = {
|
||||
tours: {
|
||||
tours: [],
|
||||
},
|
||||
};
|
||||
expect(selectTours(state))
|
||||
.toEqual([]);
|
||||
});
|
||||
});
|
||||
2
src/discussions/tours/data/selectors.js
Normal file
2
src/discussions/tours/data/selectors.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const selectTours = (state) => state.tours.tours;
|
||||
44
src/discussions/tours/data/slices.js
Normal file
44
src/discussions/tours/data/slices.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/* eslint-disable no-param-reassign,import/prefer-default-export */
|
||||
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { RequestStatus } from '../../../data/constants';
|
||||
|
||||
const userDiscussionsToursSlice = createSlice({
|
||||
name: 'userDiscussionsTours',
|
||||
initialState: {
|
||||
tours: [],
|
||||
loading: RequestStatus.SUCCESSFUL,
|
||||
error: null,
|
||||
},
|
||||
reducers: {
|
||||
discussionsTourRequest: (state) => {
|
||||
state.loading = RequestStatus.IN_PROGRESS;
|
||||
state.error = null;
|
||||
},
|
||||
fetchUserDiscussionsToursSuccess: (state, action) => {
|
||||
state.tours = action.payload;
|
||||
state.loading = RequestStatus.SUCCESSFUL;
|
||||
state.error = null;
|
||||
},
|
||||
discussionsToursRequestError: (state, action) => {
|
||||
state.loading = RequestStatus.FAILED;
|
||||
state.error = action.payload;
|
||||
},
|
||||
updateUserDiscussionsTourSuccess: (state, action) => {
|
||||
const tourIndex = state.tours.findIndex(tour => tour.id === action.payload.id);
|
||||
state.tours[tourIndex] = action.payload;
|
||||
state.loading = RequestStatus.SUCCESSFUL;
|
||||
state.error = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
discussionsTourRequest,
|
||||
fetchUserDiscussionsToursSuccess,
|
||||
discussionsToursRequestError,
|
||||
updateUserDiscussionsTourSuccess,
|
||||
} = userDiscussionsToursSlice.actions;
|
||||
|
||||
export const toursReducer = userDiscussionsToursSlice.reducer;
|
||||
46
src/discussions/tours/data/thunks.js
Normal file
46
src/discussions/tours/data/thunks.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import { getDiscssionTours, updateDiscussionTour } from './api';
|
||||
import {
|
||||
discussionsTourRequest,
|
||||
discussionsToursRequestError,
|
||||
fetchUserDiscussionsToursSuccess,
|
||||
updateUserDiscussionsTourSuccess,
|
||||
} from './slices';
|
||||
|
||||
/**
|
||||
* Action thunk to fetch the list of discussion tours for the current user.
|
||||
* @returns {function} - Thunk that dispatches the request, success, and error actions.
|
||||
*/
|
||||
export function fetchDiscussionTours() {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
dispatch(discussionsTourRequest());
|
||||
const data = await getDiscssionTours();
|
||||
dispatch(fetchUserDiscussionsToursSuccess(camelCaseObject(data)));
|
||||
} catch (error) {
|
||||
dispatch(discussionsToursRequestError());
|
||||
logError(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action thunk to update the show_tour field for a specific discussion tour for the current user.
|
||||
* @param {number} tourId - The ID of the tour to update.
|
||||
* @returns {function} - Thunk that dispatches the request, success, and error actions.
|
||||
*/
|
||||
|
||||
export function updateTourShowStatus(tourId) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
dispatch(discussionsTourRequest());
|
||||
const data = await updateDiscussionTour(tourId);
|
||||
dispatch(updateUserDiscussionsTourSuccess(camelCaseObject(data)));
|
||||
} catch (error) {
|
||||
dispatch(discussionsToursRequestError());
|
||||
logError(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
8
src/discussions/tours/data/tours.factory.js
Normal file
8
src/discussions/tours/data/tours.factory.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
const discussionTourFactory = new Factory()
|
||||
.sequence('id')
|
||||
.attr('name', ['id'], (id) => `Discussion Tour ${id}`)
|
||||
.attr('description', ['id'], (id) => `This is the description for Discussion Tour ${id}.`);
|
||||
|
||||
export default discussionTourFactory;
|
||||
31
src/discussions/tours/messages.js
Normal file
31
src/discussions/tours/messages.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
advanceButtonText: {
|
||||
id: 'tour.action.advance',
|
||||
defaultMessage: 'Next',
|
||||
description: 'Action to go to next step of tour',
|
||||
},
|
||||
dismissButtonText: {
|
||||
id: 'tour.action.dismiss',
|
||||
defaultMessage: 'Dismiss',
|
||||
description: 'Action to dismiss current tour',
|
||||
},
|
||||
endButtonText: {
|
||||
id: 'tour.action.end',
|
||||
defaultMessage: 'Okay',
|
||||
description: 'Action to end current tour',
|
||||
},
|
||||
notRespondedFilterTourBody: {
|
||||
id: 'tour.body.notRespondedFilter',
|
||||
defaultMessage: 'Now you can filter discussions to find posts with no response.',
|
||||
description: 'Body of the tour for the not responded filter',
|
||||
},
|
||||
notRespondedFilterTourTitle: {
|
||||
id: 'tour.title.notRespondedFilter',
|
||||
defaultMessage: 'New filtering option!',
|
||||
description: 'Title of the tour for the not responded filter',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -185,7 +185,6 @@ export function useActions(content) {
|
||||
.every(condition => condition === true)
|
||||
: true
|
||||
);
|
||||
|
||||
return ACTIONS_LIST.filter(
|
||||
({
|
||||
action,
|
||||
@@ -295,3 +294,7 @@ export function handleKeyDown(event) {
|
||||
selectedOption.focus();
|
||||
}
|
||||
}
|
||||
|
||||
export function isLastElementOfList(list, element) {
|
||||
return list[list.length - 1] === element;
|
||||
}
|
||||
|
||||
@@ -201,5 +201,10 @@
|
||||
"discussions.topics.sort.commentCount": "الاكثر نشاطًا",
|
||||
"discussions.topics.sort.courseStructure": "هيكل المساق",
|
||||
"discussions.topics.unnamed.label": "فئة بدون اسم",
|
||||
"discussions.subtopics.unnamed.label": "فئة فرعية بدون اسم"
|
||||
"discussions.subtopics.unnamed.label": "فئة فرعية بدون اسم",
|
||||
"tour.action.advance": "Next",
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -201,5 +201,10 @@
|
||||
"discussions.topics.sort.commentCount": "Most activity",
|
||||
"discussions.topics.sort.courseStructure": "Course Structure",
|
||||
"discussions.topics.unnamed.label": "Unnamed category",
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory"
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory",
|
||||
"tour.action.advance": "Next",
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -201,5 +201,10 @@
|
||||
"discussions.topics.sort.commentCount": "La mayoría de la actividad",
|
||||
"discussions.topics.sort.courseStructure": "Estructura del curso",
|
||||
"discussions.topics.unnamed.label": "Unnamed category",
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory"
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory",
|
||||
"tour.action.advance": "Next",
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -201,5 +201,10 @@
|
||||
"discussions.topics.sort.commentCount": "Most activity",
|
||||
"discussions.topics.sort.courseStructure": "Course Structure",
|
||||
"discussions.topics.unnamed.label": "Unnamed category",
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory"
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory",
|
||||
"tour.action.advance": "Next",
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -25,13 +25,13 @@
|
||||
"discussions.editor.cancel": "Annuler",
|
||||
"discussions.editor.error.empty": "Le contenu de la publication ne peut pas être vide.",
|
||||
"discussions.editor.delete.response.title": "Supprimer la réponse",
|
||||
"discussions.editor.delete.response.description": "Êtes-vous sûr de vouloir supprimer définitivement cette réponse ?",
|
||||
"discussions.editor.delete.response.description": "Êtes-vous sûr de vouloir supprimer définitivement cette réponse?",
|
||||
"discussions.editor.delete.comment.title": "Supprimer le commentaire",
|
||||
"discussions.editor.delete.comment.description": "Êtes-vous sûr de vouloir supprimer définitivement ce commentaire ?",
|
||||
"discussions.editor.delete.comment.description": "Êtes-vous sûr de vouloir supprimer définitivement ce commentaire?",
|
||||
"discussions.delete.confirmation.button.delete": "Supprimer",
|
||||
"discussions.editor.response.response.title": "Signaler un contenu inapproprié ?",
|
||||
"discussions.editor.response.response.title": "Signaler un contenu inapproprié?",
|
||||
"discussions.editor.response.description": "L'équipe de modération de la discussion examinera ce contenu et prendra les mesures appropriées.",
|
||||
"discussions.editor.report.comment.title": "Signaler un contenu inapproprié ?",
|
||||
"discussions.editor.report.comment.title": "Signaler un contenu inapproprié?",
|
||||
"discussions.editor.report.comment.description": "L'équipe de modération de la discussion examinera ce contenu et prendra les mesures appropriées.",
|
||||
"discussions.editor.comments.editReasonCode": "Raison de la modification",
|
||||
"discussions.editor.posts.editReasonCode.error": "Sélectionnez la raison de la modification",
|
||||
@@ -50,7 +50,7 @@
|
||||
"discussions.topics.unnamed.section.label": "Section sans nom",
|
||||
"discussions.topics.unnamed.subsection.label": "Sous-section sans nom",
|
||||
"discussions.subtopics.unnamed.topic.label": "Sujet sans nom",
|
||||
"discussions.topics.title": "Aucun sujet n'existe",
|
||||
"discussions.topics.title": "Aucun sujet n'existe",
|
||||
"discussions.topics.createTopic": "Veuillez contacter votre administrateur pour créer un sujet",
|
||||
"discussions.topics.nothing": "Rien ici encore",
|
||||
"discussions.topics.archived.label": "Archivé",
|
||||
@@ -93,13 +93,13 @@
|
||||
"discussions.sidebar.removeFilters": "Essayez de supprimer certains filtres",
|
||||
"discussions.empty.iconAlt": "Vide",
|
||||
"discussions.authors.label.staff": "Équipe pédagogique",
|
||||
"discussions.authors.label.ta": "assistant d'enseignement",
|
||||
"discussions.authors.label.ta": "Assistant d'enseignement",
|
||||
"discussions.learner.loadMostPosts": "Charger plus de messages",
|
||||
"discussions.post.anonymous.author": "anonyme",
|
||||
"discussion.banner.welcomeMessage": "🎉 Bienvenue dans la nouvelle expérience améliorée de discussions !",
|
||||
"discussion.banner.welcomeMessage": "🎉 Bienvenue dans la nouvelle expérience améliorée de discussions!",
|
||||
"discussion.banner.learnMore": "En savoir plus",
|
||||
"discussion.banner.shareFeedback": "Partager vos commentaires",
|
||||
"discussion.blackoutBanner.information": "La publication dans les discussions est temporairement désactivée par l'équipe du cours",
|
||||
"discussion.blackoutBanner.information": "La publication dans les discussions est temporairement désactivée par l'équipe du cours",
|
||||
"discussions.editor.image.warning.message": "Les images dont la largeur ou la hauteur est supérieure à 999 pixels ne seront pas visibles lorsque la publication, la réponse ou le commentaire est affiché à l'aide de discussions de cours en ligne",
|
||||
"discussions.editor.image.warning.title": "Avertissement!",
|
||||
"discussions.editor.image.warning.dismiss": "Ok",
|
||||
@@ -113,7 +113,7 @@
|
||||
"discussions.posts.actionBar.searchAllPosts": "Recherche dans les messages",
|
||||
"discussions.posts.actionBar.search": "{page, select,\n topics {Search topics}\n posts {Search all posts}\n learners {Search learners}\n myPosts {Search all posts}\n other {{page}}\n }",
|
||||
"discussions.actionBar.searchInfo": "Affichage des résultats {count} pour \"{text}\"",
|
||||
"discussions.actionBar.searchRewriteInfo": "Aucun résultat trouvé pour \"searchString}\". Affichage des résultats {count} pour \"{textSearchRewrite}\".",
|
||||
"discussions.actionBar.searchRewriteInfo": "Aucun résultat trouvé pour \"(searchString}\". Affichage des résultats {count} pour \"{textSearchRewrite}\".",
|
||||
"discussions.actionBar.searchInfoSearching": "Recherche...",
|
||||
"discussions.actionBar.clearSearch": "Effacer les résultats",
|
||||
"discussion.posts.actionBar.add": "Ajouter un message",
|
||||
@@ -151,7 +151,7 @@
|
||||
"discussions.posts.status.filter.anyStatus": "Tout statut",
|
||||
"discussions.posts.status.filter.unread": "Non lu",
|
||||
"discussions.posts.status.filter.following": "Suivi",
|
||||
"discussions.posts.status.filter.reported": "Reporté",
|
||||
"discussions.posts.status.filter.reported": "Signalé",
|
||||
"discussions.posts.status.filter.unanswered": "Non répondu",
|
||||
"discussions.posts.status.filter.unresponded": "Pas répondu",
|
||||
"discussions.posts.filter.myPosts": "Mes messages",
|
||||
@@ -181,9 +181,9 @@
|
||||
"discussions.post.closed": "Message fermé pour réponses et commentaires",
|
||||
"discussions.post.relatedTo": "Relative à",
|
||||
"discussions.editor.delete.post.title": "Supprimer le message",
|
||||
"discussions.editor.delete.post.description": "Êtes-vous sûr de vouloir supprimer définitivement ce message ?",
|
||||
"discussions.editor.delete.post.description": "Êtes-vous sûr de vouloir supprimer définitivement ce message?",
|
||||
"discussions.post.delete.confirmation.button.delete": "Supprimer",
|
||||
"discussions.editor.report.post.title": "Signaler un contenu inapproprié ?",
|
||||
"discussions.editor.report.post.title": "Signaler un contenu inapproprié?",
|
||||
"discussions.editor.report.post.description": "L'équipe de modération de la discussion examinera ce contenu et prendra les mesures appropriées.",
|
||||
"discussions.post.closePostModal.title": "Fermer le message",
|
||||
"discussions.post.closePostModal.text": "Entrez une raison pour fermer ce message. Cela ne sera affiché qu'aux autres modérateurs.",
|
||||
@@ -201,5 +201,10 @@
|
||||
"discussions.topics.sort.commentCount": "La plupart des activités",
|
||||
"discussions.topics.sort.courseStructure": "Structure du cours",
|
||||
"discussions.topics.unnamed.label": "Catégorie sans nom",
|
||||
"discussions.subtopics.unnamed.label": "Sous-catégorie sans nom"
|
||||
"discussions.subtopics.unnamed.label": "Sous-catégorie sans nom",
|
||||
"tour.action.advance": "Suivant",
|
||||
"tour.action.dismiss": "Rejeter",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Vous pouvez maintenant filtrer les discussions pour trouver les messages sans réponse.",
|
||||
"tour.title.notRespondedFilter": "Nouvelle option de filtrage!"
|
||||
}
|
||||
@@ -201,5 +201,10 @@
|
||||
"discussions.topics.sort.commentCount": "Most activity",
|
||||
"discussions.topics.sort.courseStructure": "Course Structure",
|
||||
"discussions.topics.unnamed.label": "Unnamed category",
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory"
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory",
|
||||
"tour.action.advance": "Next",
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -201,5 +201,10 @@
|
||||
"discussions.topics.sort.commentCount": "La maggior parte delle attività",
|
||||
"discussions.topics.sort.courseStructure": "Struttura Corso",
|
||||
"discussions.topics.unnamed.label": "Unnamed category",
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory"
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory",
|
||||
"tour.action.advance": "Next",
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -201,5 +201,10 @@
|
||||
"discussions.topics.sort.commentCount": "Most activity",
|
||||
"discussions.topics.sort.courseStructure": "Course Structure",
|
||||
"discussions.topics.unnamed.label": "Unnamed category",
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory"
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory",
|
||||
"tour.action.advance": "Next",
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -201,5 +201,10 @@
|
||||
"discussions.topics.sort.commentCount": "En çok etkinlik",
|
||||
"discussions.topics.sort.courseStructure": "Ders Yapısı",
|
||||
"discussions.topics.unnamed.label": "İsimsiz kategori",
|
||||
"discussions.subtopics.unnamed.label": "İsimsiz alt kategori"
|
||||
"discussions.subtopics.unnamed.label": "İsimsiz alt kategori",
|
||||
"tour.action.advance": "Next",
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -201,5 +201,10 @@
|
||||
"discussions.topics.sort.commentCount": "Most activity",
|
||||
"discussions.topics.sort.courseStructure": "Course Structure",
|
||||
"discussions.topics.unnamed.label": "Unnamed category",
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory"
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory",
|
||||
"tour.action.advance": "Next",
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
171
src/index.scss
171
src/index.scss
@@ -45,6 +45,14 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.font-size-12 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.font-size-8 {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.font-weight-500 {
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -57,9 +65,24 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
font-family: "Inter";
|
||||
}
|
||||
|
||||
.icon-size {
|
||||
height: 20px !important;
|
||||
.post-footer-icon-dimentions {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
|
||||
.like-icon-dimentions {
|
||||
width: 21px !important;
|
||||
height: 23px !important;
|
||||
}
|
||||
|
||||
.follow-icon-dimentions {
|
||||
width: 21px !important;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.dropdown-icon-dimentions {
|
||||
width: 20px !important;
|
||||
height: 21px !important;
|
||||
}
|
||||
|
||||
.post-summary-icons-dimensions {
|
||||
@@ -77,6 +100,20 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
border-right-style: solid;
|
||||
}
|
||||
|
||||
.my-14px {
|
||||
margin-top: 14px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.my-10px {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mb-14px {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.mr-0\.5 {
|
||||
margin-right: 2px;
|
||||
}
|
||||
@@ -93,6 +130,26 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.mt-14px {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.mb-10px {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mt-10px {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mt-17px {
|
||||
margin-top: 17px !important;
|
||||
}
|
||||
|
||||
.mr-36px {
|
||||
margin-right: 36.6px;
|
||||
}
|
||||
|
||||
.badge-padding {
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px
|
||||
@@ -102,7 +159,7 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
.learner > a:hover {
|
||||
.learner>a:hover {
|
||||
background-color: #F2F0EF;
|
||||
}
|
||||
|
||||
@@ -111,14 +168,27 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.py-8px {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.pb-10px {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.pt-10px {
|
||||
padding-top: 10px !important;
|
||||
}
|
||||
|
||||
.px-10px {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.question-icon-size {
|
||||
width: 1.625rem;
|
||||
height: 1.625rem;
|
||||
width: 1.4581rem;
|
||||
height: 1.4581rem;
|
||||
}
|
||||
|
||||
.question-icon-position {
|
||||
@@ -134,6 +204,7 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
header {
|
||||
.logo {
|
||||
margin-right: 1rem;
|
||||
|
||||
img {
|
||||
height: 1.75rem;
|
||||
}
|
||||
@@ -142,6 +213,7 @@ header {
|
||||
|
||||
#learner-posts-link {
|
||||
color: inherit;
|
||||
|
||||
span[role=heading]:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -170,11 +242,12 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
.pointer-cursor-hover :hover{
|
||||
.pointer-cursor-hover :hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-bar:focus-visible, .filter-bar:focus {
|
||||
.filter-bar:focus-visible,
|
||||
.filter-bar:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -198,9 +271,9 @@ header {
|
||||
};
|
||||
};
|
||||
|
||||
.container-xl{
|
||||
.container-xl {
|
||||
.course-title-lockup {
|
||||
font-size: 1.125 rem;
|
||||
font-size: 1.125rem;
|
||||
};
|
||||
|
||||
.logo {
|
||||
@@ -221,7 +294,7 @@ header {
|
||||
|
||||
.container-xl {
|
||||
padding-left: 31px;
|
||||
font-size: 1.125 rem;
|
||||
font-size: 1.125rem;
|
||||
|
||||
.nav {
|
||||
line-height: 28px;
|
||||
@@ -239,7 +312,7 @@ header {
|
||||
|
||||
.header-action-bar {
|
||||
background-color: #fff;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
box-shadow: 0px 2px 4px rgb(0 0 0 / 15%), 0px 2px 8px rgb(0 0 0 / 15%);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -250,10 +323,10 @@ header {
|
||||
}
|
||||
|
||||
.actions-dropdown {
|
||||
z-index: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.discussion-topic-group:last-of-type .divider{
|
||||
.discussion-topic-group:last-of-type .divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -269,6 +342,12 @@ header {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.btn-icon.btn-icon-primary:hover {
|
||||
background-color: #F2F0EF !important;
|
||||
color: #00262B !important
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
body:not(.tox-force-desktop) .tox .tox-dialog {
|
||||
align-self: center;
|
||||
@@ -283,3 +362,69 @@ header {
|
||||
padding: 1px 5px !important;
|
||||
}
|
||||
|
||||
.pgn__checkpoint {
|
||||
max-width: 340px !important;
|
||||
}
|
||||
|
||||
.post-card-padding {
|
||||
padding: 24px 24px 10px 24px;
|
||||
}
|
||||
|
||||
.post-card-margin {
|
||||
margin: 24px 24px 0px 24px;
|
||||
}
|
||||
|
||||
.hover-card {
|
||||
height: 36px;
|
||||
box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.15), 0px 4px 10px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 3px;
|
||||
background: #FFFFFF;
|
||||
max-width: fit-content;
|
||||
margin-left: auto;
|
||||
margin-top: -2.063rem;
|
||||
z-index: 1;
|
||||
right: 32px;
|
||||
}
|
||||
|
||||
.response-editor-position {
|
||||
margin-top: 50px !important;
|
||||
}
|
||||
|
||||
.hover-button:hover {
|
||||
background-color: #F2F0EF !important;
|
||||
height: 36px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-tertiary:hover {
|
||||
background-color: #F2F0EF;
|
||||
}
|
||||
|
||||
.btn-tertiary:disabled {
|
||||
color: #454545;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.disable-div {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.on-focus:focus-visible {
|
||||
outline: 2px solid black;
|
||||
}
|
||||
|
||||
.html-loader p:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.post-card-comment:hover,
|
||||
.post-card-comment:focus {
|
||||
.hover-card {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner-dimentions {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
@@ -58,4 +58,4 @@ global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.setTimeout(30000);
|
||||
jest.setTimeout(1000000);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { inContextTopicsReducer } from './discussions/in-context-topics/data';
|
||||
import { learnersReducer } from './discussions/learners/data';
|
||||
import { threadsReducer } from './discussions/posts/data';
|
||||
import { topicsReducer } from './discussions/topics/data';
|
||||
import { toursReducer } from './discussions/tours/data';
|
||||
|
||||
export function initializeStore(preloadedState = undefined) {
|
||||
return configureStore({
|
||||
@@ -22,6 +23,7 @@ export function initializeStore(preloadedState = undefined) {
|
||||
blocks: blocksReducer,
|
||||
learners: learnersReducer,
|
||||
courseTabs: courseTabsReducer,
|
||||
tours: toursReducer,
|
||||
},
|
||||
preloadedState,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user