import {CardRenderer} from "./CardRenderers"; import {ComponentAction, componentCategories, ComponentCategory, ComponentRuntimeData} from "./components"; import {CardType} from "./cards"; import {__, CouponsPlus, getRegisteredComponentsMeta} from "../globals"; import {FieldsMap} from "./fields"; import {FieldUpdateAction} from "../Fields"; import {SelectOption} from "./fields/MultiSelectSearch"; import {TierType} from "./tiers"; import {PopupWindowStateContext} from "./atoms"; import {ExportImportTarget} from "./export/Exporters"; import {ReactNode} from "react"; import {sortBy} from "lodash"; // Dynamic import of all meta components from the cards directory using Vite // @ts-ignore const modules = import.meta.glob(['./cards/*.tsx', './cards/conditions/*.tsx', "./cards/stubs/*.tsx"], {eager: true}); // Initialize ComponentsMeta structure export const ComponentsMeta: Record = { filters: [], conditions: [], offers: [] }; // Process all modules and add meta components to appropriate arrays const addComponentsMeta = (meta: ComponentMeta) => { if (meta.type && ComponentsMeta[meta.type]) { ComponentsMeta[meta.type].push(meta); } } Object.values(modules).forEach((module: any) => { // Find meta objects in the module (assuming they end with "Meta") Object.entries(module).forEach(([key, value]) => { if (key.endsWith('Meta') && typeof value === 'object' && value !== null) { addComponentsMeta(value as ComponentMeta); } }); }); /** * Add the external components */ for (const externalComponentType in getRegisteredComponentsMeta()) { const externalComponentsMeta = getRegisteredComponentsMeta()[externalComponentType as ComponentType] || []; externalComponentsMeta.forEach((componentMeta: ComponentMeta) => { addComponentsMeta(componentMeta); }); } export type CategorizedComponents = Partial>; export const getComponentsOrderedByCategory = (type: ComponentType, supported: string[], unsupported: string[], context?: 'tier-base', withImplementation: boolean = true): CategorizedComponents => { const components = (ComponentsMeta[type]) const orderedComponents: CategorizedComponents = {} const otherComponents: ComponentMeta[] = [] components.forEach((componentMeta) => { const isImplemented = hasImplementation(componentMeta) // if we only want implemented components if (withImplementation) { if (!isImplemented) { // skip unimplemented components return; } } else { // if we only want unimplemented components if (hasImplementation(componentMeta)) { // skip implemented components return; } } let {id, category} = componentMeta; let isSupported = true; if (supported.length) { isSupported = supported.includes(id) } else if (unsupported.length) { isSupported = !unsupported.includes(id) } if (!isSupported) { // skip unsupported components return; } if (context?.startsWith('tier-base') && componentMeta.supportsTiers !== true) { return } if (!isImplemented) { category = ComponentCategory.Pro } if (!category || category === ComponentCategory.Other) { // let's ad them here so that 'other' is always at the end otherComponents.push(componentMeta) } else { if (!orderedComponents[category]) { orderedComponents[category] = []; } orderedComponents[category]!.push(componentMeta) } }) if (otherComponents.length > 0) { orderedComponents[ComponentCategory.Other] = otherComponents; } // sort items in each category. done here for clarity and because sorting at once at the beginning of this function changes // th order of the groups to and even though there's better ways to do this i dont want to spend to much time doing this so this'll have to do. for (let category in orderedComponents) { orderedComponents[category] = sortComponents(orderedComponents[category]); } // now sort the categories theyselves const categoriesAndPriorities: [string, number][] = [] for (let category in orderedComponents) { categoriesAndPriorities.push([category, componentCategories[category].priority]) } const orderedCategories: CategorizedComponents = {} ;[...categoriesAndPriorities] .sort(([, priorityA], [, priorityB]) => (priorityA ?? 50) - (priorityB ?? 50)) .map(([name]) => name) .forEach(name => { orderedCategories[name] = orderedComponents[name] }) return orderedCategories } const sortComponents = (componentsMetaElement: ComponentMeta[]): ComponentMeta[] => { // it don't matter if we change the reference here, matter of fact im going to add a flag so that this only happens once // i don't know how to get a reference here though and im on my way now so no internet connection so tha will have to wait // I forgot about this didn't I? it's not really that big a deal anyway return componentsMetaElement.sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50)) }; export type ComponentType = 'filters' | 'conditions' | 'offers'; const getUnavailableComponentData = (type: string): CardType => ({ type, name: __('Unavailable component'), description: __('This component is currently unavailable.'), useCases: [], notes: [], fields: {}, defaultOptions: {}, supports: { recursiveness: false, } }) export const getComponentMetaData = (componentMeta: ComponentMeta | [ComponentType, string]): CardType => { const unresolvedType = Array.isArray(componentMeta) ? componentMeta[1] : componentMeta.id componentMeta = resolveComponentMeta(componentMeta); if (!componentMeta) { return getUnavailableComponentData(unresolvedType) } return CouponsPlus.components[componentMeta.type]?.[componentMeta.id] ?? CouponsPlus.stubs?.[componentMeta.type]?.[componentMeta.id] ?? getUnavailableComponentData(componentMeta.id); } export const getComponentMetaDataField = (componentMeta: ComponentMeta | [ComponentType, string], field: string) => { const resolvedMeta = resolveComponentMeta(componentMeta) const clientField = resolvedMeta?.[field as keyof ComponentMeta]; if (clientField) { return clientField as ComponentMeta[typeof field]; } return getComponentMetaData(resolvedMeta || componentMeta)[field as keyof CardType]; } const resolveComponentMeta = (componentMeta: ComponentMeta | [ComponentType, string]): ComponentMeta | undefined => { if (Array.isArray(componentMeta)) { const [parentType, type] = componentMeta; componentMeta = ComponentsMeta[parentType].find(meta => meta.id === type) as ComponentMeta | undefined } return componentMeta; } export type ComponentMeta = { id: string, type: ComponentType, icon?: CardRenderer['icon'], listTitle?: (preRenderedTitle: ReactNode, name: string) => ReactNode, relatedSearchTerms?: string[], //Title?: ({suggestedScope}: {suggestedScope: string}) => string | ReactNode, description?: ({suggestedScope}: {suggestedScope: string}) => string | ReactNode, supportsCustomTitleForSuggestedScopes?: (suggestedScope: string) => boolean, // for example, products.tsx supports custom titles for bogos and item_discounts. OnSale does not support custom title on item_discounts scope. fieldsTitlePrefix?: false | undefined, // false to disable the prefix, undefined to use the default prefix fieldHooks?: Record< // the id of the field string, Partial<{ [FieldUpdateAction.Add]: (id: string, value: SelectOption) => any; }> >, fieldsMap?: FieldsMap | ((componentRuntimeData: ComponentRuntimeData, scope: PopupWindowStateContext['scope'], suggestedScope?: string) => FieldsMap), fieldsNotes?: string[] | ((componentRuntimeData: ComponentRuntimeData, scope: PopupWindowStateContext['scope']) => string[]), // for extra notes at the bottom that depend on runtime data canShowBottomNavigation?: (componentRuntimeData: ComponentRuntimeData) => boolean prename?: (componentData: ComponentRuntimeData[]) => string, titlePrefix?: ReactNode, action?: (componentData: ComponentRuntimeData[]) => ComponentAction, category?: string, supportsTiers?: boolean, // whether this component can be used inside a tier or not. Default is false tierType?: TierType, tierFields?: string[], // (REQUIRED IF supportsTiers) IMPORTANT: these are the fields that are ONLY supported by testable partials, in dot notation, eg: ['amount', 'product.categories', etc.] IT IS VERY IMPORTANT THAT THESE FIELDS ARE DEFINED OTHERWISE USING THE WHOLE FIELDS WHEN THE BASE TESTABLE CAN BE CONFIGURED SEPARATELY WILL LEAD TO BUGS. tierDefaultOptions?: { first: () => Record, // probbaly here on new tier } configurable?: undefined | false | ((context: string | 'tier-base') => boolean), // if undefined, it means the component is configurable. Default is undefined (configurable) incompatibleWith?: string[], // these components cannot be used together with this one export?: (cardData: any, exportTarget: ExportImportTarget) => any, // used when exporting the rules to a specific format (eg: coupon conditions to woocommerce format) priority?: number, //10 based, default is 50 } export const getComponentMeta = (type: ComponentType, id: string): ComponentMeta | undefined => { return ComponentsMeta[type].find(meta => meta.id === id); } /** * 'type' here refers to the individual component type, eg: UserRole, Categories, etc */ export const getComponentMetaFromType = (type: string): typeof ComponentsMeta[ComponentType][number] | undefined => { for (const category in ComponentsMeta) { const metas = ComponentsMeta[category]; const meta = metas?.find(m => m.id === type); if (meta) { return meta; } } return undefined; } export const hasImplementation = (componentMeta: ComponentMeta | undefined): boolean => { if (!componentMeta) { return false } return CouponsPlus.components[componentMeta.type][componentMeta.id] !== undefined }