diff --git a/src/ErrorBoundary.jsx b/src/ErrorBoundary.jsx new file mode 100644 index 0000000000000000000000000000000000000000..af330d4f3717ec40f0ac360d007365bbec15f18b --- /dev/null +++ b/src/ErrorBoundary.jsx @@ -0,0 +1,27 @@ +// vendor +import React from "react"; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error) { + return { hasError: true }; + } + + componentDidCatch(error, info) { + console.error(error, info.componentStack); + } + + render() { + if (this.state.hasError) { + return this.props.fallback; + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/src/GeneralSettings/Backends/Backend.jsx b/src/GeneralSettings/Backends/Backend.jsx new file mode 100644 index 0000000000000000000000000000000000000000..1476c07cbe977727adbc304b69b405c80c4d8bef --- /dev/null +++ b/src/GeneralSettings/Backends/Backend.jsx @@ -0,0 +1,149 @@ +// vendor +import React from "react"; +import { __ } from "@wordpress/i18n"; +import { + TextControl, + Button, + __experimentalSpacer as Spacer, +} from "@wordpress/components"; +import { useState, useRef, useEffect } from "@wordpress/element"; + +// source +import BackendHeaders from "./Headers"; + +function NewBackend({ add }) { + const [name, setName] = useState(""); + const [baseUrl, setBaseUrl] = useState("https://"); + + const onClick = () => add({ name, base_url: baseUrl, headers: [] }); + + const disabled = !(name && baseUrl); + + return ( + <div + style={{ + padding: "calc(24px) calc(32px)", + width: "calc(100% - 64px)", + backgroundColor: "rgb(245, 245, 245)", + }} + > + <div + style={{ + display: "flex", + gap: "1em", + }} + > + <TextControl + label={__("Backend name", "wpct-erp-forms")} + value={name} + onChange={setName} + __nextHasNoMarginBottom + /> + <TextControl + style={{ minWidth: "250px" }} + label={__("Backend base URL", "wpct-erp-forms")} + value={baseUrl} + onChange={setBaseUrl} + __nextHasNoMarginBottom + /> + <Button + variant="primary" + onClick={() => onClick()} + style={{ marginTop: "auto", height: "32px" }} + disabled={disabled} + > + {__("Add", "wpct-erp-forms")} + </Button> + </div> + </div> + ); +} + +let focus = false; +export default function Backend({ update, remove, ...data }) { + if (data.name === "add") return <NewBackend add={update} />; + + const [name, setName] = useState(data.name); + const nameInput = useRef(); + + const setHeaders = (headers) => update({ ...data, headers }); + + useEffect(() => { + if (focus) { + nameInput.current.focus(); + } + }, []); + + const timeout = useRef(false); + useEffect(() => { + if (timeout.current === false) { + timeout.current = 0; + return; + } + + clearTimeout(timeout.current); + timeout.current = setTimeout(() => update({ ...data, name }), 500); + }, [name]); + + useEffect(() => { + timeout.current = false; + setName(data.name); + }, [data.name]); + + return ( + <div + style={{ + padding: "calc(24px) calc(32px)", + width: "calc(100% - 64px)", + backgroundColor: "rgb(245, 245, 245)", + }} + > + <div + style={{ + display: "flex", + gap: "1em", + }} + > + <TextControl + ref={nameInput} + label={__("Backend name", "wpct-erp-forms")} + value={name} + onChange={setName} + onFocus={() => (focus = true)} + onBlur={() => (focus = false)} + __nextHasNoMarginBottom={true} + /> + <TextControl + style={{ minWidth: "250px" }} + label={__("Backend base URL", "wpct-erp-forms")} + value={data.base_url} + onChange={(base_url) => update({ ...data, base_url })} + __nextHasNoMarginBottom={true} + /> + <div> + <label + style={{ + display: "block", + fontWeight: 500, + textTransform: "uppercase", + fontSize: "11px", + marginBottom: "calc(4px)", + }} + > + {__("Remove backend", "wpct-erp-forms")} + </label> + <Button + isDestructive + variant="primary" + onClick={() => remove(data)} + style={{ width: "130px", justifyContent: "center", height: "32px" }} + > + {__("Remove", "wpct-erp-forms")} + </Button> + </div> + </div> + <Spacer paddingY="calc(8px)" /> + <BackendHeaders headers={data.headers} setHeaders={setHeaders} /> + </div> + ); +} diff --git a/src/GeneralSettings/Backends/Headers.jsx b/src/GeneralSettings/Backends/Headers.jsx new file mode 100644 index 0000000000000000000000000000000000000000..62d3161cadf5d204bcc135033b887ae73a529580 --- /dev/null +++ b/src/GeneralSettings/Backends/Headers.jsx @@ -0,0 +1,87 @@ +// vendor +import React from "react"; +import { __ } from "@wordpress/i18n"; +import { + TextControl, + Button, + __experimentalSpacer as Spacer, +} from "@wordpress/components"; + +export default function BackendHeaders({ headers, setHeaders }) { + const setHeader = (attr, index, value) => { + const newHeaders = headers.map((header, i) => { + if (index === i) header[attr] = value; + return { ...header }; + }); + + setHeaders(newHeaders); + }; + + const addHeader = () => { + const newHeaders = headers.concat([{ name: "", value: "" }]); + setHeaders(newHeaders); + }; + + const dropHeader = (index) => { + const newHeaders = headers.slice(0, index).concat(headers.slice(index + 2)); + setHeaders(newHeaders); + }; + + return ( + <div className="components-base-control__label"> + <label + className="components-base-control__label" + style={{ + fontSize: "11px", + textTransform: "uppercase", + fontWeight: 500, + marginBottom: "calc(8px)", + }} + > + {__("Backend HTTP Headers", "wpct-erp-forms")} + </label> + <table style={{ width: "100%" }}> + <tbody> + {headers.map(({ name, value }, i) => ( + <tr key={i}> + <td> + <TextControl + placeholder={__("Header-Name", "wpct-erp-forms")} + value={name} + onChange={(value) => setHeader("name", i, value)} + __nextHasNoMarginBottom + /> + </td> + <td> + <TextControl + placeholder={__("Value")} + value={value} + onChange={(value) => setHeader("value", i, value)} + __nextHasNoMarginBottom + /> + </td> + <td style={{ borderLeft: "1rem solid transparent" }}> + <Button + isDestructive + variant="secondary" + onClick={() => dropHeader(i)} + style={{ height: "32px" }} + > + {__("Drop", "wpct-erp-forms")} + </Button> + </td> + </tr> + ))} + </tbody> + </table> + <Spacer paddingY="calc(3px)" /> + <Button + variant="secondary" + onClick={() => addHeader()} + style={{ height: "32px" }} + > + {__("Add", "wpct-erp-forms")} + </Button> + </div> + ); +} diff --git a/src/GeneralSettings/Backends/index.jsx b/src/GeneralSettings/Backends/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ad8c82d1136dafc6b662a1072527e96ec2ba9398 --- /dev/null +++ b/src/GeneralSettings/Backends/index.jsx @@ -0,0 +1,70 @@ +// vendor +import React from "react"; +import { __ } from "@wordpress/i18n"; +import { TabPanel } from "@wordpress/components"; + +// source +import Backend from "./Backend"; + +export default function Backends({ backends, setBackends }) { + const tabs = backends + .map(({ name, base_url, headers }) => ({ + name, + title: name, + base_url, + headers, + })) + .concat([ + { + title: __("Add Backend", "wpct-erp-forms"), + name: "add", + }, + ]); + + const updateBackend = (index, data) => { + if (index === -1) index = backends.length; + const newBackends = backends + .slice(0, index) + .concat([data]) + .concat(backends.slice(index + 1, backends.length)); + setBackends(newBackends); + }; + + const removeBackend = ({ name }) => { + const index = backends.findIndex((b) => b.name === name); + const newBackends = backends + .slice(0, index) + .concat(backends.slice(index + 2)); + setBackends(newBackends); + }; + + return ( + <div style={{ width: "100%" }}> + <label + className="components-base-control__label" + style={{ + fontSize: "11px", + textTransform: "uppercase", + fontWeight: 500, + marginBottom: "calc(8px)", + }} + > + {__("Backends", "wpct-erp-forms")} + </label> + <TabPanel tabs={tabs}> + {(backend) => ( + <Backend + {...backend} + remove={removeBackend} + update={(newBackend) => + updateBackend( + backends.findIndex(({ name }) => name === backend.name), + newBackend + ) + } + /> + )} + </TabPanel> + </div> + ); +} diff --git a/src/GeneralSettings/index.jsx b/src/GeneralSettings/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6be00ca5fadf5e5302d8745da8579cf33c2e7690 --- /dev/null +++ b/src/GeneralSettings/index.jsx @@ -0,0 +1,47 @@ +// vendor +import React from "react"; +import { __ } from "@wordpress/i18n"; +import { + Card, + CardHeader, + CardBody, + __experimentalHeading as Heading, + PanelRow, + TextControl, + __experimentalSpacer as Spacer, +} from "@wordpress/components"; + +// source +import { useGeneral } from "../providers/Settings"; +import Backends from "./Backends"; + +export default function GeneralSettings() { + const [{ receiver, backends }, save] = useGeneral(); + + const update = (field) => save({ receiver, backends, ...field }); + + return ( + <Card size="large" style={{ height: "fit-content" }}> + <CardHeader> + <Heading level={3}>{__("General", "wpct-erp-forms")}</Heading> + </CardHeader> + <CardBody> + <PanelRow> + <TextControl + label={__("Notification receiver", "wpct-erp-forms")} + onChange={(receiver) => update({ receiver })} + value={receiver} + __nextHasNoMarginBottom + /> + </PanelRow> + <Spacer paddingY="calc(8px)" /> + <PanelRow> + <Backends + backends={backends} + setBackends={(backends) => update({ backends })} + /> + </PanelRow> + </CardBody> + </Card> + ); +} diff --git a/src/Loading.jsx b/src/Loading.jsx new file mode 100644 index 0000000000000000000000000000000000000000..83421aa74ef910c5204dc02a612f3317201a1c85 --- /dev/null +++ b/src/Loading.jsx @@ -0,0 +1,15 @@ +// vendor +import React from "react"; +import { Animate, Notice } from "@wordpress/components"; + +export default function Loading({ message }) { + return ( + <Animate type="loading"> + {(className) => ( + <Notice className={className} status="success"> + {message} + </Notice> + )} + </Animate> + ); +} diff --git a/src/RestApiSettings/Forms/Form.jsx b/src/RestApiSettings/Forms/Form.jsx new file mode 100644 index 0000000000000000000000000000000000000000..cad6f665fc68a662655a7e0600e35276d3204ef0 --- /dev/null +++ b/src/RestApiSettings/Forms/Form.jsx @@ -0,0 +1,191 @@ +// vendor +import React from "react"; +import { __ } from "@wordpress/i18n"; +import { TextControl, SelectControl, Button } from "@wordpress/components"; +import { useState, useRef, useEffect } from "@wordpress/element"; + +// source +import { useForms } from "../../providers/Forms"; +import { useGeneral } from "../../providers/Settings"; + +function NewForm({ add }) { + const [{ backends }] = useGeneral(); + const backendOptions = backends.map(({ name, base_url }) => ({ + label: name, + value: base_url, + })); + const forms = useForms(); + const formOptions = forms.map(({ id, title }) => ({ + label: title, + value: id, + })); + + const [name, setName] = useState(""); + const [backend, setBackend] = useState(""); + const [endpoint, setEndpoint] = useState(""); + const [formId, setFormId] = useState(""); + + const onClick = () => add({ name, backend, endpoint, form_id: formId }); + + const disabled = !(name && backend && endpoint && formId); + + return ( + <div + style={{ + padding: "calc(24px) calc(32px)", + width: "calc(100% - 64px)", + backgroundColor: "rgb(245, 245, 245)", + }} + > + <div + style={{ + display: "flex", + gap: "1em", + }} + > + <TextControl + label={__("Bound ID", "wpct-erp-forms")} + value={name} + onChange={setName} + __nextHasNoMarginBottom + /> + <SelectControl + label={__("Backend", "wpct-erp-forms")} + value={backend} + onChange={setBackend} + options={backendOptions} + __nextHasNoMarginBottom + /> + <TextControl + label={__("Endpoint", "wpct-erp-forms")} + value={endpoint} + onChange={setEndpoint} + __nextHasNoMarginBottom + /> + <SelectControl + label={__("Form", "wpct-erp-forms")} + value={formId} + onChange={setFormId} + options={formOptions} + __nextHasNoMarginBottom + /> + <Button + variant="primary" + onClick={() => onClick()} + style={{ marginTop: "auto", height: "32px" }} + disabled={disabled} + > + {__("Add", "wpct-erp-forms")} + </Button> + </div> + </div> + ); +} +let focus; +export default function Form({ update, remove, ...data }) { + if (data.name === "add") return <NewForm add={update} />; + + const [{ backends }] = useGeneral(); + const backendOptions = backends.map(({ name, base_url }) => ({ + label: name, + value: base_url, + })); + const forms = useForms(); + const formOptions = forms.map(({ id, title }) => ({ + label: title, + value: id, + })); + + const [name, setName] = useState(data.name); + const nameInput = useRef(); + + useEffect(() => { + if (focus) { + nameInput.current.focus(); + } + }, []); + + const timeout = useRef(false); + useEffect(() => { + if (timeout.current === false) { + timeout.current = 0; + return; + } + + clearTimeout(timeout.current); + timeout.current = setTimeout(() => update({ ...data, name }), 500); + }, [name]); + + useEffect(() => { + timeout.current = false; + setName(data.name); + }, [data.name]); + + return ( + <div + style={{ + padding: "calc(24px) calc(32px)", + width: "calc(100% - 64px)", + backgroundColor: "rgb(245, 245, 245)", + }} + > + <div + style={{ + display: "flex", + gap: "1em", + }} + > + <TextControl + ref={nameInput} + label={__("Bound ID", "wpct-erp-forms")} + value={name} + onChange={setName} + onFocus={() => (focus = true)} + onBlur={() => (focus = false)} + __nextHasNoMarginBottom + /> + <SelectControl + label={__("Backend", "wpct-erp-forms")} + value={data.backend} + onChange={(backend) => update({ ...data, backend })} + options={backendOptions} + __nextHasNoMarginBottom + /> + <TextControl + label={__("Endpoint", "wpct-erp-forms")} + value={data.endpoint} + onChange={(endpoint) => update({ ...data, endpoint })} + __nextHasNoMarginBottom + /> + <SelectControl + label={__("Form", "wpct-erp-forms")} + value={data.form_id} + onChange={(form_id) => update({ ...data, form_id })} + options={formOptions} + __nextHasNoMarginBottom + /> + <div> + <label + style={{ + display: "block", + fontWeight: 500, + textTransform: "uppercase", + fontSize: "11px", + marginBottom: "calc(4px)", + }} + > + {__("Remove form", "wpct-erp-forms")} + </label> + <Button + isDestructive + variant="primary" + onClick={() => remove(data)} + style={{ width: "130px", justifyContent: "center", height: "32px" }} + > + {__("Remove", "wpct-erp-forms")} + </Button> + </div> + </div> + </div> + ); +} diff --git a/src/RestApiSettings/Forms/index.jsx b/src/RestApiSettings/Forms/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..93598ce453c7cb0b3ea66e2fc4efcf740cf6c713 --- /dev/null +++ b/src/RestApiSettings/Forms/index.jsx @@ -0,0 +1,72 @@ +// vendor +import React from "react"; +import { __ } from "@wordpress/i18n"; +import { TabPanel } from "@wordpress/components"; + +// source +import Form from "./Form"; + +export default function Forms({ forms, setForms }) { + const tabs = forms + .map(({ backend, endpoint, form_id, ref }) => ({ + name: ref, + title: ref, + form_id, + endpoint, + backend, + })) + .concat([ + { + title: __("Add Form", "wpct-erp-forms"), + name: "add", + }, + ]); + + const updateForm = (index, data) => { + data = { ...data, ref: data.name }; + delete data.name; + + if (index === -1) index = forms.length; + const newForms = forms + .slice(0, index) + .concat([data]) + .concat(forms.slice(index + 1, forms.length)); + setForms(newForms); + }; + + const removeForm = ({ name }) => { + const index = forms.findIndex((f) => f.ref === name); + const newForms = forms.slice(0, index).concat(forms.slice(index + 2)); + setForms(newForms); + }; + + return ( + <div style={{ width: "100%" }}> + <label + className="components-base-control__label" + style={{ + fontSize: "11px", + textTransform: "uppercase", + fontWeight: 500, + marginBottom: "calc(8px)", + }} + > + {__("Forms", "wpct-erp-forms")} + </label> + <TabPanel tabs={tabs}> + {(form) => ( + <Form + {...form} + remove={removeForm} + update={(newForm) => + updateForm( + forms.findIndex(({ ref }) => ref === form.name), + newForm + ) + } + /> + )} + </TabPanel> + </div> + ); +} diff --git a/src/RestApiSettings/index.jsx b/src/RestApiSettings/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9a74ac85377da911dfa59b034bbccd5fd8c2a6d5 --- /dev/null +++ b/src/RestApiSettings/index.jsx @@ -0,0 +1,30 @@ +// vendor +import React from "react"; +import { __ } from "@wordpress/i18n"; +import { + Card, + CardHeader, + CardBody, + __experimentalHeading as Heading, + PanelRow, +} from "@wordpress/components"; + +// source +import { useRestApi } from "../providers/Settings"; +import Forms from "./Forms"; + +export default function RestApiSettings() { + const [{ forms }, save] = useRestApi(); + return ( + <Card size="large" style={{ height: "fit-content" }}> + <CardHeader> + <Heading level={3}>{__("REST API", "wpct-erp-forms")}</Heading> + </CardHeader> + <CardBody> + <PanelRow> + <Forms forms={forms} setForms={(forms) => save({ forms })} /> + </PanelRow> + </CardBody> + </Card> + ); +} diff --git a/src/RpcApiSettings/Forms/Form.jsx b/src/RpcApiSettings/Forms/Form.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5c55b40c9ee4e489ab05f4cb82f6af1d823b53bc --- /dev/null +++ b/src/RpcApiSettings/Forms/Form.jsx @@ -0,0 +1,192 @@ +// vendor +import React from "react"; +import { __ } from "@wordpress/i18n"; +import { TextControl, SelectControl, Button } from "@wordpress/components"; +import { useState, useRef, useEffect } from "@wordpress/element"; + +// source +import { useForms } from "../../providers/Forms"; +import { useGeneral } from "../../providers/Settings"; + +function NewForm({ add }) { + const [{ backends }] = useGeneral(); + const backendOptions = backends.map(({ name, base_url }) => ({ + label: name, + value: base_url, + })); + const forms = useForms(); + const formOptions = forms.map(({ id, title }) => ({ + label: title, + value: id, + })); + + const [name, setName] = useState(""); + const [backend, setBackend] = useState(""); + const [model, setModel] = useState(""); + const [formId, setFormId] = useState(""); + + const onClick = () => add({ name, backend, model, form_id: formId }); + + const disabled = !(name, backend, model, formId); + + return ( + <div + style={{ + padding: "calc(24px) calc(32px)", + width: "calc(100% - 64px)", + backgroundColor: "rgb(245, 245, 245)", + }} + > + <div + style={{ + display: "flex", + gap: "1em", + }} + > + <TextControl + label={__("Bound ID", "wpct-erp-forms")} + value={name} + onChange={setName} + __nextHasNoMarginBottom + /> + <SelectControl + label={__("Backend", "wpct-erp-forms")} + value={backend} + onChange={setBackend} + options={backendOptions} + __nextHasNoMarginBottom + /> + <TextControl + label={__("Model", "wpct-erp-forms")} + value={model} + onChange={setModel} + __nextHasNoMarginBottom + /> + <SelectControl + label={__("Form", "wpct-erp-forms")} + value={formId} + onChange={setFormId} + options={formOptions} + __nextHasNoMarginBottom + /> + <Button + variant="primary" + onClick={() => onClick()} + style={{ marginTop: "auto", height: "32px" }} + disabled={disabled} + > + {__("Add", "wpct-erp-forms")} + </Button> + </div> + </div> + ); +} + +let focus; +export default function Form({ update, remove, ...data }) { + if (data.name === "add") return <NewForm add={update} />; + + const [{ backends }] = useGeneral(); + const backendOptions = backends.map(({ name, base_url }) => ({ + label: name, + value: base_url, + })); + const forms = useForms(); + const formOptions = forms.map(({ id, title }) => ({ + label: title, + value: id, + })); + + const [name, setName] = useState(data.name); + const nameInput = useRef(); + + useEffect(() => { + if (focus) { + nameInput.current.focus(); + } + }, []); + + const timeout = useRef(false); + useEffect(() => { + if (timeout.current === false) { + timeout.current = 0; + return; + } + + clearTimeout(timeout.current); + timeout.current = setTimeout(() => update({ ...data, name }), 500); + }, [name]); + + useEffect(() => { + timeout.current = false; + setName(data.name); + }, [data.name]); + + return ( + <div + style={{ + padding: "calc(24px) calc(32px)", + width: "calc(100% - 64px)", + backgroundColor: "rgb(245, 245, 245)", + }} + > + <div + style={{ + display: "flex", + gap: "1em", + }} + > + <TextControl + ref={nameInput} + label={__("Bound ID", "wpct-erp-forms")} + value={name} + onChange={setName} + onFocus={() => (focus = true)} + onBlur={() => (focus = false)} + __nextHasNoMarginBottom={true} + /> + <SelectControl + label={__("Backend", "wpct-erp-forms")} + value={data.backend} + onChange={(backend) => update({ ...data, backend })} + options={backendOptions} + __nextHasNoMarginBottom + /> + <TextControl + label={__("Model", "wpct-erp-forms")} + value={data.model} + onChange={(model) => update({ ...data, model })} + __nextHasNoMarginBottom={true} + /> + <SelectControl + label={__("Form", "wpct-erp-forms")} + value={data.form_id} + onChange={(form_id) => update({ ...data, form_id })} + options={formOptions} + __nextHasNoMarginBottom + /> + <div> + <label + style={{ + display: "block", + fontWeight: 500, + textTransform: "uppercase", + fontSize: "11px", + marginBottom: "calc(4px)", + }} + > + {__("Remove form", "wpct-erp-forms")} + </label> + <Button + isDestructive + variant="primary" + onClick={() => remove(data)} + style={{ width: "130px", justifyContent: "center", height: "32px" }} + > + {__("Remove", "wpct-erp-forms")} + </Button> + </div> + </div> + </div> + ); +} diff --git a/src/RpcApiSettings/Forms/index.jsx b/src/RpcApiSettings/Forms/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0f734c1680e04574d6acc211d2c829a2df6b5d8d --- /dev/null +++ b/src/RpcApiSettings/Forms/index.jsx @@ -0,0 +1,72 @@ +// vendor +import React from "react"; +import { __ } from "@wordpress/i18n"; +import { TabPanel } from "@wordpress/components"; + +// source +import Form from "./Form"; + +export default function Forms({ forms, setForms }) { + const tabs = forms + .map(({ backend, model, form_id, ref }) => ({ + name: ref, + title: ref, + form_id, + model, + backend, + })) + .concat([ + { + title: __("Add Form", "wpct-erp-forms"), + name: "add", + }, + ]); + + const updateForm = (index, data) => { + data = { ...data, ref: data.name }; + delete data.name; + + if (index === -1) index = forms.length; + const newForms = forms + .slice(0, index) + .concat([data]) + .concat(forms.slice(index + 1, forms.length)); + setForms(newForms); + }; + + const removeForm = ({ name }) => { + const index = forms.findIndex((f) => f.ref === name); + const newForms = forms.slice(0, index).concat(forms.slice(index + 2)); + setForms(newForms); + }; + + return ( + <div style={{ width: "100%" }}> + <label + className="components-base-control__label" + style={{ + fontSize: "11px", + textTransform: "uppercase", + fontWeight: 500, + marginBottom: "calc(8px)", + }} + > + {__("Forms", "wpct-erp-forms")} + </label> + <TabPanel tabs={tabs}> + {(form) => ( + <Form + {...form} + remove={removeForm} + update={(newForm) => + updateForm( + forms.findIndex(({ ref }) => ref === form.name), + newForm + ) + } + /> + )} + </TabPanel> + </div> + ); +} diff --git a/src/RpcApiSettings/index.jsx b/src/RpcApiSettings/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b59ff6749d3aacea1af49aa43e9af3d60268971a --- /dev/null +++ b/src/RpcApiSettings/index.jsx @@ -0,0 +1,69 @@ +// vendor +import React from "react"; +import { __ } from "@wordpress/i18n"; +import { + Card, + CardHeader, + CardBody, + __experimentalHeading as Heading, + PanelRow, + TextControl, + __experimentalSpacer as Spacer, +} from "@wordpress/components"; + +// source +import { useRpcApi } from "../providers/Settings"; +import Forms from "./Forms"; + +export default function RpcApiSettings() { + const [{ endpoint, user, password, database, forms }, save] = useRpcApi(); + + const update = (field) => + save({ endpoint, user, password, database, forms, ...field }); + + return ( + <Card size="large" style={{ height: "fit-content" }}> + <CardHeader> + <Heading level={3}>{__("RPC API", "wpct-erp-forms")}</Heading> + </CardHeader> + <CardBody> + <PanelRow> + <TextControl + label={__("Endpoint", "wpct-erp-forms")} + onChange={(endpoint) => update({ endpoint })} + value={endpoint} + __nextHasNoMarginBottom + /> + </PanelRow> + <PanelRow> + <TextControl + label={__("Database", "wpct-erp-forms")} + onChange={(database) => update({ database })} + value={database} + __nextHasNoMarginBottom + /> + </PanelRow> + <PanelRow> + <TextControl + label={__("User", "wpct-erp-forms")} + onChange={(user) => update({ user })} + value={user} + __nextHasNoMarginBottom + /> + </PanelRow> + <PanelRow> + <TextControl + label={__("Password", "wpct-erp-forms")} + onChange={(password) => update({ password })} + value={password} + __nextHasNoMarginBottom + /> + </PanelRow> + <Spacer paddingY="calc(8px)" /> + <PanelRow> + <Forms forms={forms} setForms={(forms) => update({ forms })} /> + </PanelRow> + </CardBody> + </Card> + ); +} diff --git a/src/SettingsPage/index.jsx b/src/SettingsPage/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c67c711e6f8edbea2ce828aa6dd389ccc6a09463 --- /dev/null +++ b/src/SettingsPage/index.jsx @@ -0,0 +1,87 @@ +// vendor +import React from "react"; +import { __ } from "@wordpress/i18n"; +import { + TabPanel, + __experimentalHeading as Heading, + Button, + __experimentalSpacer as Spacer, +} from "@wordpress/components"; +import { useState } from "@wordpress/element"; + +// source +import SettingsProvider, { useSubmitSettings } from "../providers/Settings"; +import FormsProvider from "../providers/Forms"; +import GeneralSettings from "../GeneralSettings"; +import RestApiSettings from "../RestApiSettings"; +import RpcApiSettings from "../RpcApiSettings"; + +const tabs = [ + { + name: "general", + title: "General", + }, + { + name: "rest-api", + title: "REST API", + }, + { + name: "rpc-api", + title: "RPC API", + }, +]; + +function Content({ tab }) { + switch (tab.name) { + case "rest-api": + return <RestApiSettings />; + case "rpc-api": + return <RpcApiSettings />; + default: + return <GeneralSettings />; + } +} + +function SaveButton() { + const submit = useSubmitSettings(); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(false); + + const onClick = () => { + setLoading(true); + submit() + .then(() => setLoading(false)) + .catch(() => setError(true)); + }; + + return ( + <Button + variant={error ? "secondary" : "primary"} + onClick={onClick} + style={{ minWidth: "130px", justifyContent: "center" }} + disabled={loading} + __next40pxDefaultSize + > + {(error && __("Error")) || __("Save")} + </Button> + ); +} + +export default function SettingsPage() { + return ( + <SettingsProvider> + <Heading level={1}>Wpct ERP Forms</Heading> + <TabPanel initialTabName="general" tabs={tabs}> + {(tab) => ( + <FormsProvider> + <Spacer /> + <Content tab={tab} /> + </FormsProvider> + )} + </TabPanel> + <Spacer /> + <SaveButton /> + </SettingsProvider> + ); +} diff --git a/src/index.jsx b/src/index.jsx index 02703e36f3814e51f09e9d591ce9a5907c3878f6..a8db10c8f87309485c847882358746aa560ae6ef 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,14 +1,18 @@ +// vendor +import React from "react"; import domReady from "@wordpress/dom-ready"; import { createRoot } from "@wordpress/element"; -const SettingsPage = () => { - return <div>Placeholder for settings page</div>; -}; +// source +import SettingsPage from "./SettingsPage/index.jsx"; +import ErrorBoundary from "./ErrorBoundary.jsx"; domReady(() => { - const root = createRoot( - document.getElementById("unadorned-announcement-bar-settings") - ); + const root = createRoot(document.getElementById("wpct-erp-forms")); - root.render(<SettingsPage />); + root.render( + <ErrorBoundary fallback={<h1>Error</h1>}> + <SettingsPage /> + </ErrorBoundary> + ); }); diff --git a/src/providers/Forms.jsx b/src/providers/Forms.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4af303c166fc411dc7cd7adc37ee897cd3db44a3 --- /dev/null +++ b/src/providers/Forms.jsx @@ -0,0 +1,43 @@ +// vendor +import React from "react"; +import { __ } from "@wordpress/i18n"; +import apiFetch from "@wordpress/api-fetch"; +import { + createContext, + useContext, + useState, + useEffect, +} from "@wordpress/element"; + +// source +import Loading from "../Loading"; + +const FormsContext = createContext([]); + +export default function FormsProvider({ children }) { + const [forms, setForms] = useState([]); + + const [loading, setLoading] = useState(true); + + useEffect(() => { + apiFetch({ + path: `${window.wpApiSettings.root}wpct/v1/erp-forms/forms`, + headers: { + "X-WP-Nonce": wpApiSettings.nonce, + }, + }) + .then((forms) => setForms(forms)) + .finally(() => setLoading(false)); + }, []); + + return ( + <FormsContext.Provider value={forms}> + {(loading && <Loading message={__("Loading", "wpct-erp-forms")} />) || + children} + </FormsContext.Provider> + ); +} + +export function useForms() { + return useContext(FormsContext); +} diff --git a/src/providers/Settings.jsx b/src/providers/Settings.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c0864147ef317bcd85c7b472671d9e90952440c4 --- /dev/null +++ b/src/providers/Settings.jsx @@ -0,0 +1,120 @@ +// vendor +import React from "react"; +import { __ } from "@wordpress/i18n"; +import apiFetch from "@wordpress/api-fetch"; +import { + createContext, + useContext, + useState, + useEffect, +} from "@wordpress/element"; + +// source +import Loading from "../Loading"; + +const noop = () => {}; + +const defaultSettings = { + "general": { + notification_receiver: `admin@${window.location.hostname}`, + backends: [], + }, + "rest-api": { + forms: [], + }, + "rpc-api": { + endpoint: "/jsonrpc", + database: "crm.lead", + user: "admin", + password: "admin", + forms: [], + }, +}; + +const SettingsContext = createContext([defaultSettings, noop]); + +export default function SettingsProvider({ children }) { + const [general, setGeneral] = useState({ ...defaultSettings.general }); + const [restApi, setRestApi] = useState({ ...defaultSettings["rest-api"] }); + const [rpcApi, setRpcApi] = useState({ ...defaultSettings["rpc-api"] }); + + const [loading, setLoading] = useState(true); + + useEffect(() => { + apiFetch({ + path: `${window.wpApiSettings.root}wpct/v1/erp-forms/settings`, + headers: { + "X-WP-Nonce": wpApiSettings.nonce, + }, + }) + .then((settings) => { + setGeneral(settings.general); + setRestApi(settings["rest-api"]); + setRpcApi(settings["rpc-api"]); + }) + .finally(() => setLoading(false)); + }, []); + + const saveSettings = () => { + return apiFetch({ + path: `${window.wpApiSettings.root}wpct/v1/erp-forms/settings`, + method: "POST", + headers: { + "X-WP-Nonce": wpApiSettings.nonce, + }, + data: { + general, + "rest-api": restApi, + "rpc-api": rpcApi, + }, + }); + }; + + return ( + <SettingsContext.Provider + value={[ + { + general, + setGeneral, + restApi, + setRestApi, + rpcApi, + setRpcApi, + }, + saveSettings, + ]} + > + {(loading && <Loading message={__("Loading", "wpct-erp-forms")} />) || + children} + </SettingsContext.Provider> + ); +} + +export function useGeneral() { + const [{ general, setGeneral }] = useContext(SettingsContext); + + const { notification_receiver: receiver, backends } = general; + + const update = ({ receiver, backends }) => + setGeneral({ + notification_receiver: receiver, + backends, + }); + + return [{ receiver, backends }, update]; +} + +export function useRestApi() { + const [{ restApi, setRestApi }] = useContext(SettingsContext); + return [restApi, setRestApi]; +} + +export function useRpcApi() { + const [{ rpcApi, setRpcApi }] = useContext(SettingsContext); + return [rpcApi, setRpcApi]; +} + +export function useSubmitSettings() { + const [, submit] = useContext(SettingsContext); + return submit; +}