diff --git a/forms-bridge/includes/class-addon.php b/forms-bridge/includes/class-addon.php index a722cb9d..f219f881 100644 --- a/forms-bridge/includes/class-addon.php +++ b/forms-bridge/includes/class-addon.php @@ -197,11 +197,7 @@ function ( $data ) { $registry = self::registry(); $addons = array(); foreach ( self::$addons as $name => $addon ) { - $logo_path = - FORMS_BRIDGE_ADDONS_DIR . - '/' . - $addon::NAME . - '/assets/logo.png'; + $logo_path = FORMS_BRIDGE_ADDONS_DIR . '/' . $addon::NAME . '/assets/logo.png'; if ( is_file( $logo_path ) && is_readable( $logo_path ) ) { $logo = plugin_dir_url( $logo_path ) . 'logo.png'; diff --git a/forms-bridge/includes/class-form-bridge.php b/forms-bridge/includes/class-form-bridge.php index a3a3afd3..1f3bab3b 100644 --- a/forms-bridge/includes/class-form-bridge.php +++ b/forms-bridge/includes/class-form-bridge.php @@ -194,21 +194,25 @@ public static function schema( $addon = null ) { 'default' => array(), ), 'is_valid' => array( - 'description' => __( - 'Validation result of the bridge setting', - 'forms-bridge' - ), + 'description' => __( 'Validation result of the bridge setting', 'forms-bridge' ), 'type' => 'boolean', 'default' => true, ), 'enabled' => array( - 'description' => __( - 'Boolean flag to enable/disable a bridge', - 'forms-bridge' - ), + 'description' => __( 'Boolean flag to enable/disable a bridge', 'forms-bridge' ), 'type' => 'boolean', 'default' => true, ), + 'allow_failure' => array( + 'description' => __( 'Whether an error on bridge submission should stop the submission loop or not', 'forms-bridge' ), + 'type' => 'boolean', + 'default' => true, + ), + 'order' => array( + 'description' => __( 'Order in which the bridge should be submitted in the submission loop', 'forms-bridge' ), + 'type' => 'integer', + 'default' => -1, + ), ), 'required' => array( 'name', @@ -221,6 +225,8 @@ public static function schema( $addon = null ) { 'workflow', 'is_valid', 'enabled', + 'allow_failure', + 'order', ), 'additionalProperties' => false, ); diff --git a/forms-bridge/includes/class-forms-bridge.php b/forms-bridge/includes/class-forms-bridge.php index 890f1e8c..ff670892 100644 --- a/forms-bridge/includes/class-forms-bridge.php +++ b/forms-bridge/includes/class-forms-bridge.php @@ -283,7 +283,23 @@ function ( $field ) { return; } - foreach ( $bridges as $bridge ) { + usort( + $bridges, + function ( $a, $b ) { + if ( 0 > $a->order ) { + return 1; + } elseif ( 0 > $b->order ) { + return -1; + } + + return $a->order - $b->order; + } + ); + + $l = count( $bridges ); + for ( $i = 0; $i < $l; ++$i ) { + $bridge = $bridges[ $i ]; + if ( ! $bridge->enabled ) { Logger::log( 'Skip submission for disabled bridge ' . $bridge->name @@ -436,6 +452,15 @@ function ( $field ) { $payload ?? $submission, $attachments ?? array() ); + + if ( + false === $bridge->allow_failure + && count( $bridges ) > 1 + && $i < count( $bridges ) - 1 + ) { + Logger::log( 'Early exit from the submission loop due to an error', Logger::ERROR ); + break; + } } finally { self::$current_bridge = null; } diff --git a/forms-bridge/includes/class-integration.php b/forms-bridge/includes/class-integration.php index 1e7d9335..26cc564a 100644 --- a/forms-bridge/includes/class-integration.php +++ b/forms-bridge/includes/class-integration.php @@ -197,10 +197,19 @@ function ( $data ) { $registry = self::registry(); $integrations = array(); foreach ( self::$integrations as $name => $integration ) { + $logo_path = FORMS_BRIDGE_INTEGRATIONS_DIR . '/' . $integration::NAME . '/assets/logo.png'; + + if ( is_file( $logo_path ) && is_readable( $logo_path ) ) { + $logo = plugin_dir_url( $logo_path ) . 'logo.png'; + } else { + $logo = ''; + } + $integrations[ $name ] = array( 'name' => $name, 'title' => $integration::TITLE, 'enabled' => $registry[ $name ] ?? false, + 'logo' => $logo, ); } diff --git a/forms-bridge/integrations/formidable/assets/logo.png b/forms-bridge/integrations/formidable/assets/logo.png new file mode 100644 index 00000000..d4dcb15f Binary files /dev/null and b/forms-bridge/integrations/formidable/assets/logo.png differ diff --git a/forms-bridge/integrations/gf/assets/logo.png b/forms-bridge/integrations/gf/assets/logo.png new file mode 100644 index 00000000..a2b626d1 Binary files /dev/null and b/forms-bridge/integrations/gf/assets/logo.png differ diff --git a/forms-bridge/integrations/ninja/assets/logo.png b/forms-bridge/integrations/ninja/assets/logo.png new file mode 100644 index 00000000..563d43e0 Binary files /dev/null and b/forms-bridge/integrations/ninja/assets/logo.png differ diff --git a/forms-bridge/integrations/woo/assets/logo.png b/forms-bridge/integrations/woo/assets/logo.png new file mode 100644 index 00000000..f2b8c877 Binary files /dev/null and b/forms-bridge/integrations/woo/assets/logo.png differ diff --git a/forms-bridge/integrations/wpcf7/assets/logo.png b/forms-bridge/integrations/wpcf7/assets/logo.png new file mode 100644 index 00000000..bb9de3ae Binary files /dev/null and b/forms-bridge/integrations/wpcf7/assets/logo.png differ diff --git a/forms-bridge/integrations/wpforms/assets/logo.png b/forms-bridge/integrations/wpforms/assets/logo.png new file mode 100644 index 00000000..8762c451 Binary files /dev/null and b/forms-bridge/integrations/wpforms/assets/logo.png differ diff --git a/src/components/Backend/index.jsx b/src/components/Backend/index.jsx index d82123d2..53288db1 100644 --- a/src/components/Backend/index.jsx +++ b/src/components/Backend/index.jsx @@ -120,7 +120,7 @@ export default function Backend({ update, remove, data, copy }) { padding: "6px 6px", }} onClick={copy} - label={__("Duplaicate", "forms-bridge")} + label={__("Duplicate", "forms-bridge")} showTooltip __next40pxDefaultSize > diff --git a/src/components/Bridge/index.jsx b/src/components/Bridge/index.jsx index 371dbb5c..c671d23b 100644 --- a/src/components/Bridge/index.jsx +++ b/src/components/Bridge/index.jsx @@ -251,7 +251,7 @@ export default function Bridge({ data, update, remove, schema, copy, names }) { padding: "6px 6px", }} onClick={copy} - label={__("Duplaicate", "forms-bridge")} + label={__("Duplicate", "forms-bridge")} showTooltip __next40pxDefaultSize > diff --git a/src/components/Bridges/index.jsx b/src/components/Bridges/index.jsx index dd3ea15f..ee1ee04b 100644 --- a/src/components/Bridges/index.jsx +++ b/src/components/Bridges/index.jsx @@ -12,8 +12,8 @@ const { TabPanel } = wp.components; const { useEffect, useMemo, useRef } = wp.element; const { __ } = wp.i18n; -const CSS = `.bridges-tabs-panel .components-tab-panel__tabs{overflow-x:auto;} -.bridges-tabs-panel .components-tab-panel__tabs>button{flex-shrink:0;}`; +const CSS = `.bridges-tabs-panel>.components-tab-panel__tabs{overflow-x:auto;} +.bridges-tabs-panel>.components-tab-panel__tabs>button{flex-shrink:0;}`; const DEFAULTS = { name: "bridge-" + Date.now(), diff --git a/src/components/Credential/index.jsx b/src/components/Credential/index.jsx index f0d08b24..2011337c 100644 --- a/src/components/Credential/index.jsx +++ b/src/components/Credential/index.jsx @@ -172,7 +172,7 @@ export default function Credential({ padding: "6px 6px", }} onClick={copy} - label={__("Duplaicate", "forms-bridge")} + label={__("Duplicate", "forms-bridge")} showTooltip __next40pxDefaultSize > diff --git a/src/components/Form/Bridges.jsx b/src/components/Form/Bridges.jsx new file mode 100644 index 00000000..a30fdcf8 --- /dev/null +++ b/src/components/Form/Bridges.jsx @@ -0,0 +1,201 @@ +const { + Button, + Modal, + __experimentalItemGroup: ItemGroup, + __experimentalItem: Item, +} = wp.components; +const { useState, useMemo } = wp.element; +const { __ } = wp.i18n; + +export default function FormBridges({ bridges, setBridges }) { + const [open, setOpen] = useState(false); + + const orderedBridges = useMemo(() => { + return bridges.sort((a, b) => { + if (isNaN(a.order)) return 1; + if (isNaN(b.order)) return -1; + return a.order - b.order; + }); + }, [bridges]); + + const move = (from, to) => { + const bridge = orderedBridges[from]; + + const slicedBridges = orderedBridges + .slice(0, from) + .concat(orderedBridges.slice(from + 1)); + + const newBridges = slicedBridges + .slice(0, to) + .concat(bridge) + .concat(slicedBridges.slice(to)); + + newBridges.forEach((bridge, index) => (bridge.order = index)); + setBridges(newBridges); + }; + + const setFailure = (index, policy) => { + const newBridges = bridges.map((bridge) => ({ ...bridge })); + newBridges[index].allow_failure = !!policy; + setBridges(newBridges); + }; + + return ( + <> + + {open && ( + setOpen(false)} + > +

+ {__( + "Manage the form's bridge chain and their submission failure policies", + "forms-bridge" + )} +

+
+
+ + {orderedBridges.map((bridge, i) => ( + + setFailure(i, policy)} + move={(direction) => move(i, i + direction)} + isLast={i === bridges.length - 1} + /> + + ))} + +
+
+
+ )} + + ); +} + +function BridgeStep({ index, name, failure, setFailure, move, isLast }) { + return ( +
+

+ setFailure(!failure)} + style={{ + fontSize: "1.25em", + marginRight: "1em", + marginLeft: "-0.7em", + cursor: "pointer", + }} + __next40pxDefaultSize + > + {failure === false ? "🔴" : "🟢"} + + {index + 1}. {name} + +

+
+ + +
+
+ ); +} diff --git a/src/components/Form/Fields.jsx b/src/components/Form/Fields.jsx new file mode 100644 index 00000000..49d3643e --- /dev/null +++ b/src/components/Form/Fields.jsx @@ -0,0 +1,42 @@ +// source +import StagePayload from "../Workflow/Payload"; + +const { __ } = wp.i18n; + +const DIFF_MOCK = { + enter: new Set(), + exit: new Set(), + mutated: new Set(), + missing: new Set(), +}; + +export default function FormFields({ fields }) { + return ( +
+
+

{__("Submission", "forms-bridge")}

+
+
+ +
+
+ ); +} diff --git a/src/components/Form/index.jsx b/src/components/Form/index.jsx new file mode 100644 index 00000000..f2754112 --- /dev/null +++ b/src/components/Form/index.jsx @@ -0,0 +1,79 @@ +// source +import { useIntegrations } from "../../hooks/useGeneral"; +import useResponsive from "../../hooks/useResponsive"; +import FormFields from "./Fields"; +import FormBridges from "./Bridges"; + +const { useMemo } = wp.element; + +export default function Form({ data, setBridges }) { + const isResponsive = useResponsive(); + + const [integrations] = useIntegrations(); + const integration = useMemo(() => { + const name = data._id.split(":")[0]; + return integrations.find((integration) => integration.name === name); + }, [integrations, data._id]); + + return ( +
+
+ +

