465 lines
13 KiB
TypeScript
465 lines
13 KiB
TypeScript
import React from "react";
|
|
import { useContext } from "react";
|
|
import { connect, DispatchProp } from "react-redux";
|
|
import { AnyAction } from "redux";
|
|
|
|
import {
|
|
rulesEngineSelectors,
|
|
serviceDataActions,
|
|
Constants,
|
|
Container,
|
|
ContainerUtils,
|
|
InventoryManager,
|
|
InfusionUtils,
|
|
Item,
|
|
ItemUtils,
|
|
RuleData,
|
|
GroupedMenuOption,
|
|
serviceDataSelectors,
|
|
PartyInfo,
|
|
ItemPlan,
|
|
ContainerLookup,
|
|
} 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 { ButtonWithMenu } from "~/subApps/sheet/components/Sidebar/components/ButtonWithMenu";
|
|
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
|
|
|
|
import SimpleQuantity from "../../components/SimpleQuantity";
|
|
import { InventoryManagerContext } from "../../managers/InventoryManagerContext";
|
|
import * as appEnvSelectors from "../../selectors/appEnv";
|
|
import { SharedAppState } from "../../stores/typings";
|
|
import { PaneIdentifierUtils } from "../../utils";
|
|
|
|
// TODO: We are typing with AnyAction here to work around issues with UnknownAction, the type that was introduced in Redux 5.
|
|
// If we decide to modernize to Redux 5, we will need to refactor much of the Rules Engine redux usage.
|
|
interface Props extends DispatchProp<AnyAction> {
|
|
item: Item;
|
|
hasMaxAttunedItems: boolean;
|
|
ruleData: RuleData;
|
|
isReadonly: boolean;
|
|
containers: Array<Container>;
|
|
onPostRemoveNavigation?: PaneComponentEnum;
|
|
partyInfo: PartyInfo | null;
|
|
inventoryManager: InventoryManager;
|
|
paneHistoryStart: PaneInfo["paneHistoryStart"];
|
|
itemPlans: Array<ItemPlan>;
|
|
containerLookup: ContainerLookup;
|
|
isUserDM: boolean;
|
|
onRemoveItem?: () => void;
|
|
}
|
|
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,
|
|
onRemoveItem,
|
|
} = this.props;
|
|
|
|
if (onRemoveItem) {
|
|
onRemoveItem();
|
|
}
|
|
|
|
if (onPostRemoveNavigation) {
|
|
paneHistoryStart(onPostRemoveNavigation);
|
|
}
|
|
|
|
inventoryManager.handleRemove({ item });
|
|
|
|
if (ItemUtils.isContainer(item)) {
|
|
paneHistoryStart(PaneComponentEnum.EQUIPMENT_MANAGE);
|
|
}
|
|
};
|
|
|
|
handleRemoveInfusion = (): void => {
|
|
const {
|
|
dispatch,
|
|
paneHistoryStart,
|
|
item,
|
|
inventoryManager,
|
|
containerLookup,
|
|
} = 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),
|
|
inventoryManager.handleAcceptOnSuccess(
|
|
ItemUtils.isShared(item, containerLookup)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
};
|
|
|
|
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 });
|
|
};
|
|
|
|
getContainerOptions = (): Array<GroupedMenuOption> => {
|
|
const { containers, item } = this.props;
|
|
|
|
return ContainerUtils.getGroupedOptions(
|
|
ItemUtils.getContainerDefinitionKey(item),
|
|
containers,
|
|
"Move To:"
|
|
);
|
|
};
|
|
|
|
renderRemove = (): React.ReactNode => {
|
|
const {
|
|
item,
|
|
containers,
|
|
isReadonly,
|
|
inventoryManager,
|
|
itemPlans,
|
|
isUserDM,
|
|
} = this.props;
|
|
|
|
const canRemoveItem = isUserDM
|
|
? true
|
|
: inventoryManager.canRemoveItem(item);
|
|
|
|
if (isReadonly || !canRemoveItem) {
|
|
return null;
|
|
}
|
|
|
|
const infusion = ItemUtils.getInfusion(item);
|
|
|
|
const name = ItemUtils.getName(item);
|
|
const isContainer = ItemUtils.isContainer(item);
|
|
const itemPlan = ItemUtils.getItemPlan(item, itemPlans);
|
|
|
|
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 (
|
|
<Button
|
|
onClick={this.handleRemove}
|
|
variant="outline"
|
|
size="xx-small"
|
|
themed
|
|
data-testid="remove-infusion-button"
|
|
>
|
|
Remove Infusion
|
|
</Button>
|
|
);
|
|
}
|
|
const hasInfusions = ContainerUtils.hasInfusions(container);
|
|
|
|
return (
|
|
<Popover
|
|
trigger={
|
|
<Button size="xx-small" variant="outline" themed>
|
|
Delete{itemPlan ? " Replicated Item" : ""}
|
|
</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>
|
|
);
|
|
}
|
|
}
|
|
|
|
if (infusion) {
|
|
return (
|
|
<Button
|
|
onClick={this.handleRemoveInfusion}
|
|
variant="outline"
|
|
size="xx-small"
|
|
themed
|
|
data-testid="remove-infusion-button"
|
|
>
|
|
Remove Infusion
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
onClick={this.handleRemove}
|
|
variant="outline"
|
|
size="xx-small"
|
|
themed
|
|
data-testid="delete-item-button"
|
|
>
|
|
Delete{itemPlan ? " Replicated Item" : ""}
|
|
</Button>
|
|
);
|
|
};
|
|
|
|
render() {
|
|
const {
|
|
item,
|
|
hasMaxAttunedItems,
|
|
isReadonly,
|
|
containers,
|
|
inventoryManager,
|
|
} = this.props;
|
|
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 parentContainer = ContainerUtils.getItemParentContainer(
|
|
containers,
|
|
ItemUtils.getContainerDefinitionKey(item)
|
|
);
|
|
if (!parentContainer) {
|
|
return null;
|
|
}
|
|
const canEquip = inventoryManager.canEquipUnequipItem(item) && !isReadonly;
|
|
const canAttune =
|
|
inventoryManager.canAttuneUnattuneItem(item) && !isReadonly;
|
|
const canModifyQuantity =
|
|
inventoryManager.canModifyQuantity(item) && !isReadonly;
|
|
const canMoveItem = inventoryManager.canMoveItem(item) && !isReadonly;
|
|
const canShareContainer =
|
|
inventoryManager.canShareContainer(item) && !isReadonly;
|
|
const canClaimContainer =
|
|
inventoryManager.canClaimContainer(item) && !isReadonly;
|
|
const partyInventoryDefinitionKey =
|
|
inventoryManager.getPartyEquipmentContainerDefinitionKey();
|
|
const characterInventoryDefinitionKey =
|
|
inventoryManager.getCharacterContainerDefinitionKey();
|
|
|
|
return (
|
|
<div className="ct-item-detail__actions">
|
|
{isStackable && (
|
|
<SimpleQuantity
|
|
quantity={quantity}
|
|
onUpdate={this.handleQuantityUpdate}
|
|
isReadonly={!canModifyQuantity}
|
|
/>
|
|
)}
|
|
|
|
{canEquip && (
|
|
<Button
|
|
onClick={isEquipped ? this.handleUnequip : this.handleEquip}
|
|
variant={isEquipped ? "solid" : "outline"}
|
|
size="xx-small"
|
|
themed
|
|
data-testid="equip-item-button"
|
|
>
|
|
{isEquipped ? "Unequip" : "Equip"}
|
|
</Button>
|
|
)}
|
|
{canAttune && (
|
|
<Button
|
|
onClick={isAttuned ? this.handleUnattune : this.handleAttune}
|
|
variant={isAttuned ? "solid" : "outline"}
|
|
size="xx-small"
|
|
themed
|
|
disabled={hasMaxAttunedItems && !isAttuned}
|
|
data-testid="attune-item-button"
|
|
>
|
|
{isAttuned ? "Unattune" : "Attune"}
|
|
</Button>
|
|
)}
|
|
{canShareContainer && partyInventoryDefinitionKey && (
|
|
<ButtonWithMenu
|
|
showSingleOption={true}
|
|
placement="bottom-end"
|
|
variant="outline"
|
|
groupedOptions={[
|
|
{
|
|
label: "Move To:",
|
|
options: [
|
|
{
|
|
label: "Party Inventory",
|
|
value: partyInventoryDefinitionKey,
|
|
},
|
|
],
|
|
},
|
|
]}
|
|
onSelect={() => this.handleMove(partyInventoryDefinitionKey)}
|
|
data-testid="move-item-button"
|
|
>
|
|
Move
|
|
</ButtonWithMenu>
|
|
)}
|
|
{canClaimContainer && characterInventoryDefinitionKey && (
|
|
<ButtonWithMenu
|
|
showSingleOption={true}
|
|
placement="bottom-end"
|
|
variant="outline"
|
|
groupedOptions={[
|
|
{
|
|
label: "Move To:",
|
|
options: [
|
|
{
|
|
label: "My Inventory",
|
|
value: characterInventoryDefinitionKey,
|
|
},
|
|
],
|
|
},
|
|
]}
|
|
onSelect={() => this.handleMove(characterInventoryDefinitionKey)}
|
|
data-testid="move-item-button"
|
|
>
|
|
Move
|
|
</ButtonWithMenu>
|
|
)}
|
|
{canMoveItem && (
|
|
<ButtonWithMenu
|
|
variant="outline"
|
|
groupedOptions={containerMoveOptions}
|
|
showSingleOption={true}
|
|
onSelect={this.handleMove}
|
|
>
|
|
Move
|
|
</ButtonWithMenu>
|
|
)}
|
|
{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),
|
|
itemPlans: rulesEngineSelectors.getAvailableItemPlans(state),
|
|
containerLookup: rulesEngineSelectors.getContainerLookup(state),
|
|
isUserDM: appEnvSelectors.getIsUserDM(state),
|
|
};
|
|
}
|
|
|
|
function ItemDetailActionsContainer(props) {
|
|
const { inventoryManager } = useContext(InventoryManagerContext);
|
|
const {
|
|
pane: { paneHistoryStart },
|
|
} = useSidebar();
|
|
|
|
return (
|
|
<ItemDetailActions
|
|
inventoryManager={inventoryManager}
|
|
paneHistoryStart={paneHistoryStart}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export default connect(mapStateToProps)(ItemDetailActionsContainer);
|