import axios, { Canceler } from "axios"; import { orderBy } from "lodash"; import * as React from "react"; import { LoadingPlaceholder, Select } from "@dndbeyond/character-components/es"; import { ApiAdapterPromise, ApiAdapterRequestConfig, ApiAdapterUtils, ApiResponse, Constants, ContainerManager, DefinitionPool, DefinitionPoolUtils, DefinitionUtils, EntitledEntity, HelperUtils, HtmlSelectOption, HtmlSelectOptionGroup, InfusionChoice, InfusionChoiceUtils, InfusionDefinitionContract, InfusionUtils, InventoryManager, Item, ItemUtils, KnownInfusionUtils, Modifier, RuleData, RuleDataUtils, TypeValueLookup, } from "@dndbeyond/character-rules-engine/es"; import { HtmlContent } from "~/components/HtmlContent"; import DataLoadingStatusEnum from "../../constants/DataLoadingStatusEnum"; import { AppLoggerUtils, TypeScriptUtils } from "../../utils"; type OnInfusionChangeFunc = ( choiceKey: string, infusionId: string | null, oldInfusionId: string | null, accept: () => void, reject: () => void ) => void; interface Props { infusionChoices: Array; contextLevel: number | null; globalModifiers: Array; typeValueLookup: TypeValueLookup; ruleData: RuleData; definitionPool: DefinitionPool; knownInfusionLookup: Record; knownReplicatedItems: Array; onInfusionChoiceItemChangePromise?: ( infusionChoiceKey: string, infusionId: string, itemDefinitionKey: string, itemName: string, accept: () => void, reject: () => void ) => void; onInfusionChoiceItemDestroyPromise?: ( infusionChoiceKey: string, accept: () => void, reject: () => void ) => void; onInfusionChoiceChangePromise?: ( infusionChoiceKey: string, infusionId: string, accept: () => void, reject: () => void ) => void; onInfusionChoiceDestroyPromise?: ( infusionChoiceKey: string, accept: () => void, reject: () => void ) => void; onInfusionChoiceCreatePromise?: ( infusionChoiceKey: string, infusionId: string, accept: () => void, reject: () => void ) => void; loadAvailableInfusions: ( additionalConfig?: Partial ) => ApiAdapterPromise< ApiResponse> >; onDefinitionsLoaded?: ( definitions: Array, accessTypes: Record ) => void; inventoryManager: InventoryManager; } interface State { equipment: Array; loadingStatus: DataLoadingStatusEnum; } class InfusionChoiceManager extends React.PureComponent { static defaultProps = { loadAvailableInfusions: null, }; loadItemsCanceler: null | Canceler = null; loadInfusionsCanceler: null | Canceler = null; constructor(props: Props) { super(props); this.state = { equipment: [], loadingStatus: DataLoadingStatusEnum.NOT_LOADED, }; } componentDidMount() { const { loadingStatus } = this.state; const { infusionChoices, inventoryManager, loadAvailableInfusions, globalModifiers, typeValueLookup, ruleData, onDefinitionsLoaded, } = this.props; if ( infusionChoices.length && loadingStatus === DataLoadingStatusEnum.NOT_LOADED ) { this.setState({ loadingStatus: DataLoadingStatusEnum.LOADING, }); // TODO fix typings "any" once axios fixes how all can have multiple return types axios .all([ inventoryManager.getInventoryShoppe({ onSuccess: (shoppeContainer: ContainerManager) => { this.setState({ equipment: shoppeContainer .getInventoryItems() .items.map((item) => item.getItem()), loadingStatus: DataLoadingStatusEnum.LOADED, }); }, additionalApiConfig: { cancelToken: new axios.CancelToken((c) => { this.loadItemsCanceler = c; }), }, }), loadAvailableInfusions({ cancelToken: new axios.CancelToken((c) => { this.loadInfusionsCanceler = c; }), }), ]) .then(([equipmentResponse, infusionResponse]) => { //do nothing with equipmentResponse since we moved converted to the inventoryManager which sets state on onsuccess let infusionData = ApiAdapterUtils.getResponseData< EntitledEntity >(infusionResponse); if ( infusionData && infusionData.definitionData.length > 0 && onDefinitionsLoaded ) { onDefinitionsLoaded( infusionData.definitionData, infusionData.accessTypes ); } this.loadItemsCanceler = null; this.loadInfusionsCanceler = null; }) .catch(AppLoggerUtils.handleAdhocApiError); } } componentWillUnmount(): void { if (this.loadItemsCanceler !== null) { this.loadItemsCanceler(); } if (this.loadInfusionsCanceler !== null) { this.loadInfusionsCanceler(); } } getItemKeyParts = (itemKey: string): Array => { return itemKey.split("|"); }; getItemKey = (id: number, name: string): string => { return [ DefinitionUtils.hack__generateDefinitionKey( Constants.FUTURE_ITEM_DEFINITION_TYPE, id ), name, ].join("|"); }; handleChoiceItemChangePromise = ( infusionChoice: InfusionChoice, itemKey: string | null, oldItemKey: string | null, accept: () => void, reject: () => void ): void => { const { onInfusionChoiceItemChangePromise, onInfusionChoiceItemDestroyPromise, } = this.props; const choiceKey = InfusionChoiceUtils.getKey(infusionChoice); if (choiceKey === null) { reject(); return; } if (itemKey) { if (onInfusionChoiceItemChangePromise) { const knownInfusion = InfusionChoiceUtils.getKnownInfusion(infusionChoice); if (knownInfusion === null) { reject(); return; } const simulatedInfusion = KnownInfusionUtils.getSimulatedInfusion(knownInfusion); if (simulatedInfusion === null) { reject(); return; } const infusionId = InfusionUtils.getId(simulatedInfusion); if (infusionId === null) { reject(); return; } const itemKeyParts = this.getItemKeyParts(itemKey); onInfusionChoiceItemChangePromise( choiceKey, infusionId, itemKeyParts[0], itemKeyParts[1], accept, reject ); } } else { if (onInfusionChoiceItemDestroyPromise) { onInfusionChoiceItemDestroyPromise(choiceKey, accept, reject); } } }; handleChoiceChangePromise = ( choiceKey: string, infusionId: string | null, oldInfusionId: string | null, accept: () => void, reject: () => void ): void => { const { onInfusionChoiceChangePromise, onInfusionChoiceDestroyPromise } = this.props; if (infusionId) { if (onInfusionChoiceChangePromise) { onInfusionChoiceChangePromise(choiceKey, infusionId, accept, reject); } } else { if (onInfusionChoiceDestroyPromise) { onInfusionChoiceDestroyPromise(choiceKey, accept, reject); } } }; handleChoiceCreatePromise = ( choiceKey: string, infusionId: string, oldInfusionId: string | null, accept: () => void, reject: () => void ) => { const { onInfusionChoiceCreatePromise } = this.props; if (onInfusionChoiceCreatePromise) { onInfusionChoiceCreatePromise(choiceKey, infusionId, accept, reject); } }; renderInfusionReplicateItemChoice = ( infusionChoice: InfusionChoice ): React.ReactNode => { const { equipment, loadingStatus } = this.state; const { contextLevel, knownReplicatedItems } = this.props; let knownInfusion = InfusionChoiceUtils.getKnownInfusion(infusionChoice); if (knownInfusion === null) { return null; } let simulatedInfusion = KnownInfusionUtils.getSimulatedInfusion(knownInfusion); if ( simulatedInfusion === null || InfusionUtils.getType(simulatedInfusion) !== Constants.InfusionTypeEnum.REPLICATE ) { return null; } let contentNode: React.ReactNode; const classNames: Array = [ "ct-infusion-choice-manager__choice", "ct-infusion-choice-manager__choice--child", ]; if (loadingStatus !== DataLoadingStatusEnum.LOADED) { contentNode = ; } else { let groups: Array = []; let infusableItems: Array = equipment.filter((item) => { const levelInfusionGranted = ItemUtils.getLevelInfusionGranted(item); const itemDefinitionKey = ItemUtils.getDefinitionKey(item); let chosenItemDefinitionKey: string | null = null; if (knownInfusion !== null) { chosenItemDefinitionKey = KnownInfusionUtils.getItemDefinitionKey(knownInfusion); } const hasKnownReplicatedItem: boolean = knownReplicatedItems.includes(itemDefinitionKey); if ( hasKnownReplicatedItem && chosenItemDefinitionKey === itemDefinitionKey ) { return true; } return ( !hasKnownReplicatedItem && levelInfusionGranted !== null && contextLevel !== null && contextLevel >= levelInfusionGranted ); }); let orderedInfusableItems = orderBy(infusableItems, (item) => ItemUtils.getName(item) ); orderedInfusableItems.forEach((item) => { const levelInfusionGranted = ItemUtils.getLevelInfusionGranted(item); if (levelInfusionGranted === null) { return; } if (!groups.hasOwnProperty(levelInfusionGranted)) { groups[levelInfusionGranted] = { optGroupLabel: `Level ${levelInfusionGranted}`, options: [], }; } const definitionName = ItemUtils.getDefinitionName(item); groups[levelInfusionGranted].options.push({ label: definitionName, value: this.getItemKey( ItemUtils.getId(item), definitionName === null ? "" : definitionName ), }); }); groups.forEach((group) => { group.options = orderBy( group.options, (option: HtmlSelectOption) => option.label ); }); let selectedValue: string | null = null; let knownItemId = KnownInfusionUtils.getItemId(knownInfusion); let knownItemName = KnownInfusionUtils.getItemName(knownInfusion); if (knownItemId && knownItemName) { selectedValue = this.getItemKey(knownItemId, knownItemName); } if (selectedValue === null) { classNames.push("ct-infusion-choice-manager__choice--todo"); } contentNode = ( {this.renderInfusionReplicateItemChoice(infusionChoice)} {warningNode} {descriptionNode} ); //` }; render() { const { loadingStatus } = this.state; const { infusionChoices } = this.props; if (!infusionChoices.length) { return null; } let contentNode: React.ReactNode; if (loadingStatus !== DataLoadingStatusEnum.LOADED) { contentNode = ; } else { contentNode = ( {infusionChoices.map((infusionChoice) => this.renderInfusionChoice(infusionChoice) )} ); } return (
Infusion Choices
{contentNode}
); } } export default InfusionChoiceManager;