{data.title}

+

{data._id}

+
+
+ +
+
+ +
+
+
+
+ ); +} diff --git a/src/components/Forms/index.jsx b/src/components/Forms/index.jsx new file mode 100644 index 00000000..6ba13f24 --- /dev/null +++ b/src/components/Forms/index.jsx @@ -0,0 +1,84 @@ +// source +import useBridges from "../../hooks/useBridges"; +import { useForms } from "../../providers/Forms"; +import Form from "../Form"; + +const { TabPanel } = wp.components; +const { useEffect, useMemo, useRef } = wp.element; +const { __ } = wp.i18n; + +const CSS = `.forms-tabs-panel>.components-tab-panel__tabs{overflow-x:auto;} +.forms-tabs-panel>.components-tab-panel__tabs>button{flex-shrink:0;}`; + +export default function Forms() { + const [forms] = useForms(); + const [bridges, setBridges] = useBridges(); + + const bridgedForms = useMemo(() => { + return forms.reduce((bridged, form) => { + const formBridges = bridges.filter( + (bridge) => bridge.form_id === form._id + ); + + if (!formBridges.length) return bridged; + return bridged.concat({ ...form, bridges: formBridges }); + }, []); + }, [forms, bridges]); + + const tabs = useMemo(() => { + return bridgedForms.map(({ title }, index) => ({ + index, + name: title, + title, + })); + }, [bridgedForms]); + + const style = useRef(document.createElement("style")); + useEffect(() => { + style.current.appendChild(document.createTextNode(CSS)); + document.head.appendChild(style.current); + + return () => { + document.head.removeChild(style.current); + }; + }, []); + + useEffect(() => { + const img = document.querySelector("#forms .addon-logo"); + if (!img) return; + img.removeAttribute("src"); + }, []); + + const updateBridges = (formBridges) => { + const order = formBridges.map(({ name }) => name); + + const newBridges = bridges.map((bridge) => { + const index = order.findIndex((name) => name === bridge.name); + if (index !== -1) { + return { + ...bridge, + allow_failure: formBridges[index].allow_failure, + order: index, + }; + } + + return bridge; + }); + + setBridges(newBridges); + }; + + return ( +
+

