1
0
mirror of https://gitlab.nic.cz/turris/reforis/foris-js.git synced 2024-07-02 20:30:27 +00:00

Merge branch 'dev' into 'master'

Initial

See merge request turris/reforis/foris-js!1
This commit is contained in:
Bogdan Bodnar 2019-08-29 08:59:10 +00:00
commit a6b642f1a3
92 changed files with 15818 additions and 0 deletions

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
**/__tests__/*
src/testUtils/

66
.eslintrc.js Normal file
View File

@ -0,0 +1,66 @@
const path = require("path");
module.exports = {
"env": {
"browser": true,
"node": true,
"es6": true,
"jest": true
},
"extends": [
"airbnb",
"airbnb/hooks"
],
"globals": {
"_": "readonly",
"babel": "readonly",
"ForisTranslations": "readonly",
"ngettext": "readonly",
"ForisPlugins": "readonly"
},
"parser": "babel-eslint",
"rules": {
"quotes": ["error", "double"],
"indent": ["error", 4],
"react/jsx-indent": ["error", 4],
"react/jsx-indent-props": ["error", 4],
"react/prop-types": "warn",
"react/no-array-index-key": "warn",
"react/button-has-type": "warn",
"import/prefer-default-export": "off",
"import/no-unresolved": [
"error",
// Ignore imports used only in tests
{ ignore: ["mockWS", "customTestRender"] }
],
"import/no-cycle": "warn",
"no-console": "error",
"no-use-before-define": ["error", {
functions: false,
classes: true,
variables: true
}],
"no-restricted-syntax": "warn",
// Should be enabled in the future
"camelcase": "off",
"no-param-reassign": "off",
"react/jsx-props-no-spreading": "off",
"react/require-default-props": "off",
"react/default-props-match-prop-types": "off",
"react/forbid-prop-types": "off",
// Permanently disabled
"react/jsx-filename-extension": "off",
"no-plusplus": "off",
"consistent-return": "off",
"radix": "off",
"no-continue": "off",
"react/no-danger": "off",
},
"settings": {
"import/resolver": {
"node": {
"paths": ["src"]
}
}
}
};

50
.gitignore vendored Normal file
View File

@ -0,0 +1,50 @@
# Common
.cache
## Logs
logs
*.log
# NodeJS
## Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
## Runtime data
pids
*.pid
*.seed
*.pid.lock
## node-waf configuration
.lock-wscript
## Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
## Dependency directories
node_modules/
## Optional npm cache directory
.npm
## Unit test / coverage reports
coverage/
htmlcov/
.tox/
.coverage
.coverage.*
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
## Translations
*.mo
/js/styleguide/
.gitignore
dist/
foris-*.tgz

26
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,26 @@
image: node:8-alpine
stages:
- test
- build
before_script:
- npm install
test:
stage: test
script:
- npm test
lint:
stage: test
script:
- npm run lint
build:
stage: build
script:
- npm pack
artifacts:
paths:
- foris-*.tgz

17
Makefile Normal file
View File

@ -0,0 +1,17 @@
.PHONY: all create-messages update-messages clean
all:
@echo "make create-messages"
@echo " Create locale messages (.pot)."
@echo "make update-messages"
@echo " Update locale messages from .pot file."
@echo "make clean"
@echo " Remove python artifacts and virtualenv."
create-messages:
pybabel extract -F babel.cfg -o ./translations/forisjs.pot .
update-messages:
pybabel update -i translations/forisjs.pot -d translations
clean:
rm -rf node_modules dist

1
babel.cfg Normal file
View File

@ -0,0 +1 @@
[javascript: src/**.js]

17
babel.config.js Normal file
View File

@ -0,0 +1,17 @@
module.exports = {
presets: [
"@babel/preset-env",
"@babel/preset-react",
],
plugins: [
"@babel/plugin-transform-runtime",
"@babel/plugin-syntax-export-default-from",
["module-resolver", {
root: ["./src"],
alias: {
test: "./test",
underscore: "lodash",
},
}],
],
};

27
jest.config.js Normal file
View File

@ -0,0 +1,27 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
// https://jestjs.io/docs/en/configuration.html
module.exports = {
moduleDirectories: [
"node_modules",
"<rootDir>/src/testUtils",
"<rootDir>/src/",
],
clearMocks: true,
collectCoverageFrom: ["src/**/*.{js,jsx}"],
coverageDirectory: "coverage",
testPathIgnorePatterns: ["/node_modules/", "/__fixtures__/"],
verbose: false,
setupFilesAfterEnv: [
"@testing-library/react/cleanup-after-each",
"<rootDir>/src/testUtils/setup",
],
globals: {
TZ: "utc",
},
};

11209
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

71
package.json Normal file
View File

@ -0,0 +1,71 @@
{
"name": "foris",
"version": "0.0.6",
"description": "Set of components and utils for Foris and its plugins.",
"author": "CZ.NIC, z.s.p.o.",
"repository": {
"type": "git",
"url": "https://gitlab.labs.nic.cz/turris/reforis/forisjs.git"
},
"keywords": [
"foris",
"reforis"
],
"license": "GPL-3.0",
"main": "./dist/index.js",
"dependencies": {
"axios": "^0.19.0",
"immutability-helper": "^3.0.1",
"prop-types": "^15.7.2",
"react-datetime": "^2.16.3",
"react-router": "^5.0.1",
"react-uid": "^2.2.0",
"moment": "^2.24.0",
"moment-timezone": "^0.5.25"
},
"peerDependencies": {
"react": "^16.9.0",
"react-dom": "^16.9.0"
},
"devDependencies": {
"@babel/cli": "^7.5.5",
"@babel/core": "^7.5.5",
"@babel/plugin-syntax-export-default-from": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.4.4",
"@babel/preset-env": "^7.4.5",
"@babel/preset-react": "^7.0.0",
"@testing-library/react": "^8.0.1",
"babel-eslint": "^9.0.0",
"babel-jest": "^24.8.0",
"babel-loader": "^8.0.6",
"babel-plugin-module-resolver": "^3.2.0",
"babel-plugin-react-transform": "^3.0.0",
"babel-polyfill": "^6.26.0",
"eslint": "^5.16.0",
"eslint-config-airbnb": "^18.0.1",
"eslint-plugin-import": "^2.17.3",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-react": "^7.13.0",
"eslint-plugin-react-hooks": "^1.6.0",
"jest": "^24.8.0",
"jest-mock-axios": "^3.0.0",
"moment": "^2.24.0",
"moment-timezone": "^0.5.25",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-styleguidist": "^9.1.11",
"snapshot-diff": "^0.5.1"
},
"scripts": {
"build": "rm -rf dist; babel src --out-dir dist --ignore '**/__tests__' --source-maps inline",
"prepare": "rm -rf ./dist && npm run build",
"lint": "eslint src",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage --colors"
},
"files": [
"dist/**",
"translations"
]
}

10
src/__mocks__/axios.js Normal file
View File

@ -0,0 +1,10 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import mockAxios from "jest-mock-axios";
export default mockAxios;

153
src/api/hooks.js Normal file
View File

@ -0,0 +1,153 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import axios from "axios";
import { useCallback, useReducer } from "react";
import { ForisURLs } from "forisUrls";
const POST_HEADERS = {
Accept: "application/json",
"Content-Type": "application/json",
"X-CSRFToken": getCookie("_csrf_token"),
};
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (`${name}=`)) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
export const TIMEOUT = 5000;
const API_ACTIONS = {
INIT: 1,
SUCCESS: 2,
FAILURE: 3,
};
const APIGetReducer = (state, action) => {
switch (action.type) {
case API_ACTIONS.INIT:
return {
...state,
isLoading: true,
isError: false,
};
case API_ACTIONS.SUCCESS:
return {
...state,
isLoading: false,
isError: false,
data: action.payload,
};
case API_ACTIONS.FAILURE:
if (action.status === 403) window.location.assign(ForisURLs.login);
return {
...state,
isLoading: false,
isError: true,
data: action.payload,
};
default:
throw new Error();
}
};
export function useAPIGet(url) {
const [state, dispatch] = useReducer(APIGetReducer, {
isLoading: false,
isError: false,
data: null,
});
const get = useCallback(async () => {
dispatch({ type: API_ACTIONS.INIT });
try {
const result = await axios.get(url, {
timeout: TIMEOUT,
});
dispatch({ type: API_ACTIONS.SUCCESS, payload: result.data });
} catch (error) {
dispatch({
type: API_ACTIONS.FAILURE,
payload: error.response.data,
status: error.response.status,
});
}
}, [url]);
return [state, get];
}
const APIPostReducer = (state, action) => {
switch (action.type) {
case API_ACTIONS.INIT:
return {
...state,
isSending: true,
isError: false,
isSuccess: false,
};
case API_ACTIONS.SUCCESS:
return {
...state,
isSending: false,
isError: false,
isSuccess: true,
data: action.payload,
};
case API_ACTIONS.FAILURE:
if (action.status === 403) window.location.assign(ForisURLs.login);
return {
...state,
isSending: false,
isError: true,
isSuccess: false,
data: action.payload,
};
default:
throw new Error();
}
};
export function useAPIPost(url) {
const [state, dispatch] = useReducer(APIPostReducer, {
isSending: false,
isError: false,
isSuccess: false,
data: null,
});
const post = async (data) => {
dispatch({ type: API_ACTIONS.INIT });
try {
const result = await axios.post(url, data, {
timeout: TIMEOUT,
headers: POST_HEADERS,
});
dispatch({ type: API_ACTIONS.SUCCESS, payload: result.data });
} catch (error) {
dispatch({
type: API_ACTIONS.FAILURE,
payload: error.response.data,
status: error.response.status,
});
}
};
return [state, post];
}

