diff --git a/README.md b/README.md index f55d183..79a6dfb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # foris-js +Set of utils and common React elements for reForis. ## Publishing package diff --git a/package-lock.json b/package-lock.json index 04e4540..12c5734 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "foris", - "version": "1.4.0", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4b44a5c..d2ca0d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "foris", - "version": "1.4.0", + "version": "2.0.0", "description": "Set of components and utils for Foris and its plugins.", "author": "CZ.NIC, z.s.p.o.", "repository": { diff --git a/scripts/collect_files.sh b/scripts/collect_files.sh index 6d45f1b..33fd3f2 100644 --- a/scripts/collect_files.sh +++ b/scripts/collect_files.sh @@ -2,7 +2,7 @@ # Collect files npm run build -cp package.json dist +cp package.json README.md dist cp -rf translations dist # Remove unwanted files rm -rf dist/**/__tests__ diff --git a/src/api/hooks.js b/src/api/hooks.js index 0429d57..b5f00d0 100644 --- a/src/api/hooks.js +++ b/src/api/hooks.js @@ -17,13 +17,13 @@ import { const DATA_METHODS = ["POST", "PATCH", "PUT"]; function createAPIHook(method) { - return (url, contentType) => { + return (urlRoot, contentType) => { const [state, dispatch] = useReducer(APIReducer, { state: API_STATE.INIT, data: null, }); - const sendRequest = useCallback(async (data) => { + const sendRequest = useCallback(async ({ data, suffix } = {}) => { const headers = { ...HEADERS }; if (contentType) { headers["Content-Type"] = contentType; @@ -31,17 +31,23 @@ function createAPIHook(method) { dispatch({ type: API_ACTIONS.INIT }); try { + // Prepare request const request = API_METHODS[method]; const config = { timeout: TIMEOUT, headers, }; + const url = suffix ? `${urlRoot}/${suffix}` : urlRoot; + + // Make request let result; if (DATA_METHODS.includes(method)) { result = await request(url, data, config); } else { result = await request(url, config); } + + // Process request result dispatch({ type: API_ACTIONS.SUCCESS, payload: result.data, @@ -53,7 +59,7 @@ function createAPIHook(method) { payload: getErrorPayload(error), }); } - }, [url, contentType]); + }, [urlRoot, contentType]); return [state, sendRequest]; }; } diff --git a/src/bootstrap/Spinner.js b/src/bootstrap/Spinner.js index 33cca31..07e856f 100644 --- a/src/bootstrap/Spinner.js +++ b/src/bootstrap/Spinner.js @@ -46,6 +46,8 @@ export function Spinner({ SpinnerElement.propTypes = { /** Spinner's size */ small: PropTypes.bool, + /** Additional className */ + className: PropTypes.string, /** Children components */ children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), @@ -53,13 +55,16 @@ SpinnerElement.propTypes = { ]), }; -export function SpinnerElement({ small, children }) { +export function SpinnerElement({ small, className, children }) { return ( <> -
+
-
{children}
+ {children &&
{children}
} ); } diff --git a/src/form/components/ForisForm.js b/src/form/components/ForisForm.js index 86b552b..80e3821 100644 --- a/src/form/components/ForisForm.js +++ b/src/form/components/ForisForm.js @@ -20,14 +20,16 @@ import { useForisModule, useForm } from "../hooks"; import { STATES as SUBMIT_BUTTON_STATES, SubmitButton } from "./SubmitButton"; ForisForm.propTypes = { - /** WebSocket object see `scr/common/WebSockets.js`. */ + /** Optional WebSocket object. See `scr/common/WebSockets.js`. + * `forisConfig.wsModule` should be specified when it's passed. + * */ ws: PropTypes.object, /** Foris configuration object. See usage in main components. */ forisConfig: PropTypes.shape({ /** reForis Flask aplication API endpoint from `src/common/API.js`. */ endpoint: PropTypes.string.isRequired, /** `foris-controller` module name to be used via WebSockets. - * If it's not passed then WebSockets aren't used + * It can be use only with `ws` prop. * */ wsModule: PropTypes.string, /** `foris-controller` action name to be used via WebSockets. @@ -49,6 +51,17 @@ ForisForm.propTypes = { children: PropTypes.node.isRequired, /** Optional override of form submit callback */ onSubmitOverridden: PropTypes.func, + + // eslint-disable-next-line react/no-unused-prop-types + customWSProp(props) { + const wsModuleIsSpecified = !!(props.forisConfig && props.forisConfig.wsModule); + if (props.ws && !wsModuleIsSpecified) { + return new Error("forisConfig.wsModule should be specified when ws object is passed."); + } + if (!props.ws && wsModuleIsSpecified) { + return new Error("forisConfig.wsModule is specified without passing ws object."); + } + }, }; ForisForm.defaultProps = { @@ -103,7 +116,7 @@ export function ForisForm({ resetFormData(); const copiedFormData = JSON.parse(JSON.stringify(formState.data)); const preparedData = prepDataToSubmit(copiedFormData); - post(preparedData); + post({ data: preparedData }); } function getSubmitButtonState() { diff --git a/src/utils/__tests__/__snapshots__/conditionalHOCs.test.js.snap b/src/utils/__tests__/__snapshots__/conditionalHOCs.test.js.snap index f2e1040..3a178ea 100644 --- a/src/utils/__tests__/__snapshots__/conditionalHOCs.test.js.snap +++ b/src/utils/__tests__/__snapshots__/conditionalHOCs.test.js.snap @@ -26,16 +26,13 @@ exports[`conditional HOCs withSpinner should render spinner 1`] = ` class="spinner-wrapper my-3 text-center" >
-
`; @@ -46,16 +43,13 @@ exports[`conditional HOCs withSpinnerOnSending should render spinner 1`] = ` class="spinner-wrapper my-3 text-center" >
-
`; diff --git a/src/webSockets/WebSockets.js b/src/webSockets/WebSockets.js index 6fb9c50..c01407f 100644 --- a/src/webSockets/WebSockets.js +++ b/src/webSockets/WebSockets.js @@ -61,9 +61,36 @@ export class WebSockets { return this; } - subscribe(params) { + subscribe(module) { this.waitForConnection(() => { - this.send("subscribe", params); + this.send("subscribe", module); + }); + return this; + } + + unbind(module, action, callback) { + const callbacks = this.callbacks[module][action]; + + const index = callbacks.indexOf(callback); + if (index !== -1) { + callbacks.splice(index, 1); + } + + if (callbacks.length === 0) { + delete this.callbacks[module][action]; + } + + if (Object.keys(this.callbacks[module]).length === 0) { + this.unsubscribe(module); + } + + return this; + } + + unsubscribe(module) { + this.waitForConnection(() => { + this.send("unsubscribe", module); + delete this.callbacks[module]; }); return this; } @@ -82,15 +109,15 @@ export class WebSockets { let chain; try { chain = this.callbacks[json.module][json.action]; - } catch (e) { - if (e instanceof TypeError) { - console.log(`Callback for this message wasn't found:${e.data}`); - } else throw e; + } catch (error) { + if (error instanceof TypeError) { + console.log(`Callback for this message wasn't found:${error.data}`); + } else throw error; } if (typeof chain === "undefined") return; - for (let i = 0; i < chain.length; i++) chain[i](json); + chain.forEach((callback) => callback(json)); } close() { diff --git a/src/webSockets/hooks.js b/src/webSockets/hooks.js index 3ca0084..7357ead 100644 --- a/src/webSockets/hooks.js +++ b/src/webSockets/hooks.js @@ -11,12 +11,21 @@ export function useWSForisModule(ws, module, action = "update_settings") { const [data, setData] = useState(null); useEffect(() => { - if (ws && module) { - ws.subscribe(module) - .bind(module, action, (msg) => { - setData(msg.data); - }); + // Sometimes we want to disable this hook if WS is not passed. We can't make conditional + // hooks, but we can disable it here. It's used especially in ForisForm when a module + // doesn't present any WS endpoint. + if (!ws) return; + + function callback(msg) { + setData(msg.data); } + + ws.subscribe(module) + .bind(module, action, callback); + + return () => { + ws.unbind(module, action, callback); + }; }, [action, module, ws]); return [data];