mirror of
				https://gitlab.nic.cz/turris/reforis/foris-js.git
				synced 2025-11-03 23:00:31 +01:00 
			
		
		
		
	Format all files with Prettier
This commit is contained in:
		@@ -22,9 +22,12 @@ function AlertContextProvider({ children }) {
 | 
			
		||||
    const { AlertContext } = window;
 | 
			
		||||
    const [alert, setAlert] = useState(null);
 | 
			
		||||
 | 
			
		||||
    const setAlertWrapper = useCallback((message, type = ALERT_TYPES.DANGER) => {
 | 
			
		||||
        setAlert({ message, type });
 | 
			
		||||
    }, [setAlert]);
 | 
			
		||||
    const setAlertWrapper = useCallback(
 | 
			
		||||
        (message, type = ALERT_TYPES.DANGER) => {
 | 
			
		||||
            setAlert({ message, type });
 | 
			
		||||
        },
 | 
			
		||||
        [setAlert]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const dismissAlert = useCallback(() => setAlert(null), [setAlert]);
 | 
			
		||||
 | 
			
		||||
@@ -38,7 +41,7 @@ function AlertContextProvider({ children }) {
 | 
			
		||||
                </Portal>
 | 
			
		||||
            )}
 | 
			
		||||
            <AlertContext.Provider value={[setAlertWrapper, dismissAlert]}>
 | 
			
		||||
                { children }
 | 
			
		||||
                {children}
 | 
			
		||||
            </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
 | 
			
		||||
with base Jinja2 templates).
 | 
			
		||||
Notice that `<div id="alert-container"/>` should be presented in HTML doc to get
 | 
			
		||||
