import { Box, Button, Stack, Tab, Tabs } from "@mui/material"; import React, { useEffect, useState } from "react"; import { actionBackgroundColor, actionTextColor, attributeRule, audience, backgroundColor, cancelAction, cancelBackgroundColor, cancelMessage, cancelShow, cancelTextColor, closeAction, closeColor, confirmAction, contentCollection, contentDisplayDescription, contentDisplayDescriptionLimit, contentDisplayImage, contentDisplayTitle, contentRank, contentShuffle, contentVisited, dateRangeEnd, dateRangeIndefinite, dateRangeStart, displayConditions, Field, Flow, fieldBackgroundColor, formElements, headline, headlineColor, hideAfter, hideAfterActionCancelHideCount, hideAfterActionCancelHideDuration, hideAfterActionClosedHideCount, hideAfterActionClosedHideDuration, hideAfterActionConfirmHideCount, hideAfterActionConfirmHideDuration, image, impressionsGlobalDuration, impressionsGlobalSession, impressionsGlobalTotal, impressionsWidgetDuration, impressionsWidgetSession, impressionsWidgetTotal, layout, message, okLinkNewTab, okLinkURL, okMessage, okShow, okShowLink, onInit, onLoad, origin, pageVisits, personalizationKey, position, positionSelector, pushDown, SelectOption, scrollPercentageToDisplay, showDelay, showOnExitIntent, targetMethod, textColor, theme, type, urlContains, widgetDescription, widgetSlug, widgetStatus, widgetTitle, } from "../data/pfa-fields"; import { findFieldById, shouldApplyDefaultValue } from "../utility/fieldLogic"; import { getValueByDotNotation, removeEmptyObjects } from "../utility/objects"; import { PathforaHandler } from "../utility/pathforaInterface"; import { BrandingSection } from "./branding"; import { DisplayRulesSection } from "./displayRules"; import { CallbackFnEditor } from "./form/callbackFn"; import { CodeEditor } from "./form/codeEditor"; import { TextInput } from "./form/input"; import { SectionHeader } from "./form/sectionHeader"; import { SelectInput } from "./form/select"; import { TextAreaInput } from "./form/textarea"; import { FormBuilder } from "./formBuilder"; import { MessagingSection } from "./messaging"; import { PositionSection } from "./position"; import { RecommendationSection } from "./recommendation"; import { TargetingSection } from "./targeting"; interface WidgetWizardProps { accountid: string; accesstoken?: string; pathforaconfig: string; availableaudiences: string; availableflows: string; availablecollections: string; availablepersonalizationkeys: string; availablefields: string; // parent fields titlefield: string; descriptionfield: string; statusfield: string; configurationfield: string; } const inputSpaceVert = 3; interface TabPanelProps { children?: React.ReactNode; index: number; value: number; } function TabPanel(props: TabPanelProps) { const { children, value, index, ...other } = props; return ( ); } const WidgetWizard: React.FC = ({ accountid, pathforaconfig, availableaudiences, availablecollections, availableflows, availablepersonalizationkeys, titlefield, descriptionfield, statusfield, configurationfield, }) => { const [formValues, setFormValues] = useState<{ [key: string]: any; }>({}); const [formFieldVisibility, setFormFieldVisibility] = useState<{ [key: string]: boolean; }>({}); const [formFieldDisabled, setFormFieldDisabled] = useState<{ [key: string]: boolean; }>({}); const [editorTypeTabValue, setEditorTypeTabValue] = useState(0); const [basicEditorTabValue, setBasicEditorTabValue] = useState(0); const [advancedEditorTabValue, setAdvancedEditorTabValue] = useState(0); const [renderedConfig, setRenderedConfig] = useState(pathforaconfig); const [pathfora, setPathfora] = useState(); const [audiences, setAudiences] = useState([]); const [collections, setCollections] = useState([]); const [_flows, setFlows] = useState([]); const [personalizationKeys, setPersonalizationKeys] = useState< SelectOption[] >([]); const [slugLink, setSlugLink] = useState(true); const fields: Field[] = [ type, headline, layout, theme, targetMethod, personalizationKey, attributeRule, backgroundColor, textColor, headlineColor, closeColor, actionBackgroundColor, actionTextColor, cancelBackgroundColor, cancelTextColor, fieldBackgroundColor, message, okShow, okMessage, okShowLink, okLinkURL, okLinkNewTab, cancelShow, cancelMessage, image, positionSelector, position, origin, pushDown, widgetTitle, widgetDescription, widgetSlug, widgetStatus, displayConditions, hideAfter, pageVisits, scrollPercentageToDisplay, showDelay, showOnExitIntent, impressionsGlobalDuration, impressionsGlobalSession, impressionsGlobalTotal, impressionsWidgetDuration, impressionsWidgetSession, impressionsWidgetTotal, hideAfterActionCancelHideCount, hideAfterActionClosedHideDuration, hideAfterActionClosedHideCount, hideAfterActionConfirmHideCount, hideAfterActionConfirmHideDuration, hideAfterActionCancelHideCount, hideAfterActionCancelHideDuration, dateRangeStart, dateRangeIndefinite, dateRangeEnd, urlContains, confirmAction, cancelAction, closeAction, onInit, onLoad, audience, formElements, contentCollection, contentRank, contentVisited, contentShuffle, contentDisplayTitle, contentDisplayImage, contentDisplayDescription, contentDisplayDescriptionLimit, ]; const tabToValueMapping = { "basic-tab-0": 0, "basic-tab-1": 1, "basic-tab-2": 2, "basic-tab-3": 3, "basic-tab-4": 4, "basic-tab-5": 5, "basic-tab-6": 6, }; useEffect(() => { if (availableaudiences) { const decodedAudiences = atob(availableaudiences); setAudiences(JSON.parse(decodedAudiences)); } if (availablecollections) { const decodedCollections = atob(availablecollections); setCollections(JSON.parse(decodedCollections)); } if (availableflows) { const decodedFlows = atob(availableflows); setFlows(JSON.parse(decodedFlows)); } if (availablepersonalizationkeys) { const decodedPersonalizationKeys = atob(availablepersonalizationkeys); const parsedPersonalizationKeys = JSON.parse(decodedPersonalizationKeys); setPersonalizationKeys( parsedPersonalizationKeys.map((label) => ({ label, value: label })), ); } }, []); // initialize the pathfora handler useEffect(() => { const pathforaHandler = async () => { try { const handler = new PathforaHandler(true, true, accountid); await new Promise((resolve) => setTimeout(resolve, 1000)); // Adjust the delay as needed setPathfora(handler); } catch (error) { console.error("Error creating PathforaHandler:", error); } }; pathforaHandler(); }, []); // render existing configuration useEffect(() => { updateValuesFromConfig(pathforaconfig); }, []); // update source fields when any value changes useEffect(() => { checkSourceLink(); }, [formValues]); useEffect(() => { const editConfig = document.getElementById( "edit-configuration", ) as HTMLInputElement; if (editConfig) { editConfig.value = renderConfiguration(); } }, [formValues]); const handlePathforaPreview = () => { const config = JSON.parse(renderedConfig); const widget = pathfora?.deserializeWidget(config); pathfora?.testWidget(widget); }; const updateValuesFromConfig = (config: string) => { if (!config) { config = JSON.stringify({ details: { status: "draft", }, }); } const inboundPathforaConfig = JSON.parse(config); // handle basic fields fields.forEach((field) => { const { id, render } = field; if (render === undefined) { return; } let value = getValueByDotNotation(inboundPathforaConfig, render); // if we have no status set it to draft if (id === widgetStatus.id && !value) { value = "draft"; } setFormValues((prevState) => ({ ...prevState, [id]: value, })); checkDependency(field.id, value); }); }; const handleEditorTypeTabChange = (_event, newValue) => { setEditorTypeTabValue(newValue); }; const handleBasicEditorTabChange = (event, _newValue) => { const id = event.target.id; const value = tabToValueMapping[id]; setBasicEditorTabValue(value); }; const handleAdvancedEditorTabChange = (_event, newValue) => { setAdvancedEditorTabValue(newValue); }; const sluggifyString = (str: string) => { return str .toLowerCase() .replace(/ /g, "_") .replace(/[^a-z0-9_-]/g, ""); }; const handleSlugLink = (fieldId: string, value: any) => { // special case handler for slug linking if (fieldId === widgetTitle.id && slugLink) { if ( formValues[widgetSlug.id] && formValues[widgetSlug.id] !== sluggifyString(formValues[widgetTitle.id]) ) { setSlugLink(false); return; } setFormValues((prevFormValues) => ({ ...prevFormValues, [widgetSlug.id]: sluggifyString(value), })); } if (fieldId === widgetSlug.id) { // if it doesn't match the slugified title then unlink it if (value !== sluggifyString(formValues[widgetTitle.id])) { setSlugLink(false); } } }; const handleChange = (fieldId: string, value: any) => { handleSlugLink(fieldId, value); setFormValues((prevFormValues) => ({ ...prevFormValues, [fieldId]: value, })); checkDependency(fieldId, value); }; const renderConfiguration = () => { let config = {}; let renderedConfigObject: any; try { renderedConfigObject = JSON.parse(renderedConfig); } catch (error) { console.error("Error parsing rendered config:", error); } fields.forEach((field) => { // check if we have a valid value set const hasValue = formValues[field.id] !== undefined; // if we have a value render its position in the config object if (field.render === undefined) { return; } const pathArray = field.render.split("."); const pathsToVerify = pathArray.slice(0, pathArray.length - 1); // loop each of the paths to verify and ensure there is a value set, we can assume these will all be objects let currentObject = config; pathsToVerify.forEach((path) => { if (!currentObject[path]) { currentObject[path] = {}; } currentObject = currentObject[path]; }); // if we have a value set, add it to the config object if (hasValue) { currentObject[pathArray[pathArray.length - 1]] = formValues[field.id]; } }); // handle translate fields fields.forEach((field) => { if (field.translate && formValues[field.id] !== undefined) { const fieldValue = formValues[field.id]; const translatedValue = field.translate.renderValue( fieldValue, renderedConfigObject, ); // render the translated value to its position const pathArray = field.translate.render.split("."); const pathsToVerify = pathArray.slice(0, pathArray.length - 1); let currentObject = config; pathsToVerify.forEach((path) => { if (!currentObject[path]) { currentObject[path] = {}; } currentObject = currentObject[path]; }); currentObject[pathArray[pathArray.length - 1]] = translatedValue; } }); config = removeEmptyObjects(config); const output = JSON.stringify(config, null, 2); setRenderedConfig(output); return output; }; const checkSourceLink = () => { const titleElement = document.getElementById( titlefield, ) as HTMLInputElement; titleElement.value = formValues[widgetTitle.id] as string; const descriptionElement = document.getElementById( descriptionfield, ) as HTMLInputElement; descriptionElement.value = formValues[widgetDescription.id] as string; const statusElement = document.getElementById( statusfield, ) as HTMLInputElement; statusElement.value = formValues[widgetStatus.id] as string; const configElement = document.getElementById( configurationfield, ) as HTMLInputElement; configElement.value = renderConfiguration(); }; const getDependentFields = (fieldId: string): string[] => { const field = fields.find((f) => f.id === fieldId); const dependentFields: string[] = []; if (field?.dependencies) { field.dependencies.forEach((dependency) => { dependency.fieldsToShow?.forEach((id) => { dependentFields.push(id); // Recursively get nested dependencies dependentFields.push(...getDependentFields(id)); }); }); } return dependentFields; }; const checkDependency = (fieldID: string, value: string) => { const field = fields.find((field) => field.id === fieldID); if (!field?.dependencies) { return; } // get all fields to show and hide them initially const uniqueFieldsToShow = new Set(); const uniqueFieldsToDisable = new Set(); field.dependencies?.forEach((dependency) => { dependency.fieldsToShow?.forEach((fieldId: string) => { uniqueFieldsToShow.add(fieldId); }); dependency.fieldsToDisable?.forEach((fieldId: string) => { uniqueFieldsToDisable.add(fieldId); }); }); const allFieldsToShow = Array.from(uniqueFieldsToShow); const allFieldsToDisable = Array.from(uniqueFieldsToDisable); // set visibility to false for all fields to show allFieldsToShow.forEach((id) => { setFormFieldVisibility((prevVisibility) => ({ ...prevVisibility, [id]: false, })); }); // set visibility to false for all fields to disable allFieldsToDisable.forEach((id) => { setFormFieldDisabled((prevDisabled) => ({ ...prevDisabled, [id]: false, })); }); let valuesToCheck: string[] = []; if (field.type === "array" && value) { valuesToCheck = value.split(","); } else { valuesToCheck = [value]; } // Collect fields that should remain visible const fieldsToKeepVisible = new Set(); const fieldsToKeepDisabled = new Set(); valuesToCheck.forEach((v) => { // see if there is a dependency where the value matches the value set for the field const dependencyMatch = field.dependencies?.find( (dependency) => dependency.value === v, ); // if there is a match, show the fields if (dependencyMatch) { dependencyMatch.fieldsToShow?.forEach((id) => { fieldsToKeepVisible.add(id); setFormFieldVisibility((prevVisibility) => ({ ...prevVisibility, [id]: true, })); // Set default value for fields that define one const dependentField = findFieldById(fields, id); if (shouldApplyDefaultValue(dependentField, formValues[id])) { setFormValues((prevFormValues) => ({ ...prevFormValues, [id]: dependentField.defaultValue, })); } }); dependencyMatch.fieldsToDisable?.forEach((id) => { fieldsToKeepDisabled.add(id); setFormFieldDisabled((prevDisabled) => ({ ...prevDisabled, [id]: true, })); }); } }); // Clear values for fields that should be hidden const fieldsToClear = allFieldsToShow .filter((id) => !fieldsToKeepVisible.has(id)) .concat(Array.from(fieldsToKeepDisabled)); if (fieldsToClear.length > 0) { setFormValues((prevFormValues) => { const newFormValues = { ...prevFormValues }; fieldsToClear.forEach((id) => { // Get all nested dependent fields recursively const nestedDependents = getDependentFields(id); // Clear the field and all its nested dependencies delete newFormValues[id]; nestedDependents.forEach((nestedId) => { delete newFormValues[nestedId]; }); }); return newFormValues; }); } }; const isFieldSet = (field: string): boolean => { return ( formValues[field] !== undefined && formValues[field] !== "" && formValues[field] !== "false" ); }; const handleConfigChange = (value: string) => { updateValuesFromConfig(value); }; const renderCallbackFunction = (value: string) => { if (!value) { return false; } let callback: any; try { callback = Function(`"use strict";return (${value})`)(); } catch (error) { console.warn("Invalid function:", error); return; } if (typeof callback !== "function") { console.warn("Invalid function:", callback, "is not a function"); return; } return callback; }; const handleCallbackChange = (field: Field, value: string) => { let callbackFnString = ""; if (value) { const callbackFn = renderCallbackFunction(value); if (!callbackFn) { throw new Error("Callback function is not defined."); } callbackFnString = callbackFn.toString(); } setFormValues((prevFormValues) => ({ ...prevFormValues, [field.id]: callbackFnString, })); }; const handleSubmit = (_event: React.FormEvent): void => { // no op for now }; return (
{/* top level widget details */} {/* Widget Status */} {/* configure widget details */} {isFieldSet(type.id) && ( {editorTypeTabValue === 1 && ( {/* */} {/* Callback Function Editor Tab */} {/* Pathfora Editor Tab */} {/* CSS Editor Tab */} {/* */} )} {editorTypeTabValue === 0 && ( handleBasicEditorTabChange(event, newValue) } sx={{ borderRight: 1, borderColor: "divider", "& .MuiButtonBase-root": { "&:focus": { boxShadow: "none", borderColor: "none", }, }, }} > {/* Messaging Tab */} {/* Taregeting Tab */} {/* Position Tab */} {/* Branding Tab */} {/* Display Rules Tab */} {/* Form Fields */} {formValues[type.id] === "form" && ( )} {/* Recommendation Tab */} {formValues[type.id] === "recommendation" && ( )} )} )}
); }; export default WidgetWizard;