import Alert from "@mui/material/Alert"; import Button from "@mui/material/Button"; import Dialog from "@mui/material/Dialog"; import DialogActions from "@mui/material/DialogActions"; import DialogContent from "@mui/material/DialogContent"; import DialogContentText from "@mui/material/DialogContentText"; import DialogTitle from "@mui/material/DialogTitle"; import Stack from "@mui/material/Stack"; import Tab from "@mui/material/Tab"; import Tabs from "@mui/material/Tabs"; import Typography from "@mui/material/Typography"; import { visuallyHidden } from "@mui/utils"; import { useContext, ReactNode, MouseEvent, PureComponent } from "react"; import React from "react"; import { connect } from "react-redux"; import { DarkBuilderSvg, FeatureFlagContext, } from "@dndbeyond/character-components/es"; import { CharacterCurrencyContract, CharacterNotes, CharacterTheme, Constants, ContainerManager, Creature, CreatureUtils, InfusionChoice, InfusionChoiceUtils, Item, ItemUtils, SnippetData, RuleData, rulesEngineSelectors, characterSelectors, BaseCharClass, ContainerUtils, InventoryManager, FormatUtils, CampaignUtils, serviceDataSelectors, PartyInfo, CoinManager, } from "@dndbeyond/character-rules-engine/es"; import { ItemName } from "~/components/ItemName"; import { Link } from "~/components/Link"; import { NumberDisplay } from "~/components/NumberDisplay"; import { TabFilter } from "~/components/TabFilter"; import { useSidebar } from "~/contexts/Sidebar"; import { PaneInfo } from "~/contexts/Sidebar/Sidebar"; import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types"; import CurrencyButton from "../../../CharacterSheet/components/CurrencyButton"; import { ItemSlotManager } from "../../../Shared/components/ItemSlotManager"; import { ThemeButton } from "../../../Shared/components/common/Button"; import { CoinManagerContext } from "../../../Shared/managers/CoinManagerContext"; import { InventoryManagerContext } from "../../../Shared/managers/InventoryManagerContext"; import { appEnvSelectors } from "../../../Shared/selectors"; import { PaneIdentifierUtils } from "../../../Shared/utils"; import ContentGroup from "../../components/ContentGroup"; import EquipmentOverview from "../../components/EquipmentOverview"; import Infusions from "../../components/Infusions"; import InventoryFilter from "../../components/InventoryFilter"; import InventoryTableHeader from "../../components/InventoryTableHeader"; import OtherPossessions from "../../components/OtherPossessions"; import { sheetAppSelectors } from "../../selectors"; import { SheetAppState } from "../../typings"; import Attunement from "../Attunement"; import Inventory from "../Inventory"; interface Props { showNotes: boolean; builderUrl: string; inventory: Array; partyInventory: Array; creatures: Array; currencies: CharacterCurrencyContract | null; notes: CharacterNotes; infusionChoices: Array; weight: number; weightSpeedType: Constants.WeightSpeedTypeEnum; ruleData: RuleData; snippetData: SnippetData; isReadonly: boolean; isMobile: boolean; theme: CharacterTheme; proficiencyBonus: number; classes: Array; inventoryManager: InventoryManager; campaignInfo: PartyInfo | null; coinManager: CoinManager; paneHistoryStart: PaneInfo["paneHistoryStart"]; } interface StateFilterData { filteredInventory: Array; filteredPartyInventory: Array; showAdvancedFilters: boolean; isFiltering: boolean; } interface State { infusableChoices: Array; filterData: StateFilterData; filteredEquippedInventory: Array; shouldShowPartyInventory: number; shouldShowDeleteOnlyInfo: boolean; } class Equipment extends PureComponent { static defaultProps = { showNotes: true, }; constructor(props: Props) { super(props); this.state = { infusableChoices: props.infusionChoices.filter((infusionChoice) => InfusionChoiceUtils.canInfuse(infusionChoice) ), filterData: { filteredInventory: [], filteredPartyInventory: [], showAdvancedFilters: false, isFiltering: false, }, filteredEquippedInventory: [], shouldShowPartyInventory: 0, // mui used 0,1,2 for tabs so 0 is off 1 is on... shouldShowDeleteOnlyInfo: false, }; } componentDidUpdate(prevProps: Props, prevState: State) { if (prevProps.infusionChoices !== this.props.infusionChoices) { this.setState({ infusableChoices: this.props.infusionChoices.filter((infusionChoice) => InfusionChoiceUtils.canInfuse(infusionChoice) ), }); } } handleCurrencyClick = ( evt: React.MouseEvent | React.KeyboardEvent, containerDefinitionKey: string ): void => { const { paneHistoryStart } = this.props; evt.stopPropagation(); evt.nativeEvent.stopImmediatePropagation(); paneHistoryStart( PaneComponentEnum.CURRENCY, PaneIdentifierUtils.generateCurrencyContext(containerDefinitionKey) ); }; handleWeightClick = (): void => { const { paneHistoryStart } = this.props; paneHistoryStart(PaneComponentEnum.ENCUMBRANCE); }; handleCampaignClick = (): void => { const { paneHistoryStart } = this.props; paneHistoryStart(PaneComponentEnum.CAMPAIGN); }; handlePossessionsManage = (): void => { const { paneHistoryStart, isReadonly } = this.props; if (!isReadonly) { paneHistoryStart( PaneComponentEnum.NOTE_MANAGE, PaneIdentifierUtils.generateNote( Constants.NoteKeyEnum.PERSONAL_POSSESSIONS ) ); } }; handleManageClick = (): void => { const { paneHistoryStart } = this.props; paneHistoryStart(PaneComponentEnum.EQUIPMENT_MANAGE); }; handleFilterUpdate = (filterData: StateFilterData): void => { this.setState({ filterData, filteredEquippedInventory: [ ...filterData.filteredInventory.filter(ItemUtils.isEquipped), ...filterData.filteredPartyInventory.filter(ItemUtils.isEquipped), ], }); }; handleInfusionChoiceShow = (infusionChoice: InfusionChoice): void => { const { paneHistoryStart } = this.props; const choiceKey = InfusionChoiceUtils.getKey(infusionChoice); if (choiceKey !== null) { paneHistoryStart( PaneComponentEnum.INFUSION_CHOICE, PaneIdentifierUtils.generateInfusionChoice(choiceKey) ); } }; handleItemShow = (item: Item): void => { const { paneHistoryStart } = this.props; paneHistoryStart( PaneComponentEnum.ITEM_DETAIL, PaneIdentifierUtils.generateItem(ItemUtils.getMappingId(item)) ); }; handleContainerShow = (container: ContainerManager): void => { const { paneHistoryStart } = this.props; paneHistoryStart( PaneComponentEnum.CONTAINER, PaneIdentifierUtils.generateContainer(container.getDefinitionKey()) ); }; handleCreatureShow = (creature: Creature): void => { const { paneHistoryStart } = this.props; paneHistoryStart( PaneComponentEnum.CREATURE, PaneIdentifierUtils.generateCreature(CreatureUtils.getMappingId(creature)) ); }; shouldRenderInfusions = (): boolean => { const { infusionChoices } = this.props; return infusionChoices.length > 0; }; renderContainer = ( container: ContainerManager, inventory: Array ): ReactNode => { const { shouldShowPartyInventory } = this.state; const { isReadonly, showNotes, theme, coinManager } = this.props; const containerMappingId = container.getMappingId(); const containerName = container.getName(); const weightInfo = container.getWeightInfo(); const containerInventory = container.getInventoryItems({ filteredInventory: inventory, }).items; const containerItem = container.getContainerItem(); const quantityNode = ( {`(${containerInventory.length})`} ); const nameNode: ReactNode = containerItem ? ( <> {quantityNode} ) : ( <> {containerName} {quantityNode} ); const headerNode: ReactNode = (
{ evt.nativeEvent.stopImmediatePropagation(); this.handleContainerShow(container); }} role="button" data-testid={`${containerItem?.getName() || containerName}-section` .toLowerCase() .replace(/\s/g, "-")} >
{containerItem && ( { if (uses === 0) { containerItem.handleUnequip(); } if (uses === 1) { containerItem.handleEquip(); } }} theme={theme} useTooltip={false} /> )}
{nameNode}
{weightInfo.capacity > 0 && ( ({Math.ceil(weightInfo.total)}/ {FormatUtils.renderWeight(weightInfo.capacity)}) )}
{coinManager.canUseCointainers() ? (
this.handleCurrencyClick( evt, ContainerUtils.getDefinitionKey(container.container) ) } />
) : ( containerItem && container.isEquipped() && !containerItem.isEquippedToCurrentCharacter() && (
Equipped by {containerItem.getEquippedCharacterName()}
) )}
); return ( ); }; renderCharacterContainers = (): ReactNode => { const { filterData } = this.state; const { inventoryManager } = this.props; return inventoryManager .getCharacterContainers() .map((container) => this.renderContainer(container, filterData.filteredInventory) ); }; renderPartyContainers = (): ReactNode => { const { filterData } = this.state; const { inventoryManager } = this.props; return inventoryManager .getPartyContainers() .map((container) => this.renderContainer(container, filterData.filteredPartyInventory) ); }; renderAttunement = (): ReactNode => { const { isMobile, theme } = this.props; return ( ); }; renderInfusions = (): ReactNode => { const { infusableChoices } = this.state; const { builderUrl, infusionChoices, isReadonly, snippetData, inventory, partyInventory, creatures, ruleData, proficiencyBonus, theme, } = this.props; const headerNode: ReactNode = ( <> Infusions{" "} {infusableChoices.length !== infusionChoices.length && ( <> ({infusableChoices.length}/{infusionChoices.length} Known Infusions) )} ); let extraNode: ReactNode = null; if (!isReadonly) { extraNode = ( Manage Infusions ); } return ( ); }; renderOtherPossessions = (): ReactNode => { const { notes, isReadonly } = this.props; if (isReadonly) { return null; } return ( ); }; renderContent = (): ReactNode => { const { isReadonly, inventoryManager, inventory, partyInventory, showNotes, } = this.props; const { shouldShowPartyInventory } = this.state; const headerNode: ReactNode = (
{ evt.nativeEvent.stopImmediatePropagation(); this.handleManageClick(); }} > Party Inventory
); return (
} filters={[ { label: "All", content: ( <> {shouldShowPartyInventory === 0 ? ( <> {this.renderCharacterContainers()} {this.renderAttunement()} {this.shouldRenderInfusions() && this.renderInfusions()} {this.renderOtherPossessions()} ) : null} {({ imsFlag }) => { return imsFlag && shouldShowPartyInventory === 1 && inventoryManager.getPartyContainers().length > 0 ? ( {this.renderPartyContainers()} ) : null; }} ), }, ...(shouldShowPartyInventory === 0 ? inventoryManager .getCharacterContainers() .map((characterContainer) => ({ label: characterContainer.getName(), content: this.renderContainer( characterContainer, inventory ), })) : []), ...(shouldShowPartyInventory === 1 ? inventoryManager.getPartyContainers().map((partyContainer) => ({ label: partyContainer.getName(), content: this.renderContainer(partyContainer, partyInventory), })) : []), ...(shouldShowPartyInventory === 0 ? [ { label: "Attunement", content: this.renderAttunement(), }, ...(this.shouldRenderInfusions() ? [ { label: "Infusions", content: this.renderInfusions(), }, ] : []), ...(!isReadonly ? [ { label: "Other Possessions", content: this.renderOtherPossessions(), }, ] : []), ] : []), ]} showAllTab={false} />
); }; renderManageButton = (): ReactNode => { const { isReadonly } = this.props; if (isReadonly) { return null; } return ( Manage Inventory ); }; toggleShouldShowPartyInventory = (event, newValue): void => { this.setState({ shouldShowPartyInventory: newValue, }); }; handleCloseDeleteOnlyInfo = (event: MouseEvent): void => { event.stopPropagation(); event.nativeEvent.stopImmediatePropagation(); this.setState({ shouldShowDeleteOnlyInfo: false, }); }; handleOpenDeleteOnlyInfo = (event: MouseEvent): void => { event.stopPropagation(); event.nativeEvent.stopImmediatePropagation(); this.setState({ shouldShowDeleteOnlyInfo: true, }); }; renderDeleteOnlyAlertAction = (): ReactNode => { const { shouldShowDeleteOnlyInfo } = this.state; const { campaignInfo } = this.props; return (
More Info Delete Only Party Inventory sharing isn’t currently enabled for this campaign. Your old items are still here, but you won’t be able to add new items until it’s turned on again. {campaignInfo ? ( ) : null}
); }; renderOverviewFiltersAndContent = (): ReactNode => { const { filterData, shouldShowPartyInventory } = this.state; const { currencies, weight, weightSpeedType, inventory, ruleData, theme, partyInventory, campaignInfo, inventoryManager, coinManager, } = this.props; const cointainerFlagEnabled: boolean = coinManager.canUseCointainers(); let coin: CharacterCurrencyContract | null = currencies; if (shouldShowPartyInventory === 1 && campaignInfo) { // We need all the party coin if flag is on, or just Party Equipment coin if not if (cointainerFlagEnabled) { coin = coinManager.getAllPartyCoin(); } else { coin = CampaignUtils.getCoin(campaignInfo); } } else if (cointainerFlagEnabled) { // We need to change it to all container currencies coin = coinManager.getAllCharacterCoin(); } return ( <>
this.handleCurrencyClick( evt, shouldShowPartyInventory === 1 ? inventoryManager.getPartyEquipmentContainerDefinitionKey() ?? inventoryManager.getCharacterContainerDefinitionKey() : inventoryManager.getCharacterContainerDefinitionKey() ) } onWeightClick={this.handleWeightClick} onCampaignClick={this.handleCampaignClick} enableManage={false} isDarkMode={theme.isDarkMode} campaignName={ campaignInfo !== null ? CampaignUtils.getName(campaignInfo) : null } />
{!filterData.showAdvancedFilters && this.renderContent()} ); }; renderOptinToInventorySharing = (): ReactNode => { const { campaignInfo } = this.props; return ( <> Want a hand with that loot? Party Inventory makes it easy for your group to keep track of all their shared loot and coin. Add it to your campaign today with a subscription to D&D Beyond! {campaignInfo ? ( ) : null}
); }; render() { const { shouldShowPartyInventory } = this.state; const { campaignInfo, inventoryManager } = this.props; return (

Inventory

{({ imsFlag }) => { return imsFlag && campaignInfo !== null ? ( <> {/* TODO: make this go away when last itme is removed... */} {shouldShowPartyInventory && inventoryManager.isSharingTurnedDeleteOnly() ? (
Party Inventory is currently turned off for this campaign.
) : null} {shouldShowPartyInventory && inventoryManager.isSharingTurnedOff() ? this.renderOptinToInventorySharing() : this.renderOverviewFiltersAndContent()} ) : ( this.renderOverviewFiltersAndContent() ); }}
); } } function mapStateToProps(state: SheetAppState) { return { builderUrl: sheetAppSelectors.getBuilderUrl(state), inventory: rulesEngineSelectors.getInventory(state), partyInventory: rulesEngineSelectors.getPartyInventory(state), creatures: rulesEngineSelectors.getCreatures(state), weight: rulesEngineSelectors.getTotalCarriedWeight(state), weightSpeedType: rulesEngineSelectors.getCurrentCarriedWeightType(state), notes: rulesEngineSelectors.getCharacterNotes(state), infusionChoices: rulesEngineSelectors.getAvailableInfusionChoices(state), currencies: rulesEngineSelectors.getCurrencies(state), ruleData: rulesEngineSelectors.getRuleData(state), snippetData: rulesEngineSelectors.getSnippetData(state), theme: rulesEngineSelectors.getCharacterTheme(state), isReadonly: appEnvSelectors.getIsReadonly(state), isMobile: appEnvSelectors.getIsMobile(state), proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state), classes: characterSelectors.getClasses(state), campaignInfo: serviceDataSelectors.getPartyInfo(state), }; } const EquipmentContainer = (props) => { const { inventoryManager } = useContext(InventoryManagerContext); const { coinManager } = useContext(CoinManagerContext); const { pane: { paneHistoryStart }, } = useSidebar(); return ( ); }; export default connect(mapStateToProps)(EquipmentContainer);