it work (In reForis it's already done with base Jinja2 templates).
 | 
			
		||||
 
 | 
			
		||||
@@ -6,9 +6,7 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import React from "react";
 | 
			
		||||
import {
 | 
			
		||||
    render, getByText, queryByText, fireEvent,
 | 
			
		||||
} from "customTestRender";
 | 
			
		||||
import { render, getByText, queryByText, fireEvent } from "customTestRender";
 | 
			
		||||
 | 
			
		||||
import { useAlert, AlertContextProvider } from "../AlertContext";
 | 
			
		||||
 | 
			
		||||
@@ -31,7 +29,7 @@ describe("AlertContext", () => {
 | 
			
		||||
        const { container } = render(
 | 
			
		||||
            <AlertContextProvider>
 | 
			
		||||
                <AlertTest />
 | 
			
		||||
            </AlertContextProvider>,
 | 
			
		||||
            </AlertContextProvider>
 | 
			
		||||
        );
 | 
			
		||||
        componentContainer = container;
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										145
									
								
								src/api/hooks.js
									
									
									
									
									
								
							
							
						
						
									
										145
									
								
								src/api/hooks.js
									
									
									
									
									
								
							@@ -5,13 +5,16 @@
 | 
			
		||||
 * See /LICENSE for more information.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    useCallback, useEffect, useReducer, useState,
 | 
			
		||||
} from "react";
 | 
			
		||||
import { useCallback, useEffect, useReducer, useState } from "react";
 | 
			
		||||
 | 
			
		||||
import { ForisURLs } from "../utils/forisUrls";
 | 
			
		||||
import {
 | 
			
		||||
    API_ACTIONS, API_METHODS, API_STATE, getErrorPayload, HEADERS, TIMEOUT,
 | 
			
		||||
    API_ACTIONS,
 | 
			
		||||
    API_METHODS,
 | 
			
		||||
    API_STATE,
 | 
			
		||||
    getErrorPayload,
 | 
			
		||||
    HEADERS,
 | 
			
		||||
    TIMEOUT,
 | 
			
		||||
} from "./utils";
 | 
			
		||||
 | 
			
		||||
const DATA_METHODS = ["POST", "PATCH", "PUT"];
 | 
			
		||||
@@ -23,76 +26,83 @@ function createAPIHook(method) {
 | 
			
		||||
            data: null,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const sendRequest = useCallback(async ({ data, suffix } = {}) => {
 | 
			
		||||
            const headers = { ...HEADERS };
 | 
			
		||||
            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);
 | 
			
		||||
        const sendRequest = useCallback(
 | 
			
		||||
            async ({ data, suffix } = {}) => {
 | 
			
		||||
                const headers = { ...HEADERS };
 | 
			
		||||
                if (contentType) {
 | 
			
		||||
                    headers["Content-Type"] = contentType;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // 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]);
 | 
			
		||||
                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,
 | 
			
		||||
                    });
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    const errorPayload = getErrorPayload(error);
 | 
			
		||||
                    dispatch({
 | 
			
		||||
                        type: API_ACTIONS.FAILURE,
 | 
			
		||||
                        status: error.response && error.response.status,
 | 
			
		||||
                        payload: errorPayload,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            [urlRoot, contentType]
 | 
			
		||||
        );
 | 
			
		||||
        return [state, sendRequest];
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function APIReducer(state, action) {
 | 
			
		||||
    switch (action.type) {
 | 
			
		||||
    case API_ACTIONS.INIT:
 | 
			
		||||
        return {
 | 
			
		||||
            ...state,
 | 
			
		||||
            state: API_STATE.SENDING,
 | 
			
		||||
        };
 | 
			
		||||
    case API_ACTIONS.SUCCESS:
 | 
			
		||||
        return {
 | 
			
		||||
            state: API_STATE.SUCCESS,
 | 
			
		||||
            data: action.payload,
 | 
			
		||||
        };
 | 
			
		||||
    case API_ACTIONS.FAILURE:
 | 
			
		||||
        if (action.status === 403) {
 | 
			
		||||
            window.location.assign(ForisURLs.login);
 | 
			
		||||
        }
 | 
			
		||||
        case API_ACTIONS.INIT:
 | 
			
		||||
            return {
 | 
			
		||||
                ...state,
 | 
			
		||||
                state: API_STATE.SENDING,
 | 
			
		||||
            };
 | 
			
		||||
        case API_ACTIONS.SUCCESS:
 | 
			
		||||
            return {
 | 
			
		||||
                state: API_STATE.SUCCESS,
 | 
			
		||||
                data: action.payload,
 | 
			
		||||
            };
 | 
			
		||||
        case API_ACTIONS.FAILURE:
 | 
			
		||||
            if (action.status === 403) {
 | 
			
		||||
                window.location.assign(ForisURLs.login);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        // Not an API error - should be rethrown.
 | 
			
		||||
        if (action.payload && action.payload.stack && action.payload.message) {
 | 
			
		||||
            throw (action.payload);
 | 
			
		||||
        }
 | 
			
		||||
            // Not an API error - should be rethrown.
 | 
			
		||||
            if (
 | 
			
		||||
                action.payload &&
 | 
			
		||||
                action.payload.stack &&
 | 
			
		||||
                action.payload.message
 | 
			
		||||
            ) {
 | 
			
		||||
                throw action.payload;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            state: API_STATE.ERROR,
 | 
			
		||||
            data: action.payload,
 | 
			
		||||
        };
 | 
			
		||||
    default:
 | 
			
		||||
        throw new Error();
 | 
			
		||||
            return {
 | 
			
		||||
                state: API_STATE.ERROR,
 | 
			
		||||
                data: action.payload,
 | 
			
		||||
            };
 | 
			
		||||
        default:
 | 
			
		||||
            throw new Error();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -102,11 +112,10 @@ const useAPIPatch = createAPIHook("PATCH");
 | 
			
		||||
const useAPIPut = createAPIHook("PUT");
 | 
			
		||||
const useAPIDelete = createAPIHook("DELETE");
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
    useAPIGet, useAPIPost, useAPIPatch, useAPIPut, useAPIDelete,
 | 
			
		||||
};
 | 
			
		||||
export { 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 [getResponse, get] = useAPIGet(endpoint);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -43,8 +43,10 @@ function getCookie(name) {
 | 
			
		||||
        for (let i = 0; i < cookies.length; i++) {
 | 
			
		||||
            const cookie = cookies[i].trim();
 | 
			
		||||
            // Does this cookie string begin with the name we want?
 | 
			
		||||
            if (cookie.substring(0, name.length + 1) === (`${name}=`)) {
 | 
			
		||||
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
 | 
			
		||||
            if (cookie.substring(0, name.length + 1) === `${name}=`) {
 | 
			
		||||
                cookieValue = decodeURIComponent(
 | 
			
		||||
                    cookie.substring(name.length + 1)
 | 
			
		||||
                );
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -35,12 +35,16 @@ Alert.defaultProps = {
 | 
			
		||||
    type: ALERT_TYPES.DANGER,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function Alert({
 | 
			
		||||
    type, onDismiss, children,
 | 
			
		||||
}) {
 | 
			
		||||
export function Alert({ type, onDismiss, children }) {
 | 
			
		||||
    return (
 | 
			
		||||
        <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}
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,21 @@
 | 
			
		||||
Bootstrap alert component.
 | 
			
		||||
 | 
			
		||||
```jsx
 | 
			
		||||
import {useState} from 'react';
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
 | 
			
		||||
function AlertExample(){
 | 
			
		||||
function AlertExample() {
 | 
			
		||||
    const [alert, setAlert] = useState(true);
 | 
			
		||||
    if (alert)
 | 
			
		||||
        return <Alert 
 | 
			
		||||
            type='warning' 
 | 
			
		||||
            onDismiss={()=>setAlert(false)}
 | 
			
		||||
        >
 | 
			
		||||
            Some warning out there!        
 | 
			
		||||
        </Alert>;
 | 
			
		||||
    return <button 
 | 
			
		||||
        className='btn btn-secondary' 
 | 
			
		||||
        onClick={()=>setAlert(true)}
 | 
			
		||||
    >Show alert again</button>;
 | 
			
		||||
};
 | 
			
		||||
<AlertExample/>
 | 
			
		||||
        return (
 | 
			
		||||
            <Alert type="warning" onDismiss={() => setAlert(false)}>
 | 
			
		||||
                Some warning out there!
 | 
			
		||||
            </Alert>
 | 
			
		||||
        );
 | 
			
		||||
    return (
 | 
			
		||||
        <button className="btn btn-secondary" onClick={() => setAlert(true)}>
 | 
			
		||||
            Show alert again
 | 
			
		||||
        </button>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
<AlertExample />;
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
@@ -25,22 +25,29 @@ Button.propTypes = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function Button({
 | 
			
		||||
    className, loading, forisFormSize, children, ...props
 | 
			
		||||
    className,
 | 
			
		||||
    loading,
 | 
			
		||||
    forisFormSize,
 | 
			
		||||
    children,
 | 
			
		||||
    ...props
 | 
			
		||||
}) {
 | 
			
		||||
    let buttonClass = className ? `btn ${className}` : "btn btn-primary ";
 | 
			
		||||
    if (forisFormSize) {
 | 
			
		||||
        buttonClass = `${buttonClass} col-sm-12 col-md-3 col-lg-2`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const span = loading
 | 
			
		||||
        ? <span className="spinner-border spinner-border-sm" role="status" aria-hidden="true" /> : null;
 | 
			
		||||
    const span = loading ? (
 | 
			
		||||
        <span
 | 
			
		||||
            className="spinner-border spinner-border-sm"
 | 
			
		||||
            role="status"
 | 
			
		||||
            aria-hidden="true"
 | 
			
		||||
        />
 | 
			
		||||
    ) : null;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <button type="button" className={buttonClass} {...props}>
 | 
			
		||||
            {span}
 | 
			
		||||
            {" "}
 | 
			
		||||
            {span ? " " : null}
 | 
			
		||||
            {" "}
 | 
			
		||||
            {children}
 | 
			
		||||
        </button>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -11,5 +11,7 @@ Can be used without parameters:
 | 
			
		||||
Using loading spinner:
 | 
			
		||||
 | 
			
		||||
```jsx
 | 
			
		||||
<Button loading disabled>Loading...</Button>
 | 
			
		||||
<Button loading disabled>
 | 
			
		||||
    Loading...
 | 
			
		||||
</Button>
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
@@ -22,9 +22,7 @@ CheckBox.defaultProps = {
 | 
			
		||||
    disabled: false,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function CheckBox({
 | 
			
		||||
    label, helpText, disabled, ...props
 | 
			
		||||
}) {
 | 
			
		||||
export function CheckBox({ label, helpText, disabled, ...props }) {
 | 
			
		||||
    const uid = useUID();
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="form-group">
 | 
			
		||||
@@ -34,12 +32,15 @@ export function CheckBox({
 | 
			
		||||
                    type="checkbox"
 | 
			
		||||
                    id={uid}
 | 
			
		||||
                    disabled={disabled}
 | 
			
		||||
 | 
			
		||||
                    {...props}
 | 
			
		||||
                />
 | 
			
		||||
                <label className="custom-control-label" htmlFor={uid}>
 | 
			
		||||
                    {label}
 | 
			
		||||
                    {helpText && <small className="form-text text-muted">{helpText}</small>}
 | 
			
		||||
                    {helpText && (
 | 
			
		||||
                        <small className="form-text text-muted">
 | 
			
		||||
                            {helpText}
 | 
			
		||||
                        </small>
 | 
			
		||||
                    )}
 | 
			
		||||
                </label>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,17 @@
 | 
			
		||||
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.
 | 
			
		||||
Checkbox with label Bootstrap component with predefined sizes and structure for
 | 
			
		||||
using in foris forms.
 | 
			
		||||
 | 
			
		||||
All additional `props` are passed to the `<input type="checkbox">` HTML
 | 
			
		||||
component.
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
import {useState} from 'react';
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
const [value, setValue] = useState(false);
 | 
			
		||||
 | 
			
		||||
<CheckBox
 | 
			
		||||
    value={value}
 | 
			
		||||
    label="Some label" 
 | 
			
		||||
    label="Some label"
 | 
			
		||||
    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";
 | 
			
		||||
 | 
			
		||||
export function DataTimeInput({
 | 
			
		||||
    value, onChange, isValidDate, dateFormat, timeFormat, children, ...props
 | 
			
		||||
    value,
 | 
			
		||||
    onChange,
 | 
			
		||||
    isValidDate,
 | 
			
		||||
    dateFormat,
 | 
			
		||||
    timeFormat,
 | 
			
		||||
    children,
 | 
			
		||||
    ...props
 | 
			
		||||
}) {
 | 
			
		||||
    function renderInput(datetimeProps) {
 | 
			
		||||
        return (
 | 
			
		||||
            <Input
 | 
			
		||||
                {...props}
 | 
			
		||||
                {...datetimeProps}
 | 
			
		||||
            >
 | 
			
		||||
            <Input {...props} {...datetimeProps}>
 | 
			
		||||
                {children}
 | 
			
		||||
            </Input>
 | 
			
		||||
        );
 | 
			
		||||
@@ -54,8 +57,12 @@ export function DataTimeInput({
 | 
			
		||||
    return (
 | 
			
		||||
        <Datetime
 | 
			
		||||
            locale={ForisTranslations.locale}
 | 
			
		||||
            dateFormat={dateFormat !== undefined ? dateFormat : DEFAULT_DATE_FORMAT}
 | 
			
		||||
            timeFormat={timeFormat !== undefined ? timeFormat : DEFAULT_TIME_FORMAT}
 | 
			
		||||
            dateFormat={
 | 
			
		||||
                dateFormat !== undefined ? dateFormat : DEFAULT_DATE_FORMAT
 | 
			
		||||
            }
 | 
			
		||||
            timeFormat={
 | 
			
		||||
                timeFormat !== undefined ? timeFormat : DEFAULT_TIME_FORMAT
 | 
			
		||||
            }
 | 
			
		||||
            value={value}
 | 
			
		||||
            onChange={onChange}
 | 
			
		||||
            isValidDate={isValidDate}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +1,26 @@
 | 
			
		||||
Adopted from `react-datetime/DateTime` datatime picker component.
 | 
			
		||||
It uses `momentjs` see example.
 | 
			
		||||
Adopted from `react-datetime/DateTime` datatime picker component. It uses
 | 
			
		||||
`momentjs` see example.
 | 
			
		||||
 | 
			
		||||
It requires `ForisTranslations.locale` to be defined in order to use right locale.
 | 
			
		||||
It requires `ForisTranslations.locale` to be defined in order to use right
 | 
			
		||||
locale.
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
ForisTranslations={locale:'en'};
 | 
			
		||||
ForisTranslations = { locale: "en" };
 | 
			
		||||
 | 
			
		||||
import {useState, useEffect} from 'react';
 | 
			
		||||
import moment from 'moment/moment';
 | 
			
		||||
import { useState, useEffect } from "react";
 | 
			
		||||
import moment from "moment/moment";
 | 
			
		||||
 | 
			
		||||
const [dataTime, setDataTime] = useState(moment());
 | 
			
		||||
const [error, setError] = useState();
 | 
			
		||||
useEffect(()=>{
 | 
			
		||||
   dataTime.isValid() ? setError(null) : setError('Invalid value!');
 | 
			
		||||
},[dataTime]);
 | 
			
		||||
 
 | 
			
		||||
useEffect(() => {
 | 
			
		||||
    dataTime.isValid() ? setError(null) : setError("Invalid value!");
 | 
			
		||||
}, [dataTime]);
 | 
			
		||||
 | 
			
		||||
<DataTimeInput
 | 
			
		||||
    label='Time to sleep'
 | 
			
		||||
    label="Time to sleep"
 | 
			
		||||
    value={dataTime}
 | 
			
		||||
    error={error}
 | 
			
		||||
    helpText='Example helptext...'
 | 
			
		||||
    onChange={value => setDataTime(value)}
 | 
			
		||||
/>
 | 
			
		||||
    helpText="Example helptext..."
 | 
			
		||||
    onChange={(value) => setDataTime(value)}
 | 
			
		||||
/>;
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
@@ -23,11 +23,7 @@ DownloadButton.defaultProps = {
 | 
			
		||||
 | 
			
		||||
export function DownloadButton({ href, className, children }) {
 | 
			
		||||
    return (
 | 
			
		||||
        <a
 | 
			
		||||
            href={href}
 | 
			
		||||
            className={`btn ${className}`.trim()}
 | 
			
		||||
            download
 | 
			
		||||
        >
 | 
			
		||||
        <a href={href} className={`btn ${className}`.trim()} download>
 | 
			
		||||
            {children}
 | 
			
		||||
        </a>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,9 @@
 | 
			
		||||
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
 | 
			
		||||
<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.
 | 
			
		||||
It use built-in browser email address checking. It's only meaningful using inside `<form>`.
 | 
			
		||||
Bootstrap component of email input with label with predefined sizes and
 | 
			
		||||
structure for using in foris forms. It use built-in browser email address
 | 
			
		||||
checking. It's only meaningful using inside `<form>`.
 | 
			
		||||
 | 
			
		||||
All additional `props` are passed to the `<input type="email">` HTML component.
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
import {useState} from 'react';
 | 
			
		||||
const [email, setEmail] = useState('Wrong email');
 | 
			
		||||
<form onSubmit={e=>e.preventDefault()}>
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
const [email, setEmail] = useState("Wrong email");
 | 
			
		||||
<form onSubmit={(e) => e.preventDefault()}>
 | 
			
		||||
    <EmailInput
 | 
			
		||||
        value={email}
 | 
			
		||||
        label="Some label" 
 | 
			
		||||
        label="Some label"
 | 
			
		||||
        helpText="Read the small text!"
 | 
			
		||||
        onChange={event =>setEmail(event.target.value)}
 | 
			
		||||
        onChange={(event) => setEmail(event.target.value)}
 | 
			
		||||
    />
 | 
			
		||||
    <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.
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
import { useState } from 'react';
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
 | 
			
		||||
const [files, setFiles] = useState([]);
 | 
			
		||||
 | 
			
		||||
@@ -15,27 +16,33 @@ const label = files.length === 1 ? files[0].name : "Choose file";
 | 
			
		||||
        files={files}
 | 
			
		||||
        label={label}
 | 
			
		||||
        helpText="Will be uploaded"
 | 
			
		||||
        onChange={event=>setFiles(event.target.files)}
 | 
			
		||||
        onChange={(event) => setFiles(event.target.files)}
 | 
			
		||||
    />
 | 
			
		||||
</form>
 | 
			
		||||
</form>;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### FileInput with multiple files
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
import { useState } from 'react';
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
 | 
			
		||||
const [files, setFiles] = useState([]);
 | 
			
		||||
 | 
			
		||||
// 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">
 | 
			
		||||
    <FileInput
 | 
			
		||||
        files={files}
 | 
			
		||||
        label={label}
 | 
			
		||||
        helpText="Will be uploaded"
 | 
			
		||||
        onChange={event=>setFiles(event.target.files)}
 | 
			
		||||
        onChange={(event) => setFiles(event.target.files)}
 | 
			
		||||
        multiple
 | 
			
		||||
    />
 | 
			
		||||
</form>
 | 
			
		||||
</form>;
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
@@ -25,25 +25,38 @@ Input.propTypes = {
 | 
			
		||||
 | 
			
		||||
/** Base bootstrap input component. */
 | 
			
		||||
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 inputClassName = `form-control ${className || ""} ${(error ? "is-invalid" : "")}`.trim();
 | 
			
		||||
    const inputClassName = `form-control ${className || ""} ${
 | 
			
		||||
        error ? "is-invalid" : ""
 | 
			
		||||
    }`.trim();
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="form-group">
 | 
			
		||||
            <label className={labelClassName} htmlFor={uid}>{label}</label>
 | 
			
		||||
            <label className={labelClassName} htmlFor={uid}>
 | 
			
		||||
                {label}
 | 
			
		||||
            </label>
 | 
			
		||||
            <div className={`input-group ${groupClassName || ""}`.trim()}>
 | 
			
		||||
                <input
 | 
			
		||||
                    className={inputClassName}
 | 
			
		||||
                    type={type}
 | 
			
		||||
                    id={uid}
 | 
			
		||||
 | 
			
		||||
                    {...props}
 | 
			
		||||
                />
 | 
			
		||||
                {children}
 | 
			
		||||
            </div>
 | 
			
		||||
            {error ? <div className="invalid-feedback">{error}</div> : null}
 | 
			
		||||
            {helpText ? <small className="form-text text-muted">{helpText}</small> : null}
 | 
			
		||||
            {helpText ? (
 | 
			
		||||
                <small className="form-text text-muted">{helpText}</small>
 | 
			
		||||
            ) : null}
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,6 @@
 | 
			
		||||
.modal.show {
 | 
			
		||||
    display: block;
 | 
			
		||||
    animation-name: modalFade;
 | 
			
		||||
    animation-duration: .3s;
 | 
			
		||||
    animation-duration: 0.3s;
 | 
			
		||||
    background: rgba(0, 0, 0, 0.2);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,9 +26,7 @@ Modal.propTypes = {
 | 
			
		||||
    ]).isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function Modal({
 | 
			
		||||
    shown, setShown, scrollable, children,
 | 
			
		||||
}) {
 | 
			
		||||
export function Modal({ shown, setShown, scrollable, children }) {
 | 
			
		||||
    const dialogRef = useRef();
 | 
			
		||||
 | 
			
		||||
    useClickOutside(dialogRef, () => setShown(false));
 | 
			
		||||
@@ -38,12 +36,12 @@ export function Modal({
 | 
			
		||||
            <div className={`modal fade ${shown ? "show" : ""}`} role="dialog">
 | 
			
		||||
                <div
 | 
			
		||||
                    ref={dialogRef}
 | 
			
		||||
                    className={`modal-dialog modal-dialog-centered${scrollable ? " modal-dialog-scrollable" : ""}`}
 | 
			
		||||
                    className={`modal-dialog modal-dialog-centered${
 | 
			
		||||
                        scrollable ? " modal-dialog-scrollable" : ""
 | 
			
		||||
                    }`}
 | 
			
		||||
                    role="document"
 | 
			
		||||
                >
 | 
			
		||||
                    <div className="modal-content">
 | 
			
		||||
                        {children}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="modal-content">{children}</div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </Portal>
 | 
			
		||||
@@ -59,7 +57,11 @@ export function ModalHeader({ setShown, title }) {
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="modal-header">
 | 
			
		||||
            <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>
 | 
			
		||||
            </button>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -85,9 +87,5 @@ ModalFooter.propTypes = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function ModalFooter({ children }) {
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="modal-footer">
 | 
			
		||||
            {children}
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
    return <div className="modal-footer">{children}</div>;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +1,36 @@
 | 
			
		||||
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
 | 
			
		||||
    <div id="modal-container"/>
 | 
			
		||||
<div id="modal-container" />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```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);
 | 
			
		||||
 | 
			
		||||
<>
 | 
			
		||||
    <Modal setShown={setShown} shown={shown}>
 | 
			
		||||
        <ModalHeader setShown={setShown} title='Warning!'/>
 | 
			
		||||
        <ModalBody><p>Bla bla bla...</p></ModalBody>
 | 
			
		||||
        <ModalHeader setShown={setShown} title="Warning!" />
 | 
			
		||||
        <ModalBody>
 | 
			
		||||
            <p>Bla bla bla...</p>
 | 
			
		||||
        </ModalBody>
 | 
			
		||||
        <ModalFooter>
 | 
			
		||||
            <button 
 | 
			
		||||
                className='btn btn-secondary' 
 | 
			
		||||
            <button
 | 
			
		||||
                className="btn btn-secondary"
 | 
			
		||||
                onClick={() => setShown(false)}
 | 
			
		||||
            >Skip it</button>
 | 
			
		||||
            >
 | 
			
		||||
                Skip it
 | 
			
		||||
            </button>
 | 
			
		||||
        </ModalFooter>
 | 
			
		||||
    </Modal>
 | 
			
		||||
    
 | 
			
		||||
    <button className='btn btn-secondary' onClick={()=>setShown(true)}>
 | 
			
		||||
 | 
			
		||||
    <button className="btn btn-secondary" onClick={() => setShown(true)}>
 | 
			
		||||
        Show modal
 | 
			
		||||
    </button>
 | 
			
		||||
</>
 | 
			
		||||
</>;
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ input[type="number"] {
 | 
			
		||||
    appearance: textfield;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input[type=number]::-webkit-inner-spin-button,
 | 
			
		||||
input[type=number]::-webkit-outer-spin-button {
 | 
			
		||||
input[type="number"]::-webkit-inner-spin-button,
 | 
			
		||||
input[type="number"]::-webkit-outer-spin-button {
 | 
			
		||||
    -webkit-appearance: none;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,10 +20,7 @@ NumberInput.propTypes = {
 | 
			
		||||
    /** Help text message. */
 | 
			
		||||
    helpText: PropTypes.string,
 | 
			
		||||
    /** Number value. */
 | 
			
		||||
    value: PropTypes.oneOfType([
 | 
			
		||||
        PropTypes.string,
 | 
			
		||||
        PropTypes.number,
 | 
			
		||||
    ]),
 | 
			
		||||
    value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
 | 
			
		||||
    /** Function called when value changes. */
 | 
			
		||||
    onChange: PropTypes.func.isRequired,
 | 
			
		||||
    /** Additional description dispaled to the right of input value. */
 | 
			
		||||
@@ -34,15 +31,21 @@ NumberInput.defaultProps = {
 | 
			
		||||
    value: 0,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function NumberInput({
 | 
			
		||||
    onChange, inlineText, value, ...props
 | 
			
		||||
}) {
 | 
			
		||||
export function NumberInput({ onChange, inlineText, value, ...props }) {
 | 
			
		||||
    function updateValue(initialValue, difference) {
 | 
			
		||||
        onChange({ target: { value: initialValue + difference } });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const enableIncrease = useConditionalTimeout({ callback: updateValue }, value, 1);
 | 
			
		||||
    const enableDecrease = useConditionalTimeout({ callback: updateValue }, value, -1);
 | 
			
		||||
    const enableIncrease = useConditionalTimeout(
 | 
			
		||||
        { callback: updateValue },
 | 
			
		||||
        value,
 | 
			
		||||
        1
 | 
			
		||||
    );
 | 
			
		||||
    const enableDecrease = useConditionalTimeout(
 | 
			
		||||
        { callback: updateValue },
 | 
			
		||||
        value,
 | 
			
		||||
        -1
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <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.
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
import {useState} from 'react';
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
const [value, setValue] = useState(42);
 | 
			
		||||
 | 
			
		||||
<NumberInput
 | 
			
		||||
    value={value}
 | 
			
		||||
    label="Some number" 
 | 
			
		||||
    label="Some number"
 | 
			
		||||
    helpText="Read the small text!"
 | 
			
		||||
    min='33'
 | 
			
		||||
    max='54'
 | 
			
		||||
    onChange={event =>setValue(event.target.value)}
 | 
			
		||||
/>
 | 
			
		||||
    min="33"
 | 
			
		||||
    max="54"
 | 
			
		||||
    onChange={(event) => setValue(event.target.value)}
 | 
			
		||||
/>;
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
@@ -31,22 +31,24 @@ export function PasswordInput({ withEye, ...props }) {
 | 
			
		||||
            autoComplete={isHidden ? "new-password" : null}
 | 
			
		||||
            {...props}
 | 
			
		||||
        >
 | 
			
		||||
            {withEye
 | 
			
		||||
                ? (
 | 
			
		||||
                    <div className="input-group-append">
 | 
			
		||||
                        <button
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            className="input-group-text"
 | 
			
		||||
                            onClick={(e) => {
 | 
			
		||||
                                e.preventDefault();
 | 
			
		||||
                                setHidden((shouldBeHidden) => !shouldBeHidden);
 | 
			
		||||
                            }}
 | 
			
		||||
                        >
 | 
			
		||||
                            <i className={`fa ${isHidden ? "fa-eye" : "fa-eye-slash"}`} />
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                )
 | 
			
		||||
                : null}
 | 
			
		||||
            {withEye ? (
 | 
			
		||||
                <div className="input-group-append">
 | 
			
		||||
                    <button
 | 
			
		||||
                        type="button"
 | 
			
		||||
                        className="input-group-text"
 | 
			
		||||
                        onClick={(e) => {
 | 
			
		||||
                            e.preventDefault();
 | 
			
		||||
                            setHidden((shouldBeHidden) => !shouldBeHidden);
 | 
			
		||||
                        }}
 | 
			
		||||
                    >
 | 
			
		||||
                        <i
 | 
			
		||||
                            className={`fa ${
 | 
			
		||||
                                isHidden ? "fa-eye" : "fa-eye-slash"
 | 
			
		||||
                            }`}
 | 
			
		||||
                        />
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            ) : null}
 | 
			
		||||
        </Input>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,18 @@
 | 
			
		||||
Password Bootstrap component input with label and predefined sizes and structure for using in foris forms.
 | 
			
		||||
Can be used with "eye" button, see example.
 | 
			
		||||
Password Bootstrap component input with label and predefined sizes and structure
 | 
			
		||||
for using in foris forms. Can be used with "eye" button, see example.
 | 
			
		||||
 | 
			
		||||
All additional `props` are passed to the `<input type="password">` HTML component.
 | 
			
		||||
All additional `props` are passed to the `<input type="password">` HTML
 | 
			
		||||
component.
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
import {useState} from 'react';
 | 
			
		||||
const [value, setValue] = useState('secret');
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
const [value, setValue] = useState("secret");
 | 
			
		||||
 | 
			
		||||
<PasswordInput
 | 
			
		||||
    withEye
 | 
			
		||||
    value={value}
 | 
			
		||||
    label="Some password" 
 | 
			
		||||
    label="Some password"
 | 
			
		||||
    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 . */
 | 
			
		||||
    label: PropTypes.string,
 | 
			
		||||
    /** Choices . */
 | 
			
		||||
    choices: PropTypes.arrayOf(PropTypes.shape({
 | 
			
		||||
        /** Choice lable . */
 | 
			
		||||
        label: PropTypes.oneOfType([
 | 
			
		||||
            PropTypes.string,
 | 
			
		||||
            PropTypes.element,
 | 
			
		||||
            PropTypes.node,
 | 
			
		||||
            PropTypes.arrayOf(PropTypes.node),
 | 
			
		||||
        ]).isRequired,
 | 
			
		||||
        /** Choice value . */
 | 
			
		||||
        value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
 | 
			
		||||
    })).isRequired,
 | 
			
		||||
    choices: PropTypes.arrayOf(
 | 
			
		||||
        PropTypes.shape({
 | 
			
		||||
            /** Choice lable . */
 | 
			
		||||
            label: PropTypes.oneOfType([
 | 
			
		||||
                PropTypes.string,
 | 
			
		||||
                PropTypes.element,
 | 
			
		||||
                PropTypes.node,
 | 
			
		||||
                PropTypes.arrayOf(PropTypes.node),
 | 
			
		||||
            ]).isRequired,
 | 
			
		||||
            /** Choice value . */
 | 
			
		||||
            value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
 | 
			
		||||
                .isRequired,
 | 
			
		||||
        })
 | 
			
		||||
    ).isRequired,
 | 
			
		||||
    /** Initial value . */
 | 
			
		||||
    value: PropTypes.string,
 | 
			
		||||
    /** Help text message . */
 | 
			
		||||
    helpText: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function RadioSet({
 | 
			
		||||
    name, label, choices, value, helpText, ...props
 | 
			
		||||
}) {
 | 
			
		||||
export function RadioSet({ name, label, choices, value, helpText, ...props }) {
 | 
			
		||||
    const uid = useUID();
 | 
			
		||||
    const radios = choices.map((choice, key) => {
 | 
			
		||||
        const id = `${name}-${key}`;
 | 
			
		||||
@@ -47,7 +48,6 @@ export function RadioSet({
 | 
			
		||||
                value={choice.value}
 | 
			
		||||
                helpText={choice.helpText}
 | 
			
		||||
                checked={choice.value === value}
 | 
			
		||||
 | 
			
		||||
                {...props}
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
@@ -55,9 +55,15 @@ export function RadioSet({
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="form-group">
 | 
			
		||||
            {label && <label htmlFor={uid} className="d-block">{label}</label>}
 | 
			
		||||
            {label && (
 | 
			
		||||
                <label htmlFor={uid} className="d-block">
 | 
			
		||||
                    {label}
 | 
			
		||||
                </label>
 | 
			
		||||
            )}
 | 
			
		||||
            {radios}
 | 
			
		||||
            {helpText && <small className="form-text text-muted">{helpText}</small>}
 | 
			
		||||
            {helpText && (
 | 
			
		||||
                <small className="form-text text-muted">{helpText}</small>
 | 
			
		||||
            )}
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
@@ -73,21 +79,28 @@ Radio.propTypes = {
 | 
			
		||||
    helpText: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function Radio({
 | 
			
		||||
    label, id, helpText, ...props
 | 
			
		||||
}) {
 | 
			
		||||
export function Radio({ label, id, helpText, ...props }) {
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <div className={`custom-control custom-radio ${!helpText ? "custom-control-inline" : ""}`.trim()}>
 | 
			
		||||
            <div
 | 
			
		||||
                className={`custom-control custom-radio ${
 | 
			
		||||
                    !helpText ? "custom-control-inline" : ""
 | 
			
		||||
                }`.trim()}
 | 
			
		||||
            >
 | 
			
		||||
                <input
 | 
			
		||||
                    id={id}
 | 
			
		||||
                    className="custom-control-input"
 | 
			
		||||
                    type="radio"
 | 
			
		||||
 | 
			
		||||
                    {...props}
 | 
			
		||||
                />
 | 
			
		||||
                <label className="custom-control-label" htmlFor={id}>{label}</label>
 | 
			
		||||
                {helpText && <small className="form-text text-muted mt-0 mb-3">{helpText}</small>}
 | 
			
		||||
                <label className="custom-control-label" htmlFor={id}>
 | 
			
		||||
                    {label}
 | 
			
		||||
                </label>
 | 
			
		||||
                {helpText && (
 | 
			
		||||
                    <small className="form-text text-muted mt-0 mb-3">
 | 
			
		||||
                        {helpText}
 | 
			
		||||
                    </small>
 | 
			
		||||
                )}
 | 
			
		||||
            </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.
 | 
			
		||||
 | 
			
		||||
Unless `helpText` is set for one of the options they are displayed inline.
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
import {useState} from 'react';
 | 
			
		||||
const CHOICES=[
 | 
			
		||||
    {value:'one',label:'1'},
 | 
			
		||||
    {value:'two',label:'2'},
 | 
			
		||||
    {value:'three',label:'3'},
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
const CHOICES = [
 | 
			
		||||
    { value: "one", label: "1" },
 | 
			
		||||
    { value: "two", label: "2" },
 | 
			
		||||
    { value: "three", label: "3" },
 | 
			
		||||
];
 | 
			
		||||
const [value, setValue] = useState(CHOICES[0].value);
 | 
			
		||||
 | 
			
		||||
@@ -17,10 +18,10 @@ const [value, setValue] = useState(CHOICES[0].value);
 | 
			
		||||
    {/*Yeah, it gets event, not value!*/}
 | 
			
		||||
    <RadioSet
 | 
			
		||||
        value={value}
 | 
			
		||||
        name='some-radio'
 | 
			
		||||
        name="some-radio"
 | 
			
		||||
        choices={CHOICES}
 | 
			
		||||
        onChange={event =>setValue(event.target.value)}
 | 
			
		||||
        onChange={(event) => setValue(event.target.value)}
 | 
			
		||||
    />
 | 
			
		||||
    <p>Selected value: {value}</p>
 | 
			
		||||
</>
 | 
			
		||||
</>;
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
@@ -15,35 +15,30 @@ Select.propTypes = {
 | 
			
		||||
    /** Choices if form of {value : "Label",...}. */
 | 
			
		||||
    choices: PropTypes.object.isRequired,
 | 
			
		||||
    /** Current value. */
 | 
			
		||||
    value: PropTypes.oneOfType([
 | 
			
		||||
        PropTypes.string,
 | 
			
		||||
        PropTypes.number,
 | 
			
		||||
    ]).isRequired,
 | 
			
		||||
    value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
 | 
			
		||||
    /** Help text message. */
 | 
			
		||||
    helpText: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function Select({
 | 
			
		||||
    label, choices, helpText, ...props
 | 
			
		||||
}) {
 | 
			
		||||
export function Select({ label, choices, helpText, ...props }) {
 | 
			
		||||
    const uid = useUID();
 | 
			
		||||
 | 
			
		||||
    const options = Object.keys(choices).sort(
 | 
			
		||||
        (a, b) => a - b || a.toString().localeCompare(b.toString()),
 | 
			
		||||
    ).map(
 | 
			
		||||
        (key) => <option key={key} value={key}>{choices[key]}</option>,
 | 
			
		||||
    );
 | 
			
		||||
    const options = Object.keys(choices)
 | 
			
		||||
        .sort((a, b) => a - b || a.toString().localeCompare(b.toString()))
 | 
			
		||||
        .map((key) => (
 | 
			
		||||
            <option key={key} value={key}>
 | 
			
		||||
                {choices[key]}
 | 
			
		||||
            </option>
 | 
			
		||||
        ));
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="form-group">
 | 
			
		||||
            <label htmlFor={uid}>{label}</label>
 | 
			
		||||
            <select
 | 
			
		||||
                className="custom-select"
 | 
			
		||||
                id={uid}
 | 
			
		||||
                {...props}
 | 
			
		||||
            >
 | 
			
		||||
            <select className="custom-select" id={uid} {...props}>
 | 
			
		||||
                {options}
 | 
			
		||||
            </select>
 | 
			
		||||
            {helpText ? <small className="form-text text-muted">{helpText}</small> : null}
 | 
			
		||||
            {helpText ? (
 | 
			
		||||
                <small className="form-text text-muted">{helpText}</small>
 | 
			
		||||
            ) : null}
 | 
			
		||||
        </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.
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
import {useState} from 'react';
 | 
			
		||||
const CHOICES={
 | 
			
		||||
    apple:'Apple',
 | 
			
		||||
    banana:'Banana',
 | 
			
		||||
    peach:'Peach',
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
const CHOICES = {
 | 
			
		||||
    apple: "Apple",
 | 
			
		||||
    banana: "Banana",
 | 
			
		||||
    peach: "Peach",
 | 
			
		||||
};
 | 
			
		||||
const [value, setValue] = useState(Object.keys(CHOICES)[0]);
 | 
			
		||||
 | 
			
		||||
@@ -17,9 +18,9 @@ const [value, setValue] = useState(Object.keys(CHOICES)[0]);
 | 
			
		||||
        label="Fruit"
 | 
			
		||||
        value={value}
 | 
			
		||||
        choices={CHOICES}
 | 
			
		||||
        onChange={event=>setValue(event.target.value)}
 | 
			
		||||
        onChange={(event) => setValue(event.target.value)}
 | 
			
		||||
    />
 | 
			
		||||
    <p>Selected choice label: {CHOICES[value]}</p>
 | 
			
		||||
    <p>Selected choice value: {value}</p>
 | 
			
		||||
</>
 | 
			
		||||
</>;
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.spinner-fs-background {
 | 
			
		||||
    background-color: rgba(2, 2, 2, .5);
 | 
			
		||||
    background-color: rgba(2, 2, 2, 0.5);
 | 
			
		||||
    color: rgb(230, 230, 230);
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
 
 | 
			
		||||
@@ -25,12 +25,12 @@ Spinner.defaultProps = {
 | 
			
		||||
    fullScreen: false,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function Spinner({
 | 
			
		||||
    fullScreen, children, className,
 | 
			
		||||
}) {
 | 
			
		||||
export function Spinner({ fullScreen, children, className }) {
 | 
			
		||||
    if (!fullScreen) {
 | 
			
		||||
        return (
 | 
			
		||||
            <div className={`spinner-wrapper ${className || "my-3 text-center"}`}>
 | 
			
		||||
            <div
 | 
			
		||||
                className={`spinner-wrapper ${className || "my-3 text-center"}`}
 | 
			
		||||
            >
 | 
			
		||||
                <SpinnerElement>{children}</SpinnerElement>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
@@ -61,7 +61,9 @@ export function SpinnerElement({ small, className, children }) {
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <div
 | 
			
		||||
                className={`spinner-border ${small ? "spinner-border-sm" : ""} ${className || ""}`.trim()}
 | 
			
		||||
                className={`spinner-border ${
 | 
			
		||||
                    small ? "spinner-border-sm" : ""
 | 
			
		||||
                } ${className || ""}`.trim()}
 | 
			
		||||
                role="status"
 | 
			
		||||
            >
 | 
			
		||||
                <span className="sr-only" />
 | 
			
		||||
 
 | 
			
		||||
@@ -16,29 +16,33 @@ Switch.propTypes = {
 | 
			
		||||
        PropTypes.node,
 | 
			
		||||
        PropTypes.arrayOf(PropTypes.node),
 | 
			
		||||
    ]).isRequired,
 | 
			
		||||
    id: PropTypes.string.isRequired,
 | 
			
		||||
    helpText: PropTypes.string,
 | 
			
		||||
    switchHeading: PropTypes.string,
 | 
			
		||||
    switchHeading: PropTypes.bool,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function Switch({
 | 
			
		||||
    label, id, helpText, switchHeading, ...props
 | 
			
		||||
}) {
 | 
			
		||||
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()}>
 | 
			
		||||
            <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>}
 | 
			
		||||
                {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.
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
import {useState} from 'react';
 | 
			
		||||
const [value, setValue] = useState('Bla bla');
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
const [value, setValue] = useState("Bla bla");
 | 
			
		||||
 | 
			
		||||
<TextInput
 | 
			
		||||
    value={value}
 | 
			
		||||
    label="Some text" 
 | 
			
		||||
    label="Some 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 />", () => {
 | 
			
		||||
    it("Render button correctly", () => {
 | 
			
		||||
        const { container } = render(<Button>Test Button</Button>);
 | 
			
		||||
        expect(container.firstChild)
 | 
			
		||||
            .toMatchSnapshot();
 | 
			
		||||
        expect(container.firstChild).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Render button with custom classes", () => {
 | 
			
		||||
        const { container } = render(<Button className="one two three">Test Button</Button>);
 | 
			
		||||
        expect(container.firstChild)
 | 
			
		||||
            .toMatchSnapshot();
 | 
			
		||||
        const { container } = render(
 | 
			
		||||
            <Button className="one two three">Test Button</Button>
 | 
			
		||||
        );
 | 
			
		||||
        expect(container.firstChild).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Render button with spinner", () => {
 | 
			
		||||
        const { container } = render(<Button loading>Test Button</Button>);
 | 
			
		||||
        expect(container.firstChild)
 | 
			
		||||
            .toMatchSnapshot();
 | 
			
		||||
        expect(container.firstChild).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -18,22 +18,16 @@ describe("<Checkbox/>", () => {
 | 
			
		||||
                label="Test label"
 | 
			
		||||
                checked
 | 
			
		||||
                helpText="Some help text"
 | 
			
		||||
                onChange={() => {
 | 
			
		||||
                }}
 | 
			
		||||
            />,
 | 
			
		||||
                onChange={() => {}}
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
        expect(container.firstChild)
 | 
			
		||||
            .toMatchSnapshot();
 | 
			
		||||
        expect(container.firstChild).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Render uncheked checkbox", () => {
 | 
			
		||||
        const { container } = render(
 | 
			
		||||
            <CheckBox
 | 
			
		||||
                label="Test label"
 | 
			
		||||
                helpText="Some help text"
 | 
			
		||||
            />,
 | 
			
		||||
            <CheckBox label="Test label" helpText="Some help text" />
 | 
			
		||||
        );
 | 
			
		||||
        expect(container.firstChild)
 | 
			
		||||
            .toMatchSnapshot();
 | 
			
		||||
        expect(container.firstChild).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,11 @@ import { DownloadButton } from "../DownloadButton";
 | 
			
		||||
 | 
			
		||||
describe("<DownloadButton />", () => {
 | 
			
		||||
    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();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -7,9 +7,7 @@
 | 
			
		||||
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    render, fireEvent, getByLabelText, wait,
 | 
			
		||||
} from "customTestRender";
 | 
			
		||||
import { render, fireEvent, getByLabelText, wait } from "customTestRender";
 | 
			
		||||
 | 
			
		||||
import { NumberInput } from "../NumberInput";
 | 
			
		||||
 | 
			
		||||
@@ -24,7 +22,7 @@ describe("<NumberInput/>", () => {
 | 
			
		||||
                helpText="Some help text"
 | 
			
		||||
                value={1}
 | 
			
		||||
                onChange={onChangeMock}
 | 
			
		||||
            />,
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
        componentContainer = container;
 | 
			
		||||
    });
 | 
			
		||||
@@ -36,12 +34,16 @@ describe("<NumberInput/>", () => {
 | 
			
		||||
    it("Increase number with button", async () => {
 | 
			
		||||
        const increaseButton = getByLabelText(componentContainer, "Increase");
 | 
			
		||||
        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 () => {
 | 
			
		||||
        const decreaseButton = getByLabelText(componentContainer, "Decrease");
 | 
			
		||||
        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"
 | 
			
		||||
                helpText="Some help text"
 | 
			
		||||
                value="Some password"
 | 
			
		||||
                onChange={() => {
 | 
			
		||||
                }}
 | 
			
		||||
            />,
 | 
			
		||||
                onChange={() => {}}
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
        expect(container.firstChild)
 | 
			
		||||
            .toMatchSnapshot();
 | 
			
		||||
        expect(container.firstChild).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -35,11 +35,9 @@ describe("<RadioSet/>", () => {
 | 
			
		||||
                value="value"
 | 
			
		||||
                choices={TEST_CHOICES}
 | 
			
		||||
                helpText="Some help text"
 | 
			
		||||
                onChange={() => {
 | 
			
		||||
                }}
 | 
			
		||||
            />,
 | 
			
		||||
                onChange={() => {}}
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
        expect(container.firstChild)
 | 
			
		||||
            .toMatchSnapshot();
 | 
			
		||||
        expect(container.firstChild).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,10 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    fireEvent, getByDisplayValue, getByText, render,
 | 
			
		||||
    fireEvent,
 | 
			
		||||
    getByDisplayValue,
 | 
			
		||||
    getByText,
 | 
			
		||||
    render,
 | 
			
		||||
} from "customTestRender";
 | 
			
		||||
 | 
			
		||||
import { Select } from "../Select";
 | 
			
		||||
@@ -29,29 +32,24 @@ describe("<Select/>", () => {
 | 
			
		||||
                value="1"
 | 
			
		||||
                choices={TEST_CHOICES}
 | 
			
		||||
                helpText="Help text"
 | 
			
		||||
 | 
			
		||||
                onChange={onChangeHandler}
 | 
			
		||||
            />,
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
        selectContainer = container;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Test with snapshot.", () => {
 | 
			
		||||
        expect(selectContainer)
 | 
			
		||||
            .toMatchSnapshot();
 | 
			
		||||
        expect(selectContainer).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Test onChange handling.", () => {
 | 
			
		||||
        const select = getByDisplayValue(selectContainer, "one");
 | 
			
		||||
        expect(select.value)
 | 
			
		||||
            .toBe("1");
 | 
			
		||||
        expect(select.value).toBe("1");
 | 
			
		||||
        fireEvent.change(select, { target: { value: "2" } });
 | 
			
		||||
 | 
			
		||||
        const option = getByText(selectContainer, "two");
 | 
			
		||||
        expect(onChangeHandler)
 | 
			
		||||
            .toBeCalled();
 | 
			
		||||
        expect(onChangeHandler).toBeCalled();
 | 
			
		||||
 | 
			
		||||
        expect(option.value)
 | 
			
		||||
            .toBe("2");
 | 
			
		||||
        expect(option.value).toBe("2");
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -18,11 +18,9 @@ describe("<TextInput/>", () => {
 | 
			
		||||
                label="Test label"
 | 
			
		||||
                helpText="Some help text"
 | 
			
		||||
                value="Some text"
 | 
			
		||||
                onChange={() => {
 | 
			
		||||
                }}
 | 
			
		||||
            />,
 | 
			
		||||
                onChange={() => {}}
 | 
			
		||||
            />
 | 
			
		||||
        );
 | 
			
		||||
        expect(container.firstChild)
 | 
			
		||||
            .toMatchSnapshot();
 | 
			
		||||
        expect(container.firstChild).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,6 @@ exports[`<Button /> Render button correctly 1`] = `
 | 
			
		||||
  class="btn btn-primary "
 | 
			
		||||
  type="button"
 | 
			
		||||
>
 | 
			
		||||
   
 | 
			
		||||
   
 | 
			
		||||
  Test Button
 | 
			
		||||
</button>
 | 
			
		||||
`;
 | 
			
		||||
@@ -16,8 +14,6 @@ exports[`<Button /> Render button with custom classes 1`] = `
 | 
			
		||||
  class="btn one two three"
 | 
			
		||||
  type="button"
 | 
			
		||||
>
 | 
			
		||||
   
 | 
			
		||||
   
 | 
			
		||||
  Test Button
 | 
			
		||||
</button>
 | 
			
		||||
`;
 | 
			
		||||
@@ -33,8 +29,6 @@ exports[`<Button /> Render button with spinner 1`] = `
 | 
			
		||||
    role="status"
 | 
			
		||||
  />
 | 
			
		||||
   
 | 
			
		||||
   
 | 
			
		||||
   
 | 
			
		||||
  Test Button
 | 
			
		||||
</button>
 | 
			
		||||
`;
 | 
			
		||||
 
 | 
			
		||||
@@ -13,9 +13,7 @@ import { API_STATE } from "../api/utils";
 | 
			
		||||
import { ForisURLs } from "../utils/forisUrls";
 | 
			
		||||
 | 
			
		||||
import { Button } from "../bootstrap/Button";
 | 
			
		||||
import {
 | 
			
		||||
    Modal, ModalHeader, ModalBody, ModalFooter,
 | 
			
		||||
} from "../bootstrap/Modal";
 | 
			
		||||
import { Modal, ModalHeader, ModalBody, ModalFooter } from "../bootstrap/Modal";
 | 
			
		||||
import { useAlert } from "../alertContext/AlertContext";
 | 
			
		||||
 | 
			
		||||
export function RebootButton(props) {
 | 
			
		||||
@@ -38,13 +36,16 @@ export function RebootButton(props) {
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <RebootModal shown={modalShown} setShown={setModalShown} onReboot={rebootHandler} />
 | 
			
		||||
            <RebootModal
 | 
			
		||||
                shown={modalShown}
 | 
			
		||||
                setShown={setModalShown}
 | 
			
		||||
                onReboot={rebootHandler}
 | 
			
		||||
            />
 | 
			
		||||
            <Button
 | 
			
		||||
                className="btn-danger"
 | 
			
		||||
                loading={triggered}
 | 
			
		||||
                disabled={triggered}
 | 
			
		||||
                onClick={() => setModalShown(true)}
 | 
			
		||||
 | 
			
		||||
                {...props}
 | 
			
		||||
            >
 | 
			
		||||
                {_("Reboot")}
 | 
			
		||||
@@ -63,10 +64,14 @@ function RebootModal({ shown, setShown, onReboot }) {
 | 
			
		||||
    return (
 | 
			
		||||
        <Modal shown={shown} setShown={setShown}>
 | 
			
		||||
            <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>
 | 
			
		||||
                <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>
 | 
			
		||||
        </Modal>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -25,11 +25,10 @@ export default function ResetWiFiSettings({ ws, endpoint }) {
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        const module = "wifi";
 | 
			
		||||
        ws.subscribe(module)
 | 
			
		||||
            .bind(module, "reset", () => {
 | 
			
		||||
                // eslint-disable-next-line no-restricted-globals
 | 
			
		||||
                setTimeout(() => location.reload(), 1000);
 | 
			
		||||
            });
 | 
			
		||||
        ws.subscribe(module).bind(module, "reset", () => {
 | 
			
		||||
            // eslint-disable-next-line no-restricted-globals
 | 
			
		||||
            setTimeout(() => location.reload(), 1000);
 | 
			
		||||
        });
 | 
			
		||||
    }, [ws]);
 | 
			
		||||
 | 
			
		||||
    const [postResetResponse, postReset] = useAPIPost(endpoint);
 | 
			
		||||
@@ -38,7 +37,10 @@ export default function ResetWiFiSettings({ ws, endpoint }) {
 | 
			
		||||
        if (postResetResponse.state === API_STATE.ERROR) {
 | 
			
		||||
            setAlert(_("An error occurred during resetting Wi-Fi settings."));
 | 
			
		||||
        } 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]);
 | 
			
		||||
 | 
			
		||||
@@ -63,7 +65,6 @@ current Wi-Fi configuration and restore the default values.
 | 
			
		||||
                    forisFormSize
 | 
			
		||||
                    loading={isLoading}
 | 
			
		||||
                    disabled={isLoading}
 | 
			
		||||
 | 
			
		||||
                    onClick={onReset}
 | 
			
		||||
                >
 | 
			
		||||
                    {_("Reset Wi-Fi Settings")}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,10 @@ import PropTypes from "prop-types";
 | 
			
		||||
import { ForisURLs } from "../../utils/forisUrls";
 | 
			
		||||
import { Button } from "../../bootstrap/Button";
 | 
			
		||||
import {
 | 
			
		||||
    Modal, ModalBody, ModalFooter, ModalHeader,
 | 
			
		||||
    Modal,
 | 
			
		||||
    ModalBody,
 | 
			
		||||
    ModalFooter,
 | 
			
		||||
    ModalHeader,
 | 
			
		||||
} from "../../bootstrap/Modal";
 | 
			
		||||
import { createAndDownloadPdf, toQRCodeContent } from "./qrCodeHelpers";
 | 
			
		||||
 | 
			
		||||
@@ -36,11 +39,21 @@ export default function WiFiQRCode({ SSID, password }) {
 | 
			
		||||
                    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>
 | 
			
		||||
            {modal
 | 
			
		||||
                ? <QRCodeModal setShown={setModal} shown={modal} SSID={SSID} password={password} />
 | 
			
		||||
                : null}
 | 
			
		||||
            {modal ? (
 | 
			
		||||
                <QRCodeModal
 | 
			
		||||
                    setShown={setModal}
 | 
			
		||||
                    shown={modal}
 | 
			
		||||
                    SSID={SSID}
 | 
			
		||||
                    password={password}
 | 
			
		||||
                />
 | 
			
		||||
            ) : null}
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
@@ -52,9 +65,7 @@ QRCodeModal.propTypes = {
 | 
			
		||||
    setShown: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function QRCodeModal({
 | 
			
		||||
    shown, setShown, SSID, password,
 | 
			
		||||
}) {
 | 
			
		||||
function QRCodeModal({ shown, setShown, SSID, password }) {
 | 
			
		||||
    return (
 | 
			
		||||
        <Modal setShown={setShown} shown={shown}>
 | 
			
		||||
            <ModalHeader setShown={setShown} title={_("Wi-Fi QR Code")} />
 | 
			
		||||
 
 | 
			
		||||
@@ -19,9 +19,7 @@ WiFiSettings.propTypes = {
 | 
			
		||||
    hasGuestNetwork: PropTypes.bool,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function WiFiSettings({
 | 
			
		||||
    ws, endpoint, resetEndpoint, hasGuestNetwork,
 | 
			
		||||
}) {
 | 
			
		||||
export function WiFiSettings({ ws, endpoint, resetEndpoint, hasGuestNetwork }) {
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <ForisForm
 | 
			
		||||
@@ -59,35 +57,41 @@ function prepDataToSubmit(formData) {
 | 
			
		||||
            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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function validator(formData) {
 | 
			
		||||
    const formErrors = formData.devices.map(
 | 
			
		||||
        (device) => {
 | 
			
		||||
            if (!device.enabled) return {};
 | 
			
		||||
    const formErrors = formData.devices.map((device) => {
 | 
			
		||||
        if (!device.enabled) return {};
 | 
			
		||||
 | 
			
		||||
            const errors = {};
 | 
			
		||||
            if (device.SSID.length > 32) errors.SSID = _("SSID can't be longer than 32 symbols");
 | 
			
		||||
            if (device.SSID.length === 0) errors.SSID = _("SSID can't be empty");
 | 
			
		||||
        const errors = {};
 | 
			
		||||
        if (device.SSID.length > 32)
 | 
			
		||||
            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 = {};
 | 
			
		||||
            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 === 0) guest_wifi_errors.SSID = _("SSID can't be empty");
 | 
			
		||||
        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 === 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) {
 | 
			
		||||
                errors.guest_wifi = guest_wifi_errors;
 | 
			
		||||
            }
 | 
			
		||||
            return errors;
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
        if (guest_wifi_errors.SSID || guest_wifi_errors.password) {
 | 
			
		||||
            errors.guest_wifi = guest_wifi_errors;
 | 
			
		||||
        }
 | 
			
		||||
        return errors;
 | 
			
		||||
    });
 | 
			
		||||
    return JSON.stringify(formErrors).match(/\[[{},?]+\]/) ? null : formErrors;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,19 +22,34 @@ describe("<ResetWiFiSettings/>", () => {
 | 
			
		||||
    let getAllByText;
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
        ({ getAllByText } = render(<ResetWiFiSettings ws={webSockets} endpoint={endpoint} />));
 | 
			
		||||
        ({ getAllByText } = render(
 | 
			
		||||
            <ResetWiFiSettings ws={webSockets} endpoint={endpoint} />
 | 
			
		||||
        ));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should display alert on open ports - success", async () => {
 | 
			
		||||
        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" } });
 | 
			
		||||
        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 () => {
 | 
			
		||||
        fireEvent.click(getAllByText("Reset Wi-Fi Settings")[1]);
 | 
			
		||||
        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."
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -292,11 +292,7 @@ export function wifiSettingsFixture() {
 | 
			
		||||
                                radar: false,
 | 
			
		||||
                            },
 | 
			
		||||
                        ],
 | 
			
		||||
                        available_htmodes: [
 | 
			
		||||
                            "NOHT",
 | 
			
		||||
                            "HT20",
 | 
			
		||||
                            "HT40",
 | 
			
		||||
                        ],
 | 
			
		||||
                        available_htmodes: ["NOHT", "HT20", "HT40"],
 | 
			
		||||
                        hwmode: "11g",
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
@@ -327,9 +323,9 @@ const oneDevice = {
 | 
			
		||||
            htmode: "HT40",
 | 
			
		||||
            hwmode: "11a",
 | 
			
		||||
            id: 0,
 | 
			
		||||
            password: "TestPass"
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
            password: "TestPass",
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const twoDevices = {
 | 
			
		||||
@@ -343,7 +339,7 @@ const twoDevices = {
 | 
			
		||||
            htmode: "HT40",
 | 
			
		||||
            hwmode: "11a",
 | 
			
		||||
            id: 0,
 | 
			
		||||
            password: "TestPass"
 | 
			
		||||
            password: "TestPass",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            SSID: "Turris2",
 | 
			
		||||
@@ -354,9 +350,9 @@ const twoDevices = {
 | 
			
		||||
            htmode: "HT40",
 | 
			
		||||
            hwmode: "11a",
 | 
			
		||||
            id: 0,
 | 
			
		||||
            password: "TestPass"
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
            password: "TestPass",
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const threeDevices = {
 | 
			
		||||
@@ -370,7 +366,7 @@ const threeDevices = {
 | 
			
		||||
            htmode: "HT40",
 | 
			
		||||
            hwmode: "11a",
 | 
			
		||||
            id: 0,
 | 
			
		||||
            password: "TestPass"
 | 
			
		||||
            password: "TestPass",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            SSID: "Turris2",
 | 
			
		||||
@@ -381,7 +377,7 @@ const threeDevices = {
 | 
			
		||||
            htmode: "HT40",
 | 
			
		||||
            hwmode: "11a",
 | 
			
		||||
            id: 0,
 | 
			
		||||
            password: "TestPass"
 | 
			
		||||
            password: "TestPass",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            SSID: "Turris3",
 | 
			
		||||
@@ -392,9 +388,9 @@ const threeDevices = {
 | 
			
		||||
            htmode: "HT40",
 | 
			
		||||
            hwmode: "11a",
 | 
			
		||||
            id: 0,
 | 
			
		||||
            password: ""
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
            password: "",
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export {oneDevice, twoDevices, threeDevices};
 | 
			
		||||
export { oneDevice, twoDevices, threeDevices };
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,9 @@ export const HELP_TEXTS = {
 | 
			
		||||
    password: _(`
 | 
			
		||||
        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: _(`
 | 
			
		||||
        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
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,11 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    fireEvent, getByText, queryByText, render, wait,
 | 
			
		||||
    fireEvent,
 | 
			
		||||
    getByText,
 | 
			
		||||
    queryByText,
 | 
			
		||||
    render,
 | 
			
		||||
    wait,
 | 
			
		||||
} from "customTestRender";
 | 
			
		||||
import mockAxios from "jest-mock-axios";
 | 
			
		||||
import { mockJSONError } from "testUtils/network";
 | 
			
		||||
@@ -19,38 +23,41 @@ import { RebootButton } from "../RebootButton";
 | 
			
		||||
describe("<RebootButton/>", () => {
 | 
			
		||||
    let componentContainer;
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
        const { container } = render(<>
 | 
			
		||||
            <div id="modal-container" />
 | 
			
		||||
            <RebootButton />
 | 
			
		||||
        </>);
 | 
			
		||||
        const { container } = render(
 | 
			
		||||
            <>
 | 
			
		||||
                <div id="modal-container" />
 | 
			
		||||
                <RebootButton />
 | 
			
		||||
            </>
 | 
			
		||||
        );
 | 
			
		||||
        componentContainer = container;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Render.", () => {
 | 
			
		||||
        expect(componentContainer)
 | 
			
		||||
            .toMatchSnapshot();
 | 
			
		||||
        expect(componentContainer).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Render modal.", () => {
 | 
			
		||||
        expect(queryByText(componentContainer, "Confirm reboot"))
 | 
			
		||||
            .toBeNull();
 | 
			
		||||
        expect(queryByText(componentContainer, "Confirm reboot")).toBeNull();
 | 
			
		||||
        fireEvent.click(getByText(componentContainer, "Reboot"));
 | 
			
		||||
        expect(componentContainer)
 | 
			
		||||
            .toMatchSnapshot();
 | 
			
		||||
        expect(componentContainer).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Confirm reboot.", () => {
 | 
			
		||||
        fireEvent.click(getByText(componentContainer, "Reboot"));
 | 
			
		||||
        fireEvent.click(getByText(componentContainer, "Confirm reboot"));
 | 
			
		||||
        expect(mockAxios.post)
 | 
			
		||||
            .toHaveBeenCalledWith("/reforis/api/reboot", undefined, expect.anything());
 | 
			
		||||
        expect(mockAxios.post).toHaveBeenCalledWith(
 | 
			
		||||
            "/reforis/api/reboot",
 | 
			
		||||
            undefined,
 | 
			
		||||
            expect.anything()
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Hold error.", async () => {
 | 
			
		||||
        fireEvent.click(getByText(componentContainer, "Reboot"));
 | 
			
		||||
        fireEvent.click(getByText(componentContainer, "Confirm reboot"));
 | 
			
		||||
        mockJSONError();
 | 
			
		||||
        await wait(() => expect(mockSetAlert)
 | 
			
		||||
            .toBeCalledWith("Reboot request failed."));
 | 
			
		||||
        await wait(() =>
 | 
			
		||||
            expect(mockSetAlert).toBeCalledWith("Reboot request failed.")
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -49,16 +49,12 @@ exports[`<RebootButton/> Render modal. 1`] = `
 | 
			
		||||
              class="btn btn-primary "
 | 
			
		||||
              type="button"
 | 
			
		||||
            >
 | 
			
		||||
               
 | 
			
		||||
               
 | 
			
		||||
              Cancel
 | 
			
		||||
            </button>
 | 
			
		||||
            <button
 | 
			
		||||
              class="btn btn-danger"
 | 
			
		||||
              type="button"
 | 
			
		||||
            >
 | 
			
		||||
               
 | 
			
		||||
               
 | 
			
		||||
              Confirm reboot
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -70,8 +66,6 @@ exports[`<RebootButton/> Render modal. 1`] = `
 | 
			
		||||
    class="btn btn-danger"
 | 
			
		||||
    type="button"
 | 
			
		||||
  >
 | 
			
		||||
     
 | 
			
		||||
     
 | 
			
		||||
    Reboot
 | 
			
		||||
  </button>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -86,8 +80,6 @@ exports[`<RebootButton/> Render. 1`] = `
 | 
			
		||||
    class="btn btn-danger"
 | 
			
		||||
    type="button"
 | 
			
		||||
  >
 | 
			
		||||
     
 | 
			
		||||
     
 | 
			
		||||
    Reboot
 | 
			
		||||
  </button>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -13,17 +13,14 @@ import { STATES, SubmitButton } from "../components/SubmitButton";
 | 
			
		||||
describe("<SubmitButton/>", () => {
 | 
			
		||||
    it("Render ready", () => {
 | 
			
		||||
        const { container } = render(<SubmitButton state={STATES.READY} />);
 | 
			
		||||
        expect(container)
 | 
			
		||||
            .toMatchSnapshot();
 | 
			
		||||
        expect(container).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
    it("Render saving", () => {
 | 
			
		||||
        const { container } = render(<SubmitButton state={STATES.SAVING} />);
 | 
			
		||||
        expect(container)
 | 
			
		||||
            .toMatchSnapshot();
 | 
			
		||||
        expect(container).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
    it("Render load", () => {
 | 
			
		||||
        const { container } = render(<SubmitButton state={STATES.LOAD} />);
 | 
			
		||||
        expect(container)
 | 
			
		||||
            .toMatchSnapshot();
 | 
			
		||||
        expect(container).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,6 @@ exports[`<SubmitButton/> Render load 1`] = `
 | 
			
		||||
      role="status"
 | 
			
		||||
    />
 | 
			
		||||
     
 | 
			
		||||
     
 | 
			
		||||
     
 | 
			
		||||
    Load settings
 | 
			
		||||
  </button>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -26,8 +24,6 @@ exports[`<SubmitButton/> Render ready 1`] = `
 | 
			
		||||
    class="btn btn-primary  col-sm-12 col-md-3 col-lg-2"
 | 
			
		||||
    type="submit"
 | 
			
		||||
  >
 | 
			
		||||
     
 | 
			
		||||
     
 | 
			
		||||
    Save
 | 
			
		||||
  </button>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -46,8 +42,6 @@ exports[`<SubmitButton/> Render saving 1`] = `
 | 
			
		||||
      role="status"
 | 
			
		||||
    />
 | 
			
		||||
     
 | 
			
		||||
     
 | 
			
		||||
     
 | 
			
		||||
    Updating
 | 
			
		||||
  </button>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,9 +7,7 @@
 | 
			
		||||
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    act, fireEvent, render, waitForElement,
 | 
			
		||||
} from "customTestRender";
 | 
			
		||||
import { act, fireEvent, render, waitForElement } from "customTestRender";
 | 
			
		||||
import mockAxios from "jest-mock-axios";
 | 
			
		||||
import { WebSockets } from "webSockets/WebSockets";
 | 
			
		||||
import { ForisForm } from "../components/ForisForm";
 | 
			
		||||
@@ -38,8 +36,12 @@ describe("useForm hook.", () => {
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
        mockPrepData = jest.fn(() => ({ field: "preparedData" }));
 | 
			
		||||
        mockPrepDataToSubmit = jest.fn(() => ({ field: "preparedDataToSubmit" }));
 | 
			
		||||
        mockValidator = jest.fn((data) => (data.field === "invalidValue" ? { field: "Error" } : {}));
 | 
			
		||||
        mockPrepDataToSubmit = jest.fn(() => ({
 | 
			
		||||
            field: "preparedDataToSubmit",
 | 
			
		||||
        }));
 | 
			
		||||
        mockValidator = jest.fn((data) =>
 | 
			
		||||
            data.field === "invalidValue" ? { field: "Error" } : {}
 | 
			
		||||
        );
 | 
			
		||||
        const { getByTestId, container } = render(
 | 
			
		||||
            <ForisForm
 | 
			
		||||
                ws={new WebSockets()}
 | 
			
		||||
@@ -53,7 +55,7 @@ describe("useForm hook.", () => {
 | 
			
		||||
                validator={mockValidator}
 | 
			
		||||
            >
 | 
			
		||||
                <Child />
 | 
			
		||||
            </ForisForm>,
 | 
			
		||||
            </ForisForm>
 | 
			
		||||
        );
 | 
			
		||||
        mockAxios.mockResponse({ field: "fetchedData" });
 | 
			
		||||
 | 
			
		||||
@@ -67,16 +69,22 @@ describe("useForm hook.", () => {
 | 
			
		||||
        expect(Child.mock.calls[0][0].formErrors).toMatchObject({});
 | 
			
		||||
 | 
			
		||||
        act(() => {
 | 
			
		||||
            fireEvent.change(input, { target: { value: "invalidValue", type: "text" } });
 | 
			
		||||
            fireEvent.change(input, {
 | 
			
		||||
                target: { value: "invalidValue", type: "text" },
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(Child).toHaveBeenCalledTimes(2);
 | 
			
		||||
        expect(mockValidator).toHaveBeenCalledTimes(2);
 | 
			
		||||
        expect(Child.mock.calls[1][0].formErrors).toMatchObject({ field: "Error" });
 | 
			
		||||
        expect(Child.mock.calls[1][0].formErrors).toMatchObject({
 | 
			
		||||
            field: "Error",
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -86,14 +94,21 @@ describe("useForm hook.", () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("Fetch data.", () => {
 | 
			
		||||
        expect(mockAxios.get).toHaveBeenCalledWith("testEndpoint", expect.anything());
 | 
			
		||||
        expect(mockAxios.get).toHaveBeenCalledWith(
 | 
			
		||||
            "testEndpoint",
 | 
			
		||||
            expect.anything()
 | 
			
		||||
        );
 | 
			
		||||
        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.", () => {
 | 
			
		||||
@@ -107,7 +122,7 @@ describe("useForm hook.", () => {
 | 
			
		||||
        expect(mockAxios.post).toHaveBeenCalledWith(
 | 
			
		||||
            "testEndpoint",
 | 
			
		||||
            { field: "preparedDataToSubmit" },
 | 
			
		||||
            expect.anything(),
 | 
			
		||||
            expect.anything()
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -16,120 +16,75 @@ import {
 | 
			
		||||
 | 
			
		||||
describe("Validation functions", () => {
 | 
			
		||||
    it("validateIPv4Address valid", () => {
 | 
			
		||||
        expect(validateIPv4Address("192.168.1.1"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv4Address("1.1.1.1"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv4Address("0.0.0.0"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv4Address("192.168.1.1")).toBe(undefined);
 | 
			
		||||
        expect(validateIPv4Address("1.1.1.1")).toBe(undefined);
 | 
			
		||||
        expect(validateIPv4Address("0.0.0.0")).toBe(undefined);
 | 
			
		||||
    });
 | 
			
		||||
    it("validateIPv4Address invalid", () => {
 | 
			
		||||
        expect(validateIPv4Address("invalid"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv4Address("192.256.1.1"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv4Address("192.168.256.1"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv4Address("192.168.1.256"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv4Address("192.168.1.256"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv4Address("invalid")).not.toBe(undefined);
 | 
			
		||||
        expect(validateIPv4Address("192.256.1.1")).not.toBe(undefined);
 | 
			
		||||
        expect(validateIPv4Address("192.168.256.1")).not.toBe(undefined);
 | 
			
		||||
        expect(validateIPv4Address("192.168.1.256")).not.toBe(undefined);
 | 
			
		||||
        expect(validateIPv4Address("192.168.1.256")).not.toBe(undefined);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("validateIPv6Address valid", () => {
 | 
			
		||||
        expect(validateIPv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:7334"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Address("0:0:0:0:0:0:0:1"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Address("::1"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Address("::"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(
 | 
			
		||||
            validateIPv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
 | 
			
		||||
        ).toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Address("0:0:0:0:0:0:0:1")).toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Address("::1")).toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Address("::")).toBe(undefined);
 | 
			
		||||
    });
 | 
			
		||||
    it("validateIPv6Address invalid", () => {
 | 
			
		||||
        expect(validateIPv6Address("invalid"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Address("1.1.1.1"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Address("1200::AB00:1234::2552:7777:1313"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Address("1200:0000:AB00:1234:O000:2552:7777:1313"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Address("invalid")).not.toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Address("1.1.1.1")).not.toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Address("1200::AB00:1234::2552:7777:1313")).not.toBe(
 | 
			
		||||
            undefined
 | 
			
		||||
        );
 | 
			
		||||
        expect(
 | 
			
		||||
            validateIPv6Address("1200:0000:AB00:1234:O000:2552:7777:1313")
 | 
			
		||||
        ).not.toBe(undefined);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("validateIPv6Prefix valid", () => {
 | 
			
		||||
        expect(validateIPv6Prefix("2002:0000::/16"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Prefix("0::/0"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Prefix("2002:0000::/16")).toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Prefix("0::/0")).toBe(undefined);
 | 
			
		||||
    });
 | 
			
		||||
    it("validateIPv6Prefix invalid", () => {
 | 
			
		||||
        expect(validateIPv6Prefix("2001:0db8:85a3:0000:0000:8a2e:0370:7334"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Prefix("::1"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Prefix("2002:0000::/999"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(
 | 
			
		||||
            validateIPv6Prefix("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
 | 
			
		||||
        ).not.toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Prefix("::1")).not.toBe(undefined);
 | 
			
		||||
        expect(validateIPv6Prefix("2002:0000::/999")).not.toBe(undefined);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("validateDomain valid", () => {
 | 
			
		||||
        expect(validateDomain("example.com"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateDomain("one.two.three"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateDomain("example.com")).toBe(undefined);
 | 
			
		||||
        expect(validateDomain("one.two.three")).toBe(undefined);
 | 
			
		||||
    });
 | 
			
		||||
    it("validateDomain invalid", () => {
 | 
			
		||||
        expect(validateDomain("test/"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateDomain("."))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateDomain("test/")).not.toBe(undefined);
 | 
			
		||||
        expect(validateDomain(".")).not.toBe(undefined);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("validateDUID valid", () => {
 | 
			
		||||
        expect(validateDUID("abcdefAB"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateDUID("ABCDEF12"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateDUID("ABCDEF12AB"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateDUID("abcdefAB")).toBe(undefined);
 | 
			
		||||
        expect(validateDUID("ABCDEF12")).toBe(undefined);
 | 
			
		||||
        expect(validateDUID("ABCDEF12AB")).toBe(undefined);
 | 
			
		||||
    });
 | 
			
		||||
    it("validateDUID invalid", () => {
 | 
			
		||||
        expect(validateDUID("gggggggg"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateDUID("abcdefABa"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateDUID("gggggggg")).not.toBe(undefined);
 | 
			
		||||
        expect(validateDUID("abcdefABa")).not.toBe(undefined);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("validateMAC valid", () => {
 | 
			
		||||
        expect(validateMAC("00:D0:56:F2:B5:12"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateMAC("00:26:DD:14:C4:EE"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateMAC("06:00:00:00:00:00"))
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateMAC("00:D0:56:F2:B5:12")).toBe(undefined);
 | 
			
		||||
        expect(validateMAC("00:26:DD:14:C4:EE")).toBe(undefined);
 | 
			
		||||
        expect(validateMAC("06:00:00:00:00:00")).toBe(undefined);
 | 
			
		||||
    });
 | 
			
		||||
    it("validateMAC invalid", () => {
 | 
			
		||||
        expect(validateMAC("00:00:00:00:00:0G"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateMAC("06:00:00:00:00:00:00"))
 | 
			
		||||
            .not
 | 
			
		||||
            .toBe(undefined);
 | 
			
		||||
        expect(validateMAC("00:00:00:00:00:0G")).not.toBe(undefined);
 | 
			
		||||
        expect(validateMAC("06:00:00:00:00:00:00")).not.toBe(undefined);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -52,19 +52,25 @@ ForisForm.propTypes = {
 | 
			
		||||
    onSubmitOverridden: PropTypes.func,
 | 
			
		||||
    /** Reference to actual form element (useful for programmatically submitting it).
 | 
			
		||||
     * Pass the output of useRef hook to this prop.
 | 
			
		||||
    */
 | 
			
		||||
     */
 | 
			
		||||
    formReference: PropTypes.object,
 | 
			
		||||
    /** reForis form components. */
 | 
			
		||||
    children: PropTypes.node.isRequired,
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line react/no-unused-prop-types
 | 
			
		||||
    customWSProp(props) {
 | 
			
		||||
        const wsModuleIsSpecified = !!(props.forisConfig && props.forisConfig.wsModule);
 | 
			
		||||
        const wsModuleIsSpecified = !!(
 | 
			
		||||
            props.forisConfig && props.forisConfig.wsModule
 | 
			
		||||
        );
 | 
			
		||||
        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) {
 | 
			
		||||
            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,
 | 
			
		||||
    children,
 | 
			
		||||
}) {
 | 
			
		||||
    const [formState, onFormChangeHandler, resetFormData] = useForm(validator, prepData);
 | 
			
		||||
    const [formState, onFormChangeHandler, resetFormData] = useForm(
 | 
			
		||||
        validator,
 | 
			
		||||
        prepData
 | 
			
		||||
    );
 | 
			
		||||
    const [setAlert, dismissAlert] = useAlert();
 | 
			
		||||
 | 
			
		||||
    const [forisModuleState] = useForisModule(ws, forisConfig);
 | 
			
		||||
@@ -141,29 +150,39 @@ export function ForisForm({
 | 
			
		||||
        return SUBMIT_BUTTON_STATES.READY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const formIsDisabled = (disabled
 | 
			
		||||
        || forisModuleState.state === API_STATE.SENDING
 | 
			
		||||
        || postState.state === API_STATE.SENDING);
 | 
			
		||||
    const formIsDisabled =
 | 
			
		||||
        disabled ||
 | 
			
		||||
        forisModuleState.state === API_STATE.SENDING ||
 | 
			
		||||
        postState.state === API_STATE.SENDING;
 | 
			
		||||
    const submitButtonIsDisabled = disabled || !!formState.errors;
 | 
			
		||||
 | 
			
		||||
    const childrenWithFormProps = React.Children.map(
 | 
			
		||||
        children,
 | 
			
		||||
        (child) => React.cloneElement(child, {
 | 
			
		||||
    const childrenWithFormProps = React.Children.map(children, (child) =>
 | 
			
		||||
        React.cloneElement(child, {
 | 
			
		||||
            initialData: formState.initialData,
 | 
			
		||||
            formData: formState.data,
 | 
			
		||||
            formErrors: formState.errors,
 | 
			
		||||
            setFormValue: onFormChangeHandler,
 | 
			
		||||
            disabled: formIsDisabled,
 | 
			
		||||
        }),
 | 
			
		||||
        })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const onSubmit = onSubmitOverridden
 | 
			
		||||
        ? onSubmitOverridden(formState.data, onFormChangeHandler, onSubmitHandler)
 | 
			
		||||
        ? onSubmitOverridden(
 | 
			
		||||
              formState.data,
 | 
			
		||||
              onFormChangeHandler,
 | 
			
		||||
              onSubmitHandler
 | 
			
		||||
          )
 | 
			
		||||
        : onSubmitHandler;
 | 
			
		||||
 | 
			
		||||
    function getMessageOnLeavingPage() {
 | 
			
		||||
        if (JSON.stringify(formState.data) === JSON.stringify(formState.initialData)) return true;
 | 
			
		||||
        return _("Changes you made may not be saved. Are you sure you want to leave?");
 | 
			
		||||
        if (
 | 
			
		||||
            JSON.stringify(formState.data) ===
 | 
			
		||||
            JSON.stringify(formState.initialData)
 | 
			
		||||
        )
 | 
			
		||||
            return true;
 | 
			
		||||
        return _(
 | 
			
		||||
            "Changes you made may not be saved. Are you sure you want to leave?"
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,11 @@
 | 
			
		||||
`<ForisForm/>` is Higher-Order Component which encapsulates entire form logic and provides with children required props.
 | 
			
		||||
This component structure provides comfort API and allows to create typical Foris module forms easily.
 | 
			
		||||
`<ForisForm/>` is Higher-Order Component which encapsulates entire form logic
 | 
			
		||||
and provides with children required props. This component structure provides
 | 
			
		||||
comfort API and allows to create typical Foris module forms easily.
 | 
			
		||||
 | 
			
		||||
## Example of usage of `<ForisForm/>`
 | 
			
		||||
 | 
			
		||||
You can pass more forms as children.
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
<ForisForm
 | 
			
		||||
    ws={ws}
 | 
			
		||||
@@ -24,7 +27,10 @@ You can pass more forms as children.
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
export default function MACForm({
 | 
			
		||||
    formData, formErrors, setFormValue, ...props
 | 
			
		||||
    formData,
 | 
			
		||||
    formErrors,
 | 
			
		||||
    setFormValue,
 | 
			
		||||
    ...props
 | 
			
		||||
}) {
 | 
			
		||||
    const macSettings = formData.mac_settings;
 | 
			
		||||
    const errors = (formErrors || {}).mac_settings || {};
 | 
			
		||||
@@ -35,38 +41,33 @@ export default function MACForm({
 | 
			
		||||
                label={_("Custom MAC address")}
 | 
			
		||||
                checked={macSettings.custom_mac_enabled}
 | 
			
		||||
                helpText={HELP_TEXTS.custom_mac_enabled}
 | 
			
		||||
 | 
			
		||||
                onChange={setFormValue(
 | 
			
		||||
                    (value) => ({ mac_settings: { custom_mac_enabled: { $set: value } } }),
 | 
			
		||||
                )}
 | 
			
		||||
 | 
			
		||||
                onChange={setFormValue((value) => ({
 | 
			
		||||
                    mac_settings: { custom_mac_enabled: { $set: value } },
 | 
			
		||||
                }))}
 | 
			
		||||
                {...props}
 | 
			
		||||
            />
 | 
			
		||||
            {macSettings.custom_mac_enabled
 | 
			
		||||
                ? (
 | 
			
		||||
                    <TextInput
 | 
			
		||||
                        label={_("MAC address")}
 | 
			
		||||
                        value={macSettings.custom_mac || ""}
 | 
			
		||||
                        helpText={HELP_TEXTS.custom_mac}
 | 
			
		||||
                        error={errors.custom_mac}
 | 
			
		||||
                        required
 | 
			
		||||
 | 
			
		||||
                        onChange={setFormValue(
 | 
			
		||||
                            (value) => ({ mac_settings: { custom_mac: { $set: value } } }),
 | 
			
		||||
                        )}
 | 
			
		||||
 | 
			
		||||
                        {...props}
 | 
			
		||||
                    />
 | 
			
		||||
                )
 | 
			
		||||
                : null}
 | 
			
		||||
            {macSettings.custom_mac_enabled ? (
 | 
			
		||||
                <TextInput
 | 
			
		||||
                    label={_("MAC address")}
 | 
			
		||||
                    value={macSettings.custom_mac || ""}
 | 
			
		||||
                    helpText={HELP_TEXTS.custom_mac}
 | 
			
		||||
                    error={errors.custom_mac}
 | 
			
		||||
                    required
 | 
			
		||||
                    onChange={setFormValue((value) => ({
 | 
			
		||||
                        mac_settings: { custom_mac: { $set: value } },
 | 
			
		||||
                    }))}
 | 
			
		||||
                    {...props}
 | 
			
		||||
                />
 | 
			
		||||
            ) : null}
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
The <ForisForm/> passes subsequent `props` to the child components.
 | 
			
		||||
 | 
			
		||||
| Prop           | Type   | Description                                                                |
 | 
			
		||||
|----------------|--------|----------------------------------------------------------------------------|
 | 
			
		||||
| -------------- | ------ | -------------------------------------------------------------------------- |
 | 
			
		||||
| `formData`     | object | Data returned from API.                                                    |
 | 
			
		||||
| `formErrors`   | object | Errors returned after validation via validator.                            |
 | 
			
		||||
| `setFormValue` | func   | Function for data update. It takes update rule as arg (see example above). |
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,7 @@ export const STATES = {
 | 
			
		||||
 | 
			
		||||
SubmitButton.propTypes = {
 | 
			
		||||
    disabled: PropTypes.bool,
 | 
			
		||||
    state: PropTypes.oneOf(Object.keys(STATES)
 | 
			
		||||
        .map((key) => STATES[key])),
 | 
			
		||||
    state: PropTypes.oneOf(Object.keys(STATES).map((key) => STATES[key])),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function SubmitButton({ disabled, state, ...props }) {
 | 
			
		||||
@@ -28,14 +27,14 @@ export function SubmitButton({ disabled, state, ...props }) {
 | 
			
		||||
 | 
			
		||||
    let labelSubmitButton;
 | 
			
		||||
    switch (state) {
 | 
			
		||||
    case STATES.SAVING:
 | 
			
		||||
        labelSubmitButton = _("Updating");
 | 
			
		||||
        break;
 | 
			
		||||
    case STATES.LOAD:
 | 
			
		||||
        labelSubmitButton = _("Load settings");
 | 
			
		||||
        break;
 | 
			
		||||
    default:
 | 
			
		||||
        labelSubmitButton = _("Save");
 | 
			
		||||
        case STATES.SAVING:
 | 
			
		||||
            labelSubmitButton = _("Updating");
 | 
			
		||||
            break;
 | 
			
		||||
        case STATES.LOAD:
 | 
			
		||||
            labelSubmitButton = _("Load settings");
 | 
			
		||||
            break;
 | 
			
		||||
        default:
 | 
			
		||||
            labelSubmitButton = _("Save");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
@@ -44,7 +43,6 @@ export function SubmitButton({ disabled, state, ...props }) {
 | 
			
		||||
            loading={loadingSubmitButton}
 | 
			
		||||
            disabled={disableSubmitButton}
 | 
			
		||||
            forisFormSize
 | 
			
		||||
 | 
			
		||||
            {...props}
 | 
			
		||||
        >
 | 
			
		||||
            {labelSubmitButton}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,57 +23,61 @@ export function useForm(validator, dataPreprocessor) {
 | 
			
		||||
        errors: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const onFormReload = useCallback((data) => {
 | 
			
		||||
        dispatch({
 | 
			
		||||
            type: FORM_ACTIONS.resetData,
 | 
			
		||||
            data,
 | 
			
		||||
            dataPreprocessor,
 | 
			
		||||
            validator,
 | 
			
		||||
        });
 | 
			
		||||
    }, [dataPreprocessor, validator]);
 | 
			
		||||
    const onFormReload = useCallback(
 | 
			
		||||
        (data) => {
 | 
			
		||||
            dispatch({
 | 
			
		||||
                type: FORM_ACTIONS.resetData,
 | 
			
		||||
                data,
 | 
			
		||||
                dataPreprocessor,
 | 
			
		||||
                validator,
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [dataPreprocessor, validator]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const onFormChangeHandler = useCallback((updateRule) => (event) => {
 | 
			
		||||
        dispatch({
 | 
			
		||||
            type: FORM_ACTIONS.updateValue,
 | 
			
		||||
            value: getChangedValue(event.target),
 | 
			
		||||
            updateRule,
 | 
			
		||||
            validator,
 | 
			
		||||
        });
 | 
			
		||||
    }, [validator]);
 | 
			
		||||
    const onFormChangeHandler = useCallback(
 | 
			
		||||
        (updateRule) => (event) => {
 | 
			
		||||
            dispatch({
 | 
			
		||||
                type: FORM_ACTIONS.updateValue,
 | 
			
		||||
                value: getChangedValue(event.target),
 | 
			
		||||
                updateRule,
 | 
			
		||||
                validator,
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [validator]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return [
 | 
			
		||||
        state,
 | 
			
		||||
        onFormChangeHandler,
 | 
			
		||||
        onFormReload,
 | 
			
		||||
    ];
 | 
			
		||||
    return [state, onFormChangeHandler, onFormReload];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function formReducer(state, action) {
 | 
			
		||||
    switch (action.type) {
 | 
			
		||||
    case FORM_ACTIONS.updateValue: {
 | 
			
		||||
        const newData = update(state.data, action.updateRule(action.value));
 | 
			
		||||
        const errors = action.validator(newData);
 | 
			
		||||
        return {
 | 
			
		||||
            ...state,
 | 
			
		||||
            data: newData,
 | 
			
		||||
            errors,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    case FORM_ACTIONS.resetData: {
 | 
			
		||||
        if (!action.data) {
 | 
			
		||||
            return { ...state, initialData: state.data };
 | 
			
		||||
        case FORM_ACTIONS.updateValue: {
 | 
			
		||||
            const newData = update(state.data, action.updateRule(action.value));
 | 
			
		||||
            const errors = action.validator(newData);
 | 
			
		||||
            return {
 | 
			
		||||
                ...state,
 | 
			
		||||
                data: newData,
 | 
			
		||||
                errors,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        case FORM_ACTIONS.resetData: {
 | 
			
		||||
            if (!action.data) {
 | 
			
		||||
                return { ...state, initialData: state.data };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        const data = action.dataPreprocessor ? action.dataPreprocessor(action.data) : action.data;
 | 
			
		||||
        return {
 | 
			
		||||
            data,
 | 
			
		||||
            initialData: data,
 | 
			
		||||
            errors: action.data ? action.validator(data) : undefined,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    default: {
 | 
			
		||||
        throw new Error();
 | 
			
		||||
    }
 | 
			
		||||
            const data = action.dataPreprocessor
 | 
			
		||||
                ? action.dataPreprocessor(action.data)
 | 
			
		||||
                : action.data;
 | 
			
		||||
            return {
 | 
			
		||||
                data,
 | 
			
		||||
                initialData: data,
 | 
			
		||||
                errors: action.data ? action.validator(data) : undefined,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        default: {
 | 
			
		||||
            throw new Error();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ window.AlertContext = React.createContext();
 | 
			
		||||
function AlertContextMock({ children }) {
 | 
			
		||||
    return (
 | 
			
		||||
        <AlertContext.Provider value={[mockSetAlert, mockDismissAlert]}>
 | 
			
		||||
            { children }
 | 
			
		||||
            {children}
 | 
			
		||||
        </AlertContext.Provider>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,15 +26,14 @@ function Wrapper({ children }) {
 | 
			
		||||
    return (
 | 
			
		||||
        <AlertContextMock>
 | 
			
		||||
            <StaticRouter>
 | 
			
		||||
                <UIDReset>
 | 
			
		||||
                    {children}
 | 
			
		||||
                </UIDReset>
 | 
			
		||||
                <UIDReset>{children}</UIDReset>
 | 
			
		||||
            </StaticRouter>
 | 
			
		||||
        </AlertContextMock>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const customTestRender = (ui, options) => render(ui, { wrapper: Wrapper, ...options });
 | 
			
		||||
const customTestRender = (ui, options) =>
 | 
			
		||||
    render(ui, { wrapper: Wrapper, ...options });
 | 
			
		||||
 | 
			
		||||
// re-export everything
 | 
			
		||||
export * from "@testing-library/react";
 | 
			
		||||
 
 | 
			
		||||
@@ -8,5 +8,7 @@
 | 
			
		||||
import mockAxios from "jest-mock-axios";
 | 
			
		||||
 | 
			
		||||
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 }) {
 | 
			
		||||
    return (
 | 
			
		||||
        <p className="text-center text-danger">{message}</p>
 | 
			
		||||
    );
 | 
			
		||||
    return <p className="text-center text-danger">{message}</p>;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,12 @@ import React from "react";
 | 
			
		||||
import { render } from "customTestRender";
 | 
			
		||||
import { API_STATE } from "api/utils";
 | 
			
		||||
import {
 | 
			
		||||
    withEither, withSpinner, withSending, withSpinnerOnSending, withError, withErrorMessage,
 | 
			
		||||
    withEither,
 | 
			
		||||
    withSpinner,
 | 
			
		||||
    withSending,
 | 
			
		||||
    withSpinnerOnSending,
 | 
			
		||||
    withError,
 | 
			
		||||
    withErrorMessage,
 | 
			
		||||
} from "../conditionalHOCs";
 | 
			
		||||
 | 
			
		||||
describe("conditional HOCs", () => {
 | 
			
		||||
@@ -52,14 +57,18 @@ describe("conditional HOCs", () => {
 | 
			
		||||
        it("should render First component", () => {
 | 
			
		||||
            const withAlternative = withSending(Alternative);
 | 
			
		||||
            const FirstWithConditional = withAlternative(First);
 | 
			
		||||
            const { getByText } = render(<FirstWithConditional apiState={API_STATE.SUCCESS} />);
 | 
			
		||||
            const { getByText } = render(
 | 
			
		||||
                <FirstWithConditional apiState={API_STATE.SUCCESS} />
 | 
			
		||||
            );
 | 
			
		||||
            expect(getByText("First")).toBeDefined();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should render Alternative component", () => {
 | 
			
		||||
            const withAlternative = withSending(Alternative);
 | 
			
		||||
            const FirstWithConditional = withAlternative(First);
 | 
			
		||||
            const { getByText } = render(<FirstWithConditional apiState={API_STATE.SENDING} />);
 | 
			
		||||
            const { getByText } = render(
 | 
			
		||||
                <FirstWithConditional apiState={API_STATE.SENDING} />
 | 
			
		||||
            );
 | 
			
		||||
            expect(getByText("Alternative")).toBeDefined();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
@@ -67,13 +76,17 @@ describe("conditional HOCs", () => {
 | 
			
		||||
    describe("withSpinnerOnSending", () => {
 | 
			
		||||
        it("should render First component", () => {
 | 
			
		||||
            const FirstWithConditional = withSpinnerOnSending(First);
 | 
			
		||||
            const { getByText } = render(<FirstWithConditional apiState={API_STATE.SUCCESS} />);
 | 
			
		||||
            const { getByText } = render(
 | 
			
		||||
                <FirstWithConditional apiState={API_STATE.SUCCESS} />
 | 
			
		||||
            );
 | 
			
		||||
            expect(getByText("First")).toBeDefined();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should render spinner", () => {
 | 
			
		||||
            const FirstWithConditional = withSpinnerOnSending(First);
 | 
			
		||||
            const { container } = render(<FirstWithConditional apiState={API_STATE.SENDING} />);
 | 
			
		||||
            const { container } = render(
 | 
			
		||||
                <FirstWithConditional apiState={API_STATE.SENDING} />
 | 
			
		||||
            );
 | 
			
		||||
            expect(container).toMatchSnapshot();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
@@ -97,13 +110,17 @@ describe("conditional HOCs", () => {
 | 
			
		||||
    describe("withErrorMessage", () => {
 | 
			
		||||
        it("should render First component", () => {
 | 
			
		||||
            const FirstWithConditional = withErrorMessage(First);
 | 
			
		||||
            const { getByText } = render(<FirstWithConditional apiState={API_STATE.SUCCESS} />);
 | 
			
		||||
            const { getByText } = render(
 | 
			
		||||
                <FirstWithConditional apiState={API_STATE.SUCCESS} />
 | 
			
		||||
            );
 | 
			
		||||
            expect(getByText("First")).toBeDefined();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should render error message", () => {
 | 
			
		||||
            const FirstWithConditional = withErrorMessage(First);
 | 
			
		||||
            const { container } = render(<FirstWithConditional apiState={API_STATE.ERROR} />);
 | 
			
		||||
            const { container } = render(
 | 
			
		||||
                <FirstWithConditional apiState={API_STATE.ERROR} />
 | 
			
		||||
            );
 | 
			
		||||
            expect(container).toMatchSnapshot();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -10,42 +10,40 @@ import { toLocaleDateString } from "../datetime";
 | 
			
		||||
describe("toLocaleDateString", () => {
 | 
			
		||||
    it("should work with different locale", () => {
 | 
			
		||||
        global.ForisTranslations = { locale: "fr" };
 | 
			
		||||
        expect(
 | 
			
		||||
            toLocaleDateString("2020-02-20T12:51:36+00:00")
 | 
			
		||||
        ).toBe("20 février 2020 12:51");
 | 
			
		||||
        expect(toLocaleDateString("2020-02-20T12:51:36+00:00")).toBe(
 | 
			
		||||
            "20 février 2020 12:51"
 | 
			
		||||
        );
 | 
			
		||||
        global.ForisTranslations = { locale: "en" };
 | 
			
		||||
    })
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should convert with default format", () => {
 | 
			
		||||
        expect(
 | 
			
		||||
            toLocaleDateString("2020-02-20T12:51:36+00:00")
 | 
			
		||||
        ).toBe("February 20, 2020 12:51 PM");
 | 
			
		||||
        expect(toLocaleDateString("2020-02-20T12:51:36+00:00")).toBe(
 | 
			
		||||
            "February 20, 2020 12:51 PM"
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should convert with custom input format", () => {
 | 
			
		||||
        expect(
 | 
			
		||||
            toLocaleDateString(
 | 
			
		||||
                "2020-02-20 12:51:36 +0000",
 | 
			
		||||
                { inputFormat: "YYYY-MM-DD HH:mm:ss Z" },
 | 
			
		||||
            )
 | 
			
		||||
            toLocaleDateString("2020-02-20 12:51:36 +0000", {
 | 
			
		||||
                inputFormat: "YYYY-MM-DD HH:mm:ss Z",
 | 
			
		||||
            })
 | 
			
		||||
        ).toBe("February 20, 2020 12:51 PM");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should convert with custom output format", () => {
 | 
			
		||||
        expect(
 | 
			
		||||
            toLocaleDateString(
 | 
			
		||||
                "2020-02-20T12:51:36+00:00",
 | 
			
		||||
                { outputFormat: "LL" },
 | 
			
		||||
            )
 | 
			
		||||
            toLocaleDateString("2020-02-20T12:51:36+00:00", {
 | 
			
		||||
                outputFormat: "LL",
 | 
			
		||||
            })
 | 
			
		||||
        ).toBe("February 20, 2020");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should convert with custom input and output format", () => {
 | 
			
		||||
        expect(
 | 
			
		||||
            toLocaleDateString(
 | 
			
		||||
                "2020-02-20 12:51:36 +0000",
 | 
			
		||||
                { inputFormat: "YYYY-MM-DD HH:mm:ss Z", outputFormat: "LL" },
 | 
			
		||||
            )
 | 
			
		||||
            toLocaleDateString("2020-02-20 12:51:36 +0000", {
 | 
			
		||||
                inputFormat: "YYYY-MM-DD HH:mm:ss Z",
 | 
			
		||||
                outputFormat: "LL",
 | 
			
		||||
            })
 | 
			
		||||
        ).toBe("February 20, 2020");
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -24,8 +24,8 @@ function withEither(conditionalFn, Either) {
 | 
			
		||||
 | 
			
		||||
function isSending(props) {
 | 
			
		||||
    if (Array.isArray(props.apiState)) {
 | 
			
		||||
        return props.apiState.some(
 | 
			
		||||
            (state) => [API_STATE.INIT, API_STATE.SENDING].includes(state),
 | 
			
		||||
        return props.apiState.some((state) =>
 | 
			
		||||
            [API_STATE.INIT, API_STATE.SENDING].includes(state)
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    return [API_STATE.INIT, API_STATE.SENDING].includes(props.apiState);
 | 
			
		||||
@@ -38,15 +38,18 @@ const withSpinnerOnSending = withSpinner(isSending);
 | 
			
		||||
// Error handling
 | 
			
		||||
 | 
			
		||||
const withError = (conditionalFn) => withEither(conditionalFn, ErrorMessage);
 | 
			
		||||
const withErrorMessage = withError(
 | 
			
		||||
    (props) => {
 | 
			
		||||
        if (Array.isArray(props.apiState)) {
 | 
			
		||||
            return props.apiState.includes(API_STATE.ERROR);
 | 
			
		||||
        }
 | 
			
		||||
        return props.apiState === API_STATE.ERROR;
 | 
			
		||||
    },
 | 
			
		||||
);
 | 
			
		||||
const withErrorMessage = withError((props) => {
 | 
			
		||||
    if (Array.isArray(props.apiState)) {
 | 
			
		||||
        return props.apiState.includes(API_STATE.ERROR);
 | 
			
		||||
    }
 | 
			
		||||
    return props.apiState === API_STATE.ERROR;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
    withEither, withSpinner, withSending, withSpinnerOnSending, withError, withErrorMessage,
 | 
			
		||||
    withEither,
 | 
			
		||||
    withSpinner,
 | 
			
		||||
    withSending,
 | 
			
		||||
    withSpinnerOnSending,
 | 
			
		||||
    withError,
 | 
			
		||||
    withErrorMessage,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
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);
 | 
			
		||||
    return parsedDate
 | 
			
		||||
        .locale(ForisTranslations.locale)
 | 
			
		||||
        .format(outputFormat);
 | 
			
		||||
    return parsedDate.locale(ForisTranslations.locale).format(outputFormat);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,11 +8,17 @@
 | 
			
		||||
import { useState, useEffect } from "react";
 | 
			
		||||
 | 
			
		||||
/** 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);
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (condition) {
 | 
			
		||||
            const interval = setTimeout(() => callback(...callbackArgs), timeout);
 | 
			
		||||
            const interval = setTimeout(
 | 
			
		||||
                () => callback(...callbackArgs),
 | 
			
		||||
                timeout
 | 
			
		||||
            );
 | 
			
		||||
            return () => setTimeout(interval);
 | 
			
		||||
        }
 | 
			
		||||
    }, [condition, callback, timeout, callbackArgs]);
 | 
			
		||||
 
 | 
			
		||||
@@ -15,21 +15,18 @@ export function undefinedIfEmpty(instance) {
 | 
			
		||||
 | 
			
		||||
/** Return object without keys that have undefined value. */
 | 
			
		||||
export function withoutUndefinedKeys(instance) {
 | 
			
		||||
    return Object.keys(instance).reduce(
 | 
			
		||||
        (accumulator, key) => {
 | 
			
		||||
            if (instance[key] !== undefined) {
 | 
			
		||||
                accumulator[key] = instance[key];
 | 
			
		||||
            }
 | 
			
		||||
            return accumulator;
 | 
			
		||||
        },
 | 
			
		||||
        {},
 | 
			
		||||
    );
 | 
			
		||||
    return Object.keys(instance).reduce((accumulator, key) => {
 | 
			
		||||
        if (instance[key] !== undefined) {
 | 
			
		||||
            accumulator[key] = instance[key];
 | 
			
		||||
        }
 | 
			
		||||
        return accumulator;
 | 
			
		||||
    }, {});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Return copy of passed object that has only desired keys. */
 | 
			
		||||
export function onlySpecifiedKeys(object, desiredKeys) {
 | 
			
		||||
    return desiredKeys.reduce(
 | 
			
		||||
        (accumulator, key) => { accumulator[key] = object[key]; return accumulator; },
 | 
			
		||||
        {},
 | 
			
		||||
    );
 | 
			
		||||
    return desiredKeys.reduce((accumulator, key) => {
 | 
			
		||||
        accumulator[key] = object[key];
 | 
			
		||||
        return accumulator;
 | 
			
		||||
    }, {});
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,10 @@ const REs = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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");
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,9 @@ export class WebSockets {
 | 
			
		||||
        this.ws = new WebSocket(URL);
 | 
			
		||||
        this.ws.onerror = (e) => {
 | 
			
		||||
            if (window.location.pathname !== ForisURLs.login) {
 | 
			
		||||
                console.error("WS: Error observed, you aren't logged probably.");
 | 
			
		||||
                console.error(
 | 
			
		||||
                    "WS: Error observed, you aren't logged probably."
 | 
			
		||||
                );
 | 
			
		||||
                window.location.replace(ForisURLs.login);
 | 
			
		||||
            }
 | 
			
		||||
            console.error(`WS: Error: ${e}`);
 | 
			
		||||
@@ -111,7 +113,9 @@ export class WebSockets {
 | 
			
		||||
            chain = this.callbacks[json.module][json.action];
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,12 @@
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
@@ -18,14 +23,16 @@ export function useWSForisModule(ws, module, action = "update_settings", control
 | 
			
		||||
 | 
			
		||||
        function callback(message) {
 | 
			
		||||
            // 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;
 | 
			
		||||
            }
 | 
			
		||||
            setData(message.data);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ws.subscribe(module)
 | 
			
		||||
            .bind(module, action, callback);
 | 
			
		||||
        ws.subscribe(module).bind(module, action, callback);
 | 
			
		||||
 | 
			
		||||
        return () => {
 | 
			
		||||
            ws.unbind(module, action, callback);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user