35
src/bootstrap/Alert.js Normal file
View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import PropTypes from "prop-types";
Alert.propTypes = {
/** Type of the alert it adds as `alert-${type}` class. */
type: PropTypes.string.isRequired,
/** Alert message. */
message: PropTypes.string,
/** Alert content. */
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
/** onDismiss handler. */
onDismiss: PropTypes.func,
};
export function Alert({
type, message, onDismiss, children,
}) {
return (
<div className={`alert alert-dismissible alert-${type}`}>
{onDismiss ? <button type="button" className="close" onClick={onDismiss}>&times;</button> : false}
{message}
{children}
</div>
);
}

20
src/bootstrap/Alert.md Normal file
View File

@ -0,0 +1,20 @@
Bootstrap alert component.
```jsx
import {useState} from 'react';
function AlertExample(){
const [alert, setAlert] = useState(true);
if (alert)
return <Alert
type='warning'
message='Some warning out there!'
onDismiss={()=>setAlert(false)}
/>;
return <button
className='btn btn-secondary'
onClick={()=>setAlert(true)}
>Show alert again</button>;
};
<AlertExample/>
```

50
src/bootstrap/Button.js Normal file
View File

@ -0,0 +1,50 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import PropTypes from "prop-types";
const OFFSET = 8;
const SIZE = 3;
const SIZE_CLASS = ` offset-lg-${OFFSET} col-lg-${SIZE}`;
const SIZE_CLASS_SM = " col-sm-12";
Button.propTypes = {
/** Additional class name. */
className: PropTypes.string,
/** Use foris form size and offset. */
forisFormSize: PropTypes.bool,
/** Show loading icon. */
loading: PropTypes.bool,
/** Button content. */
children: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
PropTypes.node,
PropTypes.arrayOf(PropTypes.node),
]).isRequired,
};
export function Button({
className, loading, forisFormSize, children, ...props
}) {
className = className ? `btn ${className}` : "btn btn-primary ";
if (forisFormSize) className += SIZE_CLASS + SIZE_CLASS_SM;
const span = loading
? <span className="spinner-border spinner-border-sm" role="status" aria-hidden="true" /> : null;
return (
<button type="button" className={className} {...props}>
{span}
{" "}
{span ? " " : null}
{" "}
{children}
</button>
);
}

15
src/bootstrap/Button.md Normal file
View File

@ -0,0 +1,15 @@
Bootstrap button component.
All additional `props` are passed to the `<button>` HTML component.
Can be used without parameters:
```jsx
<Button>Click</Button>
```
Using loading spinner:
```jsx
<Button loading disabled>Loading...</Button>
```

50
src/bootstrap/CheckBox.js Normal file
View File

@ -0,0 +1,50 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import PropTypes from "prop-types";
import { useUID } from "react-uid/dist/es5/index";
import { formFieldsSize } from "./constants";
CheckBox.propTypes = {
/** Label message */
label: PropTypes.string.isRequired,
/** Help text message */
helpText: PropTypes.string,
/** Apply default size (full-width) */
useDefaultSize: PropTypes.bool,
/** Control if checkbox is clickable */
disabled: PropTypes.bool,
};
CheckBox.defaultProps = {
useDefaultSize: true,
disabled: false,
};
export function CheckBox({
label, helpText, useDefaultSize, disabled, ...props
}) {
const uid = useUID();
return (
<div className={useDefaultSize ? formFieldsSize : ""} style={{ marginBottom: "1rem" }}>
<div className="custom-control custom-checkbox" style={{ marginBottom: "0" }}>
<input
className="custom-control-input"
type="checkbox"
id={uid}
disabled={disabled}
{...props}
/>
<label className="custom-control-label" htmlFor={uid} style={helpText ? { marginBottom: "0" } : null}>{label}</label>
</div>
{helpText ? <small className="form-text text-muted">{helpText}</small> : null}
</div>
);
}

16
src/bootstrap/Checkbox.md Normal file
View File

@ -0,0 +1,16 @@
Checkbox with label Bootstrap component with predefined sizes and structure for using in foris forms.
All additional `props` are passed to the `<input type="checkbox">` HTML component.
```js
import {useState} from 'react';
const [value, setValue] = useState(false);
<CheckBox
value={value}
label="Some label"
helpText="Read the small text!"
onChange={value => setValue(value)}
/>
```

View File

@ -0,0 +1,63 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import PropTypes from "prop-types";
import Datetime from "react-datetime/DateTime";
import moment from "moment/moment";
import { Input } from "./Input";
DataTimeInput.propTypes = {
/** Field label. */
label: PropTypes.string.isRequired,
/** Error message. */
error: PropTypes.string,
/** DataTime or Data or Time value. Can be `moment` or string. */
value: PropTypes.oneOfType([PropTypes.objectOf(moment), PropTypes.string]),
/** Help text message. */
helpText: PropTypes.string,
/** Content. */
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
isValidDate: PropTypes.bool,
onChange: PropTypes.func.isRequired,
dateFormat: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
timeFormat: PropTypes.string,
};
const DEFAULT_DATE_FORMAT = "YYYY-MM-DD";
const DEFAULT_TIME_FORMAT = "HH:mm:ss";
export function DataTimeInput({
value, onChange, isValidDate, dateFormat, timeFormat, children, ...props
}) {
function renderInput(datetimeProps) {
return (
<Input
{...props}
{...datetimeProps}
>
{children}
</Input>
);
}
return (
<Datetime
locale={ForisTranslations.locale}
dateFormat={dateFormat !== undefined ? dateFormat : DEFAULT_DATE_FORMAT}
timeFormat={timeFormat !== undefined ? timeFormat : DEFAULT_TIME_FORMAT}
value={value}
onChange={onChange}
isValidDate={isValidDate}
renderInput={renderInput}
/>
);
}

View File

@ -0,0 +1,25 @@
Adopted from `react-datetime/DateTime` datatime picker component.
It uses `momentjs` see example.
It requires `ForisTranslations.locale` to be defined in order to use right locale.
```js
ForisTranslations={locale:'en'};
import {useState, useEffect} from 'react';
import moment from 'moment/moment';
const [dataTime, setDataTime] = useState(moment());
const [error, setError] = useState();
useEffect(()=>{
dataTime.isValid() ? setError(null) : setError('Invalid value!');
},[dataTime]);
<DataTimeInput
label='Time to sleep'
value={dataTime}
error={error}
helpText='Example helptext...'
onChange={value => setDataTime(value)}
/>
```

View File

@ -0,0 +1,25 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import PropTypes from "prop-types";
import { Input } from "./Input";
export const EmailInput = ({ ...props }) => <Input type="email" {...props} />;
EmailInput.propTypes = {
/** Field label. */
label: PropTypes.string.isRequired,
/** Error message. */
error: PropTypes.string,
/** Help text message. */
helpText: PropTypes.string,
/** Email value. */
value: PropTypes.string,
};

View File

@ -0,0 +1,18 @@
Bootstrap component of email input with label with predefined sizes and structure for using in foris forms.
It use built-in browser email address checking. It's only meaningful using inside `<form>`.
All additional `props` are passed to the `<input type="email">` HTML component.
```js
import {useState} from 'react';
const [email, setEmail] = useState('Wrong email');
<form onSubmit={e=>e.preventDefault()}>
<EmailInput
value={email}
label="Some label"
helpText="Read the small text!"
onChange={target => setEmail(target.value)}
/>
<button type="submit">Try to submit</button>
</form>
```

