diff --git a/.env.development b/.env.development index 3fe9f8fa..f90df7b3 100644 --- a/.env.development +++ b/.env.development @@ -1,13 +1,13 @@ NODE_ENV='development' -PORT=8080 +PORT=1999 ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' -BASE_URL='http://localhost:8080' +BASE_URL='http://localhost:1999' CREDENTIALS_BASE_URL='http://localhost:18150' CSRF_TOKEN_API_PATH='/csrf/api/v1/token' ECOMMERCE_BASE_URL='http://localhost:18130' LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference' LMS_BASE_URL='http://localhost:18000' -LOGIN_URL='http://localhost:18000/login' +LOGIN_URL='http://localhost:1999/login' LOGOUT_URL='http://localhost:18000/logout' MARKETING_SITE_BASE_URL='http://localhost:18000' ORDER_HISTORY_URL='http://localhost:1996/orders' diff --git a/.eslintignore b/.eslintignore index 6e1aa5ed..0db94630 100755 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ coverage/* dist/ node_modules/ -jest.config.js \ No newline at end of file +__mocks__/ +__snapshots__/ diff --git a/Makefile b/Makefile index 3377acdf..7027c3b0 100755 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -transifex_resource = frontend-template-application +transifex_resource = frontend-app-logistration transifex_langs = "ar,fr,es_419,zh_CN" transifex_utils = ./node_modules/.bin/transifex-utils.js diff --git a/README.rst b/README.rst index 74e1b849..da18049f 100644 --- a/README.rst +++ b/README.rst @@ -1,62 +1,49 @@ |Build Status| |Codecov| |license| -frontend-template-application +frontend-app-logistration ================================= -Please tag **@edx/fedx-team** on any PRs or issues. Thanks. +This is a micro-frontend application responsible for the login, registration and password reset functionality. -Introduction ------------- +Development +----------- -This repository is a template for Open edX micro-frontend applications. It is flagged as a Template Repository, meaning it can be used as a basis for new GitHub repositories by clicking the green "Use this template" button above. The rest of this document describes how to work with your new micro-frontend after you've created a new repository from the template. +Start Devstack +^^^^^^^^^^^^^^ -After Copying The Template --------------------------- +To use this application `devstack `__ must be running and you must be logged into it. -You'll want to do a find-and-replace to replace all instances of ``frontend-template-application`` with the name of your new repository. +- Start devstack +- Log in (http://localhost:18000/login) -**Prerequisite** +Start the development server +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -`Devstack `_. If you start Devstack with ``make dev.up.ecommerce`` that should give you everything you need as a companion to this frontend. +In this project, install requirements and start the development server by running: -**Installation and Startup** +.. code:: bash -In the following steps, replace "frontend-template-application' with the name of the repo you created when copying this template above. + npm install + npm start # The server will run on port 1999 -1. Clone your new repo: +Once the dev server is up visit http://localhost:1999. - ``git clone https://github.com/edx/frontend-template-application.git`` +Configuration and Deployment +---------------------------- -2. Install npm dependencies: +This MFE is configured via node environment variables supplied at build time. See the .env file for the list of required environment variables. Example build syntax with a single environment variable: - ``cd frontend-template-application && npm install`` +.. code:: bash -3. Start the dev server: + NODE_ENV=development ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' npm run build - ``npm start`` -The dev server is running at `http://localhost:8080 `_. +For more information see the document: `Micro-frontend applications in Open +edX `__. -Project Structure ------------------ - -The source for this project is organized into nested submodules according to the ADR `Feature-based Application Organization `_. - -Build Process Notes -------------------- - -**Production Build** - -The production build is created with ``npm run build``. - -Internationalization --------------------- - -Please see `edx/frontend-platform's i18n module `_ for documentation on internationalization. The documentation explains how to use it, and the `How To `_ has more detail. - -.. |Build Status| image:: https://api.travis-ci.org/edx/frontend-template-application.svg?branch=master - :target: https://travis-ci.org/edx/frontend-template-application -.. |Codecov| image:: https://codecov.io/gh/edx/frontend-template-application/branch/master/graph/badge.svg - :target: https://codecov.io/gh/edx/frontend-template-application -.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-template-application.svg - :target: @edx/frontend-template-application +.. |Build Status| image:: https://api.travis-ci.org/edx/frontend-app-account.svg?branch=master + :target: https://travis-ci.org/edx/frontend-app-account +.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-app-account + :target: https://codecov.io/gh/edx/frontend-app-account +.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-account.svg + :target: @edx/frontend-app-account \ No newline at end of file diff --git a/openedx.yaml b/openedx.yaml index 9ae31706..6254e6e5 100644 --- a/openedx.yaml +++ b/openedx.yaml @@ -1,12 +1,9 @@ # This file describes this Open edX repo, as described in OEP-2: # http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification -nick: tmpa +nick: logistration oeps: {} -owner: edx/arch-team +owner: edx/vanguards openedx-release: - # The openedx-release key is described in OEP-10: - # https://open-edx-proposals.readthedocs.io/en/latest/oep-0010-proc-openedx-releases.html - # The FAQ might also be helpful: https://openedx.atlassian.net/wiki/spaces/COMM/pages/1331268879/Open+edX+Release+FAQ maybe: true # Delete this "maybe" line when you have decided about Open edX inclusion. ref: master diff --git a/package-lock.json b/package-lock.json index 7190004b..f362eeee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "@edx/frontend-template-application", + "name": "@edx/frontend-app-logistration", "version": "0.1.0", "lockfileVersion": 1, "requires": true, @@ -3183,6 +3183,53 @@ "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "dev": true }, + "@redux-saga/core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.1.3.tgz", + "integrity": "sha512-8tInBftak8TPzE6X13ABmEtRJGjtK17w7VUs7qV17S8hCO5S3+aUTWZ/DBsBJPdE8Z5jOPwYALyvofgq1Ws+kg==", + "requires": { + "@babel/runtime": "^7.6.3", + "@redux-saga/deferred": "^1.1.2", + "@redux-saga/delay-p": "^1.1.2", + "@redux-saga/is": "^1.1.2", + "@redux-saga/symbols": "^1.1.2", + "@redux-saga/types": "^1.1.0", + "redux": "^4.0.4", + "typescript-tuple": "^2.2.1" + } + }, + "@redux-saga/deferred": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.1.2.tgz", + "integrity": "sha512-908rDLHFN2UUzt2jb4uOzj6afpjgJe3MjICaUNO3bvkV/kN/cNeI9PMr8BsFXB/MR8WTAZQq/PlTq8Kww3TBSQ==" + }, + "@redux-saga/delay-p": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.1.2.tgz", + "integrity": "sha512-ojc+1IoC6OP65Ts5+ZHbEYdrohmIw1j9P7HS9MOJezqMYtCDgpkoqB5enAAZrNtnbSL6gVCWPHaoaTY5KeO0/g==", + "requires": { + "@redux-saga/symbols": "^1.1.2" + } + }, + "@redux-saga/is": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.2.tgz", + "integrity": "sha512-OLbunKVsCVNTKEf2cH4TYyNbbPgvmZ52iaxBD4I1fTif4+MTXMa4/Z07L83zW/hTCXwpSZvXogqMqLfex2Tg6w==", + "requires": { + "@redux-saga/symbols": "^1.1.2", + "@redux-saga/types": "^1.1.0" + } + }, + "@redux-saga/symbols": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.2.tgz", + "integrity": "sha512-EfdGnF423glv3uMwLsGAtE6bg+R9MdqlHEzExnfagXPrIiuxwr3bdiAwz3gi+PsrQ3yBlaBpfGLtDG8rf3LgQQ==" + }, + "@redux-saga/types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.1.0.tgz", + "integrity": "sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg==" + }, "@sindresorhus/is": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", @@ -3415,8 +3462,7 @@ "@types/node": { "version": "13.7.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.0.tgz", - "integrity": "sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==", - "dev": true + "integrity": "sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -4173,7 +4219,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", - "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.0-next.1" @@ -4183,7 +4228,6 @@ "version": "1.17.4", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", - "dev": true, "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", @@ -4201,14 +4245,12 @@ "is-callable": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" }, "is-regex": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, "requires": { "has": "^1.0.3" } @@ -4217,7 +4259,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, "requires": { "define-properties": "^1.1.3", "function-bind": "^1.1.1" @@ -4227,7 +4268,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, "requires": { "define-properties": "^1.1.3", "function-bind": "^1.1.1" @@ -5127,8 +5167,7 @@ "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, "bootstrap": { "version": "4.4.1", @@ -5627,7 +5666,6 @@ "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz", "integrity": "sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==", - "dev": true, "requires": { "css-select": "~1.2.0", "dom-serializer": "~0.1.1", @@ -5641,7 +5679,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true, "requires": { "boolbase": "~1.0.0", "css-what": "2.1", @@ -5652,14 +5689,12 @@ "css-what": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", - "dev": true + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==" }, "dom-serializer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", - "dev": true, "requires": { "domelementtype": "^1.3.0", "entities": "^1.1.1" @@ -5669,7 +5704,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, "requires": { "dom-serializer": "0", "domelementtype": "1" @@ -5975,8 +6009,7 @@ "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "commondir": { "version": "1.0.1", @@ -6831,6 +6864,11 @@ } } }, + "deep-diff": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz", + "integrity": "sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ=" + }, "deep-equal": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", @@ -7058,6 +7096,11 @@ "path-type": "^3.0.0" } }, + "discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=" + }, "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -7140,8 +7183,7 @@ "domelementtype": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" }, "domexception": { "version": "1.0.1", @@ -7156,7 +7198,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "dev": true, "requires": { "domelementtype": "1" } @@ -7165,7 +7206,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, "requires": { "dom-serializer": "0", "domelementtype": "1" @@ -7461,8 +7501,238 @@ "entities": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, + "enzyme": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.11.0.tgz", + "integrity": "sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==", + "requires": { + "array.prototype.flat": "^1.2.3", + "cheerio": "^1.0.0-rc.3", + "enzyme-shallow-equal": "^1.0.1", + "function.prototype.name": "^1.1.2", + "has": "^1.0.3", + "html-element-map": "^1.2.0", + "is-boolean-object": "^1.0.1", + "is-callable": "^1.1.5", + "is-number-object": "^1.0.4", + "is-regex": "^1.0.5", + "is-string": "^1.0.5", + "is-subset": "^0.1.1", + "lodash.escape": "^4.0.1", + "lodash.isequal": "^4.5.0", + "object-inspect": "^1.7.0", + "object-is": "^1.0.2", + "object.assign": "^4.1.0", + "object.entries": "^1.1.1", + "object.values": "^1.1.1", + "raf": "^3.4.1", + "rst-selector-parser": "^2.2.3", + "string.prototype.trim": "^1.2.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-is": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz", + "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + } + } + }, + "enzyme-adapter-react-16": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.5.tgz", + "integrity": "sha512-33yUJGT1nHFQlbVI5qdo5Pfqvu/h4qPwi1o0a6ZZsjpiqq92a3HjynDhwd1IeED+Su60HDWV8mxJqkTnLYdGkw==", + "requires": { + "enzyme-adapter-utils": "^1.13.1", + "enzyme-shallow-equal": "^1.0.4", + "has": "^1.0.3", + "object.assign": "^4.1.0", + "object.values": "^1.1.1", + "prop-types": "^15.7.2", + "react-is": "^16.13.1", + "react-test-renderer": "^16.0.0-0", + "semver": "^5.7.0" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "enzyme-adapter-utils": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.1.tgz", + "integrity": "sha512-5A9MXXgmh/Tkvee3bL/9RCAAgleHqFnsurTYCbymecO4ohvtNO5zqIhHxV370t7nJAwaCfkgtffarKpC0GPt0g==", + "requires": { + "airbnb-prop-types": "^2.16.0", + "function.prototype.name": "^1.1.2", + "object.assign": "^4.1.0", + "object.fromentries": "^2.0.2", + "prop-types": "^15.7.2", + "semver": "^5.7.1" + }, + "dependencies": { + "airbnb-prop-types": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz", + "integrity": "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==", + "requires": { + "array.prototype.find": "^2.1.1", + "function.prototype.name": "^1.1.2", + "is-regex": "^1.1.0", + "object-is": "^1.1.2", + "object.assign": "^4.1.0", + "object.entries": "^1.1.2", + "prop-types": "^15.7.2", + "prop-types-exact": "^1.2.0", + "react-is": "^16.13.1" + } + }, + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-is": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz", + "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "object.entries": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.2.tgz", + "integrity": "sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "has": "^1.0.3" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "enzyme-shallow-equal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz", + "integrity": "sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q==", + "requires": { + "has": "^1.0.3", + "object-is": "^1.1.2" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-is": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz", + "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + } + } }, "errno": { "version": "0.1.7", @@ -10287,6 +10557,21 @@ "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", "dev": true }, + "html-element-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.2.0.tgz", + "integrity": "sha512-0uXq8HsuG1v2TmQ8QkIhzbrqeskE4kn52Q18QJ9iAA/SnHoEKXWiUxHQtclRsCFWEUD2So34X+0+pZZu862nnw==", + "requires": { + "array-filter": "^1.0.0" + }, + "dependencies": { + "array-filter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", + "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=" + } + } + }, "html-encoding-sniffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", @@ -10388,7 +10673,6 @@ "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", - "dev": true, "requires": { "domelementtype": "^1.3.1", "domhandler": "^2.3.0", @@ -11181,6 +11465,11 @@ "binary-extensions": "^1.0.0" } }, + "is-boolean-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.1.tgz", + "integrity": "sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==" + }, "is-buffer": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", @@ -11373,6 +11662,11 @@ "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", "dev": true }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=" + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -11399,6 +11693,11 @@ } } }, + "is-number-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", + "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==" + }, "is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", @@ -11497,8 +11796,12 @@ "is-string": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", - "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", - "dev": true + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==" + }, + "is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=" }, "is-svg": { "version": "3.0.0", @@ -12424,8 +12727,7 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash.camelcase": { "version": "4.3.0", @@ -12449,11 +12751,26 @@ "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", "dev": true }, + "lodash.escape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", + "integrity": "sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=" + }, "lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -13160,6 +13477,11 @@ "minimist": "0.0.8" } }, + "moo": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", + "integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -13243,6 +13565,18 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "nearley": { + "version": "2.19.7", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.7.tgz", + "integrity": "sha512-Y+KNwhBPcSJKeyQCFjn8B/MIe+DDlhaaDgjVldhy5xtFewIbiQgcbZV8k2gCVwkI1ZsKCnjIYZbR+0Fim5QYgg==", + "requires": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6", + "semver": "^5.4.1" + } + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -13639,7 +13973,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dev": true, "requires": { "boolbase": "~1.0.0" } @@ -13822,7 +14155,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.2.tgz", "integrity": "sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==", - "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.0-next.1", @@ -13834,7 +14166,6 @@ "version": "1.17.4", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", - "dev": true, "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", @@ -13852,14 +14183,12 @@ "is-callable": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" }, "is-regex": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, "requires": { "has": "^1.0.3" } @@ -13868,7 +14197,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, "requires": { "define-properties": "^1.1.3", "function-bind": "^1.1.1" @@ -13878,7 +14206,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, "requires": { "define-properties": "^1.1.3", "function-bind": "^1.1.1" @@ -13909,7 +14236,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", - "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.0-next.1", @@ -13921,7 +14247,6 @@ "version": "1.17.4", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", - "dev": true, "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", @@ -13939,14 +14264,12 @@ "is-callable": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" }, "is-regex": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, "requires": { "has": "^1.0.3" } @@ -13955,7 +14278,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, "requires": { "define-properties": "^1.1.3", "function-bind": "^1.1.1" @@ -13965,7 +14287,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, "requires": { "define-properties": "^1.1.3", "function-bind": "^1.1.1" @@ -14374,7 +14695,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", - "dev": true, "requires": { "@types/node": "*" } @@ -14471,8 +14791,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pify": { "version": "3.0.0", @@ -15507,6 +15826,28 @@ "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", "dev": true }, + "raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "requires": { + "performance-now": "^2.1.0" + } + }, + "railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=" + }, + "randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "requires": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + } + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -15782,17 +16123,67 @@ } }, "react-router-dom": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz", - "integrity": "sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", + "integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", "requires": { "@babel/runtime": "^7.1.2", "history": "^4.9.0", "loose-envify": "^1.3.1", "prop-types": "^15.6.2", - "react-router": "5.1.2", + "react-router": "5.2.0", "tiny-invariant": "^1.0.2", "tiny-warning": "^1.0.0" + }, + "dependencies": { + "mini-create-react-context": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz", + "integrity": "sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA==", + "requires": { + "@babel/runtime": "^7.5.5", + "tiny-warning": "^1.0.3" + } + }, + "react-router": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", + "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.4.0", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + } + } + } + }, + "react-test-renderer": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.13.1.tgz", + "integrity": "sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ==", + "requires": { + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "react-is": "^16.8.6", + "scheduler": "^0.19.1" + }, + "dependencies": { + "scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + } } }, "react-transition-group": { @@ -15837,7 +16228,6 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -15930,6 +16320,40 @@ "symbol-observable": "^1.2.0" } }, + "redux-devtools-extension": { + "version": "2.13.8", + "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz", + "integrity": "sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==" + }, + "redux-logger": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz", + "integrity": "sha1-91VZZvMJjzyIYExEnPC69XeCdL8=", + "requires": { + "deep-diff": "^0.3.5" + } + }, + "redux-mock-store": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz", + "integrity": "sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==", + "requires": { + "lodash.isplainobject": "^4.0.6" + } + }, + "redux-saga": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.3.tgz", + "integrity": "sha512-RkSn/z0mwaSa5/xH/hQLo8gNf4tlvT18qXDNvedihLcfzh+jMchDgaariQoehCpgRltEm4zHKJyINEz6aqswTw==", + "requires": { + "@redux-saga/core": "^1.1.3" + } + }, + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, "reflect.ownkeys": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", @@ -16270,6 +16694,11 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, + "reselect": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" + }, "resolve": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.13.1.tgz", @@ -16424,8 +16853,7 @@ "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" }, "retry": { "version": "0.12.0", @@ -16488,6 +16916,15 @@ "inherits": "^2.0.1" } }, + "rst-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", + "integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=", + "requires": { + "lodash.flattendeep": "^4.4.0", + "nearley": "^2.7.10" + } + }, "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -16568,8 +17005,7 @@ "safe-buffer": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", - "dev": true + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" }, "safe-regex": { "version": "1.1.0", @@ -17045,8 +17481,7 @@ "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "semver-compare": { "version": "1.0.0", @@ -18072,6 +18507,96 @@ } } }, + "string.prototype.trim": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.2.tgz", + "integrity": "sha512-b5yrbl3BXIjHau9Prk7U0RRYcUYdN4wGSVaqoBQS50CCE3KBuYU0TYRNPFCP7aVoNMX87HKThdMRVIP3giclKg==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.0.tgz", + "integrity": "sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" + } + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + } + } + }, "string.prototype.trimleft": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", @@ -18092,11 +18617,52 @@ "function-bind": "^1.1.1" } }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + } + } + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "requires": { "safe-buffer": "~5.2.0" } @@ -18996,6 +19562,27 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typescript-compare": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", + "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", + "requires": { + "typescript-logic": "^0.0.0" + } + }, + "typescript-logic": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", + "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" + }, + "typescript-tuple": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", + "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", + "requires": { + "typescript-compare": "^0.0.2" + } + }, "uglify-js": { "version": "3.4.10", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", @@ -19300,8 +19887,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.0.0", diff --git a/package.json b/package.json index 8d628381..d8a54600 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "@edx/frontend-template-application", + "name": "@edx/frontend-app-logistration", "version": "0.1.0", "description": "Frontend application template", "repository": { "type": "git", - "url": "git+https://github.com/edx/frontend-template-application.git" + "url": "git+https://github.com/edx/frontend-app-logistration.git" }, "browserslist": [ "last 2 versions", @@ -26,12 +26,12 @@ }, "author": "edX", "license": "AGPL-3.0", - "homepage": "https://github.com/edx/frontend-template-application#readme", + "homepage": "https://github.com/edx/frontend-app-logistration#readme", "publishConfig": { "access": "public" }, "bugs": { - "url": "https://github.com/edx/frontend-template-application/issues" + "url": "https://github.com/edx/frontend-app-logistration/issues" }, "dependencies": { "@edx/frontend-component-footer": "10.0.11", @@ -44,13 +44,22 @@ "@fortawesome/free-solid-svg-icons": "5.11.2", "@fortawesome/react-fontawesome": "0.1.11", "babel-polyfill": "6.26.0", + "enzyme": "^3.11.0", + "enzyme-adapter-react-16": "^1.15.5", "prop-types": "15.7.2", "react": "16.12.0", "react-dom": "16.12.0", "react-redux": "7.1.3", "react-router": "5.1.2", - "react-router-dom": "5.1.2", - "redux": "4.0.5" + "react-router-dom": "^5.2.0", + "react-test-renderer": "^16.13.1", + "redux": "4.0.5", + "redux-devtools-extension": "^2.13.8", + "redux-logger": "^3.0.6", + "redux-mock-store": "^1.5.4", + "redux-saga": "^1.1.3", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0" }, "devDependencies": { "@edx/frontend-build": "3.0.0", diff --git a/src/data/configureStore.js b/src/data/configureStore.js new file mode 100644 index 00000000..16a19631 --- /dev/null +++ b/src/data/configureStore.js @@ -0,0 +1,33 @@ +import { getConfig } from '@edx/frontend-platform'; +import { applyMiddleware, createStore, compose } from 'redux'; +import thunkMiddleware from 'redux-thunk'; +import { composeWithDevTools } from 'redux-devtools-extension'; +import { createLogger } from 'redux-logger'; +import createSagaMiddleware from 'redux-saga'; + +import createRootReducer from './reducers'; +import rootSaga from './sagas'; + +const sagaMiddleware = createSagaMiddleware(); + +function composeMiddleware() { + if (getConfig().ENVIRONMENT === 'development') { + const loggerMiddleware = createLogger({ + collapsed: true, + }); + return composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, loggerMiddleware)); + } + + return compose(applyMiddleware(thunkMiddleware, sagaMiddleware)); +} + +export default function configureStore(initialState = {}) { + const store = createStore( + createRootReducer(), + initialState, + composeMiddleware(), + ); + sagaMiddleware.run(rootSaga); + + return store; +} diff --git a/src/data/reducers.js b/src/data/reducers.js new file mode 100755 index 00000000..f170b8fc --- /dev/null +++ b/src/data/reducers.js @@ -0,0 +1,16 @@ +import { combineReducers } from 'redux'; + +import { + reducer as logistrationReducer, + storeName as logistrationStoreName, +} from '../logistration'; +import { + reducer as forgotPasswordReducer, + storeName as forgotPasswordStoreName, +} from '../forgot-password'; + +const createRootReducer = () => combineReducers({ + [logistrationStoreName]: logistrationReducer, + [forgotPasswordStoreName]: forgotPasswordReducer, +}); +export default createRootReducer; diff --git a/src/data/sagas.js b/src/data/sagas.js new file mode 100644 index 00000000..3e1332b6 --- /dev/null +++ b/src/data/sagas.js @@ -0,0 +1,11 @@ +import { all } from 'redux-saga/effects'; + +import { saga as registrationSaga } from '../logistration'; +import { saga as forgotPasswordSaga } from '../forgot-password'; + +export default function* rootSaga() { + yield all([ + registrationSaga(), + forgotPasswordSaga(), + ]); +} diff --git a/src/data/utils/__snapshots__/reduxUtils.test.js.snap b/src/data/utils/__snapshots__/reduxUtils.test.js.snap new file mode 100644 index 00000000..5571ec8f --- /dev/null +++ b/src/data/utils/__snapshots__/reduxUtils.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getModuleState should throw an exception on a bad path 1`] = `"Unexpected state key uhoh given to getModuleState. Is your state path set up correctly?"`; diff --git a/src/data/utils/dataUtils.js b/src/data/utils/dataUtils.js new file mode 100644 index 00000000..cb1255e2 --- /dev/null +++ b/src/data/utils/dataUtils.js @@ -0,0 +1,38 @@ +import camelCase from 'lodash.camelcase'; +import snakeCase from 'lodash.snakecase'; + +export function modifyObjectKeys(object, modify) { + // If the passed in object is not an object, return it. + if ( + object === undefined || + object === null || + (typeof object !== 'object' && !Array.isArray(object)) + ) { + return object; + } + + if (Array.isArray(object)) { + return object.map(value => modifyObjectKeys(value, modify)); + } + + // Otherwise, process all its keys. + const result = {}; + Object.entries(object).forEach(([key, value]) => { + result[modify(key)] = modifyObjectKeys(value, modify); + }); + return result; +} + +export function camelCaseObject(object) { + return modifyObjectKeys(object, camelCase); +} + +export function snakeCaseObject(object) { + return modifyObjectKeys(object, snakeCase); +} + +export function convertKeyNames(object, nameMap) { + const transformer = key => (nameMap[key] === undefined ? key : nameMap[key]); + + return modifyObjectKeys(object, transformer); +} diff --git a/src/data/utils/dataUtils.test.js b/src/data/utils/dataUtils.test.js new file mode 100644 index 00000000..dee7558d --- /dev/null +++ b/src/data/utils/dataUtils.test.js @@ -0,0 +1,90 @@ +import { + modifyObjectKeys, + camelCaseObject, + snakeCaseObject, + convertKeyNames, +} from './dataUtils'; + +describe('modifyObjectKeys', () => { + it('should use the provided modify function to change all keys in and object and its children', () => { + function meowKeys(key) { + return `${key}Meow`; + } + + const result = modifyObjectKeys( + { + one: undefined, + two: null, + three: '', + four: 0, + five: NaN, + six: [1, 2, { seven: 'woof' }], + eight: { nine: { ten: 'bark' }, eleven: true }, + }, + meowKeys, + ); + + expect(result).toEqual({ + oneMeow: undefined, + twoMeow: null, + threeMeow: '', + fourMeow: 0, + fiveMeow: NaN, + sixMeow: [1, 2, { sevenMeow: 'woof' }], + eightMeow: { nineMeow: { tenMeow: 'bark' }, elevenMeow: true }, + }); + }); +}); + +describe('camelCaseObject', () => { + it('should make everything camelCase', () => { + const result = camelCaseObject({ + what_now: 'brown cow', + but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } }, + 'dot.dot.dot': 123, + }); + + expect(result).toEqual({ + whatNow: 'brown cow', + butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } }, + dotDotDot: 123, + }); + }); +}); + +describe('snakeCaseObject', () => { + it('should make everything snake_case', () => { + const result = snakeCaseObject({ + whatNow: 'brown cow', + butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } }, + 'dot.dot.dot': 123, + }); + + expect(result).toEqual({ + what_now: 'brown cow', + but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } }, + dot_dot_dot: 123, + }); + }); +}); + +describe('convertKeyNames', () => { + it('should replace the specified keynames', () => { + const result = convertKeyNames( + { + one: { two: { three: 'four' } }, + five: 'six', + }, + { + two: 'blue', + five: 'alive', + seven: 'heaven', + }, + ); + + expect(result).toEqual({ + one: { blue: { three: 'four' } }, + alive: 'six', + }); + }); +}); diff --git a/src/data/utils/index.js b/src/data/utils/index.js new file mode 100644 index 00000000..e8c75a28 --- /dev/null +++ b/src/data/utils/index.js @@ -0,0 +1,12 @@ +export { + camelCaseObject, + convertKeyNames, + modifyObjectKeys, + snakeCaseObject, +} from './dataUtils'; +export { + AsyncActionType, + getModuleState, +} from './reduxUtils'; +export { default as handleFailure } from './sagaUtils'; +export { unpackFieldErrors, handleRequestError } from './serviceUtils'; diff --git a/src/data/utils/reduxUtils.js b/src/data/utils/reduxUtils.js new file mode 100644 index 00000000..7cab9e91 --- /dev/null +++ b/src/data/utils/reduxUtils.js @@ -0,0 +1,66 @@ +/** + * Helper class to save time when writing out action types for asynchronous methods. Also helps + * ensure that actions are namespaced. + */ +export class AsyncActionType { + constructor(topic, name) { + this.topic = topic; + this.name = name; + } + + get BASE() { + return `${this.topic}__${this.name}`; + } + + get BEGIN() { + return `${this.topic}__${this.name}__BEGIN`; + } + + get SUCCESS() { + return `${this.topic}__${this.name}__SUCCESS`; + } + + get FAILURE() { + return `${this.topic}__${this.name}__FAILURE`; + } + + get RESET() { + return `${this.topic}__${this.name}__RESET`; + } + + get FORBIDDEN() { + return `${this.topic}__${this.name}__FORBIDDEN`; + } +} + +/** + * Given a state tree and an array representing a set of keys to traverse in that tree, returns + * the portion of the tree at that key path. + * + * Example: + * + * const result = getModuleState( + * { + * first: { red: { awesome: 'sauce' }, blue: { weak: 'sauce' } }, + * second: { other: 'data', } + * }, + * ['first', 'red'] + * ); + * + * result will be: + * + * { + * awesome: 'sauce' + * } + */ +export function getModuleState(state, originalPath) { + const path = [...originalPath]; // don't modify your argument + if (path.length < 1) { + return state; + } + const key = path.shift(); + if (state[key] === undefined) { + throw new Error(`Unexpected state key ${key} given to getModuleState. Is your state path set up correctly?`); + } + return getModuleState(state[key], path); +} diff --git a/src/data/utils/reduxUtils.test.js b/src/data/utils/reduxUtils.test.js new file mode 100644 index 00000000..e07adf45 --- /dev/null +++ b/src/data/utils/reduxUtils.test.js @@ -0,0 +1,52 @@ +import { + AsyncActionType, + getModuleState, +} from './reduxUtils'; + +describe('AsyncActionType', () => { + it('should return well formatted action strings', () => { + const actionType = new AsyncActionType('HOUSE_CATS', 'START_THE_RACE'); + + expect(actionType.BASE).toBe('HOUSE_CATS__START_THE_RACE'); + expect(actionType.BEGIN).toBe('HOUSE_CATS__START_THE_RACE__BEGIN'); + expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS'); + expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE'); + expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET'); + expect(actionType.FORBIDDEN).toBe('HOUSE_CATS__START_THE_RACE__FORBIDDEN'); + }); +}); + +describe('getModuleState', () => { + const state = { + first: { red: { awesome: 'sauce' }, blue: { weak: 'sauce' } }, + second: { other: 'data' }, + }; + + it('should return everything if given an empty path', () => { + expect(getModuleState(state, [])).toEqual(state); + }); + + it('should resolve paths correctly', () => { + expect(getModuleState( + state, + ['first'], + )).toEqual({ red: { awesome: 'sauce' }, blue: { weak: 'sauce' } }); + + expect(getModuleState( + state, + ['first', 'red'], + )).toEqual({ awesome: 'sauce' }); + + expect(getModuleState(state, ['second'])).toEqual({ other: 'data' }); + }); + + it('should throw an exception on a bad path', () => { + expect(() => { + getModuleState(state, ['uhoh']); + }).toThrowErrorMatchingSnapshot(); + }); + + it('should return non-objects correctly', () => { + expect(getModuleState(state, ['first', 'red', 'awesome'])).toEqual('sauce'); + }); +}); diff --git a/src/data/utils/sagaUtils.js b/src/data/utils/sagaUtils.js new file mode 100644 index 00000000..0ce3ecd0 --- /dev/null +++ b/src/data/utils/sagaUtils.js @@ -0,0 +1,16 @@ +import { put } from 'redux-saga/effects'; +import { logError } from '@edx/frontend-platform/logging'; +import { history } from '@edx/frontend-platform'; + +export default function* handleFailure(error, failureAction = null, failureRedirectPath = null) { + if (error.fieldErrors && failureAction !== null) { + yield put(failureAction({ fieldErrors: error.fieldErrors })); + } + logError(error); + if (failureAction !== null) { + yield put(failureAction(error.message)); + } + if (failureRedirectPath !== null) { + history.push(failureRedirectPath); + } +} diff --git a/src/data/utils/serviceUtils.js b/src/data/utils/serviceUtils.js new file mode 100644 index 00000000..ea22c04c --- /dev/null +++ b/src/data/utils/serviceUtils.js @@ -0,0 +1,48 @@ +/** + * Turns field errors of the form: + * + * { + * "name":{ + * "developer_message": "Nerdy message here", + * "user_message": "This value is invalid." + * }, + * "other_field": { + * "developer_message": "Other Nerdy message here", + * "user_message": "This other value is invalid." + * } + * } + * + * Into: + * + * { + * "name": "This value is invalid.", + * "other_field": "This other value is invalid" + * } + */ +export function unpackFieldErrors(fieldErrors) { + return Object.entries(fieldErrors).reduce((acc, [k, v]) => { + acc[k] = v.user_message; + return acc; + }, {}); +} + +/** + * Processes and re-throws request errors. If the response contains a field_errors field, will + * massage the data into a form expected by the client. + * + * Field errors will be packaged as an api error with a fieldErrors field usable by the client. + * Takes an optional unpack function which is used to process the field errors, + * otherwise uses the default unpackFieldErrors function. + * + * @param error The original error object. + * @param unpackFunction (Optional) A function to use to unpack the field errors as a replacement + * for the default. + */ +export function handleRequestError(error, unpackFunction = unpackFieldErrors) { + if (error.response && error.response.data.field_errors) { + const apiError = Object.create(error); + apiError.fieldErrors = unpackFunction(error.response.data.field_errors); + throw apiError; + } + throw error; +} diff --git a/src/example/ExamplePage.jsx b/src/example/ExamplePage.jsx deleted file mode 100644 index ec21e1b0..00000000 --- a/src/example/ExamplePage.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; - -export default function ExamplePage() { - return ( -
-
-

