feat: [MICROBA-1620] Wire BulkEmailForm to API (#11)
- Wires up the BulkEmailForm to be able to actually send email tasks to the instructor api - Adds testing for the BulkEmailForm, as well as BulkEmailTool itself - Does some cleanup, and adds some additional testing tools as needed
This commit is contained in:
266
package-lock.json
generated
266
package-lock.json
generated
@@ -6861,6 +6861,7 @@
|
||||
"version": "27.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.2.5.tgz",
|
||||
"integrity": "sha512-nmuM4VuDtCZcY+eTpw+0nvstwReMsjPoj7ZR80/BbixulhLaiX+fbv8oeLW8WZlJMcsGQsTmMKT/iTZu1Uy/lQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
@@ -6873,6 +6874,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -6881,6 +6883,7 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
@@ -6890,6 +6893,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -6897,17 +6901,20 @@
|
||||
"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=="
|
||||
"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=="
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"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"
|
||||
}
|
||||
@@ -7178,6 +7185,7 @@
|
||||
"version": "8.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.2.tgz",
|
||||
"integrity": "sha512-idsS/cqbYudXcVWngc1PuWNmXs416oBy2g/7Q8QAUREt5Z3MUkAL2XJD7xazLJ6esDfqRDi/ZBxk+OPPXitHRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@@ -7193,6 +7201,7 @@
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
|
||||
"integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/highlight": "^7.16.7"
|
||||
}
|
||||
@@ -7200,12 +7209,14 @@
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
|
||||
"integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw=="
|
||||
"integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz",
|
||||
"integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.16.7",
|
||||
"chalk": "^2.0.0",
|
||||
@@ -7216,6 +7227,7 @@
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
@@ -7228,6 +7240,7 @@
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz",
|
||||
"integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
@@ -7235,12 +7248,14 @@
|
||||
"aria-query": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz",
|
||||
"integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg=="
|
||||
"integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==",
|
||||
"dev": true
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
@@ -7250,6 +7265,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -7258,6 +7274,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -7268,6 +7285,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -7275,12 +7293,122 @@
|
||||
"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=="
|
||||
"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=="
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@testing-library/jest-dom": {
|
||||
"version": "5.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.1.tgz",
|
||||
"integrity": "sha512-ajUJdfDIuTCadB79ukO+0l8O+QwN0LiSxDaYUTI4LndbbUsGi6rWU1SCexXzBA2NSjlVB9/vbkasQIL3tmPBjw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@types/testing-library__jest-dom": "^5.9.1",
|
||||
"aria-query": "^5.0.0",
|
||||
"chalk": "^3.0.0",
|
||||
"css": "^3.0.0",
|
||||
"css.escape": "^1.5.1",
|
||||
"dom-accessibility-api": "^0.5.6",
|
||||
"lodash": "^4.17.15",
|
||||
"redent": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz",
|
||||
"integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"aria-query": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz",
|
||||
"integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==",
|
||||
"dev": true
|
||||
},
|
||||
"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
|
||||
},
|
||||
"indent-string": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||
"dev": true
|
||||
},
|
||||
"redent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"indent-string": "^4.0.0",
|
||||
"strip-indent": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"strip-indent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"min-indent": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7288,6 +7416,7 @@
|
||||
"version": "12.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.2.tgz",
|
||||
"integrity": "sha512-ihQiEOklNyHIpo2Y8FREkyD1QAea054U0MVbwH1m8N9TxeFz+KoJ9LkqoKqJlzx2JDm56DVwaJ1r36JYxZM05g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@testing-library/dom": "^8.0.0"
|
||||
@@ -7297,6 +7426,7 @@
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz",
|
||||
"integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
@@ -7327,7 +7457,8 @@
|
||||
"@types/aria-query": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
|
||||
"integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig=="
|
||||
"integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/babel__core": {
|
||||
"version": "7.1.14",
|
||||
@@ -7461,12 +7592,14 @@
|
||||
"@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
|
||||
"integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg=="
|
||||
"integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/istanbul-lib-report": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz",
|
||||
"integrity": "sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/istanbul-lib-coverage": "*"
|
||||
}
|
||||
@@ -7475,10 +7608,21 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
|
||||
"integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"@types/jest": {
|
||||
"version": "27.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.0.tgz",
|
||||
"integrity": "sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"jest-diff": "^27.0.0",
|
||||
"pretty-format": "^27.0.0"
|
||||
}
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
|
||||
@@ -7506,7 +7650,8 @@
|
||||
"@types/node": {
|
||||
"version": "15.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
|
||||
"integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww=="
|
||||
"integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/normalize-package-data": {
|
||||
"version": "2.4.1",
|
||||
@@ -7626,6 +7771,15 @@
|
||||
"integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/testing-library__jest-dom": {
|
||||
"version": "5.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.2.tgz",
|
||||
"integrity": "sha512-vehbtyHUShPxIa9SioxDwCvgxukDMH//icJG90sXQBUm5lJOHLT5kNeU9tnivhnA/TkOFMzGIXN2cTc4hY8/kg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/jest": "*"
|
||||
}
|
||||
},
|
||||
"@types/uglify-js": {
|
||||
"version": "3.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz",
|
||||
@@ -7683,6 +7837,7 @@
|
||||
"version": "16.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
|
||||
"integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
@@ -7690,7 +7845,8 @@
|
||||
"@types/yargs-parser": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-13.1.0.tgz",
|
||||
"integrity": "sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg=="
|
||||
"integrity": "sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg==",
|
||||
"dev": true
|
||||
},
|
||||
"@webassemblyjs/ast": {
|
||||
"version": "1.11.1",
|
||||
@@ -8115,6 +8271,7 @@
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^1.9.0"
|
||||
}
|
||||
@@ -8542,6 +8699,7 @@
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.20.0.tgz",
|
||||
"integrity": "sha512-shZRhTjLP0WWdcvHKf3rH3iW9deb3UdKbdnKUoHmmsnBhVXN3sjPJM6ZvQ2r/ywgvBVQrMnjrSyQab60G1sr2w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"is-blob": "^2.1.0",
|
||||
@@ -8551,7 +8709,8 @@
|
||||
"is-buffer": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
|
||||
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
|
||||
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -10032,6 +10191,7 @@
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
@@ -10039,7 +10199,8 @@
|
||||
"color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
||||
"dev": true
|
||||
},
|
||||
"color-string": {
|
||||
"version": "1.9.0",
|
||||
@@ -10362,6 +10523,29 @@
|
||||
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
|
||||
},
|
||||
"css": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz",
|
||||
"integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inherits": "^2.0.4",
|
||||
"source-map": "^0.6.1",
|
||||
"source-map-resolve": "^0.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map-resolve": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz",
|
||||
"integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"atob": "^2.1.2",
|
||||
"decode-uri-component": "^0.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"css-declaration-sorter": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.3.tgz",
|
||||
@@ -10439,6 +10623,12 @@
|
||||
"integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==",
|
||||
"dev": true
|
||||
},
|
||||
"css.escape": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||
"integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=",
|
||||
"dev": true
|
||||
},
|
||||
"cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@@ -11116,7 +11306,8 @@
|
||||
"dom-accessibility-api": {
|
||||
"version": "0.5.10",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.10.tgz",
|
||||
"integrity": "sha512-Xu9mD0UjrJisTmv7lmVSDMagQcU9R5hwAbxsaAE/35XPnPLJobbuREfV/rraiSaEj/UOvgrzQs66zyTWTlyd+g=="
|
||||
"integrity": "sha512-Xu9mD0UjrJisTmv7lmVSDMagQcU9R5hwAbxsaAE/35XPnPLJobbuREfV/rraiSaEj/UOvgrzQs66zyTWTlyd+g==",
|
||||
"dev": true
|
||||
},
|
||||
"dom-converter": {
|
||||
"version": "0.2.0",
|
||||
@@ -11597,7 +11788,8 @@
|
||||
"escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
|
||||
"dev": true
|
||||
},
|
||||
"escodegen": {
|
||||
"version": "2.0.0",
|
||||
@@ -12441,7 +12633,8 @@
|
||||
"fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.2.7",
|
||||
@@ -13502,7 +13695,8 @@
|
||||
"has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
||||
"dev": true
|
||||
},
|
||||
"has-symbol-support-x": {
|
||||
"version": "1.4.2",
|
||||
@@ -14835,7 +15029,8 @@
|
||||
"is-blob": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz",
|
||||
"integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw=="
|
||||
"integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==",
|
||||
"dev": true
|
||||
},
|
||||
"is-boolean-object": {
|
||||
"version": "1.1.0",
|
||||
@@ -17427,7 +17622,8 @@
|
||||
"lz-string": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
|
||||
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY="
|
||||
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=",
|
||||
"dev": true
|
||||
},
|
||||
"mailto-link": {
|
||||
"version": "1.0.0",
|
||||
@@ -17712,6 +17908,12 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"min-indent": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
|
||||
"dev": true
|
||||
},
|
||||
"mini-create-react-context": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
|
||||
@@ -19407,6 +19609,12 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"prettier": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz",
|
||||
"integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-error": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-3.0.4.tgz",
|
||||
@@ -19421,6 +19629,7 @@
|
||||
"version": "27.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.3.1.tgz",
|
||||
"integrity": "sha512-DR/c+pvFc52nLimLROYjnXPtolawm+uWDxr4FjuLDLUn+ktWnSN851KoHwHzzqq6rfCOjkzN8FLgDrSub6UDuA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/types": "^27.2.5",
|
||||
"ansi-regex": "^5.0.1",
|
||||
@@ -19431,17 +19640,20 @@
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true
|
||||
},
|
||||
"react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -20779,6 +20991,12 @@
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"rosie": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/rosie/-/rosie-2.1.0.tgz",
|
||||
"integrity": "sha512-Dbzdc+prLXZuB/suRptDnBUY29SdGvND3bLg6cll8n7PNqzuyCxSlRfrkn8PqjS9n4QVsiM7RCvxCkKAkTQRjA==",
|
||||
"dev": true
|
||||
},
|
||||
"rsvp": {
|
||||
"version": "4.8.5",
|
||||
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
|
||||
@@ -22203,6 +22421,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
@@ -22647,11 +22866,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tinymce/-/tinymce-5.10.2.tgz",
|
||||
"integrity": "sha512-5QhnZ6c8F28fYucLLc00MM37fZoAZ4g7QCYzwIl38i5TwJR5xGqzOv6YMideyLM4tytCzLCRwJoQen2LI66p5A=="
|
||||
},
|
||||
"tinymce-language-selector": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tinymce-language-selector/-/tinymce-language-selector-1.0.4.tgz",
|
||||
"integrity": "sha512-Ko2n2oRFGIZAtWqoG8O8B5XHxZEWZ/zRabPZ5rVeMzDTZhnbVX9oOgW+Ie18PRNMTNtOpI15OUhvrT07c0rVkw=="
|
||||
},
|
||||
"tmp": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||
|
||||
12
package.json
12
package.json
@@ -46,9 +46,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.1.16",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@tinymce/tinymce-react": "^3.13.0",
|
||||
"axios-mock-adapter": "^1.20.0",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "3.15.2",
|
||||
"prop-types": "15.7.2",
|
||||
@@ -59,16 +57,20 @@
|
||||
"react-router-dom": "5.3.0",
|
||||
"redux": "4.1.2",
|
||||
"regenerator-runtime": "0.13.9",
|
||||
"tinymce": "^5.10.2",
|
||||
"tinymce-language-selector": "^1.0.4"
|
||||
"tinymce": "^5.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "8.1.6",
|
||||
"axios-mock-adapter": "^1.20.0",
|
||||
"codecov": "3.8.3",
|
||||
"es-check": "6.1.1",
|
||||
"glob": "7.2.0",
|
||||
"husky": "7.0.4",
|
||||
"jest": "27.3.1",
|
||||
"reactifex": "1.1.1"
|
||||
"prettier": "^2.5.1",
|
||||
"reactifex": "1.1.1",
|
||||
"rosie": "^2.1.0",
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^12.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Button, Form } from '@edx/paragon';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import TextEditor from './TextEditor';
|
||||
|
||||
export default function BulkEmailBody() {
|
||||
const editorRef = useRef(null);
|
||||
const onInit = (event, editor) => { editorRef.current = editor; };
|
||||
|
||||
return (
|
||||
<div className="w-100 m-auto p-lg-4 p-2.5">
|
||||
<Form>
|
||||
<Form.Group controlId="emailSubject">
|
||||
<Form.Label>
|
||||
<FormattedMessage
|
||||
id="bulk.email.subject.label"
|
||||
defaultMessage="Subject:"
|
||||
description="Email subject line input label. Meant to have colon or equivilant punctuation."
|
||||
/>
|
||||
</Form.Label>
|
||||
<Form.Control className="w-lg-50" />
|
||||
</Form.Group>
|
||||
<Form.Group controlId="emailBody">
|
||||
<Form.Label>
|
||||
<FormattedMessage
|
||||
id="bulk.email.body.label"
|
||||
defaultMessage="Body:"
|
||||
description="Email Body label. Meant to have colon or equivilant punctuation."
|
||||
/>
|
||||
</Form.Label>
|
||||
<TextEditor onInit={onInit} />
|
||||
</Form.Group>
|
||||
<Button variant="primary" type="submit">
|
||||
<FormattedMessage
|
||||
id="bulk.email.submit.button"
|
||||
defaultMessage="Submit"
|
||||
description="Submit/Send email button"
|
||||
/>
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function BulkEmailRecepient() {
|
||||
return <div />;
|
||||
}
|
||||
@@ -4,12 +4,11 @@ import classnames from 'classnames';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Spinner } from '@edx/paragon';
|
||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||
import BulkEmailRecepient from './BulkEmailRecepient';
|
||||
import BulkEmailBody from './BulkEmailBody';
|
||||
import BulkEmailTaskManager from './bulk-email-task-manager/BulkEmailTaskManager';
|
||||
import Navigationtabs from '../navigation-tabs/NavigationTabs';
|
||||
import { getCourseHomeCourseMetadata } from './api';
|
||||
import { getCourseHomeCourseMetadata } from './data/api';
|
||||
import useMobileResponsive from '../../utils/useMobileResponsive';
|
||||
import BulkEmailForm from './bulk-email-form';
|
||||
|
||||
export default function BulkEmailTool() {
|
||||
const { courseId } = useParams();
|
||||
@@ -39,23 +38,20 @@ export default function BulkEmailTool() {
|
||||
}, []);
|
||||
|
||||
if (courseMetadata) {
|
||||
return (
|
||||
courseMetadata.isStaff ? (
|
||||
<div>
|
||||
<Navigationtabs courseId={courseId} tabData={courseMetadata.tabs} />
|
||||
<div className={classnames({ 'border border-primary-200': !isMobile })}>
|
||||
<div className="row">
|
||||
<BulkEmailRecepient courseId={courseId} />
|
||||
</div>
|
||||
<div className="row">
|
||||
<BulkEmailBody courseId={courseId} />
|
||||
</div>
|
||||
<div className="row">
|
||||
<BulkEmailTaskManager courseId={courseId} />
|
||||
</div>
|
||||
return courseMetadata.isStaff ? (
|
||||
<div>
|
||||
<Navigationtabs courseId={courseId} tabData={courseMetadata.tabs} />
|
||||
<div className={classnames({ 'border border-primary-200': !isMobile })}>
|
||||
<div className="row">
|
||||
<BulkEmailForm courseId={courseId} />
|
||||
</div>
|
||||
<div className="row">
|
||||
<BulkEmailTaskManager courseId={courseId} />
|
||||
</div>
|
||||
</div>
|
||||
) : <ErrorPage />
|
||||
</div>
|
||||
) : (
|
||||
<ErrorPage />
|
||||
);
|
||||
}
|
||||
return (
|
||||
@@ -64,7 +60,7 @@ export default function BulkEmailTool() {
|
||||
animation="border"
|
||||
variant="primary"
|
||||
role="status"
|
||||
screenReaderText="loading"
|
||||
screenreadertext="loading"
|
||||
className="spinner-border spinner-border-lg text-primary p-5 m-5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
225
src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx
Normal file
225
src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Form, Icon, StatefulButton, useCheckboxSetValues, useToggle,
|
||||
} from '@edx/paragon';
|
||||
import { SpinnerSimple, CheckCircle, Cancel } from '@edx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import TextEditor from '../text-editor/TextEditor';
|
||||
import { postBulkEmail } from './api';
|
||||
import BulkEmailRecipient from './BulkEmailRecipient';
|
||||
import TaskAlertModal from './TaskAlertModal';
|
||||
import useTimeout from '../../../utils/useTimeout';
|
||||
|
||||
export const FORM_SUBMIT_STATES = {
|
||||
DEFAULT: 'default',
|
||||
PENDING: 'pending',
|
||||
COMPLETE: 'complete',
|
||||
COMPLETED_DEFAULT: 'completed_default',
|
||||
ERROR: 'error',
|
||||
};
|
||||
|
||||
export default function BulkEmailForm(props) {
|
||||
const { courseId } = props;
|
||||
const [subject, setSubject] = useState('');
|
||||
const [emailFormStatus, setEmailFormStatus] = useState(FORM_SUBMIT_STATES.DEFAULT);
|
||||
const [emailFormValidation, setEmailFormValidation] = useState({
|
||||
// set these as true on initialization, to prevent invalid messages from prematurely showing
|
||||
subject: true,
|
||||
body: true,
|
||||
recipients: true,
|
||||
});
|
||||
const [selectedRecipients, { add, remove }] = useCheckboxSetValues([]);
|
||||
const [isTaskAlertOpen, openTaskAlert, closeTaskAlert] = useToggle(false);
|
||||
const editorRef = useRef(null);
|
||||
const resetEmailForm = useTimeout(() => {
|
||||
setEmailFormStatus(FORM_SUBMIT_STATES.COMPLETED_DEFAULT);
|
||||
}, 3000);
|
||||
|
||||
const onRecipientChange = (event) => {
|
||||
if (event.target.checked) {
|
||||
add(event.target.value);
|
||||
} else {
|
||||
remove(event.target.value);
|
||||
}
|
||||
};
|
||||
const onInit = (event, editor) => {
|
||||
editorRef.current = editor;
|
||||
};
|
||||
|
||||
const onSubjectChange = (event) => setSubject(event.target.value);
|
||||
|
||||
const validateEmailForm = () => {
|
||||
const subjectValid = subject.length !== 0;
|
||||
const bodyValid = editorRef.current.getContent().length !== 0;
|
||||
const recipientsValid = selectedRecipients.length !== 0;
|
||||
|
||||
setEmailFormValidation({
|
||||
subject: subjectValid,
|
||||
recipients: recipientsValid,
|
||||
body: bodyValid,
|
||||
});
|
||||
|
||||
return subjectValid && bodyValid && recipientsValid;
|
||||
};
|
||||
|
||||
const createEmailTask = async () => {
|
||||
const emailData = new FormData();
|
||||
if (validateEmailForm()) {
|
||||
setEmailFormStatus(() => FORM_SUBMIT_STATES.PENDING);
|
||||
emailData.append('action', 'send');
|
||||
emailData.append('send_to', JSON.stringify(selectedRecipients));
|
||||
emailData.append('subject', subject);
|
||||
emailData.append('message', editorRef.current.getContent());
|
||||
let data;
|
||||
try {
|
||||
data = await postBulkEmail(emailData, courseId);
|
||||
} catch (e) {
|
||||
setEmailFormStatus(FORM_SUBMIT_STATES.ERROR);
|
||||
return;
|
||||
}
|
||||
if (data.status === 200) {
|
||||
setEmailFormStatus(FORM_SUBMIT_STATES.COMPLETE);
|
||||
resetEmailForm();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-100 m-auto p-lg-4 py-2.5 px-5">
|
||||
<TaskAlertModal
|
||||
isOpen={isTaskAlertOpen}
|
||||
alertMessage={(
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="bulk.email.task.alert.recipients"
|
||||
defaultMessage="You are sending an email message with the subject {subject} to the following recipients:"
|
||||
description="A warning shown to the user after submitting the email, to confirm the email recipients."
|
||||
values={{
|
||||
subject,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<ul>
|
||||
{selectedRecipients.map((group) => (
|
||||
<li key={group}>{group}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="bulk.email.task.alert.warning"
|
||||
defaultMessage="CAUTION! When you select Send Email, your email message is added to the queue for sending, and cannot be cancelled."
|
||||
description="Warns the user in an alert that the email may not be immediately sent out to users."
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
close={(event) => {
|
||||
closeTaskAlert();
|
||||
if (event.target.name === 'continue') {
|
||||
createEmailTask();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Form>
|
||||
<BulkEmailRecipient
|
||||
selectedGroups={selectedRecipients}
|
||||
handleCheckboxes={onRecipientChange}
|
||||
isValid={emailFormValidation.recipients}
|
||||
/>
|
||||
<Form.Group controlId="emailSubject">
|
||||
<Form.Label>
|
||||
<FormattedMessage
|
||||
id="bulk.email.subject.label"
|
||||
defaultMessage="Subject:"
|
||||
description="Email subject line input label. Meant to have colon or equivilant punctuation."
|
||||
/>
|
||||
</Form.Label>
|
||||
<Form.Control name="subject" className="w-lg-50" onChange={onSubjectChange} />
|
||||
{!emailFormValidation.subject && (
|
||||
<Form.Control.Feedback className="px-3" hasIcon type="invalid">
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.subject.error"
|
||||
defaultMessage="A subject is required"
|
||||
description="An Error message located under the subject line. Visible only on failure."
|
||||
/>
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
<Form.Group controlId="emailBody">
|
||||
<Form.Label>
|
||||
<FormattedMessage
|
||||
id="bulk.email.body.label"
|
||||
defaultMessage="Body:"
|
||||
description="Email Body label. Meant to have colon or equivilant punctuation."
|
||||
/>
|
||||
</Form.Label>
|
||||
<TextEditor onInit={onInit} />
|
||||
{!emailFormValidation.body && (
|
||||
<Form.Control.Feedback className="px-3" hasIcon type="invalid">
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.body.error"
|
||||
defaultMessage="The message cannot be blank"
|
||||
description="An error message located under the body editor. Visible only on failure."
|
||||
/>
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
<Form.Group className="d-flex flex-row">
|
||||
<StatefulButton
|
||||
variant="primary"
|
||||
type="submit"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
openTaskAlert();
|
||||
}}
|
||||
state={emailFormStatus}
|
||||
icons={{
|
||||
default: <Icon className="icon-download" />,
|
||||
pending: <Icon src={SpinnerSimple} className="icon-spin" />,
|
||||
complete: <Icon src={CheckCircle} />,
|
||||
error: <Icon src={Cancel} />,
|
||||
}}
|
||||
labels={{
|
||||
default: 'Submit',
|
||||
pending: 'Submitting',
|
||||
complete: 'Task Created',
|
||||
error: 'Error',
|
||||
}}
|
||||
disabledStates={['pending', 'complete']}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="bulk.email.submit.button"
|
||||
defaultMessage="Submit"
|
||||
description="Submit/Send email button"
|
||||
/>
|
||||
</StatefulButton>
|
||||
{emailFormStatus === FORM_SUBMIT_STATES.ERROR && (
|
||||
<Form.Control.Feedback className="px-3" hasIcon={false} type="invalid">
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.error"
|
||||
defaultMessage="An error occured while attempting to send the email."
|
||||
description="An Error message located under the submit button for the email form. Visible only on a failure."
|
||||
/>
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
{(emailFormStatus === FORM_SUBMIT_STATES.COMPLETED_DEFAULT
|
||||
|| emailFormStatus === FORM_SUBMIT_STATES.COMPLETE) && (
|
||||
<Form.Control.Feedback className="px-3" hasIcon={false} type="valid">
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.complete"
|
||||
defaultMessage="A task to send the emails has been successfully created!"
|
||||
description="A success message displays under the submit button when successfully completing the form."
|
||||
/>
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BulkEmailForm.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Form } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const DEFAULT_GROUPS = {
|
||||
SELF: 'myself',
|
||||
STAFF: 'staff',
|
||||
ALL_LEARNERS: 'learners',
|
||||
VERIFIED: 'track:verified',
|
||||
AUDIT: 'track:audit',
|
||||
};
|
||||
|
||||
export default function BulkEmailRecipient(props) {
|
||||
const { handleCheckboxes, selectedGroups } = props;
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.recipients.sendLabel"
|
||||
defaultMessage="Send To:"
|
||||
description="A label before the list of potential recipients"
|
||||
/>
|
||||
</Form.Label>
|
||||
<Form.CheckboxSet name="recipientGroups" onChange={handleCheckboxes} value={selectedGroups}>
|
||||
<Form.Checkbox key="myself" value="myself">
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.recipients.myself"
|
||||
defaultMessage="Myself"
|
||||
description="A selectable choice from a list of potential email recipients"
|
||||
/>
|
||||
</Form.Checkbox>
|
||||
<Form.Checkbox key="staff" value="staff">
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.recipients.staff"
|
||||
defaultMessage="Staff/Administrators"
|
||||
description="A selectable choice from a list of potential email recipients"
|
||||
/>
|
||||
</Form.Checkbox>
|
||||
<Form.Checkbox
|
||||
key="track:audit"
|
||||
value="track:audit"
|
||||
disabled={selectedGroups.find((group) => group === DEFAULT_GROUPS.ALL_LEARNERS)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.recipients.audit"
|
||||
defaultMessage="Learners in the audit track"
|
||||
description="A selectable choice from a list of potential email recipients"
|
||||
/>
|
||||
</Form.Checkbox>
|
||||
<Form.Checkbox
|
||||
key="track:verified"
|
||||
value="track:verified"
|
||||
disabled={selectedGroups.find((group) => group === DEFAULT_GROUPS.ALL_LEARNERS)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.recipients.verified"
|
||||
defaultMessage="Learners in the verified certificate track"
|
||||
description="A selectable choice from a list of potential email recipients"
|
||||
/>
|
||||
</Form.Checkbox>
|
||||
<Form.Checkbox
|
||||
key="learners"
|
||||
value="learners"
|
||||
disabled={selectedGroups.find((group) => group === (DEFAULT_GROUPS.AUDIT || DEFAULT_GROUPS.VERIFIED))}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.recipients.learners"
|
||||
defaultMessage="All Learners"
|
||||
description="A selectable choice from a list of potential email recipients"
|
||||
/>
|
||||
</Form.Checkbox>
|
||||
</Form.CheckboxSet>
|
||||
{!props.isValid && (
|
||||
<Form.Control.Feedback className="px-3" hasIcon type="invalid">
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.recipients.error"
|
||||
defaultMessage="At least one recipient is required"
|
||||
description="An Error message located under the recipients list. Visible only on failure"
|
||||
/>
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
BulkEmailRecipient.defaultProps = {
|
||||
isValid: true,
|
||||
};
|
||||
|
||||
BulkEmailRecipient.propTypes = {
|
||||
selectedGroups: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
handleCheckboxes: PropTypes.func.isRequired,
|
||||
isValid: PropTypes.bool,
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ActionRow, AlertModal, Button } from '@edx/paragon';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
function TaskAlertModal(props) {
|
||||
const {
|
||||
isOpen, close, alertMessage, intl,
|
||||
} = props;
|
||||
|
||||
const messages = {
|
||||
taskAlertTitle: {
|
||||
id: 'bulk.email.task.alert.title',
|
||||
defaultMessage: 'Caution',
|
||||
description: 'Title in the header of the alert',
|
||||
},
|
||||
};
|
||||
return (
|
||||
<AlertModal
|
||||
title={intl.formatMessage(messages.taskAlertTitle)}
|
||||
isBlocking
|
||||
isOpen={isOpen}
|
||||
onClose={close}
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<Button variant="tertiary" onClick={close} name="cancel">
|
||||
<FormattedMessage
|
||||
id="bulk.email.task.alert.cancel"
|
||||
defaultMessage="Cancel"
|
||||
description="Cancel button for the task alert"
|
||||
/>
|
||||
</Button>
|
||||
<Button variant="primary" onClick={close} name="continue">
|
||||
<FormattedMessage
|
||||
id="bulk.email.form.recipients.Contine"
|
||||
defaultMessage="Continue"
|
||||
description="Continue button for the task alert"
|
||||
/>
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
>
|
||||
{alertMessage}
|
||||
</AlertModal>
|
||||
);
|
||||
}
|
||||
|
||||
TaskAlertModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
alertMessage: PropTypes.node.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(TaskAlertModal);
|
||||
8
src/components/bulk-email-tool/bulk-email-form/api.js
Normal file
8
src/components/bulk-email-tool/bulk-email-form/api.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
export async function postBulkEmail(email, courseId) {
|
||||
const url = `${getConfig().LMS_BASE_URL}/courses/${courseId}/instructor/api/send_email`;
|
||||
return getAuthenticatedHttpClient().post(url, email);
|
||||
}
|
||||
1
src/components/bulk-email-tool/bulk-email-form/index.js
Normal file
1
src/components/bulk-email-tool/bulk-email-form/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './BulkEmailForm';
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
render, screen, cleanup, act, fireEvent,
|
||||
} from '../../../../setupTest';
|
||||
import BulkEmailForm from '..';
|
||||
import { postBulkEmail } from '../api';
|
||||
|
||||
jest.mock('../../text-editor/TextEditor');
|
||||
jest.mock('../api', () => ({
|
||||
__esModule: true,
|
||||
postBulkEmail: jest.fn(() => ({ status: 200 })),
|
||||
}));
|
||||
|
||||
describe('bulk-email-form', () => {
|
||||
beforeEach(() => jest.resetModules());
|
||||
afterEach(cleanup);
|
||||
test('it renders', () => {
|
||||
render(<BulkEmailForm courseId="test-course-id" />);
|
||||
expect(screen.getByText('Submit')).toBeTruthy();
|
||||
});
|
||||
test('it shows a warning when clicking submit', async () => {
|
||||
render(<BulkEmailForm courseId="test-course-id" />);
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
const warning = await screen.findByText('CAUTION!', { exact: false });
|
||||
expect(warning).toBeTruthy();
|
||||
});
|
||||
test('Prevent form POST if invalid', async () => {
|
||||
render(<BulkEmailForm courseId="test-course-id" />);
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
expect(await screen.findByRole('button', { name: /continue/i })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /continue/i }));
|
||||
expect(await screen.findByText('At least one recipient is required', { exact: false })).toBeInTheDocument();
|
||||
expect(await screen.findByText('A subject is required')).toBeInTheDocument();
|
||||
});
|
||||
test('Shows complete message on completed POST', async () => {
|
||||
render(<BulkEmailForm courseId="test-course-id" />);
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: 'Myself' }));
|
||||
expect(screen.getByRole('checkbox', { name: 'Myself' })).toBeChecked();
|
||||
fireEvent.change(screen.getByRole('textbox', { name: 'Subject:' }), { target: { value: 'test subject' } });
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
expect(await screen.findByRole('button', { name: /continue/i })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /continue/i }));
|
||||
expect(await screen.findByText('Submitting')).toBeInTheDocument();
|
||||
expect(await screen.findByText('A task to send the emails has been successfully created!')).toBeInTheDocument();
|
||||
});
|
||||
test('Shows Error on failed POST', async () => {
|
||||
postBulkEmail.mockImplementation(() => {
|
||||
throw Error('api-response-error');
|
||||
});
|
||||
await act(async () => {
|
||||
render(<BulkEmailForm courseId="test-course-id" />);
|
||||
const subjectLine = screen.getByRole('textbox', { name: 'Subject:' });
|
||||
const recipient = screen.getByRole('checkbox', { name: 'Myself' });
|
||||
fireEvent.click(recipient);
|
||||
fireEvent.change(subjectLine, { target: { value: 'test subject' } });
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
expect(await screen.findByRole('button', { name: /continue/i })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /continue/i }));
|
||||
expect(await screen.findByText('Error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
|
||||
import './tab.factory';
|
||||
|
||||
export default Factory.define('courseMetadata')
|
||||
.sequence('id', i => `course-v1:edX+DemoX+Demo_Course_${i}`)
|
||||
.option('host', 'http://localhost:18000')
|
||||
.attrs({
|
||||
is_staff: true,
|
||||
original_user_is_staff: false,
|
||||
number: 'DemoX',
|
||||
org: 'edX',
|
||||
verified_mode: {
|
||||
upgrade_url: 'test',
|
||||
price: 10,
|
||||
currency_symbol: '$',
|
||||
},
|
||||
})
|
||||
.attr('tabs', ['id', 'host'], (id, host) => {
|
||||
const tabs = [
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Course',
|
||||
slug: 'courseware',
|
||||
type: 'courseware',
|
||||
},
|
||||
{ courseId: id, host, path: 'course/' },
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Discussion',
|
||||
slug: 'discussion',
|
||||
type: 'discussion',
|
||||
},
|
||||
{ courseId: id, host, path: 'discussion/forum/' },
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Wiki',
|
||||
slug: 'wiki',
|
||||
type: 'wiki',
|
||||
},
|
||||
{ courseId: id, host, path: 'course_wiki' },
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Progress',
|
||||
slug: 'progress',
|
||||
type: 'progress',
|
||||
},
|
||||
{ courseId: id, host, path: 'progress' },
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Instructor',
|
||||
slug: 'instructor',
|
||||
type: 'instructor',
|
||||
},
|
||||
{ courseId: id, host, path: 'instructor' },
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Dates',
|
||||
slug: 'dates',
|
||||
type: 'dates',
|
||||
},
|
||||
{ courseId: id, host, path: 'dates' },
|
||||
),
|
||||
];
|
||||
|
||||
return tabs;
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
|
||||
|
||||
Factory.define('tab')
|
||||
.option('courseId', 'course-v1:edX+DemoX+Demo_Course')
|
||||
.option('path', 'course/')
|
||||
.option('host', 'http://localhost:18000')
|
||||
.attrs({
|
||||
title: 'Course',
|
||||
slug: 'courseware',
|
||||
})
|
||||
.attr('tab_id', ['slug'], slug => slug)
|
||||
.attr('url', ['courseId', 'path', 'host'], (courseId, path, host) => `${host}/courses/${courseId}/${path}`);
|
||||
42
src/components/bulk-email-tool/test/BulkEmailTool.test.jsx
Normal file
42
src/components/bulk-email-tool/test/BulkEmailTool.test.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Factory } from 'rosie';
|
||||
import { render, screen } from '../../../setupTest';
|
||||
import BulkEmailTool from '../BulkEmailTool';
|
||||
import { getCourseHomeCourseMetadata } from '../data/api';
|
||||
import '../data/__factories__/courseMetadata.factory';
|
||||
|
||||
jest.mock('../text-editor/TextEditor');
|
||||
jest.mock('../bulk-email-task-manager/api', () => ({
|
||||
getInstructorTasks: jest.fn(() => ({ tasks: {} })),
|
||||
getEmailTaskHistory: jest.fn(() => ({ tasks: {} })),
|
||||
}));
|
||||
jest.mock('../data/api', () => ({
|
||||
__esModule: true,
|
||||
getCourseHomeCourseMetadata: jest.fn(() => {}),
|
||||
}));
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: jest.fn(() => ({
|
||||
courseId: 'test-course-id',
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('BulkEmailTool', () => {
|
||||
test('BulkEmailTool renders properly when given course metadata', async () => {
|
||||
const courseMetadata = Factory.build('courseMetadata');
|
||||
getCourseHomeCourseMetadata.mockImplementation(() => courseMetadata);
|
||||
render(<BulkEmailTool />);
|
||||
expect(await screen.findByText('Course')).toBeTruthy();
|
||||
});
|
||||
test('BulkEmailTool renders error page on no staff user', async () => {
|
||||
const courseMetadata = Factory.build('courseMetadata', { is_staff: false });
|
||||
getCourseHomeCourseMetadata.mockImplementation(() => courseMetadata);
|
||||
render(<BulkEmailTool />);
|
||||
expect(
|
||||
await screen.findByText('An unexpected error occurred. Please click the button below to refresh the page.'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,7 @@ import 'tinymce/plugins/emoticons/js/emojis';
|
||||
import 'tinymce/plugins/link';
|
||||
import 'tinymce/plugins/lists';
|
||||
import 'tinymce/plugins/table';
|
||||
import 'tinymce-language-selector';
|
||||
import '@edx/tinymce-language-selector';
|
||||
|
||||
import contentUiCss from 'tinymce/skins/ui/oxide/content.css';
|
||||
import contentCss from 'tinymce/skins/content/default/content.css';
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function MockTinyMCE({ onInit }) {
|
||||
const mockedEditor = {
|
||||
getContent: () => 'test body',
|
||||
};
|
||||
onInit({}, mockedEditor);
|
||||
|
||||
return <div />;
|
||||
}
|
||||
|
||||
MockTinyMCE.propTypes = {
|
||||
onInit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default function TextEditor({ onInit }) {
|
||||
return <MockTinyMCE onInit={onInit} />;
|
||||
}
|
||||
|
||||
TextEditor.propTypes = {
|
||||
onInit: PropTypes.func.isRequired,
|
||||
};
|
||||
1
src/components/bulk-email-tool/text-editor/index.js
Normal file
1
src/components/bulk-email-tool/text-editor/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './TextEditor';
|
||||
@@ -11,7 +11,7 @@ export default function NavigationTabs(props) {
|
||||
<Nav>
|
||||
{tabData && tabData.map(tab => (
|
||||
<Nav.Item key={tab.tab_id}>
|
||||
<Nav.Link eventKey={tab.tab_id} href={tab.url} className="mx-3 py-2">{tab.title}</Nav.Link>
|
||||
<Nav.Link eventKey={tab.url} href={tab.url} className="mx-3 py-2">{tab.title}</Nav.Link>
|
||||
</Nav.Item>
|
||||
))}
|
||||
</Nav>
|
||||
@@ -20,7 +20,7 @@ export default function NavigationTabs(props) {
|
||||
}
|
||||
|
||||
NavigationTabs.propTypes = {
|
||||
tabData: PropTypes.arrayOf(PropTypes.exact({
|
||||
tabData: PropTypes.arrayOf(PropTypes.shape({
|
||||
tab_id: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
import '@testing-library/jest-dom';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { render } from '@testing-library/react';
|
||||
@@ -50,9 +51,7 @@ configureI18n({
|
||||
function Wrapper({ children }) {
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-filename-extension
|
||||
<AppProvider>
|
||||
{children}
|
||||
</AppProvider>
|
||||
<AppProvider>{children}</AppProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,6 +66,4 @@ Wrapper.propTypes = {
|
||||
export * from '@testing-library/react';
|
||||
|
||||
// Override `render` method.
|
||||
export {
|
||||
renderWithProviders as render,
|
||||
};
|
||||
export { renderWithProviders as render };
|
||||
|
||||
28
src/utils/useTimeout.js
Normal file
28
src/utils/useTimeout.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* React hook to delay a function being called by a given delay amount.
|
||||
* Works by creating a ref to a setTimeout() function call to the DOM and
|
||||
* waiting for the timer to end before firing the callback. if multiple
|
||||
* timouts are made at once, the hook will remove all previous timeouts
|
||||
* and only allow one at a time.
|
||||
* @param {function} callback The function to call once the delay ends
|
||||
* @param {millisecond} delay time to delay function call
|
||||
*/
|
||||
export default function useTimeout(callback, delay) {
|
||||
const timeoutRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = timeoutRef.current;
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = setTimeout(callback, delay);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user