1
0
mirror of https://gitlab.nic.cz/turris/reforis/foris-js.git synced 2025-06-15 13:36:35 +02:00

Compare commits

..

28 Commits

Author SHA1 Message Date
bb90800945 Merge branch 'dev' into 'master'
Release 2.1.1

See merge request turris/reforis/foris-js!65
2020-01-06 09:56:29 +01:00
6d4bff2b4f Merge branch 'datepicker' into 'dev'
Display datepicker above input

Closes reforis#153

See merge request turris/reforis/foris-js!63
2020-01-06 09:45:16 +01:00
92f560b69f Display datepicker above input 2020-01-06 09:45:16 +01:00
51ba380cf0 Merge branch 'dev' into 'master'
Release 2.1.0

See merge request turris/reforis/foris-js!62
2019-12-19 12:10:18 +01:00
4eae1ed8d2 Merge branch 'ws-logging' into 'dev'
Changed levels of WS logs

Closes reforis#162

See merge request turris/reforis/foris-js!61
2019-12-19 11:58:59 +01:00
3d290114fa Changed levels of WS logs 2019-12-17 17:45:55 +01:00
3f87e9e4b4 Merge branch 'export-radio' into 'dev'
Export <Radio />.

See merge request turris/reforis/foris-js!60
2019-12-17 14:07:14 +01:00
6d5cb6a951 Export <Radio />. 2019-12-17 14:04:06 +01:00
8d3be8df67 Merge branch 'click-outside-hook' into 'dev'
Detect clicks outside element with a hook

See merge request turris/reforis/foris-js!59
2019-12-16 16:21:18 +01:00
90509f2a23 Detect clicks outside element with a hook 2019-12-16 13:44:58 +01:00
73f84a2d81 Merge branch '8-fix-git-link' into 'dev'
Fixed link to git repository

Closes #8

See merge request turris/reforis/foris-js!58
2019-12-09 12:45:01 +01:00
cea7325427 Fixed link to git repository 2019-12-09 11:51:50 +01:00
a8d8c872f9 Merge branch 'dev' into 'master'
Publish 2.0.0.

See merge request turris/reforis/foris-js!57
2019-12-09 10:51:49 +01:00
cda7898a96 Merge branch 'fix-readme' into 'dev'
Fix README.md publishing.

See merge request turris/reforis/foris-js!56
2019-12-09 10:24:42 +01:00
26bea9c7c4 Fix README.md publishing. 2019-12-06 17:26:01 +01:00
fd1518265f Merge branch 'spinner-element-class-name' into 'dev'
Add className to SpinnerElement.

See merge request turris/reforis/foris-js!55
2019-12-06 15:07:16 +01:00
61d10e91e0 Merge branch 'ws-unsubscription' into 'dev'
Add WS unsubscribtion.

See merge request turris/reforis/foris-js!53
2019-12-06 15:06:59 +01:00
aac6c6bf2a Add className to SpinnerElement. 2019-12-06 12:13:29 +01:00
d55615abcc Grammar. 2019-12-06 12:09:54 +01:00
9d322811c3 Improve docs and propTypes of WS usage in ForisForm. 2019-12-06 12:07:42 +01:00
f30685d9c2 Add WS unsubscribtion.
Code review improvements.
2019-12-06 11:46:30 +01:00
5bb298270b Merge branch '15-api-url-suffix' into 'dev'
Set suffix for API URL

Closes #15

See merge request turris/reforis/foris-js!54
2019-12-06 10:27:48 +01:00
8d0c640994 Set suffix for API URL 2019-12-06 10:27:48 +01:00
25ddb5949c Merge branch 'dev' into 'master'
Dev

See merge request turris/reforis/foris-js!52
2019-12-02 17:11:03 +01:00
7b739f55a0 Merge branch 'add-reboot-button' into 'dev'
Add reboot button

See merge request turris/reforis/foris-js!50
2019-11-29 16:37:44 +01:00
d71f638bd5 Merge branch 'download-button-class' into 'dev'
Allow adding classes to DownloadButton