51
src/bootstrap/Input.js Normal file
View File

@ -0,0 +1,51 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import { useUID } from "react-uid/dist/es5/index";
import PropTypes from "prop-types";
import { formFieldsSize } from "./constants";
Input.propTypes = {
type: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
helpText: PropTypes.string,
error: PropTypes.string,
className: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
};
/** Base bootstrap input component. */
export function Input({
type, label, helpText, error, className, children, ...props
}) {
const uid = useUID();
const inputClassName = `form-control ${className || ""} ${(error ? "is-invalid" : "")}`.trim();
return (
<div className={formFieldsSize}>
<div className="form-group">
<label htmlFor={uid}>{label}</label>
<div className="input-group">
<input
className={inputClassName}
type={type}
id={uid}
{...props}
/>
{children}
</div>
{error ? <div className="invalid-feedback">{error}</div> : null}
{helpText ? <small className="form-text text-muted">{helpText}</small> : null}
</div>
</div>
);
}

94
src/bootstrap/Modal.js Normal file
View File

@ -0,0 +1,94 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React, { useEffect, useRef } from "react";
import PropTypes from "prop-types";
import { Portal } from "utils/Portal";
Modal.propTypes = {
/** Is modal shown value */
shown: PropTypes.bool.isRequired,
/** Callback to manage modal visibility */
setShown: PropTypes.func.isRequired,
/** Modal content use following: `ModalHeader`, `ModalBody`, `ModalFooter` */
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
};
export function Modal({ shown, setShown, children }) {
const dialogRef = useRef();
useEffect(() => {
function handleClickOutsideDialog(e) {
if (!dialogRef.current.contains(e.target)) setShown(false);
}
document.addEventListener("mousedown", handleClickOutsideDialog);
return () => {
document.removeEventListener("mousedown", handleClickOutsideDialog);
};
}, [setShown]);
return (
<Portal containerId="modal-container">
<div className={`modal fade ${shown ? "show" : ""}`} role="dialog">
<div ref={dialogRef} className="modal-dialog" role="document">
<div className="modal-content">
{children}
</div>
</div>
</div>
</Portal>
);
}
ModalHeader.propTypes = {
setShown: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
};
export function ModalHeader({ setShown, title }) {
return (
<div className="modal-header">
<h5 className="modal-title">{title}</h5>
<button type="button" className="close" onClick={() => setShown(false)}>
<span aria-hidden="true">&times;</span>
</button>
</div>
);
}
ModalBody.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
};
export function ModalBody({ children }) {
return <div className="modal-body">{children}</div>;
}
ModalFooter.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
};
export function ModalFooter({ children }) {
return (
<div className="modal-footer">
{children}
</div>
);
}

27
src/bootstrap/Modal.md Normal file
View File

@ -0,0 +1,27 @@
Bootstrap modal component.
I have no idea why example doesn't work here but you can investigate HTML code...
```js
import {ModalHeader, ModalBody, ModalFooter} from './Modal';
import {useState} from 'react';
const [shown, setShown] = useState(false);
<>
<Modal shown={shown}>
<ModalHeader setShown={setShown} title='Warning!'/>
<ModalBody><p>Bla bla bla...</p></ModalBody>
<ModalFooter>
<button
className='btn btn-secondary'
onClick={() => setShown(false)}
>Skip it</button>
</ModalFooter>
</Modal>
<button
className='btn btn-secondary'
onClick={()=>setShown(true)}
>Show modal</button>
</>
```

View File

@ -0,0 +1,27 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import PropTypes from "prop-types";
import { Input } from "./Input";
export const NumberInput = ({ ...props }) => <Input type="number" {...props} />;
NumberInput.propTypes = {
/** Field label. */
label: PropTypes.string.isRequired,
/** Error message. */
error: PropTypes.string,
/** Help text message. */
helpText: PropTypes.string,
/** Number value. */
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
};

View File

@ -0,0 +1,17 @@
Bootstrap component of number input with label with predefined sizes and structure for using in foris forms.
All additional `props` are passed to the `<input type="number">` HTML component.
```js
import {useState} from 'react';
const [value, setValue] = useState(42);
<NumberInput
value={value}
label="Some number"
helpText="Read the small text!"
min='33'
max='54'
onChange={target => setValue(target.value)}
/>
```

View File

@ -0,0 +1,52 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React, { useState } from "react";
import PropTypes from "prop-types";
import { Input } from "./Input";
PasswordInput.propTypes = {
/** Field label. */
label: PropTypes.string.isRequired,
/** Error message. */
error: PropTypes.string,
/** Password value. */
value: PropTypes.string,
/** Help text message. */
helpText: PropTypes.string,
/** Use show/hide password button. */
withEye: PropTypes.bool,
};
export function PasswordInput({ withEye, ...props }) {
const [isHidden, setHidden] = useState(true);
return (
<Input
type={withEye && !isHidden ? "text" : "password"}
autoComplete={isHidden ? "new-password" : null}
{...props}
>
{withEye
? (
<div className="input-group-append">
<button
type="button"
className="input-group-text"
onClick={(e) => {
e.preventDefault();
setHidden((shouldBeHidden) => !shouldBeHidden);
}}
>
<i className={`fa ${isHidden ? "fa-eye" : "fa-eye-slash"}`} />
</button>
</div>
)
: null}
</Input>
);
}

View File

@ -0,0 +1,17 @@
Password Bootstrap component input with label and predefined sizes and structure for using in foris forms.
Can be used with "eye" button, see example.
All additional `props` are passed to the `<input type="password">` HTML component.
```js
import {useState} from 'react';
const [value, setValue] = useState('secret');
<PasswordInput
withEye
value={value}
label="Some password"
helpText="Read the small text!"
onChange={target => setValue(target.value)}
/>
```

93
src/bootstrap/RadioSet.js Normal file
View File

@ -0,0 +1,93 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import PropTypes from "prop-types";
import { useUID } from "react-uid/dist/es5/index";
import { formFieldsSize } from "./constants";
RadioSet.propTypes = {
/** Name attribute of the input HTML tag. */
name: PropTypes.string.isRequired,
/** RadioSet label . */
label: PropTypes.string,
/** Choices . */
choices: PropTypes.arrayOf(PropTypes.shape({
/** Choice lable . */
label: PropTypes.string.isRequired,
/** Choice value . */
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
})).isRequired,
/** Initial value . */
value: PropTypes.string,
/** Help text message . */
helpText: PropTypes.string,
};
export function RadioSet({
name, label, choices, value, helpText, ...props
}) {
const uid = useUID();
const radios = choices.map((choice, key) => {
const id = `${name}-${key}`;
return (
<Radio
id={id}
key={id}
name={name}
label={choice.label}
value={choice.value}
helpText={choice.helpText}
checked={choice.value === value}
{...props}
/>
);
});
return (
<div className={`form-group ${formFieldsSize}`} style={{ marginBottom: "1rem" }}>
{label
? (
<label className="col-12" htmlFor={uid} style={{ paddingLeft: "0" }}>
{label}
</label>
)
: null}
{radios}
{helpText ? <small className="form-text text-muted">{helpText}</small> : null}
</div>
);
}
Radio.propTypes = {
label: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
helpText: PropTypes.string,
};
function Radio({
label, id, helpText, ...props
}) {
return (
<>
<div className="custom-control custom-radio custom-control-inline">
<input
id={id}
className="custom-control-input"
type="radio"
{...props}
/>
<label className="custom-control-label" htmlFor={id}>{label}</label>
</div>
{helpText ? <small className="form-text text-muted">{helpText}</small> : null}
</>
);
}

24
src/bootstrap/RadioSet.md Normal file
View File

@ -0,0 +1,24 @@
Set of radio Bootstrap component input with label and predefined sizes and structure for using in foris forms.
All additional `props` are passed to the `<input type="number">` HTML component.
```js
import {useState} from 'react';
const CHOICES=[
{value:'one',label:'1'},
{value:'two',label:'2'},
{value:'three',label:'3'},
];
const [value, setValue] = useState(CHOICES[0].value);
<>
{/*Yeah, it gets event, not value!*/}
<RadioSet
value={value}
name='some-radio'
choices={CHOICES}
onChange={event=>setValue(event.target.value)}
/>
<p>Selected value: {value}</p>
</>
```

49
src/bootstrap/Select.js Normal file
View File

