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
|
||||
|
||||
# Gatsby build files
|
||||
.cache/
|
||||
public/
|
||||
# Build output
|
||||
build/
|
||||
script/__build__/
|
||||
|
||||
# Test coverage
|
||||
# Coverage reports
|
||||
coverage/
|
||||
|
||||
# Firebase
|
||||
.firebase/
|
||||
firebase-debug.log
|
||||
# Favicon cache
|
||||
.wwp-cache/
|
||||
|
||||
+39
-24
@@ -1,55 +1,68 @@
|
||||
image: node:latest
|
||||
|
||||
stages:
|
||||
- setup
|
||||
- test
|
||||
- build
|
||||
- deploy
|
||||
|
||||
.shared_runner: &shared_runner
|
||||
tags:
|
||||
- shared
|
||||
|
||||
.cache_consumer: &cache_consumer
|
||||
cache:
|
||||
policy: pull
|
||||
paths:
|
||||
- node_modules
|
||||
|
||||
.preview_job: &preview_job
|
||||
only:
|
||||
- gatsby # TODO: Change to master once merged
|
||||
- react # TODO: Change to master once merged
|
||||
|
||||
.production_job: &production_job
|
||||
only:
|
||||
- /^release-.*$/
|
||||
|
||||
.build_template: &build_template
|
||||
<<: *shared_runner
|
||||
<<: *cache_consumer
|
||||
stage: build
|
||||
dependencies: []
|
||||
artifacts:
|
||||
paths:
|
||||
- public/
|
||||
- build/
|
||||
script:
|
||||
- yarn build
|
||||
|
||||
.deploy_template: &deploy_template
|
||||
<<: *shared_runner
|
||||
<<: *cache_consumer
|
||||
stage: deploy
|
||||
script:
|
||||
- yarn firebase use --token $FIREBASE_DEPLOY_KEY default
|
||||
- yarn firebase deploy --only hosting:$DEPLOY_ENV -m "Pipeline $CI_PIPELINE_ID, Build $CI_BUILD_ID" --non-interactive --token $FIREBASE_DEPLOY_KEY
|
||||
- yarn deploy
|
||||
|
||||
cache:
|
||||
setup:
|
||||
<<: *shared_runner
|
||||
stage: setup
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
|
||||
before_script:
|
||||
- node_modules
|
||||
script:
|
||||
- yarn install
|
||||
|
||||
test-lint:
|
||||
stage: test
|
||||
script:
|
||||
- yarn test:lint
|
||||
|
||||
test-unit:
|
||||
test:
|
||||
<<: *shared_runner
|
||||
<<: *cache_consumer
|
||||
stage: test
|
||||
coverage: '/^Statements\s*:\s*([^%]+)/'
|
||||
script:
|
||||
- yarn test:unit
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage/
|
||||
script:
|
||||
- yarn test
|
||||
|
||||
build-preview:
|
||||
build_preview:
|
||||
<<: *build_template
|
||||
<<: *preview_job
|
||||
variables:
|
||||
@@ -57,31 +70,33 @@ build-preview:
|
||||
DEPLOY_ENV: preview
|
||||
GA_PROPERTY: $PREVIEW_GA_PROPERTY
|
||||
|
||||
build-production:
|
||||
build_production:
|
||||
<<: *build_template
|
||||
<<: *production_job
|
||||
variables:
|
||||
DEPLOY_ENV: production
|
||||
GA_PROPERTY: $PROD_GA_PROPERTY
|
||||
|
||||
deploy-preview:
|
||||
deploy_preview:
|
||||
<<: *deploy_template
|
||||
<<: *preview_job
|
||||
dependencies:
|
||||
- build-preview
|
||||
- build_preview
|
||||
environment:
|
||||
name: preview
|
||||
url: https://preview.regexper.com
|
||||
variables:
|
||||
DEPLOY_ENV: preview
|
||||
CLOUD_FRONT_ID: $PREVIEW_CLOUDFRONT_ID
|
||||
DEPLOY_BUCKET: $PREVIEW_DEPLOY_BUCKET
|
||||
|
||||
deploy-production:
|
||||
deploy_production:
|
||||
<<: *deploy_template
|
||||
<<: *production_job
|
||||
dependencies:
|
||||
- build-production
|
||||
- build_production
|
||||
environment:
|
||||
name: production
|
||||
url: https://regexper.com
|
||||
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
|
||||
|
||||
### 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
|
||||
|
||||
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",
|
||||
"version": "1.0.0",
|
||||
"description": "Regular expression visualization tool",
|
||||
"description": "Regular expression visualization tool using railroad diagrams",
|
||||
"homepage": "http://regexper.com",
|
||||
"author": {
|
||||
"name": "Jeffrey Avallone",
|
||||
@@ -10,21 +10,39 @@
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "gatsby develop",
|
||||
"build": "gatsby build",
|
||||
"start": "webpack-dev-server --config webpack.dev.js",
|
||||
"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:unit": "jest --coverage",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "yarn test:lint"
|
||||
}
|
||||
"test:watch": "yarn test:unit --watch",
|
||||
"test:bundle-analyzer": "cross-env NODE_ENV=production webpack --config webpack.bundle-analyzer.js",
|
||||
"i18n:scrub": "node ./script/i18n-scrub.js",
|
||||
"precommit": "run-s test:lint"
|
||||
},
|
||||
"browserslist": [
|
||||
">1%",
|
||||
"not ie < 11"
|
||||
],
|
||||
"babel": {
|
||||
"presets": [
|
||||
"env",
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-runtime",
|
||||
"transform-class-properties",
|
||||
"transform-object-rest-spread",
|
||||
"transform-decorators-legacy",
|
||||
"syntax-dynamic-import"
|
||||
]
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"postcss-import": {},
|
||||
@@ -37,79 +55,132 @@
|
||||
},
|
||||
"jest": {
|
||||
"clearMocks": true,
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.js",
|
||||
"!src/i18n.js"
|
||||
"setupTestFrameworkScriptFile": "<rootDir>/src/setup/jest.js",
|
||||
"snapshotSerializers": [
|
||||
"enzyme-to-json/serializer"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text-summary",
|
||||
"html"
|
||||
],
|
||||
"globals": {
|
||||
"__PATH_PREFIX__": ""
|
||||
},
|
||||
"moduleNameMapper": {
|
||||
"\\.css$": "identity-obj-proxy"
|
||||
},
|
||||
"modulePaths": [
|
||||
"src",
|
||||
"node_modules"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"react-testing-library/cleanup-after-each",
|
||||
"<rootDir>/jest/setup.js"
|
||||
],
|
||||
"testPathIgnorePatterns": [
|
||||
"node_modules",
|
||||
".cache"
|
||||
],
|
||||
"transform": {
|
||||
"\\.yaml$": "<rootDir>/jest/yaml.js",
|
||||
"\\.js$": "<rootDir>/jest/preprocess.js",
|
||||
"\\.svg$": "<rootDir>/jest/static-file-transform.js"
|
||||
"moduleNameMapper": {
|
||||
"\\.svg$": "__mocks__/svgMock.js",
|
||||
"\\.css$": "identity-obj-proxy"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(gatsby)/)"
|
||||
"collectCoverageFrom": [
|
||||
"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": [
|
||||
"<rootDir>/coverage",
|
||||
"<rootDir>/public"
|
||||
"coverageReporters": [
|
||||
"text-summary",
|
||||
"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": {
|
||||
"@babel/core": "^7.2.2",
|
||||
"@ungap/url-search-params": "^0.1.2",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^24.5.0",
|
||||
"babel-plugin-dynamic-import-node": "^2.2.0",
|
||||
"babel-preset-gatsby": "^0.1.6",
|
||||
"eslint": "^5.11.1",
|
||||
"eslint-plugin-jest": "^22.1.2",
|
||||
"eslint-plugin-react": "^7.12.1",
|
||||
"firebase-tools": "^6.3.0",
|
||||
"gatsby": "^2.0.81",
|
||||
"gatsby-plugin-google-analytics": "^2.0.8",
|
||||
"gatsby-plugin-manifest": "^2.0.13",
|
||||
"gatsby-plugin-offline": "^2.0.21",
|
||||
"gatsby-plugin-postcss": "^2.0.2",
|
||||
"gatsby-plugin-react-helmet": "^3.0.5",
|
||||
"gatsby-plugin-sentry": "^1.0.0",
|
||||
"husky": "^1.3.1",
|
||||
"i18next": "^15.0.7",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"i18next-xhr-backend": "^2.0.1",
|
||||
"@alienfast/i18next-loader": "^1.0.14",
|
||||
"aws-sdk": "^2.247.1",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.2.1",
|
||||
"babel-jest": "^23.0.1",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-register": "^6.26.0",
|
||||
"cheerio": "^1.0.0-rc.2",
|
||||
"colors": "^1.1.2",
|
||||
"copy-webpack-plugin": "^4.4.1",
|
||||
"cross-env": "^5.1.3",
|
||||
"css-loader": "^0.28.9",
|
||||
"enzyme": "^3.3.0",
|
||||
"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",
|
||||
"jest": "^24.5.0",
|
||||
"js-yaml": "^3.13.0",
|
||||
"immutable": "^3.8.2",
|
||||
"jest": "^23.1.0",
|
||||
"mime-types": "^2.1.18",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"postcss-cssnext": "^3.1.0",
|
||||
"postcss-import": "^12.0.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"react-feather": "^1.1.5",
|
||||
"react-helmet": "^5.2.0",
|
||||
"react-i18next": "^10.5.3",
|
||||
"react-modal": "^3.8.1",
|
||||
"react-testing-library": "^6.0.2"
|
||||
"postcss-import": "^11.1.0",
|
||||
"postcss-loader": "^2.1.0",
|
||||
"raven-js": "^3.22.2",
|
||||
"react": "^16.3.0",
|
||||
"react-dom": "^16.3.0",
|
||||
"react-ga": "^2.4.1",
|
||||
"react-i18next": "^7.3.6",
|
||||
"react-test-renderer": "^16.3.0",
|
||||
"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
|
||||
i18n.default.addResourceBundle('dev', 'translation', {});
|
||||
i18n.default.addResourceBundle('en', 'translation', {});
|
||||
i18n.default.addResourceBundle('other', 'translation', {});
|
||||
const translate = txt => `translate(${ txt })`;
|
||||
|
||||
module.exports = {
|
||||
...i18n,
|
||||
locales: [
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'other', name: 'Other' }
|
||||
],
|
||||
mockT: str => `TRANSLATE(${ str })`
|
||||
};
|
||||
i18n.init({
|
||||
fallbackLng: 'en',
|
||||
fallbackNS: 'missing',
|
||||
debug: false,
|
||||
resources: {}
|
||||
});
|
||||
|
||||
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
-6
@@ -1,6 +0,0 @@
|
||||
const reactI18next = jest.requireActual('react-i18next');
|
||||
|
||||
module.exports = {
|
||||
...reactI18next,
|
||||
Trans: require('__mocks__/component-mock').buildMock(reactI18next.Trans)
|
||||
};
|
||||
@@ -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
|
||||
|
||||
exports[`App removing rendered expression 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="withI18nextTranslation(Form)"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"js\\",
|
||||
\\"expr\\": \\"test expression\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
>
|
||||
<span
|
||||
data-component="withI18nextTranslation(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="withI18nextTranslation(Form)"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"js\\",
|
||||
\\"expr\\": \\"\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`App rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="withI18nextTranslation(Form)"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"js\\",
|
||||
\\"expr\\": \\"\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
<React.Fragment>
|
||||
<Translate(Form)
|
||||
downloadUrls={Array []}
|
||||
key="expr=undefined&syntax=undefined"
|
||||
onSubmit={[Function]}
|
||||
syntaxes={
|
||||
Object {
|
||||
"js": "JavaScript",
|
||||
"pcre": "PCRE",
|
||||
}
|
||||
]
|
||||
}"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`App rendering an expression 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="withI18nextTranslation(Form)"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"js\\",
|
||||
\\"expr\\": \\"\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`App rendering an expression 2`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="withI18nextTranslation(Form)"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"js\\",
|
||||
\\"expr\\": \\"test expression\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
/>
|
||||
<span
|
||||
data-component="withI18nextTranslation(Loader)"
|
||||
data-props="{}"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`App rendering an expression 3`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="withI18nextTranslation(Form)"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"js\\",
|
||||
\\"expr\\": \\"test expression\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
>
|
||||
<span
|
||||
data-component="withI18nextTranslation(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="withI18nextTranslation(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="withI18nextTranslation(Form)"
|
||||
data-props="{
|
||||
\\"syntax\\": \\"invalid\\",
|
||||
\\"expr\\": \\"test expression\\",
|
||||
\\"syntaxList\\": [
|
||||
{
|
||||
\\"id\\": \\"testJS\\",
|
||||
\\"label\\": \\"Testing JS\\"
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"other\\",
|
||||
\\"label\\": \\"Other\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
/>
|
||||
<span
|
||||
data-component="withI18nextTranslation(Loader)"
|
||||
data-props="{}"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`App rendering with an invalid syntax 3`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="withI18nextTranslation(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>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
+186
-120
@@ -1,165 +1,231 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation, Trans } from 'react-i18next';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import URLSearchParams from '@ungap/url-search-params';
|
||||
import { translate } from 'react-i18next';
|
||||
import URLSearchParams from '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 FormActions from 'components/FormActions';
|
||||
import Loader from 'components/Loader';
|
||||
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 {
|
||||
static propTypes = {
|
||||
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 = {}
|
||||
|
||||
state = {
|
||||
loading: false,
|
||||
loadingError: null,
|
||||
render: {}
|
||||
}
|
||||
image = React.createRef()
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.expr) {
|
||||
this.handleRender();
|
||||
}
|
||||
window.addEventListener('hashchange', this.handleHashChange);
|
||||
window.addEventListener('beforeinstallprompt', this.handleInstallPrompt);
|
||||
this.handleHashChange();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { syntax, expr } = this.props;
|
||||
|
||||
if (syntax !== prevProps.syntax || expr !== prevProps.expr) {
|
||||
this.handleRender();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('hashchange', this.handleHashChange);
|
||||
window.removeEventListener('beforeinstallprompt', this.handleInstallPrompt);
|
||||
}
|
||||
|
||||
handleSubmit = ({ syntax, expr }) => {
|
||||
if (expr) {
|
||||
document.location.hash = new URLSearchParams({
|
||||
syntax,
|
||||
expr
|
||||
}).toString();
|
||||
}
|
||||
}
|
||||
|
||||
handleRender = async () => {
|
||||
const { syntax, expr } = this.props;
|
||||
async setSvgUrl() {
|
||||
try {
|
||||
const type = 'image/svg+xml';
|
||||
const blob = await this.image.current.svgUrl(type);
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
loadingError: null,
|
||||
render: {}
|
||||
svgUrl: {
|
||||
url: URL.createObjectURL(blob),
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
try {
|
||||
const syntaxModule = await import(
|
||||
/* webpackChunkName: "render-[index]" */
|
||||
`syntax/${ syntax }`
|
||||
);
|
||||
|
||||
const exprData = syntaxModule.layout(syntaxModule.parse(expr));
|
||||
|
||||
await this.loadSVGComponent();
|
||||
console.log(syntax, expr); // eslint-disable-line no-console
|
||||
this.setState({
|
||||
loading: false,
|
||||
render: {
|
||||
image: demoImage,
|
||||
permalinkUrl: document.location.toString(),
|
||||
syntax,
|
||||
exprData,
|
||||
Component: syntaxModule.Render
|
||||
}
|
||||
expr
|
||||
}, async () => {
|
||||
await this.image.current.doReflow();
|
||||
this.setSvgUrl();
|
||||
this.setPngUrl();
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
Sentry.withScope(scope => {
|
||||
scope.setExtra('syntax', syntax);
|
||||
Sentry.captureException(e);
|
||||
});
|
||||
this.setState({
|
||||
loading: false,
|
||||
loadingError: e
|
||||
});
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
console.error(e); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
handleRetry = event => {
|
||||
handleRetry = async event => {
|
||||
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() {
|
||||
const {
|
||||
syntax,
|
||||
expr,
|
||||
permalinkUrl,
|
||||
syntaxList,
|
||||
t
|
||||
} = this.props;
|
||||
const {
|
||||
SVG,
|
||||
loading,
|
||||
loadingError,
|
||||
imageDetails,
|
||||
render: {
|
||||
syntax: renderSyntax,
|
||||
exprData,
|
||||
Component
|
||||
}
|
||||
} = this.state;
|
||||
|
||||
|
||||
const formProps = {
|
||||
onSubmit: this.handleSubmit,
|
||||
loadingFailed,
|
||||
svgUrl,
|
||||
pngUrl,
|
||||
permalinkUrl,
|
||||
syntax,
|
||||
expr,
|
||||
syntaxList
|
||||
};
|
||||
const actionProps = {
|
||||
imageDetails,
|
||||
permalinkUrl
|
||||
};
|
||||
const renderProps = {
|
||||
onRender: this.handleSvg,
|
||||
data: exprData
|
||||
};
|
||||
image,
|
||||
installPrompt
|
||||
} = this.state;
|
||||
const downloadUrls = [
|
||||
svgUrl,
|
||||
pngUrl
|
||||
].filter(Boolean);
|
||||
|
||||
const doRender = renderSyntax === syntax;
|
||||
|
||||
return <>
|
||||
<Form { ...formProps }>
|
||||
{ doRender && <FormActions { ...actionProps } /> }
|
||||
</Form>
|
||||
|
||||
{ loading && <Loader /> }
|
||||
|
||||
{ loadingError && <Message type="error" heading={ t('Render Failure') }>
|
||||
<p><Trans>
|
||||
An error occurred while rendering the regular expression.
|
||||
</Trans></p>
|
||||
<a href="#retry" onClick={ this.handleRetry }><Trans>Retry</Trans></a>
|
||||
</Message> }
|
||||
|
||||
{ doRender && <Component { ...renderProps } /> }
|
||||
</>;
|
||||
return <React.Fragment>
|
||||
<Form
|
||||
key={ toUrl({ expr, syntax }) }
|
||||
syntaxes={ syntaxes }
|
||||
downloadUrls={ downloadUrls }
|
||||
permalinkUrl={ permalinkUrl }
|
||||
syntax={ syntax }
|
||||
expr={ expr }
|
||||
onSubmit={ this.handleSubmit }/>
|
||||
{
|
||||
loading && <div className={ style.loader }>
|
||||
<LoaderIcon />
|
||||
<div className={ style.message }>Loading...</div>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
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 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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: var(--spacing-margin) 0;
|
||||
padding: 2rem;
|
||||
background: var(--color-white);
|
||||
color: var(--color-black);
|
||||
@@ -13,7 +26,6 @@
|
||||
& .message {
|
||||
font-weight: bold;
|
||||
font-size: 2.5rem;
|
||||
margin-top: 2rem;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -21,8 +33,8 @@
|
||||
& svg {
|
||||
display: block;
|
||||
transform: scaleZ(1); /* Move to separate render layer in Chrome */
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
stroke: var(--color-black);
|
||||
animation: loader-spin 1s steps(8) infinite;
|
||||
|
||||
@@ -1,90 +1,16 @@
|
||||
jest.mock('components/Form', () =>
|
||||
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'));
|
||||
jest.mock('components/SVG');
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'react-testing-library';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { mockT } from 'i18n';
|
||||
import { App } from 'components/App';
|
||||
|
||||
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 };
|
||||
import { translate } from '__mocks__/i18n';
|
||||
|
||||
describe('App', () => {
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<App expr="" syntax="js" { ...commonProps } />
|
||||
const component = shallow(
|
||||
<App t={ translate }/>
|
||||
);
|
||||
expect(asFragment()).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();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,30 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Footer rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<footer
|
||||
class="footer"
|
||||
>
|
||||
<ul
|
||||
class="list"
|
||||
>
|
||||
<footer>
|
||||
<ul>
|
||||
<li>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
<Trans>
|
||||
Created by
|
||||
<a
|
||||
href="mailto:jeff.avallone@gmail.com"
|
||||
>
|
||||
Jeff Avallone
|
||||
</a>
|
||||
</span>
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
<Trans
|
||||
i18nKey="Generated images licensed"
|
||||
>
|
||||
Generated images licensed:
|
||||
<a
|
||||
@@ -33,18 +24,17 @@ exports[`Footer rendering 1`] = `
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt="TRANSLATE(Creative Commons CC-BY-3.0 License)"
|
||||
src="cc-by.svg"
|
||||
alt="Creative Commons CC-BY-3.0 License"
|
||||
src="https://licensebuttons.net/l/by/3.0/80x15.png"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
class="buildId"
|
||||
className="buildId"
|
||||
>
|
||||
abc-123
|
||||
example build id
|
||||
</div>
|
||||
</footer>
|
||||
</DocumentFragment>
|
||||
</footer>
|
||||
`;
|
||||
|
||||
@@ -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,37 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation, Trans } from 'react-i18next';
|
||||
import { translate, Trans } from 'react-i18next';
|
||||
|
||||
import ccLogo from './cc-by.svg';
|
||||
import style from './style.css';
|
||||
|
||||
import style from './style.module.css';
|
||||
|
||||
export const Footer = ({ t, buildId }) => (
|
||||
<footer className={ style.footer }>
|
||||
<ul className={ style.list }>
|
||||
const Footer = () => (
|
||||
<footer>
|
||||
<ul>
|
||||
<li>
|
||||
<Trans>Created by <a
|
||||
href="mailto:jeff.avallone@gmail.com">Jeff Avallone</a></Trans>
|
||||
<Trans>Created by <a href="mailto:jeff.avallone@gmail.com">Jeff Avallone</a></Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>Generated images licensed: <a
|
||||
href="http://creativecommons.org/licenses/by/3.0/"
|
||||
rel="license external noopener noreferrer"
|
||||
target="_blank">
|
||||
<img src={ ccLogo }
|
||||
alt={ t('Creative Commons CC-BY-3.0 License') } />
|
||||
</a></Trans>
|
||||
<Trans i18nKey="Generated images licensed">
|
||||
Generated images licensed: <a rel="license external noopener noreferrer" target="_blank" href="http://creativecommons.org/licenses/by/3.0/">
|
||||
<img
|
||||
alt="Creative Commons CC-BY-3.0 License"
|
||||
src="https://licensebuttons.net/l/by/3.0/80x15.png" />
|
||||
</a>
|
||||
</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
<div className={ style.buildId }>
|
||||
{ buildId }
|
||||
</div>
|
||||
<div className={ style.buildId }>{ process.env.BUILD_ID }</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
Footer.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
buildId: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default withTranslation()(Footer);
|
||||
export default translate()(Footer);
|
||||
export { Footer };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import url('../../globals.module.css');
|
||||
@import url('../../globals.css');
|
||||
|
||||
.footer {
|
||||
footer {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin: var(--spacing-margin) 0;
|
||||
@@ -9,18 +9,19 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
& ul {
|
||||
@apply --inline-list;
|
||||
@apply --with-separator-left;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
& img {
|
||||
vertical-align: text-top;
|
||||
width: 80px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
composes: inline-list with-separator-left;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.buildId {
|
||||
& .buildId {
|
||||
color: color(var(--color-brown) blend(var(--color-tan) 25%));
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-testing-library';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { mockT } from 'i18n';
|
||||
import { Footer } from 'components/Footer';
|
||||
|
||||
describe('Footer', () => {
|
||||
beforeEach(() => {
|
||||
process.env.BUILD_ID = 'example build id';
|
||||
});
|
||||
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<Footer buildId="abc-123" t={ mockT } />
|
||||
const component = shallow(
|
||||
<Footer/>
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,54 +1,194 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Form rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="form"
|
||||
data-requires-js="true"
|
||||
>
|
||||
<div
|
||||
className="form"
|
||||
>
|
||||
<form
|
||||
data-testid="form"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<textarea
|
||||
data-testid="expr-input"
|
||||
autoFocus={true}
|
||||
name="expr"
|
||||
placeholder="TRANSLATE(Enter regular expression to display)"
|
||||
onChange={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
placeholder="translate(Enter regular expression to display)"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
<Trans>
|
||||
Display
|
||||
</span>
|
||||
</Trans>
|
||||
</button>
|
||||
<div
|
||||
class="select"
|
||||
className="select"
|
||||
>
|
||||
<select
|
||||
data-testid="syntax-select"
|
||||
name="syntax"
|
||||
onChange={[Function]}
|
||||
value="js"
|
||||
>
|
||||
<option
|
||||
value="testJS"
|
||||
key="js"
|
||||
value="js"
|
||||
>
|
||||
TRANSLATE(Testing JS)
|
||||
Javascript
|
||||
</option>
|
||||
<option
|
||||
value="other"
|
||||
key="pcre"
|
||||
value="pcre"
|
||||
>
|
||||
TRANSLATE(Other)
|
||||
PCRE
|
||||
</option>
|
||||
</select>
|
||||
<span
|
||||
data-component="ChevronsDown"
|
||||
data-props="{}"
|
||||
<SvgMock />
|
||||
</div>
|
||||
<ul
|
||||
className="actions"
|
||||
/>
|
||||
</div>
|
||||
Actions
|
||||
</form>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
</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>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,38 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation, 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';
|
||||
|
||||
class Form extends React.PureComponent {
|
||||
static propTypes = {
|
||||
expr: PropTypes.string,
|
||||
syntax: PropTypes.string,
|
||||
syntaxList: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string
|
||||
})),
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node
|
||||
]),
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
state = {
|
||||
expr: this.props.expr,
|
||||
syntax: this.props.syntax
|
||||
syntax: this.props.syntax || Object.keys(this.props.syntaxes)[0]
|
||||
}
|
||||
|
||||
handleSubmit = event => {
|
||||
event.preventDefault();
|
||||
|
||||
const { expr, syntax } = this.state;
|
||||
|
||||
this.props.onSubmit({ expr, syntax });
|
||||
this.props.onSubmit.call(this, {
|
||||
expr: this.state.expr,
|
||||
syntax: this.state.syntax
|
||||
});
|
||||
}
|
||||
|
||||
handleKeyPress = event => {
|
||||
@@ -41,22 +28,41 @@ class Form extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = event => this.setState({
|
||||
[event.target.name]: event.target.value
|
||||
})
|
||||
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 {
|
||||
syntaxList,
|
||||
children,
|
||||
t
|
||||
} = this.props;
|
||||
const { syntaxes, t } = this.props;
|
||||
const { expr, syntax } = this.state;
|
||||
|
||||
return <div className={ style.form } data-requires-js>
|
||||
<form data-testid="form" onSubmit={ this.handleSubmit }>
|
||||
return <div className={ style.form }>
|
||||
<form onSubmit={ this.handleSubmit }>
|
||||
<textarea
|
||||
data-testid="expr-input"
|
||||
name="expr"
|
||||
value={ expr }
|
||||
onKeyPress={ this.handleKeyPress }
|
||||
@@ -66,21 +72,34 @@ class Form extends React.PureComponent {
|
||||
<button type="submit"><Trans>Display</Trans></button>
|
||||
<div className={ style.select }>
|
||||
<select
|
||||
data-testid="syntax-select"
|
||||
name="syntax"
|
||||
value={ syntax }
|
||||
onChange={ this.handleChange } >
|
||||
{ syntaxList.map(({ id, label }) => (
|
||||
<option value={ id } key={ id }>{ t(label) }</option>
|
||||
onChange={ this.handleChange }>
|
||||
{ Object.keys(syntaxes).map(id => (
|
||||
<option value={ id } key={ id }>{ syntaxes[id] }</option>
|
||||
)) }
|
||||
</select>
|
||||
<ExpandIcon />
|
||||
<ExpandIcon/>
|
||||
</div>
|
||||
{ children }
|
||||
<ul className={ style.actions }>
|
||||
{ this.downloadActions() }
|
||||
{ this.permalinkAction() }
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
Form.propTypes = {
|
||||
expr: PropTypes.string,
|
||||
syntax: PropTypes.string,
|
||||
syntaxes: PropTypes.object,
|
||||
onSubmit: PropTypes.func,
|
||||
permalinkUrl: PropTypes.string,
|
||||
downloadUrls: PropTypes.array,
|
||||
t: PropTypes.func
|
||||
};
|
||||
|
||||
export default translate()(Form);
|
||||
export { Form };
|
||||
export default withTranslation()(Form);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import url('../../globals.module.css');
|
||||
@import url('../../globals.css');
|
||||
|
||||
:root {
|
||||
--control-gradient: var(--color-green) var(--gradient-green);
|
||||
@@ -23,7 +23,7 @@
|
||||
width: 100% !important; /* "!important" to prevent user changing width */
|
||||
height: calc(3 * var(--entry-line-height));
|
||||
box-sizing: border-box;
|
||||
font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
|
||||
}
|
||||
|
||||
& textarea::placeholder {
|
||||
@@ -44,6 +44,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
composes: fancy-select;
|
||||
.actions {
|
||||
@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;
|
||||
}
|
||||
+56
-44
@@ -1,81 +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 { render, fireEvent } from 'react-testing-library';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { mockT } from 'i18n';
|
||||
import { Form } from 'components/Form';
|
||||
import { translate } from '__mocks__/i18n';
|
||||
|
||||
const syntaxList = [
|
||||
{ id: 'testJS', label: 'Testing JS' },
|
||||
{ id: 'other', label: 'Other' }
|
||||
];
|
||||
const commonProps = { syntaxList, t: mockT };
|
||||
const syntaxes = {
|
||||
js: 'Javascript',
|
||||
pcre: 'PCRE'
|
||||
};
|
||||
|
||||
describe('Form', () => {
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<Form onSubmit={ jest.fn() } { ...commonProps }>
|
||||
Actions
|
||||
</Form>
|
||||
const component = shallow(
|
||||
<Form t={ translate } syntaxes={ syntaxes }/>
|
||||
);
|
||||
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', () => {
|
||||
test('submitting form', () => {
|
||||
const onSubmit = jest.fn();
|
||||
const { getByTestId } = render(
|
||||
<Form onSubmit={ onSubmit } { ...commonProps } />
|
||||
const component = shallow(
|
||||
<Form t={ translate } syntaxes={ syntaxes } onSubmit={ onSubmit }/>
|
||||
);
|
||||
|
||||
fireEvent.change(getByTestId('expr-input'), {
|
||||
target: { value: 'Test expression' }
|
||||
});
|
||||
fireEvent.change(getByTestId('syntax-select'), {
|
||||
target: { value: 'other' }
|
||||
});
|
||||
const exprInput = component.find('[name="expr"]');
|
||||
const syntaxInput = component.find('[name="syntax"]');
|
||||
exprInput.simulate('change', { target: { name: 'expr', value: 'Test expression' } });
|
||||
syntaxInput.simulate('change', { target: { name: 'syntax', value: 'test' } });
|
||||
|
||||
const event = new Event('submit');
|
||||
jest.spyOn(event, 'preventDefault');
|
||||
const eventObj = { preventDefault: jest.fn() };
|
||||
component.find('form').simulate('submit', eventObj);
|
||||
|
||||
fireEvent(getByTestId('form'), event);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
expect(eventObj.preventDefault).toHaveBeenCalled();
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
expr: 'Test expression',
|
||||
syntax: 'other'
|
||||
syntax: 'test'
|
||||
});
|
||||
});
|
||||
|
||||
test('submitting form with Shift+Enter', () => {
|
||||
const onSubmit = jest.fn();
|
||||
const { getByTestId } = render(
|
||||
<Form onSubmit={ onSubmit } { ...commonProps } />
|
||||
const component = shallow(
|
||||
<Form t={ translate } syntaxes={ syntaxes } onSubmit={ Function.prototype }/>
|
||||
);
|
||||
|
||||
fireEvent.keyPress(getByTestId('expr-input'), {
|
||||
const form = component.instance();
|
||||
const eventObj = {
|
||||
preventDefault: Function.prototype,
|
||||
charCode: 13,
|
||||
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', () => {
|
||||
const onSubmit = jest.fn();
|
||||
const { getByTestId } = render(
|
||||
<Form onSubmit={ onSubmit } { ...commonProps } />
|
||||
const component = shallow(
|
||||
<Form t={ translate } syntaxes={ syntaxes } onSubmit={ Function.protoytpe }/>
|
||||
);
|
||||
|
||||
fireEvent.keyPress(getByTestId('expr-input'), {
|
||||
const form = component.instance();
|
||||
const eventObj = {
|
||||
preventDefault: Function.prototype,
|
||||
charCode: 13,
|
||||
shiftKey: false
|
||||
});
|
||||
};
|
||||
jest.spyOn(form, 'handleSubmit');
|
||||
component.find('textarea').simulate('keypress', eventObj);
|
||||
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
expect(form.handleSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FormActions rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<ul
|
||||
class="actions"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`FormActions rendering download links 1`] = `
|
||||
<DocumentFragment>
|
||||
<ul
|
||||
class="actions"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
download="image.png"
|
||||
href="http://example.com/image.png"
|
||||
type="image/png"
|
||||
>
|
||||
<span
|
||||
data-component="Download"
|
||||
data-props="{}"
|
||||
/>
|
||||
TRANSLATE(Example PNG Link)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
download="image.svg"
|
||||
href="http://example.com/image.svg"
|
||||
type="image/svg+xml"
|
||||
>
|
||||
<span
|
||||
data-component="Download"
|
||||
data-props="{}"
|
||||
/>
|
||||
TRANSLATE(Example SVG Link)
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`FormActions rendering download links with data after mounting 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>
|
||||
`;
|
||||
|
||||
exports[`FormActions rendering download links with data after mounting 2`] = `
|
||||
<DocumentFragment>
|
||||
<ul
|
||||
class="actions"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
download="image.png"
|
||||
href="http://example.com/image.png"
|
||||
type="image/png"
|
||||
>
|
||||
<span
|
||||
data-component="Download"
|
||||
data-props="{}"
|
||||
/>
|
||||
TRANSLATE(Example PNG Link)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
download="image.svg"
|
||||
href="http://example.com/image.svg"
|
||||
type="image/svg+xml"
|
||||
>
|
||||
<span
|
||||
data-component="Download"
|
||||
data-props="{}"
|
||||
/>
|
||||
TRANSLATE(Example SVG Link)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="http://example.com"
|
||||
>
|
||||
<span
|
||||
data-component="Link"
|
||||
data-props="{}"
|
||||
/>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Permalink
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`FormActions rendering download links with data after mounting 3`] = `
|
||||
<DocumentFragment>
|
||||
<ul
|
||||
class="actions"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
download="image.png"
|
||||
href="http://example.com/image.png"
|
||||
type="image/png"
|
||||
>
|
||||
<span
|
||||
data-component="Download"
|
||||
data-props="{}"
|
||||
/>
|
||||
TRANSLATE(Example PNG Link)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
download="image.svg"
|
||||
href="http://example.com/image.svg"
|
||||
type="image/svg+xml"
|
||||
>
|
||||
<span
|
||||
data-component="Download"
|
||||
data-props="{}"
|
||||
/>
|
||||
TRANSLATE(Example SVG Link)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="http://example.com"
|
||||
>
|
||||
<span
|
||||
data-component="Link"
|
||||
data-props="{}"
|
||||
/>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Permalink
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</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,97 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation, 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';
|
||||
|
||||
class FormActions extends React.PureComponent {
|
||||
static propTypes = {
|
||||
permalinkUrl: PropTypes.string,
|
||||
imageDetails: PropTypes.shape({
|
||||
svg: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number
|
||||
}),
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
state = {
|
||||
svgLink: null,
|
||||
pngLink: null
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { imageDetails } = this.props;
|
||||
|
||||
if (imageDetails && imageDetails.svg) {
|
||||
this.generateDownloadLinks();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { imageDetails } = this.props;
|
||||
const { imageDetails: prevImageDetails } = prevProps;
|
||||
|
||||
if (!imageDetails) {
|
||||
this.setState({ svgLink: null, pngLink: null });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prevImageDetails) {
|
||||
this.generateDownloadLinks();
|
||||
return;
|
||||
}
|
||||
|
||||
if (imageDetails.svg !== prevImageDetails.svg
|
||||
|| imageDetails.width !== prevImageDetails.width
|
||||
|| imageDetails.height !== prevImageDetails.height) {
|
||||
this.generateDownloadLinks();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async generateDownloadLinks() {
|
||||
const { imageDetails: { svg, width, height } } = this.props;
|
||||
|
||||
this.setState({
|
||||
svgLink: await createSvgLink({ svg }),
|
||||
pngLink: await createPngLink({ svg, width, height })
|
||||
});
|
||||
}
|
||||
|
||||
downloadLink({ url, filename, type, label }) {
|
||||
const { t } = this.props;
|
||||
|
||||
return <li>
|
||||
<a href={ url } download={ filename } type={ type }>
|
||||
<DownloadIcon />{ t(label) }
|
||||
</a>
|
||||
</li>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
permalinkUrl
|
||||
} = this.props;
|
||||
const {
|
||||
svgLink,
|
||||
pngLink
|
||||
} = this.state;
|
||||
|
||||
return <ul className={ style.actions }>
|
||||
{ pngLink && this.downloadLink(pngLink) }
|
||||
{ svgLink && this.downloadLink(svgLink) }
|
||||
{ permalinkUrl && <li>
|
||||
<a href={ permalinkUrl }><LinkIcon /><Trans>Permalink</Trans></a>
|
||||
</li> }
|
||||
</ul>;
|
||||
}
|
||||
}
|
||||
|
||||
export { FormActions };
|
||||
export default withTranslation()(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,96 +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 { mockT } from 'i18n';
|
||||
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 t={ mockT } />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('rendering with a permalink', () => {
|
||||
const { asFragment } = render(
|
||||
<FormActions permalinkUrl="http://example.com" t={ mockT } />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('rendering download links', async () => {
|
||||
const imageDetails = {
|
||||
svg: 'test image',
|
||||
width: 10,
|
||||
height: 20
|
||||
};
|
||||
|
||||
const { asFragment } = render(
|
||||
<FormActions imageDetails={ imageDetails } t={ mockT } />
|
||||
);
|
||||
|
||||
// Give a beat for mocked promises to resolve
|
||||
await new Promise(resolve => setTimeout(resolve));
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('rendering download links with data after mounting', async () => {
|
||||
const { asFragment, rerender } = render(
|
||||
<FormActions t={ mockT } />
|
||||
);
|
||||
|
||||
rerender(
|
||||
<FormActions permalinkUrl="http://example.com" t={ mockT } />
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
rerender(
|
||||
<FormActions
|
||||
permalinkUrl="http://example.com"
|
||||
imageDetails={ { svg: 'test-image' } }
|
||||
t={ mockT } />
|
||||
);
|
||||
|
||||
// Give a beat for mocked promises to resolve
|
||||
await new Promise(resolve => setTimeout(resolve));
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
rerender(
|
||||
<FormActions
|
||||
permalinkUrl="http://example.com"
|
||||
imageDetails={ { svg: 'test-image', width: 10, height: 20 } }
|
||||
t={ mockT } />
|
||||
);
|
||||
|
||||
// Give a beat for mocked promises to resolve
|
||||
await new Promise(resolve => setTimeout(resolve));
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,402 +1,10 @@
|
||||
// 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="withI18nextTranslation(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="withI18nextTranslation(InstallPrompt)"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
data-requires-js="true"
|
||||
>
|
||||
<span
|
||||
data-component="withI18nextTranslation(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="withI18nextTranslation(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="withI18nextTranslation(InstallPrompt)"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
data-requires-js="true"
|
||||
>
|
||||
<span
|
||||
data-component="withI18nextTranslation(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="withI18nextTranslation(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="withI18nextTranslation(InstallPrompt)"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
data-requires-js="true"
|
||||
>
|
||||
<span
|
||||
data-component="withI18nextTranslation(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="withI18nextTranslation(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="withI18nextTranslation(InstallPrompt)"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
data-requires-js="true"
|
||||
>
|
||||
<span
|
||||
data-component="withI18nextTranslation(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="withI18nextTranslation(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="withI18nextTranslation(InstallPrompt)"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
data-requires-js="true"
|
||||
>
|
||||
<span
|
||||
data-component="withI18nextTranslation(LocaleSwitcher)"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`Header rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Modal"
|
||||
data-props="{
|
||||
\\"isOpen\\": false
|
||||
}"
|
||||
>
|
||||
<span
|
||||
data-component="withI18nextTranslation(PrivacyPolicy)"
|
||||
data-props="{}"
|
||||
/>
|
||||
</span>
|
||||
<header
|
||||
class="header"
|
||||
<header
|
||||
className="header"
|
||||
data-banner="testing"
|
||||
>
|
||||
>
|
||||
<h1>
|
||||
<a
|
||||
href="/"
|
||||
@@ -404,131 +12,31 @@ exports[`Header rendering 1`] = `
|
||||
Regexper
|
||||
</a>
|
||||
</h1>
|
||||
<ul
|
||||
class="list"
|
||||
>
|
||||
<ul>
|
||||
<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="{}"
|
||||
>
|
||||
<SvgMock />
|
||||
<Trans>
|
||||
Source on GitLab
|
||||
</span>
|
||||
</Trans>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
data-testid="privacy-link"
|
||||
href="/privacy"
|
||||
>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
href="/privacy.html"
|
||||
>
|
||||
<Trans>
|
||||
Privacy Policy
|
||||
</span>
|
||||
</Trans>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
data-component="withI18nextTranslation(InstallPrompt)"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
data-requires-js="true"
|
||||
>
|
||||
<span
|
||||
data-component="withI18nextTranslation(LocaleSwitcher)"
|
||||
data-props="{}"
|
||||
/>
|
||||
<Translate(LocaleSwitcher) />
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`Header rendering with no banner 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="Modal"
|
||||
data-props="{
|
||||
\\"isOpen\\": false
|
||||
}"
|
||||
>
|
||||
<span
|
||||
data-component="withI18nextTranslation(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="withI18nextTranslation(InstallPrompt)"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
data-requires-js="true"
|
||||
>
|
||||
<span
|
||||
data-component="withI18nextTranslation(LocaleSwitcher)"
|
||||
data-props="{}"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
</DocumentFragment>
|
||||
</header>
|
||||
`;
|
||||
|
||||
@@ -1,87 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-modal';
|
||||
import { Link } from 'gatsby';
|
||||
import { withTranslation, Trans } from 'react-i18next';
|
||||
import { translate, 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 InstallPrompt from 'components/InstallPrompt';
|
||||
import PrivacyPolicy from 'components/PrivacyPolicy';
|
||||
|
||||
import style from './style.module.css';
|
||||
|
||||
class Header extends React.PureComponent {
|
||||
state = {
|
||||
showModal: false
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
banner: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.string
|
||||
]).isRequired
|
||||
}
|
||||
|
||||
handleOpen = event => {
|
||||
if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
this.setState({ showModal: true });
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
this.setState({ showModal: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { banner } = this.props;
|
||||
const { showModal } = this.state;
|
||||
|
||||
return <>
|
||||
<Modal
|
||||
isOpen={ showModal }
|
||||
onRequestClose={ this.handleClose }>
|
||||
<PrivacyPolicy onClose={ this.handleClose } />
|
||||
</Modal>
|
||||
<header
|
||||
className={ style.header }
|
||||
data-banner={ banner || null }>
|
||||
const Header = () => (
|
||||
<header className={ style.header } data-banner={ process.env.BANNER }>
|
||||
<h1>
|
||||
<Link to="/">Regexper</Link>
|
||||
<a href="/">Regexper</a>
|
||||
</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>
|
||||
<a href="https://gitlab.com/javallone/regexper-static"
|
||||
rel="external noopener noreferrer"
|
||||
target="_blank">
|
||||
<GitlabIcon />
|
||||
<Trans>Source on GitLab</Trans>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/privacy"
|
||||
data-testid="privacy-link"
|
||||
onClick={ this.handleOpen }
|
||||
>
|
||||
<Trans>Privacy Policy</Trans>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<InstallPrompt />
|
||||
</li>
|
||||
<li data-requires-js>
|
||||
<LocaleSwitcher />
|
||||
<a href="/privacy.html"><Trans>Privacy Policy</Trans></a>
|
||||
</li>
|
||||
<li><LocaleSwitcher /></li>
|
||||
</ul>
|
||||
</header>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default translate()(Header);
|
||||
export { Header };
|
||||
export default withTranslation()(Header);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import url('../../globals.module.css');
|
||||
@import url('../../globals.css');
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
@@ -9,7 +9,6 @@
|
||||
margin: 0 calc(-1 * var(--content-margin)) var(--spacing-margin) calc(-1 * var(--content-margin));
|
||||
position: relative;
|
||||
color: var(--color-black);
|
||||
min-width: 320px;
|
||||
|
||||
&:after {
|
||||
content: attr(data-banner);
|
||||
@@ -42,12 +41,13 @@
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
composes: inline-list with-separator-right;
|
||||
& ul {
|
||||
@apply --inline-list;
|
||||
@apply --with-separator-right;
|
||||
text-align: right;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
& li {
|
||||
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 { render, fireEvent } from 'react-testing-library';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Header } from 'components/Header';
|
||||
|
||||
describe('Header', () => {
|
||||
beforeEach(() => {
|
||||
process.env.BANNER = 'testing';
|
||||
});
|
||||
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<Header banner="testing" />
|
||||
const component = shallow(
|
||||
<Header/>
|
||||
);
|
||||
expect(asFragment()).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();
|
||||
});
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,57 +1,31 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`InstallPrompt accepting install prompt 1`] = `
|
||||
<DocumentFragment>
|
||||
<a
|
||||
data-testid="install"
|
||||
href="#install"
|
||||
exports[`InstallPrompt rendering 1`] = `
|
||||
<div
|
||||
className="install"
|
||||
>
|
||||
<p
|
||||
className="cta"
|
||||
>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
<Trans>
|
||||
Add Regexper to your home screen?
|
||||
</Trans>
|
||||
</p>
|
||||
<div
|
||||
className="actions"
|
||||
>
|
||||
Add to Home Screen
|
||||
</span>
|
||||
</a>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`InstallPrompt accepting install prompt 2`] = `<DocumentFragment />`;
|
||||
|
||||
exports[`InstallPrompt rejecting install prompt 1`] = `
|
||||
<DocumentFragment>
|
||||
<a
|
||||
data-testid="install"
|
||||
href="#install"
|
||||
>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Add to Home Screen
|
||||
</span>
|
||||
</a>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`InstallPrompt rejecting install prompt 2`] = `<DocumentFragment />`;
|
||||
|
||||
exports[`InstallPrompt rendering 1`] = `<DocumentFragment />`;
|
||||
|
||||
exports[`InstallPrompt rendering after an install prompt has been requested 1`] = `<DocumentFragment />`;
|
||||
|
||||
exports[`InstallPrompt rendering after an install prompt has been requested 2`] = `
|
||||
<DocumentFragment>
|
||||
<a
|
||||
data-testid="install"
|
||||
href="#install"
|
||||
>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
Add to Home Screen
|
||||
</span>
|
||||
</a>
|
||||
</DocumentFragment>
|
||||
<button
|
||||
className="primary"
|
||||
>
|
||||
<Trans>
|
||||
Add It
|
||||
</Trans>
|
||||
</button>
|
||||
<button>
|
||||
<Trans>
|
||||
No Thanks
|
||||
</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,56 +1,23 @@
|
||||
import React from 'react';
|
||||
import { withTranslation, Trans } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate, Trans } from 'react-i18next';
|
||||
|
||||
class InstallPrompt extends React.PureComponent {
|
||||
state = {
|
||||
installPrompt: null
|
||||
}
|
||||
import style from './style.css';
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('beforeinstallprompt', this.handleInstallPrompt);
|
||||
}
|
||||
const InstallPrompt = ({ onAccept, onReject }) => (
|
||||
<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>
|
||||
);
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('beforeinstallprompt', this.handleInstallPrompt);
|
||||
}
|
||||
|
||||
handleInstallPrompt = event => {
|
||||
this.setState({
|
||||
installPrompt: event
|
||||
});
|
||||
}
|
||||
|
||||
handleInstall = async event => {
|
||||
event.preventDefault();
|
||||
|
||||
const { installPrompt } = this.state;
|
||||
|
||||
try {
|
||||
installPrompt.prompt();
|
||||
await installPrompt.userChoice;
|
||||
}
|
||||
catch {
|
||||
// User cancelled install
|
||||
}
|
||||
|
||||
this.setState({ installPrompt: null });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { installPrompt } = this.state;
|
||||
|
||||
if (!installPrompt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <a href="#install"
|
||||
data-testid="install"
|
||||
onClick={ this.handleInstall }
|
||||
>
|
||||
<Trans>Add to Home Screen</Trans>
|
||||
</a>;
|
||||
}
|
||||
}
|
||||
InstallPrompt.propTypes = {
|
||||
onAccept: PropTypes.func.isRequired,
|
||||
onReject: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default translate()(InstallPrompt);
|
||||
export { InstallPrompt };
|
||||
export default withTranslation()(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,86 +1,14 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from 'react-testing-library';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { InstallPrompt } from 'components/InstallPrompt';
|
||||
import { translate } from '__mocks__/i18n';
|
||||
|
||||
describe('InstallPrompt', () => {
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<InstallPrompt />
|
||||
const component = shallow(
|
||||
<InstallPrompt t={ translate }/>
|
||||
);
|
||||
expect(asFragment()).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));
|
||||
});
|
||||
|
||||
test('accepting install prompt', async () => {
|
||||
const { asFragment, getByTestId } = render(
|
||||
<InstallPrompt />
|
||||
);
|
||||
const promptEvent = new Event('beforeinstallprompt');
|
||||
promptEvent.prompt = jest.fn();
|
||||
promptEvent.userChoice = Promise.resolve();
|
||||
const clickEvent = new MouseEvent('click', { bubbles: true });
|
||||
jest.spyOn(clickEvent, 'preventDefault');
|
||||
|
||||
fireEvent(window, promptEvent);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
fireEvent(getByTestId('install'), clickEvent);
|
||||
|
||||
// Allow async code to run
|
||||
await new Promise(resolve => setTimeout(resolve));
|
||||
|
||||
expect(clickEvent.preventDefault).toHaveBeenCalled();
|
||||
expect(promptEvent.prompt).toHaveBeenCalled();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('rejecting install prompt', async () => {
|
||||
const { asFragment, getByTestId } = render(
|
||||
<InstallPrompt />
|
||||
);
|
||||
const promptEvent = new Event('beforeinstallprompt');
|
||||
promptEvent.prompt = jest.fn();
|
||||
promptEvent.userChoice = Promise.reject();
|
||||
const clickEvent = new MouseEvent('click', { bubbles: true });
|
||||
jest.spyOn(clickEvent, 'preventDefault');
|
||||
|
||||
fireEvent(window, promptEvent);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
fireEvent(getByTestId('install'), clickEvent);
|
||||
|
||||
// Allow async code to run
|
||||
await new Promise(resolve => setTimeout(resolve));
|
||||
|
||||
expect(clickEvent.preventDefault).toHaveBeenCalled();
|
||||
expect(promptEvent.prompt).toHaveBeenCalled();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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="withI18nextTranslation(Header)"
|
||||
data-props="{
|
||||
\\"banner\\": \\"Test Banner\\"
|
||||
}"
|
||||
/>
|
||||
<span
|
||||
data-component="SentryBoundary"
|
||||
data-props="{}"
|
||||
>
|
||||
Example content
|
||||
</span>
|
||||
<span
|
||||
data-component="withI18nextTranslation(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,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import LoaderIcon from 'react-feather/dist/icons/loader';
|
||||
|
||||
import style from './style.module.css';
|
||||
|
||||
const Loader = ({ t }) => (
|
||||
<div className={ style.loader }>
|
||||
<LoaderIcon />
|
||||
<div className={ style.message }>{ t('Loading...') }</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Loader.propTypes = {
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export { Loader };
|
||||
export default withTranslation()(Loader);
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-testing-library';
|
||||
|
||||
import { mockT } from 'i18n';
|
||||
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 t={ mockT } />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,36 +1,31 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LocaleSwitcher rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<label>
|
||||
<span
|
||||
data-component="Trans"
|
||||
data-props="{}"
|
||||
>
|
||||
<label>
|
||||
<Trans>
|
||||
Language
|
||||
</span>
|
||||
</Trans>
|
||||
<div
|
||||
class="switcher"
|
||||
className="switcher"
|
||||
>
|
||||
<select
|
||||
data-testid="language-select"
|
||||
>
|
||||
<option
|
||||
onChange={[Function]}
|
||||
value="en"
|
||||
>
|
||||
English
|
||||
<option
|
||||
key="en"
|
||||
value="en"
|
||||
>
|
||||
/displayName
|
||||
</option>
|
||||
<option
|
||||
value="other"
|
||||
key="fr"
|
||||
value="fr"
|
||||
>
|
||||
Other
|
||||
/displayName
|
||||
</option>
|
||||
</select>
|
||||
<span
|
||||
data-component="ChevronsDown"
|
||||
data-props="{}"
|
||||
/>
|
||||
<SvgMock />
|
||||
</div>
|
||||
</label>
|
||||
</DocumentFragment>
|
||||
</label>
|
||||
`;
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import React from 'react';
|
||||
import { withTranslation, 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';
|
||||
import style from './style.module.css';
|
||||
const localeToAvailable = (locale, available, defaultLocale) => {
|
||||
if (available.includes(locale)) {
|
||||
return locale;
|
||||
}
|
||||
|
||||
export class LocaleSwitcher extends React.PureComponent {
|
||||
const parts = locale.split('-');
|
||||
|
||||
if (parts.length > 0 && available.includes(parts[0])) {
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
return defaultLocale;
|
||||
};
|
||||
|
||||
class LocaleSwitcher extends React.PureComponent {
|
||||
state = {
|
||||
current: localeToAvailable(
|
||||
i18n.language || '',
|
||||
locales.map(l => l.code),
|
||||
'en')
|
||||
current: localeToAvailable(i18n.language || '', Object.keys(locales), 'en')
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -38,20 +48,16 @@ export class LocaleSwitcher extends React.PureComponent {
|
||||
return <label>
|
||||
<Trans>Language</Trans>
|
||||
<div className={ style.switcher }>
|
||||
<select data-testid="language-select"
|
||||
value={ current }
|
||||
onChange={ this.handleSelectChange }
|
||||
>
|
||||
{ locales.map(locale => (
|
||||
<option value={ locale.code } key={ locale.code }>
|
||||
{ locale.name }
|
||||
</option>
|
||||
<select value={ current } onChange={ this.handleSelectChange }>
|
||||
{ Object.keys(locales).map(locale => (
|
||||
<option value={ locale } key={ locale }>{ i18n.getFixedT(locale)('/displayName') }</option>
|
||||
)) }
|
||||
</select>
|
||||
<ExpandIcon />
|
||||
<ExpandIcon/>
|
||||
</div>
|
||||
</label>;
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation()(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 {
|
||||
--control-gradient: var(--color-tan) var(--gradient-tan);
|
||||
@@ -7,5 +7,5 @@
|
||||
}
|
||||
|
||||
.switcher {
|
||||
composes: fancy-select;
|
||||
@apply --fancy-select;
|
||||
}
|
||||
@@ -1,59 +1,31 @@
|
||||
jest.mock('react-feather/dist/icons/chevrons-down', () =>
|
||||
require('__mocks__/component-mock')(
|
||||
'react-feather/dist/icons/chevrons-down'));
|
||||
jest.mock('components/SVG');
|
||||
jest.mock('locales', () => ({
|
||||
en: {},
|
||||
fr: {}
|
||||
}));
|
||||
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from 'react-testing-library';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import i18n from 'i18n';
|
||||
import { LocaleSwitcher } from 'components/LocaleSwitcher';
|
||||
|
||||
// Ensure initial locale is always "en" during tests
|
||||
jest.mock('./locale-to-available', () => jest.fn(() => 'en'));
|
||||
import { translate } from '__mocks__/i18n';
|
||||
|
||||
describe('LocaleSwitcher', () => {
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<LocaleSwitcher />
|
||||
const component = shallow(
|
||||
<LocaleSwitcher t={ translate }/>
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('changing language', () => {
|
||||
jest.spyOn(i18n, 'changeLanguage');
|
||||
|
||||
const { getByTestId } = render(
|
||||
<LocaleSwitcher />
|
||||
const component = shallow(
|
||||
<LocaleSwitcher t={ translate }/>
|
||||
);
|
||||
const event = new Event('change', { bubbles: true });
|
||||
const select = getByTestId('language-select');
|
||||
select.value = 'other';
|
||||
const selectInput = component.find('select');
|
||||
selectInput.value = 'fr';
|
||||
selectInput.simulate('change', { target: { value: 'fr' } });
|
||||
|
||||
fireEvent(select, event);
|
||||
|
||||
expect(i18n.changeLanguage).toHaveBeenCalledWith('other');
|
||||
});
|
||||
|
||||
test('interface update from language change', () => {
|
||||
const { getByTestId } = render(
|
||||
<LocaleSwitcher />
|
||||
);
|
||||
|
||||
expect(getByTestId('language-select').value).toEqual('en');
|
||||
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));
|
||||
expect(component.state('current')).toEqual('fr');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,63 +1,30 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Message rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
className="message"
|
||||
>
|
||||
<div
|
||||
class="message"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
className="header"
|
||||
>
|
||||
<h2>
|
||||
Testing
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="content"
|
||||
className="content"
|
||||
>
|
||||
<p>
|
||||
Message content
|
||||
</p>
|
||||
</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>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Message rendering with icon 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
<div
|
||||
class="message"
|
||||
>
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
>
|
||||
@@ -73,22 +40,17 @@ exports[`Message rendering with icon 1`] = `
|
||||
Message content
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Message rendering with type 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
<div
|
||||
class="message error"
|
||||
>
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
>
|
||||
<span
|
||||
data-component="AlertOctagon"
|
||||
data-props="{}"
|
||||
/>
|
||||
<svg />
|
||||
<h2>
|
||||
Testing
|
||||
</h2>
|
||||
@@ -100,6 +62,5 @@ exports[`Message rendering with type 1`] = `
|
||||
Message content
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
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 ErrorIcon from 'react-feather/dist/icons/alert-octagon';
|
||||
import WarningIcon from 'react-feather/dist/icons/alert-triangle';
|
||||
import CloseIcon from 'react-feather/dist/icons/x-square';
|
||||
import InfoIcon from 'feather-icons/dist/icons/info.svg';
|
||||
import ErrorIcon from 'feather-icons/dist/icons/alert-octagon.svg';
|
||||
import WarningIcon from 'feather-icons/dist/icons/alert-triangle.svg';
|
||||
|
||||
const iconTypes = {
|
||||
info: InfoIcon,
|
||||
@@ -15,26 +14,21 @@ const iconTypes = {
|
||||
};
|
||||
|
||||
const renderIcon = (type, icon) => {
|
||||
const Icon = icon || iconTypes[type];
|
||||
icon = icon || iconTypes[type];
|
||||
|
||||
if (!Icon) {
|
||||
if (!icon) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <Icon />;
|
||||
const Icon = icon;
|
||||
return <Icon/>;
|
||||
};
|
||||
|
||||
const Message = ({ type, icon, heading, onClose, children }) => (
|
||||
<div className={ [
|
||||
style.message,
|
||||
type && style[type]
|
||||
].filter(Boolean).join(' ') }>
|
||||
const Message = ({ type, icon, heading, children }) => (
|
||||
<div className={ [ style.message, type && style[type] ].filter(Boolean).join(' ') }>
|
||||
<div className={ style.header }>
|
||||
{ renderIcon(type, icon) }
|
||||
<h2>{ heading }</h2>
|
||||
{ onClose && <button onClick={ onClose }>
|
||||
<CloseIcon /> Close
|
||||
</button> }
|
||||
</div>
|
||||
<div className={ style.content }>
|
||||
{ children }
|
||||
@@ -53,7 +47,6 @@ Message.propTypes = {
|
||||
PropTypes.func
|
||||
]),
|
||||
heading: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(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 { render } from 'react-testing-library';
|
||||
import { shallow, render } from 'enzyme';
|
||||
|
||||
import Message from 'components/Message';
|
||||
|
||||
describe('Message', () => {
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<Message heading="Testing">
|
||||
const component = shallow(
|
||||
<Message heading="Testing" className="testing">
|
||||
<p>Message content</p>
|
||||
</Message>
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('rendering with icon', () => {
|
||||
const Icon = () => 'Sample icon SVG';
|
||||
const { asFragment } = render(
|
||||
const component = render(
|
||||
<Message heading="Testing" icon={ Icon }>
|
||||
<p>Message content</p>
|
||||
</Message>
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('rendering with type', () => {
|
||||
const { asFragment } = render(
|
||||
const component = render(
|
||||
<Message heading="Testing" type="error">
|
||||
<p>Message content</p>
|
||||
</Message>
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('rendering with a close button', () => {
|
||||
const { asFragment } = render(
|
||||
<Message heading="Testing" onClose={ jest.fn() }>
|
||||
<p>Message content</p>
|
||||
</Message>
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Metadata rendering 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="HelmetWrapper"
|
||||
data-props="{
|
||||
\\"htmlAttributes\\": {
|
||||
\\"lang\\": \\"test-lang\\"
|
||||
}
|
||||
}"
|
||||
>
|
||||
<title>
|
||||
Regexper
|
||||
</title>
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`Metadata rendering with a title and description 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
data-component="HelmetWrapper"
|
||||
data-props="{
|
||||
\\"htmlAttributes\\": {
|
||||
\\"lang\\": \\"test-lang\\"
|
||||
}
|
||||
}"
|
||||
>
|
||||
<title>
|
||||
Regexper - Testing
|
||||
</title>
|
||||
<meta
|
||||
content="Test description"
|
||||
name="description"
|
||||
/>
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
class Metadata extends React.PureComponent {
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
i18n: PropTypes.shape({
|
||||
language: PropTypes.string.isRequired
|
||||
}).isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title, description, i18n } = this.props;
|
||||
const htmlAttributes = {
|
||||
lang: i18n.language
|
||||
};
|
||||
|
||||
return <Helmet htmlAttributes={ htmlAttributes }>
|
||||
<title>{ title ? `Regexper - ${ title }` : 'Regexper' }</title>
|
||||
{ description && <meta name="description" content={ description } /> }
|
||||
</Helmet>;
|
||||
}
|
||||
}
|
||||
|
||||
export { Metadata };
|
||||
export default withTranslation()(Metadata);
|
||||
@@ -1,35 +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';
|
||||
|
||||
const commonProps = {
|
||||
i18n: { language: 'test-lang' }
|
||||
};
|
||||
|
||||
describe('Metadata', () => {
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<Metadata { ...commonProps } />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('rendering with a title and description', () => {
|
||||
const { asFragment } = render(
|
||||
<Metadata
|
||||
title="Testing"
|
||||
description="Test description"
|
||||
{ ...commonProps } />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import Message from 'components/Message';
|
||||
|
||||
export const PrivacyPolicy = ({ t, ...props }) => (
|
||||
<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>
|
||||
);
|
||||
|
||||
PrivacyPolicy.propTypes = {
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withTranslation()(PrivacyPolicy);
|
||||
@@ -1,17 +0,0 @@
|
||||
jest.mock('components/Message', () =>
|
||||
require('__mocks__/component-mock')('components/Message'));
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'react-testing-library';
|
||||
|
||||
import { mockT } from 'i18n';
|
||||
import { PrivacyPolicy } from 'components/PrivacyPolicy';
|
||||
|
||||
describe('PrivacyPolicy', () => {
|
||||
test('rendering', () => {
|
||||
const { asFragment } = render(
|
||||
<PrivacyPolicy onClose={ jest.fn() } t={ mockT } />
|
||||
);
|
||||
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,87 +0,0 @@
|
||||
import React 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>;
|
||||
};
|
||||
|
||||
class Render extends React.PureComponent {
|
||||
static propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
onRender: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
svgContainer = React.createRef()
|
||||
|
||||
componentDidMount() {
|
||||
this.provideSVGData();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.provideSVGData();
|
||||
}
|
||||
|
||||
provideSVGData() {
|
||||
if (!this.svgContainer.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const svg = this.svgContainer.current.querySelector('svg');
|
||||
this.props.onRender({
|
||||
svg: svg.outerHTML,
|
||||
width: Number(svg.getAttribute('width')),
|
||||
height: Number(svg.getAttribute('height'))
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data } = this.props;
|
||||
|
||||
return <div className={ style.render } ref={ this.svgContainer }>
|
||||
{ render({
|
||||
...data,
|
||||
props: {
|
||||
...data.props,
|
||||
onReflow: this.provideSVGData
|
||||
}
|
||||
}) }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
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