Example Page

-

Hello world!

-
-
- ); -} diff --git a/src/example/ExamplePage.test.jsx b/src/example/ExamplePage.test.jsx deleted file mode 100644 index 3bad6f31..00000000 --- a/src/example/ExamplePage.test.jsx +++ /dev/null @@ -1,5 +0,0 @@ -describe('example', () => { - it('will pass because it is an example', () => { - - }); -}); diff --git a/src/example/data/.gitkeep b/src/example/data/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/example/data/README.rst b/src/example/data/README.rst deleted file mode 100644 index c4db0b68..00000000 --- a/src/example/data/README.rst +++ /dev/null @@ -1,4 +0,0 @@ -data folder -=========== - -This folder is the home for non-component files, such as redux reducers, actions, selectors, API client services, etc. See `Feature-based Application Organization `_. for more detail. \ No newline at end of file diff --git a/src/example/index.scss b/src/example/index.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/src/forgot-password/ForgotPasswordPage.jsx b/src/forgot-password/ForgotPasswordPage.jsx new file mode 100644 index 00000000..2c314661 --- /dev/null +++ b/src/forgot-password/ForgotPasswordPage.jsx @@ -0,0 +1,106 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { Redirect } from 'react-router-dom'; +import { Button, Input, ValidationFormGroup } from '@edx/paragon'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; + +import messages from './messages'; +import { forgotPassword } from './data/actions'; +import { forgotPasswordResultSelector } from './data/selectors'; +import RequestInProgressAlert from './RequestInProgressAlert'; +import LoginHelpLinks from '../logistration/LoginHelpLinks'; + +const ForgotPasswordPage = (props) => { + const { intl, forgotPassword, status } = props; + const [ emailInput, setEmailValue ] = useState(''); + const [ emailValid, setEmailValidValue ] = useState(true); + + const handleOnChange = (e) => { + const emailValue = e.target.value; + setEmailValue(emailValue) + validateEmail(emailValue); + } + + const validateEmail = (email) => { + const isEmailValid = email.match(/^([\w.%+-]+)@([\w-]+\.)+([\w]{2,})$/i); + setEmailValidValue(isEmailValid !== null); + } + + const handleSubmit = (e) => { + e.preventDefault(); + if (emailInput === '') { + setEmailValidValue(false); + } + if (emailValid && emailInput !== '') { + forgotPassword(emailInput); + } + } + return ( + + {status === 'complete' ? : null} +
+
+
+
+ {status === 'forbidden' ? : null} +

+ {intl.formatMessage(messages['logisration.forgot.password.page.heading'])} +

+

+ {intl.formatMessage(messages['logisration.forgot.password.page.instructions'])} +

+
+ + + handleOnChange(e)} + style={{ width: '400px' }} + /> + +
+

+ {intl.formatMessage(messages['logisration.forgot.password.page.email.field.help.text'])} +

+ +
+ +
+
+
+
+ ); +} + +ForgotPasswordPage.propTypes = { + intl: intlShape.isRequired, + forgotPassword: PropTypes.func.isRequired, + status: PropTypes.string, +} + + + +export default connect( + forgotPasswordResultSelector, + { + forgotPassword + }, +)(injectIntl(ForgotPasswordPage)); diff --git a/src/forgot-password/RequestInProgressAlert.jsx b/src/forgot-password/RequestInProgressAlert.jsx new file mode 100644 index 00000000..e490bef9 --- /dev/null +++ b/src/forgot-password/RequestInProgressAlert.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; + +import Alert from '../logistration/Alert'; + +const RequestInProgressAlert = (props) => { + + return ( + } + > + + + ); +}; + +export default RequestInProgressAlert; diff --git a/src/forgot-password/data/actions.js b/src/forgot-password/data/actions.js new file mode 100644 index 00000000..0a123eb5 --- /dev/null +++ b/src/forgot-password/data/actions.js @@ -0,0 +1,22 @@ +import { AsyncActionType } from '../../data/utils'; + +export const FORGOT_PASSWORD = new AsyncActionType('FORGOT', 'PASSWORD'); + +// Forgot Password +export const forgotPassword = email => ({ + type: FORGOT_PASSWORD.BASE, + payload: { email }, +}); + +export const forgotPasswordBegin = () => ({ + type: FORGOT_PASSWORD.BEGIN, +}); + +export const forgotPasswordSuccess = email => ({ + type: FORGOT_PASSWORD.SUCCESS, + payload: { email }, +}); + +export const forgotPasswordForbidden = () => ({ + type: FORGOT_PASSWORD.FORBIDDEN, +}); diff --git a/src/forgot-password/data/reducers.js b/src/forgot-password/data/reducers.js new file mode 100644 index 00000000..57c96224 --- /dev/null +++ b/src/forgot-password/data/reducers.js @@ -0,0 +1,33 @@ +import { FORGOT_PASSWORD } from './actions'; + +export const defaultState = { + status: null, +}; + +const reducer = (state = defaultState, action = null) => { + if (action !== null) { + switch (action.type) { + case FORGOT_PASSWORD.BEGIN: + return { + ...state, + status: 'pending', + }; + case FORGOT_PASSWORD.SUCCESS: + return { + ...state, + ...action.payload, + status: 'complete', + }; + case FORGOT_PASSWORD.FORBIDDEN: + return { + ...state, + status: 'forbidden', + }; + default: + return state; + } + } + return state; +}; + +export default reducer; diff --git a/src/forgot-password/data/sagas.js b/src/forgot-password/data/sagas.js new file mode 100644 index 00000000..e4c07919 --- /dev/null +++ b/src/forgot-password/data/sagas.js @@ -0,0 +1,33 @@ +import { call, put, takeEvery } from 'redux-saga/effects'; + +// Actions +import { + FORGOT_PASSWORD, + forgotPasswordBegin, + forgotPasswordSuccess, + forgotPasswordForbidden, +} from './actions'; + +import { forgotPassword } from './service'; + + +// Services +export function* handleForgotPassword(action) { + try { + yield put(forgotPasswordBegin()); + + yield call(forgotPassword, action.payload.email); + + yield put(forgotPasswordSuccess(action.payload.email)); + } catch (e) { + if (e.response && e.response.status === 403) { + yield put(forgotPasswordForbidden()); + } else { + throw e; + } + } +} + +export default function* saga() { + yield takeEvery(FORGOT_PASSWORD.BASE, handleForgotPassword); +} diff --git a/src/forgot-password/data/selectors.js b/src/forgot-password/data/selectors.js new file mode 100644 index 00000000..dbb3f10e --- /dev/null +++ b/src/forgot-password/data/selectors.js @@ -0,0 +1,10 @@ +import { createSelector } from 'reselect'; + +export const storeName = 'forgotPassword'; + +export const forgotPasswordSelector = state => ({ ...state[storeName] }); + +export const forgotPasswordResultSelector = createSelector( + forgotPasswordSelector, + forgotPassword => forgotPassword, +); diff --git a/src/forgot-password/data/service.js b/src/forgot-password/data/service.js new file mode 100644 index 00000000..25020c56 --- /dev/null +++ b/src/forgot-password/data/service.js @@ -0,0 +1,23 @@ +import { getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import formurlencoded from 'form-urlencoded'; + +// eslint-disable-next-line import/prefer-default-export +export async function forgotPassword(email) { + const requestConfig = { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + isPublic: true, + }; + + const { data } = await getAuthenticatedHttpClient() + .post( + `${getConfig().LMS_BASE_URL}/account/password`, + formurlencoded({ email }), + requestConfig, + ) + .catch((e) => { + throw (e); + }); + + return data; +} diff --git a/src/forgot-password/index.js b/src/forgot-password/index.js new file mode 100644 index 00000000..2d30ff06 --- /dev/null +++ b/src/forgot-password/index.js @@ -0,0 +1,5 @@ +export { default } from './ForgotPasswordPage'; +export { default as reducer } from './data/reducers'; +export { FORGOT_PASSWORD } from './data/actions'; +export { default as saga } from './data/sagas'; +export { storeName, forgotPasswordResultSelector } from './data/selectors'; diff --git a/src/forgot-password/messages.js b/src/forgot-password/messages.js new file mode 100644 index 00000000..41464ae6 --- /dev/null +++ b/src/forgot-password/messages.js @@ -0,0 +1,36 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + 'logisration.forgot.password.page.heading': { + id: 'logisration.forgot.password.page.heading', + defaultMessage: 'Password assistance', + description: 'The page heading for the forgot password page.', + }, + 'logisration.forgot.password.page.instructions': { + id: 'logisration.forgot.password.page.instructions', + defaultMessage: 'Please enter your log-in or recovery email address below and we will send you an email with instructions.', + description: 'Instructions message for forgot password page.', + }, + 'logisration.forgot.password.page.invalid.email.message': { + id: 'logisration.forgot.password.page.invalid.email.message', + defaultMessage: "The email address you've provided isn't formatted correctly.", + description: 'Invalid email address message for the forgot password page.', + }, + 'logisration.forgot.password.page.email.field.label': { + id: 'logisration.forgot.password.page.email.field.label', + defaultMessage: 'Email', + description: 'Email field label for the forgot password page.', + }, + 'logisration.forgot.password.page.email.field.help.text': { + id: 'logisration.forgot.password.page.email.field.help.text', + defaultMessage: 'The email address you used to register with edX.', + description: 'Email field help text for the forgot password page.', + }, + 'logisration.forgot.password.page.submit.button': { + id: 'logisration.forgot.password.page.submit.button', + defaultMessage: 'Recover my password', + description: 'Submit button text for the forgot password page.', + }, +}); + +export default messages; diff --git a/src/forgot-password/tests/ForgotPasswordPage.test.jsx b/src/forgot-password/tests/ForgotPasswordPage.test.jsx new file mode 100644 index 00000000..6dec4882 --- /dev/null +++ b/src/forgot-password/tests/ForgotPasswordPage.test.jsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { Router } from 'react-router-dom'; +import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import configureStore from 'redux-mock-store'; +import { createMemoryHistory } from 'history'; +import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; + +import ForgotPasswordPage from '../ForgotPasswordPage'; // eslint-disable-line import/first + +jest.mock('../data/selectors', () => { + return jest.fn().mockImplementation(() => ({ forgotPasswordSelector: () => ({}) })); +}); + +const IntlForgotPasswordPage = injectIntl(ForgotPasswordPage); +const mockStore = configureStore(); +const history = createMemoryHistory(); + +describe('ForgotPasswordPage', () => { + let props = {}; + let store = {}; + + const reduxWrapper = children => ( + + {children} + + ); + + beforeEach(() => { + store = mockStore(); + props = { + forgotPassword: jest.fn(), + status: null, + }; + }); + + it('should match default section snapshot', () => { + const tree = renderer.create(reduxWrapper()) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('should match forbidden section snapshot', () => { + props = { + ...props, + status: 'forbidden', + } + const tree = renderer.create(reduxWrapper()) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('should match success section snapshot', () => { + props = { + ...props, + status: 'complete', + } + const tree = renderer.create( + reduxWrapper( + + + + ) + ); + expect(history.location.pathname).toEqual('/login'); + }); + + it('should display need other help signing in button', () => { + const wrapper = mount(reduxWrapper()); + expect(wrapper.find('button.field-link').text()).toEqual('Need other help signing in?'); + }); +}); diff --git a/src/forgot-password/tests/__snapshots__/ForgotPasswordPage.test.jsx.snap b/src/forgot-password/tests/__snapshots__/ForgotPasswordPage.test.jsx.snap new file mode 100644 index 00000000..7be9752b --- /dev/null +++ b/src/forgot-password/tests/__snapshots__/ForgotPasswordPage.test.jsx.snap @@ -0,0 +1,279 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ForgotPasswordPage should match default section snapshot 1`] = ` +
+
+
+
+