@ -0,0 +1,49 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import PropTypes from "prop-types";
import { useUID } from "react-uid/dist/es5/index";
Select.propTypes = {
/** Select field Label. */
label: PropTypes.string.isRequired,
/** Choices if form of {value : "Label",...}. */
choices: PropTypes.object.isRequired,
/** Current value. */
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
/** Help text message. */
helpText: PropTypes.string,
};
export function Select({
label, choices, helpText, ...props
}) {
const uid = useUID();
const options = Object.keys(choices).map(
(key) => <option key={key} value={key}>{choices[key]}</option>,
);
return (
<div className="form-group col-sm-12 offset-lg-1 col-lg-10">
<label htmlFor={uid}>{label}</label>
<select
className="custom-select"
id={uid}
{...props}
>
{options}
</select>
{helpText ? <small className="form-text text-muted">{helpText}</small> : null}
</div>
);
}

25
src/bootstrap/Select.md Normal file
View File

@ -0,0 +1,25 @@
Select with options Bootstrap component input with label and predefined sizes and structure for using in foris forms.
All additional `props` are passed to the `<select>` HTML component.
```js
import {useState} from 'react';
const CHOICES={
apple:'Apple',
banana:'Banana',
peach:'Peach',
};
const [value, setValue] = useState(Object.keys(CHOICES)[0]);
<>
{/*Yeah, it gets event, not value!*/}
<Select
label="Fruit"
value={value}
choices={CHOICES}
onChange={event=>setValue(event.target.value)}
/>
<p>Selected choice label: {CHOICES[value]}</p>
<p>Selected choice value: {value}</p>
</>
```

65
src/bootstrap/Spinner.js Normal file
View File

@ -0,0 +1,65 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import PropTypes from "prop-types";
Spinner.propTypes = {
/** Children components put into `div` with "spinner-text" class. */
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
/** Render component with full-screen mode (using apropriate `.css` styles) */
fullScreen: PropTypes.bool.isRequired,
className: PropTypes.string,
};
Spinner.defaultProps = {
fullScreen: false,
};
export function Spinner({
fullScreen, children, className, ...props
}) {
if (!fullScreen) {
return (
<div className={`spinner-wrapper ${className || ""}`} {...props}>
<SpinnerElement>{children}</SpinnerElement>
</div>
);
}
return (
<div className="spinner-fs-wrapper" {...props}>
<div className="spinner-fs-background">
<SpinnerElement>{children}</SpinnerElement>
</div>
</div>
);
}
SpinnerElement.propTypes = {
/** Spinner's size */
small: PropTypes.bool,
/** Children components */
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
};
export function SpinnerElement({ small, children }) {
return (
<>
<div className={`spinner-border ${small ? "spinner-border-sm" : ""}`} role="status">
<span className="sr-only" />
</div>
<div className="spinner-text">{children}</div>
</>
);
}

5
src/bootstrap/Spinner.md Normal file
View File

@ -0,0 +1,5 @@
Spiner Bootstrap component.
```js
<Spinner>You can put text inside or any component you wish.</Spinner>
```

View File

@ -0,0 +1,24 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import PropTypes from "prop-types";
import { Input } from "./Input";
export const TextInput = ({ ...props }) => <Input type="text" {...props} />;
TextInput.propTypes = {
/** Field label. */
label: PropTypes.string.isRequired,
/** Error text. */
error: PropTypes.string,
/** Help text message. */
helpText: PropTypes.string,
};

View File

@ -0,0 +1,15 @@
Text Bootstrap component input with label and predefined sizes and structure for using in foris forms.
All additional `props` are passed to the `<input type="text">` HTML component.
```js
import {useState} from 'react';
const [value, setValue] = useState('Bla bla');
<TextInput
value={value}
label="Some text"
helpText="Read the small text!"
onChange={event => setValue(event.target.value)}
/>
```

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import { render } from "customTestRender";
import { Button } from "../Button";
describe("<Button />", () => {
it("Render button correctly", () => {
const { container } = render(<Button>Test Button</Button>);
expect(container.firstChild)
.toMatchSnapshot();
});
it("Render button with custom classes", () => {
const { container } = render(<Button className="one two three">Test Button</Button>);
expect(container.firstChild)
.toMatchSnapshot();
});
it("Render button with spinner", () => {
const { container } = render(<Button loading={true}>Test Button</Button>);
expect(container.firstChild)
.toMatchSnapshot();
});
});

View File

@ -0,0 +1,39 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import { render } from "customTestRender";
import { CheckBox } from "../CheckBox";
describe("<Checkbox/>", () => {
it("Render checkbox", () => {
const { container } = render(
<CheckBox
label="Test label"
checked
helpText="Some help text"
onChange={() => {
}}
/>
);
expect(container.firstChild)
.toMatchSnapshot();
});
it("Render uncheked checkbox", () => {
const { container } = render(
<CheckBox
label="Test label"
helpText="Some help text"
/>
);
expect(container.firstChild)
.toMatchSnapshot();
});
});

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import { render } from "customTestRender";
import { NumberInput } from "../NumberInput";
describe("<NumberInput/>", () => {
it("Render number input", () => {
const { container } = render(
<NumberInput
label="Test label"
helpText="Some help text"
value={1123}
onChange={() => {
}}
/>
);
expect(container.firstChild)
.toMatchSnapshot();
});
});

View File

@ -0,0 +1,28 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import { render } from "customTestRender";
import { PasswordInput } from "../PasswordInput";
describe("<PasswordInput/>", () => {
it("Render password input", () => {
const { container } = render(
<PasswordInput
label="Test label"
helpText="Some help text"
value="Some password"
onChange={() => {
}}
/>
);
expect(container.firstChild)
.toMatchSnapshot();
});
});

View File

@ -0,0 +1,45 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import { render } from "customTestRender";
import { RadioSet } from "../RadioSet";
const TEST_CHOICES = [
{
label: "label",
value: "value"
},
{
label: "another label",
value: "another value"
},
{
label: "another one label",
value: "another on value"
}
];
describe("<RadioSet/>", () => {
it("Render radio set", () => {
const { container } = render(
<RadioSet
name={"test_name"}
label='Radios set label'
value='value'
choices={TEST_CHOICES}
helpText={"Some help text"}
onChange={() => {
}}
/>
);
expect(container.firstChild)
.toMatchSnapshot();
});
});

View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import { fireEvent, getByDisplayValue, getByText, render } from "customTestRender";
import { Select } from "../Select";
const TEST_CHOICES = {
"1": "one",
"2": "two",
"3": "three",
};
describe("<Select/>", () => {
var selectContainer;
const onChangeHandler = jest.fn();
beforeEach(() => {
const { container } = render(
<Select
label='Test label'
value='1'
choices={TEST_CHOICES}
helpText='Help text'
onChange={onChangeHandler}
/>
);
selectContainer = container;
});
it("Test with snapshot.", () => {
expect(selectContainer)
.toMatchSnapshot();
});
it("Test onChange handling.", () => {
const select = getByDisplayValue(selectContainer, "one");
expect(select.value)
.toBe("1");
fireEvent.change(select, { target: { value: "2" } });
const option = getByText(selectContainer, "two");
expect(onChangeHandler)
.toBeCalled();
expect(option.value)
.toBe("2");
});
});

View File

@ -0,0 +1,28 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import { render } from "customTestRender";
import { TextInput } from "../TextInput";
describe("<TextInput/>", () => {
it("Render text input", () => {
const { container } = render(
<TextInput
label="Test label"
helpText="Some help text"
value="Some text"
onChange={() => {
}}
/>
);
expect(container.firstChild)
.toMatchSnapshot();
});
});

View File

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Button /> Render button correctly 1`] = `
<button
class="btn btn-primary "
type="button"
>
Test Button
</button>
`;
exports[`<Button /> Render button with custom classes 1`] = `
<button
class="btn one two three"
type="button"
>
Test Button
</button>
`;
exports[`<Button /> Render button with spinner 1`] = `
<button
class="btn btn-primary "
type="button"
>
<span
aria-hidden="true"
class="spinner-border spinner-border-sm"
role="status"
/>
Test Button
</button>
`;

View File

@ -0,0 +1,62 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Checkbox/> Render checkbox 1`] = `
<div
class="col-sm-12 offset-lg-1 col-lg-10"
style="margin-bottom: 1rem;"
>
<div
class="custom-control custom-checkbox"
style="margin-bottom: 0px;"
>
<input
checked=""
class="custom-control-input"
id="1"
type="checkbox"
/>
<label
class="custom-control-label"
for="1"
style="margin-bottom: 0px;"
>
Test label
</label>
</div>
<small
class="form-text text-muted"
>
Some help text
</small>
</div>
`;
exports[`<Checkbox/> Render uncheked checkbox 1`] = `
<div
class="col-sm-12 offset-lg-1 col-lg-10"
style="margin-bottom: 1rem;"
>
<div
class="custom-control custom-checkbox"
style="margin-bottom: 0px;"
>
<input
class="custom-control-input"
id="1"
type="checkbox"
/>
<label
class="custom-control-label"
for="1"
style="margin-bottom: 0px;"
>
Test label
</label>
</div>
<small
class="form-text text-muted"
>
Some help text
</small>
</div>
`;

