import React, {FC, ReactNode, useLayoutEffect, useRef, useState} from "react"; import {colors} from "./tree/Colors"; import {Handle} from "./tree/Handle"; import {Handle as ReactFlowHandle, NodeProps, Position} from "@xyflow/react"; import {RenderedNodeDataInBranch, TestablePartial, useNodesStore} from "./tree/store"; import {motion} from 'motion/react'; import classNames from "classnames"; import {getNodesFinder} from "./tree/NodesFinder"; import {StateBranchNode} from "./tree/StateBranchNode"; import {request} from "./tree/Color"; import {__, CouponsPlus} from "./globals"; import {EmulatedButton} from "./tree/EmulatedButton"; import {useOpenTestablesPopupWindow} from "./tree/useOpenTestablesPopupWindow"; import {getOpenPopupsCount} from "./tree/hooks"; import {cloneDeep, merge} from "lodash"; import {getPopupStackDimensionCalculators} from "./tree/ulitilies"; import {getOffersPopupWindowContext} from "./OffersBranch"; import {incompatibleOffersFromInsideTiers} from "./tree/actions/SelectOffersNew"; import {NonImplementedMessage} from "./tree/actions/NonImplementedMessage"; import {PopupWindowStateContext, ProUpgradePopupWindowIsOpen, TreeContextData} from "./tree/atoms"; import {ComponentMeta, getComponentMeta, getComponentMetaData} from "./tree/ComponentsMeta"; import {generateIdForType} from "./tree/NodeHelpers"; import {equals, TierType} from "./tree/tiers"; import {AmountValidatorOptions} from "./tree/cards/filterinterfaces"; import {pickByPaths} from "./tree/actions/SelectTierNew"; import {Button} from "./tree/cards/Button"; import {SuggestedArrow} from "./tree/SuggestedArrow"; import {useSuggestedScopeForTestableId} from "./tree/getSuggestedScopeForTestable"; import {ComponentCategory} from "./tree/components"; import {FloatingMarker} from "./tree/FloatingMarker"; import {DiscountMeta} from "./tree/cards/Discount"; import {useSetAtom} from "jotai"; export type SelectActionSplitNodeData = RenderedNodeDataInBranch & { renderedNodeType: 'action-split', } type ActionButtonProps = { label: string; icon: React.ReactNode; onClick: () => void; color: typeof colors.blue, inversedColor?: boolean } const ActionButton: FC = ({label, icon, onClick, color, inversedColor}) => { return (
{icon}
{label}
); } let offersTestableisOpen = false; let tiertestableisOpen = false; const tierIsNotImplemented = CouponsPlus.implementations.branches.tiered === false const getDefaultOptionsForFirstTierSubchild = (meta: ComponentMeta): Record => { const tiertype = meta?.tierType || TierType.Numeric let options: Record = cloneDeep(getComponentMetaData(meta).defaultOptions) || {} if (meta?.tierDefaultOptions?.first) { options = meta.tierDefaultOptions.first() } else if (tiertype === TierType.Numeric) { // as a self-invoking anonymous function to create a new instance each time options = (() => ({ quantity: equals(1) }) as AmountValidatorOptions )() } return pickByPaths(options, meta.tierFields || []) } const filteredItemsLabel = { }{__('Filtered items')}{ } const getAddOfferDescription = (suggestedScope: string | undefined): ReactNode => { if (suggestedScope === 'bogo') { return<> {__('Add a BOGO offer that uses the *').replace('*', '')} {filteredItemsLabel} {__('as the "buy" items')} } else if (suggestedScope === 'item_discounts') { return <> {__('Add a Discount offer that applies discounts to the *').replace('*', '')} {filteredItemsLabel} } else if (suggestedScope === 'cart_discounts') { return <> {__('Add a Discount offer that uses the *').replace('*', '')} {filteredItemsLabel} {__('as a condition for the cart discount')} } return <> {__('Add an offer using the *').replace('*', '')} {filteredItemsLabel} }; export const SelectActionSplitBranch: FC<{ data: SelectActionSplitNodeData } & NodeProps> = ({id}) => { const nodesFinder = getNodesFinder(); const branch = nodesFinder.get(id); /** * NOTE: BECAUSE OF SWAPPING AND RE-RENDERING: BRANCH HERE MIGHT BE UNDEFINED UPON CHANGING THE BRANCH, SO CONSIDER BRANCH POSSIBLY UNDEFINED */ const isInsideBranch = branch?.isInsideAnotherBranch(); const openTestablesPopup = useOpenTestablesPopupWindow() const setIsProUpgradePopupOpen = useSetAtom(ProUpgradePopupWindowIsOpen) /* const openExamplesPopup = useOpenExamplesPopupWindow() */ const addBranchOffer = useNodesStore(store => store.addBranchOffer) const splitContainerRef = useRef(null); const offerButtonRef = useRef(null); const tierButtonRef = useRef(null); const [splitPaths, setSplitPaths] = useState<{ width: number; height: number; centerX: number; centerY: number; leftX: number; leftY: number; rightX: number; rightY: number; } | null>(null); const handleAddOffer = () => { if (offersTestableisOpen ) { return } offersTestableisOpen = true // @ts-ignore openTestablesPopup({ // @ts-ignore context: merge( { data: getPopupStackDimensionCalculators( getOpenPopupsCount()) }, { data: { showTabs: false, extraData: { suggestedScope, }, suggested: { bogo: { type: 'category', id: ComponentCategory.BOGO }, item_discounts: { type: 'component', id: DiscountMeta.id }, cart_discounts: { type: 'component', id: DiscountMeta.id } }[suggestedScope!] } as Partial }, getOffersPopupWindowContext([ // exclude get x if innside another branch (aka in tiers) bc get x is not supported in tiers currently ...(branch?.isInsideAnotherBranch() ? incompatibleOffersFromInsideTiers : []) ]) ), onClose: (data) => { const {status, components} = data if (status === 'success' && components?.length > 0) { // let's js ad the offer to the branch, the grove will handle the necessary logic for converting the branch if necessary, adding the offer to the store, etc. addBranchOffer(components[0], branch!.data.id) close() } else { } offersTestableisOpen = false /*// @ts-ignore const saver = new TestablesSaver(context) // @ts-ignore saver.add(data.components)*/ } }) return // alight so here we'll open the testables popup window set to offers // then we'll add a handler so that we can talk to it after its been closed // there are two possible outcomes after closing: // 1- offers have been seected // 2- no offers have been selected, the user quit the popup // lets just first open it // @ts-ignore /*const context = { id: 'offers-new', scope: 'tree-node', data: { componentType: 'filter', targetType: 'root', // root | testableComposite targetId: null, // string id if testableComposite, index if root (eg: 0 for the first root, 1 for the second root, etc) } } as PopupWindowStateContext*/ /*openTestablesPopup({ // @ts-ignore context, onClose: (data) => { console.log('got a request to close the popup wit: ', data) /!*!// @ts-ignore const saver = new TestablesSaver(context) // @ts-ignore saver.add(data.components)*!/ } })*/ /** * const openNewTestablesPopupWindow = useNewTestablesPopupWindow({ * onClose: () => {} // here we'll handle the offers that were selected * }) * * opennewNewTestablesPopupWindow({ * //here we'll set the data for the popup window (context, etc.) * }) */ }; const handleAddTier = () => { if (tiertestableisOpen) { return } if (tierIsNotImplemented) { setIsProUpgradePopupOpen({ isOpen: true, title: __('Create Bulk and Tiered Offers with Pro'), }) return } //const addBranchOffer = useNodesStore(store => store.addBranchOffer) // @ts-ignore const context = { id: 'tier-new', scope: 'tier-base', data: { supportedComponentTypes: ['filter', 'condition'], componentType: 'filter', targetType: 'testableComposite', // supportsOptionalQuantity: false, supportsMultipleComponents: false, targetId: branch!.data.id, // string id if testableComposite, index if root (eg: 0 for the first root, 1 for the second root, etc.) mainWindow: { title: __('What will the tiers be based on?'), description: '', }, ...getPopupStackDimensionCalculators(getOpenPopupsCount()) } } as PopupWindowStateContext; tiertestableisOpen = true // we need: component type: filter, condition, etc openTestablesPopup({ // @ts-ignore context, onClose: (data) => { const {addTreeNode} = useNodesStore.getState() const {status, components, componentType} = data const meta = getComponentMeta(`${componentType}s`, components?.[0]?.type) // code is repeated from select-offers, should ideally be refactored to remove duplication if (status === 'success' && components?.length > 0) { const baseTestableId = generateIdForType('testable'); addTreeNode( { parentBranchTestableId: baseTestableId, testable: { id: generateIdForType('testable.partial'), options: getDefaultOptionsForFirstTierSubchild(meta!) } as Omit>, 'id'>, branch: { id: generateIdForType('branch'), type: 'select-action', children: [] } }, { parentBranchId: branch!.data.id, newTieredBranchData: { baseTestable: { ...components[0]!, testableType: componentType as 'filter' | 'condition' }, tierType: meta?.tierType || TierType.Numeric, } } ) } else { } setTimeout(() => { tiertestableisOpen = false }, 500) } }) return }; const addIcon = ( ); const tierIcon = ( ); const buttonAnchorHandleStyle: React.CSSProperties = { left: '50%', top: 0, right: 'auto', bottom: 'auto' }; const splitLineColor = request(colors.blue).text(100); const splitStartOffset = 0; const splitMinVertical = 1; const splitDiagonalLength = 72; const splitLabelRadius = 10; const leftSplitY = splitPaths ? Math.max(splitStartOffset + splitMinVertical, splitPaths.leftY - splitDiagonalLength) : 0; const rightSplitY = splitPaths ? Math.max(splitStartOffset + splitMinVertical, splitPaths.rightY - splitDiagonalLength) : 0; const leftControlY = splitPaths ? Math.max(splitStartOffset + splitMinVertical + 4, splitPaths.leftY - splitDiagonalLength / 2) : 0; const rightControlY = splitPaths ? Math.max(splitStartOffset + splitMinVertical + 4, splitPaths.rightY - splitDiagonalLength / 2) : 0; const labelT = 0.72; const leftControlX = splitPaths ? splitPaths.centerX : 0; const rightControlX = splitPaths ? splitPaths.centerX : 0; const leftLabelX = splitPaths ? ((1 - labelT) ** 2) * splitPaths.centerX + 2 * (1 - labelT) * labelT * leftControlX + (labelT ** 2) * splitPaths.leftX : 0; const leftLabelY = splitPaths ? ((1 - labelT) ** 2) * leftSplitY + 2 * (1 - labelT) * labelT * leftControlY + (labelT ** 2) * splitPaths.leftY : 0; const rightLabelX = splitPaths ? ((1 - labelT) ** 2) * splitPaths.centerX + 2 * (1 - labelT) * labelT * rightControlX + (labelT ** 2) * splitPaths.rightX : 0; const rightLabelY = splitPaths ? ((1 - labelT) ** 2) * rightSplitY + 2 * (1 - labelT) * labelT * rightControlY + (labelT ** 2) * splitPaths.rightY : 0; useLayoutEffect(() => { if (!splitContainerRef.current || !offerButtonRef.current || !tierButtonRef.current) { return; } const containerRect = splitContainerRef.current.getBoundingClientRect(); const offerRect = offerButtonRef.current.getBoundingClientRect(); const tierRect = tierButtonRef.current.getBoundingClientRect(); const leftX = offerRect.left - containerRect.left + offerRect.width / 2; const rightX = tierRect.left - containerRect.left + tierRect.width / 2; const leftY = offerRect.top - containerRect.top; const rightY = tierRect.top - containerRect.top; setSplitPaths({ width: containerRect.width, height: containerRect.height, centerX: containerRect.width / 2, centerY: containerRect.height / 2, leftX, leftY, rightX, rightY, }); }, [isInsideBranch]); const suggestedScope = useSuggestedScopeForTestableId(branch?.testableNode.getId() || '') // Vertical layout (when NOT inside another branch) const verticalSplitContent = (
{suggestedScope === 'bogo' && } {/* The inverted Y SVG lines that branch out to the two buttons */} {splitPaths && ( A B )} {/* Two action buttons side by side */}

{getAddOfferDescription(suggestedScope)}

{false && }

{__('For bulk & tiered offers')}

); // Horizontal layout (when inside another branch) const horizontalSplitContent = (
{splitPaths && ( )} {/* Two action buttons stacked vertically */}
); return ( {!isInsideBranch ? ( <> {verticalSplitContent} ) : ( <> {} {horizontalSplitContent} )} ); }