mirror of
https://gitlab.nic.cz/turris/reforis/foris-js.git
synced 2024-12-27 00:31:35 +01:00
Merge branch 'dev' into 'master'
Release v5.1.0 See merge request turris/reforis/foris-js!122
This commit is contained in:
commit
f1feffb4bb
|
@ -1,6 +1,8 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: "eslint-config-reforis",
|
extends: ["eslint-config-reforis", "prettier"],
|
||||||
|
plugins: ["prettier"],
|
||||||
rules: {
|
rules: {
|
||||||
|
"prettier/prettier": ["error"],
|
||||||
"import/prefer-default-export": "off",
|
"import/prefer-default-export": "off",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -51,3 +51,4 @@ coverage.xml
|
||||||
dist/
|
dist/
|
||||||
foris-*.tgz
|
foris-*.tgz
|
||||||
styleguide/
|
styleguide/
|
||||||
|
testUtils
|
||||||
|
|
|
@ -1,44 +1,44 @@
|
||||||
image: node:8-alpine
|
image: node:10-alpine
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- build
|
- build
|
||||||
- publish
|
- publish
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- apk add make
|
- apk add make
|
||||||
- npm install
|
- npm install
|
||||||
|
|
||||||
test:
|
test:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- make test
|
- make test
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- make lint
|
- make lint
|
||||||
|
|
||||||
build:
|
build:
|
||||||
stage: build
|
stage: build
|
||||||
script:
|
script:
|
||||||
- make pack
|
- make pack
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- dist/foris-*.tgz
|
- dist/foris-*.tgz
|
||||||
|
|
||||||
publish_beta:
|
publish_beta:
|
||||||
stage: publish
|
stage: publish
|
||||||
only:
|
only:
|
||||||
refs:
|
refs:
|
||||||
- dev
|
- dev
|
||||||
script:
|
script:
|
||||||
- make publish-beta
|
- make publish-beta
|
||||||
|
|
||||||
publish_latest:
|
publish_latest:
|
||||||
stage: publish
|
stage: publish
|
||||||
only:
|
only:
|
||||||
refs:
|
refs:
|
||||||
- master
|
- master
|
||||||
script:
|
script:
|
||||||
- make publish-latest
|
- make publish-latest
|
||||||
|
|
11
.prettierrc
Normal file
11
.prettierrc
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"singleQuote": false,
|
||||||
|
"printWidth": 80,
|
||||||
|
"proseWrap": "always",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": false,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"jsxBracketSameLine": false,
|
||||||
|
"semi": true
|
||||||
|
}
|
16
README.md
16
README.md
|
@ -1,4 +1,5 @@
|
||||||
# foris-js
|
# foris-js
|
||||||
|
|
||||||
Set of utils and common React elements for reForis.
|
Set of utils and common React elements for reForis.
|
||||||
|
|
||||||
## Publishing package
|
## Publishing package
|
||||||
|
@ -13,17 +14,20 @@ tagged `beta`. Versions names are based on commit SHA, e.g.
|
||||||
|
|
||||||
1. Crete a merge request to `dev` branch with version bumped
|
1. Crete a merge request to `dev` branch with version bumped
|
||||||
2. When merging add `[skip ci]` to commit message to prevent publishing
|
2. When merging add `[skip ci]` to commit message to prevent publishing
|
||||||
unnecessary version
|
unnecessary version
|
||||||
3. Create a merge request from `dev` to `master` branch
|
3. Create a merge request from `dev` to `master` branch
|
||||||
4. New version should be published automatically
|
4. New version should be published automatically
|
||||||
|
|
||||||
## Manually managed dependencies
|
## Manually managed dependencies
|
||||||
Because of `<ForisForm />` component it's required to use exposed `ReactRouterDOM`
|
|
||||||
object from `react-router-dom` library. `ReactRouterDOM` is exposed by
|
Because of `<ForisForm />` component it's required to use exposed
|
||||||
|
`ReactRouterDOM` object from `react-router-dom` library. `ReactRouterDOM` is
|
||||||
|
exposed by
|
||||||
[reForis](https://gitlab.labs.nic.cz/turris/reforis/reforis/blob/master/js/webpack.config.js).
|
[reForis](https://gitlab.labs.nic.cz/turris/reforis/reforis/blob/master/js/webpack.config.js).
|
||||||
It can be done by following steps:
|
It can be done by following steps:
|
||||||
|
|
||||||
1. Setting `react-router-dom` as `peerDependencies` and `devDependencies` in `package.json`.
|
1. Setting `react-router-dom` as `peerDependencies` and `devDependencies` in
|
||||||
|
`package.json`.
|
||||||
2. Adding the following rules to `externals` in `webpack.conf.js` of the plugin:
|
2. Adding the following rules to `externals` in `webpack.conf.js` of the plugin:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
@ -34,11 +38,15 @@ externals: {
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docs
|
### Docs
|
||||||
|
|
||||||
Build or watch docs to get more info about library:
|
Build or watch docs to get more info about library:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make docs
|
make docs
|
||||||
```
|
```
|
||||||
|
|
||||||
or
|
or
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make docs-watch
|
make docs-watch
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,9 +1,4 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: ["@babel/preset-env", "@babel/preset-react"],
|
||||||
"@babel/preset-env",
|
plugins: ["@babel/plugin-transform-runtime"],
|
||||||
"@babel/preset-react",
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
"@babel/plugin-transform-runtime",
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
Sooner or later you will face with situation when you want/need to make some changes in the library.
|
Sooner or later you will face with situation when you want/need to make some
|
||||||
Then the most important tool for you it's [`npm link`](https://docs.npmjs.com/cli/link).
|
changes in the library. Then the most important tool for you it's
|
||||||
|
[`npm link`](https://docs.npmjs.com/cli/link).
|
||||||
|
|
||||||
Please, notice that it will not work if you link library just from root of the repo. It happens due to location of
|
Please, notice that it will not work if you link library just from root of the
|
||||||
sources `./src`. You need to pack library first `make pack` and then link it from `./dist` directory.
|
repo. It happens due to location of sources `./src`. You need to pack library
|
||||||
|
first `make pack` and then link it from `./dist` directory.
|
||||||
|
|
||||||
Yeah it's not such comfortable solution for development. But it can fixed by writing small script similar as `make pack`
|
Yeah it's not such comfortable solution for development. But it can fixed by
|
||||||
but with linking every file and directory from `./src` to the some directory and linking then from it. Notice that you
|
writing small script similar as `make pack` but with linking every file and
|
||||||
need to link `package.json` and `package-lock.json` as well.
|
directory from `./src` to the some directory and linking then from it. Notice
|
||||||
|
that you need to link `package.json` and `package-lock.json` as well.
|
||||||
|
|
||||||
So step by step:
|
So step by step:
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
Foris JS library is set of components and utils for Foris JS application and plugins.
|
Foris JS library is set of components and utils for Foris JS application and
|
||||||
|
plugins.
|
||||||
|
|
||||||
Please notice that all of these components or utils are used in reForis and plugins. If you like to study by example I would
|
Please notice that all of these components or utils are used in reForis and
|
||||||
recommend to full-text search these repos.
|
plugins. If you like to study by example I would recommend to full-text search
|
||||||
|
these repos.
|
||||||
|
|
|
@ -27,7 +27,5 @@ module.exports = {
|
||||||
globals: {
|
globals: {
|
||||||
TZ: "utc",
|
TZ: "utc",
|
||||||
},
|
},
|
||||||
transformIgnorePatterns: [
|
transformIgnorePatterns: ["node_modules/(?!(react-datetime)/)"],
|
||||||
"node_modules/(?!(react-datetime)/)",
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
37004
package-lock.json
generated
37004
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
133
package.json
133
package.json
|
@ -1,67 +1,70 @@
|
||||||
{
|
{
|
||||||
"name": "foris",
|
"name": "foris",
|
||||||
"version": "5.0.1",
|
"version": "5.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": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitlab.labs.nic.cz/turris/reforis/foris-js.git"
|
"url": "https://gitlab.nic.cz/turris/reforis/foris-js.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"foris",
|
"foris",
|
||||||
"reforis"
|
"reforis"
|
||||||
],
|
],
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"immutability-helper": "3.0.1",
|
"immutability-helper": "3.0.1",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"qrcode.react": "^0.9.3",
|
"qrcode.react": "^0.9.3",
|
||||||
"react-datetime": "^2.16.3",
|
"react-datetime": "^2.16.3",
|
||||||
"react-uid": "^2.2.0"
|
"react-uid": "^2.2.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"bootstrap": "4.4.1",
|
"bootstrap": "4.4.1",
|
||||||
"prop-types": "15.7.2",
|
"prop-types": "15.7.2",
|
||||||
"react": "16.9.0",
|
"react": "16.9.0",
|
||||||
"react-dom": "16.9.0",
|
"react-dom": "16.9.0",
|
||||||
"react-router-dom": "^5.1.2"
|
"react-router-dom": "^5.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.8.4",
|
"@babel/cli": "^7.8.4",
|
||||||
"@babel/core": "^7.9.0",
|
"@babel/core": "^7.9.0",
|
||||||
"@babel/plugin-transform-runtime": "^7.9.0",
|
"@babel/plugin-transform-runtime": "^7.9.0",
|
||||||
"@babel/preset-env": "^7.9.0",
|
"@babel/preset-env": "^7.9.0",
|
||||||
"@babel/preset-react": "^7.9.4",
|
"@babel/preset-react": "^7.9.4",
|
||||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||||
"@testing-library/react": "^8.0.9",
|
"@testing-library/react": "^8.0.9",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
"babel-polyfill": "^6.26.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
"bootstrap": "^4.5.0",
|
"bootstrap": "^4.5.0",
|
||||||
"css-loader": "^3.5.3",
|
"css-loader": "^3.5.3",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"eslint-config-reforis": "^1.0.0",
|
"eslint-config-prettier": "^6.11.0",
|
||||||
"file-loader": "^6.0.0",
|
"eslint-config-reforis": "^1.0.0",
|
||||||
"jest": "^25.2.0",
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
"jest-mock-axios": "^3.2.0",
|
"file-loader": "^6.0.0",
|
||||||
"moment-timezone": "^0.5.28",
|
"jest": "^25.2.0",
|
||||||
"prop-types": "15.7.2",
|
"jest-mock-axios": "^3.2.0",
|
||||||
"react": "16.9.0",
|
"moment-timezone": "^0.5.28",
|
||||||
"react-dom": "16.9.0",
|
"prettier": "2.0.5",
|
||||||
"react-router-dom": "^5.1.2",
|
"prop-types": "15.7.2",
|
||||||
"react-styleguidist": "^10.6.2",
|
"react": "16.9.0",
|
||||||
"snapshot-diff": "^0.7.0",
|
"react-dom": "16.9.0",
|
||||||
"style-loader": "^1.2.1",
|
"react-router-dom": "^5.1.2",
|
||||||
"webpack": "^4.43.0"
|
"react-styleguidist": "^10.6.2",
|
||||||
},
|
"snapshot-diff": "^0.7.0",
|
||||||
"scripts": {
|
"style-loader": "^1.2.1",
|
||||||
"lint": "eslint src",
|
"webpack": "^4.43.0"
|
||||||
"lint:fix": "eslint --fix src",
|
},
|
||||||
"test": "jest",
|
"scripts": {
|
||||||
"test:watch": "jest --watch",
|
"lint": "eslint src",
|
||||||
"test:coverage": "jest --coverage --colors",
|
"lint:fix": "eslint --fix src",
|
||||||
"docs": "npx styleguidist build ",
|
"test": "jest",
|
||||||
"docs:watch": "styleguidist server"
|
"test:watch": "jest --watch",
|
||||||
}
|
"test:coverage": "jest --coverage --colors",
|
||||||
|
"docs": "npx styleguidist build ",
|
||||||
|
"docs:watch": "styleguidist server"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,12 @@ function AlertContextProvider({ children }) {
|
||||||
const { AlertContext } = window;
|
const { AlertContext } = window;
|
||||||
const [alert, setAlert] = useState(null);
|
const [alert, setAlert] = useState(null);
|
||||||
|
|
||||||
const setAlertWrapper = useCallback((message, type = ALERT_TYPES.DANGER) => {
|
const setAlertWrapper = useCallback(
|
||||||
setAlert({ message, type });
|
(message, type = ALERT_TYPES.DANGER) => {
|
||||||
}, [setAlert]);
|
setAlert({ message, type });
|
||||||
|
},
|
||||||
|
[setAlert]
|
||||||
|
);
|
||||||
|
|
||||||
const dismissAlert = useCallback(() => setAlert(null), [setAlert]);
|
const dismissAlert = useCallback(() => setAlert(null), [setAlert]);
|
||||||
|
|
||||||
|
@ -38,7 +41,7 @@ function AlertContextProvider({ children }) {
|
||||||
</Portal>
|
</Portal>
|
||||||
)}
|
)}
|
||||||
<AlertContext.Provider value={[setAlertWrapper, dismissAlert]}>
|
<AlertContext.Provider value={[setAlertWrapper, dismissAlert]}>
|
||||||
{ children }
|
{children}
|
||||||
</AlertContext.Provider>
|
</AlertContext.Provider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
It provides alert context to children. `AlertContext` allows using `useAlert` in components.
|
It provides alert context to children. `AlertContext` allows using `useAlert` in
|
||||||
|
components.
|
||||||
|
|
||||||
Notice that `<div id="alert-container"/>` should be presented in HTML doc to get it work (In reForis it's already done
|
Notice that `<div id="alert-container"/>` should be presented in HTML doc to get
|
||||||
with base Jinja2 templates).
|
it work (In reForis it's already done with base Jinja2 templates).
|
||||||
|
|
|
@ -6,9 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import { render, getByText, queryByText, fireEvent } from "customTestRender";
|
||||||
render, getByText, queryByText, fireEvent,
|
|
||||||
} from "customTestRender";
|
|
||||||
|
|
||||||
import { useAlert, AlertContextProvider } from "../AlertContext";
|
import { useAlert, AlertContextProvider } from "../AlertContext";
|
||||||
|
|
||||||
|
@ -31,7 +29,7 @@ describe("AlertContext", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<AlertContextProvider>
|
<AlertContextProvider>
|
||||||
<AlertTest />
|
<AlertTest />
|
||||||
</AlertContextProvider>,
|
</AlertContextProvider>
|
||||||
);
|
);
|
||||||
componentContainer = container;
|
componentContainer = container;
|
||||||
});
|
});
|
||||||
|
|
145
src/api/hooks.js
145
src/api/hooks.js
|
@ -5,13 +5,16 @@
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { useCallback, useEffect, useReducer, useState } from "react";
|
||||||
useCallback, useEffect, useReducer, useState,
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
import { ForisURLs } from "../utils/forisUrls";
|
import { ForisURLs } from "../utils/forisUrls";
|
||||||
import {
|
import {
|
||||||
API_ACTIONS, API_METHODS, API_STATE, getErrorPayload, HEADERS, TIMEOUT,
|
API_ACTIONS,
|
||||||
|
API_METHODS,
|
||||||
|
API_STATE,
|
||||||
|
getErrorPayload,
|
||||||
|
HEADERS,
|
||||||
|
TIMEOUT,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
const DATA_METHODS = ["POST", "PATCH", "PUT"];
|
const DATA_METHODS = ["POST", "PATCH", "PUT"];
|
||||||
|
@ -23,76 +26,83 @@ function createAPIHook(method) {
|
||||||
data: null,
|
data: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sendRequest = useCallback(async ({ data, suffix } = {}) => {
|
const sendRequest = useCallback(
|
||||||
const headers = { ...HEADERS };
|
async ({ data, suffix } = {}) => {
|
||||||
if (contentType) {
|
const headers = { ...HEADERS };
|
||||||
headers["Content-Type"] = contentType;
|
if (contentType) {
|
||||||
}
|
headers["Content-Type"] = contentType;
|
||||||
|
|
||||||
dispatch({ type: API_ACTIONS.INIT });
|
|
||||||
try {
|
|
||||||
// Prepare request
|
|
||||||
const request = API_METHODS[method];
|
|
||||||
const config = {
|
|
||||||
timeout: TIMEOUT,
|
|
||||||
headers,
|
|
||||||
};
|
|
||||||
const url = suffix ? `${urlRoot}/${suffix}` : urlRoot;
|
|
||||||
|
|
||||||
// Make request
|
|
||||||
let result;
|
|
||||||
if (DATA_METHODS.includes(method)) {
|
|
||||||
result = await request(url, data, config);
|
|
||||||
} else {
|
|
||||||
result = await request(url, config);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process request result
|
dispatch({ type: API_ACTIONS.INIT });
|
||||||
dispatch({
|
try {
|
||||||
type: API_ACTIONS.SUCCESS,
|
// Prepare request
|
||||||
payload: result.data,
|
const request = API_METHODS[method];
|
||||||
});
|
const config = {
|
||||||
} catch (error) {
|
timeout: TIMEOUT,
|
||||||
const errorPayload = getErrorPayload(error);
|
headers,
|
||||||
dispatch({
|
};
|
||||||
type: API_ACTIONS.FAILURE,
|
const url = suffix ? `${urlRoot}/${suffix}` : urlRoot;
|
||||||
status: error.response && error.response.status,
|
|
||||||
payload: errorPayload,
|
// Make request
|
||||||
});
|
let result;
|
||||||
}
|
if (DATA_METHODS.includes(method)) {
|
||||||
}, [urlRoot, contentType]);
|
result = await request(url, data, config);
|
||||||
|
} else {
|
||||||
|
result = await request(url, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process request result
|
||||||
|
dispatch({
|
||||||
|
type: API_ACTIONS.SUCCESS,
|
||||||
|
payload: result.data,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorPayload = getErrorPayload(error);
|
||||||
|
dispatch({
|
||||||
|
type: API_ACTIONS.FAILURE,
|
||||||
|
status: error.response && error.response.status,
|
||||||
|
payload: errorPayload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[urlRoot, contentType]
|
||||||
|
);
|
||||||
return [state, sendRequest];
|
return [state, sendRequest];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function APIReducer(state, action) {
|
function APIReducer(state, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case API_ACTIONS.INIT:
|
case API_ACTIONS.INIT:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
state: API_STATE.SENDING,
|
state: API_STATE.SENDING,
|
||||||
};
|
};
|
||||||
case API_ACTIONS.SUCCESS:
|
case API_ACTIONS.SUCCESS:
|
||||||
return {
|
return {
|
||||||
state: API_STATE.SUCCESS,
|
state: API_STATE.SUCCESS,
|
||||||
data: action.payload,
|
data: action.payload,
|
||||||
};
|
};
|
||||||
case API_ACTIONS.FAILURE:
|
case API_ACTIONS.FAILURE:
|
||||||
if (action.status === 403) {
|
if (action.status === 403) {
|
||||||
window.location.assign(ForisURLs.login);
|
window.location.assign(ForisURLs.login);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not an API error - should be rethrown.
|
// Not an API error - should be rethrown.
|
||||||
if (action.payload && action.payload.stack && action.payload.message) {
|
if (
|
||||||
throw (action.payload);
|
action.payload &&
|
||||||
}
|
action.payload.stack &&
|
||||||
|
action.payload.message
|
||||||
|
) {
|
||||||
|
throw action.payload;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state: API_STATE.ERROR,
|
state: API_STATE.ERROR,
|
||||||
data: action.payload,
|
data: action.payload,
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,11 +112,10 @@ const useAPIPatch = createAPIHook("PATCH");
|
||||||
const useAPIPut = createAPIHook("PUT");
|
const useAPIPut = createAPIHook("PUT");
|
||||||
const useAPIDelete = createAPIHook("DELETE");
|
const useAPIDelete = createAPIHook("DELETE");
|
||||||
|
|
||||||
export {
|
export { useAPIGet, useAPIPost, useAPIPatch, useAPIPut, useAPIDelete };
|
||||||
useAPIGet, useAPIPost, useAPIPatch, useAPIPut, useAPIDelete,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useAPIPolling(endpoint, delay = 1000, until) { // delay ms
|
export function useAPIPolling(endpoint, delay = 1000, until) {
|
||||||
|
// delay ms
|
||||||
const [state, setState] = useState({ state: API_STATE.INIT });
|
const [state, setState] = useState({ state: API_STATE.INIT });
|
||||||
const [getResponse, get] = useAPIGet(endpoint);
|
const [getResponse, get] = useAPIGet(endpoint);
|
||||||
|
|
||||||
|
|
|
@ -43,8 +43,10 @@ function getCookie(name) {
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
const cookie = cookies[i].trim();
|
const cookie = cookies[i].trim();
|
||||||
// Does this cookie string begin with the name we want?
|
// Does this cookie string begin with the name we want?
|
||||||
if (cookie.substring(0, name.length + 1) === (`${name}=`)) {
|
if (cookie.substring(0, name.length + 1) === `${name}=`) {
|
||||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
cookieValue = decodeURIComponent(
|
||||||
|
cookie.substring(name.length + 1)
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,12 +35,16 @@ Alert.defaultProps = {
|
||||||
type: ALERT_TYPES.DANGER,
|
type: ALERT_TYPES.DANGER,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Alert({
|
export function Alert({ type, onDismiss, children }) {
|
||||||
type, onDismiss, children,
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div className={`alert alert-dismissible alert-${type}`}>
|
<div className={`alert alert-dismissible alert-${type}`}>
|
||||||
{onDismiss ? <button type="button" className="close" onClick={onDismiss}>×</button> : false}
|
{onDismiss ? (
|
||||||
|
<button type="button" className="close" onClick={onDismiss}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
false
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
Bootstrap alert component.
|
Bootstrap alert component.
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
|
|
||||||
function AlertExample(){
|
function AlertExample() {
|
||||||
const [alert, setAlert] = useState(true);
|
const [alert, setAlert] = useState(true);
|
||||||
if (alert)
|
if (alert)
|
||||||
return <Alert
|
return (
|
||||||
type='warning'
|
<Alert type="warning" onDismiss={() => setAlert(false)}>
|
||||||
onDismiss={()=>setAlert(false)}
|
Some warning out there!
|
||||||
>
|
</Alert>
|
||||||
Some warning out there!
|
);
|
||||||
</Alert>;
|
return (
|
||||||
return <button
|
<button className="btn btn-secondary" onClick={() => setAlert(true)}>
|
||||||
className='btn btn-secondary'
|
Show alert again
|
||||||
onClick={()=>setAlert(true)}
|
</button>
|
||||||
>Show alert again</button>;
|
);
|
||||||
};
|
}
|
||||||
<AlertExample/>
|
<AlertExample />;
|
||||||
```
|
```
|
||||||
|
|
|
@ -25,22 +25,29 @@ Button.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Button({
|
export function Button({
|
||||||
className, loading, forisFormSize, children, ...props
|
className,
|
||||||
|
loading,
|
||||||
|
forisFormSize,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
}) {
|
}) {
|
||||||
let buttonClass = className ? `btn ${className}` : "btn btn-primary ";
|
let buttonClass = className ? `btn ${className}` : "btn btn-primary ";
|
||||||
if (forisFormSize) {
|
if (forisFormSize) {
|
||||||
buttonClass = `${buttonClass} col-sm-12 col-lg-3`;
|
buttonClass = `${buttonClass} col-sm-12 col-md-3 col-lg-2`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const span = loading
|
const span = loading ? (
|
||||||
? <span className="spinner-border spinner-border-sm" role="status" aria-hidden="true" /> : null;
|
<span
|
||||||
|
className="spinner-border spinner-border-sm"
|
||||||
|
role="status"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button type="button" className={buttonClass} {...props}>
|
<button type="button" className={buttonClass} {...props}>
|
||||||
{span}
|
{span}
|
||||||
{" "}
|
|
||||||
{span ? " " : null}
|
{span ? " " : null}
|
||||||
{" "}
|
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,5 +11,7 @@ Can be used without parameters:
|
||||||
Using loading spinner:
|
Using loading spinner:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<Button loading disabled>Loading...</Button>
|
<Button loading disabled>
|
||||||
|
Loading...
|
||||||
|
</Button>
|
||||||
```
|
```
|
||||||
|
|
|
@ -22,9 +22,7 @@ CheckBox.defaultProps = {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CheckBox({
|
export function CheckBox({ label, helpText, disabled, ...props }) {
|
||||||
label, helpText, disabled, ...props
|
|
||||||
}) {
|
|
||||||
const uid = useUID();
|
const uid = useUID();
|
||||||
return (
|
return (
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
|
@ -34,12 +32,15 @@ export function CheckBox({
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={uid}
|
id={uid}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<label className="custom-control-label" htmlFor={uid}>
|
<label className="custom-control-label" htmlFor={uid}>
|
||||||
{label}
|
{label}
|
||||||
{helpText && <small className="form-text text-muted">{helpText}</small>}
|
{helpText && (
|
||||||
|
<small className="form-text text-muted">
|
||||||
|
{helpText}
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
Checkbox with label Bootstrap component with predefined sizes and structure for using in foris forms.
|
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.
|
|
||||||
|
|
||||||
|
All additional `props` are passed to the `<input type="checkbox">` HTML
|
||||||
|
component.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const [value, setValue] = useState(false);
|
const [value, setValue] = useState(false);
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
value={value}
|
value={value}
|
||||||
label="Some label"
|
label="Some label"
|
||||||
helpText="Read the small text!"
|
helpText="Read the small text!"
|
||||||
onChange={event =>setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
|
|
@ -38,14 +38,17 @@ const DEFAULT_DATE_FORMAT = "YYYY-MM-DD";
|
||||||
const DEFAULT_TIME_FORMAT = "HH:mm:ss";
|
const DEFAULT_TIME_FORMAT = "HH:mm:ss";
|
||||||
|
|
||||||
export function DataTimeInput({
|
export function DataTimeInput({
|
||||||
value, onChange, isValidDate, dateFormat, timeFormat, children, ...props
|
value,
|
||||||
|
onChange,
|
||||||
|
isValidDate,
|
||||||
|
dateFormat,
|
||||||
|
timeFormat,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
}) {
|
}) {
|
||||||
function renderInput(datetimeProps) {
|
function renderInput(datetimeProps) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input {...props} {...datetimeProps}>
|
||||||
{...props}
|
|
||||||
{...datetimeProps}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Input>
|
</Input>
|
||||||
);
|
);
|
||||||
|
@ -54,8 +57,12 @@ export function DataTimeInput({
|
||||||
return (
|
return (
|
||||||
<Datetime
|
<Datetime
|
||||||
locale={ForisTranslations.locale}
|
locale={ForisTranslations.locale}
|
||||||
dateFormat={dateFormat !== undefined ? dateFormat : DEFAULT_DATE_FORMAT}
|
dateFormat={
|
||||||
timeFormat={timeFormat !== undefined ? timeFormat : DEFAULT_TIME_FORMAT}
|
dateFormat !== undefined ? dateFormat : DEFAULT_DATE_FORMAT
|
||||||
|
}
|
||||||
|
timeFormat={
|
||||||
|
timeFormat !== undefined ? timeFormat : DEFAULT_TIME_FORMAT
|
||||||
|
}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
isValidDate={isValidDate}
|
isValidDate={isValidDate}
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
Adopted from `react-datetime/DateTime` datatime picker component.
|
Adopted from `react-datetime/DateTime` datatime picker component. It uses
|
||||||
It uses `momentjs` see example.
|
`momentjs` see example.
|
||||||
|
|
||||||
It requires `ForisTranslations.locale` to be defined in order to use right locale.
|
It requires `ForisTranslations.locale` to be defined in order to use right
|
||||||
|
locale.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
ForisTranslations={locale:'en'};
|
ForisTranslations = { locale: "en" };
|
||||||
|
|
||||||
import {useState, useEffect} from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import moment from 'moment/moment';
|
import moment from "moment/moment";
|
||||||
|
|
||||||
const [dataTime, setDataTime] = useState(moment());
|
const [dataTime, setDataTime] = useState(moment());
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState();
|
||||||
useEffect(()=>{
|
useEffect(() => {
|
||||||
dataTime.isValid() ? setError(null) : setError('Invalid value!');
|
dataTime.isValid() ? setError(null) : setError("Invalid value!");
|
||||||
},[dataTime]);
|
}, [dataTime]);
|
||||||
|
|
||||||
<DataTimeInput
|
<DataTimeInput
|
||||||
label='Time to sleep'
|
label="Time to sleep"
|
||||||
value={dataTime}
|
value={dataTime}
|
||||||
error={error}
|
error={error}
|
||||||
helpText='Example helptext...'
|
helpText="Example helptext..."
|
||||||
onChange={value => setDataTime(value)}
|
onChange={(value) => setDataTime(value)}
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
|
|
@ -23,11 +23,7 @@ DownloadButton.defaultProps = {
|
||||||
|
|
||||||
export function DownloadButton({ href, className, children }) {
|
export function DownloadButton({ href, className, children }) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a href={href} className={`btn ${className}`.trim()} download>
|
||||||
href={href}
|
|
||||||
className={`btn ${className}`.trim()}
|
|
||||||
download
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
Hyperlink with apperance of a button.
|
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.
|
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
|
```js
|
||||||
<DownloadButton href="example.zip">Download</DownloadButton>
|
<DownloadButton href="example.zip">Download</DownloadButton>
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
Bootstrap component of email input with label with predefined sizes and structure for using in foris forms.
|
Bootstrap component of email input with label with predefined sizes and
|
||||||
It use built-in browser email address checking. It's only meaningful using inside `<form>`.
|
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.
|
All additional `props` are passed to the `<input type="email">` HTML component.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const [email, setEmail] = useState('Wrong email');
|
const [email, setEmail] = useState("Wrong email");
|
||||||
<form onSubmit={e=>e.preventDefault()}>
|
<form onSubmit={(e) => e.preventDefault()}>
|
||||||
<EmailInput
|
<EmailInput
|
||||||
value={email}
|
value={email}
|
||||||
label="Some label"
|
label="Some label"
|
||||||
helpText="Read the small text!"
|
helpText="Read the small text!"
|
||||||
onChange={event =>setEmail(event.target.value)}
|
onChange={(event) => setEmail(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<button type="submit">Try to submit</button>
|
<button type="submit">Try to submit</button>
|
||||||
</form>
|
</form>;
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
Bootstrap component for file input. Includes label and has predefined sizes and structure for using in foris forms.
|
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.
|
All additional `props` are passed to the `<input type="file">` HTML component.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
|
|
||||||
const [files, setFiles] = useState([]);
|
const [files, setFiles] = useState([]);
|
||||||
|
|
||||||
|
@ -15,27 +16,33 @@ const label = files.length === 1 ? files[0].name : "Choose file";
|
||||||
files={files}
|
files={files}
|
||||||
label={label}
|
label={label}
|
||||||
helpText="Will be uploaded"
|
helpText="Will be uploaded"
|
||||||
onChange={event=>setFiles(event.target.files)}
|
onChange={(event) => setFiles(event.target.files)}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>;
|
||||||
```
|
```
|
||||||
|
|
||||||
### FileInput with multiple files
|
### FileInput with multiple files
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
|
|
||||||
const [files, setFiles] = useState([]);
|
const [files, setFiles] = useState([]);
|
||||||
|
|
||||||
// Note that files is not an array but FileList.
|
// Note that files is not an array but FileList.
|
||||||
const label = files.length > 0 ? Array.from(files).map(file=>file.name).join(", ") : "Choose files";
|
const label =
|
||||||
|
files.length > 0
|
||||||
|
? Array.from(files)
|
||||||
|
.map((file) => file.name)
|
||||||
|
.join(", ")
|
||||||
|
: "Choose files";
|
||||||
|
|
||||||
<form className="col">
|
<form className="col">
|
||||||
<FileInput
|
<FileInput
|
||||||
files={files}
|
files={files}
|
||||||
label={label}
|
label={label}
|
||||||
helpText="Will be uploaded"
|
helpText="Will be uploaded"
|
||||||
onChange={event=>setFiles(event.target.files)}
|
onChange={(event) => setFiles(event.target.files)}
|
||||||
multiple
|
multiple
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>;
|
||||||
```
|
```
|
||||||
|
|
|
@ -25,25 +25,38 @@ Input.propTypes = {
|
||||||
|
|
||||||
/** Base bootstrap input component. */
|
/** Base bootstrap input component. */
|
||||||
export function Input({
|
export function Input({
|
||||||
type, label, helpText, error, className, children, labelClassName, groupClassName, ...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="form-group">
|
<div className="form-group">
|
||||||
<label className={labelClassName} htmlFor={uid}>{label}</label>
|
<label className={labelClassName} htmlFor={uid}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
<div className={`input-group ${groupClassName || ""}`.trim()}>
|
<div className={`input-group ${groupClassName || ""}`.trim()}>
|
||||||
<input
|
<input
|
||||||
className={inputClassName}
|
className={inputClassName}
|
||||||
type={type}
|
type={type}
|
||||||
id={uid}
|
id={uid}
|
||||||
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,6 @@
|
||||||
.modal.show {
|
.modal.show {
|
||||||
display: block;
|
display: block;
|
||||||
animation-name: modalFade;
|
animation-name: modalFade;
|
||||||
animation-duration: .3s;
|
animation-duration: 0.3s;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,9 +26,7 @@ Modal.propTypes = {
|
||||||
]).isRequired,
|
]).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Modal({
|
export function Modal({ shown, setShown, scrollable, children }) {
|
||||||
shown, setShown, scrollable, children,
|
|
||||||
}) {
|
|
||||||
const dialogRef = useRef();
|
const dialogRef = useRef();
|
||||||
|
|
||||||
useClickOutside(dialogRef, () => setShown(false));
|
useClickOutside(dialogRef, () => setShown(false));
|
||||||
|
@ -38,12 +36,12 @@ export function Modal({
|
||||||
<div className={`modal fade ${shown ? "show" : ""}`} role="dialog">
|
<div className={`modal fade ${shown ? "show" : ""}`} role="dialog">
|
||||||
<div
|
<div
|
||||||
ref={dialogRef}
|
ref={dialogRef}
|
||||||
className={`modal-dialog modal-dialog-centered${scrollable ? " modal-dialog-scrollable" : ""}`}
|
className={`modal-dialog modal-dialog-centered${
|
||||||
|
scrollable ? " modal-dialog-scrollable" : ""
|
||||||
|
}`}
|
||||||
role="document"
|
role="document"
|
||||||
>
|
>
|
||||||
<div className="modal-content">
|
<div className="modal-content">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
@ -59,7 +57,11 @@ export function ModalHeader({ setShown, title }) {
|
||||||
return (
|
return (
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<h5 className="modal-title">{title}</h5>
|
<h5 className="modal-title">{title}</h5>
|
||||||
<button type="button" className="close" onClick={() => setShown(false)}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="close"
|
||||||
|
onClick={() => setShown(false)}
|
||||||
|
>
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -85,9 +87,5 @@ ModalFooter.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ModalFooter({ children }) {
|
export function ModalFooter({ children }) {
|
||||||
return (
|
return <div className="modal-footer">{children}</div>;
|
||||||
<div className="modal-footer">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,36 @@
|
||||||
Bootstrap modal component.
|
Bootstrap modal component.
|
||||||
|
|
||||||
it's required to have an element `<div id={"modal-container"}/>` somewhere on the page since modals are rendered in portals.
|
it's required to have an element `<div id={"modal-container"}/>` somewhere on
|
||||||
|
the page since modals are rendered in portals.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
<div id="modal-container"/>
|
<div id="modal-container" />
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {ModalHeader, ModalBody, ModalFooter} from './Modal';
|
import { ModalHeader, ModalBody, ModalFooter } from "./Modal";
|
||||||
|
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const [shown, setShown] = useState(false);
|
const [shown, setShown] = useState(false);
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<Modal setShown={setShown} 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>
|
||||||
<button
|
<button
|
||||||
className='btn btn-secondary'
|
className="btn btn-secondary"
|
||||||
onClick={() => setShown(false)}
|
onClick={() => setShown(false)}
|
||||||
>Skip it</button>
|
>
|
||||||
|
Skip it
|
||||||
|
</button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<button className='btn btn-secondary' onClick={()=>setShown(true)}>
|
<button className="btn btn-secondary" onClick={() => setShown(true)}>
|
||||||
Show modal
|
Show modal
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>;
|
||||||
```
|
```
|
||||||
|
|
|
@ -4,7 +4,7 @@ input[type="number"] {
|
||||||
appearance: textfield;
|
appearance: textfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=number]::-webkit-inner-spin-button,
|
input[type="number"]::-webkit-inner-spin-button,
|
||||||
input[type=number]::-webkit-outer-spin-button {
|
input[type="number"]::-webkit-outer-spin-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,7 @@ NumberInput.propTypes = {
|
||||||
/** Help text message. */
|
/** Help text message. */
|
||||||
helpText: PropTypes.string,
|
helpText: PropTypes.string,
|
||||||
/** Number value. */
|
/** Number value. */
|
||||||
value: PropTypes.oneOfType([
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.number,
|
|
||||||
]),
|
|
||||||
/** Function called when value changes. */
|
/** Function called when value changes. */
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
/** Additional description dispaled to the right of input value. */
|
/** Additional description dispaled to the right of input value. */
|
||||||
|
@ -34,15 +31,21 @@ NumberInput.defaultProps = {
|
||||||
value: 0,
|
value: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function NumberInput({
|
export function NumberInput({ onChange, inlineText, value, ...props }) {
|
||||||
onChange, inlineText, value, ...props
|
|
||||||
}) {
|
|
||||||
function updateValue(initialValue, difference) {
|
function updateValue(initialValue, difference) {
|
||||||
onChange({ target: { value: initialValue + difference } });
|
onChange({ target: { value: initialValue + difference } });
|
||||||
}
|
}
|
||||||
|
|
||||||
const enableIncrease = useConditionalTimeout({ callback: updateValue }, value, 1);
|
const enableIncrease = useConditionalTimeout(
|
||||||
const enableDecrease = useConditionalTimeout({ callback: updateValue }, value, -1);
|
{ callback: updateValue },
|
||||||
|
value,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
const enableDecrease = useConditionalTimeout(
|
||||||
|
{ callback: updateValue },
|
||||||
|
value,
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input type="number" onChange={onChange} value={value} {...props}>
|
<Input type="number" onChange={onChange} value={value} {...props}>
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
Bootstrap component of number input with label with predefined sizes and structure for using in foris forms.
|
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.
|
All additional `props` are passed to the `<input type="number">` HTML component.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const [value, setValue] = useState(42);
|
const [value, setValue] = useState(42);
|
||||||
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={value}
|
value={value}
|
||||||
label="Some number"
|
label="Some number"
|
||||||
helpText="Read the small text!"
|
helpText="Read the small text!"
|
||||||
min='33'
|
min="33"
|
||||||
max='54'
|
max="54"
|
||||||
onChange={event =>setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
|
|
@ -31,22 +31,24 @@ export function PasswordInput({ withEye, ...props }) {
|
||||||
autoComplete={isHidden ? "new-password" : null}
|
autoComplete={isHidden ? "new-password" : null}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{withEye
|
{withEye ? (
|
||||||
? (
|
<div className="input-group-append">
|
||||||
<div className="input-group-append">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className="input-group-text"
|
||||||
className="input-group-text"
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
e.preventDefault();
|
||||||
e.preventDefault();
|
setHidden((shouldBeHidden) => !shouldBeHidden);
|
||||||
setHidden((shouldBeHidden) => !shouldBeHidden);
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<i
|
||||||
<i className={`fa ${isHidden ? "fa-eye" : "fa-eye-slash"}`} />
|
className={`fa ${
|
||||||
</button>
|
isHidden ? "fa-eye" : "fa-eye-slash"
|
||||||
</div>
|
}`}
|
||||||
)
|
/>
|
||||||
: null}
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</Input>
|
</Input>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
Password Bootstrap component input with label and predefined sizes and structure for using in foris forms.
|
Password Bootstrap component input with label and predefined sizes and structure
|
||||||
Can be used with "eye" button, see example.
|
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.
|
All additional `props` are passed to the `<input type="password">` HTML
|
||||||
|
component.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const [value, setValue] = useState('secret');
|
const [value, setValue] = useState("secret");
|
||||||
|
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
withEye
|
withEye
|
||||||
value={value}
|
value={value}
|
||||||
label="Some password"
|
label="Some password"
|
||||||
helpText="Read the small text!"
|
helpText="Read the small text!"
|
||||||
onChange={event =>setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
|
|
@ -15,26 +15,27 @@ RadioSet.propTypes = {
|
||||||
/** RadioSet label . */
|
/** RadioSet label . */
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
/** Choices . */
|
/** Choices . */
|
||||||
choices: PropTypes.arrayOf(PropTypes.shape({
|
choices: PropTypes.arrayOf(
|
||||||
/** Choice lable . */
|
PropTypes.shape({
|
||||||
label: PropTypes.oneOfType([
|
/** Choice lable . */
|
||||||
PropTypes.string,
|
label: PropTypes.oneOfType([
|
||||||
PropTypes.element,
|
PropTypes.string,
|
||||||
PropTypes.node,
|
PropTypes.element,
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
PropTypes.node,
|
||||||
]).isRequired,
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
/** Choice value . */
|
]).isRequired,
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
/** Choice value . */
|
||||||
})).isRequired,
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||||
|
.isRequired,
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
/** Initial value . */
|
/** Initial value . */
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
/** Help text message . */
|
/** Help text message . */
|
||||||
helpText: PropTypes.string,
|
helpText: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RadioSet({
|
export function RadioSet({ name, label, choices, value, helpText, ...props }) {
|
||||||
name, label, choices, value, helpText, ...props
|
|
||||||
}) {
|
|
||||||
const uid = useUID();
|
const uid = useUID();
|
||||||
const radios = choices.map((choice, key) => {
|
const radios = choices.map((choice, key) => {
|
||||||
const id = `${name}-${key}`;
|
const id = `${name}-${key}`;
|
||||||
|
@ -47,7 +48,6 @@ export function RadioSet({
|
||||||
value={choice.value}
|
value={choice.value}
|
||||||
helpText={choice.helpText}
|
helpText={choice.helpText}
|
||||||
checked={choice.value === value}
|
checked={choice.value === value}
|
||||||
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -55,9 +55,15 @@ export function RadioSet({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
{label && <label htmlFor={uid} className="d-block">{label}</label>}
|
{label && (
|
||||||
|
<label htmlFor={uid} className="d-block">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
{radios}
|
{radios}
|
||||||
{helpText && <small className="form-text text-muted">{helpText}</small>}
|
{helpText && (
|
||||||
|
<small className="form-text text-muted">{helpText}</small>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -73,21 +79,28 @@ Radio.propTypes = {
|
||||||
helpText: PropTypes.string,
|
helpText: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Radio({
|
export function Radio({ label, id, helpText, ...props }) {
|
||||||
label, id, helpText, ...props
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`custom-control custom-radio ${!helpText ? "custom-control-inline" : ""}`.trim()}>
|
<div
|
||||||
|
className={`custom-control custom-radio ${
|
||||||
|
!helpText ? "custom-control-inline" : ""
|
||||||
|
}`.trim()}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
className="custom-control-input"
|
className="custom-control-input"
|
||||||
type="radio"
|
type="radio"
|
||||||
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<label className="custom-control-label" htmlFor={id}>{label}</label>
|
<label className="custom-control-label" htmlFor={id}>
|
||||||
{helpText && <small className="form-text text-muted mt-0 mb-3">{helpText}</small>}
|
{label}
|
||||||
|
</label>
|
||||||
|
{helpText && (
|
||||||
|
<small className="form-text text-muted mt-0 mb-3">
|
||||||
|
{helpText}
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
Set of radio Bootstrap component input with label and predefined sizes and structure for using in foris forms.
|
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.
|
All additional `props` are passed to the `<input type="number">` HTML component.
|
||||||
|
|
||||||
Unless `helpText` is set for one of the options they are displayed inline.
|
Unless `helpText` is set for one of the options they are displayed inline.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const CHOICES=[
|
const CHOICES = [
|
||||||
{value:'one',label:'1'},
|
{ value: "one", label: "1" },
|
||||||
{value:'two',label:'2'},
|
{ value: "two", label: "2" },
|
||||||
{value:'three',label:'3'},
|
{ value: "three", label: "3" },
|
||||||
];
|
];
|
||||||
const [value, setValue] = useState(CHOICES[0].value);
|
const [value, setValue] = useState(CHOICES[0].value);
|
||||||
|
|
||||||
|
@ -17,10 +18,10 @@ const [value, setValue] = useState(CHOICES[0].value);
|
||||||
{/*Yeah, it gets event, not value!*/}
|
{/*Yeah, it gets event, not value!*/}
|
||||||
<RadioSet
|
<RadioSet
|
||||||
value={value}
|
value={value}
|
||||||
name='some-radio'
|
name="some-radio"
|
||||||
choices={CHOICES}
|
choices={CHOICES}
|
||||||
onChange={event =>setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<p>Selected value: {value}</p>
|
<p>Selected value: {value}</p>
|
||||||
</>
|
</>;
|
||||||
```
|
```
|
||||||
|
|
|
@ -15,35 +15,30 @@ Select.propTypes = {
|
||||||
/** Choices if form of {value : "Label",...}. */
|
/** Choices if form of {value : "Label",...}. */
|
||||||
choices: PropTypes.object.isRequired,
|
choices: PropTypes.object.isRequired,
|
||||||
/** Current value. */
|
/** Current value. */
|
||||||
value: PropTypes.oneOfType([
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.number,
|
|
||||||
]).isRequired,
|
|
||||||
/** Help text message. */
|
/** Help text message. */
|
||||||
helpText: PropTypes.string,
|
helpText: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Select({
|
export function Select({ label, choices, helpText, ...props }) {
|
||||||
label, choices, helpText, ...props
|
|
||||||
}) {
|
|
||||||
const uid = useUID();
|
const uid = useUID();
|
||||||
|
|
||||||
const options = Object.keys(choices).sort(
|
const options = Object.keys(choices)
|
||||||
(a, b) => a - b || a.toString().localeCompare(b.toString()),
|
.sort((a, b) => a - b || a.toString().localeCompare(b.toString()))
|
||||||
).map(
|
.map((key) => (
|
||||||
(key) => <option key={key} value={key}>{choices[key]}</option>,
|
<option key={key} value={key}>
|
||||||
);
|
{choices[key]}
|
||||||
|
</option>
|
||||||
|
));
|
||||||
return (
|
return (
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor={uid}>{label}</label>
|
<label htmlFor={uid}>{label}</label>
|
||||||
<select
|
<select className="custom-select" id={uid} {...props}>
|
||||||
className="custom-select"
|
|
||||||
id={uid}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{options}
|
{options}
|
||||||
</select>
|
</select>
|
||||||
{helpText ? <small className="form-text text-muted">{helpText}</small> : null}
|
{helpText ? (
|
||||||
|
<small className="form-text text-muted">{helpText}</small>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
Select with options Bootstrap component input with label and predefined sizes and structure for using in foris forms.
|
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.
|
All additional `props` are passed to the `<select>` HTML component.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const CHOICES={
|
const CHOICES = {
|
||||||
apple:'Apple',
|
apple: "Apple",
|
||||||
banana:'Banana',
|
banana: "Banana",
|
||||||
peach:'Peach',
|
peach: "Peach",
|
||||||
};
|
};
|
||||||
const [value, setValue] = useState(Object.keys(CHOICES)[0]);
|
const [value, setValue] = useState(Object.keys(CHOICES)[0]);
|
||||||
|
|
||||||
|
@ -17,9 +18,9 @@ const [value, setValue] = useState(Object.keys(CHOICES)[0]);
|
||||||
label="Fruit"
|
label="Fruit"
|
||||||
value={value}
|
value={value}
|
||||||
choices={CHOICES}
|
choices={CHOICES}
|
||||||
onChange={event=>setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<p>Selected choice label: {CHOICES[value]}</p>
|
<p>Selected choice label: {CHOICES[value]}</p>
|
||||||
<p>Selected choice value: {value}</p>
|
<p>Selected choice value: {value}</p>
|
||||||
</>
|
</>;
|
||||||
```
|
```
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner-fs-background {
|
.spinner-fs-background {
|
||||||
background-color: rgba(2, 2, 2, .5);
|
background-color: rgba(2, 2, 2, 0.5);
|
||||||
color: rgb(230, 230, 230);
|
color: rgb(230, 230, 230);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -25,12 +25,12 @@ Spinner.defaultProps = {
|
||||||
fullScreen: false,
|
fullScreen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Spinner({
|
export function Spinner({ fullScreen, children, className }) {
|
||||||
fullScreen, children, className,
|
|
||||||
}) {
|
|
||||||
if (!fullScreen) {
|
if (!fullScreen) {
|
||||||
return (
|
return (
|
||||||
<div className={`spinner-wrapper ${className || "my-3 text-center"}`}>
|
<div
|
||||||
|
className={`spinner-wrapper ${className || "my-3 text-center"}`}
|
||||||
|
>
|
||||||
<SpinnerElement>{children}</SpinnerElement>
|
<SpinnerElement>{children}</SpinnerElement>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -61,7 +61,9 @@ export function SpinnerElement({ small, className, children }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`spinner-border ${small ? "spinner-border-sm" : ""} ${className || ""}`.trim()}
|
className={`spinner-border ${
|
||||||
|
small ? "spinner-border-sm" : ""
|
||||||
|
} ${className || ""}`.trim()}
|
||||||
role="status"
|
role="status"
|
||||||
>
|
>
|
||||||
<span className="sr-only" />
|
<span className="sr-only" />
|
||||||
|
|
49
src/bootstrap/Switch.js
Normal file
49
src/bootstrap/Switch.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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";
|
||||||
|
|
||||||
|
Switch.propTypes = {
|
||||||
|
label: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.element,
|
||||||
|
PropTypes.node,
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
]).isRequired,
|
||||||
|
helpText: PropTypes.string,
|
||||||
|
switchHeading: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Switch({ label, helpText, switchHeading, ...props }) {
|
||||||
|
const uid = useUID();
|
||||||
|
return (
|
||||||
|
<div className="form-group">
|
||||||
|
<div
|
||||||
|
className={`custom-control custom-switch ${
|
||||||
|
!helpText ? "custom-control-inline" : ""
|
||||||
|
} ${switchHeading ? "switch-heading" : ""}`.trim()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="custom-control-input"
|
||||||
|
id={uid}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<label className="custom-control-label" htmlFor={uid}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
{helpText && (
|
||||||
|
<small className="form-text text-muted mt-0 mb-3">
|
||||||
|
{helpText}
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,15 +1,16 @@
|
||||||
Text Bootstrap component input with label and predefined sizes and structure for using in foris forms.
|
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.
|
All additional `props` are passed to the `<input type="text">` HTML component.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const [value, setValue] = useState('Bla bla');
|
const [value, setValue] = useState("Bla bla");
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
value={value}
|
value={value}
|
||||||
label="Some text"
|
label="Some text"
|
||||||
helpText="Read the small text!"
|
helpText="Read the small text!"
|
||||||
onChange={event =>setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
|
|
@ -14,19 +14,18 @@ import { Button } from "../Button";
|
||||||
describe("<Button />", () => {
|
describe("<Button />", () => {
|
||||||
it("Render button correctly", () => {
|
it("Render button correctly", () => {
|
||||||
const { container } = render(<Button>Test Button</Button>);
|
const { container } = render(<Button>Test Button</Button>);
|
||||||
expect(container.firstChild)
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Render button with custom classes", () => {
|
it("Render button with custom classes", () => {
|
||||||
const { container } = render(<Button className="one two three">Test Button</Button>);
|
const { container } = render(
|
||||||
expect(container.firstChild)
|
<Button className="one two three">Test Button</Button>
|
||||||
.toMatchSnapshot();
|
);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Render button with spinner", () => {
|
it("Render button with spinner", () => {
|
||||||
const { container } = render(<Button loading>Test Button</Button>);
|
const { container } = render(<Button loading>Test Button</Button>);
|
||||||
expect(container.firstChild)
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,22 +18,16 @@ describe("<Checkbox/>", () => {
|
||||||
label="Test label"
|
label="Test label"
|
||||||
checked
|
checked
|
||||||
helpText="Some help text"
|
helpText="Some help text"
|
||||||
onChange={() => {
|
onChange={() => {}}
|
||||||
}}
|
/>
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
expect(container.firstChild)
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Render uncheked checkbox", () => {
|
it("Render uncheked checkbox", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<CheckBox
|
<CheckBox label="Test label" helpText="Some help text" />
|
||||||
label="Test label"
|
|
||||||
helpText="Some help text"
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
expect(container.firstChild)
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,7 +13,11 @@ import { DownloadButton } from "../DownloadButton";
|
||||||
|
|
||||||
describe("<DownloadButton />", () => {
|
describe("<DownloadButton />", () => {
|
||||||
it("should have download attribute", () => {
|
it("should have download attribute", () => {
|
||||||
const { container } = render(<DownloadButton href="http://example.com">Test Button</DownloadButton>);
|
const { container } = render(
|
||||||
|
<DownloadButton href="http://example.com">
|
||||||
|
Test Button
|
||||||
|
</DownloadButton>
|
||||||
|
);
|
||||||
expect(container.firstChild.getAttribute("download")).not.toBeNull();
|
expect(container.firstChild.getAttribute("download")).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,9 +7,7 @@
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import {
|
import { render, fireEvent, getByLabelText, wait } from "customTestRender";
|
||||||
render, fireEvent, getByLabelText, wait,
|
|
||||||
} from "customTestRender";
|
|
||||||
|
|
||||||
import { NumberInput } from "../NumberInput";
|
import { NumberInput } from "../NumberInput";
|
||||||
|
|
||||||
|
@ -24,7 +22,7 @@ describe("<NumberInput/>", () => {
|
||||||
helpText="Some help text"
|
helpText="Some help text"
|
||||||
value={1}
|
value={1}
|
||||||
onChange={onChangeMock}
|
onChange={onChangeMock}
|
||||||
/>,
|
/>
|
||||||
);
|
);
|
||||||
componentContainer = container;
|
componentContainer = container;
|
||||||
});
|
});
|
||||||
|
@ -36,12 +34,16 @@ describe("<NumberInput/>", () => {
|
||||||
it("Increase number with button", async () => {
|
it("Increase number with button", async () => {
|
||||||
const increaseButton = getByLabelText(componentContainer, "Increase");
|
const increaseButton = getByLabelText(componentContainer, "Increase");
|
||||||
fireEvent.mouseDown(increaseButton);
|
fireEvent.mouseDown(increaseButton);
|
||||||
await wait(() => expect(onChangeMock).toHaveBeenCalledWith({ target: { value: 2 } }));
|
await wait(() =>
|
||||||
|
expect(onChangeMock).toHaveBeenCalledWith({ target: { value: 2 } })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Decrease number with button", async () => {
|
it("Decrease number with button", async () => {
|
||||||
const decreaseButton = getByLabelText(componentContainer, "Decrease");
|
const decreaseButton = getByLabelText(componentContainer, "Decrease");
|
||||||
fireEvent.mouseDown(decreaseButton);
|
fireEvent.mouseDown(decreaseButton);
|
||||||
await wait(() => expect(onChangeMock).toHaveBeenCalledWith({ target: { value: 0 } }));
|
await wait(() =>
|
||||||
|
expect(onChangeMock).toHaveBeenCalledWith({ target: { value: 0 } })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,11 +18,9 @@ describe("<PasswordInput/>", () => {
|
||||||
label="Test label"
|
label="Test label"
|
||||||
helpText="Some help text"
|
helpText="Some help text"
|
||||||
value="Some password"
|
value="Some password"
|
||||||
onChange={() => {
|
onChange={() => {}}
|
||||||
}}
|
/>
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
expect(container.firstChild)
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,11 +35,9 @@ describe("<RadioSet/>", () => {
|
||||||
value="value"
|
value="value"
|
||||||
choices={TEST_CHOICES}
|
choices={TEST_CHOICES}
|
||||||
helpText="Some help text"
|
helpText="Some help text"
|
||||||
onChange={() => {
|
onChange={() => {}}
|
||||||
}}
|
/>
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
expect(container.firstChild)
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,10 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fireEvent, getByDisplayValue, getByText, render,
|
fireEvent,
|
||||||
|
getByDisplayValue,
|
||||||
|
getByText,
|
||||||
|
render,
|
||||||
} from "customTestRender";
|
} from "customTestRender";
|
||||||
|
|
||||||
import { Select } from "../Select";
|
import { Select } from "../Select";
|
||||||
|
@ -29,29 +32,24 @@ describe("<Select/>", () => {
|
||||||
value="1"
|
value="1"
|
||||||
choices={TEST_CHOICES}
|
choices={TEST_CHOICES}
|
||||||
helpText="Help text"
|
helpText="Help text"
|
||||||
|
|
||||||
onChange={onChangeHandler}
|
onChange={onChangeHandler}
|
||||||
/>,
|
/>
|
||||||
);
|
);
|
||||||
selectContainer = container;
|
selectContainer = container;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Test with snapshot.", () => {
|
it("Test with snapshot.", () => {
|
||||||
expect(selectContainer)
|
expect(selectContainer).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Test onChange handling.", () => {
|
it("Test onChange handling.", () => {
|
||||||
const select = getByDisplayValue(selectContainer, "one");
|
const select = getByDisplayValue(selectContainer, "one");
|
||||||
expect(select.value)
|
expect(select.value).toBe("1");
|
||||||
.toBe("1");
|
|
||||||
fireEvent.change(select, { target: { value: "2" } });
|
fireEvent.change(select, { target: { value: "2" } });
|
||||||
|
|
||||||
const option = getByText(selectContainer, "two");
|
const option = getByText(selectContainer, "two");
|
||||||
expect(onChangeHandler)
|
expect(onChangeHandler).toBeCalled();
|
||||||
.toBeCalled();
|
|
||||||
|
|
||||||
expect(option.value)
|
expect(option.value).toBe("2");
|
||||||
.toBe("2");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
33
src/bootstrap/__tests__/Switch.test.js
Normal file
33
src/bootstrap/__tests__/Switch.test.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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 { Switch } from "../Switch";
|
||||||
|
|
||||||
|
describe("<Switch/>", () => {
|
||||||
|
it("Render switch", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Switch
|
||||||
|
label="Test label"
|
||||||
|
checked
|
||||||
|
helpText="Some help text"
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Render uncheked switch", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Switch label="Test label" helpText="Some help text" />
|
||||||
|
);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -18,11 +18,9 @@ describe("<TextInput/>", () => {
|
||||||
label="Test label"
|
label="Test label"
|
||||||
helpText="Some help text"
|
helpText="Some help text"
|
||||||
value="Some text"
|
value="Some text"
|
||||||
onChange={() => {
|
onChange={() => {}}
|
||||||
}}
|
/>
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
expect(container.firstChild)
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,8 +5,6 @@ exports[`<Button /> Render button correctly 1`] = `
|
||||||
class="btn btn-primary "
|
class="btn btn-primary "
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
||||||
Test Button
|
Test Button
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
@ -16,8 +14,6 @@ exports[`<Button /> Render button with custom classes 1`] = `
|
||||||
class="btn one two three"
|
class="btn one two three"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
||||||
Test Button
|
Test Button
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
@ -33,8 +29,6 @@ exports[`<Button /> Render button with spinner 1`] = `
|
||||||
role="status"
|
role="status"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Test Button
|
Test Button
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
56
src/bootstrap/__tests__/__snapshots__/Switch.test.js.snap
Normal file
56
src/bootstrap/__tests__/__snapshots__/Switch.test.js.snap
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<Switch/> Render switch 1`] = `
|
||||||
|
<div
|
||||||
|
class="form-group"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="custom-control custom-switch"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked=""
|
||||||
|
class="custom-control-input"
|
||||||
|
id="1"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="custom-control-label"
|
||||||
|
for="1"
|
||||||
|
>
|
||||||
|
Test label
|
||||||
|
</label>
|
||||||
|
<small
|
||||||
|
class="form-text text-muted mt-0 mb-3"
|
||||||
|
>
|
||||||
|
Some help text
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<Switch/> Render uncheked switch 1`] = `
|
||||||
|
<div
|
||||||
|
class="form-group"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="custom-control custom-switch"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="custom-control-input"
|
||||||
|
id="1"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="custom-control-label"
|
||||||
|
for="1"
|
||||||
|
>
|
||||||
|
Test label
|
||||||
|
</label>
|
||||||
|
<small
|
||||||
|
class="form-text text-muted mt-0 mb-3"
|
||||||
|
>
|
||||||
|
Some help text
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -7,4 +7,5 @@
|
||||||
|
|
||||||
/** Bootstrap column size for form fields */
|
/** Bootstrap column size for form fields */
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export const formFieldsSize = "col-sm-12 offset-lg-1 col-lg-10 p-0 mb-3";
|
export const formFieldsSize = "card p-4 col-sm-12 col-lg-12 p-0 mb-3";
|
||||||
|
export const buttonFormFieldsSize = "col-sm-12 col-lg-12 p-0 mb-3";
|
||||||
|
|
|
@ -13,15 +13,9 @@ import { API_STATE } from "../api/utils";
|
||||||
import { ForisURLs } from "../utils/forisUrls";
|
import { ForisURLs } from "../utils/forisUrls";
|
||||||
|
|
||||||
import { Button } from "../bootstrap/Button";
|
import { Button } from "../bootstrap/Button";
|
||||||
import {
|
import { Modal, ModalHeader, ModalBody, ModalFooter } from "../bootstrap/Modal";
|
||||||
Modal, ModalHeader, ModalBody, ModalFooter,
|
|
||||||
} from "../bootstrap/Modal";
|
|
||||||
import { useAlert } from "../alertContext/AlertContext";
|
import { useAlert } from "../alertContext/AlertContext";
|
||||||
|
|
||||||
RebootButton.propTypes = {
|
|
||||||
forisFormSize: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function RebootButton(props) {
|
export function RebootButton(props) {
|
||||||
const [triggered, setTriggered] = useState(false);
|
const [triggered, setTriggered] = useState(false);
|
||||||
const [modalShown, setModalShown] = useState(false);
|
const [modalShown, setModalShown] = useState(false);
|
||||||
|
@ -42,13 +36,16 @@ export function RebootButton(props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RebootModal shown={modalShown} setShown={setModalShown} onReboot={rebootHandler} />
|
<RebootModal
|
||||||
|
shown={modalShown}
|
||||||
|
setShown={setModalShown}
|
||||||
|
onReboot={rebootHandler}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="btn-danger"
|
className="btn-danger"
|
||||||
loading={triggered}
|
loading={triggered}
|
||||||
disabled={triggered}
|
disabled={triggered}
|
||||||
onClick={() => setModalShown(true)}
|
onClick={() => setModalShown(true)}
|
||||||
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{_("Reboot")}
|
{_("Reboot")}
|
||||||
|
@ -67,10 +64,14 @@ function RebootModal({ shown, setShown, onReboot }) {
|
||||||
return (
|
return (
|
||||||
<Modal shown={shown} setShown={setShown}>
|
<Modal shown={shown} setShown={setShown}>
|
||||||
<ModalHeader setShown={setShown} title={_("Reboot confirmation")} />
|
<ModalHeader setShown={setShown} title={_("Reboot confirmation")} />
|
||||||
<ModalBody><p>{_("Are you sure you want to restart the router?")}</p></ModalBody>
|
<ModalBody>
|
||||||
|
<p>{_("Are you sure you want to restart the router?")}</p>
|
||||||
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onClick={() => setShown(false)}>{_("Cancel")}</Button>
|
<Button onClick={() => setShown(false)}>{_("Cancel")}</Button>
|
||||||
<Button className="btn-danger" onClick={onReboot}>{_("Confirm reboot")}</Button>
|
<Button className="btn-danger" onClick={onReboot}>
|
||||||
|
{_("Confirm reboot")}
|
||||||
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { useAlert } from "../../alertContext/AlertContext";
|
||||||
import { ALERT_TYPES } from "../../bootstrap/Alert";
|
import { ALERT_TYPES } from "../../bootstrap/Alert";
|
||||||
import { useAPIPost } from "../../api/hooks";
|
import { useAPIPost } from "../../api/hooks";
|
||||||
import { API_STATE } from "../../api/utils";
|
import { API_STATE } from "../../api/utils";
|
||||||
import { formFieldsSize } from "../../bootstrap/constants";
|
import { buttonFormFieldsSize } from "../../bootstrap/constants";
|
||||||
|
|
||||||
ResetWiFiSettings.propTypes = {
|
ResetWiFiSettings.propTypes = {
|
||||||
ws: PropTypes.object.isRequired,
|
ws: PropTypes.object.isRequired,
|
||||||
|
@ -25,11 +25,10 @@ export default function ResetWiFiSettings({ ws, endpoint }) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const module = "wifi";
|
const module = "wifi";
|
||||||
ws.subscribe(module)
|
ws.subscribe(module).bind(module, "reset", () => {
|
||||||
.bind(module, "reset", () => {
|
// eslint-disable-next-line no-restricted-globals
|
||||||
// eslint-disable-next-line no-restricted-globals
|
setTimeout(() => location.reload(), 1000);
|
||||||
setTimeout(() => location.reload(), 1000);
|
});
|
||||||
});
|
|
||||||
}, [ws]);
|
}, [ws]);
|
||||||
|
|
||||||
const [postResetResponse, postReset] = useAPIPost(endpoint);
|
const [postResetResponse, postReset] = useAPIPost(endpoint);
|
||||||
|
@ -38,7 +37,10 @@ export default function ResetWiFiSettings({ ws, endpoint }) {
|
||||||
if (postResetResponse.state === API_STATE.ERROR) {
|
if (postResetResponse.state === API_STATE.ERROR) {
|
||||||
setAlert(_("An error occurred during resetting Wi-Fi settings."));
|
setAlert(_("An error occurred during resetting Wi-Fi settings."));
|
||||||
} else if (postResetResponse.state === API_STATE.SUCCESS) {
|
} else if (postResetResponse.state === API_STATE.SUCCESS) {
|
||||||
setAlert(_("Wi-Fi settings are set to defaults."), ALERT_TYPES.SUCCESS);
|
setAlert(
|
||||||
|
_("Wi-Fi settings are set to defaults."),
|
||||||
|
ALERT_TYPES.SUCCESS
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [postResetResponse, setAlert]);
|
}, [postResetResponse, setAlert]);
|
||||||
|
|
||||||
|
@ -50,20 +52,19 @@ export default function ResetWiFiSettings({ ws, endpoint }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4>{_("Reset Wi-Fi Settings")}</h4>
|
<h2>{_("Reset Wi-Fi Settings")}</h2>
|
||||||
<p>
|
<p>
|
||||||
{_(`
|
{_(`
|
||||||
If a number of wireless cards doesn't match, you may try to reset the Wi-Fi settings. Note that this will remove the
|
If a number of wireless cards doesn't match, you may try to reset the Wi-Fi settings. Note that this will remove the
|
||||||
current Wi-Fi configuration and restore the default values.
|
current Wi-Fi configuration and restore the default values.
|
||||||
`)}
|
`)}
|
||||||
</p>
|
</p>
|
||||||
<div className={`${formFieldsSize} text-right`}>
|
<div className={`${buttonFormFieldsSize} text-right`}>
|
||||||
<Button
|
<Button
|
||||||
className="btn-warning"
|
className="btn-warning"
|
||||||
forisFormSize
|
forisFormSize
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
|
||||||
onClick={onReset}
|
onClick={onReset}
|
||||||
>
|
>
|
||||||
{_("Reset Wi-Fi Settings")}
|
{_("Reset Wi-Fi Settings")}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import { Switch } from "../../bootstrap/Switch";
|
||||||
import { CheckBox } from "../../bootstrap/CheckBox";
|
import { CheckBox } from "../../bootstrap/CheckBox";
|
||||||
import { PasswordInput } from "../../bootstrap/PasswordInput";
|
import { PasswordInput } from "../../bootstrap/PasswordInput";
|
||||||
import { RadioSet } from "../../bootstrap/RadioSet";
|
import { RadioSet } from "../../bootstrap/RadioSet";
|
||||||
|
@ -18,25 +18,25 @@ import WifiGuestForm from "./WiFiGuestForm";
|
||||||
import { HELP_TEXTS, HTMODES, HWMODES } from "./constants";
|
import { HELP_TEXTS, HTMODES, HWMODES } from "./constants";
|
||||||
|
|
||||||
WiFiForm.propTypes = {
|
WiFiForm.propTypes = {
|
||||||
formData: PropTypes.shape(
|
formData: PropTypes.shape({ devices: PropTypes.arrayOf(PropTypes.object) })
|
||||||
{ devices: PropTypes.arrayOf(PropTypes.object) },
|
.isRequired,
|
||||||
).isRequired,
|
formErrors: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
|
||||||
formErrors: PropTypes.oneOfType([
|
|
||||||
PropTypes.object,
|
|
||||||
PropTypes.array,
|
|
||||||
]),
|
|
||||||
setFormValue: PropTypes.func.isRequired,
|
setFormValue: PropTypes.func.isRequired,
|
||||||
hasGuestNetwork: PropTypes.bool,
|
hasGuestNetwork: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
WiFiForm.defaultProps = {
|
WiFiForm.defaultProps = {
|
||||||
formData: { devices: [] },
|
formData: { devices: [] },
|
||||||
setFormValue: () => { },
|
setFormValue: () => {},
|
||||||
hasGuestNetwork: true,
|
hasGuestNetwork: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function WiFiForm({
|
export default function WiFiForm({
|
||||||
formData, formErrors, setFormValue, hasGuestNetwork, disabled,
|
formData,
|
||||||
|
formErrors,
|
||||||
|
setFormValue,
|
||||||
|
hasGuestNetwork,
|
||||||
|
disabled,
|
||||||
}) {
|
}) {
|
||||||
return formData.devices.map((device, index) => (
|
return formData.devices.map((device, index) => (
|
||||||
<DeviceForm
|
<DeviceForm
|
||||||
|
@ -47,6 +47,7 @@ export default function WiFiForm({
|
||||||
setFormValue={setFormValue}
|
setFormValue={setFormValue}
|
||||||
hasGuestNetwork={hasGuestNetwork}
|
hasGuestNetwork={hasGuestNetwork}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
divider={index + 1 !== formData.devices.length}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -67,6 +68,7 @@ DeviceForm.propTypes = {
|
||||||
setFormValue: PropTypes.func.isRequired,
|
setFormValue: PropTypes.func.isRequired,
|
||||||
hasGuestNetwork: PropTypes.bool,
|
hasGuestNetwork: PropTypes.bool,
|
||||||
deviceIndex: PropTypes.number,
|
deviceIndex: PropTypes.number,
|
||||||
|
divider: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
DeviceForm.defaultProps = {
|
DeviceForm.defaultProps = {
|
||||||
|
@ -75,144 +77,135 @@ DeviceForm.defaultProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function DeviceForm({
|
function DeviceForm({
|
||||||
formData, formErrors, setFormValue, hasGuestNetwork, deviceIndex, ...props
|
formData,
|
||||||
|
formErrors,
|
||||||
|
setFormValue,
|
||||||
|
hasGuestNetwork,
|
||||||
|
deviceIndex,
|
||||||
|
divider,
|
||||||
|
...props
|
||||||
}) {
|
}) {
|
||||||
const deviceID = formData.id;
|
const deviceID = formData.id;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3>{_(`Wi-Fi ${deviceID + 1}`)}</h3>
|
<Switch
|
||||||
<CheckBox
|
label={<h2>{_(`Wi-Fi ${deviceID + 1}`)}</h2>}
|
||||||
label={_("Enable")}
|
|
||||||
checked={formData.enabled}
|
checked={formData.enabled}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
onChange={setFormValue(
|
devices: {
|
||||||
(value) => ({ devices: { [deviceIndex]: { enabled: { $set: value } } } }),
|
[deviceIndex]: { enabled: { $set: value } },
|
||||||
)}
|
},
|
||||||
|
}))}
|
||||||
|
switchHeading
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{formData.enabled
|
{formData.enabled ? (
|
||||||
? (
|
<>
|
||||||
<>
|
<TextInput
|
||||||
<TextInput
|
label="SSID"
|
||||||
label="SSID"
|
value={formData.SSID}
|
||||||
value={formData.SSID}
|
error={formErrors.SSID || null}
|
||||||
error={formErrors.SSID || null}
|
required
|
||||||
required
|
onChange={setFormValue((value) => ({
|
||||||
onChange={setFormValue(
|
devices: {
|
||||||
(value) => ({
|
[deviceIndex]: {
|
||||||
devices: {
|
SSID: { $set: value },
|
||||||
[deviceIndex]: {
|
},
|
||||||
SSID: { $set: value },
|
},
|
||||||
},
|
}))}
|
||||||
},
|
{...props}
|
||||||
}),
|
>
|
||||||
)}
|
<div className="input-group-append">
|
||||||
|
<WiFiQRCode
|
||||||
{...props}
|
SSID={formData.SSID}
|
||||||
>
|
password={formData.password}
|
||||||
<div className="input-group-append">
|
|
||||||
<WiFiQRCode
|
|
||||||
SSID={formData.SSID}
|
|
||||||
password={formData.password}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TextInput>
|
|
||||||
|
|
||||||
<PasswordInput
|
|
||||||
withEye
|
|
||||||
label="Password"
|
|
||||||
value={formData.password}
|
|
||||||
error={formErrors.password}
|
|
||||||
helpText={HELP_TEXTS.password}
|
|
||||||
required
|
|
||||||
|
|
||||||
onChange={setFormValue(
|
|
||||||
(value) => (
|
|
||||||
{ devices: { [deviceIndex]: { password: { $set: value } } } }
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CheckBox
|
|
||||||
label="Hide SSID"
|
|
||||||
helpText={HELP_TEXTS.hidden}
|
|
||||||
checked={formData.hidden}
|
|
||||||
|
|
||||||
onChange={setFormValue(
|
|
||||||
(value) => (
|
|
||||||
{ devices: { [deviceIndex]: { hidden: { $set: value } } } }
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RadioSet
|
|
||||||
name={`hwmode-${deviceID}`}
|
|
||||||
label="GHz"
|
|
||||||
choices={getHwmodeChoices(formData)}
|
|
||||||
value={formData.hwmode}
|
|
||||||
helpText={HELP_TEXTS.hwmode}
|
|
||||||
|
|
||||||
onChange={setFormValue(
|
|
||||||
(value) => ({
|
|
||||||
devices: {
|
|
||||||
[deviceIndex]: {
|
|
||||||
hwmode: { $set: value },
|
|
||||||
channel: { $set: "0" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)}
|
|
||||||
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
label="802.11n/ac mode"
|
|
||||||
choices={getHtmodeChoices(formData)}
|
|
||||||
value={formData.htmode}
|
|
||||||
helpText={HELP_TEXTS.htmode}
|
|
||||||
|
|
||||||
onChange={setFormValue(
|
|
||||||
(value) => (
|
|
||||||
{ devices: { [deviceIndex]: { htmode: { $set: value } } } }
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
label="Channel"
|
|
||||||
choices={getChannelChoices(formData)}
|
|
||||||
value={formData.channel}
|
|
||||||
|
|
||||||
onChange={setFormValue(
|
|
||||||
(value) => (
|
|
||||||
{ devices: { [deviceIndex]: { channel: { $set: value } } } }
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{hasGuestNetwork && (
|
|
||||||
<WifiGuestForm
|
|
||||||
formData={{ id: deviceIndex, ...formData.guest_wifi }}
|
|
||||||
formErrors={formErrors.guest_wifi || {}}
|
|
||||||
|
|
||||||
setFormValue={setFormValue}
|
|
||||||
|
|
||||||
{...props}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</>
|
</TextInput>
|
||||||
)
|
|
||||||
: null}
|
<PasswordInput
|
||||||
|
withEye
|
||||||
|
label="Password"
|
||||||
|
value={formData.password}
|
||||||
|
error={formErrors.password}
|
||||||
|
helpText={HELP_TEXTS.password}
|
||||||
|
required
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[deviceIndex]: { password: { $set: value } },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
label="Hide SSID"
|
||||||
|
helpText={HELP_TEXTS.hidden}
|
||||||
|
checked={formData.hidden}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[deviceIndex]: { hidden: { $set: value } },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RadioSet
|
||||||
|
name={`hwmode-${deviceID}`}
|
||||||
|
label="GHz"
|
||||||
|
choices={getHwmodeChoices(formData)}
|
||||||
|
value={formData.hwmode}
|
||||||
|
helpText={HELP_TEXTS.hwmode}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[deviceIndex]: {
|
||||||
|
hwmode: { $set: value },
|
||||||
|
channel: { $set: "0" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="802.11n/ac mode"
|
||||||
|
choices={getHtmodeChoices(formData)}
|
||||||
|
value={formData.htmode}
|
||||||
|
helpText={HELP_TEXTS.htmode}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[deviceIndex]: { htmode: { $set: value } },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Channel"
|
||||||
|
choices={getChannelChoices(formData)}
|
||||||
|
value={formData.channel}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[deviceIndex]: { channel: { $set: value } },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hasGuestNetwork && (
|
||||||
|
<WifiGuestForm
|
||||||
|
formData={{
|
||||||
|
id: deviceIndex,
|
||||||
|
...formData.guest_wifi,
|
||||||
|
}}
|
||||||
|
formErrors={formErrors.guest_wifi || {}}
|
||||||
|
setFormValue={setFormValue}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{divider ? <hr /> : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -228,7 +221,9 @@ function getChannelChoices(device) {
|
||||||
availableBand.available_channels.forEach((availableChannel) => {
|
availableBand.available_channels.forEach((availableChannel) => {
|
||||||
channelChoices[availableChannel.number.toString()] = `
|
channelChoices[availableChannel.number.toString()] = `
|
||||||
${availableChannel.number}
|
${availableChannel.number}
|
||||||
(${availableChannel.frequency} MHz ${availableChannel.radar ? " ,DFS" : ""})
|
(${availableChannel.frequency} MHz ${
|
||||||
|
availableChannel.radar ? " ,DFS" : ""
|
||||||
|
})
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import { CheckBox } from "../../bootstrap/CheckBox";
|
|
||||||
import { TextInput } from "../../bootstrap/TextInput";
|
import { TextInput } from "../../bootstrap/TextInput";
|
||||||
|
import { Switch } from "../../bootstrap/Switch";
|
||||||
import { PasswordInput } from "../../bootstrap/PasswordInput";
|
import { PasswordInput } from "../../bootstrap/PasswordInput";
|
||||||
import WiFiQRCode from "./WiFiQRCode";
|
import WiFiQRCode from "./WiFiQRCode";
|
||||||
import { HELP_TEXTS } from "./constants";
|
import { HELP_TEXTS } from "./constants";
|
||||||
|
@ -26,75 +26,72 @@ WifiGuestForm.propTypes = {
|
||||||
password: PropTypes.string,
|
password: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
setFormValue: PropTypes.func.isRequired,
|
setFormValue: PropTypes.func.isRequired,
|
||||||
|
deviceIndex: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function WifiGuestForm({
|
export default function WifiGuestForm({
|
||||||
formData, formErrors, setFormValue, ...props
|
formData,
|
||||||
|
formErrors,
|
||||||
|
setFormValue,
|
||||||
|
deviceIndex,
|
||||||
|
...props
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CheckBox
|
<Switch
|
||||||
label={_("Enable Guest Wifi")}
|
label={_("Enable Guest Wi-Fi")}
|
||||||
checked={formData.enabled}
|
checked={formData.enabled}
|
||||||
helpText={HELP_TEXTS.guest_wifi_enabled}
|
helpText={HELP_TEXTS.guest_wifi_enabled}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
onChange={setFormValue(
|
devices: {
|
||||||
(value) => (
|
[formData.id]: {
|
||||||
{ devices: { [formData.id]: { guest_wifi: { enabled: { $set: value } } } } }
|
guest_wifi: { enabled: { $set: value } },
|
||||||
),
|
},
|
||||||
)}
|
},
|
||||||
|
}))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{formData.enabled
|
{formData.enabled ? (
|
||||||
? (
|
<>
|
||||||
<>
|
<TextInput
|
||||||
<TextInput
|
label="SSID"
|
||||||
label="SSID"
|
value={formData.SSID}
|
||||||
value={formData.SSID}
|
error={formErrors.SSID}
|
||||||
error={formErrors.SSID}
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[formData.id]: {
|
||||||
|
guest_wifi: { SSID: { $set: value } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="input-group-append">
|
||||||
|
<WiFiQRCode
|
||||||
|
SSID={formData.SSID}
|
||||||
|
password={formData.password}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TextInput>
|
||||||
|
|
||||||
onChange={setFormValue(
|
<PasswordInput
|
||||||
(value) => ({
|
withEye
|
||||||
devices: {
|
label={_("Password")}
|
||||||
[formData.id]: { guest_wifi: { SSID: { $set: value } } },
|
value={formData.password}
|
||||||
},
|
helpText={HELP_TEXTS.password}
|
||||||
}),
|
error={formErrors.password}
|
||||||
)}
|
required
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
{...props}
|
devices: {
|
||||||
>
|
[formData.id]: {
|
||||||
<div className="input-group-append">
|
guest_wifi: { password: { $set: value } },
|
||||||
<WiFiQRCode
|
},
|
||||||
SSID={formData.SSID}
|
},
|
||||||
password={formData.password}
|
}))}
|
||||||
/>
|
{...props}
|
||||||
</div>
|
/>
|
||||||
</TextInput>
|
</>
|
||||||
|
) : null}
|
||||||
<PasswordInput
|
|
||||||
withEye
|
|
||||||
label={_("Password")}
|
|
||||||
value={formData.password}
|
|
||||||
helpText={HELP_TEXTS.password}
|
|
||||||
error={formErrors.password}
|
|
||||||
required
|
|
||||||
|
|
||||||
onChange={setFormValue(
|
|
||||||
(value) => ({
|
|
||||||
devices: {
|
|
||||||
[formData.id]: {
|
|
||||||
guest_wifi: { password: { $set: value } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)}
|
|
||||||
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,10 @@ import PropTypes from "prop-types";
|
||||||
import { ForisURLs } from "../../utils/forisUrls";
|
import { ForisURLs } from "../../utils/forisUrls";
|
||||||
import { Button } from "../../bootstrap/Button";
|
import { Button } from "../../bootstrap/Button";
|
||||||
import {
|
import {
|
||||||
Modal, ModalBody, ModalFooter, ModalHeader,
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
} from "../../bootstrap/Modal";
|
} from "../../bootstrap/Modal";
|
||||||
import { createAndDownloadPdf, toQRCodeContent } from "./qrCodeHelpers";
|
import { createAndDownloadPdf, toQRCodeContent } from "./qrCodeHelpers";
|
||||||
|
|
||||||
|
@ -36,11 +39,21 @@ export default function WiFiQRCode({ SSID, password }) {
|
||||||
setModal(true);
|
setModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img width="20" src={QR_ICON_PATH} alt="QR" style={{ opacity: 0.67 }} />
|
<img
|
||||||
|
width="20"
|
||||||
|
src={QR_ICON_PATH}
|
||||||
|
alt="QR"
|
||||||
|
style={{ opacity: 0.67 }}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
{modal
|
{modal ? (
|
||||||
? <QRCodeModal setShown={setModal} shown={modal} SSID={SSID} password={password} />
|
<QRCodeModal
|
||||||
: null}
|
setShown={setModal}
|
||||||
|
shown={modal}
|
||||||
|
SSID={SSID}
|
||||||
|
password={password}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -52,9 +65,7 @@ QRCodeModal.propTypes = {
|
||||||
setShown: PropTypes.func.isRequired,
|
setShown: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
function QRCodeModal({
|
function QRCodeModal({ shown, setShown, SSID, password }) {
|
||||||
shown, setShown, SSID, password,
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<Modal setShown={setShown} shown={shown}>
|
<Modal setShown={setShown} shown={shown}>
|
||||||
<ModalHeader setShown={setShown} title={_("Wi-Fi QR Code")} />
|
<ModalHeader setShown={setShown} title={_("Wi-Fi QR Code")} />
|
||||||
|
|
|
@ -19,9 +19,7 @@ WiFiSettings.propTypes = {
|
||||||
hasGuestNetwork: PropTypes.bool,
|
hasGuestNetwork: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function WiFiSettings({
|
export function WiFiSettings({ ws, endpoint, resetEndpoint, hasGuestNetwork }) {
|
||||||
ws, endpoint, resetEndpoint, hasGuestNetwork,
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ForisForm
|
<ForisForm
|
||||||
|
@ -59,35 +57,41 @@ function prepDataToSubmit(formData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!device.guest_wifi.enabled) formData.devices[idx].guest_wifi = { enabled: false };
|
if (!device.guest_wifi.enabled)
|
||||||
|
formData.devices[idx].guest_wifi = { enabled: false };
|
||||||
});
|
});
|
||||||
return formData;
|
return formData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validator(formData) {
|
export function validator(formData) {
|
||||||
const formErrors = formData.devices.map(
|
const formErrors = formData.devices.map((device) => {
|
||||||
(device) => {
|
if (!device.enabled) return {};
|
||||||
if (!device.enabled) return {};
|
|
||||||
|
|
||||||
const errors = {};
|
const errors = {};
|
||||||
if (device.SSID.length > 32) errors.SSID = _("SSID can't be longer than 32 symbols");
|
if (device.SSID.length > 32)
|
||||||
if (device.SSID.length === 0) errors.SSID = _("SSID can't be empty");
|
errors.SSID = _("SSID can't be longer than 32 symbols");
|
||||||
|
if (device.SSID.length === 0) errors.SSID = _("SSID can't be empty");
|
||||||
|
|
||||||
if (device.password.length < 8) errors.password = _("Password must contain at least 8 symbols");
|
if (device.password.length < 8)
|
||||||
|
errors.password = _("Password must contain at least 8 symbols");
|
||||||
|
|
||||||
if (!device.guest_wifi.enabled) return errors;
|
if (!device.guest_wifi.enabled) return errors;
|
||||||
|
|
||||||
const guest_wifi_errors = {};
|
const guest_wifi_errors = {};
|
||||||
if (device.guest_wifi.SSID.length > 32) guest_wifi_errors.SSID = _("SSID can't be longer than 32 symbols");
|
if (device.guest_wifi.SSID.length > 32)
|
||||||
if (device.guest_wifi.SSID.length === 0) guest_wifi_errors.SSID = _("SSID can't be empty");
|
guest_wifi_errors.SSID = _("SSID can't be longer than 32 symbols");
|
||||||
|
if (device.guest_wifi.SSID.length === 0)
|
||||||
|
guest_wifi_errors.SSID = _("SSID can't be empty");
|
||||||
|
|
||||||
if (device.guest_wifi.password.length < 8) guest_wifi_errors.password = _("Password must contain at least 8 symbols");
|
if (device.guest_wifi.password.length < 8)
|
||||||
|
guest_wifi_errors.password = _(
|
||||||
|
"Password must contain at least 8 symbols"
|
||||||
|
);
|
||||||
|
|
||||||
if (guest_wifi_errors.SSID || guest_wifi_errors.password) {
|
if (guest_wifi_errors.SSID || guest_wifi_errors.password) {
|
||||||
errors.guest_wifi = guest_wifi_errors;
|
errors.guest_wifi = guest_wifi_errors;
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
return JSON.stringify(formErrors).match(/\[[{},?]+\]/) ? null : formErrors;
|
return JSON.stringify(formErrors).match(/\[[{},?]+\]/) ? null : formErrors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,19 +22,34 @@ describe("<ResetWiFiSettings/>", () => {
|
||||||
let getAllByText;
|
let getAllByText;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
({ getAllByText } = render(<ResetWiFiSettings ws={webSockets} endpoint={endpoint} />));
|
({ getAllByText } = render(
|
||||||
|
<ResetWiFiSettings ws={webSockets} endpoint={endpoint} />
|
||||||
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should display alert on open ports - success", async () => {
|
it("should display alert on open ports - success", async () => {
|
||||||
fireEvent.click(getAllByText("Reset Wi-Fi Settings")[1]);
|
fireEvent.click(getAllByText("Reset Wi-Fi Settings")[1]);
|
||||||
expect(mockAxios.post).toBeCalledWith(endpoint, undefined, expect.anything());
|
expect(mockAxios.post).toBeCalledWith(
|
||||||
|
endpoint,
|
||||||
|
undefined,
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
mockAxios.mockResponse({ data: { foo: "bar" } });
|
mockAxios.mockResponse({ data: { foo: "bar" } });
|
||||||
await wait(() => expect(mockSetAlert).toBeCalledWith("Wi-Fi settings are set to defaults.", ALERT_TYPES.SUCCESS));
|
await wait(() =>
|
||||||
|
expect(mockSetAlert).toBeCalledWith(
|
||||||
|
"Wi-Fi settings are set to defaults.",
|
||||||
|
ALERT_TYPES.SUCCESS
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should display alert on open ports - failure", async () => {
|
it("should display alert on open ports - failure", async () => {
|
||||||
fireEvent.click(getAllByText("Reset Wi-Fi Settings")[1]);
|
fireEvent.click(getAllByText("Reset Wi-Fi Settings")[1]);
|
||||||
mockJSONError();
|
mockJSONError();
|
||||||
await wait(() => expect(mockSetAlert).toBeCalledWith("An error occurred during resetting Wi-Fi settings."));
|
await wait(() =>
|
||||||
|
expect(mockSetAlert).toBeCalledWith(
|
||||||
|
"An error occurred during resetting Wi-Fi settings."
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,7 +13,12 @@ import { fireEvent, render, wait } from "customTestRender";
|
||||||
import { WebSockets } from "webSockets/WebSockets";
|
import { WebSockets } from "webSockets/WebSockets";
|
||||||
import { mockJSONError } from "testUtils/network";
|
import { mockJSONError } from "testUtils/network";
|
||||||
|
|
||||||
import { wifiSettingsFixture, oneDevice, twoDevices, threeDevices } from "./__fixtures__/wifiSettings";
|
import {
|
||||||
|
wifiSettingsFixture,
|
||||||
|
oneDevice,
|
||||||
|
twoDevices,
|
||||||
|
threeDevices,
|
||||||
|
} from "./__fixtures__/wifiSettings";
|
||||||
import { WiFiSettings, validator } from "../WiFiSettings";
|
import { WiFiSettings, validator } from "../WiFiSettings";
|
||||||
|
|
||||||
describe("<WiFiSettings/>", () => {
|
describe("<WiFiSettings/>", () => {
|
||||||
|
@ -26,7 +31,13 @@ describe("<WiFiSettings/>", () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const webSockets = new WebSockets();
|
const webSockets = new WebSockets();
|
||||||
const renderRes = render(<WiFiSettings ws={webSockets} endpoint={endpoint} resetEndpoint="foo" />);
|
const renderRes = render(
|
||||||
|
<WiFiSettings
|
||||||
|
ws={webSockets}
|
||||||
|
endpoint={endpoint}
|
||||||
|
resetEndpoint="foo"
|
||||||
|
/>
|
||||||
|
);
|
||||||
asFragment = renderRes.asFragment;
|
asFragment = renderRes.asFragment;
|
||||||
getAllByText = renderRes.getAllByText;
|
getAllByText = renderRes.getAllByText;
|
||||||
getAllByLabelText = renderRes.getAllByLabelText;
|
getAllByLabelText = renderRes.getAllByLabelText;
|
||||||
|
@ -38,7 +49,14 @@ describe("<WiFiSettings/>", () => {
|
||||||
|
|
||||||
it("should handle error", async () => {
|
it("should handle error", async () => {
|
||||||
const webSockets = new WebSockets();
|
const webSockets = new WebSockets();
|
||||||
const { getByText } = render(<WiFiSettings ws={webSockets} ws={webSockets} endpoint={endpoint} resetEndpoint="foo" />);
|
const { getByText } = render(
|
||||||
|
<WiFiSettings
|
||||||
|
ws={webSockets}
|
||||||
|
ws={webSockets}
|
||||||
|
endpoint={endpoint}
|
||||||
|
resetEndpoint="foo"
|
||||||
|
/>
|
||||||
|
);
|
||||||
const errorMessage = "An API error occurred.";
|
const errorMessage = "An API error occurred.";
|
||||||
mockJSONError(errorMessage);
|
mockJSONError(errorMessage);
|
||||||
await wait(() => {
|
await wait(() => {
|
||||||
|
@ -51,21 +69,21 @@ describe("<WiFiSettings/>", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Snapshot one module enabled.", () => {
|
it("Snapshot one module enabled.", () => {
|
||||||
fireEvent.click(getAllByText("Enable")[0]);
|
fireEvent.click(getByText("Wi-Fi 1"));
|
||||||
expect(diffSnapshot(firstRender, asFragment())).toMatchSnapshot();
|
expect(diffSnapshot(firstRender, asFragment())).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Snapshot 2.4 GHz", () => {
|
it("Snapshot 2.4 GHz", () => {
|
||||||
fireEvent.click(getAllByText("Enable")[0]);
|
fireEvent.click(getByText("Wi-Fi 1"));
|
||||||
const enabledRender = asFragment();
|
const enabledRender = asFragment();
|
||||||
fireEvent.click(getAllByText("2.4")[0]);
|
fireEvent.click(getAllByText("2.4")[0]);
|
||||||
expect(diffSnapshot(enabledRender, asFragment())).toMatchSnapshot();
|
expect(diffSnapshot(enabledRender, asFragment())).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Snapshot guest network.", () => {
|
it("Snapshot guest network.", () => {
|
||||||
fireEvent.click(getAllByText("Enable")[0]);
|
fireEvent.click(getByText("Wi-Fi 1"));
|
||||||
const enabledRender = asFragment();
|
const enabledRender = asFragment();
|
||||||
fireEvent.click(getAllByText("Enable Guest Wifi")[0]);
|
fireEvent.click(getAllByText("Enable Guest Wi-Fi")[0]);
|
||||||
expect(diffSnapshot(enabledRender, asFragment())).toMatchSnapshot();
|
expect(diffSnapshot(enabledRender, asFragment())).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -78,11 +96,15 @@ describe("<WiFiSettings/>", () => {
|
||||||
{ enabled: false, id: 1 },
|
{ enabled: false, id: 1 },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
expect(mockAxios.post).toHaveBeenCalledWith(endpoint, data, expect.anything());
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
||||||
|
endpoint,
|
||||||
|
data,
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Post form: one module enabled.", () => {
|
it("Post form: one module enabled.", () => {
|
||||||
fireEvent.click(getAllByText("Enable")[0]);
|
fireEvent.click(getByText("Wi-Fi 1"));
|
||||||
|
|
||||||
fireEvent.click(getByText("Save"));
|
fireEvent.click(getByText("Save"));
|
||||||
expect(mockAxios.post).toBeCalled();
|
expect(mockAxios.post).toBeCalled();
|
||||||
|
@ -102,11 +124,15 @@ describe("<WiFiSettings/>", () => {
|
||||||
{ enabled: false, id: 1 },
|
{ enabled: false, id: 1 },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
expect(mockAxios.post).toHaveBeenCalledWith(endpoint, data, expect.anything());
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
||||||
|
endpoint,
|
||||||
|
data,
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Post form: 2.4 GHz", () => {
|
it("Post form: 2.4 GHz", () => {
|
||||||
fireEvent.click(getAllByText("Enable")[0]);
|
fireEvent.click(getByText("Wi-Fi 1"));
|
||||||
fireEvent.click(getAllByText("2.4")[0]);
|
fireEvent.click(getAllByText("2.4")[0]);
|
||||||
|
|
||||||
fireEvent.click(getByText("Save"));
|
fireEvent.click(getByText("Save"));
|
||||||
|
@ -127,13 +153,19 @@ describe("<WiFiSettings/>", () => {
|
||||||
{ enabled: false, id: 1 },
|
{ enabled: false, id: 1 },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
expect(mockAxios.post).toHaveBeenCalledWith(endpoint, data, expect.anything());
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
||||||
|
endpoint,
|
||||||
|
data,
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Post form: guest network.", () => {
|
it("Post form: guest network.", () => {
|
||||||
fireEvent.click(getAllByText("Enable")[0]);
|
fireEvent.click(getByText("Wi-Fi 1"));
|
||||||
fireEvent.click(getAllByText("Enable Guest Wifi")[0]);
|
fireEvent.click(getAllByText("Enable Guest Wi-Fi")[0]);
|
||||||
fireEvent.change(getAllByLabelText("Password")[1], { target: { value: "test_password" } });
|
fireEvent.change(getAllByLabelText("Password")[1], {
|
||||||
|
target: { value: "test_password" },
|
||||||
|
});
|
||||||
|
|
||||||
fireEvent.click(getByText("Save"));
|
fireEvent.click(getByText("Save"));
|
||||||
expect(mockAxios.post).toBeCalled();
|
expect(mockAxios.post).toBeCalled();
|
||||||
|
@ -157,7 +189,11 @@ describe("<WiFiSettings/>", () => {
|
||||||
{ enabled: false, id: 1 },
|
{ enabled: false, id: 1 },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
expect(mockAxios.post).toHaveBeenCalledWith(endpoint, data, expect.anything());
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
||||||
|
endpoint,
|
||||||
|
data,
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Validator function using regex for one device", () => {
|
it("Validator function using regex for one device", () => {
|
||||||
|
@ -165,12 +201,16 @@ describe("<WiFiSettings/>", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Validator function using regex for two devices", () => {
|
it("Validator function using regex for two devices", () => {
|
||||||
const twoDevicesFormErrors = [{SSID: "SSID can't be empty"}, {}];
|
const twoDevicesFormErrors = [{ SSID: "SSID can't be empty" }, {}];
|
||||||
expect(validator(twoDevices)).toEqual(twoDevicesFormErrors);
|
expect(validator(twoDevices)).toEqual(twoDevicesFormErrors);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Validator function using regex for three devices", () => {
|
it("Validator function using regex for three devices", () => {
|
||||||
const threeDevicesFormErrors = [{}, {}, {password: "Password must contain at least 8 symbols"}];
|
const threeDevicesFormErrors = [
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{ password: "Password must contain at least 8 symbols" },
|
||||||
|
];
|
||||||
expect(validator(threeDevices)).toEqual(threeDevicesFormErrors);
|
expect(validator(threeDevices)).toEqual(threeDevicesFormErrors);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -292,11 +292,7 @@ export function wifiSettingsFixture() {
|
||||||
radar: false,
|
radar: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
available_htmodes: [
|
available_htmodes: ["NOHT", "HT20", "HT40"],
|
||||||
"NOHT",
|
|
||||||
"HT20",
|
|
||||||
"HT40",
|
|
||||||
],
|
|
||||||
hwmode: "11g",
|
hwmode: "11g",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -327,9 +323,9 @@ const oneDevice = {
|
||||||
htmode: "HT40",
|
htmode: "HT40",
|
||||||
hwmode: "11a",
|
hwmode: "11a",
|
||||||
id: 0,
|
id: 0,
|
||||||
password: "TestPass"
|
password: "TestPass",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const twoDevices = {
|
const twoDevices = {
|
||||||
|
@ -343,7 +339,7 @@ const twoDevices = {
|
||||||
htmode: "HT40",
|
htmode: "HT40",
|
||||||
hwmode: "11a",
|
hwmode: "11a",
|
||||||
id: 0,
|
id: 0,
|
||||||
password: "TestPass"
|
password: "TestPass",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
SSID: "Turris2",
|
SSID: "Turris2",
|
||||||
|
@ -354,9 +350,9 @@ const twoDevices = {
|
||||||
htmode: "HT40",
|
htmode: "HT40",
|
||||||
hwmode: "11a",
|
hwmode: "11a",
|
||||||
id: 0,
|
id: 0,
|
||||||
password: "TestPass"
|
password: "TestPass",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const threeDevices = {
|
const threeDevices = {
|
||||||
|
@ -370,7 +366,7 @@ const threeDevices = {
|
||||||
htmode: "HT40",
|
htmode: "HT40",
|
||||||
hwmode: "11a",
|
hwmode: "11a",
|
||||||
id: 0,
|
id: 0,
|
||||||
password: "TestPass"
|
password: "TestPass",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
SSID: "Turris2",
|
SSID: "Turris2",
|
||||||
|
@ -381,7 +377,7 @@ const threeDevices = {
|
||||||
htmode: "HT40",
|
htmode: "HT40",
|
||||||
hwmode: "11a",
|
hwmode: "11a",
|
||||||
id: 0,
|
id: 0,
|
||||||
password: "TestPass"
|
password: "TestPass",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
SSID: "Turris3",
|
SSID: "Turris3",
|
||||||
|
@ -392,9 +388,9 @@ const threeDevices = {
|
||||||
htmode: "HT40",
|
htmode: "HT40",
|
||||||
hwmode: "11a",
|
hwmode: "11a",
|
||||||
id: 0,
|
id: 0,
|
||||||
password: ""
|
password: "",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export {oneDevice, twoDevices, threeDevices};
|
export { oneDevice, twoDevices, threeDevices };
|
||||||
|
|
|
@ -5,7 +5,7 @@ exports[`<WiFiSettings/> Snapshot 2.4 GHz 1`] = `
|
||||||
- First value
|
- First value
|
||||||
+ Second value
|
+ Second value
|
||||||
|
|
||||||
@@ -246,207 +246,95 @@
|
@@ -245,207 +245,95 @@
|
||||||
value=\\"0\\"
|
value=\\"0\\"
|
||||||
>
|
>
|
||||||
auto
|
auto
|
||||||
|
@ -251,17 +251,14 @@ exports[`<WiFiSettings/> Snapshot 2.4 GHz 1`] = `
|
||||||
exports[`<WiFiSettings/> Snapshot both modules disabled. 1`] = `
|
exports[`<WiFiSettings/> Snapshot both modules disabled. 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="col-sm-12 offset-lg-1 col-lg-10 p-0 mb-3"
|
class="card p-4 col-sm-12 col-lg-12 p-0 mb-3"
|
||||||
>
|
>
|
||||||
<form>
|
<form>
|
||||||
<h3>
|
|
||||||
Wi-Fi 1
|
|
||||||
</h3>
|
|
||||||
<div
|
<div
|
||||||
class="form-group"
|
class="form-group"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="custom-control custom-checkbox "
|
class="custom-control custom-switch custom-control-inline switch-heading"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="custom-control-input"
|
class="custom-control-input"
|
||||||
|
@ -272,18 +269,18 @@ exports[`<WiFiSettings/> Snapshot both modules disabled. 1`] = `
|
||||||
class="custom-control-label"
|
class="custom-control-label"
|
||||||
for="1"
|
for="1"
|
||||||
>
|
>
|
||||||
Enable
|
<h2>
|
||||||
|
Wi-Fi 1
|
||||||
|
</h2>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3>
|
<hr />
|
||||||
Wi-Fi 2
|
|
||||||
</h3>
|
|
||||||
<div
|
<div
|
||||||
class="form-group"
|
class="form-group"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="custom-control custom-checkbox "
|
class="custom-control custom-switch custom-control-inline switch-heading"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="custom-control-input"
|
class="custom-control-input"
|
||||||
|
@ -294,7 +291,9 @@ exports[`<WiFiSettings/> Snapshot both modules disabled. 1`] = `
|
||||||
class="custom-control-label"
|
class="custom-control-label"
|
||||||
for="2"
|
for="2"
|
||||||
>
|
>
|
||||||
Enable
|
<h2>
|
||||||
|
Wi-Fi 2
|
||||||
|
</h2>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -302,17 +301,17 @@ exports[`<WiFiSettings/> Snapshot both modules disabled. 1`] = `
|
||||||
class="text-right"
|
class="text-right"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary col-sm-12 col-lg-3"
|
class="btn btn-primary col-sm-12 col-md-3 col-lg-2"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<h4>
|
<h2>
|
||||||
Reset Wi-Fi Settings
|
Reset Wi-Fi Settings
|
||||||
</h4>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
If a number of wireless cards doesn't match, you may try to reset the Wi-Fi settings. Note that this will remove the
|
If a number of wireless cards doesn't match, you may try to reset the Wi-Fi settings. Note that this will remove the
|
||||||
|
@ -320,13 +319,13 @@ current Wi-Fi configuration and restore the default values.
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
class="col-sm-12 offset-lg-1 col-lg-10 p-0 mb-3 text-right"
|
class="col-sm-12 col-lg-12 p-0 mb-3 text-right"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="btn btn-warning col-sm-12 col-lg-3"
|
class="btn btn-warning col-sm-12 col-md-3 col-lg-2"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Reset Wi-Fi Settings
|
Reset Wi-Fi Settings
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFragment>
|
</DocumentFragment>
|
||||||
|
@ -337,10 +336,10 @@ exports[`<WiFiSettings/> Snapshot guest network. 1`] = `
|
||||||
- First value
|
- First value
|
||||||
+ Second value
|
+ Second value
|
||||||
|
|
||||||
@@ -475,10 +475,89 @@
|
@@ -474,10 +474,89 @@
|
||||||
|
Parameters of the guest network can be set in the Guest network tab.
|
||||||
|
|
||||||
</small>
|
</small>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
+ <div
|
+ <div
|
||||||
|
@ -422,21 +421,21 @@ exports[`<WiFiSettings/> Snapshot guest network. 1`] = `
|
||||||
+
|
+
|
||||||
+ </small>
|
+ </small>
|
||||||
+ </div>
|
+ </div>
|
||||||
<h3>
|
<hr />
|
||||||
Wi-Fi 2
|
|
||||||
</h3>
|
|
||||||
<div
|
<div
|
||||||
class=\\"form-group\\"
|
class=\\"form-group\\"
|
||||||
@@ -502,10 +581,11 @@
|
>
|
||||||
|
<div
|
||||||
|
@@ -501,10 +580,11 @@
|
||||||
<div
|
<div
|
||||||
class=\\"text-right\\"
|
class=\\"text-right\\"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class=\\"btn btn-primary col-sm-12 col-lg-3\\"
|
class=\\"btn btn-primary col-sm-12 col-md-3 col-lg-2\\"
|
||||||
+ disabled=\\"\\"
|
+ disabled=\\"\\"
|
||||||
type=\\"submit\\"
|
type=\\"submit\\"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>"
|
</div>"
|
||||||
`;
|
`;
|
||||||
|
@ -446,9 +445,9 @@ exports[`<WiFiSettings/> Snapshot one module enabled. 1`] = `
|
||||||
- First value
|
- First value
|
||||||
+ Second value
|
+ Second value
|
||||||
|
|
||||||
@@ -23,10 +23,462 @@
|
@@ -22,10 +22,462 @@
|
||||||
>
|
Wi-Fi 1
|
||||||
Enable
|
</h2>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -880,7 +879,7 @@ exports[`<WiFiSettings/> Snapshot one module enabled. 1`] = `
|
||||||
+ class=\\"form-group\\"
|
+ class=\\"form-group\\"
|
||||||
+ >
|
+ >
|
||||||
+ <div
|
+ <div
|
||||||
+ class=\\"custom-control custom-checkbox \\"
|
+ class=\\"custom-control custom-switch\\"
|
||||||
+ >
|
+ >
|
||||||
+ <input
|
+ <input
|
||||||
+ class=\\"custom-control-input\\"
|
+ class=\\"custom-control-input\\"
|
||||||
|
@ -891,22 +890,22 @@ exports[`<WiFiSettings/> Snapshot one module enabled. 1`] = `
|
||||||
+ class=\\"custom-control-label\\"
|
+ class=\\"custom-control-label\\"
|
||||||
+ for=\\"10\\"
|
+ for=\\"10\\"
|
||||||
+ >
|
+ >
|
||||||
+ Enable Guest Wifi
|
+ Enable Guest Wi-Fi
|
||||||
+ <small
|
+ </label>
|
||||||
+ class=\\"form-text text-muted\\"
|
+ <small
|
||||||
+ >
|
+ class=\\"form-text text-muted mt-0 mb-3\\"
|
||||||
|
+ >
|
||||||
+
|
+
|
||||||
+ Enables Wi-Fi for guests, which is separated from LAN network. Devices connected to this network are allowed to
|
+ Enables Wi-Fi for guests, which is separated from LAN network. Devices connected to this network are allowed to
|
||||||
+ access the internet, but aren't allowed to access other devices and the configuration interface of the router.
|
+ access the internet, but aren't allowed to access other devices and the configuration interface of the router.
|
||||||
+ Parameters of the guest network can be set in the Guest network tab.
|
+ Parameters of the guest network can be set in the Guest network tab.
|
||||||
+
|
+
|
||||||
+ </small>
|
+ </small>
|
||||||
+ </label>
|
|
||||||
+ </div>
|
+ </div>
|
||||||
+ </div>
|
+ </div>
|
||||||
<h3>
|
<hr />
|
||||||
Wi-Fi 2
|
|
||||||
</h3>
|
|
||||||
<div
|
<div
|
||||||
class=\\"form-group\\""
|
class=\\"form-group\\"
|
||||||
|
>
|
||||||
|
<div"
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -22,7 +22,9 @@ export const HELP_TEXTS = {
|
||||||
password: _(`
|
password: _(`
|
||||||
WPA2 pre-shared key, that is required to connect to the network.
|
WPA2 pre-shared key, that is required to connect to the network.
|
||||||
`),
|
`),
|
||||||
hidden: _("If set, network is not visible when scanning for available networks."),
|
hidden: _(
|
||||||
|
"If set, network is not visible when scanning for available networks."
|
||||||
|
),
|
||||||
hwmode: _(`
|
hwmode: _(`
|
||||||
The 2.4 GHz band is more widely supported by clients, but tends to have more interference. The 5 GHz band is a
|
The 2.4 GHz band is more widely supported by clients, but tends to have more interference. The 5 GHz band is a
|
||||||
newer standard and may not be supported by all your devices. It usually has less interference, but the signal
|
newer standard and may not be supported by all your devices. It usually has less interference, but the signal
|
||||||
|
|
|
@ -8,7 +8,11 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fireEvent, getByText, queryByText, render, wait,
|
fireEvent,
|
||||||
|
getByText,
|
||||||
|
queryByText,
|
||||||
|
render,
|
||||||
|
wait,
|
||||||
} from "customTestRender";
|
} from "customTestRender";
|
||||||
import mockAxios from "jest-mock-axios";
|
import mockAxios from "jest-mock-axios";
|
||||||
import { mockJSONError } from "testUtils/network";
|
import { mockJSONError } from "testUtils/network";
|
||||||
|
@ -19,38 +23,41 @@ import { RebootButton } from "../RebootButton";
|
||||||
describe("<RebootButton/>", () => {
|
describe("<RebootButton/>", () => {
|
||||||
let componentContainer;
|
let componentContainer;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const { container } = render(<>
|
const { container } = render(
|
||||||
<div id="modal-container" />
|
<>
|
||||||
<RebootButton />
|
<div id="modal-container" />
|
||||||
</>);
|
<RebootButton />
|
||||||
|
</>
|
||||||
|
);
|
||||||
componentContainer = container;
|
componentContainer = container;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Render.", () => {
|
it("Render.", () => {
|
||||||
expect(componentContainer)
|
expect(componentContainer).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Render modal.", () => {
|
it("Render modal.", () => {
|
||||||
expect(queryByText(componentContainer, "Confirm reboot"))
|
expect(queryByText(componentContainer, "Confirm reboot")).toBeNull();
|
||||||
.toBeNull();
|
|
||||||
fireEvent.click(getByText(componentContainer, "Reboot"));
|
fireEvent.click(getByText(componentContainer, "Reboot"));
|
||||||
expect(componentContainer)
|
expect(componentContainer).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Confirm reboot.", () => {
|
it("Confirm reboot.", () => {
|
||||||
fireEvent.click(getByText(componentContainer, "Reboot"));
|
fireEvent.click(getByText(componentContainer, "Reboot"));
|
||||||
fireEvent.click(getByText(componentContainer, "Confirm reboot"));
|
fireEvent.click(getByText(componentContainer, "Confirm reboot"));
|
||||||
expect(mockAxios.post)
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
||||||
.toHaveBeenCalledWith("/reforis/api/reboot", undefined, expect.anything());
|
"/reforis/api/reboot",
|
||||||
|
undefined,
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Hold error.", async () => {
|
it("Hold error.", async () => {
|
||||||
fireEvent.click(getByText(componentContainer, "Reboot"));
|
fireEvent.click(getByText(componentContainer, "Reboot"));
|
||||||
fireEvent.click(getByText(componentContainer, "Confirm reboot"));
|
fireEvent.click(getByText(componentContainer, "Confirm reboot"));
|
||||||
mockJSONError();
|
mockJSONError();
|
||||||
await wait(() => expect(mockSetAlert)
|
await wait(() =>
|
||||||
.toBeCalledWith("Reboot request failed."));
|
expect(mockSetAlert).toBeCalledWith("Reboot request failed.")
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -49,16 +49,12 @@ exports[`<RebootButton/> Render modal. 1`] = `
|
||||||
class="btn btn-primary "
|
class="btn btn-primary "
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
||||||
Confirm reboot
|
Confirm reboot
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,8 +66,6 @@ exports[`<RebootButton/> Render modal. 1`] = `
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
||||||
Reboot
|
Reboot
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -86,8 +80,6 @@ exports[`<RebootButton/> Render. 1`] = `
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
||||||
Reboot
|
Reboot
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,17 +13,14 @@ import { STATES, SubmitButton } from "../components/SubmitButton";
|
||||||
describe("<SubmitButton/>", () => {
|
describe("<SubmitButton/>", () => {
|
||||||
it("Render ready", () => {
|
it("Render ready", () => {
|
||||||
const { container } = render(<SubmitButton state={STATES.READY} />);
|
const { container } = render(<SubmitButton state={STATES.READY} />);
|
||||||
expect(container)
|
expect(container).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
it("Render saving", () => {
|
it("Render saving", () => {
|
||||||
const { container } = render(<SubmitButton state={STATES.SAVING} />);
|
const { container } = render(<SubmitButton state={STATES.SAVING} />);
|
||||||
expect(container)
|
expect(container).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
it("Render load", () => {
|
it("Render load", () => {
|
||||||
const { container } = render(<SubmitButton state={STATES.LOAD} />);
|
const { container } = render(<SubmitButton state={STATES.LOAD} />);
|
||||||
expect(container)
|
expect(container).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
exports[`<SubmitButton/> Render load 1`] = `
|
exports[`<SubmitButton/> Render load 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary col-sm-12 col-lg-3"
|
class="btn btn-primary col-sm-12 col-md-3 col-lg-2"
|
||||||
disabled=""
|
disabled=""
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
|
@ -13,8 +13,6 @@ exports[`<SubmitButton/> Render load 1`] = `
|
||||||
role="status"
|
role="status"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Load settings
|
Load settings
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,11 +21,9 @@ exports[`<SubmitButton/> Render load 1`] = `
|
||||||
exports[`<SubmitButton/> Render ready 1`] = `
|
exports[`<SubmitButton/> Render ready 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary col-sm-12 col-lg-3"
|
class="btn btn-primary col-sm-12 col-md-3 col-lg-2"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,7 +32,7 @@ exports[`<SubmitButton/> Render ready 1`] = `
|
||||||
exports[`<SubmitButton/> Render saving 1`] = `
|
exports[`<SubmitButton/> Render saving 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary col-sm-12 col-lg-3"
|
class="btn btn-primary col-sm-12 col-md-3 col-lg-2"
|
||||||
disabled=""
|
disabled=""
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
|
@ -46,8 +42,6 @@ exports[`<SubmitButton/> Render saving 1`] = `
|
||||||
role="status"
|
role="status"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Updating
|
Updating
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,9 +7,7 @@
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import {
|
import { act, fireEvent, render, waitForElement } from "customTestRender";
|
||||||
act, fireEvent, render, waitForElement,
|
|
||||||
} from "customTestRender";
|
|
||||||
import mockAxios from "jest-mock-axios";
|
import mockAxios from "jest-mock-axios";
|
||||||
import { WebSockets } from "webSockets/WebSockets";
|
import { WebSockets } from "webSockets/WebSockets";
|
||||||
import { ForisForm } from "../components/ForisForm";
|
import { ForisForm } from "../components/ForisForm";
|
||||||
|
@ -38,8 +36,12 @@ describe("useForm hook.", () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockPrepData = jest.fn(() => ({ field: "preparedData" }));
|
mockPrepData = jest.fn(() => ({ field: "preparedData" }));
|
||||||
mockPrepDataToSubmit = jest.fn(() => ({ field: "preparedDataToSubmit" }));
|
mockPrepDataToSubmit = jest.fn(() => ({
|
||||||
mockValidator = jest.fn((data) => (data.field === "invalidValue" ? { field: "Error" } : {}));
|
field: "preparedDataToSubmit",
|
||||||
|
}));
|
||||||
|
mockValidator = jest.fn((data) =>
|
||||||
|
data.field === "invalidValue" ? { field: "Error" } : {}
|
||||||
|
);
|
||||||
const { getByTestId, container } = render(
|
const { getByTestId, container } = render(
|
||||||
<ForisForm
|
<ForisForm
|
||||||
ws={new WebSockets()}
|
ws={new WebSockets()}
|
||||||
|
@ -53,7 +55,7 @@ describe("useForm hook.", () => {
|
||||||
validator={mockValidator}
|
validator={mockValidator}
|
||||||
>
|
>
|
||||||
<Child />
|
<Child />
|
||||||
</ForisForm>,
|
</ForisForm>
|
||||||
);
|
);
|
||||||
mockAxios.mockResponse({ field: "fetchedData" });
|
mockAxios.mockResponse({ field: "fetchedData" });
|
||||||
|
|
||||||
|
@ -67,16 +69,22 @@ describe("useForm hook.", () => {
|
||||||
expect(Child.mock.calls[0][0].formErrors).toMatchObject({});
|
expect(Child.mock.calls[0][0].formErrors).toMatchObject({});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
fireEvent.change(input, { target: { value: "invalidValue", type: "text" } });
|
fireEvent.change(input, {
|
||||||
|
target: { value: "invalidValue", type: "text" },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(Child).toHaveBeenCalledTimes(2);
|
expect(Child).toHaveBeenCalledTimes(2);
|
||||||
expect(mockValidator).toHaveBeenCalledTimes(2);
|
expect(mockValidator).toHaveBeenCalledTimes(2);
|
||||||
expect(Child.mock.calls[1][0].formErrors).toMatchObject({ field: "Error" });
|
expect(Child.mock.calls[1][0].formErrors).toMatchObject({
|
||||||
|
field: "Error",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Update text value.", () => {
|
it("Update text value.", () => {
|
||||||
fireEvent.change(input, { target: { value: "newValue", type: "text" } });
|
fireEvent.change(input, {
|
||||||
|
target: { value: "newValue", type: "text" },
|
||||||
|
});
|
||||||
expect(input.value).toBe("newValue");
|
expect(input.value).toBe("newValue");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -86,14 +94,21 @@ describe("useForm hook.", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Update checkbox value.", () => {
|
it("Update checkbox value.", () => {
|
||||||
fireEvent.change(input, { target: { checked: true, type: "checkbox" } });
|
fireEvent.change(input, {
|
||||||
|
target: { checked: true, type: "checkbox" },
|
||||||
|
});
|
||||||
expect(input.checked).toBe(true);
|
expect(input.checked).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Fetch data.", () => {
|
it("Fetch data.", () => {
|
||||||
expect(mockAxios.get).toHaveBeenCalledWith("testEndpoint", expect.anything());
|
expect(mockAxios.get).toHaveBeenCalledWith(
|
||||||
|
"testEndpoint",
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
expect(mockPrepData).toHaveBeenCalledTimes(1);
|
expect(mockPrepData).toHaveBeenCalledTimes(1);
|
||||||
expect(Child.mock.calls[0][0].formData).toMatchObject({ field: "preparedData" });
|
expect(Child.mock.calls[0][0].formData).toMatchObject({
|
||||||
|
field: "preparedData",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Submit.", () => {
|
it("Submit.", () => {
|
||||||
|
@ -107,7 +122,7 @@ describe("useForm hook.", () => {
|
||||||
expect(mockAxios.post).toHaveBeenCalledWith(
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
||||||
"testEndpoint",
|
"testEndpoint",
|
||||||
{ field: "preparedDataToSubmit" },
|
{ field: "preparedDataToSubmit" },
|
||||||
expect.anything(),
|
expect.anything()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,120 +16,75 @@ import {
|
||||||
|
|
||||||
describe("Validation functions", () => {
|
describe("Validation functions", () => {
|
||||||
it("validateIPv4Address valid", () => {
|
it("validateIPv4Address valid", () => {
|
||||||
expect(validateIPv4Address("192.168.1.1"))
|
expect(validateIPv4Address("192.168.1.1")).toBe(undefined);
|
||||||
.toBe(undefined);
|
expect(validateIPv4Address("1.1.1.1")).toBe(undefined);
|
||||||
expect(validateIPv4Address("1.1.1.1"))
|
expect(validateIPv4Address("0.0.0.0")).toBe(undefined);
|
||||||
.toBe(undefined);
|
|
||||||
expect(validateIPv4Address("0.0.0.0"))
|
|
||||||
.toBe(undefined);
|
|
||||||
});
|
});
|
||||||
it("validateIPv4Address invalid", () => {
|
it("validateIPv4Address invalid", () => {
|
||||||
expect(validateIPv4Address("invalid"))
|
expect(validateIPv4Address("invalid")).not.toBe(undefined);
|
||||||
.not
|
expect(validateIPv4Address("192.256.1.1")).not.toBe(undefined);
|
||||||
.toBe(undefined);
|
expect(validateIPv4Address("192.168.256.1")).not.toBe(undefined);
|
||||||
expect(validateIPv4Address("192.256.1.1"))
|
expect(validateIPv4Address("192.168.1.256")).not.toBe(undefined);
|
||||||
.not
|
expect(validateIPv4Address("192.168.1.256")).not.toBe(undefined);
|
||||||
.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", () => {
|
it("validateIPv6Address valid", () => {
|
||||||
expect(validateIPv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:7334"))
|
expect(
|
||||||
.toBe(undefined);
|
validateIPv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
|
||||||
expect(validateIPv6Address("0:0:0:0:0:0:0:1"))
|
).toBe(undefined);
|
||||||
.toBe(undefined);
|
expect(validateIPv6Address("0:0:0:0:0:0:0:1")).toBe(undefined);
|
||||||
expect(validateIPv6Address("::1"))
|
expect(validateIPv6Address("::1")).toBe(undefined);
|
||||||
.toBe(undefined);
|
expect(validateIPv6Address("::")).toBe(undefined);
|
||||||
expect(validateIPv6Address("::"))
|
|
||||||
.toBe(undefined);
|
|
||||||
});
|
});
|
||||||
it("validateIPv6Address invalid", () => {
|
it("validateIPv6Address invalid", () => {
|
||||||
expect(validateIPv6Address("invalid"))
|
expect(validateIPv6Address("invalid")).not.toBe(undefined);
|
||||||
.not
|
expect(validateIPv6Address("1.1.1.1")).not.toBe(undefined);
|
||||||
.toBe(undefined);
|
expect(validateIPv6Address("1200::AB00:1234::2552:7777:1313")).not.toBe(
|
||||||
expect(validateIPv6Address("1.1.1.1"))
|
undefined
|
||||||
.not
|
);
|
||||||
.toBe(undefined);
|
expect(
|
||||||
expect(validateIPv6Address("1200::AB00:1234::2552:7777:1313"))
|
validateIPv6Address("1200:0000:AB00:1234:O000:2552:7777:1313")
|
||||||
.not
|
).not.toBe(undefined);
|
||||||
.toBe(undefined);
|
|
||||||
expect(validateIPv6Address("1200:0000:AB00:1234:O000:2552:7777:1313"))
|
|
||||||
.not
|
|
||||||
.toBe(undefined);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("validateIPv6Prefix valid", () => {
|
it("validateIPv6Prefix valid", () => {
|
||||||
expect(validateIPv6Prefix("2002:0000::/16"))
|
expect(validateIPv6Prefix("2002:0000::/16")).toBe(undefined);
|
||||||
.toBe(undefined);
|
expect(validateIPv6Prefix("0::/0")).toBe(undefined);
|
||||||
expect(validateIPv6Prefix("0::/0"))
|
|
||||||
.toBe(undefined);
|
|
||||||
});
|
});
|
||||||
it("validateIPv6Prefix invalid", () => {
|
it("validateIPv6Prefix invalid", () => {
|
||||||
expect(validateIPv6Prefix("2001:0db8:85a3:0000:0000:8a2e:0370:7334"))
|
expect(
|
||||||
.not
|
validateIPv6Prefix("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
|
||||||
.toBe(undefined);
|
).not.toBe(undefined);
|
||||||
expect(validateIPv6Prefix("::1"))
|
expect(validateIPv6Prefix("::1")).not.toBe(undefined);
|
||||||
.not
|
expect(validateIPv6Prefix("2002:0000::/999")).not.toBe(undefined);
|
||||||
.toBe(undefined);
|
|
||||||
expect(validateIPv6Prefix("2002:0000::/999"))
|
|
||||||
.not
|
|
||||||
.toBe(undefined);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("validateDomain valid", () => {
|
it("validateDomain valid", () => {
|
||||||
expect(validateDomain("example.com"))
|
expect(validateDomain("example.com")).toBe(undefined);
|
||||||
.toBe(undefined);
|
expect(validateDomain("one.two.three")).toBe(undefined);
|
||||||
expect(validateDomain("one.two.three"))
|
|
||||||
.toBe(undefined);
|
|
||||||
});
|
});
|
||||||
it("validateDomain invalid", () => {
|
it("validateDomain invalid", () => {
|
||||||
expect(validateDomain("test/"))
|
expect(validateDomain("test/")).not.toBe(undefined);
|
||||||
.not
|
expect(validateDomain(".")).not.toBe(undefined);
|
||||||
.toBe(undefined);
|
|
||||||
expect(validateDomain("."))
|
|
||||||
.not
|
|
||||||
.toBe(undefined);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("validateDUID valid", () => {
|
it("validateDUID valid", () => {
|
||||||
expect(validateDUID("abcdefAB"))
|
expect(validateDUID("abcdefAB")).toBe(undefined);
|
||||||
.toBe(undefined);
|
expect(validateDUID("ABCDEF12")).toBe(undefined);
|
||||||
expect(validateDUID("ABCDEF12"))
|
expect(validateDUID("ABCDEF12AB")).toBe(undefined);
|
||||||
.toBe(undefined);
|
|
||||||
expect(validateDUID("ABCDEF12AB"))
|
|
||||||
.toBe(undefined);
|
|
||||||
});
|
});
|
||||||
it("validateDUID invalid", () => {
|
it("validateDUID invalid", () => {
|
||||||
expect(validateDUID("gggggggg"))
|
expect(validateDUID("gggggggg")).not.toBe(undefined);
|
||||||
.not
|
expect(validateDUID("abcdefABa")).not.toBe(undefined);
|
||||||
.toBe(undefined);
|
|
||||||
expect(validateDUID("abcdefABa"))
|
|
||||||
.not
|
|
||||||
.toBe(undefined);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("validateMAC valid", () => {
|
it("validateMAC valid", () => {
|
||||||
expect(validateMAC("00:D0:56:F2:B5:12"))
|
expect(validateMAC("00:D0:56:F2:B5:12")).toBe(undefined);
|
||||||
.toBe(undefined);
|
expect(validateMAC("00:26:DD:14:C4:EE")).toBe(undefined);
|
||||||
expect(validateMAC("00:26:DD:14:C4:EE"))
|
expect(validateMAC("06:00:00:00:00:00")).toBe(undefined);
|
||||||
.toBe(undefined);
|
|
||||||
expect(validateMAC("06:00:00:00:00:00"))
|
|
||||||
.toBe(undefined);
|
|
||||||
});
|
});
|
||||||
it("validateMAC invalid", () => {
|
it("validateMAC invalid", () => {
|
||||||
expect(validateMAC("00:00:00:00:00:0G"))
|
expect(validateMAC("00:00:00:00:00:0G")).not.toBe(undefined);
|
||||||
.not
|
expect(validateMAC("06:00:00:00:00:00:00")).not.toBe(undefined);
|
||||||
.toBe(undefined);
|
|
||||||
expect(validateMAC("06:00:00:00:00:00:00"))
|
|
||||||
.not
|
|
||||||
.toBe(undefined);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -52,19 +52,25 @@ ForisForm.propTypes = {
|
||||||
onSubmitOverridden: PropTypes.func,
|
onSubmitOverridden: PropTypes.func,
|
||||||
/** Reference to actual form element (useful for programmatically submitting it).
|
/** Reference to actual form element (useful for programmatically submitting it).
|
||||||
* Pass the output of useRef hook to this prop.
|
* Pass the output of useRef hook to this prop.
|
||||||
*/
|
*/
|
||||||
formReference: PropTypes.object,
|
formReference: PropTypes.object,
|
||||||
/** reForis form components. */
|
/** reForis form components. */
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
|
|
||||||
// eslint-disable-next-line react/no-unused-prop-types
|
// eslint-disable-next-line react/no-unused-prop-types
|
||||||
customWSProp(props) {
|
customWSProp(props) {
|
||||||
const wsModuleIsSpecified = !!(props.forisConfig && props.forisConfig.wsModule);
|
const wsModuleIsSpecified = !!(
|
||||||
|
props.forisConfig && props.forisConfig.wsModule
|
||||||
|
);
|
||||||
if (props.ws && !wsModuleIsSpecified) {
|
if (props.ws && !wsModuleIsSpecified) {
|
||||||
return new Error("forisConfig.wsModule should be specified when ws object is passed.");
|
return new Error(
|
||||||
|
"forisConfig.wsModule should be specified when ws object is passed."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!props.ws && wsModuleIsSpecified) {
|
if (!props.ws && wsModuleIsSpecified) {
|
||||||
return new Error("forisConfig.wsModule is specified without passing ws object.");
|
return new Error(
|
||||||
|
"forisConfig.wsModule is specified without passing ws object."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -95,7 +101,10 @@ export function ForisForm({
|
||||||
formReference,
|
formReference,
|
||||||
children,
|
children,
|
||||||
}) {
|
}) {
|
||||||
const [formState, onFormChangeHandler, resetFormData] = useForm(validator, prepData);
|
const [formState, onFormChangeHandler, resetFormData] = useForm(
|
||||||
|
validator,
|
||||||
|
prepData
|
||||||
|
);
|
||||||
const [setAlert, dismissAlert] = useAlert();
|
const [setAlert, dismissAlert] = useAlert();
|
||||||
|
|
||||||
const [forisModuleState] = useForisModule(ws, forisConfig);
|
const [forisModuleState] = useForisModule(ws, forisConfig);
|
||||||
|
@ -141,29 +150,39 @@ export function ForisForm({
|
||||||
return SUBMIT_BUTTON_STATES.READY;
|
return SUBMIT_BUTTON_STATES.READY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formIsDisabled = (disabled
|
const formIsDisabled =
|
||||||
|| forisModuleState.state === API_STATE.SENDING
|
disabled ||
|
||||||
|| postState.state === API_STATE.SENDING);
|
forisModuleState.state === API_STATE.SENDING ||
|
||||||
|
postState.state === API_STATE.SENDING;
|
||||||
const submitButtonIsDisabled = disabled || !!formState.errors;
|
const submitButtonIsDisabled = disabled || !!formState.errors;
|
||||||
|
|
||||||
const childrenWithFormProps = React.Children.map(
|
const childrenWithFormProps = React.Children.map(children, (child) =>
|
||||||
children,
|
React.cloneElement(child, {
|
||||||
(child) => React.cloneElement(child, {
|
|
||||||
initialData: formState.initialData,
|
initialData: formState.initialData,
|
||||||
formData: formState.data,
|
formData: formState.data,
|
||||||
formErrors: formState.errors,
|
formErrors: formState.errors,
|
||||||
setFormValue: onFormChangeHandler,
|
setFormValue: onFormChangeHandler,
|
||||||
disabled: formIsDisabled,
|
disabled: formIsDisabled,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSubmit = onSubmitOverridden
|
const onSubmit = onSubmitOverridden
|
||||||
? onSubmitOverridden(formState.data, onFormChangeHandler, onSubmitHandler)
|
? onSubmitOverridden(
|
||||||
|
formState.data,
|
||||||
|
onFormChangeHandler,
|
||||||
|
onSubmitHandler
|
||||||
|
)
|
||||||
: onSubmitHandler;
|
: onSubmitHandler;
|
||||||
|
|
||||||
function getMessageOnLeavingPage() {
|
function getMessageOnLeavingPage() {
|
||||||
if (JSON.stringify(formState.data) === JSON.stringify(formState.initialData)) return true;
|
if (
|
||||||
return _("Changes you made may not be saved. Are you sure you want to leave?");
|
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 (
|
return (
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
`<ForisForm/>` is Higher-Order Component which encapsulates entire form logic and provides with children required props.
|
`<ForisForm/>` is Higher-Order Component which encapsulates entire form logic
|
||||||
This component structure provides comfort API and allows to create typical Foris module forms easily.
|
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/>`
|
## Example of usage of `<ForisForm/>`
|
||||||
|
|
||||||
You can pass more forms as children.
|
You can pass more forms as children.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
<ForisForm
|
<ForisForm
|
||||||
ws={ws}
|
ws={ws}
|
||||||
|
@ -24,7 +27,10 @@ You can pass more forms as children.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
export default function MACForm({
|
export default function MACForm({
|
||||||
formData, formErrors, setFormValue, ...props
|
formData,
|
||||||
|
formErrors,
|
||||||
|
setFormValue,
|
||||||
|
...props
|
||||||
}) {
|
}) {
|
||||||
const macSettings = formData.mac_settings;
|
const macSettings = formData.mac_settings;
|
||||||
const errors = (formErrors || {}).mac_settings || {};
|
const errors = (formErrors || {}).mac_settings || {};
|
||||||
|
@ -35,38 +41,33 @@ export default function MACForm({
|
||||||
label={_("Custom MAC address")}
|
label={_("Custom MAC address")}
|
||||||
checked={macSettings.custom_mac_enabled}
|
checked={macSettings.custom_mac_enabled}
|
||||||
helpText={HELP_TEXTS.custom_mac_enabled}
|
helpText={HELP_TEXTS.custom_mac_enabled}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
onChange={setFormValue(
|
mac_settings: { custom_mac_enabled: { $set: value } },
|
||||||
(value) => ({ mac_settings: { custom_mac_enabled: { $set: value } } }),
|
}))}
|
||||||
)}
|
|
||||||
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{macSettings.custom_mac_enabled
|
{macSettings.custom_mac_enabled ? (
|
||||||
? (
|
<TextInput
|
||||||
<TextInput
|
label={_("MAC address")}
|
||||||
label={_("MAC address")}
|
value={macSettings.custom_mac || ""}
|
||||||
value={macSettings.custom_mac || ""}
|
helpText={HELP_TEXTS.custom_mac}
|
||||||
helpText={HELP_TEXTS.custom_mac}
|
error={errors.custom_mac}
|
||||||
error={errors.custom_mac}
|
required
|
||||||
required
|
onChange={setFormValue((value) => ({
|
||||||
|
mac_settings: { custom_mac: { $set: value } },
|
||||||
onChange={setFormValue(
|
}))}
|
||||||
(value) => ({ mac_settings: { custom_mac: { $set: value } } }),
|
{...props}
|
||||||
)}
|
/>
|
||||||
|
) : null}
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The <ForisForm/> passes subsequent `props` to the child components.
|
The <ForisForm/> passes subsequent `props` to the child components.
|
||||||
|
|
||||||
| Prop | Type | Description |
|
| Prop | Type | Description |
|
||||||
|----------------|--------|----------------------------------------------------------------------------|
|
| -------------- | ------ | -------------------------------------------------------------------------- |
|
||||||
| `formData` | object | Data returned from API. |
|
| `formData` | object | Data returned from API. |
|
||||||
| `formErrors` | object | Errors returned after validation via validator. |
|
| `formErrors` | object | Errors returned after validation via validator. |
|
||||||
| `setFormValue` | func | Function for data update. It takes update rule as arg (see example above). |
|
| `setFormValue` | func | Function for data update. It takes update rule as arg (see example above). |
|
||||||
|
|
|
@ -18,8 +18,7 @@ export const STATES = {
|
||||||
|
|
||||||
SubmitButton.propTypes = {
|
SubmitButton.propTypes = {
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
state: PropTypes.oneOf(Object.keys(STATES)
|
state: PropTypes.oneOf(Object.keys(STATES).map((key) => STATES[key])),
|
||||||
.map((key) => STATES[key])),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SubmitButton({ disabled, state, ...props }) {
|
export function SubmitButton({ disabled, state, ...props }) {
|
||||||
|
@ -28,14 +27,14 @@ export function SubmitButton({ disabled, state, ...props }) {
|
||||||
|
|
||||||
let labelSubmitButton;
|
let labelSubmitButton;
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case STATES.SAVING:
|
case STATES.SAVING:
|
||||||
labelSubmitButton = _("Updating");
|
labelSubmitButton = _("Updating");
|
||||||
break;
|
break;
|
||||||
case STATES.LOAD:
|
case STATES.LOAD:
|
||||||
labelSubmitButton = _("Load settings");
|
labelSubmitButton = _("Load settings");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
labelSubmitButton = _("Save");
|
labelSubmitButton = _("Save");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -44,7 +43,6 @@ export function SubmitButton({ disabled, state, ...props }) {
|
||||||
loading={loadingSubmitButton}
|
loading={loadingSubmitButton}
|
||||||
disabled={disableSubmitButton}
|
disabled={disableSubmitButton}
|
||||||
forisFormSize
|
forisFormSize
|
||||||
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{labelSubmitButton}
|
{labelSubmitButton}
|
||||||
|
|
|
@ -23,57 +23,61 @@ export function useForm(validator, dataPreprocessor) {
|
||||||
errors: {},
|
errors: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onFormReload = useCallback((data) => {
|
const onFormReload = useCallback(
|
||||||
dispatch({
|
(data) => {
|
||||||
type: FORM_ACTIONS.resetData,
|
dispatch({
|
||||||
data,
|
type: FORM_ACTIONS.resetData,
|
||||||
dataPreprocessor,
|
data,
|
||||||
validator,
|
dataPreprocessor,
|
||||||
});
|
validator,
|
||||||
}, [dataPreprocessor, validator]);
|
});
|
||||||
|
},
|
||||||
|
[dataPreprocessor, validator]
|
||||||
|
);
|
||||||
|
|
||||||
const onFormChangeHandler = useCallback((updateRule) => (event) => {
|
const onFormChangeHandler = useCallback(
|
||||||
dispatch({
|
(updateRule) => (event) => {
|
||||||
type: FORM_ACTIONS.updateValue,
|
dispatch({
|
||||||
value: getChangedValue(event.target),
|
type: FORM_ACTIONS.updateValue,
|
||||||
updateRule,
|
value: getChangedValue(event.target),
|
||||||
validator,
|
updateRule,
|
||||||
});
|
validator,
|
||||||
}, [validator]);
|
});
|
||||||
|
},
|
||||||
|
[validator]
|
||||||
|
);
|
||||||
|
|
||||||
return [
|
return [state, onFormChangeHandler, onFormReload];
|
||||||
state,
|
|
||||||
onFormChangeHandler,
|
|
||||||
onFormReload,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formReducer(state, action) {
|
function formReducer(state, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case FORM_ACTIONS.updateValue: {
|
case FORM_ACTIONS.updateValue: {
|
||||||
const newData = update(state.data, action.updateRule(action.value));
|
const newData = update(state.data, action.updateRule(action.value));
|
||||||
const errors = action.validator(newData);
|
const errors = action.validator(newData);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
data: newData,
|
data: newData,
|
||||||
errors,
|
errors,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
case FORM_ACTIONS.resetData: {
|
|
||||||
if (!action.data) {
|
|
||||||
return { ...state, initialData: state.data };
|
|
||||||
}
|
}
|
||||||
|
case FORM_ACTIONS.resetData: {
|
||||||
|
if (!action.data) {
|
||||||
|
return { ...state, initialData: state.data };
|
||||||
|
}
|
||||||
|
|
||||||
const data = action.dataPreprocessor ? action.dataPreprocessor(action.data) : action.data;
|
const data = action.dataPreprocessor
|
||||||
return {
|
? action.dataPreprocessor(action.data)
|
||||||
data,
|
: action.data;
|
||||||
initialData: data,
|
return {
|
||||||
errors: action.data ? action.validator(data) : undefined,
|
data,
|
||||||
};
|
initialData: data,
|
||||||
}
|
errors: action.data ? action.validator(data) : undefined,
|
||||||
default: {
|
};
|
||||||
throw new Error();
|
}
|
||||||
}
|
default: {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
33
src/index.js
33
src/index.js
|
@ -30,18 +30,11 @@ export { PasswordInput } from "./bootstrap/PasswordInput";
|
||||||
export { Radio, RadioSet } from "./bootstrap/RadioSet";
|
export { Radio, 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 { formFieldsSize, buttonFormFieldsSize } from "./bootstrap/constants";
|
||||||
|
export { Switch } from "./bootstrap/Switch";
|
||||||
|
|
||||||
export {
|
export { Spinner, SpinnerElement } from "./bootstrap/Spinner";
|
||||||
Spinner,
|
export { Modal, ModalBody, ModalFooter, ModalHeader } from "./bootstrap/Modal";
|
||||||
SpinnerElement,
|
|
||||||
} from "./bootstrap/Spinner";
|
|
||||||
export {
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
} from "./bootstrap/Modal";
|
|
||||||
|
|
||||||
// Common
|
// Common
|
||||||
export { RebootButton } from "./common/RebootButton";
|
export { RebootButton } from "./common/RebootButton";
|
||||||
|
@ -49,7 +42,10 @@ export { WiFiSettings } from "./common/WiFiSettings/WiFiSettings";
|
||||||
|
|
||||||
// Form
|
// Form
|
||||||
export { ForisForm } from "./form/components/ForisForm";
|
export { ForisForm } from "./form/components/ForisForm";
|
||||||
export { SubmitButton, STATES as SUBMIT_BUTTON_STATES } from "./form/components/SubmitButton";
|
export {
|
||||||
|
SubmitButton,
|
||||||
|
STATES as SUBMIT_BUTTON_STATES,
|
||||||
|
} from "./form/components/SubmitButton";
|
||||||
export { useForisModule, useForm } from "./form/hooks";
|
export { useForisModule, useForm } from "./form/hooks";
|
||||||
|
|
||||||
// WebSockets
|
// WebSockets
|
||||||
|
@ -58,9 +54,18 @@ export { WebSockets } from "./webSockets/WebSockets";
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
export { Portal } from "./utils/Portal";
|
export { Portal } from "./utils/Portal";
|
||||||
export { undefinedIfEmpty, withoutUndefinedKeys, onlySpecifiedKeys } from "./utils/objectHelpers";
|
|
||||||
export {
|
export {
|
||||||
withEither, withSpinner, withSending, withSpinnerOnSending, withError, withErrorMessage,
|
undefinedIfEmpty,
|
||||||
|
withoutUndefinedKeys,
|
||||||
|
onlySpecifiedKeys,
|
||||||
|
} from "./utils/objectHelpers";
|
||||||
|
export {
|
||||||
|
withEither,
|
||||||
|
withSpinner,
|
||||||
|
withSending,
|
||||||
|
withSpinnerOnSending,
|
||||||
|
withError,
|
||||||
|
withErrorMessage,
|
||||||
} from "./utils/conditionalHOCs";
|
} from "./utils/conditionalHOCs";
|
||||||
export { ErrorMessage } from "./utils/ErrorMessage";
|
export { ErrorMessage } from "./utils/ErrorMessage";
|
||||||
export { useClickOutside } from "./utils/hooks";
|
export { useClickOutside } from "./utils/hooks";
|
||||||
|
|
|
@ -15,7 +15,7 @@ window.AlertContext = React.createContext();
|
||||||
function AlertContextMock({ children }) {
|
function AlertContextMock({ children }) {
|
||||||
return (
|
return (
|
||||||
<AlertContext.Provider value={[mockSetAlert, mockDismissAlert]}>
|
<AlertContext.Provider value={[mockSetAlert, mockDismissAlert]}>
|
||||||
{ children }
|
{children}
|
||||||
</AlertContext.Provider>
|
</AlertContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,15 +26,14 @@ function Wrapper({ children }) {
|
||||||
return (
|
return (
|
||||||
<AlertContextMock>
|
<AlertContextMock>
|
||||||
<StaticRouter>
|
<StaticRouter>
|
||||||
<UIDReset>
|
<UIDReset>{children}</UIDReset>
|
||||||
{children}
|
|
||||||
</UIDReset>
|
|
||||||
</StaticRouter>
|
</StaticRouter>
|
||||||
</AlertContextMock>
|
</AlertContextMock>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const customTestRender = (ui, options) => render(ui, { wrapper: Wrapper, ...options });
|
const customTestRender = (ui, options) =>
|
||||||
|
render(ui, { wrapper: Wrapper, ...options });
|
||||||
|
|
||||||
// re-export everything
|
// re-export everything
|
||||||
export * from "@testing-library/react";
|
export * from "@testing-library/react";
|
||||||
|
|
|
@ -8,5 +8,7 @@
|
||||||
import mockAxios from "jest-mock-axios";
|
import mockAxios from "jest-mock-axios";
|
||||||
|
|
||||||
export function mockJSONError(data) {
|
export function mockJSONError(data) {
|
||||||
mockAxios.mockError({ response: { data, headers: { "content-type": "application/json" } } });
|
mockAxios.mockError({
|
||||||
|
response: { data, headers: { "content-type": "application/json" } },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,5 @@ ErrorMessage.defaultProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ErrorMessage({ message }) {
|
export function ErrorMessage({ message }) {
|
||||||
return (
|
return <p className="text-center text-danger">{message}</p>;
|
||||||
<p className="text-center text-danger">{message}</p>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,12 @@ import React from "react";
|
||||||
import { render } from "customTestRender";
|
import { render } from "customTestRender";
|
||||||
import { API_STATE } from "api/utils";
|
import { API_STATE } from "api/utils";
|
||||||
import {
|
import {
|
||||||
withEither, withSpinner, withSending, withSpinnerOnSending, withError, withErrorMessage,
|
withEither,
|
||||||
|
withSpinner,
|
||||||
|
withSending,
|
||||||
|
withSpinnerOnSending,
|
||||||
|
withError,
|
||||||
|
withErrorMessage,
|
||||||
} from "../conditionalHOCs";
|
} from "../conditionalHOCs";
|
||||||
|
|
||||||
describe("conditional HOCs", () => {
|
describe("conditional HOCs", () => {
|
||||||
|
@ -52,14 +57,18 @@ describe("conditional HOCs", () => {
|
||||||
it("should render First component", () => {
|
it("should render First component", () => {
|
||||||
const withAlternative = withSending(Alternative);
|
const withAlternative = withSending(Alternative);
|
||||||
const FirstWithConditional = withAlternative(First);
|
const FirstWithConditional = withAlternative(First);
|
||||||
const { getByText } = render(<FirstWithConditional apiState={API_STATE.SUCCESS} />);
|
const { getByText } = render(
|
||||||
|
<FirstWithConditional apiState={API_STATE.SUCCESS} />
|
||||||
|
);
|
||||||
expect(getByText("First")).toBeDefined();
|
expect(getByText("First")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render Alternative component", () => {
|
it("should render Alternative component", () => {
|
||||||
const withAlternative = withSending(Alternative);
|
const withAlternative = withSending(Alternative);
|
||||||
const FirstWithConditional = withAlternative(First);
|
const FirstWithConditional = withAlternative(First);
|
||||||
const { getByText } = render(<FirstWithConditional apiState={API_STATE.SENDING} />);
|
const { getByText } = render(
|
||||||
|
<FirstWithConditional apiState={API_STATE.SENDING} />
|
||||||
|
);
|
||||||
expect(getByText("Alternative")).toBeDefined();
|
expect(getByText("Alternative")).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -67,13 +76,17 @@ describe("conditional HOCs", () => {
|
||||||
describe("withSpinnerOnSending", () => {
|
describe("withSpinnerOnSending", () => {
|
||||||
it("should render First component", () => {
|
it("should render First component", () => {
|
||||||
const FirstWithConditional = withSpinnerOnSending(First);
|
const FirstWithConditional = withSpinnerOnSending(First);
|
||||||
const { getByText } = render(<FirstWithConditional apiState={API_STATE.SUCCESS} />);
|
const { getByText } = render(
|
||||||
|
<FirstWithConditional apiState={API_STATE.SUCCESS} />
|
||||||
|
);
|
||||||
expect(getByText("First")).toBeDefined();
|
expect(getByText("First")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render spinner", () => {
|
it("should render spinner", () => {
|
||||||
const FirstWithConditional = withSpinnerOnSending(First);
|
const FirstWithConditional = withSpinnerOnSending(First);
|
||||||
const { container } = render(<FirstWithConditional apiState={API_STATE.SENDING} />);
|
const { container } = render(
|
||||||
|
<FirstWithConditional apiState={API_STATE.SENDING} />
|
||||||
|
);
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -97,13 +110,17 @@ describe("conditional HOCs", () => {
|
||||||
describe("withErrorMessage", () => {
|
describe("withErrorMessage", () => {
|
||||||
it("should render First component", () => {
|
it("should render First component", () => {
|
||||||
const FirstWithConditional = withErrorMessage(First);
|
const FirstWithConditional = withErrorMessage(First);
|
||||||
const { getByText } = render(<FirstWithConditional apiState={API_STATE.SUCCESS} />);
|
const { getByText } = render(
|
||||||
|
<FirstWithConditional apiState={API_STATE.SUCCESS} />
|
||||||
|
);
|
||||||
expect(getByText("First")).toBeDefined();
|
expect(getByText("First")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render error message", () => {
|
it("should render error message", () => {
|
||||||
const FirstWithConditional = withErrorMessage(First);
|
const FirstWithConditional = withErrorMessage(First);
|
||||||
const { container } = render(<FirstWithConditional apiState={API_STATE.ERROR} />);
|
const { container } = render(
|
||||||
|
<FirstWithConditional apiState={API_STATE.ERROR} />
|
||||||
|
);
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,42 +10,40 @@ import { toLocaleDateString } from "../datetime";
|
||||||
describe("toLocaleDateString", () => {
|
describe("toLocaleDateString", () => {
|
||||||
it("should work with different locale", () => {
|
it("should work with different locale", () => {
|
||||||
global.ForisTranslations = { locale: "fr" };
|
global.ForisTranslations = { locale: "fr" };
|
||||||
expect(
|
expect(toLocaleDateString("2020-02-20T12:51:36+00:00")).toBe(
|
||||||
toLocaleDateString("2020-02-20T12:51:36+00:00")
|
"20 février 2020 12:51"
|
||||||
).toBe("20 février 2020 12:51");
|
);
|
||||||
global.ForisTranslations = { locale: "en" };
|
global.ForisTranslations = { locale: "en" };
|
||||||
})
|
});
|
||||||
|
|
||||||
it("should convert with default format", () => {
|
it("should convert with default format", () => {
|
||||||
expect(
|
expect(toLocaleDateString("2020-02-20T12:51:36+00:00")).toBe(
|
||||||
toLocaleDateString("2020-02-20T12:51:36+00:00")
|
"February 20, 2020 12:51 PM"
|
||||||
).toBe("February 20, 2020 12:51 PM");
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should convert with custom input format", () => {
|
it("should convert with custom input format", () => {
|
||||||
expect(
|
expect(
|
||||||
toLocaleDateString(
|
toLocaleDateString("2020-02-20 12:51:36 +0000", {
|
||||||
"2020-02-20 12:51:36 +0000",
|
inputFormat: "YYYY-MM-DD HH:mm:ss Z",
|
||||||
{ inputFormat: "YYYY-MM-DD HH:mm:ss Z" },
|
})
|
||||||
)
|
|
||||||
).toBe("February 20, 2020 12:51 PM");
|
).toBe("February 20, 2020 12:51 PM");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should convert with custom output format", () => {
|
it("should convert with custom output format", () => {
|
||||||
expect(
|
expect(
|
||||||
toLocaleDateString(
|
toLocaleDateString("2020-02-20T12:51:36+00:00", {
|
||||||
"2020-02-20T12:51:36+00:00",
|
outputFormat: "LL",
|
||||||
{ outputFormat: "LL" },
|
})
|
||||||
)
|
|
||||||
).toBe("February 20, 2020");
|
).toBe("February 20, 2020");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should convert with custom input and output format", () => {
|
it("should convert with custom input and output format", () => {
|
||||||
expect(
|
expect(
|
||||||
toLocaleDateString(
|
toLocaleDateString("2020-02-20 12:51:36 +0000", {
|
||||||
"2020-02-20 12:51:36 +0000",
|
inputFormat: "YYYY-MM-DD HH:mm:ss Z",
|
||||||
{ inputFormat: "YYYY-MM-DD HH:mm:ss Z", outputFormat: "LL" },
|
outputFormat: "LL",
|
||||||
)
|
})
|
||||||
).toBe("February 20, 2020");
|
).toBe("February 20, 2020");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,8 +24,8 @@ function withEither(conditionalFn, Either) {
|
||||||
|
|
||||||
function isSending(props) {
|
function isSending(props) {
|
||||||
if (Array.isArray(props.apiState)) {
|
if (Array.isArray(props.apiState)) {
|
||||||
return props.apiState.some(
|
return props.apiState.some((state) =>
|
||||||
(state) => [API_STATE.INIT, API_STATE.SENDING].includes(state),
|
[API_STATE.INIT, API_STATE.SENDING].includes(state)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return [API_STATE.INIT, API_STATE.SENDING].includes(props.apiState);
|
return [API_STATE.INIT, API_STATE.SENDING].includes(props.apiState);
|
||||||
|
@ -38,15 +38,18 @@ const withSpinnerOnSending = withSpinner(isSending);
|
||||||
// Error handling
|
// Error handling
|
||||||
|
|
||||||
const withError = (conditionalFn) => withEither(conditionalFn, ErrorMessage);
|
const withError = (conditionalFn) => withEither(conditionalFn, ErrorMessage);
|
||||||
const withErrorMessage = withError(
|
const withErrorMessage = withError((props) => {
|
||||||
(props) => {
|
if (Array.isArray(props.apiState)) {
|
||||||
if (Array.isArray(props.apiState)) {
|
return props.apiState.includes(API_STATE.ERROR);
|
||||||
return props.apiState.includes(API_STATE.ERROR);
|
}
|
||||||
}
|
return props.apiState === API_STATE.ERROR;
|
||||||
return props.apiState === API_STATE.ERROR;
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
withEither, withSpinner, withSending, withSpinnerOnSending, withError, withErrorMessage,
|
withEither,
|
||||||
|
withSpinner,
|
||||||
|
withSending,
|
||||||
|
withSpinnerOnSending,
|
||||||
|
withError,
|
||||||
|
withErrorMessage,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
export function toLocaleDateString(date, { inputFormat, outputFormat = "LLL" } = {}) {
|
export function toLocaleDateString(
|
||||||
|
date,
|
||||||
|
{ inputFormat, outputFormat = "LLL" } = {}
|
||||||
|
) {
|
||||||
const parsedDate = inputFormat ? moment(date, inputFormat) : moment(date);
|
const parsedDate = inputFormat ? moment(date, inputFormat) : moment(date);
|
||||||
return parsedDate
|
return parsedDate.locale(ForisTranslations.locale).format(outputFormat);
|
||||||
.locale(ForisTranslations.locale)
|
|
||||||
.format(outputFormat);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,9 +21,13 @@ export const ForisURLs = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Notifications links are used with <Link/> inside Router, thus url subdir is not required.
|
// Notifications links are used with <Link/> inside Router, thus url subdir is not required.
|
||||||
notifications: "/notifications",
|
overview: "/overview",
|
||||||
|
notifications: "/overview#notifications",
|
||||||
notificationsSettings: "/administration/notifications-settings",
|
notificationsSettings: "/administration/notifications-settings",
|
||||||
|
|
||||||
|
approveUpdates: "/package-management/updates",
|
||||||
|
languages: "/package-management/languages",
|
||||||
|
rebootPage: "/reforis/administration/reboot",
|
||||||
luci: "/cgi-bin/luci",
|
luci: "/cgi-bin/luci",
|
||||||
|
|
||||||
// API
|
// API
|
||||||
|
|
|
@ -8,11 +8,17 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
/** Execute callback when condition is set to true. */
|
/** Execute callback when condition is set to true. */
|
||||||
export function useConditionalTimeout({ callback, timeout = 125 }, ...callbackArgs) {
|
export function useConditionalTimeout(
|
||||||
|
{ callback, timeout = 125 },
|
||||||
|
...callbackArgs
|
||||||
|
) {
|
||||||
const [condition, setCondition] = useState(false);
|
const [condition, setCondition] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (condition) {
|
if (condition) {
|
||||||
const interval = setTimeout(() => callback(...callbackArgs), timeout);
|
const interval = setTimeout(
|
||||||
|
() => callback(...callbackArgs),
|
||||||
|
timeout
|
||||||
|
);
|
||||||
return () => setTimeout(interval);
|
return () => setTimeout(interval);
|
||||||
}
|
}
|
||||||
}, [condition, callback, timeout, callbackArgs]);
|
}, [condition, callback, timeout, callbackArgs]);
|
||||||
|
|
|
@ -15,21 +15,18 @@ export function undefinedIfEmpty(instance) {
|
||||||
|
|
||||||
/** Return object without keys that have undefined value. */
|
/** Return object without keys that have undefined value. */
|
||||||
export function withoutUndefinedKeys(instance) {
|
export function withoutUndefinedKeys(instance) {
|
||||||
return Object.keys(instance).reduce(
|
return Object.keys(instance).reduce((accumulator, key) => {
|
||||||
(accumulator, key) => {
|
if (instance[key] !== undefined) {
|
||||||
if (instance[key] !== undefined) {
|
accumulator[key] = instance[key];
|
||||||
accumulator[key] = instance[key];
|
}
|
||||||
}
|
return accumulator;
|
||||||
return accumulator;
|
}, {});
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return copy of passed object that has only desired keys. */
|
/** Return copy of passed object that has only desired keys. */
|
||||||
export function onlySpecifiedKeys(object, desiredKeys) {
|
export function onlySpecifiedKeys(object, desiredKeys) {
|
||||||
return desiredKeys.reduce(
|
return desiredKeys.reduce((accumulator, key) => {
|
||||||
(accumulator, key) => { accumulator[key] = object[key]; return accumulator; },
|
accumulator[key] = object[key];
|
||||||
{},
|
return accumulator;
|
||||||
);
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,10 @@ const REs = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createValidator = (fieldType) => (value) => {
|
const createValidator = (fieldType) => (value) => {
|
||||||
if (value && value !== "") return REs[fieldType].test(value) ? undefined : ERROR_MESSAGES[fieldType];
|
if (value && value !== "")
|
||||||
|
return REs[fieldType].test(value)
|
||||||
|
? undefined
|
||||||
|
: ERROR_MESSAGES[fieldType];
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateIPv4Address = createValidator("IPv4");
|
const validateIPv4Address = createValidator("IPv4");
|
||||||
|
|
|
@ -22,7 +22,9 @@ export class WebSockets {
|
||||||
this.ws = new WebSocket(URL);
|
this.ws = new WebSocket(URL);
|
||||||
this.ws.onerror = (e) => {
|
this.ws.onerror = (e) => {
|
||||||
if (window.location.pathname !== ForisURLs.login) {
|
if (window.location.pathname !== ForisURLs.login) {
|
||||||
console.error("WS: Error observed, you aren't logged probably.");
|
console.error(
|
||||||
|
"WS: Error observed, you aren't logged probably."
|
||||||
|
);
|
||||||
window.location.replace(ForisURLs.login);
|
window.location.replace(ForisURLs.login);
|
||||||
}
|
}
|
||||||
console.error(`WS: Error: ${e}`);
|
console.error(`WS: Error: ${e}`);
|
||||||
|
@ -111,7 +113,9 @@ export class WebSockets {
|
||||||
chain = this.callbacks[json.module][json.action];
|
chain = this.callbacks[json.module][json.action];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof TypeError) {
|
if (error instanceof TypeError) {
|
||||||
console.warn(`Callback for this message wasn't found:${error.data}`);
|
console.warn(
|
||||||
|
`Callback for this message wasn't found:${error.data}`
|
||||||
|
);
|
||||||
} else throw error;
|
} else throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,12 @@
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export function useWSForisModule(ws, module, action = "update_settings", controllerID) {
|
export function useWSForisModule(
|
||||||
|
ws,
|
||||||
|
module,
|
||||||
|
action = "update_settings",
|
||||||
|
controllerID
|
||||||
|
) {
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -18,14 +23,16 @@ export function useWSForisModule(ws, module, action = "update_settings", control
|
||||||
|
|
||||||
function callback(message) {
|
function callback(message) {
|
||||||
// Accept only messages addressed to device with passed controller ID.
|
// Accept only messages addressed to device with passed controller ID.
|
||||||
if (controllerID !== undefined && controllerID !== message.controller_id) {
|
if (
|
||||||
|
controllerID !== undefined &&
|
||||||
|
controllerID !== message.controller_id
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setData(message.data);
|
setData(message.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.subscribe(module)
|
ws.subscribe(module).bind(module, action, callback);
|
||||||
.bind(module, action, callback);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ws.unbind(module, action, callback);
|
ws.unbind(module, action, callback);
|
||||||
|
|
|
@ -30,9 +30,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Alert Context",
|
name: "Alert Context",
|
||||||
components: [
|
components: ["src/alertContext/AlertContext.js"],
|
||||||
"src/alertContext/AlertContext.js",
|
|
||||||
],
|
|
||||||
exampleMode: "expand",
|
exampleMode: "expand",
|
||||||
usageMode: "expand",
|
usageMode: "expand",
|
||||||
},
|
},
|
||||||
|
@ -42,16 +40,20 @@ module.exports = {
|
||||||
components: "src/bootstrap/*.js",
|
components: "src/bootstrap/*.js",
|
||||||
exampleMode: "expand",
|
exampleMode: "expand",
|
||||||
usageMode: "expand",
|
usageMode: "expand",
|
||||||
ignore: [
|
ignore: ["src/bootstrap/constants.js"],
|
||||||
"src/bootstrap/constants.js",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
require: [
|
require: [
|
||||||
"babel-polyfill",
|
"babel-polyfill",
|
||||||
path.join(__dirname, "src/testUtils/mockGlobals"),
|
path.join(__dirname, "src/testUtils/mockGlobals"),
|
||||||
path.join(__dirname, "node_modules/bootstrap/dist/css/bootstrap.min.css"),
|
path.join(
|
||||||
path.join(__dirname, "node_modules/@fortawesome/fontawesome-free/css/all.min.css"),
|
__dirname,
|
||||||
|
"node_modules/bootstrap/dist/css/bootstrap.min.css"
|
||||||
|
),
|
||||||
|
path.join(
|
||||||
|
__dirname,
|
||||||
|
"node_modules/@fortawesome/fontawesome-free/css/all.min.css"
|
||||||
|
),
|
||||||
],
|
],
|
||||||
webpackConfig: {
|
webpackConfig: {
|
||||||
module: {
|
module: {
|
||||||
|
@ -60,10 +62,12 @@ module.exports = {
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
loader: "babel-loader",
|
loader: "babel-loader",
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: ["style-loader", "css-loader"],
|
use: ["style-loader", "css-loader"],
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
test: /\.(jpg|jpeg|png|woff|woff2|eot|ttf|svg)$/,
|
test: /\.(jpg|jpeg|png|woff|woff2|eot|ttf|svg)$/,
|
||||||
loader: "file-loader",
|
loader: "file-loader",
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue
Block a user