View File

@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<NumberInput/> Render number input 1`] = `
<div
class="col-sm-12 offset-lg-1 col-lg-10"
>
<div
class="form-group"
>
<label
for="1"
>
Test label
</label>
<div
class="input-group"
>
<input
class="form-control"
id="1"
type="number"
value="1123"
/>
</div>
<small
class="form-text text-muted"
>
Some help text
</small>
</div>
</div>
`;

View File

@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<PasswordInput/> Render password input 1`] = `
<div
class="col-sm-12 offset-lg-1 col-lg-10"
>
<div
class="form-group"
>
<label
for="1"
>
Test label
</label>
<div
class="input-group"
>
<input
autocomplete="new-password"
class="form-control"
id="1"
type="password"
value="Some password"
/>
</div>
<small
class="form-text text-muted"
>
Some help text
</small>
</div>
</div>
`;

View File

@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RadioSet/> Render radio set 1`] = `
<div
class="form-group col-sm-12 offset-lg-1 col-lg-10"
style="margin-bottom: 1rem;"
>
<label
class="col-12"
for="1"
style="padding-left: 0px;"
>
Radios set label
</label>
<div
class="custom-control custom-radio custom-control-inline"
>
<input
checked=""
class="custom-control-input"
id="test_name-0"
name="test_name"
type="radio"
value="value"
/>
<label
class="custom-control-label"
for="test_name-0"
>
label
</label>
</div>
<div
class="custom-control custom-radio custom-control-inline"
>
<input
class="custom-control-input"
id="test_name-1"
name="test_name"
type="radio"
value="another value"
/>
<label
class="custom-control-label"
for="test_name-1"
>
another label
</label>
</div>
<div
class="custom-control custom-radio custom-control-inline"
>
<input
class="custom-control-input"
id="test_name-2"
name="test_name"
type="radio"
value="another on value"
/>
<label
class="custom-control-label"
for="test_name-2"
>
another one label
</label>
</div>
<small
class="form-text text-muted"
>
Some help text
</small>
</div>
`;

View File

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Select/> Test with snapshot. 1`] = `
<div>
<div
class="form-group col-sm-12 offset-lg-1 col-lg-10"
>
<label
for="1"
>
Test label
</label>
<select
class="custom-select"
id="1"
>
<option
value="1"
>
one
</option>
<option
value="2"
>
two
</option>
<option
value="3"
>
three
</option>
</select>
<small
class="form-text text-muted"
>
Help text
</small>
</div>
</div>
`;

View File

@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<TextInput/> Render text input 1`] = `
<div
class="col-sm-12 offset-lg-1 col-lg-10"
>
<div
class="form-group"
>
<label
for="1"
>
Test label
</label>
<div
class="input-group"
>
<input
class="form-control"
id="1"
type="text"
value="Some text"
/>
</div>
<small
class="form-text text-muted"
>
Some help text
</small>
</div>
</div>
`;

View File

@ -0,0 +1,10 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
/** Bootstrap column size for form fields */
// eslint-disable-next-line import/prefer-default-export
export const formFieldsSize = "col-sm-12 offset-lg-1 col-lg-10";

14
src/forisUrls.js Normal file
View File

@ -0,0 +1,14 @@
export const REFORIS_URL_PREFIX = process.env.LIGHTTPD ? "/reforis" : "";
export const ForisURLs = {
login: `${REFORIS_URL_PREFIX}/login`,
static: `${REFORIS_URL_PREFIX}/static/reforis`,
wifi: `${REFORIS_URL_PREFIX}/network-settings/wifi`,
updates: `${REFORIS_URL_PREFIX}/updates`,
// Notifications links are used with <Link/> inside Router, thus url subdir is not required.
notifications: "/notifications",
notificationsSettings: "/administration/notifications-settings",
luci: "/cgi-bin/luci",
};

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import { render } from "customTestRender";
import { STATES, SubmitButton } from "../components/SubmitButton";
describe("<SubmitButton/>", () => {
it("Render ready", () => {
const { container } = render(<SubmitButton state={STATES.READY}/>);
expect(container)
.toMatchSnapshot();
});
it("Render saving", () => {
const { container } = render(<SubmitButton state={STATES.SAVING}/>);
expect(container)
.toMatchSnapshot();
});
it("Render load", () => {
const { container } = render(<SubmitButton state={STATES.LOAD}/>);
expect(container)
.toMatchSnapshot();
});
});

View File

@ -0,0 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SubmitButton/> Render load 1`] = `
<div>
<button
class="btn btn-primary offset-lg-8 col-lg-3 col-sm-12"
disabled=""
type="submit"
>
<span
aria-hidden="true"
class="spinner-border spinner-border-sm"
role="status"
/>
Load settings
</button>
</div>
`;
exports[`<SubmitButton/> Render ready 1`] = `
<div>
<button
class="btn btn-primary offset-lg-8 col-lg-3 col-sm-12"
type="submit"
>
Save
</button>
</div>
`;
exports[`<SubmitButton/> Render saving 1`] = `
<div>
<button
class="btn btn-primary offset-lg-8 col-lg-3 col-sm-12"
disabled=""
type="submit"
>
<span
aria-hidden="true"
class="spinner-border spinner-border-sm"
role="status"
/>
Updating
</button>
</div>
`;

View File

@ -0,0 +1,114 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from 'react';
import { act, fireEvent, render, waitForElement } from 'customTestRender';
import mockAxios from 'jest-mock-axios';
import { ForisForm } from "../components/ForisForm";
// It's possible to unittest each hooks via react-hooks-testing-library.
// But it's better and easier to test it by test components which uses this hooks.
const TestForm = ({formData, formErrors, setFormValue}) => {
return <>
<input
data-testid='test-input'
value={formData.field}
onChange={setFormValue(value => ({field: {$set: value}}))}
/>
<p>{formErrors.field}</p>
</>
};
describe('useForm hook.', () => {
let mockValidator;
let mockPrepData;
let mockPrepDataToSubmit;
let mockWebSockets;
let input;
let form;
const Child = jest.fn(props => <TestForm {...props}/>);
beforeEach(async () => {
mockPrepData = jest.fn(() => ({field: 'preparedData'}));
mockPrepDataToSubmit = jest.fn(() => ({field: 'preparedDataToSubmit'}));
mockValidator = jest.fn(data => data.field === 'invalidValue' ? {field: 'Error'} : {});
const {getByTestId, container} = render(
<ForisForm
ws={mockWebSockets}
// Just some module which exists...
forisConfig={{
endpoint: 'testEndpoint',
wsModule: 'testWSModule'
}}
prepData={mockPrepData}
prepDataToSubmit={mockPrepDataToSubmit}
validator={mockValidator}
>
<Child/>
</ForisForm>
);
mockAxios.mockResponse({field: 'fetchedData'});
input = await waitForElement(() =>
getByTestId('test-input')
);
form = container.firstChild
});
it('Validation on changing.', () => {
expect(mockValidator).toHaveBeenCalledTimes(1);
expect(Child).toHaveBeenCalledTimes(1);
expect(Child.mock.calls[0][0].formErrors).toMatchObject({});
act(() => {
fireEvent.change(input, {target: {value: 'invalidValue', type: 'text'}});
});
expect(Child).toHaveBeenCalledTimes(2);
expect(mockValidator).toHaveBeenCalledTimes(2);
expect(Child.mock.calls[1][0].formErrors).toMatchObject({field: 'Error'});
});
it('Update text value.', () => {
fireEvent.change(input, {target: {value: 'newValue', type: 'text'}})
expect(input.value).toBe('newValue');
});
it('Update text value.', () => {
fireEvent.change(input, {target: {value: 123, type: 'number'}})
expect(input.value).toBe('123');
});
it('Update checkbox value.', () => {
fireEvent.change(input, {target: {checked: true, type: 'checkbox'}})
expect(input.checked).toBe(true);
});
it('Fetch data.', () => {
expect(mockAxios.get).toHaveBeenCalledWith('testEndpoint', expect.anything());
expect(mockPrepData).toHaveBeenCalledTimes(1);
expect(Child.mock.calls[0][0].formData).toMatchObject({field: 'preparedData'});
});
it('Submit.', () => {
expect(mockAxios.get).toHaveBeenCalledTimes(1);
expect(mockPrepDataToSubmit).toHaveBeenCalledTimes(0);
fireEvent.submit(form);
expect(mockPrepDataToSubmit).toHaveBeenCalledTimes(1);
expect(mockAxios.post).toHaveBeenCalledTimes(1);
expect(mockAxios.post).toHaveBeenCalledWith(
'testEndpoint',
{'field': 'preparedDataToSubmit'},
expect.anything(),
);
});
});

