mirror of
https://gitlab.nic.cz/turris/reforis/foris-js.git
synced 2024-12-25 00:11:36 +01:00
Set babel.
This commit is contained in:
parent
02ca27e49c
commit
7b38c1658c
|
@ -59,4 +59,4 @@ module.exports = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -45,4 +45,6 @@ coverage.xml
|
||||||
|
|
||||||
/js/styleguide/
|
/js/styleguide/
|
||||||
.gitignore
|
.gitignore
|
||||||
src/
|
|
||||||
|
dist/
|
||||||
|
foris-*.tgz
|
||||||
|
|
1222
package-lock.json
generated
1222
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "foris",
|
"name": "foris",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Set of components and utils for Foris and its plugins.",
|
||||||
"author": "CZ.NIC, z.s.p.o.",
|
"author": "CZ.NIC, z.s.p.o.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -10,18 +12,20 @@
|
||||||
"reforis"
|
"reforis"
|
||||||
],
|
],
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"version": "0.0.1",
|
"main": "./dist/index.js",
|
||||||
"description": "Set of components and utils for Foris and its plugins.",
|
|
||||||
"main": "./dist/foris.js",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
"immutability-helper": "^3.0.1",
|
"immutability-helper": "^3.0.1",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^16.9.0-alpha.0",
|
|
||||||
"react-datetime": "^2.16.3",
|
"react-datetime": "^2.16.3",
|
||||||
"react-dom": "^16.9.0-alpha.0",
|
|
||||||
"react-router": "^5.0.1",
|
"react-router": "^5.0.1",
|
||||||
"react-uid": "^2.2.0"
|
"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": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.4.4",
|
"@babel/cli": "^7.4.4",
|
||||||
|
@ -36,7 +40,6 @@
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"babel-plugin-react-transform": "^3.0.0",
|
"babel-plugin-react-transform": "^3.0.0",
|
||||||
"babel-polyfill": "^6.26.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
"css-loader": "^3.0.0",
|
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^5.16.0",
|
||||||
"eslint-config-react-app": "^4.0.1",
|
"eslint-config-react-app": "^4.0.1",
|
||||||
"eslint-plugin-flowtype": "^2.50.3",
|
"eslint-plugin-flowtype": "^2.50.3",
|
||||||
|
@ -44,22 +47,24 @@
|
||||||
"eslint-plugin-jsx-a11y": "^6.2.1",
|
"eslint-plugin-jsx-a11y": "^6.2.1",
|
||||||
"eslint-plugin-react": "^7.13.0",
|
"eslint-plugin-react": "^7.13.0",
|
||||||
"eslint-plugin-react-hooks": "^1.6.0",
|
"eslint-plugin-react-hooks": "^1.6.0",
|
||||||
"file-loader": "^4.0.0",
|
|
||||||
"jest": "^24.8.0",
|
"jest": "^24.8.0",
|
||||||
"jest-mock-axios": "^3.0.0",
|
"jest-mock-axios": "^3.0.0",
|
||||||
|
"moment": "^2.24.0",
|
||||||
"moment-timezone": "^0.5.25",
|
"moment-timezone": "^0.5.25",
|
||||||
|
"react": "^16.9.0",
|
||||||
|
"react-dom": "^16.9.0",
|
||||||
"react-styleguidist": "^9.1.11",
|
"react-styleguidist": "^9.1.11",
|
||||||
"snapshot-diff": "^0.5.1",
|
"snapshot-diff": "^0.5.1"
|
||||||
"style-loader": "^0.23.1",
|
|
||||||
"webpack": "^4.33.0",
|
|
||||||
"webpack-cli": "^3.3.4"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "webpack --watch --mode=development --env.lighttpd",
|
"build": "babel ./src --out-dir ./dist -s inline",
|
||||||
"build": "webpack --mode=production --env.lighttpd",
|
"prepare": "rm -rf ./dist && npm run build",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:coverage": "jest --coverage --colors"
|
"test:coverage": "jest --coverage --colors"
|
||||||
}
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
21
src/api/constants.js
Normal file
21
src/api/constants.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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",
|
||||||
|
};
|
158
src/api/hooks.js
Normal file
158
src/api/hooks.js
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
/*
|
||||||
|
* 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 './constants';
|
||||||
|
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function adds one to its input.
|
||||||
|
* @returns {number} that number, plus one.
|
||||||
|
* @param url
|
||||||
|
*/
|
||||||
|
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];
|
||||||
|
}
|
1
src/api/index.js
Normal file
1
src/api/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./hooks";
|
29
src/bootstrap/__tests__/Button.test.js
Normal file
29
src/bootstrap/__tests__/Button.test.js
Normal 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 Button from '../components/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()
|
||||||
|
});
|
||||||
|
});
|
36
src/bootstrap/__tests__/Checkbox.test.js
Normal file
36
src/bootstrap/__tests__/Checkbox.test.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* 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 '../components/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();
|
||||||
|
});
|
||||||
|
});
|
28
src/bootstrap/__tests__/NumberInput.test.js
Normal file
28
src/bootstrap/__tests__/NumberInput.test.js
Normal 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 NumberInput from '../components/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();
|
||||||
|
});
|
||||||
|
});
|
27
src/bootstrap/__tests__/PasswordInput.test.js
Normal file
27
src/bootstrap/__tests__/PasswordInput.test.js
Normal 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 {render} from 'customTestRender';
|
||||||
|
|
||||||
|
import PasswordInput from '../components/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();
|
||||||
|
});
|
||||||
|
});
|
35
src/bootstrap/__tests__/RadioSet.test.js
Normal file
35
src/bootstrap/__tests__/RadioSet.test.js
Normal 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 {render} from 'customTestRender';
|
||||||
|
|
||||||
|
import RadioSet from '../components/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();
|
||||||
|
});
|
||||||
|
});
|
51
src/bootstrap/__tests__/Select.test.js
Normal file
51
src/bootstrap/__tests__/Select.test.js
Normal 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 {fireEvent, getByDisplayValue, getByText, render} from 'customTestRender';
|
||||||
|
|
||||||
|
import Select from '../components/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');
|
||||||
|
})
|
||||||
|
});
|
27
src/bootstrap/__tests__/TextInput.test.js
Normal file
27
src/bootstrap/__tests__/TextInput.test.js
Normal 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 {render} from 'customTestRender';
|
||||||
|
|
||||||
|
import TextInput from '../components/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();
|
||||||
|
});
|
||||||
|
});
|
37
src/bootstrap/__tests__/__snapshots__/Button.test.js.snap
Normal file
37
src/bootstrap/__tests__/__snapshots__/Button.test.js.snap
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<Button /> Render button correctly 1`] = `
|
||||||
|
<button
|
||||||
|
class="btn btn-primary "
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
Test Button
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<Button /> Render button with custom classes 1`] = `
|
||||||
|
<button
|
||||||
|
class="btn one two three"
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
Test Button
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<Button /> Render button with spinner 1`] = `
|
||||||
|
<button
|
||||||
|
class="btn btn-primary "
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="spinner-border spinner-border-sm"
|
||||||
|
role="status"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Test Button
|
||||||
|
</button>
|
||||||
|
`;
|
62
src/bootstrap/__tests__/__snapshots__/Checkbox.test.js.snap
Normal file
62
src/bootstrap/__tests__/__snapshots__/Checkbox.test.js.snap
Normal 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>
|
||||||
|
`;
|
|
@ -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>
|
||||||
|
`;
|
|
@ -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>
|
||||||
|
`;
|
73
src/bootstrap/__tests__/__snapshots__/RadioSet.test.js.snap
Normal file
73
src/bootstrap/__tests__/__snapshots__/RadioSet.test.js.snap
Normal 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>
|
||||||
|
`;
|
40
src/bootstrap/__tests__/__snapshots__/Select.test.js.snap
Normal file
40
src/bootstrap/__tests__/__snapshots__/Select.test.js.snap
Normal 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>
|
||||||
|
`;
|
32
src/bootstrap/__tests__/__snapshots__/TextInput.test.js.snap
Normal file
32
src/bootstrap/__tests__/__snapshots__/TextInput.test.js.snap
Normal 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>
|
||||||
|
`;
|
31
src/bootstrap/components/Alert.js
Normal file
31
src/bootstrap/components/Alert.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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 default function Alert({type, message, onDismiss, children}) {
|
||||||
|
return <div className={`alert alert-dismissible alert-${type}`}>
|
||||||
|
{onDismiss ? <button type="button" className="close" onClick={onDismiss}>×</button> : false}
|
||||||
|
{message}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
}
|
20
src/bootstrap/components/Alert.md
Normal file
20
src/bootstrap/components/Alert.md
Normal 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/>
|
||||||
|
|
||||||
|
```
|
43
src/bootstrap/components/Button.js
Normal file
43
src/bootstrap/components/Button.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* 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 default 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 className={className} {...props}>
|
||||||
|
{span} {span ? ' ' : null} {children}
|
||||||
|
</button>;
|
||||||
|
}
|
15
src/bootstrap/components/Button.md
Normal file
15
src/bootstrap/components/Button.md
Normal 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>
|
||||||
|
```
|
46
src/bootstrap/components/Checkbox.js
Normal file
46
src/bootstrap/components/Checkbox.js
Normal 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 {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 default 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/components/Checkbox.md
Normal file
16
src/bootstrap/components/Checkbox.md
Normal 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)}
|
||||||
|
/>
|
||||||
|
```
|
57
src/bootstrap/components/DataTimeInput.js
Normal file
57
src/bootstrap/components/DataTimeInput.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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 default 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}
|
||||||
|
/>;
|
||||||
|
}
|
25
src/bootstrap/components/DataTimeInput.md
Normal file
25
src/bootstrap/components/DataTimeInput.md
Normal 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)}
|
||||||
|
/>
|
||||||
|
```
|
27
src/bootstrap/components/EmailInput.js
Normal file
27
src/bootstrap/components/EmailInput.js
Normal 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 Input from './Input';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailInput;
|
18
src/bootstrap/components/EmailInput.md
Normal file
18
src/bootstrap/components/EmailInput.md
Normal 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>
|
||||||
|
```
|
46
src/bootstrap/components/Input.js
Normal file
46
src/bootstrap/components/Input.js
Normal 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 {useUID} from 'react-uid/dist/es5/index';
|
||||||
|
import {formFieldsSize} from './constants';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
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 default function Input({type, label, helpText, error, className, children, ...props}) {
|
||||||
|
const uid = useUID();
|
||||||
|
const inputClassName = `form-control ${className ? 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>;
|
||||||
|
}
|
88
src/bootstrap/components/Modal.js
Normal file
88
src/bootstrap/components/Modal.js
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* 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">×</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/components/Modal.md
Normal file
27
src/bootstrap/components/Modal.md
Normal 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>
|
||||||
|
</>
|
||||||
|
```
|
29
src/bootstrap/components/NumberInput.js
Normal file
29
src/bootstrap/components/NumberInput.js
Normal 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 Input from './Input';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
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,
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NumberInput;
|
17
src/bootstrap/components/NumberInput.md
Normal file
17
src/bootstrap/components/NumberInput.md
Normal 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)}
|
||||||
|
/>
|
||||||
|
```
|
44
src/bootstrap/components/PasswordInput.js
Normal file
44
src/bootstrap/components/PasswordInput.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* 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 default function PasswordInput({withEye, ...props}) {
|
||||||
|
const [isHidden, setHidden] = useState(true);
|
||||||
|
return <Input
|
||||||
|
type={withEye ? isHidden ? 'password' : 'text' : 'password'}
|
||||||
|
autoComplete={isHidden ? 'new-password' : null}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{withEye ?
|
||||||
|
<div className="input-group-append">
|
||||||
|
<button className="input-group-text" onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setHidden(isHidden => !isHidden);
|
||||||
|
}}>
|
||||||
|
<i className={'fa ' + (isHidden ? 'fa-eye' : 'fa-eye-slash')}/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
: null}
|
||||||
|
</Input>
|
||||||
|
}
|
17
src/bootstrap/components/PasswordInput.md
Normal file
17
src/bootstrap/components/PasswordInput.md
Normal 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)}
|
||||||
|
/>
|
||||||
|
```
|
80
src/bootstrap/components/RadioSet.js
Normal file
80
src/bootstrap/components/RadioSet.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* 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 default function RadioSet({name, label, choices, value, helpText, ...props}) {
|
||||||
|
const uid = useUID();
|
||||||
|
const radios = choices.map((choice, key) =>
|
||||||
|
<Radio
|
||||||
|
id={`${name}-${key}`}
|
||||||
|
key={key}
|
||||||
|
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/components/RadioSet.md
Normal file
24
src/bootstrap/components/RadioSet.md
Normal 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>
|
||||||
|
</>
|
||||||
|
```
|
45
src/bootstrap/components/Select.js
Normal file
45
src/bootstrap/components/Select.js
Normal 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 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 default 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/components/Select.md
Normal file
25
src/bootstrap/components/Select.md
Normal 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>
|
||||||
|
</>
|
||||||
|
```
|
57
src/bootstrap/components/Spinner.js
Normal file
57
src/bootstrap/components/Spinner.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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 default function Spinner({fullScreen, children, className, ...props}) {
|
||||||
|
if (!fullScreen) {
|
||||||
|
return <div className={'spinner-wrapper ' + (className ? 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/components/Spinner.md
Normal file
5
src/bootstrap/components/Spinner.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
Spiner Bootstrap component.
|
||||||
|
|
||||||
|
```js
|
||||||
|
<Spinner>You can put text inside or any component you wish.</Spinner>
|
||||||
|
```
|
26
src/bootstrap/components/TextInput.js
Normal file
26
src/bootstrap/components/TextInput.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TextInput;
|
15
src/bootstrap/components/TextInput.md
Normal file
15
src/bootstrap/components/TextInput.md
Normal 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)}
|
||||||
|
/>
|
||||||
|
```
|
9
src/bootstrap/components/constants.js
Normal file
9
src/bootstrap/components/constants.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
* 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 */
|
||||||
|
export const formFieldsSize = 'col-sm-12 offset-lg-1 col-lg-10';
|
31
src/bootstrap/index.js
Normal file
31
src/bootstrap/index.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import * as Alert from "./components/Alert";
|
||||||
|
import * as Button from "./components/Button";
|
||||||
|
import * as Checkbox from "./components/Checkbox";
|
||||||
|
import * as constants from "./components/constants";
|
||||||
|
import * as DataTimeInput from "./components/DataTimeInput";
|
||||||
|
import * as EmailInput from "./components/EmailInput";
|
||||||
|
import * as Input from "./components/Input";
|
||||||
|
import * as Modal from "./components/Modal";
|
||||||
|
import * as NumberInput from "./components/NumberInput";
|
||||||
|
import * as PasswordInput from "./components/PasswordInput";
|
||||||
|
import * as RadioSet from "./components/RadioSet";
|
||||||
|
import * as Select from "./components/Select";
|
||||||
|
import * as Spinner from "./components/Spinner";
|
||||||
|
import * as TextInput from "./components/TextInput";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
constants,
|
||||||
|
DataTimeInput,
|
||||||
|
EmailInput,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
NumberInput,
|
||||||
|
PasswordInput,
|
||||||
|
RadioSet,
|
||||||
|
Select,
|
||||||
|
Spinner,
|
||||||
|
TextInput,
|
||||||
|
}
|
26
src/form/__tests__/SubmitButton.test.js
Normal file
26
src/form/__tests__/SubmitButton.test.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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 SubmitButton, {STATES} 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();
|
||||||
|
});
|
||||||
|
});
|
51
src/form/__tests__/__snapshots__/SubmitButton.test.js.snap
Normal file
51
src/form/__tests__/__snapshots__/SubmitButton.test.js.snap
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// 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=""
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<SubmitButton/> Render saving 1`] = `
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary offset-lg-8 col-lg-3 col-sm-12"
|
||||||
|
disabled=""
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="spinner-border spinner-border-sm"
|
||||||
|
role="status"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Updating
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
115
src/form/__tests__/hooks.test.js
Normal file
115
src/form/__tests__/hooks.test.js
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: rewrite this test
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {fireEvent, render, waitForElement} from 'customTestRender';
|
||||||
|
import mockAxios from 'jest-mock-axios';
|
||||||
|
|
||||||
|
import ForisForm from '../components/ForisForm';
|
||||||
|
import API_URLs from 'common/API';
|
||||||
|
|
||||||
|
// 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: API_URLs.wan,
|
||||||
|
wsModule: 'wan'
|
||||||
|
}}
|
||||||
|
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({});
|
||||||
|
|
||||||
|
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('/api/wan', 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(
|
||||||
|
'/api/wan',
|
||||||
|
{'field': 'preparedDataToSubmit'},
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
84
src/form/__tests__/validation.test.js
Normal file
84
src/form/__tests__/validation.test.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
});
|
||||||
|
});
|
148
src/form/components/ForisForm.js
Normal file
148
src/form/components/ForisForm.js
Normal 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 {Prompt} from 'react-router';
|
||||||
|
|
||||||
|
import Spinner from 'bootstrap/components/Spinner';
|
||||||
|
import {useAPIPost} from 'api';
|
||||||
|
|
||||||
|
import {useForisModule, useForm} from '../hooks';
|
||||||
|
import SubmitButton, {STATES as SUBMIT_BUTTON_STATES} 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 default 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;
|
||||||
|
else 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?')
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Prompt message={getMessageOnLeavingPage}/>
|
||||||
|
{!alertIsDismissed ?
|
||||||
|
postState.isSuccess ?
|
||||||
|
<SuccessAlert onDismiss={() => setAlertIsDismissed(true)}/>
|
||||||
|
: postState.isError ?
|
||||||
|
<FailAlert onDismiss={() => setAlertIsDismissed(true)}/>
|
||||||
|
: null
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
{childrenWithFormProps}
|
||||||
|
<SubmitButton
|
||||||
|
state={getSubmitButtonState()}
|
||||||
|
disabled={submitButtonIsDisabled}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
}
|
49
src/form/components/SubmitButton.js
Normal file
49
src/form/components/SubmitButton.js
Normal 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 Button from 'bootstrap/components/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 default 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
|
||||||
|
loading={loadingSubmitButton}
|
||||||
|
disabled={disableSubmitButton}
|
||||||
|
forisFormSize
|
||||||
|
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{labelSubmitButton}
|
||||||
|
</Button>;
|
||||||
|
}
|
42
src/form/components/alerts.js
Normal file
42
src/form/components/alerts.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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/components/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>
|
||||||
|
}
|
100
src/form/hooks.js
Normal file
100
src/form/hooks.js
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
|
||||||
|
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: data,
|
||||||
|
prepData: prepData,
|
||||||
|
validator: validator,
|
||||||
|
});
|
||||||
|
}, [prepData, validator]);
|
||||||
|
|
||||||
|
const onFormChangeHandler = useCallback(updateRule =>
|
||||||
|
event => {
|
||||||
|
dispatch({
|
||||||
|
type: FORM_ACTIONS.updateValue,
|
||||||
|
value: getChangedValue(event.target),
|
||||||
|
updateRule: updateRule,
|
||||||
|
validator: validator,
|
||||||
|
})
|
||||||
|
}, [validator]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
state,
|
||||||
|
onFormChangeHandler,
|
||||||
|
onFormReload,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const FORM_ACTIONS = {
|
||||||
|
updateValue: 1,
|
||||||
|
resetData: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
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: 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.value;
|
||||||
|
if (target.type === 'checkbox') {
|
||||||
|
value = target.checked;
|
||||||
|
} else if (target.type === 'number') {
|
||||||
|
const parsedValue = parseInt(value);
|
||||||
|
value = 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];
|
||||||
|
}
|
5
src/form/index.js
Normal file
5
src/form/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import * as ForisForm from "./components/ForisForm";
|
||||||
|
import * as SubmitButton from "./components/SubmitButton";
|
||||||
|
import * as hooks from "./hooks";
|
||||||
|
|
||||||
|
export {ForisForm, SubmitButton, hooks}
|
16
src/index.js
Normal file
16
src/index.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import * as api from './api';
|
||||||
|
import * as bootstrap from './bootstrap';
|
||||||
|
import * as form from './form';
|
||||||
|
import * as testUtils from './testUtils';
|
||||||
|
import * as validations from './validations';
|
||||||
|
|
||||||
|
import {Portal} from './utils';
|
||||||
|
|
||||||
|
export {
|
||||||
|
api,
|
||||||
|
bootstrap,
|
||||||
|
form,
|
||||||
|
testUtils,
|
||||||
|
validations,
|
||||||
|
Portal,
|
||||||
|
}
|
37
src/testUtils/customTestRender.js
Normal file
37
src/testUtils/customTestRender.js
Normal 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}
|
11
src/testUtils/index.js
Normal file
11
src/testUtils/index.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import * as customTestRender from "./customTestRender";
|
||||||
|
import * as mockWS from "./mockWS";
|
||||||
|
import * as setupGlobals from "./setupGlobals";
|
||||||
|
import * as setupTest from "./setupTest";
|
||||||
|
|
||||||
|
export {
|
||||||
|
customTestRender,
|
||||||
|
mockWS,
|
||||||
|
setupGlobals,
|
||||||
|
setupTest,
|
||||||
|
}
|
18
src/testUtils/mockWS.js
Normal file
18
src/testUtils/mockWS.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
11
src/testUtils/setupGlobals.js
Normal file
11
src/testUtils/setupGlobals.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Fake babel (gettext) used for docs
|
||||||
|
global._ = str => str;
|
||||||
|
global.babel = {format: (str) => str};
|
||||||
|
global.ForisTranslations = {};
|
37
src/testUtils/setupTest.js
Normal file
37
src/testUtils/setupTest.js
Normal 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
export function setUpTest() {
|
||||||
|
const mockAxios = require('jest-mock-axios');
|
||||||
|
const moment = require('moment-timezone');
|
||||||
|
|
||||||
|
// Setup axios cleanup
|
||||||
|
global.afterEach(() => {
|
||||||
|
mockAxios.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock babel (gettext)
|
||||||
|
window._ = str => str;
|
||||||
|
window.babel = {format: (str) => str};
|
||||||
|
window.ForisTranslations = {};
|
||||||
|
|
||||||
|
// Mock scrollIntoView
|
||||||
|
window.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());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
15
src/utils/Portal.js
Normal file
15
src/utils/Portal.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* 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 default function Portal({containerId, children}) {
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
if (container)
|
||||||
|
return ReactDOM.createPortal(children, container);
|
||||||
|
return null;
|
||||||
|
}
|
3
src/utils/index.js
Normal file
3
src/utils/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import Portal from './Portal';
|
||||||
|
|
||||||
|
export {Portal}
|
53
src/validations.js
Normal file
53
src/validations.js
Normal 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,
|
||||||
|
};
|
99
src/webSockets/WebSockets.js
Normal file
99
src/webSockets/WebSockets.js
Normal 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 "./constants";
|
||||||
|
|
||||||
|
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 default 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();
|
||||||
|
}
|
||||||
|
}
|
22
src/webSockets/hooks.js
Normal file
22
src/webSockets/hooks.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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 default 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];
|
||||||
|
}
|
|
@ -1,34 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = () => ({
|
|
||||||
mode: 'development',
|
|
||||||
entry: './src/index.js',
|
|
||||||
resolve: {
|
|
||||||
modules: [
|
|
||||||
path.resolve(__dirname, './src'),
|
|
||||||
path.resolve(__dirname, './node_modules')
|
|
||||||
],
|
|
||||||
extensions: [".js", ".json"],
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [{
|
|
||||||
test: /\.js$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
loader: 'babel-loader'
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
filename: 'foris.js',
|
|
||||||
path: path.join(__dirname, 'dist/'),
|
|
||||||
libraryTarget: 'umd',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user