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];