View File

@ -0,0 +1,138 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import {
validateDomain,
validateDUID,
validateIPv4Address,
validateIPv6Address,
validateIPv6Prefix,
validateMAC
} from "validations";
describe("Validation functions", () => {
it("validateIPv4Address valid", () => {
expect(validateIPv4Address("192.168.1.1"))
.toBe(undefined);
expect(validateIPv4Address("1.1.1.1"))
.toBe(undefined);
expect(validateIPv4Address("0.0.0.0"))
.toBe(undefined);
});
it("validateIPv4Address invalid", () => {
expect(validateIPv4Address("invalid"))
.not
.toBe(undefined);
expect(validateIPv4Address("192.256.1.1"))
.not
.toBe(undefined);
expect(validateIPv4Address("192.168.256.1"))
.not
.toBe(undefined);
expect(validateIPv4Address("192.168.1.256"))
.not
.toBe(undefined);
expect(validateIPv4Address("192.168.1.256"))
.not
.toBe(undefined);
});
it("validateIPv6Address valid", () => {
expect(validateIPv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:7334"))
.toBe(undefined);
expect(validateIPv6Address("0:0:0:0:0:0:0:1"))
.toBe(undefined);
expect(validateIPv6Address("::1"))
.toBe(undefined);
expect(validateIPv6Address("::"))
.toBe(undefined);
});
it("validateIPv6Address invalid", () => {
expect(validateIPv6Address("invalid"))
.not
.toBe(undefined);
expect(validateIPv6Address("1.1.1.1"))
.not
.toBe(undefined);
expect(validateIPv6Address("1200::AB00:1234::2552:7777:1313"))
.not
.toBe(undefined);
expect(validateIPv6Address("1200:0000:AB00:1234:O000:2552:7777:1313"))
.not
.toBe(undefined);
});
it("validateIPv6Prefix valid", () => {
expect(validateIPv6Prefix("2002:0000::/16"))
.toBe(undefined);
expect(validateIPv6Prefix("0::/0"))
.toBe(undefined);
});
it("validateIPv6Prefix invalid", () => {
expect(validateIPv6Prefix("2001:0db8:85a3:0000:0000:8a2e:0370:7334"))
.not
.toBe(undefined);
expect(validateIPv6Prefix("::1"))
.not
.toBe(undefined);
expect(validateIPv6Prefix("2002:0000::/999"))
.not
.toBe(undefined);
});
it("validateDomain valid", () => {
expect(validateDomain("example.com"))
.toBe(undefined);
expect(validateDomain("one.two.three"))
.toBe(undefined);
});
it("validateDomain invalid", () => {
expect(validateDomain("test/"))
.not
.toBe(undefined);
expect(validateDomain("."))
.not
.toBe(undefined);
});
it("validateDUID valid", () => {
expect(validateDUID("abcdefAB"))
.toBe(undefined);
expect(validateDUID("ABCDEF12"))
.toBe(undefined);
expect(validateDUID("ABCDEF12AB"))
.toBe(undefined);
});
it("validateDUID invalid", () => {
expect(validateDUID("gggggggg"))
.not
.toBe(undefined);
expect(validateDUID("abcdefABa"))
.not
.toBe(undefined);
});
it("validateMAC valid", () => {
expect(validateMAC("00:D0:56:F2:B5:12"))
.toBe(undefined);
expect(validateMAC("00:26:DD:14:C4:EE"))
.toBe(undefined);
expect(validateMAC("06:00:00:00:00:00"))
.toBe(undefined);
});
it("validateMAC invalid", () => {
expect(validateMAC("00:00:00:00:00:0G"))
.not
.toBe(undefined);
expect(validateMAC("06:00:00:00:00:00:00"))
.not
.toBe(undefined);
});
});

View File

@ -0,0 +1,148 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import { Spinner } from "bootstrap/Spinner";
import { useAPIPost } from "api/hooks";
import { Prompt } from "react-router";
import { useForisModule, useForm } from "../hooks";
import { STATES as SUBMIT_BUTTON_STATES, SubmitButton } from "./SubmitButton";
import { FailAlert, SuccessAlert } from "./alerts";
ForisForm.propTypes = {
/** WebSocket object see `scr/common/WebSockets.js`. */
ws: PropTypes.object,
/** Foris configuration object. See usage in main components. */
forisConfig: PropTypes.shape({
/** reForis Flask aplication API endpoint from `src/common/API.js`. */
endpoint: PropTypes.string.isRequired,
/** `foris-controller` module name to be used via WebSockets.
* If it's not passed then WebSockets aren't used
* */
wsModule: PropTypes.string,
/** `foris-controller` action name to be used via WebSockets.
* If it's not passed then `update_settings` is used. see `src/common/WebSocketHooks.js`
* */
wsAction: PropTypes.string,
}).isRequired,
/** Function to prepare data recived from the API before using in forms. */
prepData: PropTypes.func.isRequired,
/** Function to prepare data from form before submitting. */
prepDataToSubmit: PropTypes.func.isRequired,
/** Function to handle response to POST request. */
postCallback: PropTypes.func.isRequired,
/** Validate data and provide validation object. Then validation errors passed to children. */
validator: PropTypes.func.isRequired,
/** Disables form */
disabled: PropTypes.bool,
/** reForis form components. */
children: PropTypes.node.isRequired,
/** Optional override of form submit callback */
onSubmitOverridden: PropTypes.func,
};
ForisForm.defaultProps = {
prepData: (data) => data,
prepDataToSubmit: (data) => data,
postCallback: () => undefined,
validator: () => undefined,
disabled: false,
};
/** Serves as HOC for all foris forms components. */
export function ForisForm({
ws,
forisConfig,
prepData,
prepDataToSubmit,
postCallback,
validator,
disabled,
onSubmitOverridden,
children,
}) {
const [formState, onFormChangeHandler, resetFormData] = useForm(validator, prepData);
const [forisModuleState] = useForisModule(ws, forisConfig);
useEffect(() => {
if (forisModuleState.data) {
resetFormData(forisModuleState.data);
}
}, [forisModuleState.data, resetFormData, prepData]);
const [postState, post] = useAPIPost(forisConfig.endpoint);
useEffect(() => {
if (postState.isSuccess) postCallback();
}, [postCallback, postState.isSuccess]);
function onSubmitHandler(e) {
e.preventDefault();
resetFormData();
const copiedFormData = JSON.parse(JSON.stringify(formState.data));
const preparedData = prepDataToSubmit(copiedFormData);
post(preparedData);
}
function getSubmitButtonState() {
if (postState.isSending) return SUBMIT_BUTTON_STATES.SAVING;
if (forisModuleState.isLoading) return SUBMIT_BUTTON_STATES.LOAD;
return SUBMIT_BUTTON_STATES.READY;
}
const [alertIsDismissed, setAlertIsDismissed] = useState(false);
if (!formState.data) return <Spinner className="row justify-content-center" />;
const formIsDisabled = disabled || forisModuleState.isLoading || postState.isSending;
const submitButtonIsDisabled = disabled || !!formState.errors;
const childrenWithFormProps = React.Children.map(
children,
(child) => React.cloneElement(child, {
formData: formState.data,
formErrors: formState.errors,
setFormValue: onFormChangeHandler,
disabled: formIsDisabled,
}),
);
const onSubmit = onSubmitOverridden
? onSubmitOverridden(formState.data, onFormChangeHandler, onSubmitHandler)
: onSubmitHandler;
function getMessageOnLeavingPage() {
if (JSON.stringify(formState.data) === JSON.stringify(formState.initialData)) return true;
return _("Changes you made may not be saved. Are you sure you want to leave?");
}
let alert = null;
if (!alertIsDismissed) {
if (postState.isSuccess) {
alert = <SuccessAlert onDismiss={() => setAlertIsDismissed(true)} />;
} else if (postState.isError) {
alert = <FailAlert onDismiss={() => setAlertIsDismissed(true)} />;
}
}
return (
<>
<Prompt message={getMessageOnLeavingPage} />
{alert}
<form onSubmit={onSubmit}>
{childrenWithFormProps}
<SubmitButton
state={getSubmitButtonState()}
disabled={submitButtonIsDisabled}
/>
</form>
</>
);
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import PropTypes from "prop-types";
import { Button } from "bootstrap/Button";
export const STATES = {
READY: 1,
SAVING: 2,
LOAD: 3,
};
SubmitButton.propTypes = {
disabled: PropTypes.bool,
state: PropTypes.oneOf(Object.keys(STATES)
.map((key) => STATES[key])),
};
export function SubmitButton({ disabled, state, ...props }) {
const disableSubmitButton = disabled || state !== STATES.READY;
const loadingSubmitButton = state !== STATES.READY;
let labelSubmitButton;
switch (state) {
case STATES.SAVING:
labelSubmitButton = _("Updating");
break;
case STATES.LOAD:
labelSubmitButton = _("Load settings");
break;
default:
labelSubmitButton = _("Save");
}
return (
<Button
type="submit"
loading={loadingSubmitButton}
disabled={disableSubmitButton}
forisFormSize
{...props}
>
{labelSubmitButton}
</Button>
);
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import React from "react";
import PropTypes from "prop-types";
import { Alert } from "bootstrap/Alert";
import { Portal } from "utils/Portal";
SuccessAlert.propTypes = {
onDismiss: PropTypes.func.isRequired,
};
const ALERT_CONTAINER_ID = "alert-container";
export function SuccessAlert({ onDismiss }) {
return (
<Portal containerId={ALERT_CONTAINER_ID}>
<Alert
type="success"
message={_("Settings were successfully saved.")}
onDismiss={onDismiss}
/>
</Portal>
);
}
FailAlert.propTypes = {
onDismiss: PropTypes.func.isRequired,
};
export function FailAlert({ onDismiss }) {
return (
<Portal containerId={ALERT_CONTAINER_ID}>
<Alert
type="danger"
message={_("Settings update was failed.")}
onDismiss={onDismiss}
/>
</Portal>
);
}

98
src/form/hooks.js Normal file
View File

@ -0,0 +1,98 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import { useCallback, useEffect, useReducer } from "react";
import update from "immutability-helper";
import { useAPIGet } from "api/hooks";
import { useWSForisModule } from "webSockets/hooks";
const FORM_ACTIONS = {
updateValue: 1,
resetData: 2,
};
export function useForm(validator, prepData) {
const [state, dispatch] = useReducer(formReducer, {
data: null,
initialData: null,
errors: {},
});
const onFormReload = useCallback((data) => {
dispatch({
type: FORM_ACTIONS.resetData,
data,
prepData,
validator,
});
}, [prepData, validator]);
const onFormChangeHandler = useCallback((updateRule) => (event) => {
dispatch({
type: FORM_ACTIONS.updateValue,
value: getChangedValue(event.target),
updateRule,
validator,
});
}, [validator]);
return [
state,
onFormChangeHandler,
onFormReload,
];
}
function formReducer(state, action) {
switch (action.type) {
case FORM_ACTIONS.updateValue: {
const newData = update(state.data, action.updateRule(action.value));
const errors = action.validator(newData);
return {
...state,
data: newData,
errors,
};
}
case FORM_ACTIONS.resetData: {
if (!action.data) return { ...state, initialData: state.data };
const prepData = action.prepData ? action.prepData(action.data) : action.data;
return {
data: prepData,
initialData: prepData,
errors: action.data ? action.validator(prepData) : undefined,
};
}
default: {
throw new Error();
}
}
}
function getChangedValue(target) {
let { value } = target;
if (target.type === "checkbox") {
value = target.checked;
} else if (target.type === "number") {
const parsedValue = parseInt(value);
value = Number.isNaN(parsedValue) ? value : parsedValue;
}
return value;
}
export function useForisModule(ws, config) {
const [APIGetState, get] = useAPIGet(config.endpoint);
const [WSData] = useWSForisModule(ws, config.wsModule, config.wsAction);
useEffect(() => {
get();
}, [WSData, get]);
return [APIGetState];
}

58
src/index.js Normal file
View File

@ -0,0 +1,58 @@
// API
export { useAPIGet, useAPIPost } from "./api/hooks";
// Bootstrap
export { Alert } from "bootstrap/Alert";
export { Button } from "bootstrap/Button";
export { CheckBox } from "bootstrap/CheckBox";
export { formFieldsSize } from "bootstrap/constants";
export { DataTimeInput } from "bootstrap/DataTimeInput";
export { EmailInput } from "bootstrap/EmailInput";
export { Input } from "bootstrap/Input";
export { NumberInput } from "bootstrap/NumberInput";
export { PasswordInput } from "bootstrap/PasswordInput";
export { RadioSet } from "bootstrap/RadioSet";
export { Select } from "bootstrap/Select";
export { TextInput } from "bootstrap/TextInput";
export {
Spinner,
SpinnerElement,
} from "bootstrap/Spinner";
export {
Modal,
ModalBody,
ModalFooter,
ModalHeader,
} from "bootstrap/Modal";
// Form
export { ForisForm } from "form/components/ForisForm";
export { SubmitButton, STATES as SUBMIT_BUTTON_STATES } from "form/components/SubmitButton";
export { useForisModule, useForm } from "form/hooks";
// Test Utils
export { mockedWS } from "testUtils/mockWS";
// WebSockets
export { useWSForisModule } from "webSockets/hooks";
export { WebSockets } from "webSockets/WebSockets";
// Utils
export { Portal } from "utils/Portal";
// Foris URL
export { ForisURLs, REFORIS_URL_PREFIX } from "forisUrls";
// Validation
export {
validateIPv4Address,
validateIPv6Address,
validateIPv6Prefix,
validateDomain,
validateDUID,
validateMAC,
validateMultipleEmails,
} from "validations";

View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
/* eslint import/export: "off" */
import React from 'react';
import PropTypes from 'prop-types';
import {UIDReset} from 'react-uid';
import {StaticRouter} from 'react-router';
import {render} from '@testing-library/react'
Wrapper.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
])
};
function Wrapper({children}) {
return <StaticRouter>
<UIDReset>
{children}
</UIDReset>
</StaticRouter>
}
const customTestRender = (ui, options) => render(ui, {wrapper: Wrapper, ...options});
// re-export everything
export * from '@testing-library/react'
// override render method
export {customTestRender as render}

