import React, {FC, useEffect, useRef, useState} from "react"; import {useAtom} from "jotai"; import classNames from "classnames"; import {POPUP_CLOSE_ANIMATION_MS, PopupWindow} from "../PopupWindow"; import {__} from "../globals"; import {EmulatedButton} from "./EmulatedButton"; import { getDefaultClosedTestableGroupModePopupWindowState, PopupWindowStateContext, TestableGroupModePopupWindowStateAtom, TreeContextData } from "./atoms"; import {useOpenTestablesPopupWindow} from "./useOpenTestablesPopupWindow"; import {TestablesSaver} from "./TestablesSaver"; import {TestableGroupMode, TestableGroupTypeId} from "./testableGroupTypes"; type IllustrationState = 'success' | 'failure' | 'skipped-success' type IllustrationToken = { state: IllustrationState, label?: string, } type IllustrationCase = { id: 'success' | 'failure', label: string, connector: string, input: IllustrationToken[], result: IllustrationToken[], emptyResultLabel?: string, helperText?: string, } type GroupModeOption = { id: TestableGroupMode, label: string, badge?: string, description: string, example: string, cases: IllustrationCase[], } const GroupModeIcon = () => ( ) const groupModeOptions: GroupModeOption[] = [ { id: TestableGroupTypeId.AND, label: __('AND'), description: __('All product groups must match. All matcing products will be passed to the next node (usually offers)'), example: __('Good for product combos, bundles and pairs.'), cases: [ { id: 'success', label: __('Pass'), connector: '+', input: [ {label: 'A', state: 'success'}, {label: 'B', state: 'success'}, ], result: [ {label: 'A', state: 'success'}, {label: 'B', state: 'success'}, ], }, { id: 'failure', label: __('Fail'), connector: '+', input: [ {label: 'A', state: 'success'}, {label: 'B', state: 'failure'}, ], result: [], emptyResultLabel: __('Empty group'), helperText: __('Any failing group clears the result.'), }, ], }, /*{ id: TestableGroupTypeId.OR, label: __('AND/OR'), badge: __('Inclusive OR'), description: __('Will select any matching product groups.'), example: __('Useful for flexible product combinations where multiple matches are allowed across different product types, like any of: Products a, b, c, and/or Categories a, b.'), cases: [ { id: 'success', label: __('Pass'), connector: __('and/or'), input: [ {label: 'A', state: 'success'}, {label: 'B', state: 'failure'}, {label: 'C', state: 'success'}, ], result: [ {label: 'A', state: 'success'}, {label: 'C', state: 'success'}, ], helperText: __('All passing groups are kept.'), }, { id: 'failure', label: __('Fail'), connector: __('and/or'), input: [ {label: 'A', state: 'failure'}, {label: 'B', state: 'failure'}, ], result: [], emptyResultLabel: __('Empty group'), }, ], }, { id: TestableGroupTypeId.OR_EXCLUSIVE, label: __('OR'), badge: __('Exclusive OR'), description: __('Will select only one matching product group. If multiple groups match, only one is selected.'), example: __('Useful for single choice product combinations, like 1 of: Product a, b, c OR 1 of Categories a or b.'), cases: [ { id: 'success', label: __('Pass'), connector: __('or'), input: [ {label: 'A', state: 'failure'}, {label: 'B', state: 'success'}, {label: 'C', state: 'skipped-success'}, ], result: [ {label: 'B', state: 'success'}, ], helperText: __('Later passing groups are ignored once one group wins.'), }, { id: 'failure', label: __('Fail'), connector: __('or'), input: [ {label: 'A', state: 'failure'}, {label: 'B', state: 'failure'}, ], result: [], emptyResultLabel: __('Empty group'), }, ], },*/ ] type IllustrationModeSizeConfig = { chipClassName: string, chipInnerBorderRadiusClassName: string, chipLabelClassName: string, iconClassName: string, connectorClassName: string, connectorStyle?: React.CSSProperties, resultLabelClassName: string, resultIconClassName: string, sequenceGapClassName: string, } const illustrationModeSizes: Record = { [TestableGroupTypeId.AND]: { chipClassName: 'h-12 min-w-[58px] rounded-[18px] px-2.5 py-2', chipInnerBorderRadiusClassName: 'rounded-[15px]', chipLabelClassName: 'left-2 top-[6px] text-[9px] tracking-[0.18em]', iconClassName: 'w-6 h-6', connectorClassName: 'px-[2px] text-2x leading-none tracking-[0.02em]', resultLabelClassName: 'text-[10px] leading-none tracking-[0.18em]', resultIconClassName: 'w-4 h-4', sequenceGapClassName: 'gap-1.5', }, [TestableGroupTypeId.OR]: { chipClassName: 'h-9 min-w-10 rounded-[15px] px-2 py-1', chipInnerBorderRadiusClassName: 'rounded-[12px]', chipLabelClassName: 'left-[6px] top-[4px] text-[7px] tracking-[0.12em]', iconClassName: 'w-4 h-4', connectorClassName: 'px-0 tracking-[0.08em]', connectorStyle: { fontSize: 8, lineHeight: '8px', }, resultLabelClassName: 'text-[8px] leading-none tracking-[0.14em]', resultIconClassName: 'w-3 h-3', sequenceGapClassName: 'gap-1', }, [TestableGroupTypeId.OR_EXCLUSIVE]: { chipClassName: 'h-11 min-w-[52px] rounded-[17px] px-2.5 py-1.5', chipInnerBorderRadiusClassName: 'rounded-[14px]', chipLabelClassName: 'left-[7px] top-[5px] text-[8px] tracking-[0.16em]', iconClassName: 'w-5 h-5', connectorClassName: 'px-[1px] tracking-[0.12em]', connectorStyle: { fontSize: 10, lineHeight: '10px', }, resultLabelClassName: 'text-[9px] leading-none tracking-[0.16em]', resultIconClassName: 'w-[14px] h-[14px]', sequenceGapClassName: 'gap-1', }, } const PassGlyph: FC<{className?: string}> = ({className}) => { return } const FailGlyph: FC<{className?: string}> = ({className}) => { return } const StatusGlyph: FC<{state: IllustrationState, className: string}> = ({state, className}) => { const isSuccess = state === 'success' || state === 'skipped-success' return isSuccess ? : } const IllustrationChip: FC<{token: IllustrationToken, mode: TestableGroupMode}> = ({token, mode}) => { const isSuccess = token.state === 'success' const isFailure = token.state === 'failure' const isSkipped = token.state === 'skipped-success' const size = illustrationModeSizes[mode] return
{token.label && {token.label} } {isSkipped && <> }
} const IllustrationConnector: FC<{children: React.ReactNode, mode: TestableGroupMode}> = ({children, mode}) => { const size = illustrationModeSizes[mode] return
{children}
} const EmptyResultChip: FC<{label: string}> = ({label}) => { return
{label}
} const ResultMarker: FC<{mode: TestableGroupMode}> = ({mode}) => { const size = illustrationModeSizes[mode] return
{__('Result')}
} const IllustrationCaseCard: FC<{modeCase: IllustrationCase, mode: TestableGroupMode}> = ({modeCase, mode}) => { const size = illustrationModeSizes[mode] return
{modeCase.label}
{modeCase.input.map((token, index) => ( {index > 0 && {modeCase.connector}} ))} {modeCase.result.length > 0 ? modeCase.result.map((token, index) => ( )) : }
{modeCase.helperText &&

{modeCase.helperText}

}
} export const TestableGroupModePopupWindow: FC = () => { const [popupWindowState, setPopupWindowState] = useAtom(TestableGroupModePopupWindowStateAtom) const openTestablePopupWindow = useOpenTestablesPopupWindow() const [isOpen, setIsOpen] = useState(false) const hasClosedRef = useRef(false) const closeStartTimeoutRef = useRef(null) const closeRemovalTimeoutRef = useRef(null) const isConditionsFlow = popupWindowState.context.data.componentType === 'condition' const clearPendingCloseTimeouts = () => { if (closeStartTimeoutRef.current !== null) { window.clearTimeout(closeStartTimeoutRef.current) closeStartTimeoutRef.current = null } if (closeRemovalTimeoutRef.current !== null) { window.clearTimeout(closeRemovalTimeoutRef.current) closeRemovalTimeoutRef.current = null } } const resetPopupState = () => { clearPendingCloseTimeouts() hasClosedRef.current = false setIsOpen(false) setPopupWindowState(getDefaultClosedTestableGroupModePopupWindowState()) } useEffect(() => { if (!popupWindowState.isOpen) { return } clearPendingCloseTimeouts() hasClosedRef.current = false setIsOpen(true) }, [popupWindowState.isOpen]) useEffect(() => { return () => { clearPendingCloseTimeouts() } }, []) const closeWithAnimation = (delayToPreserveStackRegistration: boolean = false) => { if (hasClosedRef.current) { return } hasClosedRef.current = true const startClosing = () => { setIsOpen(false) closeRemovalTimeoutRef.current = window.setTimeout(() => { resetPopupState() }, POPUP_CLOSE_ANIMATION_MS) } if (delayToPreserveStackRegistration) { closeStartTimeoutRef.current = window.setTimeout(startClosing, 0) return } startClosing() } const openTestablesPopupForMode = (groupType: TestableGroupMode) => { const context: PopupWindowStateContext = { id: `context-right-button-${popupWindowState.context.data.targetId}`, scope: 'tree-node', data: { componentType: popupWindowState.context.data.componentType, targetType: 'testableComposite', targetId: popupWindowState.context.data.targetId, groupType, extraData: { suggestedScope: popupWindowState.context.data.suggestedScope, } } } openTestablePopupWindow({ context, onClose: (data) => { if (data.status !== 'success' || !data.components) { return } const saver = new TestablesSaver(context) saver.add(data.components) closeWithAnimation(true) } }) } if (!popupWindowState.isOpen && !isOpen) { return null } return , description: isConditionsFlow ? __('Choose how the new set of conditions should combine with the current group. You\'ll pick the conditions in the next popup.') : __('Choose how the new set of products should combine with the current group. You\'ll pick the products in the next popup.'), content: ({height}) => { return
{groupModeOptions.map((option) => ( openTestablesPopupForMode(option.id)} >
{option.label} {option.badge && {option.badge} }

{option.description}

{option.example}
{option.cases.map((modeCase) => ( ))}
))}

{isConditionsFlow ? __('Once you choose a mode, the next popup will open so you can select the new set of conditions.') : __('Once you choose a mode, the next popup will open so you can select the new set of products.')}

}, showBottomNavigation: false, } ]} screenId={1} defaultScreenId={1} setCurrentScreenId={() => {}} onClose={() => resetPopupState()} /> }