From ce9cdf642b323de045ebef916b9cf33f68a03f30 Mon Sep 17 00:00:00 2001 From: Thomas Tracy Date: Thu, 27 Jan 2022 14:51:17 -0500 Subject: [PATCH] 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 --- package-lock.json | 266 ++++++++++++++++-- package.json | 12 +- .../bulk-email-tool/BulkEmailBody.jsx | 44 --- .../bulk-email-tool/BulkEmailRecepient.jsx | 5 - .../bulk-email-tool/BulkEmailTool.jsx | 34 +-- .../bulk-email-form/BulkEmailForm.jsx | 225 +++++++++++++++ .../bulk-email-form/BulkEmailRecipient.jsx | 95 +++++++ .../bulk-email-form/TaskAlertModal.jsx | 55 ++++ .../bulk-email-tool/bulk-email-form/api.js | 8 + .../bulk-email-tool/bulk-email-form/index.js | 1 + .../test/BulkEmailForm.test.jsx | 65 +++++ .../__factories__/courseMetadata.factory.js | 77 +++++ .../data/__factories__/tab.factory.js | 12 + .../bulk-email-tool/{ => data}/api.js | 0 .../test/BulkEmailTool.test.jsx | 42 +++ .../{ => text-editor}/TextEditor.jsx | 2 +- .../text-editor/__mocks__/TextEditor.jsx | 23 ++ .../bulk-email-tool/text-editor/index.js | 1 + .../navigation-tabs/NavigationTabs.jsx | 4 +- src/setupTest.js | 9 +- src/utils/useTimeout.js | 28 ++ 21 files changed, 900 insertions(+), 108 deletions(-) delete mode 100644 src/components/bulk-email-tool/BulkEmailBody.jsx delete mode 100644 src/components/bulk-email-tool/BulkEmailRecepient.jsx create mode 100644 src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx create mode 100644 src/components/bulk-email-tool/bulk-email-form/BulkEmailRecipient.jsx create mode 100644 src/components/bulk-email-tool/bulk-email-form/TaskAlertModal.jsx create mode 100644 src/components/bulk-email-tool/bulk-email-form/api.js create mode 100644 src/components/bulk-email-tool/bulk-email-form/index.js create mode 100644 src/components/bulk-email-tool/bulk-email-form/test/BulkEmailForm.test.jsx create mode 100644 src/components/bulk-email-tool/data/__factories__/courseMetadata.factory.js create mode 100644 src/components/bulk-email-tool/data/__factories__/tab.factory.js rename src/components/bulk-email-tool/{ => data}/api.js (100%) create mode 100644 src/components/bulk-email-tool/test/BulkEmailTool.test.jsx rename src/components/bulk-email-tool/{ => text-editor}/TextEditor.jsx (97%) create mode 100644 src/components/bulk-email-tool/text-editor/__mocks__/TextEditor.jsx create mode 100644 src/components/bulk-email-tool/text-editor/index.js create mode 100644 src/utils/useTimeout.js diff --git a/package-lock.json b/package-lock.json index 67675a3..d57adb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 3d4c9f6..e57fe72 100644 --- a/package.json +++ b/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" } } diff --git a/src/components/bulk-email-tool/BulkEmailBody.jsx b/src/components/bulk-email-tool/BulkEmailBody.jsx deleted file mode 100644 index 7c0a83f..0000000 --- a/src/components/bulk-email-tool/BulkEmailBody.jsx +++ /dev/null @@ -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 ( -
-
- - - - - - - - - - - - - -
-
- ); -} diff --git a/src/components/bulk-email-tool/BulkEmailRecepient.jsx b/src/components/bulk-email-tool/BulkEmailRecepient.jsx deleted file mode 100644 index ea14088..0000000 --- a/src/components/bulk-email-tool/BulkEmailRecepient.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export default function BulkEmailRecepient() { - return
; -} diff --git a/src/components/bulk-email-tool/BulkEmailTool.jsx b/src/components/bulk-email-tool/BulkEmailTool.jsx index bd44d3c..015e1d0 100644 --- a/src/components/bulk-email-tool/BulkEmailTool.jsx +++ b/src/components/bulk-email-tool/BulkEmailTool.jsx @@ -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 ? ( -
- -
-
- -
-
- -
-
- -
+ return courseMetadata.isStaff ? ( +
+ +
+
+ +
+
+
- ) : +
+ ) : ( + ); } 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" />
diff --git a/src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx b/src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx new file mode 100644 index 0000000..dd19344 --- /dev/null +++ b/src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx @@ -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 ( +
+ +

+ +

+
    + {selectedRecipients.map((group) => ( +
  • {group}
  • + ))} +
+

+ +

+ + )} + close={(event) => { + closeTaskAlert(); + if (event.target.name === 'continue') { + createEmailTask(); + } + }} + /> +
+ + + + + + + {!emailFormValidation.subject && ( + + + + )} + + + + + + + {!emailFormValidation.body && ( + + + + )} + + + { + event.preventDefault(); + openTaskAlert(); + }} + state={emailFormStatus} + icons={{ + default: , + pending: , + complete: , + error: , + }} + labels={{ + default: 'Submit', + pending: 'Submitting', + complete: 'Task Created', + error: 'Error', + }} + disabledStates={['pending', 'complete']} + > + + + {emailFormStatus === FORM_SUBMIT_STATES.ERROR && ( + + + + )} + {(emailFormStatus === FORM_SUBMIT_STATES.COMPLETED_DEFAULT + || emailFormStatus === FORM_SUBMIT_STATES.COMPLETE) && ( + + + + )} + + +
+ ); +} + +BulkEmailForm.propTypes = { + courseId: PropTypes.string.isRequired, +}; diff --git a/src/components/bulk-email-tool/bulk-email-form/BulkEmailRecipient.jsx b/src/components/bulk-email-tool/bulk-email-form/BulkEmailRecipient.jsx new file mode 100644 index 0000000..3fb3c04 --- /dev/null +++ b/src/components/bulk-email-tool/bulk-email-form/BulkEmailRecipient.jsx @@ -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 ( + + + + + + + + + + + + group === DEFAULT_GROUPS.ALL_LEARNERS)} + > + + + group === DEFAULT_GROUPS.ALL_LEARNERS)} + > + + + group === (DEFAULT_GROUPS.AUDIT || DEFAULT_GROUPS.VERIFIED))} + > + + + + {!props.isValid && ( + + + + )} + + ); +} + +BulkEmailRecipient.defaultProps = { + isValid: true, +}; + +BulkEmailRecipient.propTypes = { + selectedGroups: PropTypes.arrayOf(PropTypes.string).isRequired, + handleCheckboxes: PropTypes.func.isRequired, + isValid: PropTypes.bool, +}; diff --git a/src/components/bulk-email-tool/bulk-email-form/TaskAlertModal.jsx b/src/components/bulk-email-tool/bulk-email-form/TaskAlertModal.jsx new file mode 100644 index 0000000..6044d9b --- /dev/null +++ b/src/components/bulk-email-tool/bulk-email-form/TaskAlertModal.jsx @@ -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 ( + + + + + )} + > + {alertMessage} + + ); +} + +TaskAlertModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + close: PropTypes.func.isRequired, + alertMessage: PropTypes.node.isRequired, + intl: intlShape.isRequired, +}; + +export default injectIntl(TaskAlertModal); diff --git a/src/components/bulk-email-tool/bulk-email-form/api.js b/src/components/bulk-email-tool/bulk-email-form/api.js new file mode 100644 index 0000000..50caf4e --- /dev/null +++ b/src/components/bulk-email-tool/bulk-email-form/api.js @@ -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); +} diff --git a/src/components/bulk-email-tool/bulk-email-form/index.js b/src/components/bulk-email-tool/bulk-email-form/index.js new file mode 100644 index 0000000..4cef79c --- /dev/null +++ b/src/components/bulk-email-tool/bulk-email-form/index.js @@ -0,0 +1 @@ +export { default } from './BulkEmailForm'; diff --git a/src/components/bulk-email-tool/bulk-email-form/test/BulkEmailForm.test.jsx b/src/components/bulk-email-tool/bulk-email-form/test/BulkEmailForm.test.jsx new file mode 100644 index 0000000..5ad15d3 --- /dev/null +++ b/src/components/bulk-email-tool/bulk-email-form/test/BulkEmailForm.test.jsx @@ -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(); + expect(screen.getByText('Submit')).toBeTruthy(); + }); + test('it shows a warning when clicking submit', async () => { + render(); + fireEvent.click(screen.getByText('Submit')); + const warning = await screen.findByText('CAUTION!', { exact: false }); + expect(warning).toBeTruthy(); + }); + test('Prevent form POST if invalid', async () => { + render(); + 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(); + 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(); + 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(); + }); + }); +}); diff --git a/src/components/bulk-email-tool/data/__factories__/courseMetadata.factory.js b/src/components/bulk-email-tool/data/__factories__/courseMetadata.factory.js new file mode 100644 index 0000000..3ab0878 --- /dev/null +++ b/src/components/bulk-email-tool/data/__factories__/courseMetadata.factory.js @@ -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; + }); diff --git a/src/components/bulk-email-tool/data/__factories__/tab.factory.js b/src/components/bulk-email-tool/data/__factories__/tab.factory.js new file mode 100644 index 0000000..ceeff90 --- /dev/null +++ b/src/components/bulk-email-tool/data/__factories__/tab.factory.js @@ -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}`); diff --git a/src/components/bulk-email-tool/api.js b/src/components/bulk-email-tool/data/api.js similarity index 100% rename from src/components/bulk-email-tool/api.js rename to src/components/bulk-email-tool/data/api.js diff --git a/src/components/bulk-email-tool/test/BulkEmailTool.test.jsx b/src/components/bulk-email-tool/test/BulkEmailTool.test.jsx new file mode 100644 index 0000000..5ff2d15 --- /dev/null +++ b/src/components/bulk-email-tool/test/BulkEmailTool.test.jsx @@ -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(); + 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(); + expect( + await screen.findByText('An unexpected error occurred. Please click the button below to refresh the page.'), + ).toBeTruthy(); + }); +}); diff --git a/src/components/bulk-email-tool/TextEditor.jsx b/src/components/bulk-email-tool/text-editor/TextEditor.jsx similarity index 97% rename from src/components/bulk-email-tool/TextEditor.jsx rename to src/components/bulk-email-tool/text-editor/TextEditor.jsx index b473a9e..ac06705 100644 --- a/src/components/bulk-email-tool/TextEditor.jsx +++ b/src/components/bulk-email-tool/text-editor/TextEditor.jsx @@ -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'; diff --git a/src/components/bulk-email-tool/text-editor/__mocks__/TextEditor.jsx b/src/components/bulk-email-tool/text-editor/__mocks__/TextEditor.jsx new file mode 100644 index 0000000..fbec83b --- /dev/null +++ b/src/components/bulk-email-tool/text-editor/__mocks__/TextEditor.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +function MockTinyMCE({ onInit }) { + const mockedEditor = { + getContent: () => 'test body', + }; + onInit({}, mockedEditor); + + return
; +} + +MockTinyMCE.propTypes = { + onInit: PropTypes.func.isRequired, +}; + +export default function TextEditor({ onInit }) { + return ; +} + +TextEditor.propTypes = { + onInit: PropTypes.func.isRequired, +}; diff --git a/src/components/bulk-email-tool/text-editor/index.js b/src/components/bulk-email-tool/text-editor/index.js new file mode 100644 index 0000000..0f49036 --- /dev/null +++ b/src/components/bulk-email-tool/text-editor/index.js @@ -0,0 +1 @@ +export { default } from './TextEditor'; diff --git a/src/components/navigation-tabs/NavigationTabs.jsx b/src/components/navigation-tabs/NavigationTabs.jsx index b2797d4..7cfbc54 100644 --- a/src/components/navigation-tabs/NavigationTabs.jsx +++ b/src/components/navigation-tabs/NavigationTabs.jsx @@ -11,7 +11,7 @@ export default function NavigationTabs(props) { @@ -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, diff --git a/src/setupTest.js b/src/setupTest.js index 8665fa5..661bd5d 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -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 - - {children} - + {children} ); } @@ -67,6 +66,4 @@ Wrapper.propTypes = { export * from '@testing-library/react'; // Override `render` method. -export { - renderWithProviders as render, -}; +export { renderWithProviders as render }; diff --git a/src/utils/useTimeout.js b/src/utils/useTimeout.js new file mode 100644 index 0000000..3f4b8a0 --- /dev/null +++ b/src/utils/useTimeout.js @@ -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); + }; +}