See merge request turris/reforis/foris-js!51
2019-11-29 16:01:53 +01:00
1064277cd9 Allow adding classes to DownloadButton 2019-11-29 16:01:53 +01:00
8d1d5b57fd Merge branch 'dev' 2019-11-25 13:07:20 +01:00
18 changed files with 136 additions and 54 deletions

View File

@ -1,4 +1,5 @@
# foris-js
Set of utils and common React elements for reForis.
## Publishing package

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "foris",
"version": "1.4.0",
"version": "2.1.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,11 +1,11 @@
{
"name": "foris",
"version": "1.4.0",
"version": "2.1.1",
"description": "Set of components and utils for Foris and its plugins.",
"author": "CZ.NIC, z.s.p.o.",
"repository": {
"type": "git",
"url": "https://gitlab.labs.nic.cz/turris/reforis/forisjs.git"
"url": "https://gitlab.labs.nic.cz/turris/reforis/foris-js.git"
},
"keywords": [
"foris",

View File

@ -2,7 +2,7 @@
# Collect files
npm run build
cp package.json dist
cp package.json README.md dist
cp -rf translations dist
# Remove unwanted files
rm -rf dist/**/__tests__

View File

@ -17,13 +17,13 @@ import {
const DATA_METHODS = ["POST", "PATCH", "PUT"];
function createAPIHook(method) {
return (url, contentType) => {
return (urlRoot, contentType) => {
const [state, dispatch] = useReducer(APIReducer, {
state: API_STATE.INIT,
data: null,
});
const sendRequest = useCallback(async (data) => {
const sendRequest = useCallback(async ({ data, suffix } = {}) => {
const headers = { ...HEADERS };
if (contentType) {
headers["Content-Type"] = contentType;
@ -31,17 +31,23 @@ function createAPIHook(method) {
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.SUCCESS,
payload: result.data,
@ -53,7 +59,7 @@ function createAPIHook(method) {
payload: getErrorPayload(error),
});
}
}, [url, contentType]);
}, [urlRoot, contentType]);
return [state, sendRequest];
};
}

View File

@ -0,0 +1,4 @@
/* Override defaults from "react-datetime" - display picker above input */
.rdtPicker {
bottom: 0;
}

View File

@ -10,6 +10,7 @@ import PropTypes from "prop-types";
import Datetime from "react-datetime/DateTime";
import moment from "moment/moment";
import "react-datetime/css/react-datetime.css";
import "./DataTimeInput.css";
import { Input } from "./Input";

View File

@ -10,12 +10,25 @@ import PropTypes from "prop-types";
DownloadButton.propTypes = {
href: PropTypes.string.isRequired,
className: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
};
export function DownloadButton({ href, children }) {
return <a href={href} className="btn btn-primary" download>{children}</a>;
DownloadButton.defaultProps = {
className: "btn-primary",
};
export function DownloadButton({ href, className, children }) {
return (
<a
href={href}
className={`btn ${className}`.trim()}
download
>
{children}
</a>
);
}

View File

@ -5,10 +5,11 @@
* See /LICENSE for more information.
*/
import React, { useEffect, useRef } from "react";
import React, { useRef } from "react";
import PropTypes from "prop-types";
import { Portal } from "utils/Portal";
import { useClickOutside } from "utils/hooks";
Modal.propTypes = {
/** Is modal shown value */
@ -26,16 +27,7 @@ Modal.propTypes = {
export function Modal({ shown, setShown, children }) {
const dialogRef = useRef();
useEffect(() => {
function handleClickOutsideDialog(e) {
if (!dialogRef.current.contains(e.target)) setShown(false);
}
document.addEventListener("mousedown", handleClickOutsideDialog);
return () => {
document.removeEventListener("mousedown", handleClickOutsideDialog);
};
}, [setShown]);
useClickOutside(dialogRef, () => setShown(false));
return (
<Portal containerId="modal-container">

View File

@ -65,7 +65,7 @@ Radio.propTypes = {
helpText: PropTypes.string,
};
function Radio({
export function Radio({
label, id, helpText, ...props
}) {
return (

View File

@ -46,6 +46,8 @@ export function Spinner({
SpinnerElement.propTypes = {
/** Spinner's size */
small: PropTypes.bool,
/** Additional className */
className: PropTypes.string,
/** Children components */
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
@ -53,13 +55,16 @@ SpinnerElement.propTypes = {
]),
};
export function SpinnerElement({ small, children }) {
export function SpinnerElement({ small, className, children }) {
return (
<>
<div className={`spinner-border ${small ? "spinner-border-sm" : ""}`} role="status">
<div
className={`spinner-border ${small ? "spinner-border-sm" : ""} ${className || ""}`.trim()}
role="status"
>
<span className="sr-only" />
</div>
<div className="spinner-text">{children}</div>
{children && <div className="spinner-text">{children}</div>}
</>
);
}

View File

@ -10,6 +10,7 @@ import React from 'react';
import { act, fireEvent, render, waitForElement } from 'customTestRender';
import mockAxios from 'jest-mock-axios';
import { ForisForm } from "../components/ForisForm";
import { WebSockets } from "webSockets/WebSockets";
// It's possible to unittest each hooks via react-hooks-testing-library.
@ -30,7 +31,6 @@ describe('useForm hook.', () => {
let mockValidator;
let mockPrepData;
let mockPrepDataToSubmit;
let mockWebSockets;
let input;
let form;
const Child = jest.fn(props => <TestForm {...props}/>);
@ -41,7 +41,7 @@ describe('useForm hook.', () => {
mockValidator = jest.fn(data => data.field === 'invalidValue' ? {field: 'Error'} : {});
const {getByTestId, container} = render(
<ForisForm
ws={mockWebSockets}
ws={new WebSockets()}
// Just some module which exists...
forisConfig={{
endpoint: 'testEndpoint',

View File

@ -20,14 +20,16 @@ import { useForisModule, useForm } from "../hooks";
import { STATES as SUBMIT_BUTTON_STATES, SubmitButton } from "./SubmitButton";
ForisForm.propTypes = {
/** WebSocket object see `scr/common/WebSockets.js`. */
/** Optional WebSocket object. See `scr/common/WebSockets.js`.
* `forisConfig.wsModule` should be specified when it's passed.
* */
ws: PropTypes.object,
/** Foris configuration object. See usage in main components. */
forisConfig: PropTypes.shape({
/** reForis Flask aplication API endpoint from `src/common/API.js`. */
endpoint: PropTypes.string.isRequired,
/** `foris-controller` module name to be used via WebSockets.
* If it's not passed then WebSockets aren't used
* It can be use only with `ws` prop.
* */
wsModule: PropTypes.string,
/** `foris-controller` action name to be used via WebSockets.
@ -49,6 +51,17 @@ ForisForm.propTypes = {
children: PropTypes.node.isRequired,
/** Optional override of form submit callback */
onSubmitOverridden: PropTypes.func,
// eslint-disable-next-line react/no-unused-prop-types
customWSProp(props) {
const wsModuleIsSpecified = !!(props.forisConfig && props.forisConfig.wsModule);
if (props.ws && !wsModuleIsSpecified) {
return new Error("forisConfig.wsModule should be specified when ws object is passed.");
}
if (!props.ws && wsModuleIsSpecified) {
return new Error("forisConfig.wsModule is specified without passing ws object.");
}
},
};
ForisForm.defaultProps = {
@ -103,7 +116,7 @@ export function ForisForm({
resetFormData();
const copiedFormData = JSON.parse(JSON.stringify(formState.data));
const preparedData = prepDataToSubmit(copiedFormData);
post(preparedData);
post({ data: preparedData });
}
function getSubmitButtonState() {

View File

@ -27,7 +27,7 @@ export { FileInput } from "bootstrap/FileInput";
export { Input } from "bootstrap/Input";
export { NumberInput } from "bootstrap/NumberInput";
export { PasswordInput } from "bootstrap/PasswordInput";
export { RadioSet } from "bootstrap/RadioSet";
export { Radio, RadioSet } from "bootstrap/RadioSet";
export { Select } from "bootstrap/Select";
export { TextInput } from "bootstrap/TextInput";
export { formFieldsSize } from "bootstrap/constants";
@ -62,6 +62,7 @@ export {
withEither, withSpinner, withSending, withSpinnerOnSending, withError, withErrorMessage,
} from "utils/conditionalHOCs";
export { ErrorMessage } from "utils/ErrorMessage";
export { useClickOutside } from "utils/hooks";
// Foris URL
export { ForisURLs, REFORIS_URL_PREFIX } from "forisUrls";

View File

@ -26,16 +26,13 @@ exports[`conditional HOCs withSpinner should render spinner 1`] = `
class="spinner-wrapper my-3 text-center"
>
<div
class="spinner-border "
class="spinner-border"
role="status"
>
<span
class="sr-only"
/>
</div>
<div
class="spinner-text"
/>
</div>
</div>
`;
@ -46,16 +43,13 @@ exports[`conditional HOCs withSpinnerOnSending should render spinner 1`] = `
class="spinner-wrapper my-3 text-center"
>
<div
class="spinner-border "
class="spinner-border"
role="status"
>
<span
class="sr-only"
/>
</div>
<div
class="spinner-text"
/>
</div>
</div>
`;

View File

@ -18,3 +18,19 @@ export function useConditionalTimeout({ callback, timeout = 125 }, ...callbackAr
}, [condition, callback, timeout, callbackArgs]);
return setCondition;
}
/** Execute callback when user clicks outside specified element. */
export function useClickOutside(element, callback) {
function handleClickOutside(event) {
if (element.current && !element.current.contains(event.target)) {
callback(event);
}
}
useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
});
}

View File

@ -25,18 +25,18 @@ export class WebSockets {
console.error("WS: Error observed, you aren't logged probably.");
window.location.replace(ForisURLs.login);
}
console.log(`WS: Error: ${e}`);
console.error(`WS: Error: ${e}`);
};
this.ws.onmessage = (e) => {
console.log(`WS: Received Message: ${e.data}`);
console.debug(`WS: Received Message: ${e.data}`);
const data = JSON.parse(e.data);
this.dispatch(data);
};
this.ws.onopen = () => {
console.log("WS: Connection open.");
console.debug("WS: Connection open.");
};
this.ws.onclose = () => {
console.log("WS: Connection closed.");
console.debug("WS: Connection closed.");
};
// callbacks[module][action]
@ -61,9 +61,36 @@ export class WebSockets {
return this;
}
subscribe(params) {
subscribe(module) {
this.waitForConnection(() => {
this.send("subscribe", params);
this.send("subscribe", module);
});
return this;
}
unbind(module, action, callback) {
const callbacks = this.callbacks[module][action];
const index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
}
if (callbacks.length === 0) {
delete this.callbacks[module][action];
}
if (Object.keys(this.callbacks[module]).length === 0) {
this.unsubscribe(module);
}
return this;
}
unsubscribe(module) {
this.waitForConnection(() => {
this.send("unsubscribe", module);
delete this.callbacks[module];
});
return this;
}
@ -82,15 +109,15 @@ export class WebSockets {
let chain;
try {
chain = this.callbacks[json.module][json.action];
} catch (e) {
if (e instanceof TypeError) {
console.log(`Callback for this message wasn't found:${e.data}`);
} else throw e;
} catch (error) {
if (error instanceof TypeError) {
console.warn(`Callback for this message wasn't found:${error.data}`);
} else throw error;
}
if (typeof chain === "undefined") return;
for (let i = 0; i < chain.length; i++) chain[i](json);
chain.forEach((callback) => callback(json));
}
close() {

View File

@ -11,12 +11,21 @@ export function useWSForisModule(ws, module, action = "update_settings") {
const [data, setData] = useState(null);
useEffect(() => {
if (ws && module) {
ws.subscribe(module)
.bind(module, action, (msg) => {
setData(msg.data);
});
// Sometimes we want to disable this hook if WS is not passed. We can't make conditional
// hooks, but we can disable it here. It's used especially in ForisForm when a module
// doesn't present any WS endpoint.
if (!ws) return;
function callback(msg) {
setData(msg.data);
}
ws.subscribe(module)
.bind(module, action, callback);
return () => {
ws.unbind(module, action, callback);
};
}, [action, module, ws]);
return [data];