18
src/testUtils/mockWS.js Normal file
View File

@ -0,0 +1,18 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
// Mock foris WS
export class mockedWS {
bind() {
return this
}
subscribe() {
return this
}
}

30
src/testUtils/setup.js Normal file
View File

@ -0,0 +1,30 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import mockAxios from 'jest-mock-axios';
import moment from 'moment-timezone';
// Setup axios cleanup
global.afterEach(() => {
mockAxios.reset();
});
// Mock babel (gettext)
global._ = str => str;
global.babel = {format: (str) => str};
global.ForisTranslations = {};
// Mock scrollIntoView
global.HTMLElement.prototype.scrollIntoView = () => {
};
jest.doMock('moment', () => {
moment.tz.setDefault('UTC');
return moment;
});
Date.now = jest.fn(() => new Date(Date.UTC(2019, 1, 1, 12, 13, 14)).valueOf());

14
src/utils/Portal.js Normal file
View File

@ -0,0 +1,14 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import ReactDOM from "react-dom";
export function Portal({ containerId, children }) {
const container = document.getElementById(containerId);
if (container) return ReactDOM.createPortal(children, container);
return null;
}

53
src/validations.js Normal file
View File

@ -0,0 +1,53 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
/**
* Set of validation helpers and validation error messages.
*/
export const ERROR_MESSAGES = {
IPv4: _("This is not a valid IPv4 address."),
IPv6: _("This is not a valid IPv6 address."),
IPv6Prefix: _("This is not a valid IPv6 prefix."),
domain: _("This is not a valid domain name."),
DUID: _("This is not a valid DUID."),
MAC: _("This is not a valid MAC address."),
MultipleEmails: _("Doesn't contain a list of emails separated by commas."),
};
const REs = {
IPv4: /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
IPv6: /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/,
IPv6Prefix: /^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))$/,
domain: /^([a-zA-Z0-9-]{1,63}\.?)*$/,
DUID: /^([0-9a-fA-F]{2}){4}([0-9a-fA-F]{2})*$/,
MAC: /^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/,
MultipleEmails: /^([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+ *)( *, *[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+ *)*$/,
};
const createValidator = (fieldType) => (value) => {
if (value && value !== "") return REs[fieldType].test(value) ? undefined : ERROR_MESSAGES[fieldType];
};
const validateIPv4Address = createValidator("IPv4");
const validateIPv6Address = createValidator("IPv6");
const validateIPv6Prefix = createValidator("IPv6Prefix");
const validateDomain = createValidator("domain");
const validateDUID = createValidator("DUID");
const validateMAC = createValidator("MAC");
const validateMultipleEmails = createValidator("MultipleEmails");
export {
validateIPv4Address,
validateIPv6Address,
validateIPv6Prefix,
validateDomain,
validateDUID,
validateMAC,
validateMultipleEmails,
};

View File

@ -0,0 +1,99 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
/* eslint no-console: "off" */
import { ForisURLs } from "forisUrls";
const PROTOCOL = window.location.protocol === "http:" ? "ws" : "wss";
const URL = process.env.LIGHTTPD
? `${PROTOCOL}://${window.location.hostname}/foris-ws`
: `${PROTOCOL}://${window.location.hostname}:${9081}`;
const WAITING_FOR_CONNECTION_TIMEOUT = 500;
export class WebSockets {
constructor() {
this.ws = new WebSocket(URL);
this.ws.onerror = (e) => {
if (window.location.pathname !== ForisURLs.login) {
console.error("WS: Error observed, you aren't logged probably.");
window.location.replace(ForisURLs.login);
}
console.log(`WS: Error: ${e}`);
};
this.ws.onmessage = (e) => {
console.log(`WS: Received Message: ${e.data}`);
const data = JSON.parse(e.data);
this.dispatch(data);
};
this.ws.onopen = () => {
console.log("WS: Connection open.");
};
this.ws.onclose = () => {
console.log("WS: Connection closed.");
};
// callbacks[module][action]
this.callbacks = {};
}
waitForConnection(callback) {
if (this.ws.readyState === 1) {
callback();
} else {
const that = this;
setTimeout(() => {
that.waitForConnection(callback);
}, WAITING_FOR_CONNECTION_TIMEOUT);
}
}
bind(module, action, callback) {
this.callbacks[module] = this.callbacks[module] || {};
this.callbacks[module][action] = this.callbacks[module][action] || [];
this.callbacks[module][action].push(callback);
return this;
}
subscribe(params) {
this.waitForConnection(() => {
this.send("subscribe", params);
});
return this;
}
send(action, params) {
const payload = JSON.stringify({ action, params });
this.waitForConnection(() => {
this.ws.send(payload);
});
return this;
}
dispatch(json) {
if (!json.module) return;
let chain;
try {
chain = this.callbacks[json.module][json.action];
} catch (e) {
if (e instanceof TypeError) {
console.log(`Callback for this message wasn't found:${e.data}`);
} else throw e;
}
if (typeof chain === "undefined") return;
for (let i = 0; i < chain.length; i++) chain[i](json);
}
close() {
this.ws.close();
}
}

23
src/webSockets/hooks.js Normal file
View File

@ -0,0 +1,23 @@
/*
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This is free software, licensed under the GNU General Public License v3.
* See /LICENSE for more information.
*/
import { useEffect, useState } from "react";
export function useWSForisModule(ws, module, action = "update_settings") {
const [data, setData] = useState(null);
useEffect(() => {
if (ws && module) {
ws.subscribe(module)
.bind(module, action, (msg) => {
setData(msg.data);
});
}
}, [action, module, ws]);
return [data];
}

View File

@ -0,0 +1,75 @@
# Translations template for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 12:55+0000\n"
"Last-Translator: Stepan Henek <stepan+github@henek.name>\n"
"Language-Team: Czech <https://hosted.weblate.org/projects/turris/foris-js/cs/"
">\n"
"Language: cs\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
"X-Generator: Weblate 3.9-dev\n"
"Generated-By: Babel 2.7.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr "Tohle není platná IPv4 adresa."
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr "Tohle není platná IPv6 adresa."
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr "Tohle není platný IPv6 prefix."
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr "Tohle není platné doménové jméno."
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr "Tohle není platné DUID."
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr "Tohle není platná MAC adresa."
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr "Neobsahuje seznam e-mailů oddělených čárkou."
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
"Změny, které byly provedeny, nebyly uloženy. Jste si jistý, že chcete "
"opustit stránku?"
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr "Aktualizuji"
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr "Načítám nastavení"
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr "Uložit"
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr "Nastavení bylo úspěšně uloženo."
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr "Ukládání nastavení selhalo."

View File

@ -0,0 +1,72 @@
# Danish translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:54+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: da\n"
"Language-Team: da <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,72 @@
# German translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:54+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de\n"
"Language-Team: de <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,72 @@
# Greek translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:54+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: el\n"
"Language-Team: el <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,72 @@
# English translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:21+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,72 @@
# Finnish translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:54+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: fi\n"
"Language-Team: fi <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,72 @@
# Faroese translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:54+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: fo\n"
"Language-Team: fo <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

71
translations/forisjs.pot Normal file
View File

@ -0,0 +1,71 @@
# Translations template for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.7.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,72 @@
# French translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:54+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: fr\n"
"Language-Team: fr <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,73 @@
# Croatian translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:55+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: hr\n"
"Language-Team: hr <LL@li.org>\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,72 @@
# Hungarian translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:55+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: hu\n"
"Language-Team: hu <LL@li.org>\n"
"Plural-Forms: nplurals=1; plural=0\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,72 @@
# Italian translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:55+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: it\n"
"Language-Team: it <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,72 @@
# Japanese translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:55+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: ja\n"
"Language-Team: ja <LL@li.org>\n"
"Plural-Forms: nplurals=1; plural=0\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,72 @@
# Korean translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:55+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: ko\n"
"Language-Team: ko <LL@li.org>\n"
"Plural-Forms: nplurals=1; plural=0\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,73 @@
# Lithuanian translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:55+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: lt\n"
"Language-Team: lt <LL@li.org>\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"(n%100<10 || n%100>=20) ? 1 : 2)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,72 @@
# Norwegian Bokmål translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:56+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: nb\n"
"Language-Team: nb <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,72 @@
# Norwegian Bokmål (Norway) translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:55+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: nb_NO\n"
"Language-Team: nb_NO <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,72 @@
# Dutch translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:56+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: nl\n"
"Language-Team: nl <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,73 @@
# Polish translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:56+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: pl\n"
"Language-Team: pl <LL@li.org>\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && "
"(n%100<10 || n%100>=20) ? 1 : 2)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,73 @@
# Romanian translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:56+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: ro\n"
"Language-Team: ro <LL@li.org>\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100"
" < 20)) ? 1 : 2)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,73 @@
# Russian translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:56+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: ru\n"
"Language-Team: ru <LL@li.org>\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,72 @@
# Slovak translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:56+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: sk\n"
"Language-Team: sk <LL@li.org>\n"
"Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""

