2025-05-28 15:36:51 -07:00

451 lines
12 KiB
TypeScript

import React from "react";
import { useContext } from "react";
import { connect, DispatchProp } from "react-redux";
import {
rulesEngineSelectors,
CampaignUtils,
serviceDataActions,
Constants,
Container,
ContainerUtils,
InventoryManager,
InfusionUtils,
Item,
ItemUtils,
RuleData,
GroupedMenuOption,
serviceDataSelectors,
PartyInfo,
} from "@dndbeyond/character-rules-engine/es";
import { Button } from "~/components/Button";
import { Popover } from "~/components/Popover";
import { PopoverContent } from "~/components/PopoverContent";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import SimpleQuantity from "../../components/SimpleQuantity";
import {
RemoveButton,
ThemeButton,
ThemeButtonWithMenu,
} from "../../components/common/Button";
import { InventoryManagerContext } from "../../managers/InventoryManagerContext";
import * as appEnvSelectors from "../../selectors/appEnv";
import { SharedAppState } from "../../stores/typings";
import { PaneIdentifierUtils } from "../../utils";
interface LabelLookup {
consume: string;
remove: string;
unequip: string;
equip: string;
attune: string;
unattune: string;
}
interface Props extends DispatchProp {
item: Item;
hasMaxAttunedItems: boolean;
ruleData: RuleData;
isReadonly: boolean;
containers: Array<Container>;
onPostRemoveNavigation?: PaneComponentEnum;
partyInfo: PartyInfo | null;
inventoryManager: InventoryManager;
paneHistoryStart: PaneInfo["paneHistoryStart"];
}
interface State {
quantity: number;
newQuantity: number;
}
class ItemDetailActions extends React.PureComponent<Props, State> {
static defaultProps = {
hasMaxAttunedItems: false,
};
constructor(props: Props) {
super(props);
this.state = {
quantity: ItemUtils.getQuantity(props.item),
newQuantity: ItemUtils.getQuantity(props.item),
};
}
componentDidUpdate(
prevProps: Readonly<Props>,
prevState: Readonly<State>,
snapshot?: any
): void {
const { item } = this.props;
const nextQuantity = ItemUtils.getQuantity(item);
if (nextQuantity !== prevState.quantity) {
this.setState({
quantity: nextQuantity,
newQuantity: nextQuantity,
});
}
}
handleQuantityUpdate = (newValue: number): void => {
const { item, inventoryManager } = this.props;
inventoryManager.handleQuantitySet({ item, quantity: newValue });
};
handleRemove = (): void => {
const { paneHistoryStart, item, onPostRemoveNavigation, inventoryManager } =
this.props;
if (onPostRemoveNavigation) {
paneHistoryStart(onPostRemoveNavigation);
}
inventoryManager.handleRemove({ item });
if (ItemUtils.isContainer(item)) {
paneHistoryStart(PaneComponentEnum.EQUIPMENT_MANAGE);
}
};
handleRemoveInfusion = (): void => {
const { dispatch, paneHistoryStart, item } = this.props;
const infusion = ItemUtils.getInfusion(item);
if (infusion) {
const infusionId = InfusionUtils.getId(infusion);
if (infusionId === null) {
return;
}
if (
InfusionUtils.getType(infusion) === Constants.InfusionTypeEnum.REPLICATE
) {
const choiceKey = InfusionUtils.getChoiceKey(infusion);
if (choiceKey !== null) {
paneHistoryStart(
PaneComponentEnum.INFUSION_CHOICE,
PaneIdentifierUtils.generateInfusionChoice(choiceKey)
);
}
}
dispatch(
serviceDataActions.infusionMappingDestroy(
infusionId,
InfusionUtils.getInventoryMappingId(infusion)
)
);
}
};
handleRemoveInfusions = (itemIds: Array<number>): void => {
const { dispatch } = this.props;
itemIds.forEach((id) => {
dispatch(serviceDataActions.infusionMappingsDestroy(itemIds));
});
};
handleUnequip = (): void => {
const { inventoryManager, item } = this.props;
inventoryManager.handleUnequip({ item });
};
handleEquip = (): void => {
const { inventoryManager, item } = this.props;
inventoryManager.handleEquip({ item });
};
handleAttune = (): void => {
const { inventoryManager, item } = this.props;
inventoryManager.handleEquip({ item });
inventoryManager.handleAttune({ item });
};
handleUnattune = (): void => {
const { inventoryManager, item } = this.props;
inventoryManager.handleUnattune({ item });
};
handleMove = (containerDefinitionKey: string): void => {
const { inventoryManager, item } = this.props;
inventoryManager.handleMove({ item, containerDefinitionKey });
};
getLabels = (): LabelLookup => {
return {
consume: "Consume",
remove: "Delete Item",
unequip: "Unequip",
equip: "Equip",
attune: "Attune",
unattune: "Unattune",
};
};
getContainerOptions = (): Array<GroupedMenuOption> => {
const { containers, item, partyInfo } = this.props;
return ContainerUtils.getGroupedOptions(
ItemUtils.getContainerDefinitionKey(item),
containers,
"Move To:",
partyInfo
? CampaignUtils.getSharingState(partyInfo)
: Constants.PartyInventorySharingStateEnum.OFF
);
};
renderRemove = (): React.ReactNode => {
const { item, containers, isReadonly, inventoryManager } = this.props;
const canRemoveItem = inventoryManager.canRemoveItem(item);
if (isReadonly || !canRemoveItem) {
return null;
}
const infusion = ItemUtils.getInfusion(item);
const name = ItemUtils.getName(item);
const isContainer = ItemUtils.isContainer(item);
if (isContainer) {
const container = containers.find((container) => {
return (
ContainerUtils.getContainerType(container) ===
Constants.ContainerTypeEnum.ITEM &&
ContainerUtils.getMappingId(container) ===
ItemUtils.getMappingId(item)
);
});
if (container) {
if (infusion) {
return (
<RemoveButton
onClick={this.handleRemove}
className="ct-item-detail__action"
data-testid="remove-infusion-button"
>
Remove Infusion
</RemoveButton>
);
}
const hasInfusions = ContainerUtils.hasInfusions(container);
return (
<Popover
trigger={
<Button size="xx-small" variant="outline" themed>
Delete
</Button>
}
position="bottom"
data-testid="delete-item-button"
maxWidth={250}
>
<PopoverContent
title={`Remove ${name}?`}
content={`Removing the ${name} will also remove all of its ${
hasInfusions ? "infusions and " : " "
} contents.`}
confirmText="Delete"
onConfirm={() => this.handleRemove()}
withCancel
/>
</Popover>
// <RemoveButtonWithModal
// className="ct-item-detail__action"
// style={"outline"}
// size="small"
// modalTitle={`Delete ${name}?`}
// modalContent={`Deleting the ${name} will also delete all of its${
// hasInfusions ? " infusions and" : ""
// } contents.`}
// confirmCallback={() => {
// this.handleRemove();
// }}
// data-testid="delete-item-button"
// />
);
}
}
if (infusion) {
return (
<RemoveButton
onClick={this.handleRemoveInfusion}
className="ct-item-detail__action"
data-testid="remove-infusion-button"
>
Remove Infusion
</RemoveButton>
);
}
return (
<RemoveButton
onClick={this.handleRemove}
className="ct-item-detail__action"
data-testid="delete-item-button"
>
Delete
</RemoveButton>
);
};
render() {
const {
item,
hasMaxAttunedItems,
isReadonly,
containers,
inventoryManager,
} = this.props;
const labels = this.getLabels();
const containerMoveOptions = this.getContainerOptions();
const isEquipped = ItemUtils.isEquipped(item);
const isAttuned = ItemUtils.isAttuned(item);
const isStackable = ItemUtils.isStackable(item);
const quantity = ItemUtils.getQuantity(item);
const isContainer = ItemUtils.getDefinitionIsContainer(item);
const parentContainer = ContainerUtils.getItemParentContainer(
containers,
ItemUtils.getContainerDefinitionKey(item)
);
if (!parentContainer) {
return null;
}
const canEquip = inventoryManager.canEquipUnequipItem(item);
const canAttune = inventoryManager.canAttuneUnattuneItem(item);
const canModifyQuantity = inventoryManager.canModifyQuantity(item);
const canMoveItem =
inventoryManager.canMoveItem(item) &&
!isContainer &&
!isReadonly &&
!!containers.length;
const canShareItem = inventoryManager.canShareItem(item) && !isReadonly;
const canClaimItem = inventoryManager.canClaimItem(item) && !isReadonly;
return (
<div className="ct-item-detail__actions">
{isStackable && (
<SimpleQuantity
quantity={quantity}
onUpdate={this.handleQuantityUpdate}
isReadonly={isReadonly || !canModifyQuantity}
/>
)}
{canEquip && !isReadonly && (
<ThemeButton
onClick={isEquipped ? this.handleUnequip : this.handleEquip}
style={isEquipped ? "" : "outline"}
size="small"
className="ct-item-detail__action"
data-testid="equip-item-button"
>
{isEquipped ? labels.unequip : labels.equip}
</ThemeButton>
)}
{canAttune && !isReadonly && (
<ThemeButton
onClick={isAttuned ? this.handleUnattune : this.handleAttune}
style={isAttuned ? "" : "outline"}
size="small"
disabled={hasMaxAttunedItems && !isAttuned}
className="ct-item-detail__action"
data-testid="attune-item-button"
>
{isAttuned ? labels.unattune : labels.attune}
</ThemeButton>
)}
{canShareItem && (
<ThemeButton
style="outline"
onClick={() => {
const definitionKey =
inventoryManager.getPartyEquipmentContainerDefinitionKey();
if (definitionKey) {
this.handleMove(definitionKey);
}
}}
size="small"
className="ct-item-detail__action"
data-testid="move-item-button"
>
Move
</ThemeButton>
)}
{canClaimItem && (
<ThemeButton
style="outline"
onClick={() => {
const definitionKey =
inventoryManager.getCharacterContainerDefinitionKey();
if (definitionKey) {
this.handleMove(definitionKey);
}
}}
size="small"
className="ct-item-detail__action"
data-testid="move-item-button"
>
Move
</ThemeButton>
)}
{canMoveItem && (
<ThemeButtonWithMenu
buttonStyle="outline"
containerEl={
document.querySelector(".ct-sidebar__portal") as HTMLElement
}
className="ct-item-detail__action"
groupedOptions={containerMoveOptions}
showSingleOption={true}
onSelect={this.handleMove}
>
Move
</ThemeButtonWithMenu>
)}
{this.renderRemove()}
</div>
);
}
}
function mapStateToProps(state: SharedAppState) {
return {
ruleData: rulesEngineSelectors.getRuleData(state),
isReadonly: appEnvSelectors.getIsReadonly(state),
hasMaxAttunedItems: rulesEngineSelectors.hasMaxAttunedItems(state),
containers: rulesEngineSelectors.getInventoryContainers(state),
partyInfo: serviceDataSelectors.getPartyInfo(state),
};
}
function ItemDetailActionsContainer(props) {
const { inventoryManager } = useContext(InventoryManagerContext);
const {
pane: { paneHistoryStart },
} = useSidebar();
return (
<ItemDetailActions
inventoryManager={inventoryManager}
paneHistoryStart={paneHistoryStart}
{...props}
/>
);
}
export default connect(mapStateToProps)(ItemDetailActionsContainer);