import {atom, createStore} from "jotai"; import {cloneDeep} from "lodash"; import {ComponentRuntimeDataWithParentType} from "./components"; import {ComponentDataType} from "./store"; import {TreeDashboardSection} from "./TreeDashboardSections"; import {ReactFlowInstance} from "@xyflow/react"; import mitt from "mitt"; import {PreloadedDynamicValues} from "./export/ComponentExporterImporter"; import {NotInCartNotification, ProductData} from "./cards/BuyXGetY"; import {PopupWindowProps} from "../PopupWindow"; import {SelectOption} from "./fields/MultiSelectSearch"; import {ReactNode} from "react"; import {TestableGroupMode} from "./testableGroupTypes"; import type {ConditionColorPalette} from "./Colors"; export type Prettify = { [K in keyof T]: Prettify; } & {} /** * Atoms here are used for performance critical global state that is only used by specific components. * * Using Zustand for this would make the app re-render the whole tree on every change. */ export const atomsStore = createStore() export const ActivePopupWindowIdsAtom = atom([]) export const HoveredContextIDAtom = atom(null) export const HoveredTestableCompositeIDAtom = atom(null) export const LayoutAnimationInProgressAtom = atom(false) export const HorizontalOffsetAtom = atom(0) export const VerticalOffsetAtom = atom(0) export const DocumentDimensionsAtom = atom({ width: 0, height: 0, }) export const leftSidebarWidthAtom = atom(50 * 4) // this doesn't really change but i want to keep it consistent with the right sidebar which is dynamic export const rightSidebarWidthAtom = atom(0) export const sidebarEdgeOffsetAtom = atom(3 * 4) // again, doesn't change either but its here for consistency export const FloatingHeaderHeightAtom = atom(9 * 4) // again currently doesn't change but its here for consistency and for future use because this might become dynamic so might as well start with an atom export const MobileSidebarOpenAtom = atom(false) export const MobileOptionsPanelOpenAtom = atom(false) export const CurrentDashboardSectionIdAtom = atom(TreeDashboardSection.Offers) export const mainReactFlowInstanceAtom = atom(null) export const treeEmitterAtom = atom(mitt<{ 'renderedNodes:stabilized': ReactFlowInstance }>()) /** * Example: * { * popupWindowId: screenId * } */ export const CurrentScreenIdsAtom = atom>({}) export const viewportDimensionsAtom = atom({ width: 0, height: 0, }) const ModelBaseAtom = atom<'tree' | 'rows' | 'classic'>('tree') export const LastModelAtom = atom<'tree' | 'rows' | 'classic' | null>(null) export const ModelAtom = atom( (get) => get(ModelBaseAtom), (get, set, newMode: 'tree' | 'rows' | 'classic') => { set(ModelBaseAtom, newMode) } ) // Intercept writes: appending a single value, replacing with an array, or handling functional updates export const DynamicTestableDataBaseAtom = atom([]) export type WindowData = { window: { title?: ReactNode, description?: ReactNode, } } export type PopupWindowStateContext = { id: string, // unique id for each popup window, used to identify the popup scope: 'tree-node' | 'tier-base' | 'tier-subchild' | 'tier-branch' | 'custom-filtered-items-map' | 'preconditions' | 'predates' | 'prefilters', data: ContextData & Partial, }; export type PopupWindowState = { isOpen: boolean, context: PopupWindowStateContext | ContextData, }; export type TreeContextData = Prettify<{ componentType: ComponentDataType, supportedComponentTypes?: ComponentDataType[], unsupportedComponentTypes?: ComponentDataType[], supportsOptionalQuantity?: boolean, // this is window-specific, ignoring individual components, default is true supportedComponents?: string[], // eg: Categories.id, Brands.id, etc unsupportedComponents?: string[], // eg: Categories.id, Brands.id, etc supportsMultipleComponents?: boolean, // default is true suggestedScopeExperienceSelector?: boolean, suggested?: { type: 'category' | 'component', // a whole category, 'like' BOGO or a specific component like 'BuyXGetY' id: string, // possibly in the future that multiple ids can be supported } groupType?: TestableGroupMode, conditionColorPalette?: ConditionColorPalette, extraData?: any, showTabs?: boolean, mainWindow?: { title?: string, description?: string, }, onClose?: PopupWindowsState['onClose'], top?: PopupWindowProps['customTop'], width?: PopupWindowProps['customWidth'] } & ( // this is for when adding a new root tree { targetType: 'root', targetId: number, // index of the root } | // this is for adding to (or editing!) an existing group { targetType: 'testableComposite' | 'testablePartial', mode?: 'create' | 'edit', targetId: string, } | // this is for adding testables to the memory, but they are not directly inserted in the tree ({ targetType: 'ghost', } & ({ mode: 'create', } | { mode: 'edit', targetId: string, // an existing testable id because mode is 'edit', })) )>; export type PopupWindowsState = { context: PopupWindowStateContext, onClose: (data: { status: 'success' | 'fail', components: ComponentRuntimeDataWithParentType[], componentType: ComponentDataType, extraData?: ExtraData, }, options?: { deferWindowRemoval?: boolean skipCallback?: boolean }) => void } export type PopupWindowCloseOptions = { deferWindowRemoval?: boolean skipCallback?: boolean } export const TestablesPopupWindowsAtom = atom([/*{ context: { id: 'offers-new', scope: 'tree-node', data: { componentType: 'offer', supportedComponentTypes: ['offer'], // only offers pls 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) } }, onClose: () => { } }*/]) export const TestablesPopupWindowStateAtom = atom>({ isOpen: false, context: { id: "tree-node", data: { targetType: 'root', targetId: 0, }, }, }) export type TestableGroupModePopupWindowContextData = { targetId: string, componentType: Exclude, suggestedScope?: string, } export type TestableGroupModePopupWindowState = PopupWindowState export const getDefaultClosedTestableGroupModePopupWindowState = (): TestableGroupModePopupWindowState => cloneDeep({ isOpen: false, context: { id: 'testable-group-mode', scope: 'tree-node', data: { targetId: '', componentType: 'filter', }, }, }) export const TestableGroupModePopupWindowStateAtom = atom( getDefaultClosedTestableGroupModePopupWindowState() ) export type SelectActionContextDefault = { /** * This is for selecting a brand new action */ action: null, data?: {}, }; export type SelectActionContextOffers = { /** * This is for opening the panel from an already selected action (select-offers) */ action: 'select-offers', // or null, 'select-tiered-branch', 'select-or-branch' // once an action is selected, can you change it data: { canChangeAction?: boolean, offerId?: string, // if initialized, the mode is 'edit' }, }; export type SelectActionContext = { targetBranchId: string } & (SelectActionContextDefault | SelectActionContextOffers) export const getDefaultClosedPopupWindowState = (): PopupWindowState => cloneDeep(({ isOpen: false, //false context: { action: null, //'select-offers', targetBranchId: '' } })) export const getDefaultClosedExamplesPopupWindowState = (): PopupWindowState => cloneDeep(({ isOpen: false, //false context: { id: '' } })) export const SelectActionPopupWindowStateAtom = atom>( getDefaultClosedPopupWindowState() ) export type ExamplesContext = { id: string } export const ExamplesPopupWindowStateAtom = atom>(getDefaultClosedExamplesPopupWindowState()) export const AddNewTreePopupWindowIsOpen = atom(false) export type ProUpgradePopupWindowState = { isOpen: boolean, title?: PopupWindowProps['screens'][number]['title'], icon?: PopupWindowProps['screens'][number]['icon'], content?: PopupWindowProps['screens'][number]['content'], } export const getDefaultClosedProUpgradePopupWindowState = (): ProUpgradePopupWindowState => ({ isOpen: false, }) export const ProUpgradePopupWindowIsOpen = atom(getDefaultClosedProUpgradePopupWindowState()) export type GetYProductDataToSave = Omit; export type BuyXGetYSpecificProductWindowContextData = { index?: number, // if not set, mode will be computed as "new". modes: new | edit data?: ProductData[number], onClose: (status: 'success' | 'fail', data: GetYProductDataToSave) => void }; export type BuyXGetYNotInCartNotificationWindowContextData = { notification: NotInCartNotification, onClose: (status: 'success' | 'fail', data: NotInCartNotification) => void }; export const BuyXGetYSpecificProductWindowStateAtom = atom(null) export const BuyXGetYNotInCartNotificationWindowStateAtom = atom(null) export const CustomPresetSourceAtom = atom('') export const PermissionToLoadPresetsAtom = atom( (window as any).CouponsPlus?.dashboardPreferences?.permissions?.connect || false ) // Select Menu Popup Windows (supports multiple instances) export type SelectMenuPopupWindowContextData = { options: SelectOption[], selectedValues: SelectOption['id'][], // placeholder for future context data } export type SelectMenuPopupWindowState = { context: PopupWindowStateContext, onClose: (data: { status: 'success' | 'fail', values: SelectOption['id'][], }, options?: { deferWindowRemoval?: boolean skipCallback?: boolean }) => void } export type SelectMenuPopupWindowCloseOptions = { deferWindowRemoval?: boolean skipCallback?: boolean } export const SelectMenuPopupWindowsAtom = atom([]) // Overlay for TestableComposite "folder open" effect export type OverlayOpenCompositeState = { id: string, rootId: string, rect: DOMRect, } | null export const OverlayOpenCompositeAtom = atom(null)