View File

@ -0,0 +1,72 @@
# Swedish translations for PROJECT.
# Copyright (C) 2019 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-08-28 14:34+0200\n"
"PO-Revision-Date: 2019-08-28 17:56+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: sv\n"
"Language-Team: sv <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: src/validations.js:13
msgid "This is not a valid IPv4 address."
msgstr ""
#: src/validations.js:14
msgid "This is not a valid IPv6 address."
msgstr ""
#: src/validations.js:15
msgid "This is not a valid IPv6 prefix."
msgstr ""
#: src/validations.js:16
msgid "This is not a valid domain name."
msgstr ""
#: src/validations.js:17
msgid "This is not a valid DUID."
msgstr ""
#: src/validations.js:18
msgid "This is not a valid MAC address."
msgstr ""
#: src/validations.js:19
msgid "Doesn't contain a list of emails separated by commas."
msgstr ""
#: src/form/components/ForisForm.js:123
msgid "Changes you made may not be saved. Are you sure you want to leave?"
msgstr ""
#: src/form/components/SubmitButton.js:32
msgid "Updating"
msgstr ""
#: src/form/components/SubmitButton.js:35
msgid "Load settings"
msgstr ""
#: src/form/components/SubmitButton.js:38
msgid "Save"
msgstr ""
#: src/form/components/alerts.js:25
msgid "Settings were successfully saved."
msgstr ""
#: src/form/components/alerts.js:41
msgid "Settings update was failed."
msgstr ""