mirror of
https://gitlab.nic.cz/turris/reforis/foris-js.git
synced 2025-06-15 13:36:35 +02:00
Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
8f88b09e66 | |||
f2aa28f172 | |||
dccc9e5769 | |||
30748fab12 | |||
a17ecc6060 | |||
75ce94d3ee | |||
96263af618 | |||
50a1bfd9b5 | |||
b360431f38 | |||
9eebb35e65 | |||
cf556e3518 | |||
5977e675cf | |||
ee5f3da093 | |||
afa8c160a3 | |||
0af56ec84c | |||
96eed02d32 | |||
8479518fab | |||
800dedbcfd | |||
44e45499c6 | |||
5457f68c70 | |||
680871de62 | |||
9641fbcb50 | |||
1708f026a4 | |||
7516cfdbfe | |||
cbb1382c66 | |||
e4e28dbce4 | |||
7a14ed1b60 | |||
497a1a0ad3 | |||
4a481ea642 | |||
8296c6129e | |||
7253319b61 | |||
7371fbcac0 | |||
d658f0b112 | |||
edc5c92421 | |||
d4bc5a6998 | |||
b938dd85ff | |||
4ff3e33411 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -43,8 +43,8 @@ coverage.xml
|
|||||||
## Translations
|
## Translations
|
||||||
*.mo
|
*.mo
|
||||||
|
|
||||||
/js/styleguide/
|
|
||||||
.gitignore
|
.gitignore
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
foris-*.tgz
|
foris-*.tgz
|
||||||
|
styleguide/
|
||||||
|
@ -3,6 +3,7 @@ image: node:8-alpine
|
|||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- build
|
- build
|
||||||
|
- publish
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- npm install
|
- npm install
|
||||||
@ -24,3 +25,19 @@ build:
|
|||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- foris-*.tgz
|
- foris-*.tgz
|
||||||
|
|
||||||
|
publish_beta:
|
||||||
|
stage: publish
|
||||||
|
only:
|
||||||
|
refs:
|
||||||
|
- dev
|
||||||
|
script:
|
||||||
|
- sh scripts/publish.sh beta
|
||||||
|
|
||||||
|
publish_latest:
|
||||||
|
stage: publish
|
||||||
|
only:
|
||||||
|
refs:
|
||||||
|
- master
|
||||||
|
script:
|
||||||
|
- sh scripts/publish.sh latest
|
||||||
|
35
Makefile
35
Makefile
@ -1,17 +1,50 @@
|
|||||||
.PHONY: all create-messages update-messages clean
|
.PHONY: all install-js watch-js build-js lint-js test-js create-messages update-messages docs clean
|
||||||
|
|
||||||
all:
|
all:
|
||||||
|
@echo "make install-js"
|
||||||
|
@echo " Install dependencies"
|
||||||
|
@echo "make watch-js"
|
||||||
|
@echo " Compile JS in watch mode."
|
||||||
|
@echo "make build-js"
|
||||||
|
@echo " Compile JS."
|
||||||
|
@echo "make lint-js"
|
||||||
|
@echo " Run linter"
|
||||||
|
@echo "make test-js"
|
||||||
|
@echo " Run tests"
|
||||||
@echo "make create-messages"
|
@echo "make create-messages"
|
||||||
@echo " Create locale messages (.pot)."
|
@echo " Create locale messages (.pot)."
|
||||||
@echo "make update-messages"
|
@echo "make update-messages"
|
||||||
@echo " Update locale messages from .pot file."
|
@echo " Update locale messages from .pot file."
|
||||||
|
@echo "make docs"
|
||||||
|
@echo " Build project documentation."
|
||||||
|
@echo "make docs-watch"
|
||||||
|
@echo " Start styleguidist server."
|
||||||
@echo "make clean"
|
@echo "make clean"
|
||||||
@echo " Remove python artifacts and virtualenv."
|
@echo " Remove python artifacts and virtualenv."
|
||||||
|
|
||||||
|
install-js: package.json
|
||||||
|
npm install --save-dev
|
||||||
|
|
||||||
|
watch-js:
|
||||||
|
npm run build:watch
|
||||||
|
build-js:
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
lint:
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
test:
|
||||||
|
npm test
|
||||||
|
|
||||||
create-messages:
|
create-messages:
|
||||||
pybabel extract -F babel.cfg -o ./translations/forisjs.pot .
|
pybabel extract -F babel.cfg -o ./translations/forisjs.pot .
|
||||||
update-messages:
|
update-messages:
|
||||||
pybabel update -i translations/forisjs.pot -d translations
|
pybabel update -i translations/forisjs.pot -d translations
|
||||||
|
|
||||||
|
docs:
|
||||||
|
npm run-script docs
|
||||||
|
docs-watch:
|
||||||
|
npm run-script docs:watch
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf node_modules dist
|
rm -rf node_modules dist
|
||||||
|
17
README.md
Normal file
17
README.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# foris-js
|
||||||
|
|
||||||
|
## Publishing package
|
||||||
|
|
||||||
|
### Beta versions
|
||||||
|
|
||||||
|
Each commit to `dev` branch will result in publishing a new version of library
|
||||||
|
tagged `beta`. Versions names are based on commit SHA, e.g.
|
||||||
|
`foris@0.1.0-beta.d9073aa4`.
|
||||||
|
|
||||||
|
### Preparing a release
|
||||||
|
|
||||||
|
1. Crete a merge request to `dev` branch with version bumped
|
||||||
|
2. When merging add `[skip ci]` to commit message to prevent publishing
|
||||||
|
unnecessary version
|
||||||
|
3. Create a merge request from `dev` to `master` branch
|
||||||
|
4. New version should be published automatically
|
1
docs/intro.md
Normal file
1
docs/intro.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
Foris JS library is set of componets and utils for Foris JS application and plugins.
|
@ -24,4 +24,8 @@ module.exports = {
|
|||||||
globals: {
|
globals: {
|
||||||
TZ: "utc",
|
TZ: "utc",
|
||||||
},
|
},
|
||||||
|
transform: {
|
||||||
|
"^.+\\.js$": "babel-jest",
|
||||||
|
"^.+\\.css$": "jest-transform-css",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
2134
package-lock.json
generated
2134
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "foris",
|
"name": "foris",
|
||||||
"version": "0.0.7",
|
"version": "1.1.0",
|
||||||
"description": "Set of components and utils for Foris and its plugins.",
|
"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": {
|
||||||
@ -15,54 +15,67 @@
|
|||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
"immutability-helper": "^3.0.1",
|
"immutability-helper": "^3.0.0",
|
||||||
|
"jest-transform-css": "^2.0.0",
|
||||||
|
"moment": "^2.24.0",
|
||||||
|
"moment-timezone": "^0.5.25",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react-datetime": "^2.16.3",
|
"react-datetime": "^2.16.3",
|
||||||
"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": {
|
"peerDependencies": {
|
||||||
"react": "^16.9.0",
|
"react": "^16.9.0",
|
||||||
"react-dom": "^16.9.0"
|
"react-dom": "^16.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.5.5",
|
"@babel/cli": "^7.4.4",
|
||||||
"@babel/core": "^7.5.5",
|
"@babel/core": "^7.4.5",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.4.4",
|
||||||
"@babel/plugin-syntax-export-default-from": "^7.2.0",
|
"@babel/plugin-syntax-export-default-from": "^7.2.0",
|
||||||
"@babel/plugin-transform-runtime": "^7.4.4",
|
"@babel/plugin-transform-runtime": "^7.4.4",
|
||||||
"@babel/preset-env": "^7.4.5",
|
"@babel/preset-env": "^7.4.5",
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
"@testing-library/react": "^8.0.1",
|
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||||
|
"@testing-library/react": "^8.0.9",
|
||||||
"babel-eslint": "^9.0.0",
|
"babel-eslint": "^9.0.0",
|
||||||
"babel-jest": "^24.8.0",
|
"babel-jest": "^24.8.0",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"babel-plugin-module-resolver": "^3.2.0",
|
"babel-plugin-module-resolver": "^3.2.0",
|
||||||
"babel-plugin-react-transform": "^3.0.0",
|
"babel-plugin-react-transform": "^3.0.0",
|
||||||
"babel-polyfill": "^6.26.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
"eslint": "^5.16.0",
|
"bootstrap": "^4.3.1",
|
||||||
|
"copy-webpack-plugin": "^5.0.4",
|
||||||
|
"css-loader": "^3.2.0",
|
||||||
|
"eslint": "^6.1.0",
|
||||||
"eslint-config-airbnb": "^18.0.1",
|
"eslint-config-airbnb": "^18.0.1",
|
||||||
"eslint-plugin-import": "^2.17.3",
|
"eslint-plugin-import": "^2.18.2",
|
||||||
"eslint-plugin-jsx-a11y": "^6.2.1",
|
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||||
"eslint-plugin-react": "^7.13.0",
|
"eslint-plugin-react": "^7.14.3",
|
||||||
"eslint-plugin-react-hooks": "^1.6.0",
|
"eslint-plugin-react-hooks": "^1.7.0",
|
||||||
|
"file-loader": "^4.2.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": "^2.24.0",
|
||||||
"moment-timezone": "^0.5.25",
|
"moment-timezone": "^0.5.25",
|
||||||
"react": "^16.9.0",
|
"react": "^16.9.0",
|
||||||
"react-dom": "^16.9.0",
|
"react-dom": "^16.9.0",
|
||||||
"react-styleguidist": "^9.1.11",
|
"react-styleguidist": "^9.1.16",
|
||||||
"snapshot-diff": "^0.5.1"
|
"snapshot-diff": "^0.5.1",
|
||||||
|
"style-loader": "^1.0.0",
|
||||||
|
"webpack": "^4.41.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rm -rf dist; babel src --out-dir dist --ignore '**/__tests__' --source-maps inline",
|
"build": "rm -rf dist; babel src --out-dir dist --ignore '**/__tests__' --source-maps inline --copy-files",
|
||||||
|
"build:watch": "babel src --verbose --watch --out-dir dist --ignore '**/__tests__' --source-maps inline --copy-files",
|
||||||
"prepare": "rm -rf ./dist && npm run build",
|
"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",
|
||||||
|
"test:update-snapshots": "jest -u",
|
||||||
|
"docs": "npx styleguidist build ",
|
||||||
|
"docs:watch": "styleguidist server"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/**",
|
"dist/**",
|
||||||
|
23
scripts/publish.sh
Executable file
23
scripts/publish.sh
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if test -z "$NPM_TOKEN"
|
||||||
|
then
|
||||||
|
echo "\$NPM_TOKEN is not set"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
# Need to replace "_" with "_" as GitLab CI won't accept secret vars with "-"
|
||||||
|
echo "//registry.npmjs.org/:_authToken=$(echo "$NPM_TOKEN" | tr _ -)" > .npmrc
|
||||||
|
echo "unsafe-perm = true" >> ~/.npmrc
|
||||||
|
if test "$1" = "beta"
|
||||||
|
then
|
||||||
|
BETA_VERSION=$(npx -c 'echo "$npm_package_version"')-beta.$CI_COMMIT_SHORT_SHA
|
||||||
|
npm version "$BETA_VERSION" --git-tag-version false
|
||||||
|
npm publish --tag beta
|
||||||
|
elif test "$1" = "latest"
|
||||||
|
then
|
||||||
|
npm publish
|
||||||
|
else
|
||||||
|
echo "Usage: publish.sh [ beta | latest ]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
35
src/alertContext/AlertContext.js
Normal file
35
src/alertContext/AlertContext.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, { useState } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import { Alert } from "bootstrap/Alert";
|
||||||
|
|
||||||
|
const AlertContext = React.createContext();
|
||||||
|
|
||||||
|
AlertContextProvider.propTypes = {
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
PropTypes.node,
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
function AlertContextProvider({ children }) {
|
||||||
|
const [alert, setAlert] = useState(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{alert && <Alert type="danger" message={alert} onDismiss={() => setAlert(null)} />}
|
||||||
|
<AlertContext.Provider value={setAlert}>
|
||||||
|
{ children }
|
||||||
|
</AlertContext.Provider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AlertContext, AlertContextProvider };
|
48
src/alertContext/__tests__/AlertContext.test.js
Normal file
48
src/alertContext/__tests__/AlertContext.test.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* 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, { useContext } from "react";
|
||||||
|
import { render, getByText, queryByText, fireEvent } from "customTestRender";
|
||||||
|
|
||||||
|
import { AlertContext, AlertContextProvider } from "../AlertContext";
|
||||||
|
|
||||||
|
function AlertTest() {
|
||||||
|
const setAlert = useContext(AlertContext);
|
||||||
|
return <button onClick={() => setAlert("Alert content")}>Set alert</button>;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("AlertContext", () => {
|
||||||
|
let componentContainer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const { container } = render(
|
||||||
|
<AlertContextProvider>
|
||||||
|
<AlertTest />
|
||||||
|
</AlertContextProvider>
|
||||||
|
);
|
||||||
|
componentContainer = container;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render component without alert", () => {
|
||||||
|
expect(componentContainer).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render alert", () => {
|
||||||
|
fireEvent.click(getByText(componentContainer, "Set alert"));
|
||||||
|
expect(componentContainer).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should dismiss alert", () => {
|
||||||
|
fireEvent.click(getByText(componentContainer, "Set alert"));
|
||||||
|
// Alert is present
|
||||||
|
expect(getByText(componentContainer, "Alert content")).toBeDefined();
|
||||||
|
|
||||||
|
fireEvent.click(componentContainer.querySelector(".close"));
|
||||||
|
// Alert is gone
|
||||||
|
expect(queryByText(componentContainer, "Alert content")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,28 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`AlertContext should render alert 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="alert alert-dismissible alert-danger"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="close"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
Alert content
|
||||||
|
</div>
|
||||||
|
<button>
|
||||||
|
Set alert
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`AlertContext should render component without alert 1`] = `
|
||||||
|
<div>
|
||||||
|
<button>
|
||||||
|
Set alert
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
41
src/api/delete.js
Normal file
41
src/api/delete.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useReducer, useCallback } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import {
|
||||||
|
API_ACTIONS, TIMEOUT, HEADERS, APIReducer, getErrorMessage,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
export function useAPIDelete(url) {
|
||||||
|
const [state, dispatch] = useReducer(APIReducer, {
|
||||||
|
isSending: false,
|
||||||
|
isError: false,
|
||||||
|
isSuccess: false,
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestDelete = useCallback(async () => {
|
||||||
|
dispatch({ type: API_ACTIONS.INIT });
|
||||||
|
try {
|
||||||
|
await axios.delete(url, {
|
||||||
|
timeout: TIMEOUT,
|
||||||
|
headers: HEADERS,
|
||||||
|
});
|
||||||
|
dispatch({ type: API_ACTIONS.SUCCESS });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: API_ACTIONS.FAILURE,
|
||||||
|
payload: getErrorMessage(error),
|
||||||
|
status: error.response.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
return [state, requestDelete];
|
||||||
|
}
|
65
src/api/get.js
Normal file
65
src/api/get.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useReducer, useCallback } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import { ForisURLs } from "forisUrls";
|
||||||
|
import { API_ACTIONS, TIMEOUT } from "./utils";
|
||||||
|
|
||||||
|
const APIGetReducer = (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case API_ACTIONS.INIT:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLoading: true,
|
||||||
|
isError: false,
|
||||||
|
};
|
||||||
|
case API_ACTIONS.SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLoading: false,
|
||||||
|
isError: false,
|
||||||
|
data: action.payload,
|
||||||
|
};
|
||||||
|
case API_ACTIONS.FAILURE:
|
||||||
|
if (action.status === 403) window.location.assign(ForisURLs.login);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLoading: false,
|
||||||
|
isError: true,
|
||||||
|
data: action.payload,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useAPIGet(url) {
|
||||||
|
const [state, dispatch] = useReducer(APIGetReducer, {
|
||||||
|
isLoading: false,
|
||||||
|
isError: false,
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
const get = useCallback(async () => {
|
||||||
|
dispatch({ type: API_ACTIONS.INIT });
|
||||||
|
try {
|
||||||
|
const result = await axios.get(url, {
|
||||||
|
timeout: TIMEOUT,
|
||||||
|
});
|
||||||
|
dispatch({ type: API_ACTIONS.SUCCESS, payload: result.data });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: API_ACTIONS.FAILURE,
|
||||||
|
payload: error.response.data,
|
||||||
|
status: error.response.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
return [state, get];
|
||||||
|
}
|
153
src/api/hooks.js
153
src/api/hooks.js
@ -1,153 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
import { useCallback, useReducer } from "react";
|
|
||||||
|
|
||||||
import { ForisURLs } from "forisUrls";
|
|
||||||
|
|
||||||
|
|
||||||
const POST_HEADERS = {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-CSRFToken": getCookie("_csrf_token"),
|
|
||||||
};
|
|
||||||
|
|
||||||
function getCookie(name) {
|
|
||||||
let cookieValue = null;
|
|
||||||
if (document.cookie && document.cookie !== "") {
|
|
||||||
const cookies = document.cookie.split(";");
|
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
|
||||||
const cookie = cookies[i].trim();
|
|
||||||
// Does this cookie string begin with the name we want?
|
|
||||||
if (cookie.substring(0, name.length + 1) === (`${name}=`)) {
|
|
||||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cookieValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TIMEOUT = 5000;
|
|
||||||
|
|
||||||
const API_ACTIONS = {
|
|
||||||
INIT: 1,
|
|
||||||
SUCCESS: 2,
|
|
||||||
FAILURE: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
const APIGetReducer = (state, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case API_ACTIONS.INIT:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isLoading: true,
|
|
||||||
isError: false,
|
|
||||||
};
|
|
||||||
case API_ACTIONS.SUCCESS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isLoading: false,
|
|
||||||
isError: false,
|
|
||||||
data: action.payload,
|
|
||||||
};
|
|
||||||
case API_ACTIONS.FAILURE:
|
|
||||||
if (action.status === 403) window.location.assign(ForisURLs.login);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isLoading: false,
|
|
||||||
isError: true,
|
|
||||||
data: action.payload,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useAPIGet(url) {
|
|
||||||
const [state, dispatch] = useReducer(APIGetReducer, {
|
|
||||||
isLoading: false,
|
|
||||||
isError: false,
|
|
||||||
data: null,
|
|
||||||
});
|
|
||||||
const get = useCallback(async () => {
|
|
||||||
dispatch({ type: API_ACTIONS.INIT });
|
|
||||||
try {
|
|
||||||
const result = await axios.get(url, {
|
|
||||||
timeout: TIMEOUT,
|
|
||||||
});
|
|
||||||
dispatch({ type: API_ACTIONS.SUCCESS, payload: result.data });
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: API_ACTIONS.FAILURE,
|
|
||||||
payload: error.response.data,
|
|
||||||
status: error.response.status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [url]);
|
|
||||||
|
|
||||||
return [state, get];
|
|
||||||
}
|
|
||||||
|
|
||||||
const APIPostReducer = (state, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case API_ACTIONS.INIT:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isSending: true,
|
|
||||||
isError: false,
|
|
||||||
isSuccess: false,
|
|
||||||
};
|
|
||||||
case API_ACTIONS.SUCCESS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isSending: false,
|
|
||||||
isError: false,
|
|
||||||
isSuccess: true,
|
|
||||||
data: action.payload,
|
|
||||||
};
|
|
||||||
case API_ACTIONS.FAILURE:
|
|
||||||
if (action.status === 403) window.location.assign(ForisURLs.login);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isSending: false,
|
|
||||||
isError: true,
|
|
||||||
isSuccess: false,
|
|
||||||
data: action.payload,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useAPIPost(url) {
|
|
||||||
const [state, dispatch] = useReducer(APIPostReducer, {
|
|
||||||
isSending: false,
|
|
||||||
isError: false,
|
|
||||||
isSuccess: false,
|
|
||||||
data: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const post = async (data) => {
|
|
||||||
dispatch({ type: API_ACTIONS.INIT });
|
|
||||||
try {
|
|
||||||
const result = await axios.post(url, data, {
|
|
||||||
timeout: TIMEOUT,
|
|
||||||
headers: POST_HEADERS,
|
|
||||||
});
|
|
||||||
dispatch({ type: API_ACTIONS.SUCCESS, payload: result.data });
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: API_ACTIONS.FAILURE,
|
|
||||||
payload: error.response.data,
|
|
||||||
status: error.response.status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return [state, post];
|
|
||||||
}
|
|
40
src/api/patch.js
Normal file
40
src/api/patch.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useReducer } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import {
|
||||||
|
API_ACTIONS, TIMEOUT, HEADERS, APIReducer, getErrorMessage,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
export function useAPIPatch(url) {
|
||||||
|
const [state, dispatch] = useReducer(APIReducer, {
|
||||||
|
isSending: false,
|
||||||
|
isError: false,
|
||||||
|
isSuccess: false,
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const patch = async (data) => {
|
||||||
|
dispatch({ type: API_ACTIONS.INIT });
|
||||||
|
try {
|
||||||
|
const result = await axios.patch(url, data, {
|
||||||
|
timeout: TIMEOUT,
|
||||||
|
headers: HEADERS,
|
||||||
|
});
|
||||||
|
dispatch({ type: API_ACTIONS.SUCCESS, payload: result.data });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: API_ACTIONS.FAILURE,
|
||||||
|
payload: getErrorMessage(error),
|
||||||
|
status: error.response.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return [state, patch];
|
||||||
|
}
|
45
src/api/post.js
Normal file
45
src/api/post.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 { useReducer } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import {
|
||||||
|
API_ACTIONS, TIMEOUT, HEADERS, APIReducer, getErrorMessage,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
export function useAPIPost(url, contentType) {
|
||||||
|
const [state, dispatch] = useReducer(APIReducer, {
|
||||||
|
isSending: false,
|
||||||
|
isError: false,
|
||||||
|
isSuccess: false,
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = { ...HEADERS };
|
||||||
|
if (contentType) {
|
||||||
|
headers["Content-Type"] = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const post = async (data) => {
|
||||||
|
dispatch({ type: API_ACTIONS.INIT });
|
||||||
|
try {
|
||||||
|
const result = await axios.post(url, data, {
|
||||||
|
timeout: TIMEOUT,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
dispatch({ type: API_ACTIONS.SUCCESS, payload: result.data });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: API_ACTIONS.FAILURE,
|
||||||
|
payload: getErrorMessage(error),
|
||||||
|
status: error.response.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return [state, post];
|
||||||
|
}
|
77
src/api/utils.js
Normal file
77
src/api/utils.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* 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 { ForisURLs } from "forisUrls";
|
||||||
|
|
||||||
|
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 HEADERS = {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-CSRFToken": getCookie("_csrf_token"),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TIMEOUT = 5000;
|
||||||
|
|
||||||
|
export const API_ACTIONS = {
|
||||||
|
INIT: 1,
|
||||||
|
SUCCESS: 2,
|
||||||
|
FAILURE: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function APIReducer(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 getErrorMessage(error) {
|
||||||
|
let payload = "An unknown error occurred";
|
||||||
|
if (error.response.headers["content-type"] === "application/json") {
|
||||||
|
payload = error.response.data;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useUID } from "react-uid/dist/es5/index";
|
import { useUID } from "react-uid";
|
||||||
|
|
||||||
import { formFieldsSize } from "./constants";
|
import { formFieldsSize } from "./constants";
|
||||||
|
|
||||||
|
@ -11,6 +11,6 @@ const [value, setValue] = useState(false);
|
|||||||
value={value}
|
value={value}
|
||||||
label="Some label"
|
label="Some label"
|
||||||
helpText="Read the small text!"
|
helpText="Read the small text!"
|
||||||
onChange={value => setValue(value)}
|
onChange={event =>setValue(event.target.value)}
|
||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
@ -9,6 +9,7 @@ import React from "react";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import Datetime from "react-datetime/DateTime";
|
import Datetime from "react-datetime/DateTime";
|
||||||
import moment from "moment/moment";
|
import moment from "moment/moment";
|
||||||
|
import "react-datetime/css/react-datetime.css";
|
||||||
|
|
||||||
import { Input } from "./Input";
|
import { Input } from "./Input";
|
||||||
|
|
||||||
|
21
src/bootstrap/DownloadButton.js
Normal file
21
src/bootstrap/DownloadButton.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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
DownloadButton.propTypes = {
|
||||||
|
href: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
PropTypes.node,
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DownloadButton({ href, children }) {
|
||||||
|
return <a href={href} className="btn btn-primary" download>{children}</a>;
|
||||||
|
}
|
7
src/bootstrap/DownloadButton.md
Normal file
7
src/bootstrap/DownloadButton.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Hyperlink with apperance of a button.
|
||||||
|
|
||||||
|
It has `download` attribute, which prevents closing WebSocket connection on Firefox. See [related issue](https://bugzilla.mozilla.org/show_bug.cgi?id=858538) for more details.
|
||||||
|
|
||||||
|
```js
|
||||||
|
<DownloadButton href="example.zip">Download</DownloadButton>
|
||||||
|
```
|
@ -11,7 +11,7 @@ const [email, setEmail] = useState('Wrong email');
|
|||||||
value={email}
|
value={email}
|
||||||
label="Some label"
|
label="Some label"
|
||||||
helpText="Read the small text!"
|
helpText="Read the small text!"
|
||||||
onChange={target => setEmail(target.value)}
|
onChange={event =>setEmail(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<button type="submit">Try to submit</button>
|
<button type="submit">Try to submit</button>
|
||||||
</form>
|
</form>
|
||||||
|
34
src/bootstrap/FileInput.js
Normal file
34
src/bootstrap/FileInput.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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";
|
||||||
|
|
||||||
|
FileInput.propTypes = {
|
||||||
|
/** Field label. */
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
/** Error message. */
|
||||||
|
error: PropTypes.string,
|
||||||
|
/** Help text message. */
|
||||||
|
helpText: PropTypes.string,
|
||||||
|
/** Email value. */
|
||||||
|
value: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FileInput({ ...props }) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
className="custom-file-input"
|
||||||
|
labelClassName="custom-file-label"
|
||||||
|
groupClassName="custom-file"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
15
src/bootstrap/FileInput.md
Normal file
15
src/bootstrap/FileInput.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
Bootstrap component for file input. Includes label and has predefined sizes and structure for using in foris forms.
|
||||||
|
|
||||||
|
All additional `props` are passed to the `<input type="file">` HTML component.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {useState} from 'react';
|
||||||
|
const [files, setFiles] = useState([]);
|
||||||
|
|
||||||
|
<FileInput
|
||||||
|
files={files}
|
||||||
|
label="Some file"
|
||||||
|
helpText="Will be uploaded"
|
||||||
|
onChange={event =>setFiles(event.target.files)}
|
||||||
|
/>
|
||||||
|
```
|
@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useUID } from "react-uid/dist/es5/index";
|
import { useUID } from "react-uid";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import { formFieldsSize } from "./constants";
|
import { formFieldsSize } from "./constants";
|
||||||
@ -21,19 +21,20 @@ Input.propTypes = {
|
|||||||
PropTypes.arrayOf(PropTypes.node),
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
PropTypes.node,
|
PropTypes.node,
|
||||||
]),
|
]),
|
||||||
|
labelClassName: PropTypes.string,
|
||||||
|
groupClassName: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Base bootstrap input component. */
|
/** Base bootstrap input component. */
|
||||||
export function Input({
|
export function Input({
|
||||||
type, label, helpText, error, className, children, ...props
|
type, label, helpText, error, className, children, labelClassName, groupClassName, ...props
|
||||||
}) {
|
}) {
|
||||||
const uid = useUID();
|
const uid = useUID();
|
||||||
const inputClassName = `form-control ${className || ""} ${(error ? "is-invalid" : "")}`.trim();
|
const inputClassName = `form-control ${className || ""} ${(error ? "is-invalid" : "")}`.trim();
|
||||||
return (
|
return (
|
||||||
<div className={formFieldsSize}>
|
<div className={`form-group ${formFieldsSize}`}>
|
||||||
<div className="form-group">
|
<label className={labelClassName} htmlFor={uid}>{label}</label>
|
||||||
<label htmlFor={uid}>{label}</label>
|
<div className={`input-group ${groupClassName || ""}`.trim()}>
|
||||||
<div className="input-group">
|
|
||||||
<input
|
<input
|
||||||
className={inputClassName}
|
className={inputClassName}
|
||||||
type={type}
|
type={type}
|
||||||
@ -46,6 +47,5 @@ export function Input({
|
|||||||
{error ? <div className="invalid-feedback">{error}</div> : null}
|
{error ? <div className="invalid-feedback">{error}</div> : null}
|
||||||
{helpText ? <small className="form-text text-muted">{helpText}</small> : null}
|
{helpText ? <small className="form-text text-muted">{helpText}</small> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
Bootstrap modal component.
|
Bootstrap modal component.
|
||||||
|
|
||||||
I have no idea why example doesn't work here but you can investigate HTML code...
|
it's required to have an element `<div id={"modal-container"}/>` somewhere on the page since modals are rendered in portals.
|
||||||
|
|
||||||
|
```js
|
||||||
|
<div id="modal-container"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
I have no idea why example doesn't work here but you can investigate HTML code and Foris project.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {ModalHeader, ModalBody, ModalFooter} from './Modal';
|
import {ModalHeader, ModalBody, ModalFooter} from './Modal';
|
||||||
|
|
||||||
@ -8,7 +15,7 @@ import {useState} from 'react';
|
|||||||
const [shown, setShown] = useState(false);
|
const [shown, setShown] = useState(false);
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<Modal shown={shown}>
|
<Modal setShown={setShown} shown={shown}>
|
||||||
<ModalHeader setShown={setShown} title='Warning!'/>
|
<ModalHeader setShown={setShown} title='Warning!'/>
|
||||||
<ModalBody><p>Bla bla bla...</p></ModalBody>
|
<ModalBody><p>Bla bla bla...</p></ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
@ -19,9 +26,8 @@ const [shown, setShown] = useState(false);
|
|||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<button
|
<button className='btn btn-secondary' onClick={()=>setShown(true)}>
|
||||||
className='btn btn-secondary'
|
Show modal
|
||||||
onClick={()=>setShown(true)}
|
</button>
|
||||||
>Show modal</button>
|
|
||||||
</>
|
</>
|
||||||
```
|
```
|
||||||
|
10
src/bootstrap/NumberInput.css
Normal file
10
src/bootstrap/NumberInput.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
input[type="number"] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=number]::-webkit-inner-spin-button,
|
||||||
|
input[type=number]::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
@ -6,11 +6,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Input } from "./Input";
|
|
||||||
|
|
||||||
export const NumberInput = ({ ...props }) => <Input type="number" {...props} />;
|
import { useConditionalTimeout } from "utils/hooks";
|
||||||
|
import { Input } from "./Input";
|
||||||
|
import "./NumberInput.css";
|
||||||
|
|
||||||
NumberInput.propTypes = {
|
NumberInput.propTypes = {
|
||||||
/** Field label. */
|
/** Field label. */
|
||||||
@ -24,4 +24,49 @@ NumberInput.propTypes = {
|
|||||||
PropTypes.string,
|
PropTypes.string,
|
||||||
PropTypes.number,
|
PropTypes.number,
|
||||||
]),
|
]),
|
||||||
|
/** Function called when value changes. */
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
/** Additional description dispaled to the right of input value. */
|
||||||
|
inlineText: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
NumberInput.defaultProps = {
|
||||||
|
value: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NumberInput({
|
||||||
|
onChange, inlineText, value, ...props
|
||||||
|
}) {
|
||||||
|
function updateValue(initialValue, difference) {
|
||||||
|
onChange({ target: { value: initialValue + difference } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const enableIncrease = useConditionalTimeout({ callback: updateValue }, value, 1);
|
||||||
|
const enableDecrease = useConditionalTimeout({ callback: updateValue }, value, -1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input type="number" onChange={onChange} value={value} {...props}>
|
||||||
|
<div className="input-group-append">
|
||||||
|
{inlineText && <p className="input-group-text">{inlineText}</p>}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-secondary"
|
||||||
|
onMouseDown={() => enableIncrease(true)}
|
||||||
|
onMouseUp={() => enableIncrease(false)}
|
||||||
|
aria-label="Increase"
|
||||||
|
>
|
||||||
|
<i className="fas fa-plus" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-secondary"
|
||||||
|
onMouseDown={() => enableDecrease(true)}
|
||||||
|
onMouseUp={() => enableDecrease(false)}
|
||||||
|
aria-label="Decrease"
|
||||||
|
>
|
||||||
|
<i className="fas fa-minus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Input>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -12,6 +12,6 @@ const [value, setValue] = useState(42);
|
|||||||
helpText="Read the small text!"
|
helpText="Read the small text!"
|
||||||
min='33'
|
min='33'
|
||||||
max='54'
|
max='54'
|
||||||
onChange={target => setValue(target.value)}
|
onChange={event =>setValue(event.target.value)}
|
||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
@ -12,6 +12,6 @@ const [value, setValue] = useState('secret');
|
|||||||
value={value}
|
value={value}
|
||||||
label="Some password"
|
label="Some password"
|
||||||
helpText="Read the small text!"
|
helpText="Read the small text!"
|
||||||
onChange={target => setValue(target.value)}
|
onChange={event =>setValue(event.target.value)}
|
||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useUID } from "react-uid/dist/es5/index";
|
import { useUID } from "react-uid";
|
||||||
|
|
||||||
import { formFieldsSize } from "./constants";
|
import { formFieldsSize } from "./constants";
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useUID } from "react-uid/dist/es5/index";
|
import { useUID } from "react-uid";
|
||||||
|
|
||||||
|
|
||||||
Select.propTypes = {
|
Select.propTypes = {
|
||||||
|
19
src/bootstrap/__tests__/DownloadButton.test.js
Normal file
19
src/bootstrap/__tests__/DownloadButton.test.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* 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 { DownloadButton } from "../DownloadButton";
|
||||||
|
|
||||||
|
describe("<DownloadButton />", () => {
|
||||||
|
it("should have download attribute", () => {
|
||||||
|
const { container } = render(<DownloadButton href="http://example.com">Test Button</DownloadButton>);
|
||||||
|
expect(container.firstChild.getAttribute("download")).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
@ -7,23 +7,39 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { render } from "customTestRender";
|
import { render, fireEvent, getByLabelText, wait } from "customTestRender";
|
||||||
|
|
||||||
import { NumberInput } from "../NumberInput";
|
import { NumberInput } from "../NumberInput";
|
||||||
|
|
||||||
|
|
||||||
describe("<NumberInput/>", () => {
|
describe("<NumberInput/>", () => {
|
||||||
it("Render number input", () => {
|
const onChangeMock = jest.fn();
|
||||||
|
let componentContainer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label="Test label"
|
label="Test label"
|
||||||
helpText="Some help text"
|
helpText="Some help text"
|
||||||
value={1123}
|
value={1}
|
||||||
onChange={() => {
|
onChange={onChangeMock}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(container.firstChild)
|
componentContainer = container;
|
||||||
.toMatchSnapshot();
|
});
|
||||||
|
|
||||||
|
it("Render number input", () => {
|
||||||
|
expect(componentContainer.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Increase number with button", async () => {
|
||||||
|
const increaseButton = getByLabelText(componentContainer, "Increase");
|
||||||
|
fireEvent.mouseDown(increaseButton);
|
||||||
|
await wait(() => expect(onChangeMock).toHaveBeenCalledWith({"target": {"value": 2}}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Decrease number with button", async () => {
|
||||||
|
const decreaseButton = getByLabelText(componentContainer, "Decrease");
|
||||||
|
fireEvent.mouseDown(decreaseButton);
|
||||||
|
await wait(() => expect(onChangeMock).toHaveBeenCalledWith({"target": {"value": 0}}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,10 +2,7 @@
|
|||||||
|
|
||||||
exports[`<NumberInput/> Render number input 1`] = `
|
exports[`<NumberInput/> Render number input 1`] = `
|
||||||
<div
|
<div
|
||||||
class="col-sm-12 offset-lg-1 col-lg-10"
|
class="form-group col-sm-12 offset-lg-1 col-lg-10"
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="form-group"
|
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
for="1"
|
for="1"
|
||||||
@ -19,8 +16,30 @@ exports[`<NumberInput/> Render number input 1`] = `
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
id="1"
|
id="1"
|
||||||
type="number"
|
type="number"
|
||||||
value="1123"
|
value="1"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
class="input-group-append"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="Increase"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="fas fa-plus"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="Decrease"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="fas fa-minus"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<small
|
<small
|
||||||
class="form-text text-muted"
|
class="form-text text-muted"
|
||||||
@ -28,5 +47,4 @@ exports[`<NumberInput/> Render number input 1`] = `
|
|||||||
Some help text
|
Some help text
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
@ -2,10 +2,7 @@
|
|||||||
|
|
||||||
exports[`<PasswordInput/> Render password input 1`] = `
|
exports[`<PasswordInput/> Render password input 1`] = `
|
||||||
<div
|
<div
|
||||||
class="col-sm-12 offset-lg-1 col-lg-10"
|
class="form-group col-sm-12 offset-lg-1 col-lg-10"
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="form-group"
|
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
for="1"
|
for="1"
|
||||||
@ -29,5 +26,4 @@ exports[`<PasswordInput/> Render password input 1`] = `
|
|||||||
Some help text
|
Some help text
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
@ -2,10 +2,7 @@
|
|||||||
|
|
||||||
exports[`<TextInput/> Render text input 1`] = `
|
exports[`<TextInput/> Render text input 1`] = `
|
||||||
<div
|
<div
|
||||||
class="col-sm-12 offset-lg-1 col-lg-10"
|
class="form-group col-sm-12 offset-lg-1 col-lg-10"
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="form-group"
|
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
for="1"
|
for="1"
|
||||||
@ -28,5 +25,4 @@ exports[`<TextInput/> Render text input 1`] = `
|
|||||||
Some help text
|
Some help text
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
export const REFORIS_URL_PREFIX = process.env.LIGHTTPD ? "/reforis" : "";
|
export const REFORIS_URL_PREFIX = process.env.LIGHTTPD ? "/reforis" : "";
|
||||||
|
|
||||||
export const ForisURLs = {
|
export const ForisURLs = {
|
||||||
|
@ -9,7 +9,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import { Spinner } from "bootstrap/Spinner";
|
import { Spinner } from "bootstrap/Spinner";
|
||||||
import { useAPIPost } from "api/hooks";
|
import { useAPIPost } from "api/post";
|
||||||
|
|
||||||
import { Prompt } from "react-router";
|
import { Prompt } from "react-router";
|
||||||
import { useForisModule, useForm } from "../hooks";
|
import { useForisModule, useForm } from "../hooks";
|
||||||
|
73
src/form/components/ForisForm.md
Normal file
73
src/form/components/ForisForm.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
`<ForisForm/>` is Higher-Order Component which encapsulates entire form logic and provides with children required props.
|
||||||
|
This component structure provides comfort API and allows to create typical Foris module forms easily.
|
||||||
|
|
||||||
|
## Example of usage of `<ForisForm/>`
|
||||||
|
You can pass more forms as children.
|
||||||
|
```js
|
||||||
|
<ForisForm
|
||||||
|
ws={ws}
|
||||||
|
forisConfig={{
|
||||||
|
endpoint: API_URLs.wan,
|
||||||
|
wsModule: "wan",
|
||||||
|
}}
|
||||||
|
prepData={prepData}
|
||||||
|
prepDataToSubmit={prepDataToSubmit}
|
||||||
|
validator={validator}
|
||||||
|
>
|
||||||
|
<WANForm />
|
||||||
|
<WAN6Form />
|
||||||
|
<MACForm />
|
||||||
|
</ForisForm>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example of children forms `props` usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default function MACForm({
|
||||||
|
formData, formErrors, setFormValue, ...props
|
||||||
|
}) {
|
||||||
|
const macSettings = formData.mac_settings;
|
||||||
|
const errors = (formErrors || {}).mac_settings || {};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>{_("MAC")}</h3>
|
||||||
|
<CheckBox
|
||||||
|
label={_("Custom MAC address")}
|
||||||
|
checked={macSettings.custom_mac_enabled}
|
||||||
|
helpText={HELP_TEXTS.custom_mac_enabled}
|
||||||
|
|
||||||
|
onChange={setFormValue(
|
||||||
|
(value) => ({ mac_settings: { custom_mac_enabled: { $set: value } } }),
|
||||||
|
)}
|
||||||
|
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{macSettings.custom_mac_enabled
|
||||||
|
? (
|
||||||
|
<TextInput
|
||||||
|
label={_("MAC address")}
|
||||||
|
value={macSettings.custom_mac || ""}
|
||||||
|
helpText={HELP_TEXTS.custom_mac}
|
||||||
|
error={errors.custom_mac}
|
||||||
|
required
|
||||||
|
|
||||||
|
onChange={setFormValue(
|
||||||
|
(value) => ({ mac_settings: { custom_mac: { $set: value } } }),
|
||||||
|
)}
|
||||||
|
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The <ForisForm/> passes subsequent `props` to the child components.
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
|----------------|--------|----------------------------------------------------------------------------|
|
||||||
|
| `formData` | object | Data returned from API. |
|
||||||
|
| `formErrors` | object | Errors returned after validation via validator. |
|
||||||
|
| `setFormValue` | func | Function for data update. It takes update rule as arg (see example above). |
|
||||||
|
| `disabled` | bool | Flag to disable form elements (during updates or loadings e.t.c.). |
|
@ -8,7 +8,7 @@
|
|||||||
import { useCallback, useEffect, useReducer } from "react";
|
import { useCallback, useEffect, useReducer } from "react";
|
||||||
import update from "immutability-helper";
|
import update from "immutability-helper";
|
||||||
|
|
||||||
import { useAPIGet } from "api/hooks";
|
import { useAPIGet } from "api/get";
|
||||||
import { useWSForisModule } from "webSockets/hooks";
|
import { useWSForisModule } from "webSockets/hooks";
|
||||||
|
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ const FORM_ACTIONS = {
|
|||||||
resetData: 2,
|
resetData: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useForm(validator, prepData) {
|
export function useForm(validator, dataPreprocessor) {
|
||||||
const [state, dispatch] = useReducer(formReducer, {
|
const [state, dispatch] = useReducer(formReducer, {
|
||||||
data: null,
|
data: null,
|
||||||
initialData: null,
|
initialData: null,
|
||||||
@ -28,10 +28,10 @@ export function useForm(validator, prepData) {
|
|||||||
dispatch({
|
dispatch({
|
||||||
type: FORM_ACTIONS.resetData,
|
type: FORM_ACTIONS.resetData,
|
||||||
data,
|
data,
|
||||||
prepData,
|
dataPreprocessor,
|
||||||
validator,
|
validator,
|
||||||
});
|
});
|
||||||
}, [prepData, validator]);
|
}, [dataPreprocessor, validator]);
|
||||||
|
|
||||||
const onFormChangeHandler = useCallback((updateRule) => (event) => {
|
const onFormChangeHandler = useCallback((updateRule) => (event) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
@ -41,6 +41,7 @@ export function useForm(validator, prepData) {
|
|||||||
validator,
|
validator,
|
||||||
});
|
});
|
||||||
}, [validator]);
|
}, [validator]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
state,
|
state,
|
||||||
onFormChangeHandler,
|
onFormChangeHandler,
|
||||||
@ -61,12 +62,15 @@ function formReducer(state, action) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
case FORM_ACTIONS.resetData: {
|
case FORM_ACTIONS.resetData: {
|
||||||
if (!action.data) return { ...state, initialData: state.data };
|
if (!action.data) {
|
||||||
const prepData = action.prepData ? action.prepData(action.data) : action.data;
|
return { ...state, initialData: state.data };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = action.dataPreprocessor ? action.dataPreprocessor(action.data) : action.data;
|
||||||
return {
|
return {
|
||||||
data: prepData,
|
data,
|
||||||
initialData: prepData,
|
initialData: data,
|
||||||
errors: action.data ? action.validator(prepData) : undefined,
|
errors: action.data ? action.validator(data) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@ -82,6 +86,9 @@ function getChangedValue(target) {
|
|||||||
} else if (target.type === "number") {
|
} else if (target.type === "number") {
|
||||||
const parsedValue = parseInt(value);
|
const parsedValue = parseInt(value);
|
||||||
value = Number.isNaN(parsedValue) ? value : parsedValue;
|
value = Number.isNaN(parsedValue) ? value : parsedValue;
|
||||||
|
} else if (target.type === "file") {
|
||||||
|
// Return first file (we don't need multiple yet)
|
||||||
|
[value] = target.files;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
20
src/index.js
20
src/index.js
@ -1,19 +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.
|
||||||
|
*/
|
||||||
|
|
||||||
// API
|
// API
|
||||||
export { useAPIGet, useAPIPost } from "./api/hooks";
|
export { useAPIGet } from "api/get";
|
||||||
|
export { useAPIPost } from "api/post";
|
||||||
|
export { useAPIDelete } from "api/delete";
|
||||||
|
export { useAPIPatch } from "api/patch";
|
||||||
|
|
||||||
// Bootstrap
|
// Bootstrap
|
||||||
export { Alert } from "bootstrap/Alert";
|
export { Alert } from "bootstrap/Alert";
|
||||||
export { Button } from "bootstrap/Button";
|
export { Button } from "bootstrap/Button";
|
||||||
export { CheckBox } from "bootstrap/CheckBox";
|
export { CheckBox } from "bootstrap/CheckBox";
|
||||||
export { formFieldsSize } from "bootstrap/constants";
|
export { DownloadButton } from "bootstrap/DownloadButton";
|
||||||
export { DataTimeInput } from "bootstrap/DataTimeInput";
|
export { DataTimeInput } from "bootstrap/DataTimeInput";
|
||||||
export { EmailInput } from "bootstrap/EmailInput";
|
export { EmailInput } from "bootstrap/EmailInput";
|
||||||
|
export { FileInput } from "bootstrap/FileInput";
|
||||||
export { Input } from "bootstrap/Input";
|
export { Input } from "bootstrap/Input";
|
||||||
export { NumberInput } from "bootstrap/NumberInput";
|
export { NumberInput } from "bootstrap/NumberInput";
|
||||||
export { PasswordInput } from "bootstrap/PasswordInput";
|
export { PasswordInput } from "bootstrap/PasswordInput";
|
||||||
export { RadioSet } from "bootstrap/RadioSet";
|
export { RadioSet } from "bootstrap/RadioSet";
|
||||||
export { Select } from "bootstrap/Select";
|
export { Select } from "bootstrap/Select";
|
||||||
export { TextInput } from "bootstrap/TextInput";
|
export { TextInput } from "bootstrap/TextInput";
|
||||||
|
export { formFieldsSize } from "bootstrap/constants";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Spinner,
|
Spinner,
|
||||||
@ -37,6 +49,7 @@ export { WebSockets } from "webSockets/WebSockets";
|
|||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
export { Portal } from "utils/Portal";
|
export { Portal } from "utils/Portal";
|
||||||
|
export { undefinedIfEmpty, withoutUndefinedKeys, onlySpecifiedKeys } from "utils/objectHelpers";
|
||||||
|
|
||||||
// Foris URL
|
// Foris URL
|
||||||
export { ForisURLs, REFORIS_URL_PREFIX } from "forisUrls";
|
export { ForisURLs, REFORIS_URL_PREFIX } from "forisUrls";
|
||||||
@ -51,3 +64,6 @@ export {
|
|||||||
validateMAC,
|
validateMAC,
|
||||||
validateMultipleEmails,
|
validateMultipleEmails,
|
||||||
} from "validations";
|
} from "validations";
|
||||||
|
|
||||||
|
// Alert context
|
||||||
|
export { AlertContext, AlertContextProvider } from "alertContext/AlertContext";
|
||||||
|
20
src/utils/hooks.js
Normal file
20
src/utils/hooks.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
/** Execute callback when condition is set to true. */
|
||||||
|
export function useConditionalTimeout({ callback, timeout = 125 }, ...callbackArgs) {
|
||||||
|
const [condition, setCondition] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (condition) {
|
||||||
|
const interval = setTimeout(() => callback(...callbackArgs), timeout);
|
||||||
|
return () => setTimeout(interval);
|
||||||
|
}
|
||||||
|
}, [condition, callback, timeout, callbackArgs]);
|
||||||
|
return setCondition;
|
||||||
|
}
|
35
src/utils/objectHelpers.js
Normal file
35
src/utils/objectHelpers.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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Return undefined if object has no keys, otherwise return object. */
|
||||||
|
export function undefinedIfEmpty(instance) {
|
||||||
|
if (Object.keys(instance).length > 0) {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return object without keys that have undefined value. */
|
||||||
|
export function withoutUndefinedKeys(instance) {
|
||||||
|
return Object.keys(instance).reduce(
|
||||||
|
(accumulator, key) => {
|
||||||
|
if (instance[key] !== undefined) {
|
||||||
|
accumulator[key] = instance[key];
|
||||||
|
}
|
||||||
|
return accumulator;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return copy of passed object that has only desired keys. */
|
||||||
|
export function onlySpecifiedKeys(object, desiredKeys) {
|
||||||
|
return desiredKeys.reduce(
|
||||||
|
(accumulator, key) => { accumulator[key] = object[key]; return accumulator; },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
60
styleguide.config.js
Normal file
60
styleguide.config.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* 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 = {
|
||||||
|
title: "Foris JS docs",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
name: "Foris JS",
|
||||||
|
content: "docs/intro.md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Foris forms",
|
||||||
|
components: [
|
||||||
|
"src/form/components/ForisForm.js",
|
||||||
|
"src/form/components/alerts.js",
|
||||||
|
"src/form/components/SubmitButton.js",
|
||||||
|
],
|
||||||
|
exampleMode: "expand",
|
||||||
|
usageMode: "expand",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bootstrap components",
|
||||||
|
description: "Set of bootstrap components.",
|
||||||
|
components: "src/bootstrap/*.js",
|
||||||
|
exampleMode: "expand",
|
||||||
|
usageMode: "expand",
|
||||||
|
ignore: [
|
||||||
|
"src/bootstrap/constants.js",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
require: [
|
||||||
|
"babel-polyfill",
|
||||||
|
path.join(__dirname, "node_modules/bootstrap/dist/css/bootstrap.min.css"),
|
||||||
|
path.join(__dirname, "node_modules/@fortawesome/fontawesome-free/css/all.min.css"),
|
||||||
|
],
|
||||||
|
webpackConfig: {
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
loader: "babel-loader",
|
||||||
|
}, {
|
||||||
|
test: /\.css$/,
|
||||||
|
use: ["style-loader", "css-loader"],
|
||||||
|
}, {
|
||||||
|
test: /\.(jpg|jpeg|png|woff|woff2|eot|ttf|svg)$/,
|
||||||
|
loader: "file-loader",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
Reference in New Issue
Block a user