Compare commits
247 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54b48f5581 | |||
| 52d378be61 | |||
| f7c803a3a6 | |||
| d3a0746ebb | |||
| 03ef9b2203 | |||
| 6c934bcb0f | |||
| 10196afec5 | |||
| 79fa7d073d | |||
| 04cec072e3 | |||
| a0468fd06f | |||
| 99b28eb5f2 | |||
| 472273e7e2 | |||
| 76785dcfa8 | |||
| 14eeab2098 | |||
| fd656c8649 | |||
| 7a8e90d3ca | |||
| 919612bc0d | |||
| cda91cd3f5 | |||
| a3de3e522f | |||
| 60e524c134 | |||
| 295739b079 | |||
| 6b8a92df8b | |||
| 12436fbd9e | |||
| 9ff0a51006 | |||
| 81aceeba5f | |||
| 8eb2398de1 | |||
| befa23b4ea | |||
| 2656edf27e | |||
| 2fc961725a | |||
| efc8f6744a | |||
| 479d035cd0 | |||
| b575b9a0cc | |||
| 1eb3ca9182 | |||
| a2b9808613 | |||
| 589b1aa9e0 | |||
| 49e56aebd7 | |||
| 48903eb54e | |||
| 43160ca71e | |||
| 16e900e5df | |||
| 41676a370f | |||
| e71f1b4cc2 | |||
| 8ba954c743 | |||
| d88defcc65 | |||
| 3916d63f2d | |||
| b9f6766a66 | |||
| 41f10c0dc3 | |||
| 9a10fc35e8 | |||
| 2a4e195742 | |||
| 6ced6fddd5 | |||
| ab786ae242 | |||
| a22e858648 | |||
| 7aa95e092b | |||
| 537bf3eb11 | |||
| c7a7acc1c9 | |||
| 25197b9abd | |||
| 4685337599 | |||
| ced39a279f | |||
| f2d10483ad | |||
| 92800241d7 | |||
| beb73f3e69 | |||
| 8f50e2e232 | |||
| cb0c39b081 | |||
| 133e0c9ec3 | |||
| d63d2484da | |||
| 99ecc123f4 | |||
| 54a45ea3be | |||
| 821c2eb7fe | |||
| 031dc2235a | |||
| 1a8bb762cb | |||
| 7698901451 | |||
| f3a2acdd7f | |||
| 0ac27391c7 | |||
| f0062c94be | |||
| f5f30a854b | |||
| 1b87abb4fe | |||
| 0b76e5979c | |||
| a471a33014 | |||
| 5c2b06d4e9 | |||
| 2d19ae09bf | |||
| 8ef5a5483d | |||
| a3ad7d0e08 | |||
| 50a2339cc1 | |||
| cd6ccf5838 | |||
| 0234371309 | |||
| fe7caf5faa | |||
| 91b6ac8668 | |||
| cbe538c007 | |||
| e28fd69c73 | |||
| 9c9d3141e9 | |||
| 18bd368525 | |||
| 1e8ee71aef | |||
| e5e6f1d0c8 | |||
| 692b9fa2df | |||
| f90b4a7bed | |||
| 33a473734b | |||
| aa278fb193 | |||
| 50d05c423d | |||
| 2dd2132a2b | |||
| 0a9b0f6bfb | |||
| ef33cdab04 | |||
| d6cdad7ec3 | |||
| 354b65b623 | |||
| 9716e166df | |||
| 45b652d9af | |||
| bb37848265 | |||
| 381df8bf93 | |||
| 6285bd4320 | |||
| cf824ac334 | |||
| a1ef89b6fb | |||
| 9eea045d2b | |||
| 1362ceb8c8 | |||
| 6e8d7c297a | |||
| 44e6dae289 | |||
| 1f9ba28099 | |||
| 6c4972e726 | |||
| 13dc496a02 | |||
| c7aca59afc | |||
| d2651c585f | |||
| 6ff9145603 | |||
| 19d34a4d9e | |||
| f364673388 | |||
| e04e4edc1f | |||
| 3ead0c13df | |||
| dea6d92272 | |||
| 2a0e0149fd | |||
| c047dab5a4 | |||
| 06a90429ff | |||
| 6bc4306ca3 | |||
| 82b780e9c3 | |||
| 3fdc74bdf2 | |||
| d69ab00ad5 | |||
| 8e8fbd3219 | |||
| 0d41100e04 | |||
| 5218906385 | |||
| adba2999bf | |||
| 17e8be5f42 | |||
| 9adaa6041d | |||
| 8a18304225 | |||
| 521f7965b0 | |||
| 74f1513311 | |||
| d634985698 | |||
| f3e3e7922c | |||
| 569c06b041 | |||
| abe646cec8 | |||
| 8b2ce32b75 | |||
| 7a8a9836aa | |||
| 79191c0fd7 | |||
| d9af19ca63 | |||
| fcba4b75ec | |||
| 2904519ff5 | |||
| 927718832b | |||
| eb384831fe | |||
| 895827e881 | |||
| 672ded87af | |||
| bec4279c31 | |||
| 6cf064eaf0 | |||
| d328727ceb | |||
| b83f5cd34d | |||
| 597cce4566 | |||
| 364139d362 | |||
| 3931cee8af | |||
| 91ee254477 | |||
| 8817b5f027 | |||
| 6b55f1ec72 | |||
| 6ab4978a03 | |||
| d8a8177f41 | |||
| 888336cbec | |||
| 5ab4a70414 | |||
| 94e511bb96 | |||
| 44a9cad9b3 | |||
| 24062d978a | |||
| 1fd797f52a | |||
| 20adf55c11 | |||
| 7238643740 | |||
| ad6583d5dc | |||
| 21146549f7 | |||
| 355ef79d20 | |||
| 63766e84e9 | |||
| 4923bbd985 | |||
| 63e56c5df7 | |||
| eda8daf8dd | |||
| d4d764f81e | |||
| ca41d2015a | |||
| 152bf1e361 | |||
| c81943628c | |||
| 4600d1748a | |||
| e0716ef683 | |||
| edf4ecd081 | |||
| 7caf439c53 | |||
| 5f11a11ba2 | |||
| 637c9c2afd | |||
| cabc2adc1f | |||
| 90e57e26e7 | |||
| 6ec546ace1 | |||
| 8b86ddc14c | |||
| 5afca2241a | |||
| b009d078b6 | |||
| 6bf094a4c1 | |||
| fd5a8786d0 | |||
| 214a9eb5c1 | |||
| 1e07be44da | |||
| 1e45520e67 | |||
| 6dcd0595ae | |||
| 9abbb86f2c | |||
| a30d1f07fc | |||
| 70455ea2b8 | |||
| 37af8c24c8 | |||
| bbdacca1da | |||
| 84df219d7f | |||
| 95a6709ec0 | |||
| bdf54945fe | |||
| eef5d50436 | |||
| 69999fa948 | |||
| 49a236bf89 | |||
| 1de5079aa9 | |||
| 810b37aa47 | |||
| c368e9031f | |||
| b3e4bd2cff | |||
| 4e46aa76be | |||
| adb579ed8b | |||
| 189dfe29f2 | |||
| a3d6717786 | |||
| 026cc1fcea | |||
| 9f5bb8faf1 | |||
| c38a9ddb3b | |||
| 10125066d9 | |||
| 5050291ab7 | |||
| ef4259908c | |||
| 3e729b2a34 | |||
| 916b38c6c5 | |||
| 738902d0ce | |||
| cf0c175d0a | |||
| cca35117f5 | |||
| 74d622c7a4 | |||
| 2377cb2497 | |||
| 1576904f9c | |||
| 5984f59063 | |||
| 7ef40cba9c | |||
| f8aaeba7a9 | |||
| 8eb4b450ca | |||
| e52103a516 | |||
| 5e8501b25e | |||
| 40d08ddef8 | |||
| 97a61dc0f3 | |||
| d78f4efd16 | |||
| c67101a209 | |||
| ae2b7c74dd |
@@ -1,88 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true,
|
|
||||||
"node": true,
|
|
||||||
"jest/globals": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:jest/recommended"
|
|
||||||
],
|
|
||||||
"parser": "babel-eslint",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
},
|
|
||||||
"ecmaVersion": 2018,
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"react",
|
|
||||||
"jest"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"arrow-parens": [
|
|
||||||
"error",
|
|
||||||
"as-needed"
|
|
||||||
],
|
|
||||||
"arrow-spacing": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"before": true,
|
|
||||||
"after": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"comma-dangle": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"objects": "never",
|
|
||||||
"arrays": "never",
|
|
||||||
"imports": "never",
|
|
||||||
"exports": "never",
|
|
||||||
"functions": "never"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"indent": [
|
|
||||||
"error",
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"linebreak-style": [
|
|
||||||
"error",
|
|
||||||
"unix"
|
|
||||||
],
|
|
||||||
"max-len": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"code": 80
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-var": "error",
|
|
||||||
"quotes": [
|
|
||||||
"error",
|
|
||||||
"single"
|
|
||||||
],
|
|
||||||
"semi": [
|
|
||||||
"error",
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"space-before-function-paren": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"named": "never",
|
|
||||||
"anonymous": "never",
|
|
||||||
"asyncArrow": "always"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"template-curly-spacing": [
|
|
||||||
"error",
|
|
||||||
"always"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"react": {
|
|
||||||
"version": "16.7"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"projects": {
|
|
||||||
"default": "regexper"
|
|
||||||
},
|
|
||||||
"targets": {
|
|
||||||
"regexper": {
|
|
||||||
"hosting": {
|
|
||||||
"production": [
|
|
||||||
"regexper"
|
|
||||||
],
|
|
||||||
"preview": [
|
|
||||||
"regexper-preview"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+6
-7
@@ -18,13 +18,12 @@ node_modules/
|
|||||||
# Yarn Integrity file
|
# Yarn Integrity file
|
||||||
.yarn-integrity
|
.yarn-integrity
|
||||||
|
|
||||||
# Gatsby build files
|
# Build output
|
||||||
.cache/
|
build/
|
||||||
public/
|
script/__build__/
|
||||||
|
|
||||||
# Test coverage
|
# Coverage reports
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
# Firebase
|
# Favicon cache
|
||||||
.firebase/
|
.wwp-cache/
|
||||||
firebase-debug.log
|
|
||||||
|
|||||||
+38
-23
@@ -1,55 +1,68 @@
|
|||||||
image: node:latest
|
image: node:latest
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
|
- setup
|
||||||
- test
|
- test
|
||||||
- build
|
- build
|
||||||
- deploy
|
- deploy
|
||||||
|
|
||||||
|
.shared_runner: &shared_runner
|
||||||
|
tags:
|
||||||
|
- shared
|
||||||
|
|
||||||
|
.cache_consumer: &cache_consumer
|
||||||
|
cache:
|
||||||
|
policy: pull
|
||||||
|
paths:
|
||||||
|
- node_modules
|
||||||
|
|
||||||
.preview_job: &preview_job
|
.preview_job: &preview_job
|
||||||
only:
|
only:
|
||||||
- gatsby # TODO: Change to master once merged
|
- react # TODO: Change to master once merged
|
||||||
|
|
||||||
.production_job: &production_job
|
.production_job: &production_job
|
||||||
only:
|
only:
|
||||||
- /^release-.*$/
|
- /^release-.*$/
|
||||||
|
|
||||||
.build_template: &build_template
|
.build_template: &build_template
|
||||||
|
<<: *shared_runner
|
||||||
|
<<: *cache_consumer
|
||||||
stage: build
|
stage: build
|
||||||
dependencies: []
|
dependencies: []
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- public/
|
- build/
|
||||||
script:
|
script:
|
||||||
- yarn build
|
- yarn build
|
||||||
|
|
||||||
.deploy_template: &deploy_template
|
.deploy_template: &deploy_template
|
||||||
|
<<: *shared_runner
|
||||||
|
<<: *cache_consumer
|
||||||
stage: deploy
|
stage: deploy
|
||||||
script:
|
script:
|
||||||
- yarn firebase use --token $FIREBASE_DEPLOY_KEY default
|
- yarn deploy
|
||||||
- yarn firebase deploy --only hosting:$DEPLOY_ENV -m "Pipeline $CI_PIPELINE_ID, Build $CI_BUILD_ID" --non-interactive --token $FIREBASE_DEPLOY_KEY
|
|
||||||
|
|
||||||
|
setup:
|
||||||
|
<<: *shared_runner
|
||||||
|
stage: setup
|
||||||
cache:
|
cache:
|
||||||
paths:
|
paths:
|
||||||
- node_modules/
|
- node_modules
|
||||||
|
script:
|
||||||
before_script:
|
|
||||||
- yarn install
|
- yarn install
|
||||||
|
|
||||||
test-lint:
|
test:
|
||||||
stage: test
|
<<: *shared_runner
|
||||||
script:
|
<<: *cache_consumer
|
||||||
- yarn test:lint
|
|
||||||
|
|
||||||
test-unit:
|
|
||||||
stage: test
|
stage: test
|
||||||
coverage: '/^Statements\s*:\s*([^%]+)/'
|
coverage: '/^Statements\s*:\s*([^%]+)/'
|
||||||
script:
|
|
||||||
- yarn test:unit
|
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- coverage/
|
- coverage/
|
||||||
|
script:
|
||||||
|
- yarn test
|
||||||
|
|
||||||
build-preview:
|
build_preview:
|
||||||
<<: *build_template
|
<<: *build_template
|
||||||
<<: *preview_job
|
<<: *preview_job
|
||||||
variables:
|
variables:
|
||||||
@@ -57,31 +70,33 @@ build-preview:
|
|||||||
DEPLOY_ENV: preview
|
DEPLOY_ENV: preview
|
||||||
GA_PROPERTY: $PREVIEW_GA_PROPERTY
|
GA_PROPERTY: $PREVIEW_GA_PROPERTY
|
||||||
|
|
||||||
build-production:
|
build_production:
|
||||||
<<: *build_template
|
<<: *build_template
|
||||||
<<: *production_job
|
<<: *production_job
|
||||||
variables:
|
variables:
|
||||||
DEPLOY_ENV: production
|
DEPLOY_ENV: production
|
||||||
GA_PROPERTY: $PROD_GA_PROPERTY
|
GA_PROPERTY: $PROD_GA_PROPERTY
|
||||||
|
|
||||||
deploy-preview:
|
deploy_preview:
|
||||||
<<: *deploy_template
|
<<: *deploy_template
|
||||||
<<: *preview_job
|
<<: *preview_job
|
||||||
dependencies:
|
dependencies:
|
||||||
- build-preview
|
- build_preview
|
||||||
environment:
|
environment:
|
||||||
name: preview
|
name: preview
|
||||||
url: https://preview.regexper.com
|
url: https://preview.regexper.com
|
||||||
variables:
|
variables:
|
||||||
DEPLOY_ENV: preview
|
CLOUD_FRONT_ID: $PREVIEW_CLOUDFRONT_ID
|
||||||
|
DEPLOY_BUCKET: $PREVIEW_DEPLOY_BUCKET
|
||||||
|
|
||||||
deploy-production:
|
deploy_production:
|
||||||
<<: *deploy_template
|
<<: *deploy_template
|
||||||
<<: *production_job
|
<<: *production_job
|
||||||
dependencies:
|
dependencies:
|
||||||
- build-production
|
- build_production
|
||||||
environment:
|
environment:
|
||||||
name: production
|
name: production
|
||||||
url: https://regexper.com
|
url: https://regexper.com
|
||||||
variables:
|
variables:
|
||||||
DEPLOY_ENV: production
|
CLOUD_FRONT_ID: $PROD_CLOUDFRONT_ID
|
||||||
|
DEPLOY_BUCKET: $PROD_DEPLOY_BUCKET
|
||||||
|
|||||||
@@ -20,6 +20,43 @@ To start a development server, run:
|
|||||||
|
|
||||||
$ yarn start
|
$ yarn start
|
||||||
|
|
||||||
|
### Translating
|
||||||
|
|
||||||
|
A helper tool is available to support maintaining translations. Running `yarn i18n:scrub` will update locale data files under `src/locales` by normalizing YAML syntax across all the files, maintaining a `missing.yaml` file under each locale, and indicating any existing translations that appear to be no longer used. For this, the `src/locales/en/translation.yaml` file is used as a source of truth.
|
||||||
|
|
||||||
|
To add a new locale, first create a directory for the locale under `src/locales` (no files need to be added at this point), then run `yarn i18n:scrub`. This will create a `src/locales/<locale name>/missing.yaml` file that contains required translations. Add a `src/locales/<locale name>/translation.yaml` file and at a minimum add a translation for the `/displayName` value. This should be the translated name of the locale your are adding.
|
||||||
|
|
||||||
|
If you are looking to translation missing text for a locale that is already present in the project, start by running `yarn i18n:scrub`. Check the `src/locales/<locale name>/missing.yaml` file for translations that are needed. Add any updated translations to `src/locales/<locale name>/translation.yaml`.
|
||||||
|
|
||||||
|
Before committing any updated translations, run `yarn i18n:scrub` again to maintain syntax in the YAML files and remove any translated entries from `missing.yaml`. Commit any changes to the `translation.yaml` and `missing.yaml` files.
|
||||||
|
|
||||||
|
### Available scripts
|
||||||
|
|
||||||
|
* `yarn start` - Start a development server on port 8080
|
||||||
|
* `yarn start:prod` - Run a build and start a web server on port 8080. This will not automatically rebuild.
|
||||||
|
* `yarn build` - Run a production build (used for deployments and for rebuilding when running `yarn start:prod`)
|
||||||
|
* `yarn deploy` - Deploy application to AWS S3 bucket
|
||||||
|
* `yarn test` - Run lint and unit tests
|
||||||
|
* `yarn test:lint` - Run eslint
|
||||||
|
* `yarn test:unit` - Run jest unit tests
|
||||||
|
* `yarn test:watch` - Run jest in watch mode
|
||||||
|
* `yarn test:bundle-analyzer` - Generate webpack-bundle-analyzer report
|
||||||
|
* `yarn i18n:scrub` - Scrubs i18n locale configs. Adds missing keys and normalizes YAML formatting
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Several environment variables are used to configure the application at build-time. None of these values are required during testing.
|
||||||
|
|
||||||
|
* `NODE_ENV` - Effects build-time optimizations. Set to `"production"` for builds and unit tests in package.json. Setting to anything else will show a banner in the application's header.
|
||||||
|
* `GA_PROPERTY` - Google Analytics property ID.
|
||||||
|
* `SENTRY_KEY` - Sentry.io DSN key for error reporting.
|
||||||
|
* `CI_COMMIT_REF_SLUG`, `CI_COMMIT_SHA` - GitLab CI values used to generate build ID. Displayed in application footer and used in Sentry.io error reports.
|
||||||
|
* `CLOUD_FRONT_ID` - AWS CloudFront distribution ID to invalidating when running `yarn deploy`
|
||||||
|
* `DEPLOY_BUCKET` - AWS S3 bucket to deploy application to when running `yarn deploy`.
|
||||||
|
* `DEPLOY_ENV` - Environment the application will be deployed to. Used to report environment in Sentry.io error reports. Typically set to either "preview" or "production".
|
||||||
|
* `BANNER` - Text to display in header banner. Generally generated from `NODE_ENV`
|
||||||
|
* `BUILD_ID` - Application build ID. Generated from `CI_COMMIT_REF_SLUG` and `CI_COMMIT_SHA` if not set.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
See [LICENSE.txt](/LICENSE.txt) file for licensing details.
|
See [LICENSE.txt](/LICENSE.txt) file for licensing details.
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
s3Bucket: process.env.DEPLOY_BUCKET,
|
||||||
|
cloudFrontId: process.env.CLOUD_FRONT_ID,
|
||||||
|
deployFrom: path.resolve(__dirname, 'build'),
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
match: /^service-worker.js/,
|
||||||
|
CacheControl: 'max-age=0'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /^(js|css|icons-\w{8})/,
|
||||||
|
CacheControl: 'public, max-age=31536000'
|
||||||
|
},
|
||||||
|
{ // Default config. MUST BE LAST
|
||||||
|
match: /./,
|
||||||
|
CacheControl: 'public, max-age=96400'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"hosting": [
|
|
||||||
{
|
|
||||||
"target": "production",
|
|
||||||
"public": "public"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"target": "preview",
|
|
||||||
"public": "public"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Modal from 'react-modal';
|
|
||||||
import * as Sentry from '@sentry/browser';
|
|
||||||
import { I18nextProvider } from 'react-i18next';
|
|
||||||
|
|
||||||
import i18n from 'i18n';
|
|
||||||
import Layout from 'components/Layout';
|
|
||||||
|
|
||||||
import 'site.css';
|
|
||||||
import style from 'globals.module.css';
|
|
||||||
|
|
||||||
Modal.setAppElement('#___gatsby');
|
|
||||||
|
|
||||||
Modal.defaultProps = {
|
|
||||||
...Modal.defaultProps,
|
|
||||||
className: style.modal,
|
|
||||||
overlayClassName: style.modalOverlay
|
|
||||||
};
|
|
||||||
|
|
||||||
export const onClientEntry = () => {
|
|
||||||
Sentry.getCurrentHub().getClient().getOptions().enabled =
|
|
||||||
(navigator.doNotTrack !== '1' && window.doNotTrack !== '1');
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/prop-types
|
|
||||||
export const wrapPageElement = ({ element }) => {
|
|
||||||
return <Layout>{ element }</Layout>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/prop-types
|
|
||||||
export const wrapRootElement = ({ element }) => {
|
|
||||||
return <I18nextProvider i18n={ i18n }>{ element }</I18nextProvider>;
|
|
||||||
};
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
const pkg = require('./package.json');
|
|
||||||
|
|
||||||
const buildId = [
|
|
||||||
process.env.CI_COMMIT_REF_SLUG || 'prerelese',
|
|
||||||
(process.env.CI_COMMIT_SHA || 'gitsha').slice(0, 7)
|
|
||||||
].join('-');
|
|
||||||
const banner = process.env.BANNER || (process.env.NODE_ENV === 'production'
|
|
||||||
? false
|
|
||||||
: (process.env.NODE_ENV || 'development'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
siteMetadata: {
|
|
||||||
description: pkg.description,
|
|
||||||
buildId,
|
|
||||||
banner,
|
|
||||||
defaultSyntax: 'js',
|
|
||||||
syntaxList: [
|
|
||||||
{ id: 'js', label: 'JavaScript' },
|
|
||||||
{ id: 'pcre', label: 'PCRE' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
'gatsby-plugin-react-helmet',
|
|
||||||
'gatsby-plugin-postcss',
|
|
||||||
{
|
|
||||||
resolve: 'gatsby-plugin-google-analytics',
|
|
||||||
options: {
|
|
||||||
trackingId: process.env.GA_PROPERTY,
|
|
||||||
anonymize: true,
|
|
||||||
respectDNT: true,
|
|
||||||
storeGac: false,
|
|
||||||
cookieExpires: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
resolve: 'gatsby-plugin-sentry',
|
|
||||||
options: {
|
|
||||||
dsn: process.env.SENTRY_DSN,
|
|
||||||
environment: process.env.DEPLOY_ENV || process.env.NODE_ENV,
|
|
||||||
debug: (process.env.NODE_ENV !== 'production'),
|
|
||||||
release: buildId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
resolve: 'gatsby-plugin-manifest',
|
|
||||||
options: {
|
|
||||||
name: 'Regexper',
|
|
||||||
short_name: 'Regexper',
|
|
||||||
start_url: '/',
|
|
||||||
background_color: '#6b6659',
|
|
||||||
theme_color: '#bada55',
|
|
||||||
display: 'standalone',
|
|
||||||
icon: 'src/icon.svg'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'gatsby-plugin-offline'
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
|
|
||||||
exports.onCreateWebpackConfig = ({ actions }) => {
|
|
||||||
actions.setWebpackConfig({
|
|
||||||
resolve: {
|
|
||||||
modules: [path.resolve(__dirname, 'src'), 'node_modules']
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { I18nextProvider } from 'react-i18next';
|
|
||||||
|
|
||||||
import i18n from 'i18n';
|
|
||||||
import Layout from 'components/Layout';
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/prop-types
|
|
||||||
export const wrapPageElement = ({ element }) => {
|
|
||||||
return <Layout>{ element }</Layout>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/prop-types
|
|
||||||
export const wrapRootElement = ({ element }) => {
|
|
||||||
return <I18nextProvider i18n={ i18n }>{ element }</I18nextProvider>;
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
const babelOptions = {
|
|
||||||
presets: ['babel-preset-gatsby'],
|
|
||||||
plugins: ['dynamic-import-node']
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = require('babel-jest').createTransformer(babelOptions);
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
global.___loader = {
|
|
||||||
enqueue: jest.fn()
|
|
||||||
};
|
|
||||||
|
|
||||||
global.Element.prototype.getBBox = jest.fn();
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
process(src, filename) {
|
|
||||||
return `module.exports = ${ JSON.stringify(path.basename(filename)) };`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
const yaml = require('js-yaml');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
process(src) {
|
|
||||||
return `module.exports = ${ JSON.stringify(yaml.safeLoad(src)) };`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
+143
-72
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "regexper",
|
"name": "regexper",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Regular expression visualization tool",
|
"description": "Regular expression visualization tool using railroad diagrams",
|
||||||
"homepage": "http://regexper.com",
|
"homepage": "http://regexper.com",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Jeffrey Avallone",
|
"name": "Jeffrey Avallone",
|
||||||
@@ -10,21 +10,39 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "gatsby develop",
|
"start": "webpack-dev-server --config webpack.dev.js",
|
||||||
"build": "gatsby build",
|
"start:prod": "run-s build start:http-server",
|
||||||
|
"start:http-server": "http-server -c0 ./build",
|
||||||
|
"build": "cross-env NODE_ENV=production run-s build:webpack:web build:webpack:prerender build:prerender",
|
||||||
|
"build:webpack:web": "webpack --config webpack.prod-web.js",
|
||||||
|
"build:webpack:prerender": "webpack --config webpack.prod-prerender.js",
|
||||||
|
"build:prerender": "node ./script/__build__/prerender.js",
|
||||||
|
"deploy": "node ./script/s3-upload.js deploy.config.js",
|
||||||
|
"test": "run-s test:lint 'test:unit --coverage'",
|
||||||
|
"test:unit": "cross-env NODE_ENV=production jest",
|
||||||
"test:lint": "eslint --ignore-path .gitignore .",
|
"test:lint": "eslint --ignore-path .gitignore .",
|
||||||
"test:unit": "jest --coverage",
|
"test:watch": "yarn test:unit --watch",
|
||||||
"test:watch": "jest --watch"
|
"test:bundle-analyzer": "cross-env NODE_ENV=production webpack --config webpack.bundle-analyzer.js",
|
||||||
},
|
"i18n:scrub": "node ./script/i18n-scrub.js",
|
||||||
"husky": {
|
"precommit": "run-s test:lint"
|
||||||
"hooks": {
|
|
||||||
"pre-commit": "yarn test:lint"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
">1%",
|
">1%",
|
||||||
"not ie < 11"
|
"not ie < 11"
|
||||||
],
|
],
|
||||||
|
"babel": {
|
||||||
|
"presets": [
|
||||||
|
"env",
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"transform-runtime",
|
||||||
|
"transform-class-properties",
|
||||||
|
"transform-object-rest-spread",
|
||||||
|
"transform-decorators-legacy",
|
||||||
|
"syntax-dynamic-import"
|
||||||
|
]
|
||||||
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"postcss-import": {},
|
"postcss-import": {},
|
||||||
@@ -37,79 +55,132 @@
|
|||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"clearMocks": true,
|
"clearMocks": true,
|
||||||
"collectCoverageFrom": [
|
"setupTestFrameworkScriptFile": "<rootDir>/src/setup/jest.js",
|
||||||
"src/**/*.js",
|
"snapshotSerializers": [
|
||||||
"!src/i18n.js"
|
"enzyme-to-json/serializer"
|
||||||
],
|
],
|
||||||
"coverageReporters": [
|
|
||||||
"text-summary",
|
|
||||||
"html"
|
|
||||||
],
|
|
||||||
"globals": {
|
|
||||||
"__PATH_PREFIX__": ""
|
|
||||||
},
|
|
||||||
"moduleNameMapper": {
|
|
||||||
"\\.css$": "identity-obj-proxy"
|
|
||||||
},
|
|
||||||
"modulePaths": [
|
"modulePaths": [
|
||||||
"src",
|
"src",
|
||||||
"node_modules"
|
"node_modules"
|
||||||
],
|
],
|
||||||
"setupFilesAfterEnv": [
|
"moduleNameMapper": {
|
||||||
"react-testing-library/cleanup-after-each",
|
"\\.svg$": "__mocks__/svgMock.js",
|
||||||
"<rootDir>/jest/setup.js"
|
"\\.css$": "identity-obj-proxy"
|
||||||
],
|
|
||||||
"testPathIgnorePatterns": [
|
|
||||||
"node_modules",
|
|
||||||
".cache"
|
|
||||||
],
|
|
||||||
"transform": {
|
|
||||||
"\\.yaml$": "<rootDir>/jest/yaml.js",
|
|
||||||
"\\.js$": "<rootDir>/jest/preprocess.js",
|
|
||||||
"\\.svg$": "<rootDir>/jest/static-file-transform.js"
|
|
||||||
},
|
},
|
||||||
"transformIgnorePatterns": [
|
"collectCoverageFrom": [
|
||||||
"node_modules/(?!(gatsby)/)"
|
"src/**/*.js",
|
||||||
|
"!src/i18n.js",
|
||||||
|
"!src/prerender.js",
|
||||||
|
"!src/setup/service-worker.js",
|
||||||
|
"!src/setup/jest.js",
|
||||||
|
"!src/pages/**/config.js",
|
||||||
|
"!src/pages/**/browser.js"
|
||||||
],
|
],
|
||||||
"watchPathIgnorePatterns": [
|
"coverageReporters": [
|
||||||
"<rootDir>/coverage",
|
"text-summary",
|
||||||
"<rootDir>/public"
|
"html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"node": true,
|
||||||
|
"jest/globals": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:react/recommended"
|
||||||
|
],
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"react",
|
||||||
|
"jest"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"indent": [
|
||||||
|
"error",
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"linebreak-style": [
|
||||||
|
"error",
|
||||||
|
"unix"
|
||||||
|
],
|
||||||
|
"quotes": [
|
||||||
|
"error",
|
||||||
|
"single"
|
||||||
|
],
|
||||||
|
"semi": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.2.2",
|
"@alienfast/i18next-loader": "^1.0.14",
|
||||||
"@ungap/url-search-params": "^0.1.2",
|
"aws-sdk": "^2.247.1",
|
||||||
"babel-core": "^7.0.0-bridge.0",
|
"babel-core": "^6.26.0",
|
||||||
"babel-jest": "^24.5.0",
|
"babel-eslint": "^8.2.1",
|
||||||
"babel-plugin-dynamic-import-node": "^2.2.0",
|
"babel-jest": "^23.0.1",
|
||||||
"babel-preset-gatsby": "^0.1.6",
|
"babel-loader": "^7.1.2",
|
||||||
"eslint": "^5.11.1",
|
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||||
"eslint-plugin-jest": "^22.1.2",
|
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||||
"eslint-plugin-react": "^7.12.1",
|
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||||
"firebase-tools": "^6.3.0",
|
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||||
"gatsby": "^2.0.81",
|
"babel-plugin-transform-runtime": "^6.23.0",
|
||||||
"gatsby-plugin-google-analytics": "^2.0.8",
|
"babel-preset-env": "^1.6.1",
|
||||||
"gatsby-plugin-manifest": "^2.0.13",
|
"babel-preset-react": "^6.24.1",
|
||||||
"gatsby-plugin-offline": "^2.0.21",
|
"babel-register": "^6.26.0",
|
||||||
"gatsby-plugin-postcss": "^2.0.2",
|
"cheerio": "^1.0.0-rc.2",
|
||||||
"gatsby-plugin-react-helmet": "^3.0.5",
|
"colors": "^1.1.2",
|
||||||
"gatsby-plugin-sentry": "^1.0.0",
|
"copy-webpack-plugin": "^4.4.1",
|
||||||
"husky": "^1.3.1",
|
"cross-env": "^5.1.3",
|
||||||
"i18next": "^15.0.7",
|
"css-loader": "^0.28.9",
|
||||||
"i18next-browser-languagedetector": "^3.0.1",
|
"enzyme": "^3.3.0",
|
||||||
"i18next-xhr-backend": "^2.0.1",
|
"enzyme-adapter-react-16": "^1.1.1",
|
||||||
|
"enzyme-to-json": "^3.3.1",
|
||||||
|
"eslint": "^4.17.0",
|
||||||
|
"eslint-plugin-jest": "^21.8.0",
|
||||||
|
"eslint-plugin-react": "^7.6.1",
|
||||||
|
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||||
|
"feather-icons": "^4.5.0",
|
||||||
|
"html-webpack-plugin": "^3.0.0",
|
||||||
|
"i18next": "^11.3.2",
|
||||||
|
"i18next-browser-languagedetector": "^2.1.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^24.5.0",
|
"immutable": "^3.8.2",
|
||||||
"js-yaml": "^3.13.0",
|
"jest": "^23.1.0",
|
||||||
|
"mime-types": "^2.1.18",
|
||||||
|
"npm-run-all": "^4.1.2",
|
||||||
"postcss-cssnext": "^3.1.0",
|
"postcss-cssnext": "^3.1.0",
|
||||||
"postcss-import": "^12.0.1",
|
"postcss-import": "^11.1.0",
|
||||||
"prop-types": "^15.6.2",
|
"postcss-loader": "^2.1.0",
|
||||||
"react": "^16.7.0",
|
"raven-js": "^3.22.2",
|
||||||
"react-dom": "^16.7.0",
|
"react": "^16.3.0",
|
||||||
"react-feather": "^1.1.5",
|
"react-dom": "^16.3.0",
|
||||||
"react-helmet": "^5.2.0",
|
"react-ga": "^2.4.1",
|
||||||
"react-i18next": "^10.5.3",
|
"react-i18next": "^7.3.6",
|
||||||
"react-modal": "^3.8.1",
|
"react-test-renderer": "^16.3.0",
|
||||||
"react-testing-library": "^6.0.2"
|
"recursive-readdir": "^2.2.2",
|
||||||
|
"style-loader": "^0.21.0",
|
||||||
|
"svg-react-loader": "^0.4.5",
|
||||||
|
"uglifyjs-webpack-plugin": "^1.1.8",
|
||||||
|
"url-search-params": "^0.10.0",
|
||||||
|
"webapp-webpack-plugin": "^2.1.0",
|
||||||
|
"webpack": "^4.1.1",
|
||||||
|
"webpack-cli": "^3.0.1",
|
||||||
|
"webpack-merge": "^4.1.1",
|
||||||
|
"webpack-node-externals": "^1.6.0",
|
||||||
|
"workbox-webpack-plugin": "^3.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"http-server": "^0.11.1",
|
||||||
|
"husky": "^0.14.3",
|
||||||
|
"js-yaml": "^3.10.0",
|
||||||
|
"webpack-bundle-analyzer": "^2.11.1",
|
||||||
|
"webpack-dev-server": "^3.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# humanstxt.org/
|
||||||
|
# The humans responsible & technology colophon
|
||||||
|
|
||||||
|
# TEAM
|
||||||
|
|
||||||
|
Creator: Jeff Avallone
|
||||||
|
Site: http://gitlab.com/javallone
|
||||||
|
Twitter: @javallone
|
||||||
|
|
||||||
|
# THANKS
|
||||||
|
|
||||||
|
strfriend.com for the idea, whatever happened to you?
|
||||||
|
|
||||||
|
# TECHNOLOGY COLOPHON
|
||||||
|
|
||||||
|
HTML5, CSS3, SVG, React, Feather Icons
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# robotstxt.org/
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
const util = require('util');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
const colors = require('colors/safe');
|
||||||
|
|
||||||
|
const readdir = util.promisify(fs.readdir);
|
||||||
|
const readFile = util.promisify(fs.readFile);
|
||||||
|
const writeFile = util.promisify(fs.writeFile);
|
||||||
|
|
||||||
|
const localesDir = path.resolve(__dirname, '../src/locales');
|
||||||
|
|
||||||
|
const loadLocales = async () => {
|
||||||
|
const languages = (await readdir(localesDir)).filter(name => name !== 'index.js');
|
||||||
|
|
||||||
|
let localeData = {};
|
||||||
|
|
||||||
|
await Promise.all(languages.map(async lang => {
|
||||||
|
const langDir = path.resolve(localesDir, lang);
|
||||||
|
const namespaces = (await readdir(langDir))
|
||||||
|
.filter(file => /\.yaml$/.test(file));
|
||||||
|
|
||||||
|
localeData[lang] = {};
|
||||||
|
|
||||||
|
await Promise.all(namespaces.map(async ns => {
|
||||||
|
const nsFile = path.resolve(langDir, ns);
|
||||||
|
localeData[lang][ns.replace('.yaml', '')] = yaml.safeLoad(await readFile(nsFile));
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
|
||||||
|
return localeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveLocales = async locales => {
|
||||||
|
await Promise.all(Object.keys(locales).map(async langName => {
|
||||||
|
const lang = locales[langName];
|
||||||
|
|
||||||
|
await Promise.all(Object.keys(lang).map(async nsName => {
|
||||||
|
const nsFile = path.resolve(localesDir, langName, `${ nsName }.yaml`);
|
||||||
|
const yamlDump = yaml.safeDump(lang[nsName], {
|
||||||
|
sortKeys: true
|
||||||
|
});
|
||||||
|
await writeFile(nsFile, yamlDump);
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
loadLocales()
|
||||||
|
.then(async locales => {
|
||||||
|
const sourceLocale = locales.en.translation;
|
||||||
|
const requiredKeys = Object.keys(sourceLocale);
|
||||||
|
const languages = Object.keys(locales).filter(lang => lang !== 'en');
|
||||||
|
|
||||||
|
languages.forEach(langName => {
|
||||||
|
const lang = locales[langName];
|
||||||
|
const presentKeys = Object.keys(lang).filter(nsName => nsName !== 'missing').reduce((list, nsName) => {
|
||||||
|
return list.concat(Object.keys(lang[nsName]));
|
||||||
|
}, []);
|
||||||
|
const missingKeys = requiredKeys.filter(key => !presentKeys.includes(key));
|
||||||
|
const extraKeys = presentKeys.filter(key => !requiredKeys.includes(key));
|
||||||
|
|
||||||
|
if (!lang.translation) {
|
||||||
|
lang.translation = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lang.missing) {
|
||||||
|
lang.missing = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
missingKeys.forEach(key => {
|
||||||
|
if (lang.missing[key]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(colors.yellow.bold('MISSING:'), `${ langName } needs value for "${ colors.bold(key) }".`); //eslint-disable-line no-console
|
||||||
|
lang.missing[key] = sourceLocale[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
presentKeys.forEach(key => {
|
||||||
|
if (!lang.missing[key]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(colors.yellow.bold('DEFINED:'), `Removing "${ colors.bold(key) }" from ${ langName}.missing. It is defined elsewhere.`); // eslint-disable-line no-console
|
||||||
|
delete lang.missing[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
extraKeys.forEach(key => {
|
||||||
|
console.log(colors.yellow.bold('EXTRA:'), `${ langName } has extra key for "${ colors.bold(key) }". It should be removed.`); // eslint-disable-line no-console
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return locales;
|
||||||
|
})
|
||||||
|
.then(saveLocales)
|
||||||
|
.then(() => {
|
||||||
|
console.log('Done updating locales'); // eslint-disable-line no-console
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error(colors.red.bold('FAILED:'), e); // eslint-disable-line no-console
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// NOTE: This script *MUST* be built with webpack since it requires React
|
||||||
|
// components. The script is built and run as part of `yarn build`
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { renderToString } from 'react-dom/server';
|
||||||
|
import fs from 'fs';
|
||||||
|
import util from 'util';
|
||||||
|
import cheerio from 'cheerio';
|
||||||
|
import colors from 'colors/safe';
|
||||||
|
|
||||||
|
import 'i18n';
|
||||||
|
|
||||||
|
const readdir = util.promisify(fs.readdir);
|
||||||
|
const readFile = util.promisify(fs.readFile);
|
||||||
|
const writeFile = util.promisify(fs.writeFile);
|
||||||
|
|
||||||
|
readdir('./src/pages')
|
||||||
|
.then(pages => (
|
||||||
|
Promise.all(pages.map(async page => {
|
||||||
|
const Component = (await import(`pages/${ page }/Component`)).default;
|
||||||
|
const pagePath = `./build/${ page }.html`;
|
||||||
|
|
||||||
|
const markup = cheerio.load(await readFile(pagePath));
|
||||||
|
|
||||||
|
markup('#root').html(renderToString(<Component/>));
|
||||||
|
await writeFile(pagePath, markup.html());
|
||||||
|
|
||||||
|
console.log(colors.green.bold('PRERENDERED:'), `${ page }.html`); // eslint-disable-line no-console
|
||||||
|
}))
|
||||||
|
))
|
||||||
|
.then(() => console.log('Done prerendering')) // eslint-disable-line no-console
|
||||||
|
.catch(e => {
|
||||||
|
console.error(colors.red.bold('FAIL:'), e); // eslint-disable-line no-console
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const colors = require('colors/safe');
|
||||||
|
const readdir = require('recursive-readdir');
|
||||||
|
const AWS = require('aws-sdk');
|
||||||
|
const mime = require('mime-types');
|
||||||
|
|
||||||
|
const s3 = new AWS.S3();
|
||||||
|
const cloudFront = new AWS.CloudFront();
|
||||||
|
const config = require(path.resolve(process.argv[2]));
|
||||||
|
|
||||||
|
const configFor = path => {
|
||||||
|
const { match, ...conf } = config.paths.find(conf => conf.match.test(path)); // eslint-disable-line no-unused-vars
|
||||||
|
return {
|
||||||
|
ContentType: mime.lookup(path) || 'application/octet-stream',
|
||||||
|
...conf
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const bucketContents = s3.listObjectsV2({
|
||||||
|
Bucket: config.s3Bucket
|
||||||
|
}).promise()
|
||||||
|
.then(result => {
|
||||||
|
return result.Contents.map(item => item.Key);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(colors.red.bold('Failed to fetch bucket contents:'), err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadDetails = readdir(config.deployFrom)
|
||||||
|
.then(paths => paths.map(p => {
|
||||||
|
const key = path.relative(config.deployFrom, p);
|
||||||
|
return {
|
||||||
|
Key: key,
|
||||||
|
Body: fs.createReadStream(p),
|
||||||
|
...configFor(key)
|
||||||
|
};
|
||||||
|
}))
|
||||||
|
.catch(err => {
|
||||||
|
console.error(colors.red.bold('Error:'), err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all([bucketContents, uploadDetails]).then(([bucket, upload]) => {
|
||||||
|
const deleteKeys = bucket.filter(key => !upload.find(conf => key === conf.Key));
|
||||||
|
|
||||||
|
const uploadPromises = upload.map(params => {
|
||||||
|
console.log(`Starting upload for ${ params.Key }`);
|
||||||
|
return s3.upload({
|
||||||
|
Bucket: config.s3Bucket,
|
||||||
|
...params
|
||||||
|
}).promise()
|
||||||
|
.then(() => console.log(colors.green(`${ params.Key } successful`)))
|
||||||
|
.catch(err => {
|
||||||
|
console.error(colors.red.bold(`${ params.Key } failed`));
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(uploadPromises)
|
||||||
|
.then(() => {
|
||||||
|
if (deleteKeys.length === 0) {
|
||||||
|
console.log('No files to delete');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Deleting ${ deleteKeys.length } stale files`);
|
||||||
|
return s3.deleteObjects({
|
||||||
|
Bucket: config.s3Bucket,
|
||||||
|
Delete: {
|
||||||
|
Objects: deleteKeys.map(key => ({ Key: key }))
|
||||||
|
}
|
||||||
|
}).promise()
|
||||||
|
.then(() => console.log(colors.green('Delete successful')))
|
||||||
|
.catch(err => {
|
||||||
|
console.error(colors.red.bold('Delete failed'));
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return cloudFront.createInvalidation({
|
||||||
|
DistributionId: config.cloudFrontId,
|
||||||
|
InvalidationBatch: {
|
||||||
|
CallerReference: `deploy-${ process.env.CI_COMMIT_REF_SLUG }-${ process.env.CI_COMMIT_SHA }`,
|
||||||
|
Paths: {
|
||||||
|
Quantity: 1,
|
||||||
|
Items: [
|
||||||
|
'/*'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).promise();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(colors.red.bold('Error:'), err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/* eslint-disable react/prop-types */
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import reflowable from 'components/SVG/reflowable';
|
||||||
|
|
||||||
|
@reflowable
|
||||||
|
class SVGElement extends React.PureComponent {
|
||||||
|
reflow() {
|
||||||
|
return this.setBBox(this.props.bbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <text>Mock content</text>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SVGElement;
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
|
|
||||||
const buildMock = component => {
|
|
||||||
const componentName = component.displayName || component.name || 'Component';
|
|
||||||
const Mock = ({ children, ...props }) => (
|
|
||||||
<span
|
|
||||||
data-component={ componentName }
|
|
||||||
data-props={ JSON.stringify(props, null, ' ') }>{ children }</span>
|
|
||||||
);
|
|
||||||
Mock.propTypes = component.propTypes;
|
|
||||||
return Mock;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = path => {
|
|
||||||
const actual = jest.requireActual(path);
|
|
||||||
return buildMock(actual.default || actual);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.buildMock = buildMock;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
const gatsby = jest.requireActual('gatsby');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
...gatsby,
|
|
||||||
graphql: jest.fn().mockImplementation(([query]) => query),
|
|
||||||
Link: jest.fn().mockImplementation(({ to, ...rest }) =>
|
|
||||||
React.createElement('a', {
|
|
||||||
...rest,
|
|
||||||
href: to
|
|
||||||
})
|
|
||||||
),
|
|
||||||
StaticQuery: jest.fn()
|
|
||||||
};
|
|
||||||
+18
-13
@@ -1,15 +1,20 @@
|
|||||||
const i18n = jest.requireActual('i18n');
|
import React from 'react';
|
||||||
|
import i18n from 'i18next';
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
|
|
||||||
// Load empty resource bundle to reduce logging output
|
const translate = txt => `translate(${ txt })`;
|
||||||
i18n.default.addResourceBundle('dev', 'translation', {});
|
|
||||||
i18n.default.addResourceBundle('en', 'translation', {});
|
|
||||||
i18n.default.addResourceBundle('other', 'translation', {});
|
|
||||||
|
|
||||||
module.exports = {
|
i18n.init({
|
||||||
...i18n,
|
fallbackLng: 'en',
|
||||||
locales: [
|
fallbackNS: 'missing',
|
||||||
{ code: 'en', name: 'English' },
|
debug: false,
|
||||||
{ code: 'other', name: 'Other' }
|
resources: {}
|
||||||
],
|
});
|
||||||
mockT: str => `TRANSLATE(${ str })`
|
|
||||||
};
|
const I18nWrapper = ({ children }) => ( // eslint-disable-line react/prop-types
|
||||||
|
<I18nextProvider i18n={ i18n }>
|
||||||
|
{ React.cloneElement(React.Children.only(children), { t: translate }) }
|
||||||
|
</I18nextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { translate, i18n, I18nWrapper };
|
||||||
|
|||||||
Vendored
-8
@@ -1,8 +0,0 @@
|
|||||||
const reactI18next = jest.requireActual('react-i18next');
|
|
||||||
const i18n = require('i18n');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
...reactI18next,
|
|
||||||
Trans: require('__mocks__/component-mock').buildMock(reactI18next.Trans),
|
|
||||||
useTranslation: () => ({ i18n, t: i18n.mockT })
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const SvgMock = () => <svg></svg>;
|
||||||
|
|
||||||
|
export default SvgMock;
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`layout running against a node with a basic bounding box 1`] = `
|
|
||||||
Object {
|
|
||||||
"box": Object {
|
|
||||||
"axisX1": 0,
|
|
||||||
"axisX2": 20,
|
|
||||||
"axisY": 5,
|
|
||||||
"height": 10,
|
|
||||||
"width": 20,
|
|
||||||
},
|
|
||||||
"props": Object {},
|
|
||||||
"type": "Example",
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`layout running against a node with a complete bounding box 1`] = `
|
|
||||||
Object {
|
|
||||||
"box": Object {
|
|
||||||
"axisX1": 5,
|
|
||||||
"axisX2": 15,
|
|
||||||
"axisY": 2,
|
|
||||||
"height": 10,
|
|
||||||
"width": 20,
|
|
||||||
},
|
|
||||||
"props": Object {},
|
|
||||||
"type": "Example",
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`layout running against a node with props 1`] = `
|
|
||||||
Object {
|
|
||||||
"box": Object {
|
|
||||||
"axisX1": 0,
|
|
||||||
"axisX2": 0,
|
|
||||||
"axisY": 0,
|
|
||||||
"height": 0,
|
|
||||||
"width": 0,
|
|
||||||
},
|
|
||||||
"props": Object {
|
|
||||||
"property": "example",
|
|
||||||
},
|
|
||||||
"type": "Example",
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`layout running against a simple node 1`] = `
|
|
||||||
Object {
|
|
||||||
"box": Object {
|
|
||||||
"axisX1": 0,
|
|
||||||
"axisX2": 0,
|
|
||||||
"axisY": 0,
|
|
||||||
"height": 0,
|
|
||||||
"width": 0,
|
|
||||||
},
|
|
||||||
"props": Object {},
|
|
||||||
"type": "Example",
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`layout running layout on children 1`] = `
|
|
||||||
Object {
|
|
||||||
"box": Object {
|
|
||||||
"axisX1": 0,
|
|
||||||
"axisX2": 0,
|
|
||||||
"axisY": 0,
|
|
||||||
"height": 0,
|
|
||||||
"width": 0,
|
|
||||||
},
|
|
||||||
"children": Array [
|
|
||||||
Object {
|
|
||||||
"box": Object {
|
|
||||||
"axisX1": 0,
|
|
||||||
"axisX2": 20,
|
|
||||||
"axisY": 5,
|
|
||||||
"height": 10,
|
|
||||||
"width": 20,
|
|
||||||
},
|
|
||||||
"props": Object {},
|
|
||||||
"type": "Other",
|
|
||||||
},
|
|
||||||
"string example",
|
|
||||||
],
|
|
||||||
"props": Object {},
|
|
||||||
"type": "Example",
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,255 +1,17 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`App removing rendered expression 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="Form"
|
|
||||||
data-props="{
|
|
||||||
\\"syntax\\": \\"js\\",
|
|
||||||
\\"expr\\": \\"test expression\\",
|
|
||||||
\\"syntaxList\\": [
|
|
||||||
{
|
|
||||||
\\"id\\": \\"testJS\\",
|
|
||||||
\\"label\\": \\"Testing JS\\"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\\"id\\": \\"other\\",
|
|
||||||
\\"label\\": \\"Other\\"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="FormActions"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
data-component="Render"
|
|
||||||
data-props="{
|
|
||||||
\\"data\\": \\"LAYOUT(PARSED(test expression))\\"
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`App removing rendered expression 2`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="Form"
|
|
||||||
data-props="{
|
|
||||||
\\"syntax\\": \\"js\\",
|
|
||||||
\\"expr\\": \\"\\",
|
|
||||||
\\"syntaxList\\": [
|
|
||||||
{
|
|
||||||
\\"id\\": \\"testJS\\",
|
|
||||||
\\"label\\": \\"Testing JS\\"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\\"id\\": \\"other\\",
|
|
||||||
\\"label\\": \\"Other\\"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`App rendering 1`] = `
|
exports[`App rendering 1`] = `
|
||||||
<DocumentFragment>
|
<React.Fragment>
|
||||||
<span
|
<Translate(Form)
|
||||||
data-component="Form"
|
downloadUrls={Array []}
|
||||||
data-props="{
|
key="expr=undefined&syntax=undefined"
|
||||||
\\"syntax\\": \\"js\\",
|
onSubmit={[Function]}
|
||||||
\\"expr\\": \\"\\",
|
syntaxes={
|
||||||
\\"syntaxList\\": [
|
Object {
|
||||||
{
|
"js": "JavaScript",
|
||||||
\\"id\\": \\"testJS\\",
|
"pcre": "PCRE",
|
||||||
\\"label\\": \\"Testing JS\\"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\\"id\\": \\"other\\",
|
|
||||||
\\"label\\": \\"Other\\"
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`App rendering an expression 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="Form"
|
|
||||||
data-props="{
|
|
||||||
\\"syntax\\": \\"js\\",
|
|
||||||
\\"expr\\": \\"\\",
|
|
||||||
\\"syntaxList\\": [
|
|
||||||
{
|
|
||||||
\\"id\\": \\"testJS\\",
|
|
||||||
\\"label\\": \\"Testing JS\\"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\\"id\\": \\"other\\",
|
|
||||||
\\"label\\": \\"Other\\"
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}"
|
|
||||||
/>
|
/>
|
||||||
</DocumentFragment>
|
</React.Fragment>
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`App rendering an expression 2`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="Form"
|
|
||||||
data-props="{
|
|
||||||
\\"syntax\\": \\"js\\",
|
|
||||||
\\"expr\\": \\"test expression\\",
|
|
||||||
\\"syntaxList\\": [
|
|
||||||
{
|
|
||||||
\\"id\\": \\"testJS\\",
|
|
||||||
\\"label\\": \\"Testing JS\\"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\\"id\\": \\"other\\",
|
|
||||||
\\"label\\": \\"Other\\"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
data-component="Loader"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`App rendering an expression 3`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="Form"
|
|
||||||
data-props="{
|
|
||||||
\\"syntax\\": \\"js\\",
|
|
||||||
\\"expr\\": \\"test expression\\",
|
|
||||||
\\"syntaxList\\": [
|
|
||||||
{
|
|
||||||
\\"id\\": \\"testJS\\",
|
|
||||||
\\"label\\": \\"Testing JS\\"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\\"id\\": \\"other\\",
|
|
||||||
\\"label\\": \\"Other\\"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="FormActions"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
data-component="Render"
|
|
||||||
data-props="{
|
|
||||||
\\"data\\": \\"LAYOUT(PARSED(test expression))\\"
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`App rendering with an invalid syntax 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="Form"
|
|
||||||
data-props="{
|
|
||||||
\\"syntax\\": \\"invalid\\",
|
|
||||||
\\"expr\\": \\"\\",
|
|
||||||
\\"syntaxList\\": [
|
|
||||||
{
|
|
||||||
\\"id\\": \\"testJS\\",
|
|
||||||
\\"label\\": \\"Testing JS\\"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\\"id\\": \\"other\\",
|
|
||||||
\\"label\\": \\"Other\\"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`App rendering with an invalid syntax 2`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="Form"
|
|
||||||
data-props="{
|
|
||||||
\\"syntax\\": \\"invalid\\",
|
|
||||||
\\"expr\\": \\"test expression\\",
|
|
||||||
\\"syntaxList\\": [
|
|
||||||
{
|
|
||||||
\\"id\\": \\"testJS\\",
|
|
||||||
\\"label\\": \\"Testing JS\\"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\\"id\\": \\"other\\",
|
|
||||||
\\"label\\": \\"Other\\"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
data-component="Loader"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`App rendering with an invalid syntax 3`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="Form"
|
|
||||||
data-props="{
|
|
||||||
\\"syntax\\": \\"invalid\\",
|
|
||||||
\\"expr\\": \\"test expression\\",
|
|
||||||
\\"syntaxList\\": [
|
|
||||||
{
|
|
||||||
\\"id\\": \\"testJS\\",
|
|
||||||
\\"label\\": \\"Testing JS\\"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\\"id\\": \\"other\\",
|
|
||||||
\\"label\\": \\"Other\\"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
data-component="Message"
|
|
||||||
data-props="{
|
|
||||||
\\"type\\": \\"error\\",
|
|
||||||
\\"heading\\": \\"TRANSLATE(Render Failure)\\"
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
An error occurred while rendering the regular expression.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href="#retry"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Retry
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
+186
-120
@@ -1,165 +1,231 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withTranslation, Trans } from 'react-i18next';
|
import { translate } from 'react-i18next';
|
||||||
import * as Sentry from '@sentry/browser';
|
import URLSearchParams from 'url-search-params';
|
||||||
import URLSearchParams from '@ungap/url-search-params';
|
import Raven from 'raven-js';
|
||||||
|
|
||||||
|
import LoaderIcon from 'feather-icons/dist/icons/loader.svg';
|
||||||
|
|
||||||
|
import style from './style.css';
|
||||||
|
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
import FormActions from 'components/FormActions';
|
|
||||||
import Loader from 'components/Loader';
|
|
||||||
import Message from 'components/Message';
|
import Message from 'components/Message';
|
||||||
|
import InstallPrompt from 'components/InstallPrompt';
|
||||||
|
import { demoImage } from 'devel';
|
||||||
|
|
||||||
|
const syntaxes = {
|
||||||
|
js: 'JavaScript',
|
||||||
|
pcre: 'PCRE'
|
||||||
|
};
|
||||||
|
|
||||||
|
const toUrl = params => new URLSearchParams(params).toString();
|
||||||
|
|
||||||
class App extends React.PureComponent {
|
class App extends React.PureComponent {
|
||||||
static propTypes = {
|
state = {}
|
||||||
syntax: PropTypes.string.isRequired,
|
|
||||||
expr: PropTypes.string.isRequired,
|
|
||||||
permalinkUrl: PropTypes.string,
|
|
||||||
syntaxList: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
id: PropTypes.string,
|
|
||||||
label: PropTypes.string
|
|
||||||
})),
|
|
||||||
t: PropTypes.func.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
image = React.createRef()
|
||||||
loading: false,
|
|
||||||
loadingError: null,
|
|
||||||
render: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.expr) {
|
window.addEventListener('hashchange', this.handleHashChange);
|
||||||
this.handleRender();
|
window.addEventListener('beforeinstallprompt', this.handleInstallPrompt);
|
||||||
}
|
this.handleHashChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentWillUnmount() {
|
||||||
const { syntax, expr } = this.props;
|
window.removeEventListener('hashchange', this.handleHashChange);
|
||||||
|
window.removeEventListener('beforeinstallprompt', this.handleInstallPrompt);
|
||||||
if (syntax !== prevProps.syntax || expr !== prevProps.expr) {
|
|
||||||
this.handleRender();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit = ({ syntax, expr }) => {
|
async setSvgUrl() {
|
||||||
if (expr) {
|
try {
|
||||||
document.location.hash = new URLSearchParams({
|
const type = 'image/svg+xml';
|
||||||
syntax,
|
const blob = await this.image.current.svgUrl(type);
|
||||||
expr
|
|
||||||
}).toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleRender = async () => {
|
|
||||||
const { syntax, expr } = this.props;
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
svgUrl: {
|
||||||
loadingError: null,
|
url: URL.createObjectURL(blob),
|
||||||
render: {}
|
label: 'Download SVG',
|
||||||
|
filename: 'image.svg',
|
||||||
|
type
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPngUrl() {
|
||||||
|
try {
|
||||||
|
const type = 'image/png';
|
||||||
|
const blob = await this.image.current.pngUrl(type);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
pngUrl: {
|
||||||
|
url: URL.createObjectURL(blob),
|
||||||
|
label: 'Download PNG',
|
||||||
|
filename: 'image.png',
|
||||||
|
type
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSVGComponent() {
|
||||||
|
if (this.state.SVG) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
loading: true,
|
||||||
|
loadingFailed: false
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const SVG = await import(/* webpackChunkName: "render" */ 'components/SVG');
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
SVG: SVG.default,
|
||||||
|
loading: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Raven.captureException(e);
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
loadingFailed: e
|
||||||
|
});
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInstallPrompt = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
installPrompt: event
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = ({expr, syntax}) => {
|
||||||
|
if (expr) {
|
||||||
|
document.location.hash = toUrl({ syntax, expr });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHashChange = async () => {
|
||||||
|
const query = document.location.hash.slice(1);
|
||||||
|
const params = new URLSearchParams(query);
|
||||||
|
const { expr, syntax } = (() => {
|
||||||
|
if (params.get('syntax')) {
|
||||||
|
return {
|
||||||
|
syntax: params.get('syntax'),
|
||||||
|
expr: params.get('expr')
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Assuming old-style URL
|
||||||
|
return {
|
||||||
|
syntax: 'js',
|
||||||
|
expr: query
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
if (!expr) {
|
if (!expr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
|
||||||
loading: true
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const syntaxModule = await import(
|
await this.loadSVGComponent();
|
||||||
/* webpackChunkName: "render-[index]" */
|
console.log(syntax, expr); // eslint-disable-line no-console
|
||||||
`syntax/${ syntax }`
|
|
||||||
);
|
|
||||||
|
|
||||||
const exprData = syntaxModule.layout(syntaxModule.parse(expr));
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
image: demoImage,
|
||||||
render: {
|
permalinkUrl: document.location.toString(),
|
||||||
syntax,
|
syntax,
|
||||||
exprData,
|
expr
|
||||||
Component: syntaxModule.Render
|
}, async () => {
|
||||||
}
|
await this.image.current.doReflow();
|
||||||
|
this.setSvgUrl();
|
||||||
|
this.setPngUrl();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
Sentry.withScope(scope => {
|
console.error(e); // eslint-disable-line no-console
|
||||||
scope.setExtra('syntax', syntax);
|
|
||||||
Sentry.captureException(e);
|
|
||||||
});
|
|
||||||
this.setState({
|
|
||||||
loading: false,
|
|
||||||
loadingError: e
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRetry = event => {
|
handleRetry = async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.handleRender();
|
this.handleHashChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSvg = imageDetails => this.setState({ imageDetails });
|
handleInstallReject = () => {
|
||||||
|
this.setState({ installPrompt: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInstallAccept = async () => {
|
||||||
|
const { installPrompt } = this.state;
|
||||||
|
|
||||||
|
this.setState({ installPrompt: null });
|
||||||
|
installPrompt.prompt();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
syntax,
|
SVG,
|
||||||
expr,
|
|
||||||
permalinkUrl,
|
|
||||||
syntaxList,
|
|
||||||
t
|
|
||||||
} = this.props;
|
|
||||||
const {
|
|
||||||
loading,
|
loading,
|
||||||
loadingError,
|
loadingFailed,
|
||||||
imageDetails,
|
svgUrl,
|
||||||
render: {
|
pngUrl,
|
||||||
syntax: renderSyntax,
|
permalinkUrl,
|
||||||
exprData,
|
|
||||||
Component
|
|
||||||
}
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
|
|
||||||
const formProps = {
|
|
||||||
onSubmit: this.handleSubmit,
|
|
||||||
syntax,
|
syntax,
|
||||||
expr,
|
expr,
|
||||||
syntaxList
|
image,
|
||||||
};
|
installPrompt
|
||||||
const actionProps = {
|
} = this.state;
|
||||||
imageDetails,
|
const downloadUrls = [
|
||||||
permalinkUrl
|
svgUrl,
|
||||||
};
|
pngUrl
|
||||||
const renderProps = {
|
].filter(Boolean);
|
||||||
onRender: this.handleSvg,
|
|
||||||
data: exprData
|
|
||||||
};
|
|
||||||
|
|
||||||
const doRender = renderSyntax === syntax;
|
return <React.Fragment>
|
||||||
|
<Form
|
||||||
return <>
|
key={ toUrl({ expr, syntax }) }
|
||||||
<Form { ...formProps }>
|
syntaxes={ syntaxes }
|
||||||
{ doRender && <FormActions { ...actionProps } /> }
|
downloadUrls={ downloadUrls }
|
||||||
</Form>
|
permalinkUrl={ permalinkUrl }
|
||||||
|
syntax={ syntax }
|
||||||
{ loading && <Loader /> }
|
expr={ expr }
|
||||||
|
onSubmit={ this.handleSubmit }/>
|
||||||
{ loadingError && <Message type="error" heading={ t('Render Failure') }>
|
{
|
||||||
<p><Trans>
|
loading && <div className={ style.loader }>
|
||||||
An error occurred while rendering the regular expression.
|
<LoaderIcon />
|
||||||
</Trans></p>
|
<div className={ style.message }>Loading...</div>
|
||||||
<a href="#retry" onClick={ this.handleRetry }><Trans>Retry</Trans></a>
|
</div>
|
||||||
</Message> }
|
}
|
||||||
|
{
|
||||||
{ doRender && <Component { ...renderProps } /> }
|
loadingFailed && <Message type="error" heading="Render Failure">
|
||||||
</>;
|
An error occurred while rendering the regular expression. <a href="#retry" onClick={ this.handleRetry }>Retry</a>
|
||||||
|
</Message>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
image && <div className={ style.render }>
|
||||||
|
<SVG data={ image } ref={ this.image }/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
installPrompt && <InstallPrompt onAccept={ this.handleInstallAccept } onReject={ this.handleInstallReject } />
|
||||||
|
}
|
||||||
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
App.propTypes = {
|
||||||
|
t: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default translate()(App);
|
||||||
export { App };
|
export { App };
|
||||||
export default withTranslation()(App);
|
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
@import url('../../globals.module.css');
|
@import url('../../globals.css');
|
||||||
|
|
||||||
|
.render {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--color-white);
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: auto;
|
||||||
|
margin: var(--spacing-margin) 0;
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
display: block;
|
||||||
|
transform: scaleZ(1); /* Move to separate render layer in Chrome */
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: var(--spacing-margin) 0;
|
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
background: var(--color-white);
|
background: var(--color-white);
|
||||||
color: var(--color-black);
|
color: var(--color-black);
|
||||||
@@ -13,7 +26,6 @@
|
|||||||
& .message {
|
& .message {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
margin-top: 2rem;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -21,8 +33,8 @@
|
|||||||
& svg {
|
& svg {
|
||||||
display: block;
|
display: block;
|
||||||
transform: scaleZ(1); /* Move to separate render layer in Chrome */
|
transform: scaleZ(1); /* Move to separate render layer in Chrome */
|
||||||
width: 4rem;
|
width: 5rem;
|
||||||
height: 4rem;
|
height: 5rem;
|
||||||
stroke: var(--color-black);
|
stroke: var(--color-black);
|
||||||
animation: loader-spin 1s steps(8) infinite;
|
animation: loader-spin 1s steps(8) infinite;
|
||||||
|
|
||||||
@@ -1,90 +1,16 @@
|
|||||||
jest.mock('components/Form', () =>
|
jest.mock('components/SVG');
|
||||||
require('__mocks__/component-mock')('components/Form'));
|
|
||||||
jest.mock('components/FormActions', () =>
|
|
||||||
require('__mocks__/component-mock')('components/FormActions'));
|
|
||||||
jest.mock('components/Loader', () =>
|
|
||||||
require('__mocks__/component-mock')('components/Loader'));
|
|
||||||
jest.mock('components/Message', () =>
|
|
||||||
require('__mocks__/component-mock')('components/Message'));
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from 'react-testing-library';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import { mockT } from 'i18n';
|
|
||||||
import { App } from 'components/App';
|
import { App } from 'components/App';
|
||||||
|
import { translate } from '__mocks__/i18n';
|
||||||
jest.mock('syntax/js', () => ({
|
|
||||||
parse: expr => `PARSED(${ expr })`,
|
|
||||||
layout: parsed => `LAYOUT(${ parsed })`,
|
|
||||||
Render: require('__mocks__/component-mock').buildMock(function Render() {})
|
|
||||||
}));
|
|
||||||
|
|
||||||
const syntaxList = [
|
|
||||||
{ id: 'testJS', label: 'Testing JS' },
|
|
||||||
{ id: 'other', label: 'Other' }
|
|
||||||
];
|
|
||||||
const commonProps = { syntaxList, t: mockT };
|
|
||||||
|
|
||||||
describe('App', () => {
|
describe('App', () => {
|
||||||
test('rendering', () => {
|
test('rendering', () => {
|
||||||
const { asFragment } = render(
|
const component = shallow(
|
||||||
<App expr="" syntax="js" { ...commonProps } />
|
<App t={ translate }/>
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
|
||||||
|
|
||||||
test('rendering an expression', async () => {
|
|
||||||
const { asFragment, rerender } = render(
|
|
||||||
<App expr="" syntax="js" { ...commonProps } />
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
|
|
||||||
rerender(
|
|
||||||
<App expr="test expression" syntax="js" { ...commonProps } />
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
|
|
||||||
// Give a beat for module to load
|
|
||||||
await new Promise(resolve => setTimeout(resolve));
|
|
||||||
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rendering with an invalid syntax', async () => {
|
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
||||||
|
|
||||||
const { asFragment, rerender } = render(
|
|
||||||
<App expr="" syntax="invalid" { ...commonProps } />
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
|
|
||||||
rerender(
|
|
||||||
<App expr="test expression" syntax="invalid" { ...commonProps } />
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
|
|
||||||
// Give a beat for module to load
|
|
||||||
await new Promise(resolve => setTimeout(resolve));
|
|
||||||
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('removing rendered expression', async () => {
|
|
||||||
const { asFragment, rerender } = render(
|
|
||||||
<App expr="test expression" syntax="js" { ...commonProps } />
|
|
||||||
);
|
|
||||||
|
|
||||||
// Give a beat for module to load
|
|
||||||
await new Promise(resolve => setTimeout(resolve));
|
|
||||||
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
|
|
||||||
rerender(
|
|
||||||
<App expr="" syntax="js" { ...commonProps } />
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,30 +1,21 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Footer rendering 1`] = `
|
exports[`Footer rendering 1`] = `
|
||||||
<DocumentFragment>
|
<footer>
|
||||||
<footer
|
<ul>
|
||||||
class="footer"
|
|
||||||
>
|
|
||||||
<ul
|
|
||||||
class="list"
|
|
||||||
>
|
|
||||||
<li>
|
<li>
|
||||||
<span
|
<Trans>
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Created by
|
Created by
|
||||||
<a
|
<a
|
||||||
href="mailto:jeff.avallone@gmail.com"
|
href="mailto:jeff.avallone@gmail.com"
|
||||||
>
|
>
|
||||||
Jeff Avallone
|
Jeff Avallone
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</Trans>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span
|
<Trans
|
||||||
data-component="Trans"
|
i18nKey="Generated images licensed"
|
||||||
data-props="{}"
|
|
||||||
>
|
>
|
||||||
Generated images licensed:
|
Generated images licensed:
|
||||||
<a
|
<a
|
||||||
@@ -33,18 +24,17 @@ exports[`Footer rendering 1`] = `
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="TRANSLATE(Creative Commons CC-BY-3.0 License)"
|
alt="Creative Commons CC-BY-3.0 License"
|
||||||
src="cc-by.svg"
|
src="https://licensebuttons.net/l/by/3.0/80x15.png"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</Trans>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div
|
<div
|
||||||
class="buildId"
|
className="buildId"
|
||||||
>
|
>
|
||||||
abc-123
|
example build id
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://web.resource.org/cc/"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="80"
|
|
||||||
height="15"
|
|
||||||
id="svg2279"
|
|
||||||
sodipodi:version="0.32"
|
|
||||||
inkscape:version="0.45+devel"
|
|
||||||
version="1.0"
|
|
||||||
sodipodi:docname="by.svg"
|
|
||||||
inkscape:output_extension="org.inkscape.output.svg.inkscape">
|
|
||||||
<defs
|
|
||||||
id="defs2281">
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath3442">
|
|
||||||
<rect
|
|
||||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.92243534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="rect3444"
|
|
||||||
width="20.614058"
|
|
||||||
height="12.483703"
|
|
||||||
x="171.99832"
|
|
||||||
y="239.1203" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#999999"
|
|
||||||
borderopacity="1"
|
|
||||||
gridtolerance="10000"
|
|
||||||
guidetolerance="10"
|
|
||||||
objecttolerance="10"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="10.5125"
|
|
||||||
inkscape:cx="40"
|
|
||||||
inkscape:cy="7.5"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
width="80px"
|
|
||||||
height="15px"
|
|
||||||
showborder="true"
|
|
||||||
inkscape:showpageshadow="false"
|
|
||||||
inkscape:window-width="935"
|
|
||||||
inkscape:window-height="624"
|
|
||||||
inkscape:window-x="50"
|
|
||||||
inkscape:window-y="160" />
|
|
||||||
<metadata
|
|
||||||
id="metadata2284">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1">
|
|
||||||
<g
|
|
||||||
id="BY"
|
|
||||||
transform="matrix(0.9875019,0,0,0.9333518,-323.90064,-271.87688)">
|
|
||||||
<g
|
|
||||||
transform="translate(158,54)"
|
|
||||||
id="g3693">
|
|
||||||
<rect
|
|
||||||
y="237.86218"
|
|
||||||
x="170.5"
|
|
||||||
height="15"
|
|
||||||
width="80"
|
|
||||||
id="rect3695"
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.04161763;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
<rect
|
|
||||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.92243534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="rect3697"
|
|
||||||
width="77"
|
|
||||||
height="12"
|
|
||||||
x="172"
|
|
||||||
y="239.36218" />
|
|
||||||
<path
|
|
||||||
sodipodi:nodetypes="cccscc"
|
|
||||||
id="path3699"
|
|
||||||
d="M 171.99996,239.37505 L 171.99996,251.37505 L 192.33474,251.37505 C 193.64339,249.62474 194.52652,247.59057 194.52652,245.37505 C 194.52652,243.17431 193.65859,241.1179 192.36599,239.37505 L 171.99996,239.37505 z"
|
|
||||||
style="fill:#abb1aa;fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.46913578" />
|
|
||||||
<g
|
|
||||||
clip-path="url(#clipPath3442)"
|
|
||||||
transform="matrix(0.9612533,0,0,0.9612533,6.8341566,9.5069994)"
|
|
||||||
id="g3701">
|
|
||||||
<path
|
|
||||||
style="opacity:1;fill:#ffffff"
|
|
||||||
d="M 190.06417,245.36206 C 190.06667,249.25405 186.91326,252.41072 183.02153,252.41323 C 179.12979,252.41572 175.97262,249.26256 175.97036,245.3706 C 175.97036,245.36783 175.97036,245.36507 175.97036,245.36206 C 175.9681,241.47007 179.12126,238.3134 183.013,238.31113 C 186.90524,238.30864 190.06191,241.46181 190.06417,245.3538 C 190.06417,245.35628 190.06417,245.35929 190.06417,245.36206 z"
|
|
||||||
rx="22.939548"
|
|
||||||
type="arc"
|
|
||||||
cy="264.3577"
|
|
||||||
ry="22.939548"
|
|
||||||
cx="296.35416"
|
|
||||||
id="path3703" />
|
|
||||||
<path
|
|
||||||
style="opacity:1"
|
|
||||||
id="path3705"
|
|
||||||
d="M 188.74576,239.62226 C 190.30843,241.18492 191.08988,243.09869 191.08988,245.36206 C 191.08988,247.62592 190.32197,249.51913 188.78615,251.04165 C 187.15627,252.64521 185.22995,253.44672 183.00722,253.44672 C 180.81132,253.44672 178.91837,252.65172 177.32887,251.06174 C 175.73912,249.47198 174.94436,247.57226 174.94436,245.36206 C 174.94436,243.15235 175.73912,241.23908 177.32887,239.62226 C 178.87799,238.0591 180.77094,237.27764 183.00722,237.27764 C 185.2706,237.27764 187.18312,238.05909 188.74576,239.62226 z M 178.38093,240.67355 C 177.05978,242.008 176.39945,243.57116 176.39945,245.36429 C 176.39945,247.15694 177.05326,248.70682 178.36062,250.01393 C 179.66822,251.32153 181.22487,251.97509 183.03105,251.97509 C 184.83724,251.97509 186.40716,251.31502 187.74161,249.99412 C 189.0086,248.76725 189.64234,247.22467 189.64234,245.36429 C 189.64234,243.51799 188.99831,241.95084 187.71101,240.66354 C 186.42396,239.37649 184.86406,238.7327 183.03105,238.7327 C 181.19804,238.73271 179.64767,239.37975 178.38093,240.67355 z M 181.85761,244.57559 C 181.65573,244.13545 181.35354,243.91525 180.95051,243.91525 C 180.23802,243.91525 179.8819,244.39501 179.8819,245.35404 C 179.8819,246.31328 180.23802,246.79255 180.95051,246.79255 C 181.421,246.79255 181.75705,246.55908 181.95869,246.09111 L 182.94629,246.61701 C 182.47555,247.45339 181.76934,247.87168 180.82763,247.87168 C 180.10136,247.87168 179.51953,247.64899 179.08265,247.20409 C 178.64502,246.7587 178.42684,246.14477 178.42684,245.36206 C 178.42684,244.59313 178.65204,243.98271 179.10271,243.53056 C 179.55338,243.07838 180.11463,242.8524 180.7875,242.8524 C 181.78288,242.8524 182.49561,243.24465 182.92647,244.02835 L 181.85761,244.57559 z M 186.50398,244.57559 C 186.30184,244.13545 186.00567,243.91525 185.61517,243.91525 C 184.88839,243.91525 184.52474,244.39501 184.52474,245.35404 C 184.52474,246.31328 184.88839,246.79255 185.61517,246.79255 C 186.08642,246.79255 186.41644,246.55908 186.6048,246.09111 L 187.61447,246.61701 C 187.14448,247.45339 186.43926,247.87168 185.49931,247.87168 C 184.77403,247.87168 184.19346,247.64899 183.75683,247.20409 C 183.32096,246.7587 183.10254,246.14477 183.10254,245.36206 C 183.10254,244.59313 183.32422,243.98271 183.76737,243.53056 C 184.21026,243.07838 184.77404,242.8524 185.4592,242.8524 C 186.45282,242.8524 187.16455,243.24465 187.5939,244.02835 L 186.50398,244.57559 z" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<path
|
|
||||||
id="text3707"
|
|
||||||
d="M 357.4197,298.68502 C 357.66518,298.68503 357.85131,298.63145 357.9781,298.52427 C 358.10488,298.41711 358.16827,298.25904 358.16828,298.05007 C 358.16827,297.84377 358.10488,297.68704 357.9781,297.57987 C 357.85131,297.47003 357.66518,297.41511 357.4197,297.4151 L 356.55784,297.4151 L 356.55784,298.68502 L 357.4197,298.68502 M 357.4723,301.30928 C 357.78522,301.30928 358.0199,301.24363 358.17637,301.11235 C 358.33552,300.98108 358.4151,300.78282 358.4151,300.51758 C 358.4151,300.2577 358.33686,300.06346 358.18041,299.93486 C 358.02396,299.80358 357.78792,299.73795 357.4723,299.73794 L 356.55784,299.73794 L 356.55784,301.30928 L 357.4723,301.30928 M 358.92089,299.15121 C 359.25538,299.24766 359.51434,299.42582 359.69779,299.6857 C 359.88121,299.94558 359.97293,300.26439 359.97294,300.64216 C 359.97293,301.22086 359.776,301.6522 359.38217,301.9362 C 358.98833,302.22019 358.38947,302.36218 357.5856,302.36218 L 355.00001,302.36218 L 355.00001,296.36218 L 357.33878,296.36218 C 358.17771,296.36219 358.78466,296.48811 359.15962,296.73995 C 359.53727,296.9918 359.7261,297.39501 359.7261,297.94959 C 359.7261,298.24163 359.65732,298.49079 359.51975,298.69708 C 359.38217,298.9007 359.18255,299.05208 358.92089,299.15121 M 359.83746,296.36218 L 361.54096,296.36218 L 362.91671,298.50016 L 364.29245,296.36218 L 366,296.36218 L 363.69764,299.83439 L 363.69764,302.36218 L 362.13982,302.36218 L 362.13982,299.83439 L 359.83746,296.36218"
|
|
||||||
style="font-size:8.25858784px;font-style:normal;font-weight:bold;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:'Bitstream Vera Sans'" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 8.1 KiB |
@@ -1,38 +1,27 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { translate, Trans } from 'react-i18next';
|
||||||
import { useTranslation, Trans } from 'react-i18next';
|
|
||||||
|
|
||||||
import ccLogo from './cc-by.svg';
|
import style from './style.css';
|
||||||
|
|
||||||
import style from './style.module.css';
|
const Footer = () => (
|
||||||
|
<footer>
|
||||||
export const Footer = ({ buildId }) => {
|
<ul>
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return <footer className={ style.footer }>
|
|
||||||
<ul className={ style.list }>
|
|
||||||
<li>
|
<li>
|
||||||
<Trans>Created by <a
|
<Trans>Created by <a href="mailto:jeff.avallone@gmail.com">Jeff Avallone</a></Trans>
|
||||||
href="mailto:jeff.avallone@gmail.com">Jeff Avallone</a></Trans>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Trans>Generated images licensed: <a
|
<Trans i18nKey="Generated images licensed">
|
||||||
href="http://creativecommons.org/licenses/by/3.0/"
|
Generated images licensed: <a rel="license external noopener noreferrer" target="_blank" href="http://creativecommons.org/licenses/by/3.0/">
|
||||||
rel="license external noopener noreferrer"
|
<img
|
||||||
target="_blank">
|
alt="Creative Commons CC-BY-3.0 License"
|
||||||
<img src={ ccLogo }
|
src="https://licensebuttons.net/l/by/3.0/80x15.png" />
|
||||||
alt={ t('Creative Commons CC-BY-3.0 License') } />
|
</a>
|
||||||
</a></Trans>
|
</Trans>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className={ style.buildId }>
|
<div className={ style.buildId }>{ process.env.BUILD_ID }</div>
|
||||||
{ buildId }
|
</footer>
|
||||||
</div>
|
);
|
||||||
</footer>;
|
|
||||||
};
|
|
||||||
|
|
||||||
Footer.propTypes = {
|
export default translate()(Footer);
|
||||||
buildId: PropTypes.string.isRequired
|
export { Footer };
|
||||||
};
|
|
||||||
|
|
||||||
export default Footer;
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@import url('../../globals.module.css');
|
@import url('../../globals.css');
|
||||||
|
|
||||||
.footer {
|
footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin: var(--spacing-margin) 0;
|
margin: var(--spacing-margin) 0;
|
||||||
@@ -9,18 +9,19 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& ul {
|
||||||
|
@apply --inline-list;
|
||||||
|
@apply --with-separator-left;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
& img {
|
& img {
|
||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
& .buildId {
|
||||||
composes: inline-list with-separator-left;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buildId {
|
|
||||||
color: color(var(--color-brown) blend(var(--color-tan) 25%));
|
color: color(var(--color-brown) blend(var(--color-tan) 25%));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from 'react-testing-library';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import Footer from 'components/Footer';
|
import { Footer } from 'components/Footer';
|
||||||
|
|
||||||
describe('Footer', () => {
|
describe('Footer', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env.BUILD_ID = 'example build id';
|
||||||
|
});
|
||||||
|
|
||||||
test('rendering', () => {
|
test('rendering', () => {
|
||||||
const { asFragment } = render(
|
const component = shallow(
|
||||||
<Footer buildId="abc-123" />
|
<Footer/>
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,54 +1,194 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Form rendering 1`] = `
|
exports[`Form rendering 1`] = `
|
||||||
<DocumentFragment>
|
|
||||||
<div
|
<div
|
||||||
class="form"
|
className="form"
|
||||||
data-requires-js="true"
|
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
data-testid="form"
|
onSubmit={[Function]}
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
data-testid="expr-input"
|
autoFocus={true}
|
||||||
name="expr"
|
name="expr"
|
||||||
placeholder="TRANSLATE(Enter regular expression to display)"
|
onChange={[Function]}
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
placeholder="translate(Enter regular expression to display)"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<span
|
<Trans>
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Display
|
Display
|
||||||
</span>
|
</Trans>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
class="select"
|
className="select"
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
data-testid="syntax-select"
|
|
||||||
name="syntax"
|
name="syntax"
|
||||||
|
onChange={[Function]}
|
||||||
|
value="js"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
value="testJS"
|
key="js"
|
||||||
|
value="js"
|
||||||
>
|
>
|
||||||
TRANSLATE(Testing JS)
|
Javascript
|
||||||
</option>
|
</option>
|
||||||
<option
|
<option
|
||||||
value="other"
|
key="pcre"
|
||||||
|
value="pcre"
|
||||||
>
|
>
|
||||||
TRANSLATE(Other)
|
PCRE
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<span
|
<SvgMock />
|
||||||
data-component="ChevronsDown"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
Actions
|
<ul
|
||||||
|
className="actions"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Form rendering with download URLs 1`] = `
|
||||||
|
<div
|
||||||
|
className="form"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
autoFocus={true}
|
||||||
|
name="expr"
|
||||||
|
onChange={[Function]}
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
placeholder="translate(Enter regular expression to display)"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<Trans>
|
||||||
|
Display
|
||||||
|
</Trans>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className="select"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
name="syntax"
|
||||||
|
onChange={[Function]}
|
||||||
|
value="js"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
key="js"
|
||||||
|
value="js"
|
||||||
|
>
|
||||||
|
Javascript
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
key="pcre"
|
||||||
|
value="pcre"
|
||||||
|
>
|
||||||
|
PCRE
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<SvgMock />
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
className="actions"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
key="0"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
download="image.svg"
|
||||||
|
href="#svg"
|
||||||
|
type="image/svg+xml"
|
||||||
|
>
|
||||||
|
<SvgMock />
|
||||||
|
<Trans>
|
||||||
|
Download SVG
|
||||||
|
</Trans>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
key="1"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
download="image.png"
|
||||||
|
href="#png"
|
||||||
|
type="image/png"
|
||||||
|
>
|
||||||
|
<SvgMock />
|
||||||
|
<Trans>
|
||||||
|
Download PNG
|
||||||
|
</Trans>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Form rendering with permalink URL 1`] = `
|
||||||
|
<div
|
||||||
|
className="form"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
autoFocus={true}
|
||||||
|
name="expr"
|
||||||
|
onChange={[Function]}
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
placeholder="translate(Enter regular expression to display)"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<Trans>
|
||||||
|
Display
|
||||||
|
</Trans>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className="select"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
name="syntax"
|
||||||
|
onChange={[Function]}
|
||||||
|
value="js"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
key="js"
|
||||||
|
value="js"
|
||||||
|
>
|
||||||
|
Javascript
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
key="pcre"
|
||||||
|
value="pcre"
|
||||||
|
>
|
||||||
|
PCRE
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<SvgMock />
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
className="actions"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="#permalink"
|
||||||
|
>
|
||||||
|
<SvgMock />
|
||||||
|
<Trans>
|
||||||
|
Permalink
|
||||||
|
</Trans>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,73 +1,105 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation, Trans } from 'react-i18next';
|
import { translate, Trans } from 'react-i18next';
|
||||||
|
|
||||||
import ExpandIcon from 'react-feather/dist/icons/chevrons-down';
|
import DownloadIcon from 'feather-icons/dist/icons/download.svg';
|
||||||
|
import LinkIcon from 'feather-icons/dist/icons/link.svg';
|
||||||
|
import ExpandIcon from 'feather-icons/dist/icons/chevrons-down.svg';
|
||||||
|
|
||||||
import style from './style.module.css';
|
import style from './style.css';
|
||||||
|
|
||||||
const Form = ({ syntaxList, children, onSubmit, ...props }) => {
|
class Form extends React.PureComponent {
|
||||||
const { t } = useTranslation();
|
state = {
|
||||||
const [ expr, exprUpdate ] = useState(props.expr);
|
expr: this.props.expr,
|
||||||
const [ syntax, syntaxUpdate ] = useState(props.syntax);
|
syntax: this.props.syntax || Object.keys(this.props.syntaxes)[0]
|
||||||
|
|
||||||
const handleExprChange = useCallback(event => {
|
|
||||||
exprUpdate(event.target.value);
|
|
||||||
}, [exprUpdate]);
|
|
||||||
const handleSyntaxChange = useCallback(event => {
|
|
||||||
syntaxUpdate(event.target.value);
|
|
||||||
}, [syntaxUpdate]);
|
|
||||||
const handleSubmit = useCallback(event => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
onSubmit({ expr, syntax });
|
|
||||||
}, [expr, syntax, onSubmit]);
|
|
||||||
const handleKeyPress = useCallback(event => {
|
|
||||||
if (event.charCode === 13 && event.shiftKey) {
|
|
||||||
handleSubmit(event);
|
|
||||||
}
|
}
|
||||||
}, [handleSubmit]);
|
|
||||||
|
|
||||||
return <div className={ style.form } data-requires-js>
|
handleSubmit = event => {
|
||||||
<form data-testid="form" onSubmit={ handleSubmit }>
|
event.preventDefault();
|
||||||
|
this.props.onSubmit.call(this, {
|
||||||
|
expr: this.state.expr,
|
||||||
|
syntax: this.state.syntax
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyPress = event => {
|
||||||
|
if (event.charCode === 13 && event.shiftKey) {
|
||||||
|
this.handleSubmit(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = event => this.setState({ [event.target.name]: event.target.value })
|
||||||
|
|
||||||
|
permalinkAction() {
|
||||||
|
const { permalinkUrl } = this.props;
|
||||||
|
|
||||||
|
if (!permalinkUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <li>
|
||||||
|
<a href={ permalinkUrl }><LinkIcon/><Trans>Permalink</Trans></a>
|
||||||
|
</li>;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadActions() {
|
||||||
|
const { downloadUrls } = this.props;
|
||||||
|
|
||||||
|
if (!downloadUrls) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloadUrls.map(({ url, filename, type, label }, i) => <li key={ i }>
|
||||||
|
<a href={ url } download={ filename } type={ type }>
|
||||||
|
<DownloadIcon/><Trans>{ label }</Trans>
|
||||||
|
</a>
|
||||||
|
</li>);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { syntaxes, t } = this.props;
|
||||||
|
const { expr, syntax } = this.state;
|
||||||
|
|
||||||
|
return <div className={ style.form }>
|
||||||
|
<form onSubmit={ this.handleSubmit }>
|
||||||
<textarea
|
<textarea
|
||||||
data-testid="expr-input"
|
|
||||||
name="expr"
|
name="expr"
|
||||||
value={ expr }
|
value={ expr }
|
||||||
onKeyPress={ handleKeyPress }
|
onKeyPress={ this.handleKeyPress }
|
||||||
onChange={ handleExprChange }
|
onChange={ this.handleChange }
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder={ t('Enter regular expression to display') }></textarea>
|
placeholder={ t('Enter regular expression to display') }></textarea>
|
||||||
<button type="submit"><Trans>Display</Trans></button>
|
<button type="submit"><Trans>Display</Trans></button>
|
||||||
<div className={ style.select }>
|
<div className={ style.select }>
|
||||||
<select
|
<select
|
||||||
data-testid="syntax-select"
|
|
||||||
name="syntax"
|
name="syntax"
|
||||||
value={ syntax }
|
value={ syntax }
|
||||||
onChange={ handleSyntaxChange } >
|
onChange={ this.handleChange }>
|
||||||
{ syntaxList.map(({ id, label }) => (
|
{ Object.keys(syntaxes).map(id => (
|
||||||
<option value={ id } key={ id }>{ t(label) }</option>
|
<option value={ id } key={ id }>{ syntaxes[id] }</option>
|
||||||
)) }
|
)) }
|
||||||
</select>
|
</select>
|
||||||
<ExpandIcon/>
|
<ExpandIcon/>
|
||||||
</div>
|
</div>
|
||||||
{ children }
|
<ul className={ style.actions }>
|
||||||
|
{ this.downloadActions() }
|
||||||
|
{ this.permalinkAction() }
|
||||||
|
</ul>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>;
|
</div>;
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Form.propTypes = {
|
Form.propTypes = {
|
||||||
expr: PropTypes.string,
|
expr: PropTypes.string,
|
||||||
syntax: PropTypes.string,
|
syntax: PropTypes.string,
|
||||||
syntaxList: PropTypes.arrayOf(PropTypes.shape({
|
syntaxes: PropTypes.object,
|
||||||
id: PropTypes.string,
|
onSubmit: PropTypes.func,
|
||||||
label: PropTypes.string
|
permalinkUrl: PropTypes.string,
|
||||||
})),
|
downloadUrls: PropTypes.array,
|
||||||
onSubmit: PropTypes.func.isRequired,
|
t: PropTypes.func
|
||||||
children: PropTypes.oneOfType([
|
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
|
||||||
PropTypes.node
|
|
||||||
])
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Form;
|
export default translate()(Form);
|
||||||
|
export { Form };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import url('../../globals.module.css');
|
@import url('../../globals.css');
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--control-gradient: var(--color-green) var(--gradient-green);
|
--control-gradient: var(--color-green) var(--gradient-green);
|
||||||
@@ -44,6 +44,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select {
|
.actions {
|
||||||
composes: fancy-select;
|
@apply --list-reset;
|
||||||
|
margin-top: var(--spacing-margin);
|
||||||
|
|
||||||
|
@media (min-width: 700px) {
|
||||||
|
@apply --inline-list;
|
||||||
|
@apply --with-separator-left;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
@apply --fancy-select;
|
||||||
}
|
}
|
||||||
+57
-44
@@ -1,80 +1,93 @@
|
|||||||
jest.mock('react-feather/dist/icons/chevrons-down', () =>
|
|
||||||
require('__mocks__/component-mock')(
|
|
||||||
'react-feather/dist/icons/chevrons-down'));
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, fireEvent } from 'react-testing-library';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import Form from 'components/Form';
|
import { Form } from 'components/Form';
|
||||||
|
import { translate } from '__mocks__/i18n';
|
||||||
|
|
||||||
const syntaxList = [
|
const syntaxes = {
|
||||||
{ id: 'testJS', label: 'Testing JS' },
|
js: 'Javascript',
|
||||||
{ id: 'other', label: 'Other' }
|
pcre: 'PCRE'
|
||||||
];
|
};
|
||||||
const commonProps = { syntaxList };
|
|
||||||
|
|
||||||
describe('Form', () => {
|
describe('Form', () => {
|
||||||
test('rendering', () => {
|
test('rendering', () => {
|
||||||
const { asFragment } = render(
|
const component = shallow(
|
||||||
<Form onSubmit={ jest.fn() } { ...commonProps }>
|
<Form t={ translate } syntaxes={ syntaxes }/>
|
||||||
Actions
|
|
||||||
</Form>
|
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rendering with download URLs', () => {
|
||||||
|
const downloadUrls = [
|
||||||
|
{ url: '#svg', filename: 'image.svg', type: 'image/svg+xml', label: 'Download SVG' },
|
||||||
|
{ url: '#png', filename: 'image.png', type: 'image/png', label: 'Download PNG' }
|
||||||
|
];
|
||||||
|
const component = shallow(
|
||||||
|
<Form t={ translate } syntaxes={ syntaxes } downloadUrls={ downloadUrls }/>
|
||||||
|
);
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rendering with permalink URL', () => {
|
||||||
|
const permalinkUrl = '#permalink';
|
||||||
|
const component = shallow(
|
||||||
|
<Form t={ translate } syntaxes={ syntaxes } permalinkUrl={ permalinkUrl }/>
|
||||||
|
);
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('submitting expression', () => {
|
describe('submitting expression', () => {
|
||||||
test('submitting form', () => {
|
test('submitting form', () => {
|
||||||
const onSubmit = jest.fn();
|
const onSubmit = jest.fn();
|
||||||
const { getByTestId } = render(
|
const component = shallow(
|
||||||
<Form onSubmit={ onSubmit } { ...commonProps } />
|
<Form t={ translate } syntaxes={ syntaxes } onSubmit={ onSubmit }/>
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.change(getByTestId('expr-input'), {
|
const exprInput = component.find('[name="expr"]');
|
||||||
target: { value: 'Test expression' }
|
const syntaxInput = component.find('[name="syntax"]');
|
||||||
});
|
exprInput.simulate('change', { target: { name: 'expr', value: 'Test expression' } });
|
||||||
fireEvent.change(getByTestId('syntax-select'), {
|
syntaxInput.simulate('change', { target: { name: 'syntax', value: 'test' } });
|
||||||
target: { value: 'other' }
|
|
||||||
});
|
|
||||||
|
|
||||||
const event = new Event('submit');
|
const eventObj = { preventDefault: jest.fn() };
|
||||||
jest.spyOn(event, 'preventDefault');
|
component.find('form').simulate('submit', eventObj);
|
||||||
|
|
||||||
fireEvent(getByTestId('form'), event);
|
expect(eventObj.preventDefault).toHaveBeenCalled();
|
||||||
|
|
||||||
expect(event.preventDefault).toHaveBeenCalled();
|
|
||||||
expect(onSubmit).toHaveBeenCalledWith({
|
expect(onSubmit).toHaveBeenCalledWith({
|
||||||
expr: 'Test expression',
|
expr: 'Test expression',
|
||||||
syntax: 'other'
|
syntax: 'test'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('submitting form with Shift+Enter', () => {
|
test('submitting form with Shift+Enter', () => {
|
||||||
const onSubmit = jest.fn();
|
const component = shallow(
|
||||||
const { getByTestId } = render(
|
<Form t={ translate } syntaxes={ syntaxes } onSubmit={ Function.prototype }/>
|
||||||
<Form onSubmit={ onSubmit } { ...commonProps } />
|
|
||||||
);
|
);
|
||||||
|
const form = component.instance();
|
||||||
fireEvent.keyPress(getByTestId('expr-input'), {
|
const eventObj = {
|
||||||
|
preventDefault: Function.prototype,
|
||||||
charCode: 13,
|
charCode: 13,
|
||||||
shiftKey: true
|
shiftKey: true
|
||||||
});
|
};
|
||||||
|
jest.spyOn(form, 'handleSubmit');
|
||||||
|
component.find('textarea').simulate('keypress', eventObj);
|
||||||
|
|
||||||
expect(onSubmit).toHaveBeenCalled();
|
expect(form.handleSubmit).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('not submitting with just Enter', () => {
|
test('not submitting with just Enter', () => {
|
||||||
const onSubmit = jest.fn();
|
const component = shallow(
|
||||||
const { getByTestId } = render(
|
<Form t={ translate } syntaxes={ syntaxes } onSubmit={ Function.protoytpe }/>
|
||||||
<Form onSubmit={ onSubmit } { ...commonProps } />
|
|
||||||
);
|
);
|
||||||
|
const form = component.instance();
|
||||||
fireEvent.keyPress(getByTestId('expr-input'), {
|
const eventObj = {
|
||||||
|
preventDefault: Function.prototype,
|
||||||
charCode: 13,
|
charCode: 13,
|
||||||
shiftKey: false
|
shiftKey: false
|
||||||
});
|
};
|
||||||
|
jest.spyOn(form, 'handleSubmit');
|
||||||
|
component.find('textarea').simulate('keypress', eventObj);
|
||||||
|
|
||||||
expect(onSubmit).not.toHaveBeenCalled();
|
expect(form.handleSubmit).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`FormActions rendering 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<ul
|
|
||||||
class="actions"
|
|
||||||
/>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`FormActions rendering with a permalink 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<ul
|
|
||||||
class="actions"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="http://example.com"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Link"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Permalink
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import React, { useState, useCallback, useEffect } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation, Trans } from 'react-i18next';
|
|
||||||
|
|
||||||
import DownloadIcon from 'react-feather/dist/icons/download';
|
|
||||||
import LinkIcon from 'react-feather/dist/icons/link';
|
|
||||||
|
|
||||||
import style from './style.module.css';
|
|
||||||
|
|
||||||
import { createPngLink, createSvgLink } from './links';
|
|
||||||
|
|
||||||
const downloadLink = (link, t) => {
|
|
||||||
const { url, filename, type, label } = link;
|
|
||||||
return <li>
|
|
||||||
<a href={ url } download={ filename } type={ type }>
|
|
||||||
<DownloadIcon />{ t(label) }
|
|
||||||
</a>
|
|
||||||
</li>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FormActions = ({
|
|
||||||
permalinkUrl,
|
|
||||||
imageDetails
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [svgLink, setSvgLink] = useState(null);
|
|
||||||
const [pngLink, setPngLink] = useState(null);
|
|
||||||
|
|
||||||
const generateDownloadLinks = useCallback(async () => {
|
|
||||||
const { svg, width, height } = imageDetails;
|
|
||||||
|
|
||||||
setSvgLink(await createSvgLink({ svg }));
|
|
||||||
setPngLink(await createPngLink({ svg, width, height }));
|
|
||||||
}, [setSvgLink, setPngLink, imageDetails]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (imageDetails && imageDetails.svg) {
|
|
||||||
generateDownloadLinks();
|
|
||||||
}
|
|
||||||
}, [imageDetails]);
|
|
||||||
|
|
||||||
return <ul className={ style.actions }>
|
|
||||||
{ pngLink && downloadLink(pngLink, t) }
|
|
||||||
{ svgLink && downloadLink(svgLink, t) }
|
|
||||||
{ permalinkUrl && <li>
|
|
||||||
<a href={ permalinkUrl }><LinkIcon /><Trans>Permalink</Trans></a>
|
|
||||||
</li> }
|
|
||||||
</ul>;
|
|
||||||
};
|
|
||||||
|
|
||||||
FormActions.propTypes = {
|
|
||||||
permalinkUrl: PropTypes.string,
|
|
||||||
imageDetails: PropTypes.shape({
|
|
||||||
svg: PropTypes.string,
|
|
||||||
width: PropTypes.number,
|
|
||||||
height: PropTypes.number
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormActions;
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
const createSvgLink = async ({ svg }) => {
|
|
||||||
try {
|
|
||||||
const type = 'image/svg+xml';
|
|
||||||
const blob = new Blob([svg], { type });
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: URL.createObjectURL(blob),
|
|
||||||
label: 'Download SVG',
|
|
||||||
filename: 'image.svg',
|
|
||||||
type
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error(e); // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createPngLink = async ({ svg, width, height }) => {
|
|
||||||
try {
|
|
||||||
const type = 'image/png';
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
const loader = new Image();
|
|
||||||
|
|
||||||
loader.width = canvas.width = width * 2;
|
|
||||||
loader.height = canvas.height = height * 2;
|
|
||||||
|
|
||||||
await new Promise(resolve => {
|
|
||||||
loader.onload = resolve;
|
|
||||||
loader.src = 'data:image/svg+xml,' + encodeURIComponent(svg);
|
|
||||||
});
|
|
||||||
|
|
||||||
context.drawImage(loader, 0, 0, loader.width, loader.height);
|
|
||||||
|
|
||||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, type));
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: URL.createObjectURL(blob),
|
|
||||||
label: 'Download PNG',
|
|
||||||
filename: 'image.png',
|
|
||||||
type
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error(e); // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export { createSvgLink, createPngLink };
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
@import url('../../globals.module.css');
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
composes: inline-list with-separator-left;
|
|
||||||
float: right;
|
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
& li {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& svg {
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
jest.mock('./links');
|
|
||||||
jest.mock('react-feather/dist/icons/download', () =>
|
|
||||||
require('__mocks__/component-mock')(
|
|
||||||
'react-feather/dist/icons/download'));
|
|
||||||
jest.mock('react-feather/dist/icons/link', () =>
|
|
||||||
require('__mocks__/component-mock')(
|
|
||||||
'react-feather/dist/icons/link'));
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { render } from 'react-testing-library';
|
|
||||||
|
|
||||||
import FormActions from 'components/FormActions';
|
|
||||||
import { createPngLink, createSvgLink } from './links';
|
|
||||||
|
|
||||||
createPngLink.mockResolvedValue({
|
|
||||||
url: 'http://example.com/image.png',
|
|
||||||
filename: 'image.png',
|
|
||||||
type: 'image/png',
|
|
||||||
label: 'Example PNG Link'
|
|
||||||
});
|
|
||||||
createSvgLink.mockResolvedValue({
|
|
||||||
url: 'http://example.com/image.svg',
|
|
||||||
filename: 'image.svg',
|
|
||||||
type: 'image/svg+xml',
|
|
||||||
label: 'Example SVG Link'
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('FormActions', () => {
|
|
||||||
test('rendering', () => {
|
|
||||||
const { asFragment } = render(
|
|
||||||
<FormActions/>
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rendering with a permalink', () => {
|
|
||||||
const { asFragment } = render(
|
|
||||||
<FormActions permalinkUrl="http://example.com" />
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,400 +1,8 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Header opening the Privacy Policy modal 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="Modal"
|
|
||||||
data-props="{
|
|
||||||
\\"isOpen\\": true
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="PrivacyPolicy"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<header
|
|
||||||
class="header"
|
|
||||||
>
|
|
||||||
<h1>
|
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
>
|
|
||||||
Regexper
|
|
||||||
</a>
|
|
||||||
</h1>
|
|
||||||
<ul
|
|
||||||
class="list"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://gitlab.com/javallone/regexper-static"
|
|
||||||
rel="external noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Gitlab"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Source on GitLab
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
data-testid="privacy-link"
|
|
||||||
href="/privacy"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Privacy Policy
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span
|
|
||||||
data-component="InstallPrompt"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
data-requires-js="true"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="LocaleSwitcher"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</header>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Header opening the Privacy Policy modal while holding alt key 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="Modal"
|
|
||||||
data-props="{
|
|
||||||
\\"isOpen\\": false
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="PrivacyPolicy"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<header
|
|
||||||
class="header"
|
|
||||||
>
|
|
||||||
<h1>
|
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
>
|
|
||||||
Regexper
|
|
||||||
</a>
|
|
||||||
</h1>
|
|
||||||
<ul
|
|
||||||
class="list"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://gitlab.com/javallone/regexper-static"
|
|
||||||
rel="external noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Gitlab"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Source on GitLab
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
data-testid="privacy-link"
|
|
||||||
href="/privacy"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Privacy Policy
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span
|
|
||||||
data-component="InstallPrompt"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
data-requires-js="true"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="LocaleSwitcher"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</header>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Header opening the Privacy Policy modal while holding ctrl key 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="Modal"
|
|
||||||
data-props="{
|
|
||||||
\\"isOpen\\": false
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="PrivacyPolicy"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<header
|
|
||||||
class="header"
|
|
||||||
>
|
|
||||||
<h1>
|
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
>
|
|
||||||
Regexper
|
|
||||||
</a>
|
|
||||||
</h1>
|
|
||||||
<ul
|
|
||||||
class="list"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://gitlab.com/javallone/regexper-static"
|
|
||||||
rel="external noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Gitlab"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Source on GitLab
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
data-testid="privacy-link"
|
|
||||||
href="/privacy"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Privacy Policy
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span
|
|
||||||
data-component="InstallPrompt"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
data-requires-js="true"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="LocaleSwitcher"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</header>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Header opening the Privacy Policy modal while holding meta key 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="Modal"
|
|
||||||
data-props="{
|
|
||||||
\\"isOpen\\": false
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="PrivacyPolicy"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<header
|
|
||||||
class="header"
|
|
||||||
>
|
|
||||||
<h1>
|
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
>
|
|
||||||
Regexper
|
|
||||||
</a>
|
|
||||||
</h1>
|
|
||||||
<ul
|
|
||||||
class="list"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://gitlab.com/javallone/regexper-static"
|
|
||||||
rel="external noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Gitlab"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Source on GitLab
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
data-testid="privacy-link"
|
|
||||||
href="/privacy"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Privacy Policy
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span
|
|
||||||
data-component="InstallPrompt"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
data-requires-js="true"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="LocaleSwitcher"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</header>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Header opening the Privacy Policy modal while holding shift key 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="Modal"
|
|
||||||
data-props="{
|
|
||||||
\\"isOpen\\": false
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="PrivacyPolicy"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<header
|
|
||||||
class="header"
|
|
||||||
>
|
|
||||||
<h1>
|
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
>
|
|
||||||
Regexper
|
|
||||||
</a>
|
|
||||||
</h1>
|
|
||||||
<ul
|
|
||||||
class="list"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://gitlab.com/javallone/regexper-static"
|
|
||||||
rel="external noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Gitlab"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Source on GitLab
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
data-testid="privacy-link"
|
|
||||||
href="/privacy"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Privacy Policy
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span
|
|
||||||
data-component="InstallPrompt"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
data-requires-js="true"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="LocaleSwitcher"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</header>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Header rendering 1`] = `
|
exports[`Header rendering 1`] = `
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="Modal"
|
|
||||||
data-props="{
|
|
||||||
\\"isOpen\\": false
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="PrivacyPolicy"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<header
|
<header
|
||||||
class="header"
|
className="header"
|
||||||
data-banner="testing"
|
data-banner="testing"
|
||||||
>
|
>
|
||||||
<h1>
|
<h1>
|
||||||
@@ -404,131 +12,31 @@ exports[`Header rendering 1`] = `
|
|||||||
Regexper
|
Regexper
|
||||||
</a>
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
<ul
|
<ul>
|
||||||
class="list"
|
|
||||||
>
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="https://gitlab.com/javallone/regexper-static"
|
href="https://gitlab.com/javallone/regexper-static"
|
||||||
rel="external noopener noreferrer"
|
rel="external noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span
|
<SvgMock />
|
||||||
data-component="Gitlab"
|
<Trans>
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Source on GitLab
|
Source on GitLab
|
||||||
</span>
|
</Trans>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
data-testid="privacy-link"
|
href="/privacy.html"
|
||||||
href="/privacy"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
>
|
||||||
|
<Trans>
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</span>
|
</Trans>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span
|
<Translate(LocaleSwitcher) />
|
||||||
data-component="InstallPrompt"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
data-requires-js="true"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="LocaleSwitcher"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</header>
|
</header>
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Header rendering with no banner 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="Modal"
|
|
||||||
data-props="{
|
|
||||||
\\"isOpen\\": false
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="PrivacyPolicy"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<header
|
|
||||||
class="header"
|
|
||||||
>
|
|
||||||
<h1>
|
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
>
|
|
||||||
Regexper
|
|
||||||
</a>
|
|
||||||
</h1>
|
|
||||||
<ul
|
|
||||||
class="list"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://gitlab.com/javallone/regexper-static"
|
|
||||||
rel="external noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Gitlab"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Source on GitLab
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
data-testid="privacy-link"
|
|
||||||
href="/privacy"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Privacy Policy
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span
|
|
||||||
data-component="InstallPrompt"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
data-requires-js="true"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="LocaleSwitcher"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</header>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,77 +1,28 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { translate, Trans } from 'react-i18next';
|
||||||
import Modal from 'react-modal';
|
|
||||||
import { Link } from 'gatsby';
|
|
||||||
import { Trans } from 'react-i18next';
|
|
||||||
|
|
||||||
import GitlabIcon from 'react-feather/dist/icons/gitlab';
|
import style from './style.css';
|
||||||
|
import GitlabIcon from 'feather-icons/dist/icons/gitlab.svg';
|
||||||
|
|
||||||
import LocaleSwitcher from 'components/LocaleSwitcher';
|
import LocaleSwitcher from 'components/LocaleSwitcher';
|
||||||
import InstallPrompt from 'components/InstallPrompt';
|
|
||||||
import PrivacyPolicy from 'components/PrivacyPolicy';
|
|
||||||
|
|
||||||
import style from './style.module.css';
|
const Header = () => (
|
||||||
|
<header className={ style.header } data-banner={ process.env.BANNER }>
|
||||||
const Header = ({ banner }) => {
|
|
||||||
const [ showModal, updateShowModal] = useState(false);
|
|
||||||
const handleClose = useCallback(() => {
|
|
||||||
updateShowModal(false);
|
|
||||||
}, [updateShowModal]);
|
|
||||||
const handleOpen = useCallback(event => {
|
|
||||||
if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
updateShowModal(true);
|
|
||||||
}, [updateShowModal]);
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<Modal
|
|
||||||
isOpen={ showModal }
|
|
||||||
onRequestClose={ handleClose }>
|
|
||||||
<PrivacyPolicy onClose={ handleClose } />
|
|
||||||
</Modal>
|
|
||||||
<header
|
|
||||||
className={ style.header }
|
|
||||||
data-banner={ banner || null }>
|
|
||||||
<h1>
|
<h1>
|
||||||
<Link to="/">Regexper</Link>
|
<a href="/">Regexper</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<ul className={ style.list }>
|
<ul>
|
||||||
|
<li><a href="https://gitlab.com/javallone/regexper-static" rel="external noopener noreferrer" target="_blank">
|
||||||
|
<GitlabIcon/><Trans>Source on GitLab</Trans>
|
||||||
|
</a></li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://gitlab.com/javallone/regexper-static"
|
<a href="/privacy.html"><Trans>Privacy Policy</Trans></a>
|
||||||
rel="external noopener noreferrer"
|
|
||||||
target="_blank">
|
|
||||||
<GitlabIcon />
|
|
||||||
<Trans>Source on GitLab</Trans>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link to="/privacy"
|
|
||||||
data-testid="privacy-link"
|
|
||||||
onClick={ handleOpen }
|
|
||||||
>
|
|
||||||
<Trans>Privacy Policy</Trans>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<InstallPrompt />
|
|
||||||
</li>
|
|
||||||
<li data-requires-js>
|
|
||||||
<LocaleSwitcher />
|
|
||||||
</li>
|
</li>
|
||||||
|
<li><LocaleSwitcher /></li>
|
||||||
</ul>
|
</ul>
|
||||||
</header>
|
</header>
|
||||||
</>;
|
);
|
||||||
};
|
|
||||||
|
|
||||||
Header.propTypes = {
|
export default translate()(Header);
|
||||||
banner: PropTypes.oneOfType([
|
export { Header };
|
||||||
PropTypes.bool,
|
|
||||||
PropTypes.string
|
|
||||||
]).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import url('../../globals.module.css');
|
@import url('../../globals.css');
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
margin: 0 calc(-1 * var(--content-margin)) var(--spacing-margin) calc(-1 * var(--content-margin));
|
margin: 0 calc(-1 * var(--content-margin)) var(--spacing-margin) calc(-1 * var(--content-margin));
|
||||||
position: relative;
|
position: relative;
|
||||||
color: var(--color-black);
|
color: var(--color-black);
|
||||||
min-width: 320px;
|
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: attr(data-banner);
|
content: attr(data-banner);
|
||||||
@@ -42,12 +41,13 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
& ul {
|
||||||
composes: inline-list with-separator-right;
|
@apply --inline-list;
|
||||||
|
@apply --with-separator-right;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
& li {
|
& li {
|
||||||
line-height: 2.4rem;
|
line-height: 2.4rem;
|
||||||
@@ -1,62 +1,17 @@
|
|||||||
jest.mock('react-modal', () =>
|
|
||||||
require('__mocks__/component-mock')('react-modal'));
|
|
||||||
jest.mock('react-feather/dist/icons/gitlab', () =>
|
|
||||||
require('__mocks__/component-mock')('react-feather/dist/icons/gitlab'));
|
|
||||||
jest.mock('components/LocaleSwitcher', () =>
|
|
||||||
require('__mocks__/component-mock')('components/LocaleSwitcher'));
|
|
||||||
jest.mock('components/InstallPrompt', () =>
|
|
||||||
require('__mocks__/component-mock')('components/InstallPrompt'));
|
|
||||||
jest.mock('components/PrivacyPolicy', () =>
|
|
||||||
require('__mocks__/component-mock')('components/PrivacyPolicy'));
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, fireEvent } from 'react-testing-library';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import Header from 'components/Header';
|
import { Header } from 'components/Header';
|
||||||
|
|
||||||
describe('Header', () => {
|
describe('Header', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env.BANNER = 'testing';
|
||||||
|
});
|
||||||
|
|
||||||
test('rendering', () => {
|
test('rendering', () => {
|
||||||
const { asFragment } = render(
|
const component = shallow(
|
||||||
<Header banner="testing" />
|
<Header/>
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
|
||||||
|
|
||||||
test('rendering with no banner', () => {
|
|
||||||
const { asFragment } = render(
|
|
||||||
<Header banner={ false } />
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('opening the Privacy Policy modal', () => {
|
|
||||||
const { asFragment, getByTestId } = render(
|
|
||||||
<Header banner={ false } />
|
|
||||||
);
|
|
||||||
const event = new MouseEvent('click', { bubbles: true });
|
|
||||||
jest.spyOn(event, 'preventDefault');
|
|
||||||
|
|
||||||
fireEvent(getByTestId('privacy-link'), event);
|
|
||||||
|
|
||||||
expect(event.preventDefault).toHaveBeenCalled();
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
['shift', 'ctrl', 'alt', 'meta'].forEach(key => {
|
|
||||||
test(`opening the Privacy Policy modal while holding ${ key } key`, () => {
|
|
||||||
const { asFragment, getByTestId } = render(
|
|
||||||
<Header banner={ false } />
|
|
||||||
);
|
|
||||||
const event = new MouseEvent('click', {
|
|
||||||
bubbles: true,
|
|
||||||
[key + 'Key']: true
|
|
||||||
});
|
|
||||||
jest.spyOn(event, 'preventDefault');
|
|
||||||
|
|
||||||
fireEvent(getByTestId('privacy-link'), event);
|
|
||||||
|
|
||||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,31 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`InstallPrompt rendering 1`] = `<DocumentFragment />`;
|
exports[`InstallPrompt rendering 1`] = `
|
||||||
|
<div
|
||||||
exports[`InstallPrompt rendering after an install prompt has been requested 1`] = `<DocumentFragment />`;
|
className="install"
|
||||||
|
|
||||||
exports[`InstallPrompt rendering after an install prompt has been requested 2`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<a
|
|
||||||
data-testid="install"
|
|
||||||
href="#install"
|
|
||||||
>
|
>
|
||||||
<span
|
<p
|
||||||
data-component="Trans"
|
className="cta"
|
||||||
data-props="{}"
|
|
||||||
>
|
>
|
||||||
Add to Home Screen
|
<Trans>
|
||||||
</span>
|
Add Regexper to your home screen?
|
||||||
</a>
|
</Trans>
|
||||||
</DocumentFragment>
|
</p>
|
||||||
|
<div
|
||||||
|
className="actions"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="primary"
|
||||||
|
>
|
||||||
|
<Trans>
|
||||||
|
Add It
|
||||||
|
</Trans>
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
<Trans>
|
||||||
|
No Thanks
|
||||||
|
</Trans>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,41 +1,23 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React from 'react';
|
||||||
import { Trans } from 'react-i18next';
|
import PropTypes from 'prop-types';
|
||||||
|
import { translate, Trans } from 'react-i18next';
|
||||||
|
|
||||||
const InstallPrompt = () => {
|
import style from './style.css';
|
||||||
const [ installPrompt, updateInstallPrompt ] = useState(null);
|
|
||||||
|
|
||||||
const handleInstall = useCallback(async event => {
|
const InstallPrompt = ({ onAccept, onReject }) => (
|
||||||
event.preventDefault();
|
<div className={ style.install }>
|
||||||
|
<p className={ style.cta }><Trans>Add Regexper to your home screen?</Trans></p>
|
||||||
|
<div className={ style.actions }>
|
||||||
|
<button className={ style.primary } onClick={ onAccept }><Trans>Add It</Trans></button>
|
||||||
|
<button onClick={ onReject }><Trans>No Thanks</Trans></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
InstallPrompt.propTypes = {
|
||||||
installPrompt.prompt();
|
onAccept: PropTypes.func.isRequired,
|
||||||
await installPrompt.userChoice;
|
onReject: PropTypes.func.isRequired
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// User cancelled install
|
|
||||||
}
|
|
||||||
|
|
||||||
updateInstallPrompt(null);
|
|
||||||
}, [installPrompt, updateInstallPrompt]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener('beforeinstallprompt', updateInstallPrompt);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('beforeinstallprompt', updateInstallPrompt);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!installPrompt) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <a href="#install"
|
|
||||||
data-testid="install"
|
|
||||||
onClick={ handleInstall }
|
|
||||||
>
|
|
||||||
<Trans>Add to Home Screen</Trans>
|
|
||||||
</a>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InstallPrompt;
|
export default translate()(InstallPrompt);
|
||||||
|
export { InstallPrompt };
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
@import url('../../globals.css');
|
||||||
|
|
||||||
|
.install {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--color-tan);
|
||||||
|
color: var(--color-black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta {
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--spacing-margin);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
padding: var(--spacing-margin);
|
||||||
|
|
||||||
|
& button {
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: 2.8rem;
|
||||||
|
border: 0 none;
|
||||||
|
background: var(--color-green) var(--gradient-green);
|
||||||
|
color: var(--color-black);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 40vw;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, fireEvent } from 'react-testing-library';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import InstallPrompt from 'components/InstallPrompt';
|
import { InstallPrompt } from 'components/InstallPrompt';
|
||||||
|
import { translate } from '__mocks__/i18n';
|
||||||
|
|
||||||
describe('InstallPrompt', () => {
|
describe('InstallPrompt', () => {
|
||||||
test('rendering', () => {
|
test('rendering', () => {
|
||||||
const { asFragment } = render(
|
const component = shallow(
|
||||||
<InstallPrompt />
|
<InstallPrompt t={ translate }/>
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
|
||||||
|
|
||||||
test('rendering after an install prompt has been requested', () => {
|
|
||||||
const { asFragment } = render(
|
|
||||||
<InstallPrompt />
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
|
|
||||||
const event = new Event('beforeinstallprompt', {
|
|
||||||
prompt: jest.fn()
|
|
||||||
});
|
|
||||||
fireEvent(window, event);
|
|
||||||
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('removing event listener', () => {
|
|
||||||
jest.spyOn(window, 'addEventListener');
|
|
||||||
jest.spyOn(window, 'removeEventListener');
|
|
||||||
|
|
||||||
const { unmount } = render(
|
|
||||||
<InstallPrompt />
|
|
||||||
);
|
|
||||||
|
|
||||||
unmount();
|
|
||||||
|
|
||||||
expect(window.removeEventListener).toHaveBeenCalledWith(
|
|
||||||
'beforeinstallprompt',
|
|
||||||
expect.any(Function));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Layout rendering 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="SentryBoundary"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
<noscript />
|
|
||||||
<span
|
|
||||||
data-component="Header"
|
|
||||||
data-props="{
|
|
||||||
\\"banner\\": \\"Test Banner\\"
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
data-component="SentryBoundary"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Example content
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
data-component="Footer"
|
|
||||||
data-props="{
|
|
||||||
\\"buildId\\": \\"test-buildid\\"
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { graphql, StaticQuery } from 'gatsby';
|
|
||||||
|
|
||||||
import SentryBoundary from 'components/SentryBoundary';
|
|
||||||
import Header from 'components/Header';
|
|
||||||
import Footer from 'components/Footer';
|
|
||||||
|
|
||||||
const query = graphql`
|
|
||||||
query LayoutQuery {
|
|
||||||
site {
|
|
||||||
siteMetadata {
|
|
||||||
banner
|
|
||||||
buildId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const noscriptStyle = `
|
|
||||||
[data-requires-js] {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Layout = ({ banner, buildId, children }) => <SentryBoundary>
|
|
||||||
<noscript>
|
|
||||||
<style type="text/css">{ noscriptStyle }</style>
|
|
||||||
</noscript>
|
|
||||||
<Header banner={ banner } />
|
|
||||||
<SentryBoundary>
|
|
||||||
{ children }
|
|
||||||
</SentryBoundary>
|
|
||||||
<Footer buildId={ buildId } />
|
|
||||||
</SentryBoundary>;
|
|
||||||
|
|
||||||
Layout.propTypes = {
|
|
||||||
banner: PropTypes.oneOfType([
|
|
||||||
PropTypes.bool,
|
|
||||||
PropTypes.string
|
|
||||||
]).isRequired,
|
|
||||||
buildId: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.oneOfType([
|
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
|
||||||
PropTypes.node
|
|
||||||
]).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/display-name
|
|
||||||
export default props => (
|
|
||||||
<StaticQuery query={ query } render={ ({ site: { siteMetadata } }) => (
|
|
||||||
<Layout { ...props } { ...siteMetadata } />
|
|
||||||
) } />
|
|
||||||
);
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
jest.mock('components/SentryBoundary', () =>
|
|
||||||
require('__mocks__/component-mock')('components/SentryBoundary'));
|
|
||||||
jest.mock('components/Header', () =>
|
|
||||||
require('__mocks__/component-mock')('components/Header'));
|
|
||||||
jest.mock('components/Footer', () =>
|
|
||||||
require('__mocks__/component-mock')('components/Footer'));
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { render } from 'react-testing-library';
|
|
||||||
|
|
||||||
import { Layout } from 'components/Layout';
|
|
||||||
|
|
||||||
describe('Layout', () => {
|
|
||||||
test('rendering', () => {
|
|
||||||
const { asFragment } = render(
|
|
||||||
<Layout banner="Test Banner" buildId="test-buildid">
|
|
||||||
Example content
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Loader rendering 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<div
|
|
||||||
class="loader"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
height="24"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<line
|
|
||||||
x1="12"
|
|
||||||
x2="12"
|
|
||||||
y1="2"
|
|
||||||
y2="6"
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1="12"
|
|
||||||
x2="12"
|
|
||||||
y1="18"
|
|
||||||
y2="22"
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1="4.93"
|
|
||||||
x2="7.76"
|
|
||||||
y1="4.93"
|
|
||||||
y2="7.76"
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1="16.24"
|
|
||||||
x2="19.07"
|
|
||||||
y1="16.24"
|
|
||||||
y2="19.07"
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1="2"
|
|
||||||
x2="6"
|
|
||||||
y1="12"
|
|
||||||
y2="12"
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1="18"
|
|
||||||
x2="22"
|
|
||||||
y1="12"
|
|
||||||
y2="12"
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1="4.93"
|
|
||||||
x2="7.76"
|
|
||||||
y1="19.07"
|
|
||||||
y2="16.24"
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1="16.24"
|
|
||||||
x2="19.07"
|
|
||||||
y1="7.76"
|
|
||||||
y2="4.93"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<div
|
|
||||||
class="message"
|
|
||||||
>
|
|
||||||
TRANSLATE(Loading...)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import LoaderIcon from 'react-feather/dist/icons/loader';
|
|
||||||
|
|
||||||
import style from './style.module.css';
|
|
||||||
|
|
||||||
const Loader = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return <div className={ style.loader }>
|
|
||||||
<LoaderIcon />
|
|
||||||
<div className={ style.message }>{ t('Loading...') }</div>
|
|
||||||
</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Loader;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { render } from 'react-testing-library';
|
|
||||||
|
|
||||||
import Loader from 'components/Loader';
|
|
||||||
|
|
||||||
describe('Loader', () => {
|
|
||||||
test('rendering', () => {
|
|
||||||
// Using full rendering here since styles for this depend on the structure
|
|
||||||
// of the SVG.
|
|
||||||
const { asFragment } = render(
|
|
||||||
<Loader/>
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,36 +1,31 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`LocaleSwitcher rendering 1`] = `
|
exports[`LocaleSwitcher rendering 1`] = `
|
||||||
<DocumentFragment>
|
|
||||||
<label>
|
<label>
|
||||||
<span
|
<Trans>
|
||||||
data-component="Trans"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Language
|
Language
|
||||||
</span>
|
</Trans>
|
||||||
<div
|
<div
|
||||||
class="switcher"
|
className="switcher"
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
data-testid="language-select"
|
onChange={[Function]}
|
||||||
>
|
|
||||||
<option
|
|
||||||
value="en"
|
value="en"
|
||||||
>
|
>
|
||||||
English
|
<option
|
||||||
|
key="en"
|
||||||
|
value="en"
|
||||||
|
>
|
||||||
|
/displayName
|
||||||
</option>
|
</option>
|
||||||
<option
|
<option
|
||||||
value="other"
|
key="fr"
|
||||||
|
value="fr"
|
||||||
>
|
>
|
||||||
Other
|
/displayName
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<span
|
<SvgMock />
|
||||||
data-component="ChevronsDown"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,47 +1,63 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React from 'react';
|
||||||
import { Trans } from 'react-i18next';
|
import { translate, Trans } from 'react-i18next';
|
||||||
|
import i18n from 'i18next';
|
||||||
|
|
||||||
import ExpandIcon from 'react-feather/dist/icons/chevrons-down';
|
import style from './style.css';
|
||||||
|
import ExpandIcon from 'feather-icons/dist/icons/chevrons-down.svg';
|
||||||
|
|
||||||
import i18n, { locales } from 'i18n';
|
import locales from 'locales';
|
||||||
|
|
||||||
import localeToAvailable from './locale-to-available';
|
const localeToAvailable = (locale, available, defaultLocale) => {
|
||||||
import style from './style.module.css';
|
if (available.includes(locale)) {
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
|
||||||
const LocaleSwitcher = () => {
|
const parts = locale.split('-');
|
||||||
const [ current, updateCurrent ] = useState(localeToAvailable(
|
|
||||||
i18n.language || '',
|
|
||||||
locales.map(l => l.code),
|
|
||||||
'en'));
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (parts.length > 0 && available.includes(parts[0])) {
|
||||||
i18n.on('languageChanged', updateCurrent);
|
return parts[0];
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return defaultLocale;
|
||||||
i18n.off('languageChanged', updateCurrent);
|
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
|
||||||
const handleSelectChange = useCallback(({ target }) => {
|
class LocaleSwitcher extends React.PureComponent {
|
||||||
|
state = {
|
||||||
|
current: localeToAvailable(i18n.language || '', Object.keys(locales), 'en')
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
i18n.on('languageChanged', this.handleLanguageChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
i18n.off('languageChanged', this.handleLanguageChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelectChange = ({ target }) => {
|
||||||
i18n.changeLanguage(target.value);
|
i18n.changeLanguage(target.value);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
handleLanguageChange = lang => {
|
||||||
|
this.setState({ current: lang });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { current } = this.state;
|
||||||
|
|
||||||
return <label>
|
return <label>
|
||||||
<Trans>Language</Trans>
|
<Trans>Language</Trans>
|
||||||
<div className={ style.switcher }>
|
<div className={ style.switcher }>
|
||||||
<select data-testid="language-select"
|
<select value={ current } onChange={ this.handleSelectChange }>
|
||||||
value={ current }
|
{ Object.keys(locales).map(locale => (
|
||||||
onChange={ handleSelectChange }
|
<option value={ locale } key={ locale }>{ i18n.getFixedT(locale)('/displayName') }</option>
|
||||||
>
|
|
||||||
{ locales.map(locale => (
|
|
||||||
<option value={ locale.code } key={ locale.code }>
|
|
||||||
{ locale.name }
|
|
||||||
</option>
|
|
||||||
)) }
|
)) }
|
||||||
</select>
|
</select>
|
||||||
<ExpandIcon/>
|
<ExpandIcon/>
|
||||||
</div>
|
</div>
|
||||||
</label>;
|
</label>;
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default LocaleSwitcher;
|
export default translate()(LocaleSwitcher);
|
||||||
|
export { LocaleSwitcher };
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
const localeToAvailable = (locale, available, defaultLocale) => {
|
|
||||||
if (available.includes(locale)) {
|
|
||||||
return locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = locale.split('-');
|
|
||||||
|
|
||||||
if (parts.length > 0 && available.includes(parts[0])) {
|
|
||||||
return parts[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultLocale;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default localeToAvailable;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import localeToAvailable from './locale-to-available';
|
|
||||||
|
|
||||||
describe('localeToAvailable', () => {
|
|
||||||
test('when requested language and region are available', () => {
|
|
||||||
expect(localeToAvailable('en-US', ['en', 'en-US', 'other'], 'other'))
|
|
||||||
.toEqual('en-US');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('when only requested language is available', () => {
|
|
||||||
expect(localeToAvailable('en-US', ['en', 'en-GB', 'other'], 'other'))
|
|
||||||
.toEqual('en');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('when language is unavailable', () => {
|
|
||||||
expect(localeToAvailable('en-US', ['tlh', 'other'], 'other'))
|
|
||||||
.toEqual('other');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
@import url('../../globals.module.css');
|
@import url('../../globals.css');
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--control-gradient: var(--color-tan) var(--gradient-tan);
|
--control-gradient: var(--color-tan) var(--gradient-tan);
|
||||||
@@ -7,5 +7,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.switcher {
|
.switcher {
|
||||||
composes: fancy-select;
|
@apply --fancy-select;
|
||||||
}
|
}
|
||||||
@@ -1,63 +1,31 @@
|
|||||||
jest.mock('react-feather/dist/icons/chevrons-down', () =>
|
jest.mock('components/SVG');
|
||||||
require('__mocks__/component-mock')(
|
jest.mock('locales', () => ({
|
||||||
'react-feather/dist/icons/chevrons-down'));
|
en: {},
|
||||||
|
fr: {}
|
||||||
|
}));
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, fireEvent, act } from 'react-testing-library';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import i18n from 'i18n';
|
import { LocaleSwitcher } from 'components/LocaleSwitcher';
|
||||||
import LocaleSwitcher from 'components/LocaleSwitcher';
|
import { translate } from '__mocks__/i18n';
|
||||||
|
|
||||||
// Ensure initial locale is always "en" during tests
|
|
||||||
jest.mock('./locale-to-available', () => jest.fn(() => 'en'));
|
|
||||||
|
|
||||||
describe('LocaleSwitcher', () => {
|
describe('LocaleSwitcher', () => {
|
||||||
test('rendering', () => {
|
test('rendering', () => {
|
||||||
const { asFragment } = render(
|
const component = shallow(
|
||||||
<LocaleSwitcher />
|
<LocaleSwitcher t={ translate }/>
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('changing language', () => {
|
test('changing language', () => {
|
||||||
jest.spyOn(i18n, 'changeLanguage');
|
const component = shallow(
|
||||||
|
<LocaleSwitcher t={ translate }/>
|
||||||
const { getByTestId } = render(
|
|
||||||
<LocaleSwitcher />
|
|
||||||
);
|
);
|
||||||
const event = new Event('change', { bubbles: true });
|
const selectInput = component.find('select');
|
||||||
const select = getByTestId('language-select');
|
selectInput.value = 'fr';
|
||||||
select.value = 'other';
|
selectInput.simulate('change', { target: { value: 'fr' } });
|
||||||
|
|
||||||
fireEvent(select, event);
|
expect(component.state('current')).toEqual('fr');
|
||||||
|
|
||||||
expect(i18n.changeLanguage).toHaveBeenCalledWith('other');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('interface update from language change', () => {
|
|
||||||
const { getByTestId } = render(
|
|
||||||
<LocaleSwitcher />
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByTestId('language-select').value).toEqual('en');
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
i18n.emit('languageChanged', 'other');
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getByTestId('language-select').value).toEqual('other');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('disconnecting event handler on unmount', () => {
|
|
||||||
const { unmount } = render(
|
|
||||||
<LocaleSwitcher />
|
|
||||||
);
|
|
||||||
|
|
||||||
jest.spyOn(i18n, 'off');
|
|
||||||
|
|
||||||
unmount();
|
|
||||||
expect(i18n.off).toHaveBeenCalledWith(
|
|
||||||
'languageChanged',
|
|
||||||
expect.any(Function));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,60 +1,27 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Message rendering 1`] = `
|
exports[`Message rendering 1`] = `
|
||||||
<DocumentFragment>
|
|
||||||
<div
|
<div
|
||||||
class="message"
|
className="message"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="header"
|
className="header"
|
||||||
>
|
>
|
||||||
<h2>
|
<h2>
|
||||||
Testing
|
Testing
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="content"
|
className="content"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Message content
|
Message content
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Message rendering with a close button 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<div
|
|
||||||
class="message"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="header"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
Testing
|
|
||||||
</h2>
|
|
||||||
<button>
|
|
||||||
<span
|
|
||||||
data-component="XSquare"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="content"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Message content
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Message rendering with icon 1`] = `
|
exports[`Message rendering with icon 1`] = `
|
||||||
<DocumentFragment>
|
|
||||||
<div
|
<div
|
||||||
class="message"
|
class="message"
|
||||||
>
|
>
|
||||||
@@ -74,21 +41,16 @@ exports[`Message rendering with icon 1`] = `
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Message rendering with type 1`] = `
|
exports[`Message rendering with type 1`] = `
|
||||||
<DocumentFragment>
|
|
||||||
<div
|
<div
|
||||||
class="message error"
|
class="message error"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="header"
|
class="header"
|
||||||
>
|
>
|
||||||
<span
|
<svg />
|
||||||
data-component="AlertOctagon"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
<h2>
|
<h2>
|
||||||
Testing
|
Testing
|
||||||
</h2>
|
</h2>
|
||||||
@@ -101,5 +63,4 @@ exports[`Message rendering with type 1`] = `
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import style from './style.module.css';
|
import style from './style.css';
|
||||||
|
|
||||||
import InfoIcon from 'react-feather/dist/icons/info';
|
import InfoIcon from 'feather-icons/dist/icons/info.svg';
|
||||||
import ErrorIcon from 'react-feather/dist/icons/alert-octagon';
|
import ErrorIcon from 'feather-icons/dist/icons/alert-octagon.svg';
|
||||||
import WarningIcon from 'react-feather/dist/icons/alert-triangle';
|
import WarningIcon from 'feather-icons/dist/icons/alert-triangle.svg';
|
||||||
import CloseIcon from 'react-feather/dist/icons/x-square';
|
|
||||||
|
|
||||||
const iconTypes = {
|
const iconTypes = {
|
||||||
info: InfoIcon,
|
info: InfoIcon,
|
||||||
@@ -15,26 +14,21 @@ const iconTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderIcon = (type, icon) => {
|
const renderIcon = (type, icon) => {
|
||||||
const Icon = icon || iconTypes[type];
|
icon = icon || iconTypes[type];
|
||||||
|
|
||||||
if (!Icon) {
|
if (!icon) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Icon = icon;
|
||||||
return <Icon/>;
|
return <Icon/>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Message = ({ type, icon, heading, onClose, children }) => (
|
const Message = ({ type, icon, heading, children }) => (
|
||||||
<div className={ [
|
<div className={ [ style.message, type && style[type] ].filter(Boolean).join(' ') }>
|
||||||
style.message,
|
|
||||||
type && style[type]
|
|
||||||
].filter(Boolean).join(' ') }>
|
|
||||||
<div className={ style.header }>
|
<div className={ style.header }>
|
||||||
{ renderIcon(type, icon) }
|
{ renderIcon(type, icon) }
|
||||||
<h2>{ heading }</h2>
|
<h2>{ heading }</h2>
|
||||||
{ onClose && <button onClick={ onClose }>
|
|
||||||
<CloseIcon /> Close
|
|
||||||
</button> }
|
|
||||||
</div>
|
</div>
|
||||||
<div className={ style.content }>
|
<div className={ style.content }>
|
||||||
{ children }
|
{ children }
|
||||||
@@ -53,7 +47,6 @@ Message.propTypes = {
|
|||||||
PropTypes.func
|
PropTypes.func
|
||||||
]),
|
]),
|
||||||
heading: PropTypes.string.isRequired,
|
heading: PropTypes.string.isRequired,
|
||||||
onClose: PropTypes.func,
|
|
||||||
children: PropTypes.oneOfType([
|
children: PropTypes.oneOfType([
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
PropTypes.node
|
PropTypes.node
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
@import url('../../globals.css');
|
||||||
|
|
||||||
|
.message {
|
||||||
|
background: var(--color-tan);
|
||||||
|
color: var(--color-black);
|
||||||
|
margin: var(--spacing-margin) 0;
|
||||||
|
box-shadow: 0 0 1rem color(var(--color-black) alpha(0.7));
|
||||||
|
|
||||||
|
& .header {
|
||||||
|
& h2 {
|
||||||
|
display: inline-block;
|
||||||
|
padding: var(--spacing-margin);
|
||||||
|
line-height: 2.8rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
padding: var(--spacing-margin);
|
||||||
|
height: 2.8rem;
|
||||||
|
width: 2.8rem;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .content {
|
||||||
|
padding: var(--spacing-margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
& p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: var(--spacing-margin) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
& .header {
|
||||||
|
background: var(--color-red);
|
||||||
|
color: var(--color-white);
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
background: var(--color-white);
|
||||||
|
color: var(--color-red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
& .header {
|
||||||
|
background: var(--color-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
background: var(--color-black);
|
||||||
|
color: var(--color-orange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.info {
|
||||||
|
& .header {
|
||||||
|
background: var(--color-blue);
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
background: var(--color-white);
|
||||||
|
color: var(--color-blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
@import url('../../globals.module.css');
|
|
||||||
|
|
||||||
.message {
|
|
||||||
background: var(--color-tan);
|
|
||||||
color: var(--color-black);
|
|
||||||
margin: var(--spacing-margin) 0;
|
|
||||||
box-shadow: 0 0 1rem color(var(--color-black) alpha(0.7));
|
|
||||||
max-height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
& h2 {
|
|
||||||
display: inline-block;
|
|
||||||
padding: var(--spacing-margin);
|
|
||||||
line-height: 2.8rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
& svg {
|
|
||||||
padding: var(--spacing-margin);
|
|
||||||
height: 2.8rem;
|
|
||||||
width: 2.8rem;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
& button {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
border: 0 none;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--color-tan);
|
|
||||||
cursor: pointer;
|
|
||||||
float: right;
|
|
||||||
font-size: 0;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:active {
|
|
||||||
color: var(--color-white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: var(--spacing-margin);
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
& p {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: var(--spacing-margin);
|
|
||||||
}
|
|
||||||
|
|
||||||
& p:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
& .header {
|
|
||||||
background: var(--color-red);
|
|
||||||
color: var(--color-white);
|
|
||||||
|
|
||||||
& svg {
|
|
||||||
background: var(--color-white);
|
|
||||||
color: var(--color-red);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
& .header {
|
|
||||||
background: var(--color-orange);
|
|
||||||
|
|
||||||
& > svg {
|
|
||||||
background: var(--color-black);
|
|
||||||
color: var(--color-orange);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
& .header {
|
|
||||||
background: var(--color-blue);
|
|
||||||
color: var(--color-white);
|
|
||||||
|
|
||||||
& > svg {
|
|
||||||
background: var(--color-white);
|
|
||||||
color: var(--color-blue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +1,34 @@
|
|||||||
jest.mock('react-feather/dist/icons/info', () =>
|
|
||||||
require('__mocks__/component-mock')('react-feather/dist/icons/info'));
|
|
||||||
jest.mock('react-feather/dist/icons/alert-octagon', () =>
|
|
||||||
require('__mocks__/component-mock')(
|
|
||||||
'react-feather/dist/icons/alert-octagon'
|
|
||||||
));
|
|
||||||
jest.mock('react-feather/dist/icons/alert-triangle', () =>
|
|
||||||
require('__mocks__/component-mock')(
|
|
||||||
'react-feather/dist/icons/alert-triangle'
|
|
||||||
));
|
|
||||||
jest.mock('react-feather/dist/icons/x-square', () =>
|
|
||||||
require('__mocks__/component-mock')('react-feather/dist/icons/x-square'));
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from 'react-testing-library';
|
import { shallow, render } from 'enzyme';
|
||||||
|
|
||||||
import Message from 'components/Message';
|
import Message from 'components/Message';
|
||||||
|
|
||||||
describe('Message', () => {
|
describe('Message', () => {
|
||||||
test('rendering', () => {
|
test('rendering', () => {
|
||||||
const { asFragment } = render(
|
const component = shallow(
|
||||||
<Message heading="Testing">
|
<Message heading="Testing" className="testing">
|
||||||
<p>Message content</p>
|
<p>Message content</p>
|
||||||
</Message>
|
</Message>
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rendering with icon', () => {
|
test('rendering with icon', () => {
|
||||||
const Icon = () => 'Sample icon SVG';
|
const Icon = () => 'Sample icon SVG';
|
||||||
const { asFragment } = render(
|
const component = render(
|
||||||
<Message heading="Testing" icon={ Icon }>
|
<Message heading="Testing" icon={ Icon }>
|
||||||
<p>Message content</p>
|
<p>Message content</p>
|
||||||
</Message>
|
</Message>
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rendering with type', () => {
|
test('rendering with type', () => {
|
||||||
const { asFragment } = render(
|
const component = render(
|
||||||
<Message heading="Testing" type="error">
|
<Message heading="Testing" type="error">
|
||||||
<p>Message content</p>
|
<p>Message content</p>
|
||||||
</Message>
|
</Message>
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
|
||||||
|
|
||||||
test('rendering with a close button', () => {
|
|
||||||
const { asFragment } = render(
|
|
||||||
<Message heading="Testing" onClose={ jest.fn() }>
|
|
||||||
<p>Message content</p>
|
|
||||||
</Message>
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Metadata rendering 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="HelmetWrapper"
|
|
||||||
data-props="{
|
|
||||||
\\"title\\": \\"Regexper\\",
|
|
||||||
\\"htmlAttributes\\": {},
|
|
||||||
\\"meta\\": [
|
|
||||||
{
|
|
||||||
\\"name\\": \\"description\\"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Metadata rendering with a title and description 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<span
|
|
||||||
data-component="HelmetWrapper"
|
|
||||||
data-props="{
|
|
||||||
\\"title\\": \\"Regexper - Testing\\",
|
|
||||||
\\"htmlAttributes\\": {},
|
|
||||||
\\"meta\\": [
|
|
||||||
{
|
|
||||||
\\"name\\": \\"description\\",
|
|
||||||
\\"content\\": \\"Test description\\"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { Helmet } from 'react-helmet';
|
|
||||||
|
|
||||||
const Metadata = ({ title, description }) => {
|
|
||||||
const { i18n } = useTranslation();
|
|
||||||
const helmetProps = {
|
|
||||||
title: title ? `Regexper - ${ title }` : 'Regexper',
|
|
||||||
htmlAttributes: {
|
|
||||||
lang: i18n.language
|
|
||||||
},
|
|
||||||
meta: [
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
content: description
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Helmet { ...helmetProps }></Helmet>;
|
|
||||||
};
|
|
||||||
|
|
||||||
Metadata.propTypes = {
|
|
||||||
title: PropTypes.string,
|
|
||||||
description: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Metadata;
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
jest.mock('react-helmet', () => {
|
|
||||||
const helmet = jest.requireActual('react-helmet');
|
|
||||||
return {
|
|
||||||
...helmet,
|
|
||||||
Helmet: require('__mocks__/component-mock').buildMock(helmet.Helmet)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { render } from 'react-testing-library';
|
|
||||||
|
|
||||||
import Metadata from 'components/Metadata';
|
|
||||||
|
|
||||||
describe('Metadata', () => {
|
|
||||||
test('rendering', () => {
|
|
||||||
const { asFragment } = render(
|
|
||||||
<Metadata/>
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rendering with a title and description', () => {
|
|
||||||
const { asFragment } = render(
|
|
||||||
<Metadata
|
|
||||||
title="Testing"
|
|
||||||
description="Test description" />
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useTranslation, Trans } from 'react-i18next';
|
|
||||||
|
|
||||||
import Message from 'components/Message';
|
|
||||||
|
|
||||||
export const PrivacyPolicy = props => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return <Message type="info" heading={ t('Privacy Policy') } { ...props }>
|
|
||||||
<Trans i18nKey="Privacy policy copy">
|
|
||||||
<p>
|
|
||||||
Regexper and the tools used to create it are all open source. If you are
|
|
||||||
concerned that the JavaScript being delivered is in any way malicious,
|
|
||||||
please inspect the source in the <a
|
|
||||||
href="https://gitlab.com/javallone/regexper-static"
|
|
||||||
rel="external noopener noreferrer"
|
|
||||||
target="_blank">GitLab repository</a>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
There are two data collection tools integrated in the app. These tools
|
|
||||||
are not used to collect personal information:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<b>Google Analytics</b> is used to track browser usage data and
|
|
||||||
application performance. It is configured to anonymize the client IP
|
|
||||||
address.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<b>Sentry.io</b> is a tool used to capture and report client-side
|
|
||||||
JavaScript errors. It is configured to not store the client IP
|
|
||||||
address.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
Regexper honors the browser <b>“Do Not Track”</b> setting
|
|
||||||
and will not enable these data collection tools if that setting is
|
|
||||||
enabled. Also, most popular ad blockers will prevent these tools from
|
|
||||||
sending any tracking data. Disabling or blocking these data collection
|
|
||||||
tools will <b>not</b> impact the performance of this app. The
|
|
||||||
information collected by these tools is used to monitor application
|
|
||||||
performance, determine browser support, and collect error reports.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Regexper is not supported by ad revenue or sales of any kind.
|
|
||||||
</p>
|
|
||||||
</Trans>
|
|
||||||
</Message>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PrivacyPolicy;
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
jest.mock('components/Message', () =>
|
|
||||||
require('__mocks__/component-mock')('components/Message'));
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { render } from 'react-testing-library';
|
|
||||||
|
|
||||||
import PrivacyPolicy from 'components/PrivacyPolicy';
|
|
||||||
|
|
||||||
describe('PrivacyPolicy', () => {
|
|
||||||
test('rendering', () => {
|
|
||||||
const { asFragment } = render(
|
|
||||||
<PrivacyPolicy onClose={ jest.fn() } />
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`RavenBoundary rendering (with error) 1`] = `<Child />`;
|
||||||
|
|
||||||
|
exports[`RavenBoundary rendering (with error) 2`] = `
|
||||||
|
<Translate(RavenError)
|
||||||
|
details={
|
||||||
|
Object {
|
||||||
|
"extra": Object {
|
||||||
|
"details": "test details",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error={
|
||||||
|
Object {
|
||||||
|
"error": "test error",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`RavenBoundary rendering 1`] = `<Child />`;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import RavenError from 'components/RavenError';
|
||||||
|
|
||||||
|
class RavenBoundary extends React.Component {
|
||||||
|
state = {
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error, errorInfo) {
|
||||||
|
this.setState({ error, errorInfo });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { error, errorInfo } = this.state;
|
||||||
|
const { children } = this.props;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
const errorProps = {
|
||||||
|
details: { extra: errorInfo },
|
||||||
|
error
|
||||||
|
};
|
||||||
|
|
||||||
|
return <RavenError { ...errorProps }/>;
|
||||||
|
} else {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RavenBoundary.propTypes = {
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
PropTypes.node
|
||||||
|
]).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RavenBoundary;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import RavenBoundary from 'components/RavenBoundary';
|
||||||
|
|
||||||
|
const testError = { error: 'test error' };
|
||||||
|
const testDetails = { details: 'test details' };
|
||||||
|
|
||||||
|
describe('RavenBoundary', () => {
|
||||||
|
test('rendering', () => {
|
||||||
|
const Child = () => <b>Child</b>;
|
||||||
|
const component = shallow(
|
||||||
|
<RavenBoundary>
|
||||||
|
<Child/>
|
||||||
|
</RavenBoundary>
|
||||||
|
);
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rendering (with error)', () => {
|
||||||
|
const Child = () => <b>Child</b>;
|
||||||
|
const component = shallow(
|
||||||
|
<RavenBoundary>
|
||||||
|
<Child/>
|
||||||
|
</RavenBoundary>
|
||||||
|
);
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
component.instance().componentDidCatch(testError, testDetails);
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`RavenError rendering 1`] = `
|
||||||
|
<Message
|
||||||
|
heading="translate(An error has occurred)"
|
||||||
|
type="error"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey="This error has been logged"
|
||||||
|
>
|
||||||
|
This error has been logged. You may also
|
||||||
|
<a
|
||||||
|
href="#error-report"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
fill out a report
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</Message>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { translate, Trans } from 'react-i18next';
|
||||||
|
import Raven from 'raven-js';
|
||||||
|
|
||||||
|
import Message from 'components/Message';
|
||||||
|
|
||||||
|
class RavenError extends React.Component {
|
||||||
|
componentDidMount() {
|
||||||
|
const { error, details } = this.props;
|
||||||
|
Raven.captureException(error, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
reportError = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (Raven.lastEventId()) {
|
||||||
|
Raven.showReportDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
return <Message type="error" heading={ t('An error has occurred') }>
|
||||||
|
<p><Trans i18nKey="This error has been logged">
|
||||||
|
This error has been logged. You may also <a href="#error-report" onClick={ this.reportError }>fill out a report</a>.
|
||||||
|
</Trans></p>
|
||||||
|
</Message>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RavenError.propTypes = {
|
||||||
|
error: PropTypes.object.isRequired,
|
||||||
|
details: PropTypes.object.isRequired,
|
||||||
|
t: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default translate()(RavenError);
|
||||||
|
export { RavenError };
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
jest.mock('raven-js');
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import Raven from 'raven-js';
|
||||||
|
|
||||||
|
import { RavenError } from 'components/RavenError';
|
||||||
|
import { translate } from '__mocks__/i18n';
|
||||||
|
|
||||||
|
const testError = { error: 'test error' };
|
||||||
|
const testDetails = { details: 'test details' };
|
||||||
|
|
||||||
|
describe('RavenError', () => {
|
||||||
|
test('rendering', () => {
|
||||||
|
const component = shallow(
|
||||||
|
<RavenError
|
||||||
|
error={ testError }
|
||||||
|
details={ testDetails }
|
||||||
|
t={ translate }/>
|
||||||
|
);
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('captures exception', () => {
|
||||||
|
shallow(
|
||||||
|
<RavenError
|
||||||
|
error={ testError }
|
||||||
|
details={ testDetails }
|
||||||
|
t={ translate }/>
|
||||||
|
);
|
||||||
|
expect(Raven.captureException).toHaveBeenCalledWith(testError, testDetails);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error reporting', () => {
|
||||||
|
test('clicking to fill out a report when an event has been logged', () => {
|
||||||
|
Raven.lastEventId.mockReturnValue(1);
|
||||||
|
const component = shallow(
|
||||||
|
<RavenError
|
||||||
|
error={ testError }
|
||||||
|
details={ testDetails }
|
||||||
|
t={ translate }/>
|
||||||
|
);
|
||||||
|
const eventObj = { preventDefault: jest.fn() };
|
||||||
|
component.find('a').simulate('click', eventObj);
|
||||||
|
|
||||||
|
expect(eventObj.preventDefault).toHaveBeenCalled();
|
||||||
|
expect(Raven.showReportDialog).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking to fill out a report when an event has not been logged', () => {
|
||||||
|
Raven.lastEventId.mockReturnValue(false);
|
||||||
|
const component = shallow(
|
||||||
|
<RavenError
|
||||||
|
error={ testError }
|
||||||
|
details={ testDetails }
|
||||||
|
t={ translate }/>
|
||||||
|
);
|
||||||
|
const eventObj = { preventDefault: jest.fn() };
|
||||||
|
component.find('a').simulate('click', eventObj);
|
||||||
|
|
||||||
|
expect(eventObj.preventDefault).toHaveBeenCalled();
|
||||||
|
expect(Raven.showReportDialog).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,510 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Render debugging 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<div
|
|
||||||
class="render"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
style="background-color: rgb(255, 255, 255);"
|
|
||||||
viewBox="0 0 "
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
>
|
|
||||||
<metadata>
|
|
||||||
<rdf:rdf>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:license
|
|
||||||
rdf:about="http://creativecommons.org/licenses/by/3.0/"
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Reproduction"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Distribution"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Notice"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Attribution"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
</cc:license>
|
|
||||||
|
|
||||||
|
|
||||||
</rdf:rdf>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
transform="translate(10 10)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Text"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Example
|
|
||||||
</span>
|
|
||||||
<rect
|
|
||||||
height="50"
|
|
||||||
style="fill: transparent; stroke: red; stroke-width: 1px; stroke-dasharray: 2,2; opacity: 0.5;"
|
|
||||||
width="100"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
cx="5"
|
|
||||||
cy="10"
|
|
||||||
r="3"
|
|
||||||
style="fill: red; opacity: 0.5;"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
cx="95"
|
|
||||||
cy="10"
|
|
||||||
r="3"
|
|
||||||
style="fill: red; opacity: 0.5;"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Render types Box 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<div
|
|
||||||
class="render"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
style="background-color: rgb(255, 255, 255);"
|
|
||||||
viewBox="0 0 "
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
>
|
|
||||||
<metadata>
|
|
||||||
<rdf:rdf>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:license
|
|
||||||
rdf:about="http://creativecommons.org/licenses/by/3.0/"
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Reproduction"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Distribution"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Notice"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Attribution"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
</cc:license>
|
|
||||||
|
|
||||||
|
|
||||||
</rdf:rdf>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
transform="translate(10 10)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Box"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Text"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Example
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Render types HorizontalLayout 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<div
|
|
||||||
class="render"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
style="background-color: rgb(255, 255, 255);"
|
|
||||||
viewBox="0 0 "
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
>
|
|
||||||
<metadata>
|
|
||||||
<rdf:rdf>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:license
|
|
||||||
rdf:about="http://creativecommons.org/licenses/by/3.0/"
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Reproduction"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Distribution"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Notice"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Attribution"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
</cc:license>
|
|
||||||
|
|
||||||
|
|
||||||
</rdf:rdf>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
transform="translate(10 10)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="HorizontalLayout"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Text"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Example
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
data-component="Text"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Another Example
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Render types Loop 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<div
|
|
||||||
class="render"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
style="background-color: rgb(255, 255, 255);"
|
|
||||||
viewBox="0 0 "
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
>
|
|
||||||
<metadata>
|
|
||||||
<rdf:rdf>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:license
|
|
||||||
rdf:about="http://creativecommons.org/licenses/by/3.0/"
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Reproduction"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Distribution"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Notice"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Attribution"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
</cc:license>
|
|
||||||
|
|
||||||
|
|
||||||
</rdf:rdf>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
transform="translate(10 10)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Loop"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Text"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Example
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Render types Pin 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<div
|
|
||||||
class="render"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
style="background-color: rgb(255, 255, 255);"
|
|
||||||
viewBox="0 0 "
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
>
|
|
||||||
<metadata>
|
|
||||||
<rdf:rdf>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:license
|
|
||||||
rdf:about="http://creativecommons.org/licenses/by/3.0/"
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Reproduction"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Distribution"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Notice"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Attribution"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
</cc:license>
|
|
||||||
|
|
||||||
|
|
||||||
</rdf:rdf>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
transform="translate(10 10)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Pin"
|
|
||||||
data-props="{}"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Render types Text 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<div
|
|
||||||
class="render"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
style="background-color: rgb(255, 255, 255);"
|
|
||||||
viewBox="0 0 "
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
>
|
|
||||||
<metadata>
|
|
||||||
<rdf:rdf>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:license
|
|
||||||
rdf:about="http://creativecommons.org/licenses/by/3.0/"
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Reproduction"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Distribution"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Notice"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Attribution"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
</cc:license>
|
|
||||||
|
|
||||||
|
|
||||||
</rdf:rdf>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
transform="translate(10 10)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Text"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Example
|
|
||||||
</span>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Render types VerticalLayout 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<div
|
|
||||||
class="render"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
style="background-color: rgb(255, 255, 255);"
|
|
||||||
viewBox="0 0 "
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
>
|
|
||||||
<metadata>
|
|
||||||
<rdf:rdf>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:license
|
|
||||||
rdf:about="http://creativecommons.org/licenses/by/3.0/"
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Reproduction"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Distribution"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Notice"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Attribution"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
</cc:license>
|
|
||||||
|
|
||||||
|
|
||||||
</rdf:rdf>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
transform="translate(10 10)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="VerticalLayout"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-component="Text"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Example
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
data-component="Text"
|
|
||||||
data-props="{}"
|
|
||||||
>
|
|
||||||
Another Example
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import React, { useRef, useCallback, useEffect } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import nodeTypes from 'rendering/types';
|
|
||||||
|
|
||||||
import style from './style.module.css';
|
|
||||||
|
|
||||||
const debugBox = {
|
|
||||||
fill: 'transparent',
|
|
||||||
stroke: 'red',
|
|
||||||
strokeWidth: '1px',
|
|
||||||
strokeDasharray: '2,2',
|
|
||||||
opacity: 0.5
|
|
||||||
};
|
|
||||||
const debugPin = {
|
|
||||||
fill: 'red',
|
|
||||||
opacity: 0.5
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line react/prop-types
|
|
||||||
const renderDebug = ({ x, y, width, height, axisX1, axisX2, axisY }) => <>
|
|
||||||
<rect style={ debugBox } x={ x } y={ y } width={ width } height={ height }/>
|
|
||||||
<circle style={ debugPin } cx={ axisX1 } cy={ axisY } r="3" />
|
|
||||||
<circle style={ debugPin } cx={ axisX2 } cy={ axisY } r="3" />
|
|
||||||
</>;
|
|
||||||
|
|
||||||
const render = (data, key) => {
|
|
||||||
if (typeof data === 'string') {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { type, props, debug, box } = data;
|
|
||||||
const children = (data.children || []).map(render);
|
|
||||||
|
|
||||||
return <React.Fragment key={ key }>
|
|
||||||
{ React.createElement(
|
|
||||||
nodeTypes[type] ? nodeTypes[type].default : type,
|
|
||||||
props,
|
|
||||||
children.length === 1 ? children[0] : children) }
|
|
||||||
{ debug && renderDebug(box) }
|
|
||||||
</React.Fragment>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Render = ({ data, onRender }) => {
|
|
||||||
const svgContainer = useRef();
|
|
||||||
|
|
||||||
const provideSVGData = useCallback(() => {
|
|
||||||
if (!svgContainer.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const svg = svgContainer.current.querySelector('svg');
|
|
||||||
onRender({
|
|
||||||
svg: svg.outerHTML,
|
|
||||||
width: Number(svg.getAttribute('width')),
|
|
||||||
height: Number(svg.getAttribute('height'))
|
|
||||||
});
|
|
||||||
}, [svgContainer, onRender]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
provideSVGData();
|
|
||||||
}, [provideSVGData]);
|
|
||||||
|
|
||||||
return <div className={ style.render } ref={ svgContainer }>
|
|
||||||
{ render({
|
|
||||||
...data,
|
|
||||||
props: {
|
|
||||||
...data.props,
|
|
||||||
onReflow: provideSVGData
|
|
||||||
}
|
|
||||||
}) }
|
|
||||||
</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
Render.propTypes = {
|
|
||||||
data: PropTypes.object.isRequired,
|
|
||||||
onRender: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Render;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
@import url('../../globals.module.css');
|
|
||||||
|
|
||||||
.render {
|
|
||||||
width: 100%;
|
|
||||||
background: var(--color-white);
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow: auto;
|
|
||||||
margin: var(--spacing-margin) 0;
|
|
||||||
|
|
||||||
& svg {
|
|
||||||
display: block;
|
|
||||||
transform: scaleZ(1); /* Move to separate render layer in Chrome */
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
jest.mock('rendering/Box', () =>
|
|
||||||
require('__mocks__/component-mock')('rendering/Box'));
|
|
||||||
jest.mock('rendering/HorizontalLayout', () =>
|
|
||||||
require('__mocks__/component-mock')('rendering/HorizontalLayout'));
|
|
||||||
jest.mock('rendering/Loop', () =>
|
|
||||||
require('__mocks__/component-mock')('rendering/Loop'));
|
|
||||||
jest.mock('rendering/Pin', () =>
|
|
||||||
require('__mocks__/component-mock')('rendering/Pin'));
|
|
||||||
jest.mock('rendering/Text', () =>
|
|
||||||
require('__mocks__/component-mock')('rendering/Text'));
|
|
||||||
jest.mock('rendering/VerticalLayout', () =>
|
|
||||||
require('__mocks__/component-mock')('rendering/VerticalLayout'));
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { render } from 'react-testing-library';
|
|
||||||
|
|
||||||
import Render from 'components/Render';
|
|
||||||
|
|
||||||
const testType = (name, item) => {
|
|
||||||
test(name, () => {
|
|
||||||
const data = { type: 'SVG', children: [item] };
|
|
||||||
const { asFragment } = render(
|
|
||||||
<Render data={ data } onRender={ jest.fn() }/>
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Render', () => {
|
|
||||||
test('debugging', () => {
|
|
||||||
const data = {
|
|
||||||
type: 'SVG',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'Text',
|
|
||||||
debug: true,
|
|
||||||
box: {
|
|
||||||
width: 100,
|
|
||||||
height: 50,
|
|
||||||
axisY: 10,
|
|
||||||
axisX1: 5,
|
|
||||||
axisX2: 95
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
'Example'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
const { asFragment } = render(
|
|
||||||
<Render data={ data } onRender={ jest.fn() }/>
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('types', () => {
|
|
||||||
testType('Pin', {
|
|
||||||
type: 'Pin'
|
|
||||||
});
|
|
||||||
|
|
||||||
testType('Text', {
|
|
||||||
type: 'Text',
|
|
||||||
children: [
|
|
||||||
'Example'
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
testType('Box', {
|
|
||||||
type: 'Box',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'Text',
|
|
||||||
children: [
|
|
||||||
'Example'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
testType('Loop', {
|
|
||||||
type: 'Loop',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'Text',
|
|
||||||
children: [
|
|
||||||
'Example'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
testType('HorizontalLayout', {
|
|
||||||
type: 'HorizontalLayout',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'Text',
|
|
||||||
children: [
|
|
||||||
'Example'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'Text',
|
|
||||||
children: [
|
|
||||||
'Another Example'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
testType('VerticalLayout', {
|
|
||||||
type: 'VerticalLayout',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'Text',
|
|
||||||
children: [
|
|
||||||
'Example'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'Text',
|
|
||||||
children: [
|
|
||||||
'Another Example'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import style from './style';
|
||||||
|
|
||||||
|
import reflowable from './reflowable';
|
||||||
|
|
||||||
|
@reflowable
|
||||||
|
class Box extends React.PureComponent {
|
||||||
|
static defaultProps = {
|
||||||
|
padding: 5,
|
||||||
|
radius: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
label = React.createRef()
|
||||||
|
|
||||||
|
children = [React.createRef()]
|
||||||
|
|
||||||
|
reflow() {
|
||||||
|
const { padding, useAnchors } = this.props;
|
||||||
|
const box = this.children[0].current.getBBox();
|
||||||
|
const labelBox = this.label.current ? this.label.current.getBBox() : { width: 0, height: 0};
|
||||||
|
|
||||||
|
this.setBBox({
|
||||||
|
width: Math.max(box.width + 2 * padding, labelBox.width),
|
||||||
|
height: box.height + 2 * padding + labelBox.height,
|
||||||
|
axisY: (useAnchors ? box.axisY : box.height / 2) + padding + labelBox.height,
|
||||||
|
axisX1: useAnchors ? box.axisX1 + padding : 0,
|
||||||
|
axisX2: useAnchors ? box.axisX2 + padding : box.width + 2 * padding
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setStateAsync({
|
||||||
|
width: this.getBBox().width,
|
||||||
|
height: box.height + 2 * padding,
|
||||||
|
contentTransform: `translate(${ padding } ${ padding + labelBox.height })`,
|
||||||
|
rectTransform: `translate(0 ${ labelBox.height })`,
|
||||||
|
labelTransform: `translate(0 ${ labelBox.height })`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { theme, radius, label, children } = this.props;
|
||||||
|
const { width, height, labelTransform, rectTransform, contentTransform } = this.state || {};
|
||||||
|
|
||||||
|
const rectProps = {
|
||||||
|
style: style[theme],
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
rx: radius,
|
||||||
|
ry: radius,
|
||||||
|
transform: rectTransform
|
||||||
|
};
|
||||||
|
const textProps = {
|
||||||
|
transform: labelTransform,
|
||||||
|
style: style.infoText,
|
||||||
|
ref: this.label
|
||||||
|
};
|
||||||
|
|
||||||
|
return <React.Fragment>
|
||||||
|
<rect { ...rectProps } ></rect>
|
||||||
|
{ label && <text { ...textProps }>{ label }</text> }
|
||||||
|
<g transform={ contentTransform }>
|
||||||
|
{ React.cloneElement(React.Children.only(children), {
|
||||||
|
ref: this.children[0]
|
||||||
|
}) }
|
||||||
|
</g>
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
label: PropTypes.string,
|
||||||
|
padding: PropTypes.number,
|
||||||
|
useAnchors: PropTypes.bool,
|
||||||
|
radius: PropTypes.number,
|
||||||
|
theme: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Box;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
|
import Box from './Box';
|
||||||
|
|
||||||
|
import SVGElement from '__mocks__/SVGElement';
|
||||||
|
|
||||||
|
const originalGetBBox = window.Element.prototype.getBBox;
|
||||||
|
|
||||||
|
describe('Box', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
window.Element.prototype.getBBox = function() {
|
||||||
|
return { width: 100, height: 10 };
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.Element.prototype.getBBox = originalGetBBox;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rendering', async () => {
|
||||||
|
const component = mount(
|
||||||
|
<Box>
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
await component.instance().doReflow();
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rendering with content anchors', async () => {
|
||||||
|
const component = mount(
|
||||||
|
<Box useAnchors>
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
await component.instance().doReflow();
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rendering with label', async () => {
|
||||||
|
const component = mount(
|
||||||
|
<Box label="Test label">
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
await component.instance().doReflow();
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { List } from 'immutable';
|
||||||
|
|
||||||
|
import style from './style';
|
||||||
|
|
||||||
|
import reflowable from './reflowable';
|
||||||
|
import Path from './path';
|
||||||
|
|
||||||
|
@reflowable
|
||||||
|
class HorizontalLayout extends React.PureComponent {
|
||||||
|
static defaultProps = {
|
||||||
|
withConnectors: false,
|
||||||
|
spacing: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
childTransforms: List()
|
||||||
|
}
|
||||||
|
|
||||||
|
children = []
|
||||||
|
|
||||||
|
updateChildTransforms(childBoxes) {
|
||||||
|
return this.state.childTransforms.withMutations(transforms => (
|
||||||
|
childBoxes.forEach((box, i) => (
|
||||||
|
transforms.set(i, `translate(${ box.offsetX } ${ box.offsetY })`)
|
||||||
|
))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConnectorPaths(childBoxes) {
|
||||||
|
let last = childBoxes[0];
|
||||||
|
|
||||||
|
return childBoxes.slice(1).reduce((path, box) => {
|
||||||
|
try {
|
||||||
|
return path
|
||||||
|
.moveTo({ x: last.offsetX + last.axisX2, y: this.getBBox().axisY })
|
||||||
|
.lineTo({ x: box.offsetX + box.axisX1 });
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
last = box;
|
||||||
|
}
|
||||||
|
}, new Path()).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
reflow() {
|
||||||
|
const { spacing, withConnectors } = this.props;
|
||||||
|
|
||||||
|
const childBoxes = this.children.map(child => child.current.getBBox());
|
||||||
|
const verticalCenter = childBoxes.reduce((center, box) => Math.max(center, box.axisY), 0);
|
||||||
|
const width = childBoxes.reduce((width, box) => width + box.width, 0) + (childBoxes.length - 1) * spacing;
|
||||||
|
const height = childBoxes.reduce((ascHeight, box) => Math.max(ascHeight, box.axisY), 0) +
|
||||||
|
childBoxes.reduce((decHeight, box) => Math.max(decHeight, box.height - box.axisY), 0);
|
||||||
|
this.setBBox({ width, height, axisY: verticalCenter }, { axisX1: true, axisX2: true });
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
childBoxes.forEach(box => {
|
||||||
|
box.offsetX = offset;
|
||||||
|
box.offsetY = this.getBBox().axisY - box.axisY;
|
||||||
|
offset += box.width + spacing;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setStateAsync({
|
||||||
|
childTransforms: this.updateChildTransforms(childBoxes),
|
||||||
|
connectorPaths: withConnectors ? this.updateConnectorPaths(childBoxes) : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { children } = this.props;
|
||||||
|
const { childTransforms, connectorPaths } = this.state;
|
||||||
|
|
||||||
|
this.makeRefCollection(this.children, React.Children.count(children));
|
||||||
|
|
||||||
|
return <React.Fragment>
|
||||||
|
<path d={ connectorPaths } style={ style.connectors }></path>
|
||||||
|
{ React.Children.map(children, (child, i) => (
|
||||||
|
<g transform={ childTransforms.get(i) }>
|
||||||
|
{ React.cloneElement(child, {
|
||||||
|
ref: this.children[i]
|
||||||
|
}) }
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalLayout.propTypes = {
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
PropTypes.node
|
||||||
|
]).isRequired,
|
||||||
|
spacing: PropTypes.number,
|
||||||
|
withConnectors: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HorizontalLayout;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
|
import HorizontalLayout from './HorizontalLayout';
|
||||||
|
|
||||||
|
import SVGElement from '__mocks__/SVGElement';
|
||||||
|
|
||||||
|
describe('HorizontalLayout', () => {
|
||||||
|
test('rendering', async () => {
|
||||||
|
const component = mount(
|
||||||
|
<HorizontalLayout>
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
</HorizontalLayout>
|
||||||
|
);
|
||||||
|
await component.instance().doReflow();
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rendering with connectors', async () => {
|
||||||
|
const component = mount(
|
||||||
|
<HorizontalLayout withConnectors>
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
</HorizontalLayout>
|
||||||
|
);
|
||||||
|
await component.instance().doReflow();
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import style from './style';
|
||||||
|
|
||||||
|
import reflowable from './reflowable';
|
||||||
|
|
||||||
|
const namespaceProps = {
|
||||||
|
'xmlns': 'http://www.w3.org/2000/svg',
|
||||||
|
'xmlns:cc': 'http://creativecommons.org/ns#',
|
||||||
|
'xmlns:rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
|
||||||
|
};
|
||||||
|
const metadata = `<rdf:rdf>
|
||||||
|
<cc:license rdf:about="http://creativecommons.org/licenses/by/3.0/">
|
||||||
|
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"></cc:permits>
|
||||||
|
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"></cc:permits>
|
||||||
|
<cc:requires rdf:resource="http://creativecommons.org/ns#Notice"></cc:requires>
|
||||||
|
<cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"></cc:requires>
|
||||||
|
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"></cc:permits>
|
||||||
|
</cc:license>
|
||||||
|
</rdf:rdf>`;
|
||||||
|
|
||||||
|
@reflowable
|
||||||
|
class Image extends React.PureComponent {
|
||||||
|
static defaultProps = {
|
||||||
|
padding: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
svg = React.createRef()
|
||||||
|
|
||||||
|
children = [React.createRef()]
|
||||||
|
|
||||||
|
async svgUrl(type) {
|
||||||
|
const markup = this.svg.current.outerHTML;
|
||||||
|
return new Blob([markup], { type });
|
||||||
|
}
|
||||||
|
|
||||||
|
async pngUrl(type) {
|
||||||
|
const markup = this.svg.current.outerHTML;
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
const loader = new window.Image(); // Using window.Image to avoid name conflict :(
|
||||||
|
|
||||||
|
loader.width = canvas.width = Number(this.svg.current.getAttribute('width')) * 2;
|
||||||
|
loader.height = canvas.height = Number(this.svg.current.getAttribute('height')) * 2;
|
||||||
|
|
||||||
|
await new Promise(resolve => {
|
||||||
|
loader.onload = resolve;
|
||||||
|
loader.src = 'data:image/svg+xml,' + encodeURIComponent(markup);
|
||||||
|
});
|
||||||
|
|
||||||
|
context.drawImage(loader, 0, 0, loader.width, loader.height);
|
||||||
|
return new Promise(resolve => canvas.toBlob(resolve, type));
|
||||||
|
}
|
||||||
|
|
||||||
|
reflow() {
|
||||||
|
const { padding } = this.props;
|
||||||
|
const box = this.children[0].current.getBBox();
|
||||||
|
|
||||||
|
this.setStateAsync({
|
||||||
|
width: Math.round(box.width + 2 * padding),
|
||||||
|
height: Math.round(box.height + 2 * padding)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { width, height } = this.state;
|
||||||
|
const { padding, children } = this.props;
|
||||||
|
|
||||||
|
const svgProps = {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
viewBox: [0, 0, width, height].join(' '),
|
||||||
|
style: style.image,
|
||||||
|
ref: this.svg,
|
||||||
|
...namespaceProps
|
||||||
|
};
|
||||||
|
|
||||||
|
return <svg { ...svgProps }>
|
||||||
|
<metadata dangerouslySetInnerHTML={{ __html: metadata }}></metadata>
|
||||||
|
<g transform={ `translate(${ padding } ${ padding })` }>
|
||||||
|
{ React.cloneElement(React.Children.only(children), {
|
||||||
|
ref: this.children[0]
|
||||||
|
}) }
|
||||||
|
</g>
|
||||||
|
</svg>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Image.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
padding: PropTypes.number
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Image;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
|
import Image from './Image';
|
||||||
|
|
||||||
|
import SVGElement from '__mocks__/SVGElement';
|
||||||
|
|
||||||
|
describe('Image', () => {
|
||||||
|
test('rendering', async () => {
|
||||||
|
const component = mount(
|
||||||
|
<Image>
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
</Image>
|
||||||
|
);
|
||||||
|
await component.instance().doReflow();
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import style from './style';
|
||||||
|
|
||||||
|
import reflowable from './reflowable';
|
||||||
|
import Path from './path';
|
||||||
|
|
||||||
|
const skipPath = (box, greedy) => {
|
||||||
|
const vert = Math.max(0, box.axisY - 10);
|
||||||
|
const horiz = box.width - 10;
|
||||||
|
|
||||||
|
let path = new Path({ relative: true });
|
||||||
|
|
||||||
|
if (!greedy) {
|
||||||
|
path
|
||||||
|
.moveTo({ x: 10, y: box.axisY + box.offsetY - 15, relative: false })
|
||||||
|
.lineTo({ x: 5, y: 5 })
|
||||||
|
.moveTo({ x: -5, y: -5 })
|
||||||
|
.lineTo({ x: -5, y: 5 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
.moveTo({ x: 0, y: box.axisY + box.offsetY, relative: false })
|
||||||
|
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: -10 })
|
||||||
|
.lineTo({ y: -vert })
|
||||||
|
.quadraticCurveTo({ cx: 0, cy: -10, x: 10, y: -10 })
|
||||||
|
.lineTo({ x: horiz })
|
||||||
|
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: 10 })
|
||||||
|
.lineTo({ y: vert })
|
||||||
|
.quadraticCurveTo({ cx: 0, cy: 10, x: 10, y: 10 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const repeatPath = (box, greedy) => {
|
||||||
|
const vert = box.height - box.axisY - 10;
|
||||||
|
|
||||||
|
let path = new Path({ relative: true });
|
||||||
|
|
||||||
|
if (greedy) {
|
||||||
|
path
|
||||||
|
.moveTo({ x: box.offsetX + box.width + 10, y: box.axisY + box.offsetY + 15, relative: false })
|
||||||
|
.lineTo({ x: 5, y: -5 })
|
||||||
|
.moveTo({ x: -5, y: 5 })
|
||||||
|
.lineTo({ x: -5, y: -5 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
.moveTo({ x: box.offsetX, y: box.axisY + box.offsetY, relative: false })
|
||||||
|
.quadraticCurveTo({ cx: -10, cy: 0, x: -10, y: 10 })
|
||||||
|
.lineTo({ y: vert })
|
||||||
|
.quadraticCurveTo({ cx: 0, cy: 10, x: 10, y: 10 })
|
||||||
|
.lineTo({ x: box.width })
|
||||||
|
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: -10 })
|
||||||
|
.lineTo({ y: -vert })
|
||||||
|
.quadraticCurveTo({ cx: 0, cy: -10, x: -10, y: -10 });
|
||||||
|
};
|
||||||
|
|
||||||
|
@reflowable
|
||||||
|
class Loop extends React.PureComponent {
|
||||||
|
label = React.createRef()
|
||||||
|
|
||||||
|
children = [React.createRef()]
|
||||||
|
|
||||||
|
get contentOffset() {
|
||||||
|
const { skip, repeat } = this.props;
|
||||||
|
|
||||||
|
if (skip) {
|
||||||
|
return { x: 15, y: 10 };
|
||||||
|
} else if (repeat) {
|
||||||
|
return { x: 10, y: 0 };
|
||||||
|
} else {
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reflow() {
|
||||||
|
const { skip, repeat, greedy } = this.props;
|
||||||
|
const box = this.children[0].current.getBBox();
|
||||||
|
const labelBox = this.label.current ? this.label.current.getBBox() : { width: 0, height: 0 };
|
||||||
|
|
||||||
|
let height = box.height + labelBox.height;
|
||||||
|
if (skip) {
|
||||||
|
height += 10;
|
||||||
|
}
|
||||||
|
if (repeat) {
|
||||||
|
height += 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setBBox({
|
||||||
|
width: box.width + this.contentOffset.x * 2,
|
||||||
|
height,
|
||||||
|
axisY: box.axisY + this.contentOffset.y,
|
||||||
|
axisX1: box.axisX1 + this.contentOffset.x,
|
||||||
|
axisX2: box.axisX2 + this.contentOffset.x
|
||||||
|
});
|
||||||
|
|
||||||
|
box.offsetX = this.contentOffset.x;
|
||||||
|
box.offsetY = this.contentOffset.y;
|
||||||
|
|
||||||
|
this.setStateAsync({
|
||||||
|
labelTransform: `translate(${ this.getBBox().width - labelBox.width - 10 } ${ this.getBBox().height + 2 })`,
|
||||||
|
loopPaths: [
|
||||||
|
skip && skipPath(box, greedy),
|
||||||
|
repeat && repeatPath(box, greedy)
|
||||||
|
].filter(Boolean).join('')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { label, children } = this.props;
|
||||||
|
const { loopPaths, labelTransform } = this.state || {};
|
||||||
|
|
||||||
|
const textProps = {
|
||||||
|
transform: labelTransform,
|
||||||
|
style: style.infoText,
|
||||||
|
ref: this.label
|
||||||
|
};
|
||||||
|
|
||||||
|
return <React.Fragment>
|
||||||
|
<path d={ loopPaths } style={ style.connectors }></path>
|
||||||
|
{ label && <text { ...textProps }>{ label }</text> }
|
||||||
|
<g transform={ `translate(${ this.contentOffset.x } ${ this.contentOffset.y })` }>
|
||||||
|
{ React.cloneElement(React.Children.only(children), {
|
||||||
|
ref: this.children[0]
|
||||||
|
}) }
|
||||||
|
</g>
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loop.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
greedy: PropTypes.bool,
|
||||||
|
label: PropTypes.string,
|
||||||
|
skip: PropTypes.bool,
|
||||||
|
repeat: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loop;
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
|
import Loop from './Loop';
|
||||||
|
|
||||||
|
import SVGElement from '__mocks__/SVGElement';
|
||||||
|
|
||||||
|
const originalGetBBox = window.Element.prototype.getBBox;
|
||||||
|
|
||||||
|
describe('Loop', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
window.Element.prototype.getBBox = function() {
|
||||||
|
return { width: 100, height: 10 };
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.Element.prototype.getBBox = originalGetBBox;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rendering', async () => {
|
||||||
|
const component = mount(
|
||||||
|
<Loop>
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
</Loop>
|
||||||
|
);
|
||||||
|
await component.instance().doReflow();
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rendering with skip path', async () => {
|
||||||
|
const component = mount(
|
||||||
|
<Loop skip>
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
</Loop>
|
||||||
|
);
|
||||||
|
await component.instance().doReflow();
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rendering with repeat path', async () => {
|
||||||
|
const component = mount(
|
||||||
|
<Loop repeat>
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
</Loop>
|
||||||
|
);
|
||||||
|
await component.instance().doReflow();
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rendering with repeat path and label', async () => {
|
||||||
|
const component = mount(
|
||||||
|
<Loop repeat label="Test label">
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
</Loop>
|
||||||
|
);
|
||||||
|
await component.instance().doReflow();
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rendering with skip and repeat paths', async () => {
|
||||||
|
const component = mount(
|
||||||
|
<Loop skip repeat>
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
</Loop>
|
||||||
|
);
|
||||||
|
await component.instance().doReflow();
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rendering with greedy skip and repeat paths', async () => {
|
||||||
|
const component = mount(
|
||||||
|
<Loop greedy skip repeat>
|
||||||
|
<SVGElement bbox={{ width: 100, height: 100 }}/>
|
||||||
|
</Loop>
|
||||||
|
);
|
||||||
|
await component.instance().doReflow();
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import style from './style';
|
||||||
|
|
||||||
|
import reflowable from './reflowable';
|
||||||
|
|
||||||
|
@reflowable
|
||||||
|
class Pin extends React.PureComponent {
|
||||||
|
static defaultProps = {
|
||||||
|
radius: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
reflow() {
|
||||||
|
const { radius } = this.props;
|
||||||
|
|
||||||
|
this.setBBox({
|
||||||
|
width: radius * 2,
|
||||||
|
height: radius * 2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { radius } = this.props;
|
||||||
|
|
||||||
|
const circleProps = {
|
||||||
|
r: radius,
|
||||||
|
style: style.pin,
|
||||||
|
transform: `translate(${ radius } ${ radius })`
|
||||||
|
};
|
||||||
|
|
||||||
|
return <circle { ...circleProps }></circle>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Pin.propTypes = {
|
||||||
|
radius: PropTypes.number
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pin;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
|
import Pin from './Pin';
|
||||||
|
|
||||||
|
describe('Pin', () => {
|
||||||
|
test('rendering', async () => {
|
||||||
|
const component = mount(
|
||||||
|
<Pin/>
|
||||||
|
);
|
||||||
|
await component.instance().doReflow();
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import style from './style';
|
||||||
|
|
||||||
|
import reflowable from './reflowable';
|
||||||
|
|
||||||
|
@reflowable
|
||||||
|
class Text extends React.PureComponent {
|
||||||
|
text = React.createRef()
|
||||||
|
|
||||||
|
reflow() {
|
||||||
|
const box = this.text.current.getBBox();
|
||||||
|
|
||||||
|
this.setBBox({
|
||||||
|
width: box.width,
|
||||||
|
height: box.height
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setStateAsync({
|
||||||
|
transform: `translate(${-box.x} ${-box.y})`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent() {
|
||||||
|
const { children, quoted } = this.props;
|
||||||
|
if (!quoted) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <React.Fragment>
|
||||||
|
<tspan style={ style.textQuote }>“</tspan>
|
||||||
|
<tspan>{ children }</tspan>
|
||||||
|
<tspan style={ style.textQuote }>”</tspan>
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { theme } = this.props;
|
||||||
|
const { transform } = this.state || {};
|
||||||
|
|
||||||
|
const textProps = {
|
||||||
|
style: { ...style.text, ...style[theme] },
|
||||||
|
transform,
|
||||||
|
ref: this.text
|
||||||
|
};
|
||||||
|
|
||||||
|
return <text { ...textProps }>
|
||||||
|
{ this.renderContent() }
|
||||||
|
</text>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text.propTypes = {
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
PropTypes.node
|
||||||
|
]).isRequired,
|
||||||
|
quoted: PropTypes.bool,
|
||||||
|
theme: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Text;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
|
import Text from './Text';
|
||||||
|
|
||||||
|
const originalGetBBox = window.Element.prototype.getBBox;
|
||||||
|
|
||||||
|
describe('Text', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
window.Element.prototype.getBBox = function() {
|
||||||
|
return { x: 10, y: 10 };
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.Element.prototype.getBBox = originalGetBBox;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rendering', async () => {
|
||||||
|
const component = mount(
|
||||||
|
<Text>Test content</Text>
|
||||||
|
);
|
||||||
|
await component.instance().doReflow();
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rendering with quotes', async () => {
|
||||||
|
const component = mount(
|
||||||
|
<Text quoted>Test content</Text>
|
||||||
|
);
|
||||||
|
await component.instance().doReflow();
|
||||||
|
component.update();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { List } from 'immutable';
|
||||||
|
|
||||||
|
import style from './style';
|
||||||
|
|
||||||
|
import reflowable from './reflowable';
|
||||||
|
import Path from './path';
|
||||||
|
|
||||||
|
const connectorMargin = 20;
|
||||||
|
|
||||||
|
@reflowable
|
||||||
|
class VerticalLayout extends React.PureComponent {
|
||||||
|
static defaultProps = {
|
||||||
|
withConnectors: false,
|
||||||
|
spacing: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
childTransforms: List()
|
||||||
|
}
|
||||||
|
|
||||||
|
children = []
|
||||||
|
|
||||||
|
updateChildTransforms(childBoxes) {
|
||||||
|
return this.state.childTransforms.withMutations(transforms => (
|
||||||
|
childBoxes.forEach((box, i) => (
|
||||||
|
transforms.set(i, `translate(${ box.offsetX } ${ box.offsetY })`)
|
||||||
|
))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
makeCurve(box) {
|
||||||
|
const thisBox = this.getBBox();
|
||||||
|
const distance = Math.abs(box.offsetY + box.axisY - thisBox.axisY);
|
||||||
|
|
||||||
|
if (distance >= 15) {
|
||||||
|
const curve = (box.axisY + box.offsetY > thisBox.axisY) ? 10 : -10;
|
||||||
|
|
||||||
|
return new Path()
|
||||||
|
// Left
|
||||||
|
.moveTo({ x: 10, y: box.axisY + box.offsetY - curve })
|
||||||
|
.quadraticCurveTo({ cx: 0, cy: curve, x: 10, y: curve, relative: true })
|
||||||
|
.lineTo({ x: box.offsetX + box.axisX1 })
|
||||||
|
// Right
|
||||||
|
.moveTo({ x: thisBox.width - 10, y: box.axisY + box.offsetY - curve })
|
||||||
|
.quadraticCurveTo({ cx: 0, cy: curve, x: -10, y: curve, relative: true })
|
||||||
|
.lineTo({ x: box.offsetX + box.axisX2 });
|
||||||
|
} else {
|
||||||
|
const anchor = box.offsetY + box.axisY - thisBox.axisY;
|
||||||
|
|
||||||
|
return new Path()
|
||||||
|
// Left
|
||||||
|
.moveTo({ x: 0, y: thisBox.axisY })
|
||||||
|
.cubicCurveTo({ cx1: 15, cy1: 0, cx2: 10, cy2: anchor, x: 20, y: anchor, relative: true })
|
||||||
|
.lineTo({ x: box.offsetX + box.axisX1 })
|
||||||
|
// Right
|
||||||
|
.moveTo({ x: thisBox.width, y: thisBox.axisY })
|
||||||
|
.cubicCurveTo({ cx1: -15, cy1: 0, cx2: -10, cy2: anchor, x: -20, y: anchor, relative: true })
|
||||||
|
.lineTo({ x: box.offsetX + box.axisX2 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
makeSide(box) {
|
||||||
|
const thisBox = this.getBBox();
|
||||||
|
const distance = Math.abs(box.offsetY + box.axisY - thisBox.axisY);
|
||||||
|
|
||||||
|
if (distance >= 15) {
|
||||||
|
const shift = (box.offsetY + box.axisY > thisBox.axisY) ? 10 : -10;
|
||||||
|
const edge = box.offsetY + box.axisY - shift;
|
||||||
|
|
||||||
|
return new Path()
|
||||||
|
// Left
|
||||||
|
.moveTo({ x: 0, y: thisBox.axisY })
|
||||||
|
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: shift, relative: true })
|
||||||
|
.lineTo({ y: edge })
|
||||||
|
// Right
|
||||||
|
.moveTo({ x: thisBox.width, y: thisBox.axisY })
|
||||||
|
.quadraticCurveTo({ cx: -10, cy: 0, x: -10, y: shift, relative: true })
|
||||||
|
.lineTo({ y: edge });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reflow() {
|
||||||
|
const { spacing, withConnectors } = this.props;
|
||||||
|
|
||||||
|
const childBoxes = this.children.map(child => child.current.getBBox());
|
||||||
|
const horizontalCenter = childBoxes.reduce((center, box) => Math.max(center, box.width / 2), 0);
|
||||||
|
const margin = withConnectors ? connectorMargin : 0;
|
||||||
|
const width = childBoxes.reduce((width, box) => Math.max(width, box.width), 0) + 2 * margin;
|
||||||
|
const height = childBoxes.reduce((height, box) => height + box.height, 0) + (childBoxes.length - 1) * spacing;
|
||||||
|
this.setBBox({ width, height }, { axisY: true, axisX1: true, axisX2: true });
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
childBoxes.forEach(box => {
|
||||||
|
box.offsetX = horizontalCenter - box.width / 2 + margin;
|
||||||
|
box.offsetY = offset;
|
||||||
|
offset += spacing + box.height;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setStateAsync({
|
||||||
|
childTransforms: this.updateChildTransforms(childBoxes),
|
||||||
|
connectorPaths: withConnectors ? [
|
||||||
|
...childBoxes.map(box => this.makeCurve(box)),
|
||||||
|
this.makeSide(childBoxes[0]),
|
||||||
|
this.makeSide(childBoxes[childBoxes.length - 1])
|
||||||
|
].join('') : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { children } = this.props;
|
||||||
|
const { childTransforms, connectorPaths } = this.state;
|
||||||
|
|
||||||
|
this.makeRefCollection(this.children, React.Children.count(children));
|
||||||
|
|
||||||
|
return <React.Fragment>
|
||||||
|
<path d={ connectorPaths } style={ style.connectors }></path>
|
||||||
|
{ React.Children.map(children, (child, i) => (
|
||||||
|
<g transform={ childTransforms.get(i) }>
|
||||||
|
{ React.cloneElement(child, {
|
||||||
|
ref: this.children[i]
|
||||||
|
}) }
|
||||||
|
</g>
|
||||||
|
)) }
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalLayout.propTypes = {
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
PropTypes.node
|
||||||
|
]).isRequired,
|
||||||
|
spacing: PropTypes.number,
|
||||||
|
withConnectors: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VerticalLayout;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user