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 (
{value === index && {children}}
);
}
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 (
);
};
export default WidgetWizard;