diff --git a/.gitmodules b/.gitmodules index ea1b2ddf..01b4e6d7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "mvis/ivis-core"] path = mvis/ivis-core - url = git@gitlab.d3s.mff.cuni.cz:evif/ivis-core.git + url = https://gitlab.d3s.mff.cuni.cz/evif/ivis-core.git diff --git a/README.md b/README.md index 0cc70fac..f338782b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Mailtrain v2 (beta) -[Mailtrain](http://mailtrain.org) is a self hosted newsletter application built on Node.js (v10+) and MySQL (v8+) or MariaDB (v10+). +Mailtrain is a self hosted newsletter application built on Node.js (v10+) and MySQL (v8+) or MariaDB (v10+). ![](https://mailtrain.org/mailtrain.png) @@ -176,18 +176,15 @@ To deploy Mailtrain with Docker, you need the following three dependencies insta - [Docker](https://www.docker.com/) - [Docker Compose](https://docs.docker.com/compose/) -- Git - Typically already present. If not, just use the package manager of your OS distribution to install it. These are the steps to start Mailtrain via docker-compose: -1. Download Mailtrain using git +1. Download Mailtrain's docker-compose build file ``` - git clone https://github.com/Mailtrain-org/mailtrain.git - cd mailtrain - git checkout development + curl -O https://raw.githubusercontent.com/Mailtrain-org/mailtrain/development/docker-compose.yml ``` -2. Deploy Mailtrain via docker-compose (in the root directory of the Mailtrain project). This will take quite some time when run for the first time. Subsequent executions will be fast. +2. Deploy Mailtrain via docker-compose (in the directory to which you downloaded the `docker-compose.yml` file). This will take quite some time when run for the first time. Subsequent executions will be fast. ``` docker-compose up ``` @@ -201,6 +198,8 @@ These are the steps to start Mailtrain via docker-compose: 4. Authenticate as `admin`:`test` +The instructions above use an automatically built Docker image on DockerHub (https://hub.docker.com/r/mailtrain/mailtrain). If you want to build the Docker image yourself (e.g. when doing development), use the `docker-compose-local-build.yml` located in the project's root directory. + ## License diff --git a/TODO.md b/TODO.md index 1641a058..e6827c53 100644 --- a/TODO.md +++ b/TODO.md @@ -1,21 +1,21 @@ -### Front page -- Some dashboard - -### Campaigns -- List of sent RSS campaigns (?) - -### Pull requests -- Support ldaps:// - 5325f2ea7864ce5f42a9a6df3408af7ffbd32591 -- Support https - abd788d8f4d18b5a977226ba1224cba7f2b7fa9b -- Support warn of failed login - 4bd1e994b27420ba366d9b0429e9014e5bf01f13 -- Add X-Mailer header option in settings to override or disable it - 44fe8882b876bdfd9990110496d16f819dc64ac3 -- Add custom unsubscribe option in a campaign - 68cb8384f7dfdbcaf2932293ec5a2f1ec0a1554e - -### API -- Add API extensions - -### GDPR -- Refuse editing subscriptions which have been anonymized -- Add field to subscriptions which says till when the consent has been given -- Provide a link (and merge tag) that will update the consent date to now -- Add campaign trigger that triggers if the consent for specific subscription field is about to expire (i.e. it is greater than now - seconds) +### Front page +- Some dashboard + +### Campaigns +- List of sent RSS campaigns (?) + +### Pull requests +- Support ldaps:// - 5325f2ea7864ce5f42a9a6df3408af7ffbd32591 +- Support https - abd788d8f4d18b5a977226ba1224cba7f2b7fa9b +- Support warn of failed login - 4bd1e994b27420ba366d9b0429e9014e5bf01f13 +- Add X-Mailer header option in settings to override or disable it - 44fe8882b876bdfd9990110496d16f819dc64ac3 +- Add custom unsubscribe option in a campaign - 68cb8384f7dfdbcaf2932293ec5a2f1ec0a1554e + +### API +- Add API extensions + +### GDPR +- Refuse editing subscriptions which have been anonymized +- Add field to subscriptions which says till when the consent has been given +- Provide a link (and merge tag) that will update the consent date to now +- Add campaign trigger that triggers if the consent for specific subscription field is about to expire (i.e. it is greater than now - seconds) diff --git a/UPGRADE.md b/UPGRADE.md index 2abf71ce..e98d3d28 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,21 +1,21 @@ -## Migration from Mailtrain v1 to Mailtrain v2 - -The migration should happen almost automatically. There are however the following caveats: - -1. Structure of config files (under `config`) has changed at many places. Revisit the default config (`config/default.toml`) - and update your configs accordingly. - -2. Images uploaded in a template editor (Mosaico, Grapesjs, etc.) need to be manually moved to a new destination (under `client`). - For Mosaico, this means to move folders named by a number from `public/mosaico` to `client/static/mosaico`. - -3. Directory for custom Mosaico templates has changed from `public/mosaico/templates` to `client/static/mosaico/templates`. - -4. Imports are not migrated. If you have any pending imports, complete them before migration to v2. - -5. Zone MTA configuration endpoint (webhooks/zone-mta/sender-config) has changed. The send-configuration CID has to be - part of the URL - e.g. webhooks/zone-mta/sender-config/system. - -6. If there are lists that contain birthday or date fields that were created before - commit `bc73a0df0cab9943d726bd12fc1c6f2ff1279aa7` (on Jan 3, 2018), they still have TIMESTAMP data type in DB instead - of DATETIME. The problem was that that commit did not introduce migration from TIMESTAMP to DATETIME. - Mailtrain v2 does this migration, however in some corner cases, this may shift the date by a day back or forth. +## Migration from Mailtrain v1 to Mailtrain v2 + +The migration should happen almost automatically. There are however the following caveats: + +1. Structure of config files (under `config`) has changed at many places. Revisit the default config (`config/default.toml`) + and update your configs accordingly. + +2. Images uploaded in a template editor (Mosaico, Grapesjs, etc.) need to be manually moved to a new destination (under `client`). + For Mosaico, this means to move folders named by a number from `public/mosaico` to `client/static/mosaico`. + +3. Directory for custom Mosaico templates has changed from `public/mosaico/templates` to `client/static/mosaico/templates`. + +4. Imports are not migrated. If you have any pending imports, complete them before migration to v2. + +5. Zone MTA configuration endpoint (webhooks/zone-mta/sender-config) has changed. The send-configuration CID has to be + part of the URL - e.g. webhooks/zone-mta/sender-config/system. + +6. If there are lists that contain birthday or date fields that were created before + commit `bc73a0df0cab9943d726bd12fc1c6f2ff1279aa7` (on Jan 3, 2018), they still have TIMESTAMP data type in DB instead + of DATETIME. The problem was that that commit did not introduce migration from TIMESTAMP to DATETIME. + Mailtrain v2 does this migration, however in some corner cases, this may shift the date by a day back or forth. diff --git a/client/.gitignore b/client/.gitignore index 0a524405..9b1c8b13 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -1 +1 @@ -/dist +/dist diff --git a/client/package-lock.json b/client/package-lock.json index afcc9efe..7b6681a0 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -3666,6 +3666,11 @@ "integrity": "sha512-ZUXBUyGLeoJxp4Nt6G/GjBRLnyz8IKQGexZ2ndWaoegThgMGFO1tdDYID5gBV32/1S83osjJHyfzvanE/8HY4Q==", "dev": true }, + "ellipsize": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ellipsize/-/ellipsize-0.1.0.tgz", + "integrity": "sha1-nUNoLUS5GtFuvYQmisEDFwplU/g=" + }, "elliptic": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", @@ -3997,30 +4002,6 @@ "randomatic": "^3.0.0", "repeat-element": "^1.1.2", "repeat-string": "^1.5.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "randomatic": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", - "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", - "requires": { - "is-number": "^4.0.0", - "kind-of": "^6.0.0", - "math-random": "^1.0.1" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" - } - } - } } }, "find-cache-dir": { @@ -4489,7 +4470,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4507,11 +4489,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4524,15 +4508,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4635,7 +4622,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4645,6 +4633,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4657,6 +4646,7 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4762,7 +4752,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4877,6 +4868,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4894,6 +4886,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5143,589 +5136,11 @@ } }, "grapesjs-mjml": { - "version": "0.0.27", - "resolved": "https://registry.npmjs.org/grapesjs-mjml/-/grapesjs-mjml-0.0.27.tgz", - "integrity": "sha512-P+J7IVxNrv7fn04UD6Rixu6+M0r8hZV4Z0AkCtWHLLhbwGHsgmZ9G4KlcI1K7egpH18G0019GTkw2NBqpbA3cQ==", + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/grapesjs-mjml/-/grapesjs-mjml-0.0.31.tgz", + "integrity": "sha512-HHf2LAZBAOItrSNJL3158T/YuO0TwxmWFSYvp5b60BlP1MkpF/yQs+5UptcC6hg/hZVeOx2ao95FY5OqOk7gZQ==", "requires": { "mjml": "^3.3.5" - }, - "dependencies": { - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", - "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==" - }, - "hoist-non-react-statics": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", - "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" - }, - "mjml": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml/-/mjml-3.3.5.tgz", - "integrity": "sha512-a7uMTYsXNPfwe4ncjQXobcqWsdOoKa4sk2+F59g8Q7AFZDLIFV0Ya5fcp3spBADovCLycZ8seF79K3svU/KDIg==", - "requires": { - "lodash": "^4.17.4", - "mjml-accordion": "~3.3.5", - "mjml-button": "~3.3.5", - "mjml-carousel": "~3.3.5", - "mjml-cli": "~3.3.5", - "mjml-column": "~3.3.5", - "mjml-container": "~3.3.5", - "mjml-core": "~3.3.5", - "mjml-divider": "~3.3.5", - "mjml-group": "~3.3.5", - "mjml-head-attributes": "~3.3.5", - "mjml-head-font": "~3.3.5", - "mjml-head-preview": "~3.3.5", - "mjml-head-style": "~3.3.5", - "mjml-head-title": "~3.3.5", - "mjml-hero": "~3.3.5", - "mjml-html": "~3.3.5", - "mjml-image": "~3.3.5", - "mjml-invoice": "~3.3.5", - "mjml-list": "~3.3.5", - "mjml-location": "~3.3.5", - "mjml-navbar": "~3.3.5", - "mjml-raw": "~3.3.5", - "mjml-section": "~3.3.5", - "mjml-social": "~3.3.5", - "mjml-spacer": "~3.3.5", - "mjml-table": "~3.3.5", - "mjml-text": "~3.3.5", - "mjml-wrapper": "~3.3.5" - } - }, - "mjml-accordion": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-accordion/-/mjml-accordion-3.3.5.tgz", - "integrity": "sha512-pK30iNhIvqnDnidqVKFw4GJEfKWCukYYw4hByIagwikdVKdbXHc1pf/IpFsKtJ8zx9Cdi4JeFVWYwwikU2v1Bw==", - "requires": { - "classnames": "^2.2.5", - "lodash": "^4.17.4", - "mjml-core": "~3.3.5", - "react": "^15.4.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-button": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-button/-/mjml-button-3.3.5.tgz", - "integrity": "sha512-PKvk6arRTYLY4PcpPdf9bTpFvZtw/2KIID4H0+lYLz0C8pIuHPoHh4wTaXguBGdT9gLfpMAp2WleMOUD6zmEPA==", - "requires": { - "lodash": "^4.17.4", - "mjml-core": "~3.3.5", - "react": "^15.4.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-carousel": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-carousel/-/mjml-carousel-3.3.5.tgz", - "integrity": "sha512-yGXLZBUxQLKiCxS5H5MSoAbGJcSmdcBYFHqLsukiHAHbsetpang9BbI0v3X+PrvOco6DzL2SlOG8EDXnrAuNUw==", - "requires": { - "lodash": "^4.17.4", - "mjml-core": "~3.3.5", - "react": "^15.4.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-cli": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-cli/-/mjml-cli-3.3.5.tgz", - "integrity": "sha512-LmOht5KFC/9oRngLitnGUpB2lNPUz2TGNeRIxBIJNC0FtaBpUz3g5LCofZHYvSMOEcgVqISeOHsmNIV75l3lLw==", - "requires": { - "chokidar": "^1.6.1", - "commander": "^2.9.0", - "glob": "^7.1.1", - "lodash": "^4.17.4", - "mjml-core": "~3.3.5", - "mz": "^2.6.0" - } - }, - "mjml-column": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-column/-/mjml-column-3.3.5.tgz", - "integrity": "sha512-VvU7PQ9phme1oWoKugw1UTNeqNXMqDVOkrrtMNpKiLOByuG0vMegwWGJOIsuznuy80Pl5wnVk9XuynMbOhb82A==", - "requires": { - "classnames": "^2.2.5", - "lodash": "^4.17.4", - "mjml-core": "~3.3.5", - "react": "^15.4.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-core": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-3.3.5.tgz", - "integrity": "sha512-Cqc+8HLyMe26Co1jnyKTt+m10rQnXmAjXYcrdJV8QrjpDeLIZd1dClYTBjEAtREmUf5cAovdRHulPLfiOF9sZA==", - "requires": { - "cheerio": "^0.22.0", - "classnames": "^2.2.5", - "debug": "^2.6.0", - "he": "^1.1.0", - "hoist-non-react-statics": "^1.2.0", - "html-minifier": "^3.2.3", - "immutable": "^3.8.1", - "jquery": "^3.1.1", - "js-beautify": "^1.6.8", - "juice": "^4.0.2", - "lodash": "^4.17.4", - "mjml-validator": "~3.3.3", - "react": "^15.4.2", - "react-dom": "^15.4.2", - "warning": "^3.0.0" - }, - "dependencies": { - "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" - }, - "immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" - }, - "juice": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz", - "integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==", - "requires": { - "cheerio": "^0.22.0", - "commander": "^2.15.1", - "cross-spawn": "^5.1.0", - "deep-extend": "^0.5.1", - "mensch": "^0.3.3", - "slick": "^1.12.2", - "web-resource-inliner": "^4.2.1" - } - }, - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - }, - "react-dom": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", - "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=", - "requires": { - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-divider": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-divider/-/mjml-divider-3.3.5.tgz", - "integrity": "sha512-LIEwKA2v/hxU7YKL/h/H4TRCL47167W6JQ+Ju7JZE9ropPvEHnmd5sWme9wc9iib70+6RIk25JvGjxfQZJeOOw==", - "requires": { - "lodash": "^4.17.4", - "mjml-core": "~3.3.5", - "react": "^15.4.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-group": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-group/-/mjml-group-3.3.5.tgz", - "integrity": "sha512-PNAmb+TqTkglKJFQpgaxuKSxfo7m/MCKNxgOjY6xaTswY/5BVtYO1ne2+raYBGWR2OLw3f8vNV4EZW+REy3fSQ==", - "requires": { - "classnames": "^2.2.5", - "lodash": "^4.17.4", - "mjml-core": "~3.3.5", - "react": "^15.4.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-head-attributes": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-head-attributes/-/mjml-head-attributes-3.3.5.tgz", - "integrity": "sha512-P0QzVA+xHp0Dn5DzC+sMsdu06a2q3ZqtHho6ex9cRAJ1DrgXr043XT5VddycaW0BssG8tca31nA+eELiP8GxYw==", - "requires": { - "lodash": "^4.17.4" - } - }, - "mjml-head-font": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-head-font/-/mjml-head-font-3.3.5.tgz", - "integrity": "sha512-GosELGr8UsMkzfhPU8QJLvsCjrJMyJn6eA2u7icI0e8Q5G13GxUYIdyh1ICepn9Ff/NZp7mkwXCDmelJmlhxng==", - "requires": { - "lodash": "^4.17.4" - } - }, - "mjml-head-preview": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-head-preview/-/mjml-head-preview-3.3.5.tgz", - "integrity": "sha512-hoiGShrkk2fw4lIsgoaC+L40eV6AGi2/p9C2BSAKTmeKPl70sUIk9TKpu+g3atAtV10EkHiG5XBhZCZSbZwnRA==" - }, - "mjml-head-style": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-head-style/-/mjml-head-style-3.3.5.tgz", - "integrity": "sha512-51o0rB7fCx2f58hI/FHQH1X72ahap25oXeiCtJ+X+hH4xdtUaRL5stPM5WdED2kRDCrurMdNxo4s9xN+immPeA==" - }, - "mjml-head-title": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-head-title/-/mjml-head-title-3.3.5.tgz", - "integrity": "sha512-DGcdLRKOxIGFJLESQi8ftIb6dKCIlPOPRpXVJ1FPoQ0/qPLXbXpHqJPRLIGzAD8tIPtVa/MbXk+Wm5+DP/Nf6w==" - }, - "mjml-hero": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-hero/-/mjml-hero-3.3.5.tgz", - "integrity": "sha512-Q8jM/wtpDkq+8WOK2V3W59MGAmW7kNZc7U+MTwuF/xjdJIUrYkOBgYyE/iv8dVUkOXXtI2tnouJ+nWxHfLJX3A==", - "requires": { - "classnames": "^2.2.5", - "lodash": "^4.17.4", - "mjml-core": "~3.3.5", - "react": "^15.4.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-image": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-image/-/mjml-image-3.3.5.tgz", - "integrity": "sha512-UDHyaTfwchOtZp3PN9pDJpcFSv3YbEb6U25SGT0UHoJTckKOAcKWoaPGfCvi1DvnQ9+r3xqZV0/oI1qdXxa3Pw==", - "requires": { - "lodash": "^4.17.4", - "mjml-core": "~3.3.5", - "react": "^15.4.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-navbar": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-navbar/-/mjml-navbar-3.3.5.tgz", - "integrity": "sha512-elHcplAHvgSeJYHT0qtA+mT//8RKTBXwVOqk4b+4HV7Y9084p7rKsqtHYArMO8mfbXRYqrJ5zWn9rOU5UvSGPg==", - "requires": { - "lodash": "^4.17.4", - "mjml-core": "~3.3.5", - "mjml-section": "~3.3.3", - "react": "^15.4.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-raw": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-raw/-/mjml-raw-3.3.5.tgz", - "integrity": "sha512-7gP98gI+eHkoc1Y91Uq57Ow3/GvF82tTmt3bjTq6aCtHKLPRDVGg+mF99JGdyqDSPQOY/rLh1W1XWgZ0JknNqQ==", - "requires": { - "mjml-core": "~3.3.5", - "react": "^15.4.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-section": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-section/-/mjml-section-3.3.5.tgz", - "integrity": "sha512-71Y5fCwoBq/v1Vl5/cmXvjrc6RZoIXzYVFuXFrWkaOO2cJ/ozXNXLRPCbj4DmjK55RITXktS2Y8X22FOgvM0EA==", - "requires": { - "lodash": "^4.17.4", - "mjml-core": "~3.3.5", - "react": "^15.4.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-social": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-social/-/mjml-social-3.3.5.tgz", - "integrity": "sha512-0x2mhrMoLQu8dhZnlZJZrfb0eVmUUXzkp0yNlIPlDR07WywPKbFOorIQXMT5rSvihskDpklRHA8lz13bBJ/ofw==", - "requires": { - "lodash": "^4.17.4", - "mjml-core": "~3.3.5", - "react": "^15.4.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-spacer": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-spacer/-/mjml-spacer-3.3.5.tgz", - "integrity": "sha512-9uAgL4ga1cSRBJVRQhjLJCgv0DlAfi71V2LbZRa7mFJ3biPTZuGw2OYdWXopX3Iu5MtkIuJ4+dMPV+C14NGR9Q==", - "requires": { - "mjml-core": "~3.3.5", - "react": "^15.4.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-table": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-table/-/mjml-table-3.3.5.tgz", - "integrity": "sha512-sZ5wOpHRVlag1llIu7NQcFOvo/9JQudn62lEcJqRXBygUE5BNkswN1L2oS6wHYVWiwynDleIF7Jg2l0Dv5RJDA==", - "requires": { - "mjml-core": "~3.3.5", - "react": "^15.4.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-text": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-text/-/mjml-text-3.3.5.tgz", - "integrity": "sha512-4W6J5Dl1HAwYVpropG/BxkKDI4nvOdEoPT9T9v/kZtjcQAEj0VTrsOF2GbZbBA9gCuXZMNssUqsnuy6OvlUv2w==", - "requires": { - "classnames": "^2.2.5", - "lodash": "^4.17.4", - "mjml-core": "~3.3.5", - "react": "^15.4.2" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-validator": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-3.3.5.tgz", - "integrity": "sha512-+SPP64nJVG9IX1QwcxPY7POiMZZKlD+jxiuYo1F453IXsezeZCAUq+CmcBrDMHkG5ms1uC5d/GPZ4ICMu2V69A==", - "requires": { - "lodash": "^4.17.4", - "warning": "^3.0.0" - } - }, - "mjml-wrapper": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-wrapper/-/mjml-wrapper-3.3.5.tgz", - "integrity": "sha512-G88x20fqEJWVi4xuN1KGpxNQJdELCC44RiY0bWd7GnH+eAcWcA7/HS2VWmq6f7G3Wi8+avdI51Yd+5uvjxWElA==", - "requires": { - "lodash": "^4.17.2", - "mjml-core": "~3.3.5", - "mjml-section": "~3.3.3", - "react": "^15.4.1" - }, - "dependencies": { - "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - } } }, "grapesjs-preset-newsletter": { @@ -7033,9 +6448,9 @@ "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" }, "math-random": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", - "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", + "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==" }, "md5.js": { "version": "1.3.5", @@ -7256,6 +6671,153 @@ } } }, + "mjml": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml/-/mjml-3.3.5.tgz", + "integrity": "sha512-a7uMTYsXNPfwe4ncjQXobcqWsdOoKa4sk2+F59g8Q7AFZDLIFV0Ya5fcp3spBADovCLycZ8seF79K3svU/KDIg==", + "requires": { + "lodash": "^4.17.4", + "mjml-accordion": "~3.3.5", + "mjml-button": "~3.3.5", + "mjml-carousel": "~3.3.5", + "mjml-cli": "~3.3.5", + "mjml-column": "~3.3.5", + "mjml-container": "~3.3.5", + "mjml-core": "~3.3.5", + "mjml-divider": "~3.3.5", + "mjml-group": "~3.3.5", + "mjml-head-attributes": "~3.3.5", + "mjml-head-font": "~3.3.5", + "mjml-head-preview": "~3.3.5", + "mjml-head-style": "~3.3.5", + "mjml-head-title": "~3.3.5", + "mjml-hero": "~3.3.5", + "mjml-html": "~3.3.5", + "mjml-image": "~3.3.5", + "mjml-invoice": "~3.3.5", + "mjml-list": "~3.3.5", + "mjml-location": "~3.3.5", + "mjml-navbar": "~3.3.5", + "mjml-raw": "~3.3.5", + "mjml-section": "~3.3.5", + "mjml-social": "~3.3.5", + "mjml-spacer": "~3.3.5", + "mjml-table": "~3.3.5", + "mjml-text": "~3.3.5", + "mjml-wrapper": "~3.3.5" + } + }, + "mjml-accordion": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-accordion/-/mjml-accordion-3.3.5.tgz", + "integrity": "sha512-pK30iNhIvqnDnidqVKFw4GJEfKWCukYYw4hByIagwikdVKdbXHc1pf/IpFsKtJ8zx9Cdi4JeFVWYwwikU2v1Bw==", + "requires": { + "classnames": "^2.2.5", + "lodash": "^4.17.4", + "mjml-core": "~3.3.5", + "react": "^15.4.2" + }, + "dependencies": { + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", + "requires": { + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" + } + } + } + }, + "mjml-button": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-button/-/mjml-button-3.3.5.tgz", + "integrity": "sha512-PKvk6arRTYLY4PcpPdf9bTpFvZtw/2KIID4H0+lYLz0C8pIuHPoHh4wTaXguBGdT9gLfpMAp2WleMOUD6zmEPA==", + "requires": { + "lodash": "^4.17.4", + "mjml-core": "~3.3.5", + "react": "^15.4.2" + }, + "dependencies": { + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", + "requires": { + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" + } + } + } + }, + "mjml-carousel": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-carousel/-/mjml-carousel-3.3.5.tgz", + "integrity": "sha512-yGXLZBUxQLKiCxS5H5MSoAbGJcSmdcBYFHqLsukiHAHbsetpang9BbI0v3X+PrvOco6DzL2SlOG8EDXnrAuNUw==", + "requires": { + "lodash": "^4.17.4", + "mjml-core": "~3.3.5", + "react": "^15.4.2" + }, + "dependencies": { + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", + "requires": { + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" + } + } + } + }, + "mjml-cli": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-cli/-/mjml-cli-3.3.5.tgz", + "integrity": "sha512-LmOht5KFC/9oRngLitnGUpB2lNPUz2TGNeRIxBIJNC0FtaBpUz3g5LCofZHYvSMOEcgVqISeOHsmNIV75l3lLw==", + "requires": { + "chokidar": "^1.6.1", + "commander": "^2.9.0", + "glob": "^7.1.1", + "lodash": "^4.17.4", + "mjml-core": "~3.3.5", + "mz": "^2.6.0" + } + }, + "mjml-column": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-column/-/mjml-column-3.3.5.tgz", + "integrity": "sha512-VvU7PQ9phme1oWoKugw1UTNeqNXMqDVOkrrtMNpKiLOByuG0vMegwWGJOIsuznuy80Pl5wnVk9XuynMbOhb82A==", + "requires": { + "classnames": "^2.2.5", + "lodash": "^4.17.4", + "mjml-core": "~3.3.5", + "react": "^15.4.2" + }, + "dependencies": { + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", + "requires": { + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" + } + } + } + }, "mjml-container": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/mjml-container/-/mjml-container-3.3.5.tgz", @@ -7265,6 +6827,42 @@ "mjml-core": "~3.3.5", "react": "^15.4.2" }, + "dependencies": { + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", + "requires": { + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" + } + } + } + }, + "mjml-core": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-3.3.5.tgz", + "integrity": "sha512-Cqc+8HLyMe26Co1jnyKTt+m10rQnXmAjXYcrdJV8QrjpDeLIZd1dClYTBjEAtREmUf5cAovdRHulPLfiOF9sZA==", + "requires": { + "cheerio": "^0.22.0", + "classnames": "^2.2.5", + "debug": "^2.6.0", + "he": "^1.1.0", + "hoist-non-react-statics": "^1.2.0", + "html-minifier": "^3.2.3", + "immutable": "^3.8.1", + "jquery": "^3.1.1", + "js-beautify": "^1.6.8", + "juice": "^4.0.2", + "lodash": "^4.17.4", + "mjml-validator": "~3.3.3", + "react": "^15.4.2", + "react-dom": "^15.4.2", + "warning": "^3.0.0" + }, "dependencies": { "commander": { "version": "2.19.0", @@ -7296,72 +6894,144 @@ }, "hoist-non-react-statics": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" }, - "mjml-core": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-3.3.5.tgz", - "integrity": "sha512-Cqc+8HLyMe26Co1jnyKTt+m10rQnXmAjXYcrdJV8QrjpDeLIZd1dClYTBjEAtREmUf5cAovdRHulPLfiOF9sZA==", + "immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" + }, + "juice": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz", + "integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==", "requires": { "cheerio": "^0.22.0", - "classnames": "^2.2.5", - "debug": "^2.6.0", - "he": "^1.1.0", - "hoist-non-react-statics": "^1.2.0", - "html-minifier": "^3.2.3", - "immutable": "^3.8.1", - "jquery": "^3.1.1", - "js-beautify": "^1.6.8", - "juice": "^4.0.2", - "lodash": "^4.17.4", - "mjml-validator": "~3.3.3", - "react": "^15.4.2", - "react-dom": "^15.4.2", - "warning": "^3.0.0" - }, - "dependencies": { - "immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" - }, - "juice": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz", - "integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==", - "requires": { - "cheerio": "^0.22.0", - "commander": "^2.15.1", - "cross-spawn": "^5.1.0", - "deep-extend": "^0.5.1", - "mensch": "^0.3.3", - "slick": "^1.12.2", - "web-resource-inliner": "^4.2.1" - } - }, - "react-dom": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", - "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=", - "requires": { - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } + "commander": "^2.15.1", + "cross-spawn": "^5.1.0", + "deep-extend": "^0.5.1", + "mensch": "^0.3.3", + "slick": "^1.12.2", + "web-resource-inliner": "^4.2.1" } }, - "mjml-validator": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-3.3.5.tgz", - "integrity": "sha512-+SPP64nJVG9IX1QwcxPY7POiMZZKlD+jxiuYo1F453IXsezeZCAUq+CmcBrDMHkG5ms1uC5d/GPZ4ICMu2V69A==", + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", "requires": { - "lodash": "^4.17.4", - "warning": "^3.0.0" + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" } }, + "react-dom": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", + "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=", + "requires": { + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" + } + } + } + }, + "mjml-divider": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-divider/-/mjml-divider-3.3.5.tgz", + "integrity": "sha512-LIEwKA2v/hxU7YKL/h/H4TRCL47167W6JQ+Ju7JZE9ropPvEHnmd5sWme9wc9iib70+6RIk25JvGjxfQZJeOOw==", + "requires": { + "lodash": "^4.17.4", + "mjml-core": "~3.3.5", + "react": "^15.4.2" + }, + "dependencies": { + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", + "requires": { + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" + } + } + } + }, + "mjml-group": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-group/-/mjml-group-3.3.5.tgz", + "integrity": "sha512-PNAmb+TqTkglKJFQpgaxuKSxfo7m/MCKNxgOjY6xaTswY/5BVtYO1ne2+raYBGWR2OLw3f8vNV4EZW+REy3fSQ==", + "requires": { + "classnames": "^2.2.5", + "lodash": "^4.17.4", + "mjml-core": "~3.3.5", + "react": "^15.4.2" + }, + "dependencies": { + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", + "requires": { + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" + } + } + } + }, + "mjml-head-attributes": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-head-attributes/-/mjml-head-attributes-3.3.5.tgz", + "integrity": "sha512-P0QzVA+xHp0Dn5DzC+sMsdu06a2q3ZqtHho6ex9cRAJ1DrgXr043XT5VddycaW0BssG8tca31nA+eELiP8GxYw==", + "requires": { + "lodash": "^4.17.4" + } + }, + "mjml-head-font": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-head-font/-/mjml-head-font-3.3.5.tgz", + "integrity": "sha512-GosELGr8UsMkzfhPU8QJLvsCjrJMyJn6eA2u7icI0e8Q5G13GxUYIdyh1ICepn9Ff/NZp7mkwXCDmelJmlhxng==", + "requires": { + "lodash": "^4.17.4" + } + }, + "mjml-head-preview": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-head-preview/-/mjml-head-preview-3.3.5.tgz", + "integrity": "sha512-hoiGShrkk2fw4lIsgoaC+L40eV6AGi2/p9C2BSAKTmeKPl70sUIk9TKpu+g3atAtV10EkHiG5XBhZCZSbZwnRA==" + }, + "mjml-head-style": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-head-style/-/mjml-head-style-3.3.5.tgz", + "integrity": "sha512-51o0rB7fCx2f58hI/FHQH1X72ahap25oXeiCtJ+X+hH4xdtUaRL5stPM5WdED2kRDCrurMdNxo4s9xN+immPeA==" + }, + "mjml-head-title": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-head-title/-/mjml-head-title-3.3.5.tgz", + "integrity": "sha512-DGcdLRKOxIGFJLESQi8ftIb6dKCIlPOPRpXVJ1FPoQ0/qPLXbXpHqJPRLIGzAD8tIPtVa/MbXk+Wm5+DP/Nf6w==" + }, + "mjml-hero": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-hero/-/mjml-hero-3.3.5.tgz", + "integrity": "sha512-Q8jM/wtpDkq+8WOK2V3W59MGAmW7kNZc7U+MTwuF/xjdJIUrYkOBgYyE/iv8dVUkOXXtI2tnouJ+nWxHfLJX3A==", + "requires": { + "classnames": "^2.2.5", + "lodash": "^4.17.4", + "mjml-core": "~3.3.5", + "react": "^15.4.2" + }, + "dependencies": { "react": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", @@ -7386,102 +7056,30 @@ "react": "^15.4.2" }, "dependencies": { - "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", - "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==" - }, - "hoist-non-react-statics": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", - "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" - }, - "mjml-core": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-3.3.5.tgz", - "integrity": "sha512-Cqc+8HLyMe26Co1jnyKTt+m10rQnXmAjXYcrdJV8QrjpDeLIZd1dClYTBjEAtREmUf5cAovdRHulPLfiOF9sZA==", - "requires": { - "cheerio": "^0.22.0", - "classnames": "^2.2.5", - "debug": "^2.6.0", - "he": "^1.1.0", - "hoist-non-react-statics": "^1.2.0", - "html-minifier": "^3.2.3", - "immutable": "^3.8.1", - "jquery": "^3.1.1", - "js-beautify": "^1.6.8", - "juice": "^4.0.2", - "lodash": "^4.17.4", - "mjml-validator": "~3.3.3", - "react": "^15.4.2", - "react-dom": "^15.4.2", - "warning": "^3.0.0" - }, - "dependencies": { - "immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" - }, - "juice": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz", - "integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==", - "requires": { - "cheerio": "^0.22.0", - "commander": "^2.15.1", - "cross-spawn": "^5.1.0", - "deep-extend": "^0.5.1", - "mensch": "^0.3.3", - "slick": "^1.12.2", - "web-resource-inliner": "^4.2.1" - } - }, - "react-dom": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", - "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=", - "requires": { - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-validator": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-3.3.5.tgz", - "integrity": "sha512-+SPP64nJVG9IX1QwcxPY7POiMZZKlD+jxiuYo1F453IXsezeZCAUq+CmcBrDMHkG5ms1uC5d/GPZ4ICMu2V69A==", - "requires": { - "lodash": "^4.17.4", - "warning": "^3.0.0" - } - }, + } + } + }, + "mjml-image": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-image/-/mjml-image-3.3.5.tgz", + "integrity": "sha512-UDHyaTfwchOtZp3PN9pDJpcFSv3YbEb6U25SGT0UHoJTckKOAcKWoaPGfCvi1DvnQ9+r3xqZV0/oI1qdXxa3Pw==", + "requires": { + "lodash": "^4.17.4", + "mjml-core": "~3.3.5", + "react": "^15.4.2" + }, + "dependencies": { "react": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", @@ -7509,111 +7107,6 @@ "react": "^15.4.2" }, "dependencies": { - "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", - "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==" - }, - "hoist-non-react-statics": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", - "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" - }, - "mjml-core": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-3.3.5.tgz", - "integrity": "sha512-Cqc+8HLyMe26Co1jnyKTt+m10rQnXmAjXYcrdJV8QrjpDeLIZd1dClYTBjEAtREmUf5cAovdRHulPLfiOF9sZA==", - "requires": { - "cheerio": "^0.22.0", - "classnames": "^2.2.5", - "debug": "^2.6.0", - "he": "^1.1.0", - "hoist-non-react-statics": "^1.2.0", - "html-minifier": "^3.2.3", - "immutable": "^3.8.1", - "jquery": "^3.1.1", - "js-beautify": "^1.6.8", - "juice": "^4.0.2", - "lodash": "^4.17.4", - "mjml-validator": "~3.3.3", - "react": "^15.4.2", - "react-dom": "^15.4.2", - "warning": "^3.0.0" - }, - "dependencies": { - "immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" - }, - "juice": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz", - "integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==", - "requires": { - "cheerio": "^0.22.0", - "commander": "^2.15.1", - "cross-spawn": "^5.1.0", - "deep-extend": "^0.5.1", - "mensch": "^0.3.3", - "slick": "^1.12.2", - "web-resource-inliner": "^4.2.1" - } - }, - "react-dom": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", - "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=", - "requires": { - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-table": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-table/-/mjml-table-3.3.5.tgz", - "integrity": "sha512-sZ5wOpHRVlag1llIu7NQcFOvo/9JQudn62lEcJqRXBygUE5BNkswN1L2oS6wHYVWiwynDleIF7Jg2l0Dv5RJDA==", - "requires": { - "mjml-core": "~3.3.5", - "react": "^15.4.2" - } - }, - "mjml-validator": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-3.3.5.tgz", - "integrity": "sha512-+SPP64nJVG9IX1QwcxPY7POiMZZKlD+jxiuYo1F453IXsezeZCAUq+CmcBrDMHkG5ms1uC5d/GPZ4ICMu2V69A==", - "requires": { - "lodash": "^4.17.4", - "warning": "^3.0.0" - } - }, "react": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", @@ -7638,102 +7131,6 @@ "react": "^15.4.2" }, "dependencies": { - "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", - "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==" - }, - "hoist-non-react-statics": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", - "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" - }, - "mjml-core": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-3.3.5.tgz", - "integrity": "sha512-Cqc+8HLyMe26Co1jnyKTt+m10rQnXmAjXYcrdJV8QrjpDeLIZd1dClYTBjEAtREmUf5cAovdRHulPLfiOF9sZA==", - "requires": { - "cheerio": "^0.22.0", - "classnames": "^2.2.5", - "debug": "^2.6.0", - "he": "^1.1.0", - "hoist-non-react-statics": "^1.2.0", - "html-minifier": "^3.2.3", - "immutable": "^3.8.1", - "jquery": "^3.1.1", - "js-beautify": "^1.6.8", - "juice": "^4.0.2", - "lodash": "^4.17.4", - "mjml-validator": "~3.3.3", - "react": "^15.4.2", - "react-dom": "^15.4.2", - "warning": "^3.0.0" - }, - "dependencies": { - "immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" - }, - "juice": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz", - "integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==", - "requires": { - "cheerio": "^0.22.0", - "commander": "^2.15.1", - "cross-spawn": "^5.1.0", - "deep-extend": "^0.5.1", - "mensch": "^0.3.3", - "slick": "^1.12.2", - "web-resource-inliner": "^4.2.1" - } - }, - "react-dom": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", - "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=", - "requires": { - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } - } - }, - "mjml-validator": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-3.3.5.tgz", - "integrity": "sha512-+SPP64nJVG9IX1QwcxPY7POiMZZKlD+jxiuYo1F453IXsezeZCAUq+CmcBrDMHkG5ms1uC5d/GPZ4ICMu2V69A==", - "requires": { - "lodash": "^4.17.4", - "warning": "^3.0.0" - } - }, "react": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", @@ -7760,123 +7157,207 @@ "react": "^15.4.2" }, "dependencies": { - "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + } + } + }, + "mjml-navbar": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-navbar/-/mjml-navbar-3.3.5.tgz", + "integrity": "sha512-elHcplAHvgSeJYHT0qtA+mT//8RKTBXwVOqk4b+4HV7Y9084p7rKsqtHYArMO8mfbXRYqrJ5zWn9rOU5UvSGPg==", + "requires": { + "lodash": "^4.17.4", + "mjml-core": "~3.3.5", + "mjml-section": "~3.3.3", + "react": "^15.4.2" + }, + "dependencies": { + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", "requires": { - "ms": "2.0.0" + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" } - }, - "deep-extend": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", - "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==" - }, - "hoist-non-react-statics": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", - "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" - }, - "mjml-core": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-3.3.5.tgz", - "integrity": "sha512-Cqc+8HLyMe26Co1jnyKTt+m10rQnXmAjXYcrdJV8QrjpDeLIZd1dClYTBjEAtREmUf5cAovdRHulPLfiOF9sZA==", + } + } + }, + "mjml-raw": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-raw/-/mjml-raw-3.3.5.tgz", + "integrity": "sha512-7gP98gI+eHkoc1Y91Uq57Ow3/GvF82tTmt3bjTq6aCtHKLPRDVGg+mF99JGdyqDSPQOY/rLh1W1XWgZ0JknNqQ==", + "requires": { + "mjml-core": "~3.3.5", + "react": "^15.4.2" + }, + "dependencies": { + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", "requires": { - "cheerio": "^0.22.0", - "classnames": "^2.2.5", - "debug": "^2.6.0", - "he": "^1.1.0", - "hoist-non-react-statics": "^1.2.0", - "html-minifier": "^3.2.3", - "immutable": "^3.8.1", - "jquery": "^3.1.1", - "js-beautify": "^1.6.8", - "juice": "^4.0.2", - "lodash": "^4.17.4", - "mjml-validator": "~3.3.3", - "react": "^15.4.2", - "react-dom": "^15.4.2", - "warning": "^3.0.0" - }, - "dependencies": { - "immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" - }, - "juice": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz", - "integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==", - "requires": { - "cheerio": "^0.22.0", - "commander": "^2.15.1", - "cross-spawn": "^5.1.0", - "deep-extend": "^0.5.1", - "mensch": "^0.3.3", - "slick": "^1.12.2", - "web-resource-inliner": "^4.2.1" - } - }, - "react-dom": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", - "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=", - "requires": { - "fbjs": "^0.8.9", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" - } - } + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" } - }, - "mjml-image": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-image/-/mjml-image-3.3.5.tgz", - "integrity": "sha512-UDHyaTfwchOtZp3PN9pDJpcFSv3YbEb6U25SGT0UHoJTckKOAcKWoaPGfCvi1DvnQ9+r3xqZV0/oI1qdXxa3Pw==", + } + } + }, + "mjml-section": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-section/-/mjml-section-3.3.5.tgz", + "integrity": "sha512-71Y5fCwoBq/v1Vl5/cmXvjrc6RZoIXzYVFuXFrWkaOO2cJ/ozXNXLRPCbj4DmjK55RITXktS2Y8X22FOgvM0EA==", + "requires": { + "lodash": "^4.17.4", + "mjml-core": "~3.3.5", + "react": "^15.4.2" + }, + "dependencies": { + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", "requires": { - "lodash": "^4.17.4", - "mjml-core": "~3.3.5", - "react": "^15.4.2" + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" } - }, - "mjml-text": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-text/-/mjml-text-3.3.5.tgz", - "integrity": "sha512-4W6J5Dl1HAwYVpropG/BxkKDI4nvOdEoPT9T9v/kZtjcQAEj0VTrsOF2GbZbBA9gCuXZMNssUqsnuy6OvlUv2w==", + } + } + }, + "mjml-social": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-social/-/mjml-social-3.3.5.tgz", + "integrity": "sha512-0x2mhrMoLQu8dhZnlZJZrfb0eVmUUXzkp0yNlIPlDR07WywPKbFOorIQXMT5rSvihskDpklRHA8lz13bBJ/ofw==", + "requires": { + "lodash": "^4.17.4", + "mjml-core": "~3.3.5", + "react": "^15.4.2" + }, + "dependencies": { + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", "requires": { - "classnames": "^2.2.5", - "lodash": "^4.17.4", - "mjml-core": "~3.3.5", - "react": "^15.4.2" + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" } - }, - "mjml-validator": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-3.3.5.tgz", - "integrity": "sha512-+SPP64nJVG9IX1QwcxPY7POiMZZKlD+jxiuYo1F453IXsezeZCAUq+CmcBrDMHkG5ms1uC5d/GPZ4ICMu2V69A==", + } + } + }, + "mjml-spacer": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-spacer/-/mjml-spacer-3.3.5.tgz", + "integrity": "sha512-9uAgL4ga1cSRBJVRQhjLJCgv0DlAfi71V2LbZRa7mFJ3biPTZuGw2OYdWXopX3Iu5MtkIuJ4+dMPV+C14NGR9Q==", + "requires": { + "mjml-core": "~3.3.5", + "react": "^15.4.2" + }, + "dependencies": { + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", "requires": { - "lodash": "^4.17.4", - "warning": "^3.0.0" + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" } - }, + } + } + }, + "mjml-table": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-table/-/mjml-table-3.3.5.tgz", + "integrity": "sha512-sZ5wOpHRVlag1llIu7NQcFOvo/9JQudn62lEcJqRXBygUE5BNkswN1L2oS6wHYVWiwynDleIF7Jg2l0Dv5RJDA==", + "requires": { + "mjml-core": "~3.3.5", + "react": "^15.4.2" + }, + "dependencies": { + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", + "requires": { + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" + } + } + } + }, + "mjml-text": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-text/-/mjml-text-3.3.5.tgz", + "integrity": "sha512-4W6J5Dl1HAwYVpropG/BxkKDI4nvOdEoPT9T9v/kZtjcQAEj0VTrsOF2GbZbBA9gCuXZMNssUqsnuy6OvlUv2w==", + "requires": { + "classnames": "^2.2.5", + "lodash": "^4.17.4", + "mjml-core": "~3.3.5", + "react": "^15.4.2" + }, + "dependencies": { + "react": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", + "requires": { + "create-react-class": "^15.6.0", + "fbjs": "^0.8.9", + "loose-envify": "^1.1.0", + "object-assign": "^4.1.0", + "prop-types": "^15.5.10" + } + } + } + }, + "mjml-validator": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-3.3.5.tgz", + "integrity": "sha512-+SPP64nJVG9IX1QwcxPY7POiMZZKlD+jxiuYo1F453IXsezeZCAUq+CmcBrDMHkG5ms1uC5d/GPZ4ICMu2V69A==", + "requires": { + "lodash": "^4.17.4", + "warning": "^3.0.0" + } + }, + "mjml-wrapper": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/mjml-wrapper/-/mjml-wrapper-3.3.5.tgz", + "integrity": "sha512-G88x20fqEJWVi4xuN1KGpxNQJdELCC44RiY0bWd7GnH+eAcWcA7/HS2VWmq6f7G3Wi8+avdI51Yd+5uvjxWElA==", + "requires": { + "lodash": "^4.17.2", + "mjml-core": "~3.3.5", + "mjml-section": "~3.3.3", + "react": "^15.4.1" + }, + "dependencies": { "react": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", @@ -9447,6 +8928,28 @@ "performance-now": "^2.1.0" } }, + "randomatic": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", + "requires": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, "randombytes": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", diff --git a/client/package.json b/client/package.json index 06fb2659..8530c2f0 100644 --- a/client/package.json +++ b/client/package.json @@ -26,14 +26,15 @@ "bootstrap": "^4.2.1", "datatables.net": "^1.10.19", "datatables.net-bs4": "^1.10.19", + "ellipsize": "^0.1.0", "grapesjs": "^0.14.49", - "grapesjs-mjml": "0.0.27", + "grapesjs-mjml": "0.0.31", "grapesjs-preset-newsletter": "^0.2.20", "i18next": "^13.1.0", "i18next-browser-languagedetector": "^2.2.4", "immutable": "^4.0.0-rc.12", "juice": "^5.1.0", - "mjml4-in-browser": "^1.0.2", + "mjml4-in-browser": "^1.1.0", "moment": "^2.23.0", "moment-timezone": "^0.5.23", "popper.js": "^1.14.6", diff --git a/client/src/account/styles.scss b/client/src/account/styles.scss index eb25bdb6..f8e9249f 100644 --- a/client/src/account/styles.scss +++ b/client/src/account/styles.scss @@ -1,9 +1,9 @@ -.api { - :global .card h4 { - margin-top: 0px; - } - - h4 { - margin-top: 45px; - } +.api { + :global .card h4 { + margin-top: 0px; + } + + h4 { + margin-top: 45px; + } } \ No newline at end of file diff --git a/client/src/campaigns/CUD.js b/client/src/campaigns/CUD.js index b2da20c7..6c81937d 100644 --- a/client/src/campaigns/CUD.js +++ b/client/src/campaigns/CUD.js @@ -164,43 +164,45 @@ export default class CUD extends Component { } } + getFormValuesMutator(data) { + // The source cannot be changed once campaign is created. Thus we don't have to initialize fields for all other sources + if (data.source === CampaignSource.TEMPLATE) { + data.data_sourceTemplate = data.data.sourceTemplate; + } + + if (data.source === CampaignSource.URL) { + data.data_sourceUrl = data.data.sourceUrl; + } + + if (data.type === CampaignType.RSS) { + data.data_feedUrl = data.data.feedUrl; + } + + for (const overridable of campaignOverridables) { + data[overridable + '_overriden'] = data[overridable + '_override'] !== null; + } + + const lsts = []; + for (const lst of data.lists) { + const lstUid = this.getNextListEntryId(); + + const prefix = 'lists_' + lstUid + '_'; + + data[prefix + 'list'] = lst.list; + data[prefix + 'segment'] = lst.segment; + data[prefix + 'useSegmentation'] = !!lst.segment; + + lsts.push(lstUid); + } + data.lists = lsts; + + // noinspection JSIgnoredPromiseFromCall + this.fetchSendConfiguration(data.send_configuration); + } + componentDidMount() { if (this.props.entity) { - this.getFormValuesFromEntity(this.props.entity, data => { - // The source cannot be changed once campaign is created. Thus we don't have to initialize fields for all other sources - if (data.source === CampaignSource.TEMPLATE) { - data.data_sourceTemplate = data.data.sourceTemplate; - } - - if (data.source === CampaignSource.URL) { - data.data_sourceUrl = data.data.sourceUrl; - } - - if (data.type === CampaignType.RSS) { - data.data_feedUrl = data.data.feedUrl; - } - - for (const overridable of campaignOverridables) { - data[overridable + '_overriden'] = data[overridable + '_override'] !== null; - } - - const lsts = []; - for (const lst of data.lists) { - const lstUid = this.getNextListEntryId(); - - const prefix = 'lists_' + lstUid + '_'; - - data[prefix + 'list'] = lst.list; - data[prefix + 'segment'] = lst.segment; - data[prefix + 'useSegmentation'] = !!lst.segment; - - lsts.push(lstUid); - } - data.lists = lsts; - - // noinspection JSIgnoredPromiseFromCall - this.fetchSendConfiguration(data.send_configuration); - }); + this.getFormValuesFromEntity(this.props.entity, ::this.getFormValuesMutator); if (this.props.entity.status === CampaignStatus.SENDING) { this.disableForm(); @@ -337,7 +339,13 @@ export default class CUD extends Component { validateNamespace(t, state); } - async submitHandler() { + static AfterSubmitAction = { + STAY: 0, + LEAVE: 1, + STATUS: 2 + } + + async submitHandler(afterSubmitAction) { const isEdit = !!this.props.entity; const t = this.props.t; @@ -353,7 +361,7 @@ export default class CUD extends Component { this.disableForm(); this.setFormStatusMessage('info', t('saving')); - const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { + const submitResult = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { data.source = Number.parseInt(data.source); data.data = {}; @@ -411,14 +419,31 @@ export default class CUD extends Component { } }); - if (submitResponse) { - const sourceTypeKey = Number.parseInt(this.getFormValue('source')); + if (submitResult) { if (this.props.entity) { - this.navigateToWithFlashMessage('/campaigns', 'success', t('campaignSaved')); - } else if (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) { - this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/content`, 'success', t('campaignSaved')); + if (afterSubmitAction === CUD.AfterSubmitAction.STATUS) { + this.navigateToWithFlashMessage(`/campaigns/${this.props.entity.id}/status`, 'success', t('Campaign updated')); + } else if (afterSubmitAction === CUD.AfterSubmitAction.LEAVE) { + this.navigateToWithFlashMessage('/campaigns', 'success', t('Campaign updated')); + } else { + await this.getFormValuesFromURL(`rest/campaigns-settings/${this.props.entity.id}`, ::this.getFormValuesMutator); + this.enableForm(); + this.setFormStatusMessage('success', t('Campaign updated')); + } } else { - this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/status`, 'success', t('campaignSaved')); + const sourceTypeKey = Number.parseInt(this.getFormValue('source')); + + if (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) { + this.navigateToWithFlashMessage(`/campaigns/${submitResult}/content`, 'success', t('Campaign created')); + } else { + if (afterSubmitAction === CUD.AfterSubmitAction.STATUS) { + this.navigateToWithFlashMessage(`/campaigns/${submitResult}/status`, 'success', t('Campaign created')); + } else if (afterSubmitAction === CUD.AfterSubmitAction.LEAVE) { + this.navigateToWithFlashMessage(`/campaigns`, 'success', t('Campaign created')); + } else { + this.navigateToWithFlashMessage(`/campaigns/${submitResult}/edit`, 'success', t('Campaign created')); + } + } } } else { this.enableForm(); @@ -583,13 +608,21 @@ export default class CUD extends Component { sendSettings = []; const addOverridable = (id, label) => { - sendSettings.push(); - - if (this.getFormValue(id + '_overriden')) { - sendSettings.push(); - } else { + if(this.state.sendConfiguration[id + '_overridable']){ + if (this.getFormValue(id + '_overriden')) { + sendSettings.push(); + } else { + sendSettings.push( + + {this.state.sendConfiguration[id]} + + ); + } + sendSettings.push(); + } + else{ sendSettings.push( - + {this.state.sendConfiguration[id]} ); @@ -666,17 +699,6 @@ export default class CUD extends Component { templateEdit = } - let saveButtonLabel; - if (isEdit) { - saveButtonLabel = t('save'); - } else if (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) { - saveButtonLabel = t('saveAndEditContent'); - } else { - saveButtonLabel = t('saveCampaignAndGoToStatus'); - } - - - return (
{canDelete && @@ -719,25 +741,45 @@ export default class CUD extends Component {
- +
- {sendSettings} + - + {sendSettings} + + +

- - +
+ + +
+ + {sourceEdit && + <> +
+
+ {sourceEdit} +
+ + } - {sourceEdit &&
} - {sourceEdit} {templateEdit} -
diff --git a/client/src/campaigns/Status.js b/client/src/campaigns/Status.js index afa700ea..4af80eca 100644 --- a/client/src/campaigns/Status.js +++ b/client/src/campaigns/Status.js @@ -225,6 +225,18 @@ class SendControls extends Component { await this.refreshEntity(); } + async confirmStart() { + const t = this.props.t; + this.actionDialog( + t('confirmLaunch'), + t('doYouWantToLaunchTheCampaign?All'), + async () => { + await this.startAsync(); + await this.refreshEntity(); + } + ); + } + async resetAsync() { const t = this.props.t; this.actionDialog( @@ -306,7 +318,7 @@ class SendControls extends Component { {this.getFormValue('sendLater') ? - {this.props.children} - - ) - } -} - -export class Icon extends Component { - static propTypes = { - icon: PropTypes.string.isRequired, - family: PropTypes.string, - title: PropTypes.string, - className: PropTypes.string - } - - static defaultProps = { - family: 'fas' - } - - render() { - const props = this.props; - - if (props.family === 'fas' || props.family === 'far') { - return ; - } else { - console.error(`Icon font family ${props.family} not supported. (icon: ${props.icon}, title: ${props.title})`) - return null; - } - } -} - -@withComponentMixins([ - withErrorHandling -]) -export class Button extends Component { - static propTypes = { - onClickAsync: PropTypes.func, - label: PropTypes.string, - icon: PropTypes.string, - iconTitle: PropTypes.string, - className: PropTypes.string, - title: PropTypes.string, - type: PropTypes.string - } - - @withAsyncErrorHandler - async onClick(evt) { - if (this.props.onClickAsync) { - evt.preventDefault(); - await this.props.onClickAsync(evt); - } - } - - render() { - const props = this.props; - - let className = 'btn'; - if (props.className) { - className = className + ' ' + props.className; - } - - let type = props.type || 'button'; - - let icon; - if (props.icon) { - icon = - } - - let iconSpacer; - if (props.icon && props.label) { - iconSpacer = ' '; - } - - return ( - - ); - } -} - -export class ButtonDropdown extends Component { - static propTypes = { - label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - className: PropTypes.string, - buttonClassName: PropTypes.string, - menuClassName: PropTypes.string - } - - render() { - const props = this.props; - - const className = 'dropdown' + (props.className ? ' ' + props.className : ''); - const buttonClassName = 'btn dropdown-toggle' + (props.buttonClassName ? ' ' + props.buttonClassName : ''); - const menuClassName = 'dropdown-menu' + (props.menuClassName ? ' ' + props.menuClassName : ''); - - return ( -
- -
    - {props.children} -
- -
- ); - } -} - -@withComponentMixins([ - withErrorHandling -]) -export class ActionLink extends Component { - static propTypes = { - onClickAsync: PropTypes.func, - className: PropTypes.string, - href: PropTypes.string - } - - @withAsyncErrorHandler - async onClick(evt) { - if (this.props.onClickAsync) { - evt.preventDefault(); - evt.stopPropagation(); - - await this.props.onClickAsync(evt); - } - } - - render() { - const props = this.props; - - return ( - {props.children} - ); - } -} - - -export class DropdownActionLink extends Component { - static propTypes = { - onClickAsync: PropTypes.func, - className: PropTypes.string, - disabled: PropTypes.bool - } - - render() { - const props = this.props; - - let clsName = "dropdown-item "; - if (props.disabled) { - clsName += "disabled "; - } - - clsName += props.className; - - return ( - {props.children} - ); - } -} - - -export class DropdownDivider extends Component { - static propTypes = { - className: PropTypes.string - } - - render() { - const props = this.props; - - let className = 'dropdown-divider'; - if (props.className) { - className = className + ' ' + props.className; - } - - return ( -
- ); - } -} - - -@withComponentMixins([ - withTranslation, - withErrorHandling -]) -export class ModalDialog extends Component { - constructor(props) { - super(props); - - const t = props.t; - - this.state = { - buttons: this.props.buttons || [ { label: t('close'), className: 'btn-secondary', onClickAsync: null } ] - }; - } - - static propTypes = { - title: PropTypes.string, - onCloseAsync: PropTypes.func, - onButtonClickAsync: PropTypes.func, - buttons: PropTypes.array, - hidden: PropTypes.bool, - className: PropTypes.string - } - - /* - this.props.hidden - this is the desired state of the modal - this.hidden - this is the actual state of the modal - this is because there is no public API on Bootstrap modal to know whether the modal is shown or not - */ - - componentDidMount() { - const jqModal = jQuery(this.domModal); - - jqModal.on('shown.bs.modal', () => jqModal.focus()); - jqModal.on('hide.bs.modal', ::this.onHide); - - this.hidden = this.props.hidden; - jqModal.modal({ - show: !this.props.hidden - }); - } - - componentDidUpdate() { - if (this.props.hidden != this.hidden) { - const jqModal = jQuery(this.domModal); - this.hidden = this.props.hidden; - jqModal.modal(this.props.hidden ? 'hide' : 'show'); - } - } - - componentWillUnmount() { - // We discard the modal in a hard way (without hiding it). Thus we have to take care of the backgrop too. - jQuery('.modal-backdrop').remove(); - } - - onHide(evt) { - // Hide event is emited is both when hidden through user action or through API. We have to let the API - // calls through, otherwise the modal would never hide. The user actions, which change the desired state, - // are capture, converted to onClose callback and prevented. It's up to the parent to decide whether to - // hide the modal or not. - if (!this.props.hidden) { - // noinspection JSIgnoredPromiseFromCall - this.onClose(); - evt.preventDefault(); - } - } - - @withAsyncErrorHandler - async onClose() { - if (this.props.onCloseAsync) { - await this.props.onCloseAsync(); - } - } - - async onButtonClick(idx) { - const buttonSpec = this.state.buttons[idx]; - if (buttonSpec.onClickAsync) { - await buttonSpec.onClickAsync(idx); - } - } - - render() { - const props = this.props; - const t = props.t; - - const buttons = []; - for (let idx = 0; idx < this.state.buttons.length; idx++) { - const buttonSpec = this.state.buttons[idx]; - const button = -
-
{this.props.children}
-
- {buttons} -
- - - - ); - } -} - +'use strict'; + +import React, {Component} from 'react'; +import {withTranslation} from './i18n'; +import PropTypes + from 'prop-types'; +import { + withAsyncErrorHandler, + withErrorHandling +} from './error-handling'; +import {withComponentMixins} from "./decorator-helpers"; + +@withComponentMixins([ + withTranslation, + withErrorHandling +]) +export class DismissibleAlert extends Component { + static propTypes = { + severity: PropTypes.string.isRequired, + onCloseAsync: PropTypes.func + } + + @withAsyncErrorHandler + onClose() { + if (this.props.onCloseAsync) { + this.props.onCloseAsync(); + } + } + + render() { + const t = this.props.t; + + return ( +
+ + {this.props.children} +
+ ) + } +} + +export class Icon extends Component { + static propTypes = { + icon: PropTypes.string.isRequired, + family: PropTypes.string, + title: PropTypes.string, + className: PropTypes.string + } + + static defaultProps = { + family: 'fas' + } + + render() { + const props = this.props; + + if (props.family === 'fas' || props.family === 'far') { + return ; + } else { + console.error(`Icon font family ${props.family} not supported. (icon: ${props.icon}, title: ${props.title})`) + return null; + } + } +} + +@withComponentMixins([ + withErrorHandling +]) +export class Button extends Component { + static propTypes = { + onClickAsync: PropTypes.func, + label: PropTypes.string, + icon: PropTypes.string, + iconTitle: PropTypes.string, + className: PropTypes.string, + title: PropTypes.string, + type: PropTypes.string, + disabled: PropTypes.bool + } + + @withAsyncErrorHandler + async onClick(evt) { + if (this.props.onClickAsync) { + evt.preventDefault(); + await this.props.onClickAsync(evt); + } + } + + render() { + const props = this.props; + + let className = 'btn'; + if (props.className) { + className = className + ' ' + props.className; + } + + let type = props.type || 'button'; + + let icon; + if (props.icon) { + icon = + } + + let iconSpacer; + if (props.icon && props.label) { + iconSpacer = ' '; + } + + return ( + + ); + } +} + +export class ButtonDropdown extends Component { + static propTypes = { + label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + className: PropTypes.string, + buttonClassName: PropTypes.string, + menuClassName: PropTypes.string + } + + render() { + const props = this.props; + + const className = 'dropdown' + (props.className ? ' ' + props.className : ''); + const buttonClassName = 'btn dropdown-toggle' + (props.buttonClassName ? ' ' + props.buttonClassName : ''); + const menuClassName = 'dropdown-menu' + (props.menuClassName ? ' ' + props.menuClassName : ''); + + return ( +
+ +
    + {props.children} +
+ +
+ ); + } +} + +@withComponentMixins([ + withErrorHandling +]) +export class ActionLink extends Component { + static propTypes = { + onClickAsync: PropTypes.func, + className: PropTypes.string, + href: PropTypes.string + } + + @withAsyncErrorHandler + async onClick(evt) { + if (this.props.onClickAsync) { + evt.preventDefault(); + evt.stopPropagation(); + + await this.props.onClickAsync(evt); + } + } + + render() { + const props = this.props; + + return ( + {props.children} + ); + } +} + + +export class DropdownActionLink extends Component { + static propTypes = { + onClickAsync: PropTypes.func, + className: PropTypes.string, + disabled: PropTypes.bool + } + + render() { + const props = this.props; + + let clsName = "dropdown-item "; + if (props.disabled) { + clsName += "disabled "; + } + + clsName += props.className; + + return ( + {props.children} + ); + } +} + + +export class DropdownDivider extends Component { + static propTypes = { + className: PropTypes.string + } + + render() { + const props = this.props; + + let className = 'dropdown-divider'; + if (props.className) { + className = className + ' ' + props.className; + } + + return ( +
+ ); + } +} + + +@withComponentMixins([ + withTranslation, + withErrorHandling +]) +export class ModalDialog extends Component { + constructor(props) { + super(props); + + const t = props.t; + + this.state = { + buttons: this.props.buttons || [ { label: t('close'), className: 'btn-secondary', onClickAsync: null } ] + }; + } + + static propTypes = { + title: PropTypes.string, + onCloseAsync: PropTypes.func, + onButtonClickAsync: PropTypes.func, + buttons: PropTypes.array, + hidden: PropTypes.bool, + className: PropTypes.string + } + + /* + this.props.hidden - this is the desired state of the modal + this.hidden - this is the actual state of the modal - this is because there is no public API on Bootstrap modal to know whether the modal is shown or not + */ + + componentDidMount() { + const jqModal = jQuery(this.domModal); + + jqModal.on('shown.bs.modal', () => jqModal.focus()); + jqModal.on('hide.bs.modal', ::this.onHide); + + this.hidden = this.props.hidden; + jqModal.modal({ + show: !this.props.hidden + }); + } + + componentDidUpdate() { + if (this.props.hidden != this.hidden) { + const jqModal = jQuery(this.domModal); + this.hidden = this.props.hidden; + jqModal.modal(this.props.hidden ? 'hide' : 'show'); + } + } + + componentWillUnmount() { + // We discard the modal in a hard way (without hiding it). Thus we have to take care of the backgrop too. + jQuery('.modal-backdrop').remove(); + } + + onHide(evt) { + // Hide event is emited is both when hidden through user action or through API. We have to let the API + // calls through, otherwise the modal would never hide. The user actions, which change the desired state, + // are capture, converted to onClose callback and prevented. It's up to the parent to decide whether to + // hide the modal or not. + if (!this.props.hidden) { + // noinspection JSIgnoredPromiseFromCall + this.onClose(); + evt.preventDefault(); + } + } + + @withAsyncErrorHandler + async onClose() { + if (this.props.onCloseAsync) { + await this.props.onCloseAsync(); + } + } + + async onButtonClick(idx) { + const buttonSpec = this.state.buttons[idx]; + if (buttonSpec.onClickAsync) { + await buttonSpec.onClickAsync(idx); + } + } + + render() { + const props = this.props; + const t = props.t; + + const buttons = []; + for (let idx = 0; idx < this.state.buttons.length; idx++) { + const buttonSpec = this.state.buttons[idx]; + const button = +
+
{this.props.children}
+
+ {buttons} +
+ + + + ); + } +} + diff --git a/client/src/lib/decorator-helpers.js b/client/src/lib/decorator-helpers.js index b3f248c3..7ac9ac24 100644 --- a/client/src/lib/decorator-helpers.js +++ b/client/src/lib/decorator-helpers.js @@ -1,128 +1,128 @@ -'use strict'; - -import React from "react"; - -export function createComponentMixin(contexts, deps, decoratorFn) { - return { - contexts, - deps, - decoratorFn - }; -} - -export function withComponentMixins(mixins, delegateFuns) { - const mixinsClosure = new Set(); - for (const mixin of mixins) { - mixinsClosure.add(mixin); - for (const dep of mixin.deps) { - mixinsClosure.add(dep); - } - } - - const contexts = new Map(); - for (const mixin of mixinsClosure.values()) { - for (const ctx of mixin.contexts) { - contexts.set(ctx.propName, ctx.context); - } - } - - return TargetClass => { - const ctors = []; - const mixinDelegateFuns = []; - - if (delegateFuns) { - mixinDelegateFuns.push(...delegateFuns); - } - - function TargetClassWithCtors(props) { - if (!new.target) { - throw new TypeError(); - } - - const self = Reflect.construct(TargetClass, [props], new.target); - - for (const ctor of ctors) { - ctor(self, props); - } - - return self; - } - - TargetClassWithCtors.prototype = TargetClass.prototype; - - for (const attr in TargetClass) { - TargetClassWithCtors[attr] = TargetClass[attr]; - } - - - class ComponentMixinsInner extends React.Component { - render() { - const props = { - ...this.props, - ref: this.props._decoratorInnerInstanceRefFn - }; - delete props._decoratorInnerInstanceRefFn; - - return ( - - ); - } - } - - let DecoratedInner = ComponentMixinsInner; - - for (const mixin of mixinsClosure.values()) { - const res = mixin.decoratorFn(DecoratedInner, TargetClassWithCtors); - - if (res.cls) { - DecoratedInner = res.cls; - } - - if (res.ctor) { - ctors.push(res.ctor); - } - - if (res.delegateFuns) { - mixinDelegateFuns.push(...res.delegateFuns); - } - } - - class ComponentMixinsOuter extends React.Component { - render() { - let innerFn = parentProps => { - const props = { - ...parentProps, - _decoratorInnerInstanceRefFn: node => this._decoratorInnerInstance = node - }; - - return - } - - for (const [propName, Context] of contexts.entries()) { - const existingInnerFn = innerFn; - innerFn = parentProps => ( - - { - value => existingInnerFn({ - ...parentProps, - [propName]: value - }) - } - - ); - } - - return innerFn(this.props); - } - } - - for (const fun of mixinDelegateFuns) { - ComponentMixinsOuter.prototype[fun] = function (...args) { - return this._decoratorInnerInstance[fun](...args); - } - } - - return ComponentMixinsOuter; - }; -} - +'use strict'; + +import React from "react"; + +export function createComponentMixin(contexts, deps, decoratorFn) { + return { + contexts, + deps, + decoratorFn + }; +} + +export function withComponentMixins(mixins, delegateFuns) { + const mixinsClosure = new Set(); + for (const mixin of mixins) { + mixinsClosure.add(mixin); + for (const dep of mixin.deps) { + mixinsClosure.add(dep); + } + } + + const contexts = new Map(); + for (const mixin of mixinsClosure.values()) { + for (const ctx of mixin.contexts) { + contexts.set(ctx.propName, ctx.context); + } + } + + return TargetClass => { + const ctors = []; + const mixinDelegateFuns = []; + + if (delegateFuns) { + mixinDelegateFuns.push(...delegateFuns); + } + + function TargetClassWithCtors(props) { + if (!new.target) { + throw new TypeError(); + } + + const self = Reflect.construct(TargetClass, [props], new.target); + + for (const ctor of ctors) { + ctor(self, props); + } + + return self; + } + + TargetClassWithCtors.prototype = TargetClass.prototype; + + for (const attr in TargetClass) { + TargetClassWithCtors[attr] = TargetClass[attr]; + } + + + class ComponentMixinsInner extends React.Component { + render() { + const props = { + ...this.props, + ref: this.props._decoratorInnerInstanceRefFn + }; + delete props._decoratorInnerInstanceRefFn; + + return ( + + ); + } + } + + let DecoratedInner = ComponentMixinsInner; + + for (const mixin of mixinsClosure.values()) { + const res = mixin.decoratorFn(DecoratedInner, TargetClassWithCtors); + + if (res.cls) { + DecoratedInner = res.cls; + } + + if (res.ctor) { + ctors.push(res.ctor); + } + + if (res.delegateFuns) { + mixinDelegateFuns.push(...res.delegateFuns); + } + } + + class ComponentMixinsOuter extends React.Component { + render() { + let innerFn = parentProps => { + const props = { + ...parentProps, + _decoratorInnerInstanceRefFn: node => this._decoratorInnerInstance = node + }; + + return + } + + for (const [propName, Context] of contexts.entries()) { + const existingInnerFn = innerFn; + innerFn = parentProps => ( + + { + value => existingInnerFn({ + ...parentProps, + [propName]: value + }) + } + + ); + } + + return innerFn(this.props); + } + } + + for (const fun of mixinDelegateFuns) { + ComponentMixinsOuter.prototype[fun] = function (...args) { + return this._decoratorInnerInstance[fun](...args); + } + } + + return ComponentMixinsOuter; + }; +} + diff --git a/client/src/lib/error-handling.js b/client/src/lib/error-handling.js index 68752e4b..d70b4f34 100644 --- a/client/src/lib/error-handling.js +++ b/client/src/lib/error-handling.js @@ -1,76 +1,76 @@ -'use strict'; - -import React from "react"; -import PropTypes from 'prop-types'; -import {createComponentMixin} from "./decorator-helpers"; - -function handleError(that, error) { - let errorHandled; - if (that.errorHandler) { - errorHandled = that.errorHandler(error); - } - - if (!errorHandled && that.props.parentErrorHandler) { - errorHandled = handleError(that.props.parentErrorHandler, error); - } - - if (!errorHandled) { - throw error; - } - - return errorHandled; -} - -export const ParentErrorHandlerContext = React.createContext(null); -export const withErrorHandling = createComponentMixin([{context: ParentErrorHandlerContext, propName: 'parentErrorHandler'}], [], (TargetClass, InnerClass) => { - /* Example of use: - this.getFormValuesFromURL(....).catch(error => this.handleError(error)); - - It's equivalent to: - - @withAsyncErrorHandler - async loadFormValues() { - await this.getFormValuesFromURL(...); - } - */ - - const originalRender = InnerClass.prototype.render; - - InnerClass.prototype.render = function() { - return ( - - {originalRender.apply(this)} - - ); - } - - InnerClass.prototype.handleError = function(error) { - handleError(this, error); - }; - - return {}; -}); - -export function withAsyncErrorHandler(target, name, descriptor) { - let fn = descriptor.value; - - descriptor.value = async function () { - try { - await fn.apply(this, arguments) - } catch (error) { - handleError(this, error); - } - }; - - return descriptor; -} - -export function wrapWithAsyncErrorHandler(self, fn) { - return async function () { - try { - await fn.apply(this, arguments) - } catch (error) { - handleError(self, error); - } - }; -} +'use strict'; + +import React from "react"; +import PropTypes from 'prop-types'; +import {createComponentMixin} from "./decorator-helpers"; + +function handleError(that, error) { + let errorHandled; + if (that.errorHandler) { + errorHandled = that.errorHandler(error); + } + + if (!errorHandled && that.props.parentErrorHandler) { + errorHandled = handleError(that.props.parentErrorHandler, error); + } + + if (!errorHandled) { + throw error; + } + + return errorHandled; +} + +export const ParentErrorHandlerContext = React.createContext(null); +export const withErrorHandling = createComponentMixin([{context: ParentErrorHandlerContext, propName: 'parentErrorHandler'}], [], (TargetClass, InnerClass) => { + /* Example of use: + this.getFormValuesFromURL(....).catch(error => this.handleError(error)); + + It's equivalent to: + + @withAsyncErrorHandler + async loadFormValues() { + await this.getFormValuesFromURL(...); + } + */ + + const originalRender = InnerClass.prototype.render; + + InnerClass.prototype.render = function() { + return ( + + {originalRender.apply(this)} + + ); + } + + InnerClass.prototype.handleError = function(error) { + handleError(this, error); + }; + + return {}; +}); + +export function withAsyncErrorHandler(target, name, descriptor) { + let fn = descriptor.value; + + descriptor.value = async function () { + try { + await fn.apply(this, arguments) + } catch (error) { + handleError(this, error); + } + }; + + return descriptor; +} + +export function wrapWithAsyncErrorHandler(self, fn) { + return async function () { + try { + await fn.apply(this, arguments) + } catch (error) { + handleError(self, error); + } + }; +} diff --git a/client/src/lib/form.js b/client/src/lib/form.js index 6cda6538..747a4801 100644 --- a/client/src/lib/form.js +++ b/client/src/lib/form.js @@ -100,7 +100,7 @@ class Form extends Component { evt.preventDefault(); if (this.props.onSubmitAsync) { - await owner.formHandleChangedError(async () => await this.props.onSubmitAsync(evt)); + await owner.formHandleChangedError(async () => await this.props.onSubmitAsync()); } } @@ -339,7 +339,8 @@ class CheckBox extends Component { text: PropTypes.string.isRequired, label: PropTypes.string, help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - format: PropTypes.string + format: PropTypes.string, + className: PropTypes.string } render() { @@ -348,12 +349,12 @@ class CheckBox extends Component { const id = this.props.id; const htmlId = 'form_' + id; - const className = owner.addFormValidationClass('form-check-input', id); + const inputClassName = owner.addFormValidationClass('form-check-input', id); return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help, -
- owner.updateFormValue(id, !owner.getFormValue(id))}/> - +
+ owner.updateFormValue(id, !owner.getFormValue(id))}/> +
); } diff --git a/client/src/lib/helpers.js b/client/src/lib/helpers.js new file mode 100644 index 00000000..8c2f8e27 --- /dev/null +++ b/client/src/lib/helpers.js @@ -0,0 +1,8 @@ +'use strict'; + +import ellipsize from "ellipsize"; + + +export function ellipsizeBreadcrumbLabel(label) { + return ellipsize(label, 40) +} \ No newline at end of file diff --git a/client/src/lib/i18n.js b/client/src/lib/i18n.js index f30a317e..315c7a42 100644 --- a/client/src/lib/i18n.js +++ b/client/src/lib/i18n.js @@ -1,72 +1,75 @@ -'use strict'; - -import React, {Component} from 'react'; -import i18n - from 'i18next'; -import {withNamespaces} from "react-i18next"; -import LanguageDetector - from 'i18next-browser-languagedetector'; -import mailtrainConfig - from 'mailtrainConfig'; - -import {convertToFake, getLang} from '../../../shared/langs'; -import {createComponentMixin} from "./decorator-helpers"; - -import lang_en_US_common from "../../../locales/en-US/common"; - -const resourcesCommon = { - 'en-US': lang_en_US_common, - 'fk-FK': convertToFake(lang_en_US_common) -}; - -const resources = {}; -for (const lng of mailtrainConfig.enabledLanguages) { - const langDesc = getLang(lng); - resources[langDesc.longCode] = { - common: resourcesCommon[langDesc.longCode] - }; -} - -i18n - .use(LanguageDetector) - .init({ - resources, - - fallbackLng: mailtrainConfig.defaultLanguage, - defaultNS: 'common', - - interpolation: { - escapeValue: false // not needed for react - }, - - react: { - wait: true - }, - - detection: { - order: ['querystring', 'cookie', 'localStorage', 'navigator'], - lookupQuerystring: 'locale', - lookupCookie: 'i18nextLng', - lookupLocalStorage: 'i18nextLng', - caches: ['localStorage', 'cookie'] - }, - - whitelist: mailtrainConfig.enabledLanguages, - load: 'currentOnly', - - debug: true - }) - - -export default i18n; - - -export const withTranslation = createComponentMixin([], [], (TargetClass, InnerClass) => { - return { - cls: withNamespaces()(TargetClass) - } -}); - -export function tMark(key) { - return key; -} +'use strict'; + +import React, {Component} from 'react'; +import i18n + from 'i18next'; +import {withNamespaces} from "react-i18next"; +import LanguageDetector + from 'i18next-browser-languagedetector'; +import mailtrainConfig + from 'mailtrainConfig'; + +import {convertToFake, getLang} from '../../../shared/langs'; +import {createComponentMixin} from "./decorator-helpers"; + +import lang_en_US_common from "../../../locales/en-US/common"; +import lang_es_ES_common from "../../../locales/es-ES/common"; + + +const resourcesCommon = { + 'en-US': lang_en_US_common, + 'es-ES': lang_es_ES_common, + 'fk-FK': convertToFake(lang_en_US_common) +}; + +const resources = {}; +for (const lng of mailtrainConfig.enabledLanguages) { + const langDesc = getLang(lng); + resources[langDesc.longCode] = { + common: resourcesCommon[langDesc.longCode] + }; +} + +i18n + .use(LanguageDetector) + .init({ + resources, + + fallbackLng: mailtrainConfig.defaultLanguage, + defaultNS: 'common', + + interpolation: { + escapeValue: false // not needed for react + }, + + react: { + wait: true + }, + + detection: { + order: ['querystring', 'cookie', 'localStorage', 'navigator'], + lookupQuerystring: 'locale', + lookupCookie: 'i18nextLng', + lookupLocalStorage: 'i18nextLng', + caches: ['localStorage', 'cookie'] + }, + + whitelist: mailtrainConfig.enabledLanguages, + load: 'currentOnly', + + debug: false + }); + + +export default i18n; + + +export const withTranslation = createComponentMixin([], [], (TargetClass, InnerClass) => { + return { + cls: withNamespaces()(TargetClass) + }; +}); + +export function tMark(key) { + return key; +} diff --git a/client/src/lib/mjml-mosaico.js b/client/src/lib/mjml-mosaico.js new file mode 100644 index 00000000..0fa1ae3f --- /dev/null +++ b/client/src/lib/mjml-mosaico.js @@ -0,0 +1,81 @@ +'use strict'; + +import {registerDependencies, registerComponent, BodyComponent} from "mjml4-in-browser"; + +registerDependencies({ + 'mj-column': ['mj-basic-component'], + 'mj-basic-component': [] +}); + +class MjBasicComponent extends BodyComponent { + // Tell the parser that our component won't contain other mjml tags + static endingTag = true + + // Tells the validator which attributes are allowed for mj-layout + static allowedAttributes = { + 'stars-color': 'color', + 'color': 'color', + 'font-size': 'unit(px)', + 'align': 'enum(left,right,center)', + } + + // What the name suggests. Fallback value for this.getAttribute('attribute-name'). + static defaultAttributes = { + 'stars-color': 'yellow', + color: 'black', + 'font-size': '12px', + 'align': 'center', + } + + // This functions allows to define styles that can be used when rendering (see render() below) + getStyles() { + return { + wrapperDiv: { + color: this.getAttribute('stars-color'), // this.getAttribute(attrName) is the recommended way to access the attributes our component received in the mjml + 'font-size': this.getAttribute('font-size'), + }, + contentP: { + 'text-align': this.getAttribute('align'), + 'font-size': '20px' + }, + contentSpan: { + color: this.getAttribute('color') + } + } + } + + /* + Render is the only required function in a component. + It must return an html string. + */ + render() { + return ` +
+

+ + + ${this.getContent()} + + +

+
+ ` + } +} + + +export function registerComponents() { + registerComponent(MjBasicComponent) +} + diff --git a/client/src/lib/modals.js b/client/src/lib/modals.js index 6fb2c935..438be7d2 100644 --- a/client/src/lib/modals.js +++ b/client/src/lib/modals.js @@ -94,7 +94,7 @@ export class RestActionModalDialog extends Component { return (
+ ); + } + } + } +} + + +@withRouter +@withComponentMixins([ + withErrorHandling +]) +export class SectionContent extends Component { + constructor(props) { + super(props); + + this.state = { + flashMessageText: '' + }; + + this.historyUnlisten = props.history.listen((location, action) => { + // noinspection JSIgnoredPromiseFromCall + this.closeFlashMessage(); + }); + } + + static propTypes = { + structure: PropTypes.object.isRequired, + root: PropTypes.string.isRequired + } + + setFlashMessage(severity, text) { + this.setState({ + flashMessageText: text, + flashMessageSeverity: severity + }); + } + + navigateTo(path) { + this.props.history.push(path); + } + + navigateBack() { + this.props.history.goBack(); + } + + navigateToWithFlashMessage(path, severity, text) { + this.props.history.push(path); + this.setFlashMessage(severity, text); + } + + ensureAuthenticated() { + if (!mailtrainConfig.isAuthenticated) { + this.navigateTo('/login?next=' + encodeURIComponent(window.location.pathname)); + } + } + + errorHandler(error) { + if (error instanceof interoperableErrors.NotLoggedInError) { + if (window.location.pathname !== '/login') { // There may be multiple async requests failing at the same time. So we take the pathname only from the first one. + this.navigateTo('/login?next=' + encodeURIComponent(window.location.pathname)); + } + } else if (error.response && error.response.data && error.response.data.message) { + console.error(error); + this.navigateToWithFlashMessage(this.props.root, 'danger', error.response.data.message); + } else { + console.error(error); + this.navigateToWithFlashMessage(this.props.root, 'danger', error.message); + } + return true; + } + + async closeFlashMessage() { + this.setState({ + flashMessageText: '' + }); + } + + renderRoute(route) { + let flashMessage; + if (this.state.flashMessageText) { + flashMessage = {this.state.flashMessageText}; + } + + const render = props => ; + + return + } + + render() { + let routes = getRoutes('', {}, [], this.props.structure, [], null, null); + + return ( + + {routes.map(x => this.renderRoute(x))} + + ); + } +} + +@withComponentMixins([ + withTranslation +]) +export class Section extends Component { + constructor(props) { + super(props); + } + + static propTypes = { + structure: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired, + root: PropTypes.string.isRequired + } + + render() { + let structure = this.props.structure; + if (typeof structure === 'function') { + structure = structure(this.props.t); + } + + return ( + + + + ); + } +} + + +export class Title extends Component { + render() { + return ( +
+

{this.props.children}

+
+
+ ); + } +} + +export class Toolbar extends Component { + static propTypes = { + className: PropTypes.string, + }; + + render() { + let className = styles.toolbar + ' ' + styles.buttonRow; + if (this.props.className) { + className += ' ' + this.props.className; + } + + return ( +
+ {this.props.children} +
+ ); + } +} + +export class LinkButton extends Component { + static propTypes = { + label: PropTypes.string, + icon: PropTypes.string, + className: PropTypes.string, + to: PropTypes.string + }; + + render() { + const props = this.props; + + return ( +