import clsx from "clsx"; import { FC, HTMLAttributes, useContext, useEffect, useState } from "react"; import { useSelector } from "react-redux"; import { ApiAdapterUtils, apiCreatorSelectors, CharClass, Choice, ChoiceUtils, ClassDefinitionContract, ClassFeature, ClassFeatureUtils, ClassUtils, Constants, ContainerManager, Feat, FeatUtils, } from "@dndbeyond/character-rules-engine"; import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading"; import { InventoryManagerContext } from "~/tools/js/Shared/managers/InventoryManagerContext"; import { appEnvSelectors } from "~/tools/js/Shared/selectors"; import { AppLoggerUtils } from "~/tools/js/Shared/utils"; import LoadingPlaceholder from "~/tools/js/smartComponents/LoadingPlaceholder"; import { DataLoadingStatusEnum } from "~/tools/js/smartComponents/componentConstants"; import { FeatureChoice } from "./FeatureChoice"; import styles from "./styles.module.css"; export interface FeatureChoicesProps extends HTMLAttributes { choices: Array; showHeading?: boolean; onChoiceChange: ( choiceId: string, type: number, value: any, parentChoiceId: string | null ) => void; collapseDescription?: boolean; charClass?: CharClass; classFeature?: ClassFeature; shouldFetch?: boolean; // Optional prop to control fetching behavior itemShoppe?: ContainerManager | null; // Optional prop for cases where the itemShoppe is fetched from the parent component (only used if the Feature Choices component is rendered multiple times in the same parent - e.g. in the Item Plans Manage Pane) } export const FeatureChoices: FC = ({ className, choices, showHeading, onChoiceChange, collapseDescription, charClass, classFeature, shouldFetch = true, // Default to true if not provided itemShoppe = null, ...props }) => { const isReadonly = useSelector(appEnvSelectors.getIsReadonly); const loadAvailableFeats = useSelector( apiCreatorSelectors.makeLoadAvailableFeats ); const loadAvailableSubclasses = useSelector( apiCreatorSelectors.makeLoadAvailableSubclasses ); const { inventoryManager } = useContext(InventoryManagerContext); const [subclassData, setSubclassData] = useState< Array >([]); const [featsData, setFeatsData] = useState>([]); const [localItemShoppe, setLocalItemShoppe] = useState(itemShoppe); const [featLoadingStatus, setFeatLoadingStatus] = useState(DataLoadingStatusEnum.NOT_INITIALIZED); const [subclassLoadingStatus, setSubclassLoadingStatus] = useState(DataLoadingStatusEnum.NOT_INITIALIZED); const [itemsLoadingStatus, setItemsLoadingStatus] = useState(DataLoadingStatusEnum.NOT_INITIALIZED); const hasFeatChoice = choices.some( (choice) => ChoiceUtils.getType(choice) === Constants.BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION ); const hasSubclassChoice = choices.some( (choice) => ChoiceUtils.getType(choice) === Constants.BuilderChoiceTypeEnum.SUB_CLASS_OPTION ); const hasItemMappings = classFeature ? ClassFeatureUtils.getHasItemMappings(classFeature) : false; useEffect(() => { const abortController = new AbortController(); if ( hasFeatChoice && shouldFetch && featLoadingStatus !== DataLoadingStatusEnum.LOADED ) { setFeatLoadingStatus(DataLoadingStatusEnum.LOADING); loadAvailableFeats({ signal: abortController.signal, }) .then((response) => { let featsData: Array = []; const data = ApiAdapterUtils.getResponseData(response); if (data !== null) { featsData = data.map((definition) => FeatUtils.simulateFeat(definition) ); } setFeatsData(featsData); setFeatLoadingStatus(DataLoadingStatusEnum.LOADED); }) .catch((e) => { if (abortController.signal.aborted) return; AppLoggerUtils.handleAdhocApiError(e); }); } if ( charClass && hasSubclassChoice && shouldFetch && subclassLoadingStatus !== DataLoadingStatusEnum.LOADED ) { setSubclassLoadingStatus(DataLoadingStatusEnum.LOADING); loadAvailableSubclasses(ClassUtils.getId(charClass), { signal: abortController.signal, }) .then((response) => { let subclassData: Array = []; const data = ApiAdapterUtils.getResponseData(response); if (data !== null) { subclassData = data; } setSubclassData(subclassData); setSubclassLoadingStatus(DataLoadingStatusEnum.LOADED); }) .catch((e) => { if (abortController.signal.aborted) return; AppLoggerUtils.handleAdhocApiError(e); }); } if ( localItemShoppe === null && hasItemMappings && shouldFetch && itemsLoadingStatus !== DataLoadingStatusEnum.LOADED ) { if (inventoryManager) { setItemsLoadingStatus(DataLoadingStatusEnum.LOADING); inventoryManager .getInventoryShoppe({ onSuccess: (shoppeContainer: ContainerManager) => { setLocalItemShoppe(shoppeContainer); setItemsLoadingStatus(DataLoadingStatusEnum.LOADED); }, additionalApiConfig: { signal: abortController.signal, }, }) .catch((e) => { if (abortController.signal.aborted) return; AppLoggerUtils.handleAdhocApiError(e); }); } else { setItemsLoadingStatus(DataLoadingStatusEnum.LOADED); } } return () => { abortController.abort(); }; }, [ hasFeatChoice, hasSubclassChoice, hasItemMappings, subclassLoadingStatus, featLoadingStatus, itemsLoadingStatus, charClass, loadAvailableFeats, loadAvailableSubclasses, shouldFetch, inventoryManager, ]); const isDataLoaded = (): boolean => { if (hasSubclassChoice) { if (subclassLoadingStatus !== DataLoadingStatusEnum.LOADED) { return false; } } if (hasFeatChoice) { if (featLoadingStatus !== DataLoadingStatusEnum.LOADED) { return false; } } if (itemShoppe !== null) { return true; } else if (hasItemMappings) { if (itemsLoadingStatus !== DataLoadingStatusEnum.LOADED) { return false; } } return true; }; return !isReadonly && choices.length > 0 ? (
<> {showHeading && ( {`Option${ choices.length > 1 ? "s" : "" }`} )} {isDataLoaded() ? ( choices.map((choice) => ( )) ) : ( )}
) : null; };