+ {__("Bridged forms", "forms-bridge")} +

+ + {(tab) => { + const form = bridgedForms[tab.index]; + return
; + }} + +
+ ); +} diff --git a/src/components/Jobs/index.jsx b/src/components/Jobs/index.jsx index e7f8bbe4..cfd91c46 100644 --- a/src/components/Jobs/index.jsx +++ b/src/components/Jobs/index.jsx @@ -209,7 +209,7 @@ function JobsContent({ loading, config, setEdit, reset, copy }) { padding: "6px 6px", }} onClick={copy} - label={__("Duplaicate", "forms-bridge")} + label={__("Duplicate", "forms-bridge")} showTooltip __next40pxDefaultSize > diff --git a/src/components/Mutations/index.jsx b/src/components/Mutations/index.jsx index 5384480f..f5dd0a5a 100644 --- a/src/components/Mutations/index.jsx +++ b/src/components/Mutations/index.jsx @@ -100,8 +100,21 @@ export default function Mutations({ onRequestClose={onClose} className="no-scrollable" > +

+ {__( + "Transform the form submission with field mappings and value mutations", + "forms-bridge" + )} +

button{flex-shrink:0;}`; +const CSS = `.settings-tabs-panel>.components-tab-panel__tabs{overflow-x:auto;} +.settings-tabs-panel>.components-tab-panel__tabs>button{flex-shrink:0;}`; export default function Settings() { const [tab, setTab] = useTab(); const [addons] = useAddons(); + const tabRef = useRef(tab); + const panelRef = useRef(); + const tabs = useMemo(() => { + const tabs = [ + { name: "general", title: __("General", "forms-bridge") }, + { name: "http", title: __("HTTP", "forms-bridge") }, + { name: "forms", title: __("Forms", "forms-bridge") }, + ]; + const addonTabs = addons .filter(({ enabled }) => enabled) .map(({ name, title }) => ({ name, title })); - return [ - { name: "general", title: __("General", "forms-bridge") }, - { name: "http", title: __("HTTP", "forms-bridge") }, - ].concat(addonTabs); + return tabs.concat(addonTabs); }, [addons]); + const onSelectTab = (tab) => { + tabRef.current = tab; + setTab(tab); + }; + + useEffect(() => { + if (tab === tabRef.current || !panelRef.current) return; + + const index = tabs.findIndex(({ name }) => tab === name); + const button = panelRef.current.querySelectorAll("button")[index]; + button.click(); + }, [tab, tabs]); + const style = useRef(document.createElement("style")); useEffect(() => { style.current.appendChild(document.createTextNode(CSS)); @@ -47,9 +67,10 @@ export default function Settings() {
{(tab) => (
@@ -74,6 +95,8 @@ export default function Settings() { ) : tab.name === "http" ? ( + ) : tab.name === "forms" ? ( + ) : ( )} diff --git a/src/hooks/useBridges.js b/src/hooks/useBridges.js index 1463b81a..07036848 100644 --- a/src/hooks/useBridges.js +++ b/src/hooks/useBridges.js @@ -1,13 +1,35 @@ -import { useApis } from "../providers/Settings"; +import { useAddons } from "../providers/Settings"; const { useMemo } = wp.element; export default function useBridges() { - const [apis] = useApis(); + const [addons, patch] = useAddons(); - return useMemo(() => { - return Object.keys(apis).reduce((bridges, api) => { - return bridges.concat(apis[api].backends || []); + const bridges = useMemo(() => { + return Object.keys(addons).reduce((bridges, addon) => { + const addonBridges = addons[addon].bridges || []; + return bridges.concat( + addonBridges.map((bridge) => ({ ...bridge, addon })) + ); }, []); - }, [apis]); + }, [addons]); + + const setBridges = (bridges) => { + const newAddons = Object.keys(addons).reduce((newAddons, addon) => { + const addonBridges = bridges + .filter((bridge) => bridge.addon === addon) + .map((bridge) => { + const newBridge = { ...bridge }; + delete newBridge.addon; + return newBridge; + }); + + newAddons[addon] = { ...addons[addon], bridges: addonBridges }; + return newAddons; + }, {}); + + patch(newAddons); + }; + + return [bridges, setBridges]; } diff --git a/src/providers/Forms.jsx b/src/providers/Forms.jsx index 648ed6e8..7ff4ec3a 100644 --- a/src/providers/Forms.jsx +++ b/src/providers/Forms.jsx @@ -2,6 +2,7 @@ import { useLoading } from "./Loading"; import { useError } from "./Error"; import { useIntegrations } from "../hooks/useGeneral"; import diff from "../lib/diff"; +import { useSettings } from "./Settings"; const { createContext, useContext, useState, useEffect, useRef } = wp.element; const apiFetch = wp.apiFetch; @@ -10,6 +11,7 @@ const { __ } = wp.i18n; const FormsContext = createContext([]); export default function FormsProvider({ children }) { + const [settings] = useSettings(); const [loading, setLoading] = useLoading(); const [, setError] = useError(); const [forms, setForms] = useState([]); @@ -29,11 +31,11 @@ export default function FormsProvider({ children }) { }, [integrations]); useEffect(() => { - if (loading) return; + if (loading || window.__wpfbInvalidated) return; if (invalid.current) { fetch().then(() => (invalid.current = false)); } - }, [loading, integrations]); + }, [settings, loading, integrations]); const fetch = useRef(() => { setLoading(true); diff --git a/src/providers/Jobs.jsx b/src/providers/Jobs.jsx index 134c2823..67bb683f 100644 --- a/src/providers/Jobs.jsx +++ b/src/providers/Jobs.jsx @@ -99,7 +99,7 @@ export default function JobsProvider({ children }) { } }) .catch((err) => { - console.log(err); + console.error(err); setError(__("Job reset error", "forms-bridge")); }) .finally(() => setLoading(false)); diff --git a/src/providers/Schemas.jsx b/src/providers/Schemas.jsx index 5fcfdb6f..492125bd 100644 --- a/src/providers/Schemas.jsx +++ b/src/providers/Schemas.jsx @@ -52,7 +52,8 @@ export default function SchemasProvider({ children }) { }, []); useEffect(() => { - if (tab && tab !== "general" && tab !== "http") fetch(tab); + if (tab && tab !== "general" && tab !== "http" && tab !== "forms") + fetch(tab); }, [fetch, tab]); const schema = useMemo(() => {