+ Password assistance +

+

+ Please enter your log-in or recovery email address below and we will send you an email with instructions. +

+
+
+ + + + The email address you've provided isn't formatted correctly. + +
+
+

+ The email address you used to register with edX. +

+ +
+
+
+
+ + +
+
+`; + +exports[`ForgotPasswordPage should match forbidden section snapshot 1`] = ` +
+
+
+
+
+
+ +
+
+ + Your previous request is still in progress, please try again in a few moments. + +
+
+

+ Password assistance +

+

+ Please enter your log-in or recovery email address below and we will send you an email with instructions. +

+
+
+ + + + The email address you've provided isn't formatted correctly. + +
+
+

+ The email address you used to register with edX. +

+ +
+
+
+
+ + +
+
+`; diff --git a/src/index.jsx b/src/index.jsx index c837c753..f2958020 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -6,22 +6,41 @@ import { import { AppProvider, ErrorPage } from '@edx/frontend-platform/react'; import React from 'react'; import ReactDOM from 'react-dom'; +import { Route, Switch } from 'react-router-dom'; import Header, { messages as headerMessages } from '@edx/frontend-component-header'; import Footer, { messages as footerMessages } from '@edx/frontend-component-footer'; +import configureStore from './data/configureStore'; +import { LoginPage, RegistrationPage, NotFoundPage } from './logistration'; +import ForgotPasswordPage from './forgot-password'; import appMessages from './i18n'; -import ExamplePage from './example/ExamplePage'; import './index.scss'; import './assets/favicon.ico'; +const HeaderFooterLayout = ({ children }) => ( +
+
+
+ {children} +
+
+
+); + subscribe(APP_READY, () => { ReactDOM.render( - -
- -