New source found from dndbeyond.com

This commit is contained in:
2026-07-08 01:00:09 -07:00
parent 9a983a6d7b
commit dcefa0d2f2
2865 changed files with 222467 additions and 49053 deletions
@@ -11,18 +11,21 @@ import {
HtmlSelectOptionGroup,
} from "@dndbeyond/character-rules-engine/es";
import { Accordion } from "~/components/Accordion";
import { CollapsibleContent } from "~/components/CollapsibleContent";
import { HtmlContent } from "~/components/HtmlContent";
import { Select } from "~/components/Select";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { useRuleData } from "~/hooks/useRuleData";
import Collapsible from "~/tools/js/smartComponents/Collapsible";
import { Select } from "~/tools/js/smartComponents/legacy";
import styles from "./styles.module.css";
// TODO this needs to not extend builder contract and options typing needs to be fixed
// TODO scss to styles.module.css
interface Props extends Omit<BuilderChoiceContract, "options"> {
choice: Choice;
description?: string;
descriptionIntro?: string;
choiceInfo: ChoiceData;
classId?: number | null;
maxDescriptionLength?: number;
@@ -42,6 +45,7 @@ interface Props extends Omit<BuilderChoiceContract, "options"> {
export const DetailChoice: FC<Props> = ({
choice,
description,
descriptionIntro,
optionValue,
parentChoiceId,
maxDescriptionLength = 750,
@@ -66,7 +70,7 @@ export const DetailChoice: FC<Props> = ({
(ChoiceUtils.isInfinite(choice) && !ChoiceUtils.isOptionSelected(choice)) ||
ChoiceUtils.isTodo(choice);
const handleChoiceChange = (value: string): void => {
const handleChoiceChange = (value: string | number): void => {
if (onChange && id !== null) {
onChange(id, type, value, parentChoiceId);
}
@@ -92,44 +96,53 @@ export const DetailChoice: FC<Props> = ({
choiceDescription = description || chosenOption["description"];
}
const choiceDescriptionIntro = descriptionIntro ? (
<p className={styles.descriptionIntro}>{descriptionIntro}</p>
) : null;
let choiceNode: ReactNode;
if (choiceDescription) {
switch (type) {
case Constants.BuilderChoiceTypeEnum.ENTITY_SPELL_OPTION:
choiceNode = (
<Collapsible layoutType={"minimal"} header="Spell Details">
<Accordion variant="text" summary="Spell Details" size="xx-small">
<HtmlContent
className="detail-choice-description"
html={choiceDescription}
withoutTooltips
/>
</Collapsible>
</Accordion>
);
break;
default:
if (collapseDescription) {
choiceNode = (
<Collapsible layoutType={"minimal"} header="Show Details">
<Accordion variant="text" summary="Show Details" size="xx-small">
{choiceDescriptionIntro}
<HtmlContent
className="detail-choice-description"
html={choiceDescription}
withoutTooltips
/>
</Collapsible>
</Accordion>
);
} else if (choiceDescription.length > maxDescriptionLength) {
choiceNode = (
<CollapsibleContent className="detail-choice-description">
{choiceDescription}
{choiceDescriptionIntro}
<HtmlContent html={choiceDescription} />
</CollapsibleContent>
);
} else {
choiceNode = (
<HtmlContent
className="detail-choice-description"
html={choiceDescription}
withoutTooltips
/>
<>
{choiceDescriptionIntro}
<HtmlContent
className="detail-choice-description"
html={choiceDescription}
withoutTooltips
/>
</>
);
}
}
@@ -145,7 +158,8 @@ export const DetailChoice: FC<Props> = ({
])}
>
<Select
className="detail-choice-input"
value={optionValue}
placeholder={label ? `- ${label} -` : "- Choose an Option -"}
options={ChoiceUtils.getSortedRenderOptions(
options,
classId,
@@ -158,9 +172,10 @@ export const DetailChoice: FC<Props> = ({
choiceInfo,
optionValue
)}
value={optionValue}
placeholder={label ? `- ${label} -` : "- Choose an Option -"}
onChange={handleChoiceChange}
searchThreshold={10}
isHighlighted={hasTodo}
name={`detail-choice-select-${choice.id}`}
/>
{choiceNode}
</div>
@@ -1,158 +1,173 @@
import { useContext, useEffect, useRef } from "react";
import clsx from "clsx";
import { useContext, useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
import { DispatchProp, useDispatch, useSelector } from "react-redux";
import { useSelector } from "react-redux";
import { TypeScriptUtils } from "@dndbeyond/character-components/es";
import {
ApiAdapterUtils,
CharacterTheme,
ConfigUtils,
rulesEngineSelectors,
Constants,
ApiException,
CampaignDataContract,
CampaignUtils,
characterEnvSelectors,
} from "@dndbeyond/character-rules-engine/es";
import { Dice, DiceNotifier, IRollContext } from "@dndbeyond/dice";
import { DiceToolbar } from "@dndbeyond/dice-components";
import { Platform } from "@dndbeyond/event-pipeline-lib/shared";
import D20 from "@dndbeyond/fontawesome-cache/svgs/light/dice-d20.svg";
import { GameLogContext } from "@dndbeyond/game-log-components";
import { getMessageBroker } from "@dndbeyond/message-broker-lib";
import {
CustomDiceRoller,
rollerImport,
} from "@dndbeyond/pocket-dimension-dice/components/CustomDiceRoller";
import "@dndbeyond/pocket-dimension-dice/components/CustomDiceRoller/index.css";
import { Context as DiceWorkerContext } from "@dndbeyond/pocket-dimension-dice/context";
import { instrumentDiceClear } from "@dndbeyond/pocket-dimension-dice/helpers/bi/instrumentDiceClear";
import { instrumentDiceContainerClose } from "@dndbeyond/pocket-dimension-dice/helpers/bi/instrumentDiceContainerClose";
import { instrumentDiceContainerOpen } from "@dndbeyond/pocket-dimension-dice/helpers/bi/instrumentDiceContainerOpen";
import { instrumentDiceRoll } from "@dndbeyond/pocket-dimension-dice/helpers/bi/instrumentDiceRoll";
import { useDiceSets } from "@dndbeyond/pocket-dimension-dice/hooks/useDiceSets";
import { useUserDiceDataStore } from "@dndbeyond/pocket-dimension-dice/hooks/useUserDiceDataStore";
import { AnchoredPopover } from "@dndbeyond/ttui/components/AnchoredPopover";
import { Button } from "~/components/Button";
import { useAuth } from "~/contexts/Authentication";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { useDispatch } from "~/hooks/useDispatch";
import { appEnvActions } from "../../actions/appEnv";
import {
appEnvSelectors,
characterRollContextSelectors,
} from "../../selectors";
import {
DiceFeatureConfigurationState,
SharedAppState,
} from "../../stores/typings";
import { AppLoggerUtils } from "../../utils";
import { appEnvSelectors } from "../../selectors";
import styles from "./DiceContainer.module.css";
interface Props extends DispatchProp {
campaign: CampaignDataContract | null;
canShow: boolean;
theme: CharacterTheme;
diceEnabled: boolean;
diceFeatureConfiguration: DiceFeatureConfigurationState;
characterRollContext: IRollContext;
}
const location = "characterSheet";
// <canvas id="dice-rolling-canvas" className={'dice-rolling-panel__container'} />
// this is the canvas element that we will use to draw the dice
// TODO: this is a temporary solution, we need to find a better way to do this
// waiting for adventure team to provide a better solution
const canvasElement = document.createElement("canvas");
canvasElement.id = "dice-rolling-canvas";
canvasElement.classList.add("dice-rolling-panel__container");
export default function DiceContainer({ canShow = true }) {
export default function DiceContainer({ canShow = true, isVttView = false }) {
const { nodeEnv, env } = useContext(DiceWorkerContext);
const dispatch = useDispatch();
const theme = useSelector(rulesEngineSelectors.getCharacterTheme);
const diceEnabled = useSelector(appEnvSelectors.getDiceEnabled);
const diceFeatureConfiguration = useSelector(
appEnvSelectors.getDiceFeatureConfiguration
);
const characterRollContext = useSelector(
characterRollContextSelectors.getCharacterRollContext
);
const campaign = useSelector(rulesEngineSelectors.getCampaign);
const userConfigApi = "/diceuserconfig/v1/get";
const authUser = useAuth();
let userId: string | null = null;
if (authUser?.id) {
userId = authUser.id;
}
const [{ messageTargetOptions, defaultMessageTargetOption }] =
useContext(GameLogContext);
const { campaign, characterName } = useCharacterEngine();
const user = useAuth();
const [{ entityId }] = useContext(GameLogContext);
const canvasContainer = useRef<HTMLDivElement | null>(null);
const [isCustomDiceRollerOpen, setIsCustomDiceRollerOpen] = useState(false);
const diceSets = useDiceSets(nodeEnv, env);
const setId = useUserDiceDataStore(
(state) => state.getUserDiceData(Number(user?.id))?.setId
);
const dmId = campaign
? String(CampaignUtils.getDmUserId(campaign))
: undefined;
const isSheet =
useSelector(characterEnvSelectors.getContext) ===
Constants.AppContextTypeEnum.SHEET;
useEffect(() => {
if (!canvasContainer.current?.contains(canvasElement)) {
canvasContainer.current?.appendChild(canvasElement);
}
});
useEffect(() => {
getMessageBroker().then((mb) => {
if (mb && campaign && userId) {
mb.gameId = isSheet ? CampaignUtils.getId(campaign).toString() : "0";
mb.userId = userId;
if (mb && user?.id) {
if (campaign) {
mb.gameId = CampaignUtils.getId(campaign).toString();
} else {
mb.gameId = undefined;
}
mb.userId = user.id;
}
ConfigUtils.configureRulesEngine({ messageBroker: mb });
if (diceFeatureConfiguration.enabled) {
const rulesEngineConfig = ConfigUtils.getCurrentRulesEngineConfig();
if (rulesEngineConfig.apiAdapter) {
rulesEngineConfig.apiAdapter
.get(`${diceFeatureConfiguration.apiEndpoint}${userConfigApi}`)
.then((x) => {
const diceCanvas = canvasElement;
Dice.init({
isDebug: false,
assetBaseLocation: diceFeatureConfiguration.assetBaseLocation,
trackingId: diceFeatureConfiguration.trackingId,
sessionName: "Dice",
notifier: new DiceNotifier(),
canvas: diceCanvas || undefined,
previewMode: false,
diceUserConfig: ApiAdapterUtils.getResponseData(x) as any,
})
.then(() =>
document.addEventListener("click", () => Dice.clear())
)
.catch((error: Error) => {
AppLoggerUtils.logMessage(
error.message,
Constants.LogMessageType.WARNING
);
dispatch(
appEnvActions.dataSet({
diceEnabled: false,
})
);
});
})
.catch((error: ApiException) => {
AppLoggerUtils.logMessage(
error.message,
Constants.LogMessageType.WARNING
);
dispatch(
appEnvActions.dataSet({
diceEnabled: false,
})
);
});
}
} else {
Dice.analyticsInit(diceFeatureConfiguration.trackingId, "Dice");
}
});
}, [diceFeatureConfiguration, dispatch]);
}, [
campaign,
diceFeatureConfiguration.apiEndpoint,
diceFeatureConfiguration.assetBaseLocation,
diceFeatureConfiguration.enabled,
diceFeatureConfiguration.trackingId,
dispatch,
isSheet,
user?.id,
]);
if (isVttView) return null;
return ReactDOM.createPortal(
<div className={"dice-rolling-panel"} ref={canvasContainer}>
{canShow && diceEnabled && (
<DiceToolbar
rollContext={characterRollContext}
rollTargetOptions={
messageTargetOptions && isSheet
? Object.values(messageTargetOptions?.entities).filter(
TypeScriptUtils.isNotNullOrUndefined
)
: undefined
<AnchoredPopover
offset={16}
open={isCustomDiceRollerOpen}
allowedPlacements={["top-start"]}
referenceComponent={
<Button
className={clsx([
styles.button,
isCustomDiceRollerOpen && styles.customDiceRollOpen,
])}
onClick={() => {
setIsCustomDiceRollerOpen(!isCustomDiceRollerOpen);
// isCustomDiceRollerOpen is the previous state here
if (isCustomDiceRollerOpen) {
instrumentDiceContainerClose(
user,
Platform.Web,
location,
campaign?.id,
undefined,
env
);
} else {
instrumentDiceContainerOpen(
user,
Platform.Web,
location,
campaign?.id,
undefined,
env
);
}
}}
themed
onMouseEnter={rollerImport}
>
<D20 />
</Button>
}
rollTargetDefault={defaultMessageTargetOption}
themeColor={theme.themeColor}
userId={parseInt(userId || "0", 10)}
/>
floatingArrow
floatingArrowClassName={styles.floatingArrow}
>
{diceSets && (
<CustomDiceRoller
rollCallback={() => setIsCustomDiceRollerOpen(false)}
onClose={() => setIsCustomDiceRollerOpen(false)}
diceSets={diceSets}
gameId={campaign?.id}
visible={isCustomDiceRollerOpen}
setId={setId}
entityType="character"
entityId={entityId}
name={characterName ?? undefined}
rollInstrumentationCallback={(eventPayload) =>
instrumentDiceRoll(
user,
eventPayload,
Platform.Web,
location,
campaign?.id,
undefined,
env
)
}
clearDiceCallback={() =>
instrumentDiceClear(
user,
Platform.Web,
location,
campaign?.id,
undefined,
env
)
}
dmId={dmId}
/>
)}
</AnchoredPopover>
)}
{/* We want this one so it is sticking around,
also this is where the canvas goes when we append */}
@@ -1,117 +0,0 @@
import { Box } from "@mui/material";
import { StepType, TourContext, TourProvider } from "@reactour/tour";
import { Dispatch, ReactNode, SetStateAction, useState } from "react";
import { useSelector } from "react-redux";
import { rulesEngineSelectors } from "@dndbeyond/character-rules-engine";
import { checkContrast } from "../../utils/Color";
import GuidedTourButton from "./GuidedTourButton";
interface GuidedTourProps {
children?: ReactNode;
steps?: StepType[];
step?: number;
setStep?: SetStateAction<any>;
beforeClose?: (target: Element | null) => void;
afterOpen?: (target: Element | null) => void;
showOnFirstLoad?: boolean;
cookieName?: string;
}
export default ({
children,
step,
setStep,
steps = [],
beforeClose,
afterOpen,
showOnFirstLoad,
cookieName = "ddbGuidedTour",
}: GuidedTourProps) => {
const [defaultStep, setDefaultStep] = useState(0);
const { isDarkMode, backgroundColor, themeColor } = useSelector(
rulesEngineSelectors.getCharacterTheme
);
const bgColor = backgroundColor.slice(0, 7);
const accentColor = checkContrast(backgroundColor, themeColor)
? themeColor
: "#C53131";
const handleBeforeClose = () => {
const html = document.querySelector("html");
if (html) html.style.overflowY = "visible";
if (beforeClose) beforeClose(null);
};
const handleAfterOpen = () => {
const html = document.querySelector("html");
if (html) html.style.overflowY = "hidden";
if (afterOpen) afterOpen(null);
};
const handleLoad = (setIsOpen: Dispatch<SetStateAction<boolean>>) => {
if (showOnFirstLoad) {
const viewedTour = localStorage.getItem(cookieName);
if (!viewedTour) {
setIsOpen(true);
localStorage.setItem(cookieName, "true");
}
}
};
return (
<Box
sx={{
".reactour__popover": {
backgroundColor: `${bgColor} !important`,
color: isDarkMode
? "rgba(255,255,255,0.9) !important"
: "rgba(0,0,0,0.9) !important",
borderRadius: 1,
borderWidth: 1,
borderStyle: "solid",
borderColor: isDarkMode
? "rgba(255,255,255,0.3) !important"
: "rgba(0,0,0,0.3) !important",
},
}}
>
<TourProvider
steps={steps}
currentStep={step || defaultStep}
setCurrentStep={setStep || setDefaultStep}
afterOpen={handleAfterOpen}
beforeClose={handleBeforeClose}
showDots={false}
styles={{
badge: (base) => ({
...base,
backgroundColor: accentColor,
color: bgColor,
fontWeight: 700,
}),
close: (base) => ({
...base,
color: isDarkMode ? "rgba(255,255,255,0.9)" : "rgba(0,0,0,0.9)",
}),
dot: (base, status) => ({
...base,
background: status?.current ? accentColor : "transparent",
color: status?.current ? accentColor : "transparent",
}),
}}
>
<TourContext.Consumer>
{({ setIsOpen }) => (
<div onLoad={() => handleLoad(setIsOpen)}>
{children}
<GuidedTourButton />
</div>
)}
</TourContext.Consumer>
</TourProvider>
</Box>
);
};
@@ -1,27 +0,0 @@
import QuestionMarkIcon from "@mui/icons-material/QuestionMark";
import { Button } from "@mui/material";
import { useTour } from "@reactour/tour";
export default () => {
const { setIsOpen } = useTour();
return (
<Button
className="ct-character-guided-tour"
variant="contained"
color="info"
onClick={() => setIsOpen(true)}
sx={{
position: "fixed",
zIndex: 9,
p: 1,
borderRadius: "100%",
minWidth: 0,
bottom: { xs: 100, md: 20 },
right: 20,
}}
>
<QuestionMarkIcon />
</Button>
);
};
@@ -1,4 +0,0 @@
import GuidedTourButton from "./GuidedTourButton";
export default GuidedTourButton;
export { GuidedTourButton };
@@ -1,50 +0,0 @@
import { Button, Typography } from "@mui/material";
import { useTour } from "@reactour/tour";
import { useSelector } from "react-redux";
import { rulesEngineSelectors } from "@dndbeyond/character-rules-engine";
import { checkContrast } from "../../../utils/Color";
export default ({ title, content, showClose = false }) => {
const { themeColor, backgroundColor, isDarkMode } = useSelector(
rulesEngineSelectors.getCharacterTheme
);
const { setIsOpen } = useTour();
const accentColor = checkContrast(backgroundColor, themeColor)
? themeColor
: "#C53131";
return (
<>
<Typography variant="h4" component="p" mb={1}>
{title}
</Typography>
<Typography
component="div"
variant="body2"
sx={{ a: { color: accentColor, textDecoration: "none" } }}
>
{content}
</Typography>
{showClose && (
<Button
variant="outlined"
onClick={() => setIsOpen(false)}
sx={{
display: "inherit",
my: 1,
mx: "auto",
fontSize: 12,
color: isDarkMode
? "rgba(255,255,255,0.9) !important"
: "rgba(0,0,0,0.9) !important",
borderColor: accentColor,
}}
>
Start Playing!
</Button>
)}
</>
);
};
@@ -1,10 +1,10 @@
import React from "react";
import { useContext } from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
rulesEngineSelectors,
CampaignUtils,
serviceDataActions,
Constants,
Container,
@@ -17,6 +17,8 @@ import {
GroupedMenuOption,
serviceDataSelectors,
PartyInfo,
ItemPlan,
ContainerLookup,
} from "@dndbeyond/character-rules-engine/es";
import { Button } from "~/components/Button";
@@ -24,29 +26,18 @@ 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 {
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 {
// 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;
@@ -56,6 +47,10 @@ interface Props extends DispatchProp {
partyInfo: PartyInfo | null;
inventoryManager: InventoryManager;
paneHistoryStart: PaneInfo["paneHistoryStart"];
itemPlans: Array<ItemPlan>;
containerLookup: ContainerLookup;
isUserDM: boolean;
onRemoveItem?: () => void;
}
interface State {
quantity: number;
@@ -98,8 +93,17 @@ class ItemDetailActions extends React.PureComponent<Props, State> {
};
handleRemove = (): void => {
const { paneHistoryStart, item, onPostRemoveNavigation, inventoryManager } =
this.props;
const {
paneHistoryStart,
item,
onPostRemoveNavigation,
inventoryManager,
onRemoveItem,
} = this.props;
if (onRemoveItem) {
onRemoveItem();
}
if (onPostRemoveNavigation) {
paneHistoryStart(onPostRemoveNavigation);
@@ -113,7 +117,13 @@ class ItemDetailActions extends React.PureComponent<Props, State> {
};
handleRemoveInfusion = (): void => {
const { dispatch, paneHistoryStart, item } = this.props;
const {
dispatch,
paneHistoryStart,
item,
inventoryManager,
containerLookup,
} = this.props;
const infusion = ItemUtils.getInfusion(item);
if (infusion) {
@@ -137,7 +147,10 @@ class ItemDetailActions extends React.PureComponent<Props, State> {
dispatch(
serviceDataActions.infusionMappingDestroy(
infusionId,
InfusionUtils.getInventoryMappingId(infusion)
InfusionUtils.getInventoryMappingId(infusion),
inventoryManager.handleAcceptOnSuccess(
ItemUtils.isShared(item, containerLookup)
)
)
);
}
@@ -180,34 +193,30 @@ class ItemDetailActions extends React.PureComponent<Props, State> {
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;
const { containers, item } = this.props;
return ContainerUtils.getGroupedOptions(
ItemUtils.getContainerDefinitionKey(item),
containers,
"Move To:",
partyInfo
? CampaignUtils.getSharingState(partyInfo)
: Constants.PartyInventorySharingStateEnum.OFF
"Move To:"
);
};
renderRemove = (): React.ReactNode => {
const { item, containers, isReadonly, inventoryManager } = this.props;
const {
item,
containers,
isReadonly,
inventoryManager,
itemPlans,
isUserDM,
} = this.props;
const canRemoveItem = isUserDM
? true
: inventoryManager.canRemoveItem(item);
const canRemoveItem = inventoryManager.canRemoveItem(item);
if (isReadonly || !canRemoveItem) {
return null;
}
@@ -216,6 +225,7 @@ class ItemDetailActions extends React.PureComponent<Props, State> {
const name = ItemUtils.getName(item);
const isContainer = ItemUtils.isContainer(item);
const itemPlan = ItemUtils.getItemPlan(item, itemPlans);
if (isContainer) {
const container = containers.find((container) => {
@@ -230,13 +240,15 @@ class ItemDetailActions extends React.PureComponent<Props, State> {
if (container) {
if (infusion) {
return (
<RemoveButton
<Button
onClick={this.handleRemove}
className="ct-item-detail__action"
variant="outline"
size="xx-small"
themed
data-testid="remove-infusion-button"
>
Remove Infusion
</RemoveButton>
</Button>
);
}
const hasInfusions = ContainerUtils.hasInfusions(container);
@@ -245,7 +257,7 @@ class ItemDetailActions extends React.PureComponent<Props, State> {
<Popover
trigger={
<Button size="xx-small" variant="outline" themed>
Delete
Delete{itemPlan ? " Replicated Item" : ""}
</Button>
}
position="bottom"
@@ -262,43 +274,34 @@ class ItemDetailActions extends React.PureComponent<Props, State> {
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
<Button
onClick={this.handleRemoveInfusion}
className="ct-item-detail__action"
variant="outline"
size="xx-small"
themed
data-testid="remove-infusion-button"
>
Remove Infusion
</RemoveButton>
</Button>
);
}
return (
<RemoveButton
<Button
onClick={this.handleRemove}
className="ct-item-detail__action"
variant="outline"
size="xx-small"
themed
data-testid="delete-item-button"
>
Delete
</RemoveButton>
Delete{itemPlan ? " Replicated Item" : ""}
</Button>
);
};
@@ -310,14 +313,11 @@ class ItemDetailActions extends React.PureComponent<Props, State> {
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)
@@ -325,16 +325,20 @@ class ItemDetailActions extends React.PureComponent<Props, State> {
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;
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">
@@ -342,80 +346,86 @@ class ItemDetailActions extends React.PureComponent<Props, State> {
<SimpleQuantity
quantity={quantity}
onUpdate={this.handleQuantityUpdate}
isReadonly={isReadonly || !canModifyQuantity}
isReadonly={!canModifyQuantity}
/>
)}
{canEquip && !isReadonly && (
<ThemeButton
{canEquip && (
<Button
onClick={isEquipped ? this.handleUnequip : this.handleEquip}
style={isEquipped ? "" : "outline"}
size="small"
className="ct-item-detail__action"
variant={isEquipped ? "solid" : "outline"}
size="xx-small"
themed
data-testid="equip-item-button"
>
{isEquipped ? labels.unequip : labels.equip}
</ThemeButton>
{isEquipped ? "Unequip" : "Equip"}
</Button>
)}
{canAttune && !isReadonly && (
<ThemeButton
{canAttune && (
<Button
onClick={isAttuned ? this.handleUnattune : this.handleAttune}
style={isAttuned ? "" : "outline"}
size="small"
variant={isAttuned ? "solid" : "outline"}
size="xx-small"
themed
disabled={hasMaxAttunedItems && !isAttuned}
className="ct-item-detail__action"
data-testid="attune-item-button"
>
{isAttuned ? labels.unattune : labels.attune}
</ThemeButton>
{isAttuned ? "Unattune" : "Attune"}
</Button>
)}
{canShareItem && (
<ThemeButton
style="outline"
onClick={() => {
const definitionKey =
inventoryManager.getPartyEquipmentContainerDefinitionKey();
if (definitionKey) {
this.handleMove(definitionKey);
}
}}
size="small"
className="ct-item-detail__action"
{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
</ThemeButton>
</ButtonWithMenu>
)}
{canClaimItem && (
<ThemeButton
style="outline"
onClick={() => {
const definitionKey =
inventoryManager.getCharacterContainerDefinitionKey();
if (definitionKey) {
this.handleMove(definitionKey);
}
}}
size="small"
className="ct-item-detail__action"
{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
</ThemeButton>
</ButtonWithMenu>
)}
{canMoveItem && (
<ThemeButtonWithMenu
buttonStyle="outline"
containerEl={
document.querySelector(".ct-sidebar__portal") as HTMLElement
}
className="ct-item-detail__action"
<ButtonWithMenu
variant="outline"
groupedOptions={containerMoveOptions}
showSingleOption={true}
onSelect={this.handleMove}
>
Move
</ThemeButtonWithMenu>
</ButtonWithMenu>
)}
{this.renderRemove()}
</div>
@@ -430,6 +440,9 @@ function mapStateToProps(state: SharedAppState) {
hasMaxAttunedItems: rulesEngineSelectors.hasMaxAttunedItems(state),
containers: rulesEngineSelectors.getInventoryContainers(state),
partyInfo: serviceDataSelectors.getPartyInfo(state),
itemPlans: rulesEngineSelectors.getAvailableItemPlans(state),
containerLookup: rulesEngineSelectors.getContainerLookup(state),
isUserDM: appEnvSelectors.getIsUserDM(state),
};
}
@@ -438,6 +451,7 @@ function ItemDetailActionsContainer(props) {
const {
pane: { paneHistoryStart },
} = useSidebar();
return (
<ItemDetailActions
inventoryManager={inventoryManager}
@@ -1,12 +1,9 @@
import axios, { Canceler } from "axios";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
Button,
LoadingPlaceholder,
Select,
} from "@dndbeyond/character-components/es";
import { Button, LoadingPlaceholder } from "@dndbeyond/character-components/es";
import {
ApiAdapterPromise,
ApiAdapterRequestConfig,
@@ -30,9 +27,11 @@ import {
StartingEquipmentSlotContract,
TypeValueLookup,
DefinitionUtils,
HtmlSelectOption,
} from "@dndbeyond/character-rules-engine/es";
import { Link } from "~/components/Link";
import { Select } from "~/components/Select";
// TODO need to remove this builder reference and replace with better heading
import PageSubHeader from "../../../CharacterBuilder/components/PageSubHeader";
@@ -42,6 +41,7 @@ import * as apiCreatorSelectors from "../../selectors/composite/apiCreator";
import { SharedAppState } from "../../stores/typings";
import { AppLoggerUtils, AppNotificationUtils } from "../../utils";
import StartingEquipmentSlots from "./StartingEquipmentSlots";
import styles from "./styles.module.css";
import { StartingEquipmentRuleSlotSelection } from "./typings";
//duplicated from characterActions typingParts to avoid import issues
@@ -51,6 +51,8 @@ interface StartingEquipmentAddRequestPayloadItem {
entityId: number;
entityTypeId: number;
quantity: number;
originEntityId: number | null;
originEntityTypeId: number | null;
}
//duplicated from characterActions typingParts to avoid import issues
interface StartingEquipmentAddRequestPayload {
@@ -75,7 +77,9 @@ interface RuleChoiceGroupClearPayload {
groupKey: RuleSlotChoiceKey;
}
interface Props extends DispatchProp {
// 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> {
isInitialView: boolean;
onStartingEquipmentChoose?: () => void;
startingClass: CharClass | null;
@@ -527,6 +531,8 @@ class StartingEquipment extends React.PureComponent<Props, State> {
entityId: item.id,
entityTypeId: item.entityTypeId,
quantity: rule.quantity ? rule.quantity : 1,
originEntityId: null, //This was added for 2024 Artificer - they could be used for the starting equipment data origin
originEntityTypeId: null, //This was added for 2024 Artificer - they could be used for the starting equipment data origin
});
}
break;
@@ -673,9 +679,9 @@ class StartingEquipment extends React.PureComponent<Props, State> {
diceValues = DiceUtils.getDiceValuesRange(wealthDice);
}
const diceOptions = diceValues.map((value) => ({
const diceOptions: Array<HtmlSelectOption> = diceValues.map((value) => ({
value,
label: value,
label: String(value),
}));
let totalGold = this.getStartingGoldTotal();
@@ -691,10 +697,13 @@ class StartingEquipment extends React.PureComponent<Props, State> {
</div>
<div className="starting-equipment-gold-entry-input">
<Select
className={styles.select}
placeholder={`${diceCount}d${diceValue}`}
options={diceOptions}
onChange={this.handleGoldRolledNumberChange}
value={rolledGoldTotal}
name="starting-gold-rolled"
searchThreshold={null}
/>
</div>
<div className="starting-equipment-gold-entry-multiplier">
@@ -1,6 +1,6 @@
import React from "react";
import { Checkbox, Select } from "@dndbeyond/character-components/es";
import { Checkbox } from "@dndbeyond/character-components/es";
import {
BaseItemDefinitionContract,
Constants,
@@ -13,9 +13,11 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { HtmlContent } from "~/components/HtmlContent";
import { Select } from "~/components/Select";
import { TypeScriptUtils } from "../../../utils";
import { StartingEquipmentRuleSlotSelection } from "../typings";
import styles from "./styles.module.css";
interface Props {
isMultiSlot: boolean;
@@ -235,13 +237,14 @@ export default class StartingEquipmentRuleSlot extends React.PureComponent<Props
contentNode = (
<div className="starting-equipment-rule">
<Select
className={styles.select}
options={groupedOptions}
value={null}
preventClickPropagating={true}
onChange={this.handleRuleSelection.bind(
this,
ruleIdx
)}
name={`${name}-rule-${ruleIdx}`}
/>
</div>
);
@@ -1,63 +0,0 @@
import React, { useContext, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import {
rulesEngineSelectors,
FeatureManager,
} from "@dndbeyond/character-rules-engine/es";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
import { PaneInitFailureContent } from "~/subApps/sheet/components/Sidebar/components/PaneInitFailureContent";
import { PaneIdentifiersBlessing } from "~/subApps/sheet/components/Sidebar/types";
import { Snippet } from "~/tools/js/smartComponents";
import { CharacterFeaturesManagerContext } from "../../../managers/CharacterFeaturesManagerContext";
interface Props {
identifiers: PaneIdentifiersBlessing | null;
}
const BlessingPane: React.FC<Props> = ({ identifiers }) => {
const { characterFeaturesManager } = useContext(
CharacterFeaturesManagerContext
);
const snippetData = useSelector(rulesEngineSelectors.getSnippetData);
// const ruleData = useSelector(rulesEngineSelectors.getRuleData);
// const abilityLookup = useSelector(rulesEngineSelectors.getAbilityLookup);
// const dataOriginRefData = useSelector(rulesEngineSelectors.getDataOriginRefData);
// const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
const proficiencyBonus = useSelector(
rulesEngineSelectors.getProficiencyBonus
);
const theme = useSelector(rulesEngineSelectors.getCharacterTheme);
const [blessing, setBlessing] = useState<FeatureManager | null>(undefined!);
useEffect(() => {
const id = identifiers?.id ?? null;
if (id) {
const blessing = characterFeaturesManager.getBlessing(id);
setBlessing(blessing);
}
}, [identifiers, characterFeaturesManager]);
if (blessing === null) {
return <PaneInitFailureContent />;
}
return blessing ? (
<div className="ct-blessing-pane" key={blessing.getId()}>
<Header>{blessing.getName()}</Header>
<Snippet
snippetData={snippetData}
proficiencyBonus={proficiencyBonus}
theme={theme}
>
{blessing.getDescription()}
</Snippet>
</div>
) : null;
};
export default BlessingPane;
@@ -1,4 +0,0 @@
import BlessingPane from "./BlessingPane";
export default BlessingPane;
export { BlessingPane };
@@ -1,5 +1,6 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
BaseInventoryContract,
@@ -49,7 +50,9 @@ import { appEnvSelectors } from "../../../selectors";
import { SharedAppState } from "../../../stores/typings";
import { PaneIdentifierUtils } from "../../../utils";
interface Props extends DispatchProp {
// 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> {
spells: Array<Spell>;
spellCasterInfo: SpellCasterInfo;
ruleData: RuleData;
@@ -1,5 +1,6 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
AnySimpleDataType,
@@ -39,7 +40,9 @@ import SpellDetail from "../../../components/SpellDetail";
import { appEnvSelectors } from "../../../selectors";
import { SharedAppState } from "../../../stores/typings";
interface Props extends DispatchProp {
// 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> {
spells: Array<Spell>;
spellCasterInfo: SpellCasterInfo;
ruleData: RuleData;
@@ -203,13 +206,13 @@ class ClassSpellPane extends React.PureComponent<Props, State> {
let expandedDataOriginRef = SpellUtils.getExpandedDataOriginRef(spell);
if (expandedDataOriginRef !== null) {
parentNode = (
<React.Fragment>
<>
{parentNode} &bull;{" "}
{EntityUtils.getDataOriginRefName(
expandedDataOriginRef,
dataOriginRefData
)}
</React.Fragment>
</>
);
}
@@ -1,6 +1,7 @@
import { orderBy } from "lodash";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
rulesEngineSelectors,
@@ -21,7 +22,9 @@ import { SharedAppState } from "../../../stores/typings";
import ConditionManagePaneSpecialConditions from "./ConditionManagePaneSpecialConditions";
import ConditionManagePaneStandardConditions from "./ConditionManagePaneStandardConditions";
interface Props extends DispatchProp {
// 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> {
activeConditions: Array<Condition>;
ruleData: RuleData;
isReadonly: boolean;
@@ -30,7 +30,7 @@ export default class ConditionManagePaneSpecialConditions extends React.PureComp
} = this.props;
return (
<React.Fragment>
<>
{specialConditions.map((condition) => {
let activeCondition = activeConditions.find(
(activeCondition) =>
@@ -53,7 +53,7 @@ export default class ConditionManagePaneSpecialConditions extends React.PureComp
/>
);
})}
</React.Fragment>
</>
);
}
}
@@ -26,7 +26,7 @@ export default class ConditionManagePaneStandardConditions extends React.PureCom
} = this.props;
return (
<React.Fragment>
<>
{standardConditions.map((condition) => {
let isActive = activeConditionIds.includes(
ConditionUtils.getId(condition)
@@ -43,7 +43,7 @@ export default class ConditionManagePaneStandardConditions extends React.PureCom
/>
);
})}
</React.Fragment>
</>
);
}
}
@@ -2,13 +2,10 @@ import { orderBy } from "lodash";
import React from "react";
import { useContext } from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import { Collapsible } from "@dndbeyond/character-components/es";
import {
ApiAdapterPromise,
ApiAdapterRequestConfig,
ApiResponse,
BaseItemDefinitionContract,
CharacterTheme,
Constants,
Container,
@@ -36,6 +33,8 @@ import {
CharacterCurrencyContract,
FormatUtils,
CoinManager,
ItemPlanUtils,
EntityUtils,
} from "@dndbeyond/character-rules-engine/es";
import { EditableName } from "~/components/EditableName";
@@ -67,7 +66,9 @@ import { AppNotificationUtils, PaneIdentifierUtils } from "../../../utils";
import ItemDetailActions from "../../ItemDetailActions";
import { CurrencyErrorTypeEnum } from "../CurrencyPane/CurrencyPaneConstants";
interface Props extends DispatchProp {
// 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> {
weaponSpellDamageGroups: Array<WeaponSpellDamageGroup>;
ruleData: RuleData;
snippetData: SnippetData;
@@ -75,7 +76,6 @@ interface Props extends DispatchProp {
infusionChoiceLookup: InfusionChoiceLookup;
identifiers: PaneIdentifiersContainer | null;
isReadonly: boolean;
proficiencyBonus: number;
theme: CharacterTheme;
containers: Array<Container>;
globalModifiers: Array<Modifier>;
@@ -343,6 +343,21 @@ class ContainerPane extends React.PureComponent<Props, State> {
}
};
handleParentClick = (): void => {
const { item } = this.state;
const { paneHistoryPush } = this.props;
if (item) {
const itemPlan = item.getItemPlan();
if (itemPlan) {
const dataOrigin = ItemPlanUtils.getDataOrigin(itemPlan);
let component = getDataOriginComponentInfo(dataOrigin);
if (component.type !== PaneComponentEnum.ERROR_404) {
paneHistoryPush(component.type, component.identifiers);
}
}
}
};
render() {
const { item, currentContainer, isCustomizeClosed } = this.state;
const {
@@ -352,7 +367,6 @@ class ContainerPane extends React.PureComponent<Props, State> {
entityValueLookup,
infusionChoiceLookup,
isReadonly,
proficiencyBonus,
theme,
containers,
globalModifiers,
@@ -360,7 +374,6 @@ class ContainerPane extends React.PureComponent<Props, State> {
inventory,
identifiers,
partyInfo,
inventoryManager,
} = this.props;
if (currentContainer === null) {
@@ -369,9 +382,12 @@ class ContainerPane extends React.PureComponent<Props, State> {
const shopOpenInitially = identifiers?.showAddItems ?? false;
const canCustomize = item
? inventoryManager.canCustomizeItem(item.item)
: false;
let parentName: string | null = null;
const itemPlan = item ? item.getItemPlan() : null;
if (itemPlan) {
const dataOrigin = ItemPlanUtils.getDataOrigin(itemPlan);
parentName = EntityUtils.getDataOriginName(dataOrigin);
}
return (
<div className="ct-container-pane">
@@ -379,6 +395,8 @@ class ContainerPane extends React.PureComponent<Props, State> {
preview={
<Preview imageUrl={item ? item.getAvatarUrl() : undefined} />
}
parent={parentName}
onClick={this.handleParentClick}
>
{item ? (
<EditableName onClick={this.handleOpenCustomize}>
@@ -411,12 +429,12 @@ class ContainerPane extends React.PureComponent<Props, State> {
entityValueLookup={entityValueLookup}
infusionChoiceLookup={infusionChoiceLookup}
isReadonly={isReadonly}
proficiencyBonus={proficiencyBonus}
showActions={false}
showCustomize={canCustomize}
showCustomize={!isReadonly}
container={currentContainer.container}
isCustomizeClosed={isCustomizeClosed}
onCustomizeClick={this.handleOpenCustomize}
partyInfo={partyInfo}
/>
</Collapsible>
)}
@@ -424,7 +442,6 @@ class ContainerPane extends React.PureComponent<Props, State> {
currentContainer={currentContainer.container}
containers={containers}
ruleData={ruleData}
proficiencyBonus={proficiencyBonus}
theme={theme}
globalModifiers={globalModifiers}
valueLookupByType={valueLookupByType}
@@ -484,7 +501,6 @@ function mapStateToProps(state: SharedAppState) {
snippetData: rulesEngineSelectors.getSnippetData(state),
infusionChoiceLookup: rulesEngineSelectors.getInfusionChoiceLookup(state),
isReadonly: appEnvSelectors.getIsReadonly(state),
proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state),
theme: rulesEngineSelectors.getCharacterTheme(state),
containers: rulesEngineSelectors.getInventoryContainers(state),
globalModifiers: rulesEngineSelectors.getValidGlobalModifiers(state),
@@ -1,7 +1,7 @@
import React, { useContext } from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import { FeatureFlagContext } from "@dndbeyond/character-components/es";
import {
characterActions,
CharacterCurrencyContract,
@@ -15,12 +15,12 @@ import {
serviceDataSelectors,
} from "@dndbeyond/character-rules-engine/es";
import { SettingsButton } from "~/subApps/sheet/components/SettingsButton";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
import { PaneIdentifiersCurrencyContext } from "~/subApps/sheet/components/Sidebar/types";
import CurrencyCollapsible from "../../../../CharacterSheet/components/CurrencyCollapsible";
import Lifestyle from "../../../../CharacterSheet/components/Lifestyle";
import SettingsButton from "../../../../CharacterSheet/components/SettingsButton";
import * as toastActions from "../../../actions/toastMessage/actions";
import { CURRENCY_VALUE } from "../../../constants/App";
import { CoinManagerContext } from "../../../managers/CoinManagerContext";
@@ -29,7 +29,9 @@ import { SharedAppState } from "../../../stores/typings";
import { SettingsContextsEnum } from "../SettingsPane/typings";
import { CurrencyErrorTypeEnum } from "./CurrencyPaneConstants";
interface Props extends DispatchProp {
// 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> {
lifestyle: CharacterLifestyleContract | null;
coin: CharacterCurrencyContract;
ruleData: RuleData;
@@ -136,7 +138,7 @@ class CurrencyPane extends React.PureComponent<Props> {
const { isReadonly, ruleData, lifestyle } = this.props;
return (
<React.Fragment>
<>
<div
role="heading"
aria-level={2}
@@ -159,7 +161,7 @@ class CurrencyPane extends React.PureComponent<Props> {
lifestyle={lifestyle}
ruleData={ruleData}
/>
</React.Fragment>
</>
);
};
@@ -190,86 +192,79 @@ class CurrencyPane extends React.PureComponent<Props> {
);
return (
<FeatureFlagContext.Consumer>
{({ imsFlag }) => (
<div className="ct-currency-pane">
<Header
callout={
<SettingsButton
context={SettingsContextsEnum.COIN}
isReadonly={isReadonly}
/>
}
<div className="ct-currency-pane">
<Header
callout={
!isReadonly && (
<SettingsButton context={SettingsContextsEnum.COIN} />
)
}
>
Manage Coin
</Header>
{partyDefinitionKey ? (
<>
<div
role="heading"
aria-level={2}
className="ct-currency-pane__subheader"
>
Manage Coin
</Header>
{imsFlag &&
coinManager.isSharingTurnedOnOrDeleteOnly() &&
partyDefinitionKey ? (
<React.Fragment>
<div
role="heading"
aria-level={2}
className="ct-currency-pane__subheader"
>
My Coin
</div>
{characterContainer && (
<CurrencyCollapsible
heading={this.deriveCoinLabel(
ContainerUtils.getName(characterContainer)
)}
initiallyCollapsed={
containerDefinitionKeyContext !==
ContainerUtils.getDefinitionKey(characterContainer)
}
isReadonly={isReadonly}
container={characterContainer}
handleCurrencyChangeError={this.handleCurrencyChangeError.bind(
this
)}
handleCurrencyAdjust={this.handleCurrencyAdjust.bind(this)}
handleAmountSet={this.handleAmountSet.bind(this)}
/>
My Coin
</div>
{characterContainer && (
<CurrencyCollapsible
heading={this.deriveCoinLabel(
ContainerUtils.getName(characterContainer)
)}
<Lifestyle
isReadonly={isReadonly}
handleLifestyleUpdate={this.handleLifestyleUpdate.bind(this)}
lifestyle={lifestyle}
ruleData={ruleData}
/>
<div
role="heading"
aria-level={2}
className="ct-currency-pane__subheader"
>
Party Coin
</div>
{partyContainer && (
<CurrencyCollapsible
heading={this.deriveCoinLabel(
ContainerUtils.getName(partyContainer)
)}
initiallyCollapsed={
containerDefinitionKeyContext !==
ContainerUtils.getDefinitionKey(partyContainer)
}
isReadonly={isReadonly}
container={partyContainer}
handleCurrencyChangeError={this.handleCurrencyChangeError.bind(
this
)}
handleCurrencyAdjust={this.handleCurrencyAdjust.bind(this)}
handleAmountSet={this.handleAmountSet.bind(this)}
/>
initiallyCollapsed={
containerDefinitionKeyContext !==
ContainerUtils.getDefinitionKey(characterContainer)
}
isReadonly={isReadonly}
container={characterContainer}
handleCurrencyChangeError={this.handleCurrencyChangeError.bind(
this
)}
</React.Fragment>
) : (
characterContainer && this.renderCharacterCoin(characterContainer)
handleCurrencyAdjust={this.handleCurrencyAdjust.bind(this)}
handleAmountSet={this.handleAmountSet.bind(this)}
/>
)}
</div>
<Lifestyle
isReadonly={isReadonly}
handleLifestyleUpdate={this.handleLifestyleUpdate.bind(this)}
lifestyle={lifestyle}
ruleData={ruleData}
/>
<div
role="heading"
aria-level={2}
className="ct-currency-pane__subheader"
>
Party Coin
</div>
{partyContainer && (
<CurrencyCollapsible
heading={this.deriveCoinLabel(
ContainerUtils.getName(partyContainer)
)}
initiallyCollapsed={
containerDefinitionKeyContext !==
ContainerUtils.getDefinitionKey(partyContainer)
}
isReadonly={isReadonly}
container={partyContainer}
handleCurrencyChangeError={this.handleCurrencyChangeError.bind(
this
)}
handleCurrencyAdjust={this.handleCurrencyAdjust.bind(this)}
handleAmountSet={this.handleAmountSet.bind(this)}
/>
)}
</>
) : (
characterContainer && this.renderCharacterCoin(characterContainer)
)}
</FeatureFlagContext.Consumer>
</div>
);
}
@@ -297,39 +292,72 @@ class CurrencyPane extends React.PureComponent<Props> {
const containerDefinitionKeyContext =
identifiers?.containerDefinitionKeyContext;
return (
<FeatureFlagContext.Consumer>
{({ imsFlag }) => (
<div className="ct-currency-pane">
<Header
callout={
<SettingsButton
context={SettingsContextsEnum.COIN}
isReadonly={isReadonly}
/>
<div className="ct-currency-pane">
<Header
callout={
!isReadonly && (
<SettingsButton context={SettingsContextsEnum.COIN} />
)
}
>
Manage Coin
</Header>
<div
role="heading"
aria-level={2}
className="ct-currency-pane__subheader"
>
My Coin
</div>
{characterContainers.map((characterContainer) =>
ContainerUtils.getCoin(characterContainer) ? (
<CurrencyCollapsible
heading={this.deriveCoinLabel(
ContainerUtils.getName(characterContainer)
)}
key={ContainerUtils.getDefinitionKey(characterContainer)}
initiallyCollapsed={
containerDefinitionKeyContext !==
ContainerUtils.getDefinitionKey(characterContainer)
}
>
Manage Coin
</Header>
isReadonly={isReadonly}
container={characterContainer}
handleCurrencyChangeError={this.handleCurrencyChangeError.bind(
this
)}
handleCurrencyAdjust={this.handleCurrencyAdjust.bind(this)}
handleAmountSet={this.handleAmountSet.bind(this)}
/>
) : null
)}
<Lifestyle
isReadonly={isReadonly}
handleLifestyleUpdate={this.handleLifestyleUpdate.bind(this)}
lifestyle={lifestyle}
ruleData={ruleData}
/>
{partyDefinitionKey && (
<>
<div
role="heading"
aria-level={2}
className="ct-currency-pane__subheader"
>
My Coin
Party Coin
</div>
{characterContainers.map((characterContainer) =>
ContainerUtils.getCoin(characterContainer) ? (
{partyContainers.map((partyContainer) =>
ContainerUtils.getCoin(partyContainer) ? (
<CurrencyCollapsible
heading={this.deriveCoinLabel(
ContainerUtils.getName(characterContainer)
ContainerUtils.getName(partyContainer)
)}
key={ContainerUtils.getDefinitionKey(characterContainer)}
key={ContainerUtils.getDefinitionKey(partyContainer)}
initiallyCollapsed={
containerDefinitionKeyContext !==
ContainerUtils.getDefinitionKey(characterContainer)
ContainerUtils.getDefinitionKey(partyContainer)
}
isReadonly={isReadonly}
container={characterContainer}
container={partyContainer}
handleCurrencyChangeError={this.handleCurrencyChangeError.bind(
this
)}
@@ -338,51 +366,9 @@ class CurrencyPane extends React.PureComponent<Props> {
/>
) : null
)}
<Lifestyle
isReadonly={isReadonly}
handleLifestyleUpdate={this.handleLifestyleUpdate.bind(this)}
lifestyle={lifestyle}
ruleData={ruleData}
/>
{imsFlag &&
coinManager.isSharingTurnedOnOrDeleteOnly() &&
partyDefinitionKey && (
<>
<div
role="heading"
aria-level={2}
className="ct-currency-pane__subheader"
>
Party Coin
</div>
{partyContainers.map((partyContainer) =>
ContainerUtils.getCoin(partyContainer) ? (
<CurrencyCollapsible
heading={this.deriveCoinLabel(
ContainerUtils.getName(partyContainer)
)}
key={ContainerUtils.getDefinitionKey(partyContainer)}
initiallyCollapsed={
containerDefinitionKeyContext !==
ContainerUtils.getDefinitionKey(partyContainer)
}
isReadonly={isReadonly}
container={partyContainer}
handleCurrencyChangeError={this.handleCurrencyChangeError.bind(
this
)}
handleCurrencyAdjust={this.handleCurrencyAdjust.bind(
this
)}
handleAmountSet={this.handleAmountSet.bind(this)}
/>
) : null
)}
</>
)}
</div>
</>
)}
</FeatureFlagContext.Consumer>
</div>
);
}
@@ -1,6 +1,3 @@
import AddIcon from "@mui/icons-material/Add";
import CloseIcon from "@mui/icons-material/Close";
import RemoveIcon from "@mui/icons-material/Remove";
import React, { useContext } from "react";
import { Button } from "@dndbeyond/character-components/es";
@@ -9,6 +6,9 @@ import {
CoinManager,
Constants,
} from "@dndbeyond/character-rules-engine/es";
import RemoveIcon from "@dndbeyond/fontawesome-cache/svgs/sharp-regular/minus.svg";
import AddIcon from "@dndbeyond/fontawesome-cache/svgs/sharp-regular/plus.svg";
import CloseIcon from "@dndbeyond/fontawesome-cache/svgs/sharp-regular/xmark.svg";
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
@@ -16,6 +16,7 @@ import { ThemeButton } from "../../../../components/common/Button";
import { CURRENCY_VALUE } from "../../../../constants/App";
import { CoinManagerContext } from "../../../../managers/CoinManagerContext";
import CurrencyPaneAdjusterType from "../CurrencyPaneAdjusterType";
import styles from "./styles.module.css";
interface Props {
onAdjust?: (
@@ -184,7 +185,7 @@ class CurrencyPaneAdjuster extends React.PureComponent<
<div className="ct-currency-pane__adjuster-action ct-currency-pane__adjuster-action--positive">
<Button size="medium" onClick={this.handleAdd}>
<span className="ct-currency-pane__adjuster-actions-button-content">
<AddIcon sx={{ width: "13px", height: "13px" }} />
<AddIcon className={styles.icon} />
Add
</span>
</Button>
@@ -192,7 +193,7 @@ class CurrencyPaneAdjuster extends React.PureComponent<
<div className="ct-currency-pane__adjuster-action ct-currency-pane__adjuster-action--negative">
<Button size="medium" onClick={this.handleRemove}>
<span className="ct-currency-pane__adjuster-actions-button-content">
<RemoveIcon sx={{ width: "13px", height: "13px" }} />
<RemoveIcon className={styles.icon} />
Remove
</span>
</Button>
@@ -204,7 +205,7 @@ class CurrencyPaneAdjuster extends React.PureComponent<
onClick={this.handleReset}
>
<span className="ct-currency-pane__adjuster-actions-button-content">
<CloseIcon sx={{ width: "13px", height: "13px" }} />
<CloseIcon className={styles.icon} />
Clear
</span>
</ThemeButton>
@@ -1,12 +1,12 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
import { FormatUtils } from "@dndbeyond/character-rules-engine/es";
interface Props {
interface Props extends PropsWithChildren {
propertyKey: string;
className: string;
}
export default class CurrencyPaneEditor extends React.PureComponent<Props> {
export default class CurrencyPaneEditor extends PureComponent<Props> {
static defaultProps = {
className: "",
};
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class CurrencyPaneEditorValue extends React.PureComponent {
export default class CurrencyPaneEditorValue extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-currency-pane__editor-value">
@@ -1,11 +1,11 @@
import React from "react";
import { Select } from "@dndbeyond/character-components/es";
import {
HelperUtils,
HtmlSelectOption,
} from "@dndbeyond/character-rules-engine/es";
import { Select } from "~/components/Select";
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
import CurrencyPaneEditor from "../CurrencyPaneEditor";
@@ -62,7 +62,8 @@ export default class CurrencyPaneSelectEditor extends React.PureComponent<
options={options}
value={value}
onChange={this.handleChange}
isReadonly={isReadonly}
disabled={isReadonly}
name={`currency-select-${propertyKey}`}
/>
</CurrencyPaneEditorValue>
</CurrencyPaneEditor>
@@ -1,5 +1,6 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import { Collapsible } from "@dndbeyond/character-components/es";
import {
@@ -40,7 +41,9 @@ type EditCustomContractProperties = Omit<
"id" | "entityTypeId"
>;
interface Props extends DispatchProp {
// 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> {
actions: Array<Action>;
entityValueLookup: EntityValueLookup;
abilityLookup: AbilityLookup;
@@ -1,7 +1,7 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import { Select } from "@dndbeyond/character-components/es";
import {
rulesEngineSelectors,
Action,
@@ -12,6 +12,7 @@ import {
RuleData,
} from "@dndbeyond/character-rules-engine/es";
import { Select } from "~/components/Select";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
@@ -22,7 +23,9 @@ import { SharedAppState } from "../../../stores/typings";
import { PaneIdentifierUtils } from "../../../utils";
import CustomActionsPaneSummary from "./CustomActionsPaneSummary";
interface Props extends DispatchProp {
// 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> {
actions: Array<Action>;
ruleData: RuleData;
paneContext: PaneInfo;
@@ -124,17 +127,16 @@ class CustomActionsPane extends React.PureComponent<Props> {
<Heading>Add new Actions</Heading>
<Select
options={customActionOptions}
resetAfterChoice={true}
onChange={this.handleActionAdd}
value={null}
name="custom-action-add"
resetAfterChoice
/>
</div>
);
};
render() {
const {} = this.props;
return (
<div className="ct-custom-actions-pane">
<Header>Manage Custom Actions</Header>
@@ -1,5 +1,6 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
Collapsible,
@@ -32,7 +33,9 @@ import { RemoveButton } from "../../../components/common/Button";
import { appEnvSelectors } from "../../../selectors";
import { SharedAppState } from "../../../stores/typings";
interface Props extends DispatchProp {
// 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> {
customSkills: Array<Skill>;
ruleData: RuleData;
identifiers: PaneIdentifiersCustomSkill | null;
@@ -198,8 +201,6 @@ class CustomSkillPane extends React.PureComponent<Props, State> {
const proficiencyLevel = SkillUtils.getProficiencyLevel(skill);
const name = SkillUtils.getName(skill);
const infoItemProps = { role: "listitem", inline: true };
return (
<div className="ct-custom-skill-pane" key={skill.id}>
<Header parent="Skills" onClick={this.handleSkillsManageShow}>
@@ -222,9 +223,7 @@ class CustomSkillPane extends React.PureComponent<Props, State> {
{this.renderCustomize()}
{notes && (
<div className="ct-custom-skill-pane__properties" role="list">
<InfoItem label="Notes" {...infoItemProps}>
{notes}
</InfoItem>
<InfoItem label="Notes">{notes}</InfoItem>
</div>
)}
{description && (
@@ -2,6 +2,7 @@ import axios, { Canceler } from "axios";
import { uniq } from "lodash";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import { LoadingPlaceholder } from "@dndbeyond/character-components/es";
import {
@@ -27,7 +28,10 @@ interface BackdropGroups {
label: string;
backdrops: Array<CharacterBackdropContract>;
}
interface Props extends DispatchProp {
// 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> {
decorationInfo: DecorationInfo;
startingClass: CharClass | null;
}
@@ -171,7 +175,7 @@ class BackdropManager extends React.PureComponent<Props, State> {
});
return (
<React.Fragment>
<>
{startingClassBackdrop && (
<div className="ct-decoration-manager__group">
<Heading>Default</Heading>
@@ -195,7 +199,7 @@ class BackdropManager extends React.PureComponent<Props, State> {
{backdropGroups.map((backdropGroup) =>
this.renderItems(backdropGroup.label, backdropGroup.backdrops)
)}
</React.Fragment>
</>
);
};
@@ -0,0 +1,4 @@
import BackdropManager from "./BackdropManager";
export default BackdropManager;
export { BackdropManager };
@@ -1,13 +1,13 @@
import React from "react";
import { FC, MouseEvent, PropsWithChildren } from "react";
interface Props {
interface Props extends PropsWithChildren {
onClick?: (decorationKey: string) => void;
label: string;
isActive: boolean;
isReadonly: boolean;
decorationKey: string;
}
const CurrentDecorationItem: React.FC<Props> = ({
const CurrentDecorationItem: FC<Props> = ({
label,
onClick,
isActive,
@@ -18,7 +18,7 @@ const CurrentDecorationItem: React.FC<Props> = ({
return (
<div
className="ct-decorate-pane__grid-item"
onClick={(evt: React.MouseEvent) => {
onClick={(evt: MouseEvent) => {
evt.stopPropagation();
if (onClick) {
onClick(decorationKey);
@@ -0,0 +1,4 @@
import CurrentDecorationItem from "./CurrentDecorationItem";
export default CurrentDecorationItem;
export { CurrentDecorationItem };
@@ -1,5 +1,5 @@
import React, { useContext, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useSelector } from "react-redux";
import {
AbilitySummary,
@@ -14,13 +14,12 @@ import {
DecorationUtils,
rulesEngineSelectors,
} from "@dndbeyond/character-rules-engine/es";
import { Dice } from "@dndbeyond/dice";
import { useAbilities } from "~/hooks/useAbilities";
import { useDispatch } from "~/hooks/useDispatch";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
import { appEnvActions } from "../../../actions/appEnv";
import CtaPreferenceManager from "../../../components/CtaPreferenceManager";
import { AttributesManagerContext } from "../../../managers/AttributesManagerContext";
import { characterRollContextSelectors } from "../../../selectors";
@@ -48,7 +47,6 @@ export default function DecoratePane() {
const dispatch = useDispatch();
const decorationInfo = useSelector(rulesEngineSelectors.getDecorationInfo);
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
const diceEnabled = useSelector(appEnvSelectors.getDiceEnabled);
const preferences = useSelector(rulesEngineSelectors.getCharacterPreferences);
const characterRollContext = useSelector(
characterRollContextSelectors.getCharacterRollContext
@@ -63,22 +61,6 @@ export default function DecoratePane() {
);
};
//TODO this is repeated a few times now.. reusable thing? hook?
const handleDiceToggle = (): void => {
const newDiceEnabledSetting: boolean = !diceEnabled;
try {
localStorage.setItem("dice-enabled", newDiceEnabledSetting.toString());
Dice.setEnabled(newDiceEnabledSetting);
} catch (e) {}
dispatch(
appEnvActions.dataSet({
diceEnabled: newDiceEnabledSetting,
})
);
};
const handleClick = (type: SHOP_KEY): void => {
setCurrentShop(type);
};
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, MouseEvent, PropsWithChildren } from "react";
interface Props {
interface Props extends PropsWithChildren {
avatarId: number | null;
avatarName?: string | null;
isSelected?: boolean;
@@ -9,8 +9,8 @@ interface Props {
className?: string;
innerClassName?: string;
}
export default class DecorationPreviewItem extends React.PureComponent<Props> {
handleClick = (evt: React.MouseEvent): void => {
export default class DecorationPreviewItem extends PureComponent<Props> {
handleClick = (evt: MouseEvent): void => {
const { onSelected, avatarId } = this.props;
if (onSelected) {
@@ -2,6 +2,7 @@ import axios, { Canceler } from "axios";
import { uniq } from "lodash";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import { LoadingPlaceholder } from "@dndbeyond/character-components/es";
import {
@@ -26,7 +27,9 @@ interface FrameGroupInfo {
frames: Array<CharacterPortraitFrameContract>;
}
interface Props extends DispatchProp {
// 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> {
decorationInfo: DecorationInfo;
}
interface State {
@@ -167,7 +170,7 @@ class FrameManager extends React.PureComponent<Props, State> {
});
return (
<React.Fragment>
<>
<div className="ct-decoration-manager__group">
<Heading>Default</Heading>
<div className="ct-decoration-manager__list">
@@ -184,7 +187,7 @@ class FrameManager extends React.PureComponent<Props, State> {
{frameGroups.map((frameGroup) =>
this.renderFrames(frameGroup.label, frameGroup.frames)
)}
</React.Fragment>
</>
);
};
@@ -1,6 +1,7 @@
import axios, { Canceler } from "axios";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
ApiRequests,
@@ -25,7 +26,9 @@ import LoadingPlaceholder from "~/tools/js/smartComponents/LoadingPlaceholder";
import DecorationPreviewItem from "../DecorationPreviewItem";
interface Props extends DispatchProp {
// 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> {
decorationInfo: DecorationInfo;
species: Race | null;
handleSelectPortrait?: (portrait: CharacterPortraitContract) => void;
@@ -383,7 +386,7 @@ class PortraitManager extends React.PureComponent<Props, State> {
const otherPortraitLabel: string =
speciesPortraits.length > 0 ? "Other Portraits" : "Portraits";
return (
<React.Fragment>
<>
{this.renderUploadedPortrait()}
{this.renderPortraitUploader()}
{species &&
@@ -393,7 +396,7 @@ class PortraitManager extends React.PureComponent<Props, State> {
speciesPortraits
)}
{this.renderPortraits(otherPortraitLabel, otherPortraits)}
</React.Fragment>
</>
);
};
@@ -1,7 +1,7 @@
import axios, { Canceler } from "axios";
import { uniq } from "lodash";
import { useContext, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useSelector } from "react-redux";
import {
AbilitySummary,
@@ -17,6 +17,7 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { useAbilities } from "~/hooks/useAbilities";
import { useDispatch } from "~/hooks/useDispatch";
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
import { AttributesManagerContext } from "~/tools/js/Shared/managers/AttributesManagerContext";
@@ -1,12 +1,13 @@
import clsx from "clsx";
import { sortBy } from "lodash";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
Collapsible,
DamageTypeIcon,
DataOriginName,
Select,
ComponentConstants,
} from "@dndbeyond/character-components/es";
import {
@@ -29,8 +30,10 @@ import {
CharacterTheme,
} from "@dndbeyond/character-rules-engine/es";
import { Select } from "~/components/Select";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
import formStyles from "~/subApps/builder/styles/form.module.css";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
import { getDataOriginComponentInfo } from "~/subApps/sheet/components/Sidebar/helpers/paneUtils";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
@@ -38,6 +41,7 @@ import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import * as appEnvSelectors from "../../../selectors/appEnv";
import { SharedAppState } from "../../../stores/typings";
import DefenseManagePaneCustomItem from "./DefenseManagePaneCustomItem";
import styles from "./styles.module.css";
const DEFENSE_ADJUSTMENT_TYPE_OPTION = {
RESISTANCE: 1,
@@ -54,7 +58,9 @@ interface AdjustmentGroupInfo {
adjustments: Array<AdjustmentGroupEntry>;
}
interface Props extends DispatchProp {
// 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> {
resistances: Array<DefenseAdjustment>;
immunities: Array<DefenseAdjustment>;
vulnerabilities: Array<DefenseAdjustment>;
@@ -324,7 +330,7 @@ class DefenseManagePane extends React.PureComponent<Props, State> {
{ label: "Vulnerabilities", adjustments: [] },
];
customDefenseAdjustments.forEach((defenseAdjustment, newDefIdx) => {
const { type, adjustmentId, source } = defenseAdjustment;
const { type, adjustmentId } = defenseAdjustment;
let groupIdx: number | null = null;
let definition:
@@ -484,26 +490,30 @@ class DefenseManagePane extends React.PureComponent<Props, State> {
header="Customize"
className="ct-defense-manage-pane__custom"
>
<div className="ct-defense-manage-pane__custom-fields">
<div className="dct-defense-manage-pane__custom-field">
<label className={clsx([formStyles.label, styles.label])}>
Defense Type
</label>
<Select
options={adjustmentTypeOptions}
onChange={this.handleChangeAdjustmentType}
value={adjustmentType}
name="defense-adjustment-type"
/>
{adjustmentType !== null && (
<>
<label className={clsx([formStyles.label, styles.label])}>
Defense Sub-Type
</label>
<Select
options={adjustmentTypeOptions}
onChange={this.handleChangeAdjustmentType}
value={adjustmentType}
options={sortBy(adjustmentSubTypeOptions, "label")}
onChange={this.handleChangeAdjustmentSubType}
resetAfterChoice={true}
placeholder={subTypePlaceholder}
value={null}
name="defense-adjustment-sub-type"
/>
</div>
<div className="ct-defense-manage-pane__custom-field">
{adjustmentType !== null && (
<Select
options={sortBy(adjustmentSubTypeOptions, "label")}
onChange={this.handleChangeAdjustmentSubType}
resetAfterChoice={true}
placeholder={subTypePlaceholder}
value={null}
/>
)}
</div>
</div>
</>
)}
<div className="ct-defense-manage-pane__custom-items">
{this.renderCustomDefenseAdjustmentList()}
</div>
@@ -523,11 +533,11 @@ class DefenseManagePane extends React.PureComponent<Props, State> {
const { resistances, vulnerabilities, immunities } = this.props;
return (
<React.Fragment>
<>
{this.renderDamageAdjustmentGroup("Resistances", resistances)}
{this.renderDamageAdjustmentGroup("Immunities", immunities)}
{this.renderDamageAdjustmentGroup("Vulnerabilities", vulnerabilities)}
</React.Fragment>
</>
);
};
@@ -1,5 +1,6 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
AlignmentContract,
@@ -23,7 +24,9 @@ import DescriptionPaneNumberEditor from "./DescriptionPaneNumberEditor";
import DescriptionPaneSelectEditor from "./DescriptionPaneSelectEditor";
import DescriptionPaneTextEditor from "./DescriptionPaneTextEditor";
interface Props extends DispatchProp {
// 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> {
height: string | null;
weight: number | null;
size: SizeContract | null;
@@ -123,7 +126,7 @@ class DescriptionPane extends React.PureComponent<Props> {
}
return (
<React.Fragment>
<>
<DescriptionPaneEntry>
<DescriptionPaneSelectEditor
label="Alignment"
@@ -157,7 +160,7 @@ class DescriptionPane extends React.PureComponent<Props> {
{lifestyleDescriptionNode}
</DescriptionPaneEntryContent>
</DescriptionPaneEntry>
</React.Fragment>
</>
);
};
@@ -165,7 +168,7 @@ class DescriptionPane extends React.PureComponent<Props> {
const { height, weight, skin, eyes, hair, age, gender } = this.props;
return (
<React.Fragment>
<>
<DescriptionPaneEntry>
<DescriptionPaneTextEditor
label="Hair"
@@ -227,7 +230,7 @@ class DescriptionPane extends React.PureComponent<Props> {
maxLength={128}
/>
</DescriptionPaneEntry>
</React.Fragment>
</>
);
};
@@ -1,12 +1,12 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
import { FormatUtils } from "@dndbeyond/character-rules-engine/es";
interface Props {
interface Props extends PropsWithChildren {
propertyKey: string;
className: string;
}
export default class DescriptionPaneEditor extends React.PureComponent<Props> {
export default class DescriptionPaneEditor extends PureComponent<Props> {
render() {
const { propertyKey, className } = this.props;
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class DescriptionPaneEditorLabel extends React.PureComponent {
export default class DescriptionPaneEditorLabel extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-description-pane__editor-label">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class DescriptionPaneEditorValue extends React.PureComponent {
export default class DescriptionPaneEditorValue extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-description-pane__editor-value">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class DescriptionPaneEntry extends React.PureComponent {
export default class DescriptionPaneEntry extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-description-pane__entry">{this.props.children}</div>
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class DescriptionPaneEntryContent extends React.PureComponent {
export default class DescriptionPaneEntryContent extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-description-pane__entry-content">
@@ -1,11 +1,12 @@
import React from "react";
import { Select } from "@dndbeyond/character-components/es";
import {
HelperUtils,
HtmlSelectOption,
} from "@dndbeyond/character-rules-engine/es";
import { Select } from "~/components/Select";
import DescriptionPaneEditor from "../DescriptionPaneEditor";
import DescriptionPaneEditorLabel from "../DescriptionPaneEditorLabel";
import DescriptionPaneEditorValue from "../DescriptionPaneEditorValue";
@@ -56,11 +57,11 @@ export default class DescriptionPaneSelectEditor extends React.PureComponent<
<DescriptionPaneEditorLabel>{label}</DescriptionPaneEditorLabel>
<DescriptionPaneEditorValue>
<Select
className="ct-description-pane__editor-input"
placeholder="--"
options={options}
value={value}
onChange={this.handleChange}
name={propertyKey}
/>
</DescriptionPaneEditorValue>
</DescriptionPaneEditor>
@@ -9,10 +9,10 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { NumberDisplay } from "~/components/NumberDisplay";
import { SettingsButton } from "~/subApps/sheet/components/SettingsButton";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
import SettingsButton from "../../../../CharacterSheet/components/SettingsButton";
import { appEnvSelectors } from "../../../selectors";
import { SharedAppState } from "../../../stores/typings";
import { SettingsContextsEnum } from "../SettingsPane/typings";
@@ -30,7 +30,6 @@ interface Props {
}
class EncumbrancePane extends React.PureComponent<Props> {
renderWeight = (label, weight): React.ReactNode => {
const { theme } = this.props;
return (
<div className="ct-encumbrance-pane__weight">
<span className="ct-encumbrance-pane__weight-label">{label}:</span>
@@ -47,14 +46,14 @@ class EncumbrancePane extends React.PureComponent<Props> {
// item weight should be the weight of items without coins
// and coin weight should be the weight of all coins in all equipped containers
return (
<React.Fragment>
<>
<Heading>Current Weight Distribution</Heading>
<div className="ct-encumbrance-pane__weights">
{this.renderWeight("Items", itemWeight)}
{!preferences.ignoreCoinWeight &&
this.renderWeight("Coins", coinWeight)}
</div>
</React.Fragment>
</>
);
};
@@ -72,10 +71,9 @@ class EncumbrancePane extends React.PureComponent<Props> {
<div className="ct-encumbrance-pane">
<Header
callout={
<SettingsButton
context={SettingsContextsEnum.ENCUMBRANCE}
isReadonly={isReadonly}
/>
!isReadonly && (
<SettingsButton context={SettingsContextsEnum.ENCUMBRANCE} />
)
}
>
Encumbrance
@@ -84,7 +82,7 @@ class EncumbrancePane extends React.PureComponent<Props> {
<div className="ct-encumbrance-pane__info">
{preferences.encumbranceType ===
Constants.PreferenceEncumbranceTypeEnum.NONE && (
<React.Fragment>
<>
<div className="ct-encumbrance-pane__weights">
{this.renderWeight("Carrying Capacity", carryCapacity)}
{this.renderWeight("Push, Drag, or Lift", pushDragLiftWeight)}
@@ -94,11 +92,11 @@ class EncumbrancePane extends React.PureComponent<Props> {
<p>
Encumbrance rules are not currently applied to this character.
</p>
</React.Fragment>
</>
)}
{preferences.encumbranceType ===
Constants.PreferenceEncumbranceTypeEnum.ENCUMBRANCE && (
<React.Fragment>
<>
<div className="ct-encumbrance-pane__weights">
{this.renderWeight("Carrying Capacity", carryCapacity)}
{this.renderWeight("Push, Drag, or Lift", pushDragLiftWeight)}
@@ -131,11 +129,11 @@ class EncumbrancePane extends React.PureComponent<Props> {
amount it can push, drag, or lift. For a Tiny creature, halve
these weights.
</p>
</React.Fragment>
</>
)}
{preferences.encumbranceType ===
Constants.PreferenceEncumbranceTypeEnum.VARIANT && (
<React.Fragment>
<>
<div className="ct-encumbrance-pane__weights">
{this.renderWeight("Carrying Capacity", carryCapacity)}
{this.renderWeight("Push, Drag, or Lift", pushDragLiftWeight)}
@@ -167,7 +165,7 @@ class EncumbrancePane extends React.PureComponent<Props> {
disadvantage on ability checks, attack rolls, and saving throws
that use Strength, Dexterity, or Constitution.
</p>
</React.Fragment>
</>
)}
</div>
</div>
@@ -14,15 +14,14 @@ import {
rulesEngineSelectors,
BaseItemDefinitionContract,
RuleData,
CampaignUtils,
CharacterTheme,
Container,
ContainerUtils,
InventoryManager,
Constants,
serviceDataSelectors,
PartyInfo,
ItemManager,
ContainerManager,
} from "@dndbeyond/character-rules-engine/es";
import { Button } from "~/components/Button";
@@ -33,23 +32,24 @@ 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 { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { CustomItemCreator } from "../../../components/CustomItemCreator";
import EquipmentShop from "../../../components/EquipmentShop";
import ItemDetail from "../../../components/ItemDetail";
import { ItemSlotManager } from "../../../components/ItemSlotManager";
import { ThemeButtonWithMenu } from "../../../components/common/Button";
import { InventoryManagerContext } from "../../../managers/InventoryManagerContext";
import { SharedAppState } from "../../../stores/typings";
import styles from "./styles.module.css";
interface Props {
loadAvailableItems: (
additionalConfig?: Partial<ApiAdapterRequestConfig>
) => ApiAdapterPromise<ApiResponse<Array<BaseItemDefinitionContract>>>;
ruleData: RuleData;
proficiencyBonus: number;
containers: Array<Container>;
theme: CharacterTheme;
partyInfo: PartyInfo | null;
@@ -59,79 +59,82 @@ interface Props {
class EquipmentManagePane extends React.PureComponent<Props> {
renderInventory = (inventory: Array<ItemManager>): React.ReactNode => {
const {
theme,
ruleData,
proficiencyBonus,
inventoryManager,
partyInfo,
containers,
} = this.props;
const { theme, ruleData, inventoryManager, partyInfo, containers } =
this.props;
return inventory.map((item) => {
return inventory.map((item, i) => {
const isContainer = item.isContainer();
const container = inventoryManager.getContainer(
item.generateContainerDefinitionKey()
);
// to ensure key is unique even if two items are the same, append index
// the index helps in re-renders when items are added/removed/moved around
const itemKey = item.getUniqueKey() + i;
const canMove = inventoryManager.canMoveItem(item.item);
const canDelete = inventoryManager.canRemoveItem(item.item);
const calloutButton: React.ReactNode = isContainer ? (
<Popover
trigger={
<Button size="xx-small" variant="outline" themed>
Delete
</Button>
}
position="bottomRight"
data-testid="remove-container-button"
maxWidth={250}
>
<PopoverContent
title={`Remove ${item.getName()}?`}
content={`Removing the ${item.getName()} will also remove all of its ${
container && container.hasInfusions() ? "infusions and " : " "
} contents.`}
confirmText="Delete"
onConfirm={() => item.handleRemove()}
withCancel
/>
</Popover>
) : canMove ? (
<ThemeButtonWithMenu
showSingleOption={true}
containerEl={
document.querySelector(".ct-sidebar__portal") as HTMLElement
}
groupedOptions={ContainerUtils.getGroupedOptions(
item.getContainerDefinitionKey(),
containers,
"Move To:",
partyInfo
? CampaignUtils.getSharingState(partyInfo)
: Constants.PartyInventorySharingStateEnum.OFF
)}
buttonStyle="outline"
onSelect={(containerDefinitionKey) =>
item.handleMove({ containerDefinitionKey })
}
>
Move
</ThemeButtonWithMenu>
) : (
<Button
onClick={() => item.handleRemove()}
size="xx-small"
variant="outline"
themed
>
Delete
</Button>
);
//if the item is a container - render the delete button withPopover only if it "canDelete"
//if the item is not a container - render the move button if it "canMove" otherwise render the delete button if it "canDelete"
//if neither - render null;
let calloutButton: React.ReactNode = null;
if (isContainer && canDelete) {
calloutButton = (
<Popover
trigger={
<Button size="xx-small" variant="outline" themed>
Delete
</Button>
}
position="bottomRight"
data-testid="remove-container-button"
maxWidth={250}
>
<PopoverContent
title={`Remove ${item.getName()}?`}
content={`Removing the ${item.getName()} will also remove all of its ${
container && container.hasInfusions() ? "infusions and " : " "
} contents.`}
confirmText="Delete"
onConfirm={() => item.handleRemove()}
withCancel
/>
</Popover>
);
} else if (!isContainer && canMove) {
calloutButton = (
<ButtonWithMenu
showSingleOption={true}
placement="bottom-end"
variant="outline"
groupedOptions={ContainerUtils.getGroupedOptions(
item.getContainerDefinitionKey(),
containers,
"Move To:"
)}
onSelect={(containerDefinitionKey) =>
item.handleMove({ containerDefinitionKey })
}
>
Move
</ButtonWithMenu>
);
} else if (!isContainer && canDelete) {
calloutButton = (
<Button
onClick={() => item.handleRemove()}
size="xx-small"
variant="outline"
themed
>
Delete
</Button>
);
}
return (
<Collapsible
key={item.getUniqueKey()}
key={itemKey}
className={`ct-equipment-manage-pane__item${
item.isContainer()
? " ct-equipment-manage-pane__item--is-container"
@@ -158,7 +161,6 @@ class EquipmentManagePane extends React.PureComponent<Props> {
}
}}
useTooltip={false}
showEmptySlot={!isContainer}
/>
</div>
<div
@@ -186,22 +188,24 @@ class EquipmentManagePane extends React.PureComponent<Props> {
ruleData={ruleData}
showCustomize={false}
showImage={false}
proficiencyBonus={proficiencyBonus}
partyInfo={partyInfo}
/>
</Collapsible>
);
});
};
renderContainers = (): React.ReactNode => {
const { theme, inventoryManager } = this.props;
renderContainers = (containers: ContainerManager[]): React.ReactNode => {
return containers.map((container) => {
const isEquipmentContainer =
container.isCharacterContainer() || container.isPartyContainer();
return inventoryManager.getAllContainers().map((container) => {
const containerInventory = container.getInventoryItems({
includeContainer: true,
}).items;
const inventoryLength = containerInventory.length;
const inventoryLength =
containerInventory.length - (isEquipmentContainer ? 0 : 1); //don't count the container item itself if its an item and not the character/party container
let isEmpty: boolean = inventoryLength === 0;
let headerNode: React.ReactNode = (
@@ -239,11 +243,11 @@ class EquipmentManagePane extends React.PureComponent<Props> {
render() {
const {
ruleData,
proficiencyBonus,
containers,
theme,
partyInfo,
paneHistoryPush,
inventoryManager,
} = this.props;
return (
@@ -255,13 +259,18 @@ class EquipmentManagePane extends React.PureComponent<Props> {
containers={containers}
theme={theme}
ruleData={ruleData}
proficiencyBonus={proficiencyBonus}
/>
<CustomItemCreator containers={containers} />
</Collapsible>
{/*container collapsibles*/}
{this.renderContainers()}
<Heading className={styles.containerHeading}>My Inventory</Heading>
{this.renderContainers(inventoryManager.getCharacterContainers())}
{inventoryManager.getPartyContainers().length > 0 && (
<Heading className={styles.containerHeading}>Party Inventory</Heading>
)}
{this.renderContainers(inventoryManager.getPartyContainers())}
<div className="ct-equipment-manage-pane__links">
<div className="ct-equipment-manage-pane__link">
@@ -299,7 +308,6 @@ class EquipmentManagePane extends React.PureComponent<Props> {
function mapStateToProps(state: SharedAppState) {
return {
ruleData: rulesEngineSelectors.getRuleData(state),
proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state),
containers: rulesEngineSelectors.getInventoryContainers(state),
theme: rulesEngineSelectors.getCharacterTheme(state),
partyInfo: serviceDataSelectors.getPartyInfo(state),
@@ -1,9 +1,7 @@
import { FC, HTMLAttributes, useEffect, useRef, useState } from "react";
import {
LoadingPlaceholder,
ThemedExportSvg,
} from "@dndbeyond/character-components/es";
import { LoadingPlaceholder } from "@dndbeyond/character-components/es";
import PdfSvg from "@dndbeyond/fontawesome-cache/svgs/light/file-pdf.svg";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { usePdfExport } from "~/hooks/usePdfExport";
@@ -44,9 +42,10 @@ export const ExportPdfPane: FC<Props> = ({ ...props }) => {
return (
<div className="ct-export-pdf-pane" {...props}>
<div className="ct-export-pdf-pane__splash-icon">
<ThemedExportSvg theme={characterTheme} />
</div>
<PdfSvg
className="ddbc-svg ddbc-svg--themed ct-export-pdf-pane__splash-icon"
style={{ fill: characterTheme.themeColor }}
/>
<Header>PDF Generated</Header>
<div className="ct-export-pdf-pane__url">
<input
@@ -3,13 +3,13 @@ import axios, { Canceler } from "axios";
import { orderBy } from "lodash";
import React, { useContext } from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import { v4 as uuidv4 } from "uuid";
import {
Checkbox,
Collapsible,
LoadingPlaceholder,
Select,
} from "@dndbeyond/character-components/es";
import {
CharacterTheme,
@@ -30,6 +30,7 @@ import {
import { HtmlContent } from "~/components/HtmlContent";
import { Reference } from "~/components/Reference";
import { Select } from "~/components/Select";
import { useExtras } from "~/hooks/useExtras";
import { CreatureBlock } from "~/subApps/sheet/components/CreatureBlock";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
@@ -48,6 +49,7 @@ import {
} from "../../../utils";
import ExtraManagePaneAddListing from "./ExtraManagePaneAddListing";
import ExtraManagePaneCurrentListing from "./ExtraManagePaneCurrentListing";
import styles from "./styles.module.css";
//TODO: this comes from ExtrasManager types
interface ShoppeState {
@@ -83,7 +85,9 @@ enum DataRetrievalStatusEnum {
LOADED = "LOADED",
}
interface Props extends DispatchProp {
// 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> {
ruleData: RuleData;
pageSize: number;
creatureGroupRulesLookup: CreatureGroupRulesLookup;
@@ -410,10 +414,13 @@ class ExtraManagePane extends React.PureComponent<Props, State> {
<div className="ct-extra-manage-pane__filters-challenge-fields">
<div className="ct-extra-manage-pane__filters-challenge-field">
<Select
className={styles.levelSelect}
value={filterChallengeMin}
options={challengeOptions}
onChange={this.handleChallengeMinChange}
placeholder="--"
name="min-challenge"
searchThreshold={null}
/>
</div>
<div className="ct-extra-manage-pane__filters-challenge-sep">
@@ -421,10 +428,13 @@ class ExtraManagePane extends React.PureComponent<Props, State> {
</div>
<div className="ct-extra-manage-pane__filters-challenge-field">
<Select
className={styles.levelSelect}
value={filterChallengeMax}
options={challengeOptions}
onChange={this.handleChallengeMaxChange}
placeholder="--"
name="max-challenge"
searchThreshold={null}
/>
</div>
</div>
@@ -449,6 +459,7 @@ class ExtraManagePane extends React.PureComponent<Props, State> {
<ExtraManagePaneAddListing
{...listingComponentPropMappingFunc(extra)}
isReadonly={isReadonly}
key={listingComponentPropMappingFunc(extra).key}
/>
))}
</div>
@@ -575,6 +586,7 @@ class ExtraManagePane extends React.PureComponent<Props, State> {
onChange={this.handleSelectedGroupChange}
placeholder="-- Choose a Category --"
disabled={dataStatus === DataRetrievalStatusEnum.LOADING}
name="extra-category"
/>
</div>
{selectedGroup && (
@@ -1,6 +1,5 @@
import React from "react";
import { MarketplaceCta } from "@dndbeyond/character-components/es";
import {
CharacterTheme,
Creature,
@@ -9,6 +8,7 @@ import {
VehicleManager,
} from "@dndbeyond/character-rules-engine/es";
import { MarketplaceCta } from "~/components/MarketplaceCta";
import { Reference } from "~/components/Reference";
import { CreatureBlock } from "~/subApps/sheet/components/CreatureBlock";
@@ -1,22 +1,22 @@
import React from "react";
import { ReactNode, FC, PropsWithChildren } from "react";
import {
Collapsible,
CollapsibleHeaderContent,
} from "@dndbeyond/character-components/es";
interface Props {
headerCallout: React.ReactNode;
heading: React.ReactNode;
metaItems: Array<React.ReactNode>;
interface Props extends PropsWithChildren {
headerCallout: ReactNode;
heading: ReactNode;
metaItems: Array<ReactNode>;
}
export const ExtraManagePaneListingExtra: React.FC<Props> = ({
export const ExtraManagePaneListingExtra: FC<Props> = ({
headerCallout,
heading,
metaItems,
children,
}) => {
let header: React.ReactNode = (
let header: ReactNode = (
<CollapsibleHeaderContent
heading={heading}
metaItems={metaItems}
@@ -3,11 +3,11 @@ import { orderBy } from "lodash";
import * as React from "react";
import { useContext } from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
CreaturePreview,
ItemPreview,
MarketplaceCta,
LoadingPlaceholder,
} from "@dndbeyond/character-components/es";
import {
@@ -42,10 +42,12 @@ import {
ClassUtils,
BaseCharClass,
ContainerManager,
ContainerLookup,
} from "@dndbeyond/character-rules-engine/es";
import { InventoryManager } from "@dndbeyond/character-rules-engine/es";
import { HtmlContent } from "~/components/HtmlContent";
import { MarketplaceCta } from "~/components/MarketplaceCta";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
@@ -72,7 +74,9 @@ enum UiViewEnum {
type SimpleOnClickHandler = () => void;
interface Props extends DispatchProp {
// 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> {
infusionChoices: Array<InfusionChoice>;
inventory: Array<Item>;
inventoryLookup: InventoryLookup;
@@ -87,7 +91,6 @@ interface Props extends DispatchProp {
identifiers: PaneIdentifiersInfusionChoice;
isReadonly: boolean;
proficiencyBonus: number;
maximumInfusions?: number;
currentInfusions?: number;
characterId: number;
@@ -95,6 +98,8 @@ interface Props extends DispatchProp {
inventoryManager: InventoryManager;
classes: Array<BaseCharClass>;
paneHistoryPush: PaneInfo["paneHistoryPush"];
containerLookup: ContainerLookup;
partyInventoryLookup: InventoryLookup;
}
interface StateData {
infusionChoice: InfusionChoice | null;
@@ -408,7 +413,13 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
};
handleInfusionRemove = (): void => {
const { dispatch, inventoryLookup } = this.props;
const {
dispatch,
inventoryLookup,
partyInventoryLookup,
inventoryManager,
containerLookup,
} = this.props;
const { infusionChoice } = this.state;
if (infusionChoice === null) {
@@ -424,7 +435,7 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
const inventoryMappingId = InfusionUtils.getInventoryMappingId(infusion);
const item = HelperUtils.lookupDataOrFallback(
inventoryLookup,
{ ...inventoryLookup, ...partyInventoryLookup },
inventoryMappingId
);
@@ -434,7 +445,12 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
dispatch(
serviceDataActions.infusionMappingDestroy(
infusionId,
InfusionUtils.getInventoryMappingId(infusion)
InfusionUtils.getInventoryMappingId(infusion),
item
? inventoryManager.handleAcceptOnSuccess(
ItemUtils.isShared(item, containerLookup)
)
: undefined
)
);
}
@@ -624,8 +640,7 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
renderUiChooseItem = (): React.ReactNode => {
const { infusionChoice } = this.state;
const { inventory, ruleData, proficiencyBonus, theme, inventoryManager } =
this.props;
const { inventory, ruleData, theme, inventoryManager } = this.props;
if (infusionChoice === null) {
return null;
@@ -653,7 +668,7 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
}
return (
<React.Fragment>
<>
<Header
parent={this.renderInfusionChoiceName()}
onClick={this.handleChangeUiView.bind(this, UiViewEnum.REVIEW)}
@@ -683,7 +698,6 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
infusionChoice={infusionChoice}
ruleData={ruleData}
onItemSelected={this.handleChooseExistingItem}
proficiencyBonus={proficiencyBonus}
/>
</div>
</div>
@@ -703,7 +717,7 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
</div>
</div>
</div>
</React.Fragment>
</>
);
};
@@ -742,7 +756,7 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
}
return (
<React.Fragment>
<>
<Header
parent={this.renderInfusionChoiceName()}
onClick={this.handleChangeUiView.bind(this, UiViewEnum.REVIEW)}
@@ -784,7 +798,7 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
</div>
</div>
</div>
</React.Fragment>
</>
);
};
@@ -870,10 +884,10 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
}
return (
<React.Fragment>
<>
Infusion: {InfusionUtils.getName(simulatedInfusion)}
{extraNameNode}
</React.Fragment>
</>
);
};
@@ -958,11 +972,7 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
}
} else if (infusionType === Constants.InfusionTypeEnum.REPLICATE) {
itemHeaderNode = "Item to be replicated";
itemContentNode = (
<React.Fragment>
{KnownInfusionUtils.getItemName(knownInfusion)}
</React.Fragment>
);
itemContentNode = <>{KnownInfusionUtils.getItemName(knownInfusion)}</>;
}
const modifierData = InfusionUtils.getModifierData(simulatedInfusion);
@@ -1014,7 +1024,7 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
}
return (
<React.Fragment>
<>
<Header>{this.renderInfusionChoiceName()}</Header>
{currentInfusions >= maximumInfusions && (
<ThemeButton
@@ -1047,7 +1057,7 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
)}
{!isReadonly && this.renderActions()}
{this.renderDescription()}
</React.Fragment>
</>
);
};
@@ -1087,8 +1097,14 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
renderInfusedUi = (): React.ReactNode => {
const { infusionChoice } = this.state;
const { ruleData, creatureLookup, inventoryLookup, isReadonly, theme } =
this.props;
const {
ruleData,
creatureLookup,
inventoryLookup,
isReadonly,
theme,
partyInventoryLookup,
} = this.props;
if (infusionChoice === null) {
return null;
@@ -1101,7 +1117,7 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
const inventoryMappingId = InfusionUtils.getInventoryMappingId(infusion);
const item = HelperUtils.lookupDataOrFallback(
inventoryLookup,
{ ...inventoryLookup, ...partyInventoryLookup },
inventoryMappingId
);
const creatureMappingId = InfusionUtils.getCreatureMappingId(infusion);
@@ -1114,7 +1130,7 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
}
return (
<React.Fragment>
<>
<Header>{this.renderInfusionChoiceName()}</Header>
{item !== null && (
<InfusionChoicePaneStep
@@ -1138,17 +1154,17 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
</div>
)}
{this.renderDescription()}
</React.Fragment>
</>
);
};
renderLoadingUi = (): React.ReactNode => {
return (
<React.Fragment>
<>
<Header>{this.renderInfusionChoiceName()}</Header>
<LoadingPlaceholder />
{this.renderDescription()}
</React.Fragment>
</>
);
};
@@ -1195,7 +1211,6 @@ class InfusionChoicePane extends React.PureComponent<Props, State> {
return (
<div className="ct-infusion-choice-pane__marketplace-cta">
<MarketplaceCta
showImage={false}
sourceName={sourceName}
description="To unlock this infusion, check out the Marketplace to view purchase options."
/>
@@ -1258,10 +1273,11 @@ function mapStateToProps(state: SharedAppState) {
typeValueLookup: rulesEngineSelectors.getCharacterValueLookupByType(state),
ruleData: rulesEngineSelectors.getRuleData(state),
isReadonly: appEnvSelectors.getIsReadonly(state),
proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state),
characterId: rulesEngineSelectors.getId(state),
theme: rulesEngineSelectors.getCharacterTheme(state),
classes: rulesEngineSelectors.getClasses(state),
containerLookup: rulesEngineSelectors.getContainerLookup(state),
partyInventoryLookup: rulesEngineSelectors.getPartyInventoryLookup(state),
};
}
@@ -1,12 +1,12 @@
import * as React from "react";
import { ReactNode, PureComponent, PropsWithChildren, MouseEvent } from "react";
interface Props {
header: React.ReactNode;
extra?: React.ReactNode;
interface Props extends PropsWithChildren {
header: ReactNode;
extra?: ReactNode;
onClick?: () => void;
}
export class InfusionChoicePaneStep extends React.PureComponent<Props> {
handleClick = (evt: React.MouseEvent): void => {
export class InfusionChoicePaneStep extends PureComponent<Props> {
handleClick = (evt: MouseEvent): void => {
const { onClick } = this.props;
if (onClick) {
@@ -30,7 +30,6 @@ interface Props {
infusionChoice: InfusionChoice;
onItemSelected: (item: Item) => void;
ruleData: RuleData;
proficiencyBonus: number;
theme: CharacterTheme;
}
export class InfusionChoicePaneStore extends React.PureComponent<Props> {
@@ -60,7 +59,6 @@ export class InfusionChoicePaneStore extends React.PureComponent<Props> {
items,
onItemSelected,
ruleData,
proficiencyBonus,
theme,
} = this.props;
@@ -133,7 +131,6 @@ export class InfusionChoicePaneStore extends React.PureComponent<Props> {
showActions={false}
showImage={false}
showAbilities={false}
proficiencyBonus={proficiencyBonus}
/>
</Collapsible>
);
@@ -0,0 +1,4 @@
import withAvailableItems from "./withAvailableItems";
export default withAvailableItems;
export { withAvailableItems };
@@ -1,5 +1,5 @@
import axios, { Canceler } from "axios";
import * as React from "react";
import { ComponentProps, ComponentType, JSX, PureComponent } from "react";
import {
CharacterTheme,
@@ -14,7 +14,7 @@ import {
import DataLoadingStatusEnum from "../../../../constants/DataLoadingStatusEnum";
import { AppLoggerUtils } from "../../../../utils";
function getDisplayName(WrappedComponent: React.ComponentType) {
function getDisplayName(WrappedComponent: ComponentType) {
return WrappedComponent.displayName || WrappedComponent.name || "Component";
}
@@ -30,10 +30,10 @@ interface State {
availableItems: Array<Item>;
}
export function withAvailableItems<
C extends React.ComponentType<React.ComponentProps<C>>,
ResolvedProps = JSX.LibraryManagedAttributes<C, React.ComponentProps<C>>
C extends ComponentType<ComponentProps<C>>,
ResolvedProps = JSX.LibraryManagedAttributes<C, ComponentProps<C>>
>(WrappedComponent) {
return class withAvailableItems extends React.PureComponent<
return class withAvailableItems extends PureComponent<
ResolvedProps & RequiredWithAvailableItemsProps,
State
> {
@@ -115,7 +115,7 @@ export function withAvailableItems<
itemLoadingStatus={loadingStatus}
{...(this.props as JSX.LibraryManagedAttributes<
C,
React.ComponentProps<C>
ComponentProps<C>
>)}
/>
);
@@ -1,291 +0,0 @@
import React, { useContext } from "react";
import { connect } from "react-redux";
import {
Action,
ActionUtils,
CharacterTheme,
Constants,
ContainerLookup,
DataOrigin,
EntityValueLookup,
HelperUtils,
Infusion,
InfusionChoiceLookup,
InfusionUtils,
InventoryManager,
Item,
ItemUtils,
PartyInfo,
RuleData,
rulesEngineSelectors,
serviceDataSelectors,
SnippetData,
Spell,
WeaponSpellDamageGroup,
} from "@dndbeyond/character-rules-engine/es";
import { EditableName } from "~/components/EditableName";
import { ItemName } from "~/components/ItemName";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
import { Preview } from "~/subApps/sheet/components/Sidebar/components/Preview";
import {
getDataOriginComponentInfo,
getSpellComponentInfo,
} from "~/subApps/sheet/components/Sidebar/helpers/paneUtils";
import {
PaneComponentEnum,
PaneIdentifiersItem,
} from "~/subApps/sheet/components/Sidebar/types";
import { PaneInitFailureContent } from "../../../../../../subApps/sheet/components/Sidebar/components/PaneInitFailureContent";
import ItemDetail from "../../../components/ItemDetail";
import ItemListInformationCollapsible from "../../../components/ItemListInformationCollapsible";
import { InventoryManagerContext } from "../../../managers/InventoryManagerContext";
import { appEnvSelectors } from "../../../selectors";
import { SharedAppState } from "../../../stores/typings";
import { PaneIdentifierUtils } from "../../../utils";
interface Props {
items: Array<Item>;
weaponSpellDamageGroups: Array<WeaponSpellDamageGroup>;
ruleData: RuleData;
snippetData: SnippetData;
entityValueLookup: EntityValueLookup;
infusionChoiceLookup: InfusionChoiceLookup;
identifiers: PaneIdentifiersItem | null;
isReadonly: boolean;
proficiencyBonus: number;
theme: CharacterTheme;
containerLookup: ContainerLookup;
inventoryManager: InventoryManager;
partyInfo: PartyInfo;
paneHistoryPush: PaneInfo["paneHistoryPush"];
}
interface State {
item: Item | null;
isCustomizeClosed: boolean;
}
class ItemPane extends React.PureComponent<Props, State> {
constructor(props) {
super(props);
this.state = this.generateStateData(props, true);
}
componentDidUpdate(
prevProps: Readonly<Props>,
prevState: Readonly<State>
): void {
const { items, identifiers } = this.props;
if (items !== prevProps.items || identifiers !== prevProps.identifiers) {
this.setState(
this.generateStateData(this.props, prevState.isCustomizeClosed)
);
}
}
generateStateData = (props: Props, isCustomizeClosed: boolean): State => {
const { items, identifiers } = props;
let foundItem: Item | null | undefined = null;
if (identifiers !== null) {
foundItem = items.find(
(item) => identifiers.id === ItemUtils.getMappingId(item)
);
}
return {
item: foundItem ? foundItem : null,
isCustomizeClosed,
};
};
handleCustomItemEdit = (adjustments: Record<string, any>): void => {
const { item } = this.state;
const { inventoryManager } = this.props;
if (item === null) {
return;
}
if (adjustments.notes === "") {
adjustments.notes = null;
}
inventoryManager.handleCustomEdit({
item,
adjustments,
});
};
handleCustomDataUpdate = (
adjustmentType: Constants.AdjustmentTypeEnum,
value: any
): void => {
const { item } = this.state;
const { inventoryManager } = this.props;
if (item) {
inventoryManager.handleCustomizationSet({
item,
adjustmentType,
value,
});
}
};
handleRemoveCustomizations = (): void => {
const { item } = this.state;
const { inventoryManager } = this.props;
if (item) {
inventoryManager.handleCustomizationsRemove({ item });
}
};
handleDataOriginClick = (dataOrigin: DataOrigin) => {
const { paneHistoryPush } = this.props;
let component = getDataOriginComponentInfo(dataOrigin);
if (component.type !== PaneComponentEnum.ERROR_404) {
paneHistoryPush(component.type, component.identifiers);
}
};
handleSpellClick = (spell: Spell): void => {
const { paneHistoryPush } = this.props;
let component = getSpellComponentInfo(spell);
if (component.type) {
paneHistoryPush(component.type, component.identifiers);
}
};
handleInfusionClick = (infusion: Infusion): void => {
const { paneHistoryPush } = this.props;
const choiceKey = InfusionUtils.getChoiceKey(infusion);
if (choiceKey !== null) {
paneHistoryPush(
PaneComponentEnum.INFUSION_CHOICE,
PaneIdentifierUtils.generateInfusionChoice(choiceKey)
);
}
};
handleActionClick = (action: Action): void => {
const { paneHistoryPush } = this.props;
const dataOrigin = ActionUtils.getDataOrigin(action);
let component = getDataOriginComponentInfo(dataOrigin);
if (component.type !== PaneComponentEnum.ERROR_404) {
paneHistoryPush(component.type, component.identifiers);
}
};
handleOpenCustomize = () => {
this.setState({ isCustomizeClosed: !this.state.isCustomizeClosed });
};
render() {
const { item, isCustomizeClosed } = this.state;
const {
weaponSpellDamageGroups,
ruleData,
snippetData,
entityValueLookup,
infusionChoiceLookup,
isReadonly,
proficiencyBonus,
theme,
containerLookup,
inventoryManager,
partyInfo,
} = this.props;
if (item === null) {
return (
<PaneInitFailureContent errorMessage="That item is no longer in your inventory! Please try again." />
);
}
const canCustomize = inventoryManager.canCustomizeItem(item) && !isReadonly;
return (
<div className="ct-item-pane">
<Header preview={<Preview imageUrl={ItemUtils.getAvatarUrl(item)} />}>
<EditableName onClick={this.handleOpenCustomize}>
<ItemName item={item} />
</EditableName>
</Header>
<ItemListInformationCollapsible item={item} ruleData={ruleData} />
<ItemDetail
theme={theme}
key={ItemUtils.getUniqueKey(item)}
item={item}
weaponSpellDamageGroups={weaponSpellDamageGroups}
ruleData={ruleData}
snippetData={snippetData}
onCustomDataUpdate={this.handleCustomDataUpdate}
onCustomizationsRemove={this.handleRemoveCustomizations}
onDataOriginClick={this.handleDataOriginClick}
onSpellClick={this.handleSpellClick}
onInfusionClick={this.handleInfusionClick}
onMasteryActionClick={this.handleActionClick}
entityValueLookup={entityValueLookup}
infusionChoiceLookup={infusionChoiceLookup}
isReadonly={isReadonly}
proficiencyBonus={proficiencyBonus}
container={HelperUtils.lookupDataOrFallback(
containerLookup,
ItemUtils.getContainerDefinitionKey(item)
)}
showCustomize={canCustomize}
onPostRemoveNavigation={PaneComponentEnum.EQUIPMENT_MANAGE}
partyInfo={partyInfo}
onCustomItemEdit={this.handleCustomItemEdit}
isCustomizeClosed={isCustomizeClosed}
onCustomizeClick={this.handleOpenCustomize}
/>
</div>
);
}
}
function mapStateToProps(state: SharedAppState) {
return {
ruleData: rulesEngineSelectors.getRuleData(state),
items: rulesEngineSelectors.getAllInventoryItems(state),
weaponSpellDamageGroups:
rulesEngineSelectors.getWeaponSpellDamageGroups(state),
entityValueLookup:
rulesEngineSelectors.getCharacterValueLookupByEntity(state),
snippetData: rulesEngineSelectors.getSnippetData(state),
infusionChoiceLookup: rulesEngineSelectors.getInfusionChoiceLookup(state),
isReadonly: appEnvSelectors.getIsReadonly(state),
proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state),
theme: rulesEngineSelectors.getCharacterTheme(state),
containerLookup: rulesEngineSelectors.getContainerLookup(state),
partyInfo: serviceDataSelectors.getPartyInfo(state),
};
}
const ItemPaneContainer = (props) => {
const { inventoryManager } = useContext(InventoryManagerContext);
const {
pane: { paneHistoryPush },
} = useSidebar();
return (
<ItemPane
paneHistoryPush={paneHistoryPush}
inventoryManager={inventoryManager}
{...props}
/>
);
};
export default connect(mapStateToProps)(ItemPaneContainer);
@@ -1,4 +0,0 @@
import ItemPane from "./ItemPane";
export default ItemPane;
export { ItemPane };
@@ -1,243 +0,0 @@
import axios, { Canceler } from "axios";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { Checkbox } from "@dndbeyond/character-components/es";
import {
ApiAdapterUtils,
ApiRequests,
rulesEngineSelectors,
characterActions,
Condition,
RuleData,
ShortModelInfoContract,
CharacterTheme,
} from "@dndbeyond/character-rules-engine/es";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
import { RestoreLifeManager } from "~/subApps/sheet/components/Sidebar/panes/HitPointsManagePane/RestoreLifeManager/RestoreLifeManager";
import { toastMessageActions } from "../../../actions/toastMessage";
import { ThemeButton } from "../../../components/common/Button";
import { SharedAppState } from "../../../stores/typings";
import { AppLoggerUtils } from "../../../utils";
interface Props extends DispatchProp {
activeConditions: Array<Condition>;
ruleData: RuleData;
isDead: boolean;
theme: CharacterTheme;
}
interface State {
resetMaxHpModifier: boolean;
adjustConditionLevel: boolean;
restMessage: string | null;
}
class LongRestPane extends React.PureComponent<Props, State> {
loadMessageCanceler: null | Canceler = null;
constructor(props) {
super(props);
this.state = {
resetMaxHpModifier: true,
adjustConditionLevel: false,
restMessage: null,
};
}
componentDidMount() {
this.loadMessage();
}
componentWillUnmount(): void {
if (this.loadMessageCanceler !== null) {
this.loadMessageCanceler();
}
}
loadMessage = (): void => {
if (this.loadMessageCanceler !== null) {
this.loadMessageCanceler();
}
ApiRequests.getCharacterRestLong({
cancelToken: new axios.CancelToken((c) => {
this.loadMessageCanceler = c;
}),
})
.then((response) => {
let message = ApiAdapterUtils.getResponseData(response);
if (message !== null) {
this.setState({
restMessage: message,
});
}
this.loadMessageCanceler = null;
})
.catch(AppLoggerUtils.handleAdhocApiError);
};
reset = (): void => {
this.setState({
resetMaxHpModifier: true,
});
};
handleReset = (): void => {
this.setState({
resetMaxHpModifier: true,
adjustConditionLevel: false,
});
};
handleSave = (): void => {
const { resetMaxHpModifier, adjustConditionLevel } = this.state;
const { dispatch } = this.props;
dispatch(
characterActions.longRest(resetMaxHpModifier, adjustConditionLevel)
);
dispatch(
toastMessageActions.toastSuccess(
"Long Rest Taken",
"You have completed a long rest. Relevant abilities have been reset."
)
);
};
handleResetMaxHpChange = (enabled: boolean): void => {
this.setState({
resetMaxHpModifier: enabled,
});
};
handleAdjustExhaustionLevel = (enabled: boolean): void => {
this.setState({
adjustConditionLevel: enabled,
});
};
handleRestoreToLife = (restoreType: ShortModelInfoContract): void => {
const { dispatch } = this.props;
const restoreChoice = restoreType.name === "Full" ? "full" : "1";
dispatch(characterActions.restoreLife(restoreType.id));
dispatch(
toastMessageActions.toastSuccess(
"Character Restored to Life",
`You have been restored to life with ${restoreChoice} HP.`
)
);
};
renderRecover = (): React.ReactNode => {
const { resetMaxHpModifier, adjustConditionLevel, restMessage } =
this.state;
const { isDead, activeConditions } = this.props;
if (isDead) {
return (
<div className="ct-reset-pane__recover-sources">
Your character is dead
</div>
);
}
const exhaustionIsActive = activeConditions.find(
(condition) => condition.level !== null
);
return (
<React.Fragment>
<div className="ct-reset-pane__recover-sources">
{restMessage === null
? "Asking the server what will be reset..."
: restMessage}
</div>
<div className="ct-reset-pane__recover-max-hp">
<Checkbox
label="Reset Maximum HP changes during this rest"
initiallyEnabled={resetMaxHpModifier}
onChange={this.handleResetMaxHpChange}
/>
</div>
{exhaustionIsActive && (
<div className="ct-reset-pane__reset-exhaustion-level">
<Checkbox
label="Recover 1 Level of Exhaustion during this rest (requires food and drink)"
initiallyEnabled={adjustConditionLevel}
onChange={this.handleAdjustExhaustionLevel}
/>
</div>
)}
</React.Fragment>
);
};
renderActions = (): React.ReactNode => {
const { isDead, ruleData, theme } = this.props;
if (isDead) {
return (
<React.Fragment>
<RestoreLifeManager onSave={this.handleRestoreToLife} />
</React.Fragment>
);
}
return (
<div className="ct-reset-pane__actions">
<div className="ct-reset-pane__action">
<ThemeButton onClick={this.handleSave} enableConfirm={true}>
Take Long Rest
</ThemeButton>
</div>
<div className="ct-reset-pane__action">
<ThemeButton onClick={this.handleReset} style="outline">
Reset
</ThemeButton>
</div>
</div>
);
};
render() {
return (
<div className="ct-reset-pane">
<Header>Long Rest</Header>
<div className="ct-reset-pane__intro">
<p>
A long rest is a period of extended downtime, at least 8 hours long,
during which a character sleeps for at least 6 hours and performs no
more than 2 hours of light activity, such as reading, talking,
eating, or standing watch.
</p>
<p>
If the rest is interrupted by a period of strenuous activity at
least 1 hour of walking, fighting, casting spells, or similar
adventuring activity the characters must begin the rest again to
gain any benefit from it.
</p>
</div>
<div className="ct-reset-pane__recover">
<div className="ct-reset-pane__recover-heading">Recover</div>
{this.renderRecover()}
</div>
{this.renderActions()}
</div>
);
}
}
function mapStateToProps(state: SharedAppState) {
return {
activeConditions: rulesEngineSelectors.getActiveConditions(state),
isDead: rulesEngineSelectors.isDead(state),
ruleData: rulesEngineSelectors.getRuleData(state),
theme: rulesEngineSelectors.getCharacterTheme(state),
};
}
export default connect(mapStateToProps)(LongRestPane);
@@ -1,4 +0,0 @@
import LongRestPane from "./LongRestPane";
export default LongRestPane;
export { LongRestPane };
@@ -1,5 +1,6 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
characterActions,
@@ -17,7 +18,9 @@ import { PaneInitFailureContent } from "../../../../../../subApps/sheet/componen
import { ThemeButton } from "../../../components/common/Button";
import { SharedAppState } from "../../../stores/typings";
interface Props extends DispatchProp {
// 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> {
notes: CharacterNotes;
placeholder: string;
autoSaveDelay: number;
@@ -192,7 +195,7 @@ class NoteManagePane extends React.PureComponent<Props, State> {
if (hasBeenEdited) {
if (isSaveDirty) {
calloutNode = (
<React.Fragment>
<>
<div className="ct-note-manage-pane__status ct-note-manage-pane__status--dirty">
<div className="ct-note-manage-pane__status-text">
Unsaved changes
@@ -201,7 +204,7 @@ class NoteManagePane extends React.PureComponent<Props, State> {
<ThemeButton size="small" onClick={this.handleSave}>
Save
</ThemeButton>
</React.Fragment>
</>
);
} else {
calloutNode = (
@@ -1,5 +1,4 @@
import React, { FC } from "react";
import { useDispatch } from "react-redux";
import {
Constants,
@@ -8,6 +7,7 @@ import {
import { Button } from "~/components/Button";
import { useSidebar } from "~/contexts/Sidebar/Sidebar";
import { useDispatch } from "~/hooks/useDispatch";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
import { PaneInitFailureContent } from "~/subApps/sheet/components/Sidebar/components/PaneInitFailureContent";
import {
@@ -40,7 +40,7 @@ export const PreferencesHitPointConfirmPane: FC<Props> = ({ identifiers }) => {
switch (hitPointType) {
case Constants.PreferenceHitPointTypeEnum.FIXED:
return (
<React.Fragment>
<>
<Header>Fixed Hit Points</Header>
<div className="change-preference-content">
<p>
@@ -49,12 +49,12 @@ export const PreferencesHitPointConfirmPane: FC<Props> = ({ identifiers }) => {
</p>
<p>Any rolled hit point totals will be lost.</p>
</div>
</React.Fragment>
</>
);
case Constants.PreferenceHitPointTypeEnum.MANUAL:
return (
<React.Fragment>
<>
<Header>Manual Hit Points</Header>
<div className="change-preference-content">
<p>
@@ -65,7 +65,7 @@ export const PreferencesHitPointConfirmPane: FC<Props> = ({ identifiers }) => {
rolled values.
</p>
</div>
</React.Fragment>
</>
);
}
@@ -1,5 +1,6 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
characterActions,
@@ -19,7 +20,9 @@ import {
import { SimpleClassSpellList } from "../../../components/SimpleClassSpellList";
import { SharedAppState } from "../../../stores/typings";
interface Props extends DispatchProp {
// 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> {
identifiers: PaneIdentifiersPreferenceOptionalClassFeaturesConfirm | null;
classSpellListSpellsLookup: ClassSpellListSpellsLookup;
paneHistoryStart: PaneInfo["paneHistoryStart"];
@@ -81,7 +84,7 @@ class PreferencesOptionalClassFeaturesConfirmPane extends React.PureComponent<
const { classSpellListSpellsLookup } = this.props;
return (
<React.Fragment>
<>
<Header>
{newIsEnabled ? "Enable" : "Disable"} Optional Class Features
</Header>
@@ -99,7 +102,7 @@ class PreferencesOptionalClassFeaturesConfirmPane extends React.PureComponent<
classSpellListSpellsLookup={classSpellListSpellsLookup}
/>
</div>
</React.Fragment>
</>
);
};
@@ -1,5 +1,6 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
characterActions,
@@ -18,7 +19,9 @@ import {
import { SimpleClassSpellList } from "../../../components/SimpleClassSpellList";
import { SharedAppState } from "../../../stores/typings";
interface Props extends DispatchProp {
// 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> {
identifiers: PaneIdentifiersPreferenceOptionalOriginsConfirm | null;
classSpellListSpellsLookup: ClassSpellListSpellsLookup;
paneHistoryStart: PaneInfo["paneHistoryStart"];
@@ -77,7 +80,7 @@ class PreferencesOptionalOriginsConfirmPane extends React.PureComponent<
const { classSpellListSpellsLookup } = this.props;
return (
<React.Fragment>
<>
<Header>
{newIsEnabled ? "Enable" : "Disable"} Customized Origins
</Header>
@@ -95,7 +98,7 @@ class PreferencesOptionalOriginsConfirmPane extends React.PureComponent<
classSpellListSpellsLookup={classSpellListSpellsLookup}
/>
</div>
</React.Fragment>
</>
);
};
@@ -1,7 +1,8 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { useSearchParams } from "react-router-dom";
import { AnyAction } from "redux";
import { PrivacyTypeRadio } from "@dndbeyond/character-components/es";
import {
AnySimpleDataType,
characterActions,
@@ -21,8 +22,9 @@ import {
rulesEngineSelectors,
RaceUtils,
} from "@dndbeyond/character-rules-engine/es";
import { Dice } from "@dndbeyond/dice";
import { CheckboxGroup, CheckboxInfo } from "~/components/CheckboxGroup";
import { PrivacyTypeRadio } from "~/components/PrivacyTypeRadio";
import { SourceCategoryDescription } from "~/constants";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
@@ -31,17 +33,15 @@ import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import config from "../../../../config";
import { appEnvActions } from "../../../actions";
import {
CheckboxInfo,
FormCheckBoxesField,
} from "../../../components/common/FormCheckBoxesField";
import { appEnvSelectors } from "../../../selectors";
import { SharedAppState } from "../../../stores/typings";
import { PaneIdentifierUtils } from "../../../utils";
import PreferencesPaneSelectField from "./PreferencesPaneSelectField";
import { ToggleInfo } from "./PreferencesPaneTogglesField";
import styles from "./styles.module.css";
interface Props extends DispatchProp {
// 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> {
activeSourceCategories: Array<number>;
preferences: CharacterPreferences;
ruleData: RuleData;
@@ -52,10 +52,12 @@ interface Props extends DispatchProp {
classSpellListSpellsLookup: ClassSpellListSpellsLookup;
characterTheme: CharacterTheme;
paneHistoryStart: PaneInfo["paneHistoryStart"];
isVttView: boolean;
}
class PreferencesPane extends React.PureComponent<Props> {
static defaultProps = {
diceEnabled: false,
isVttView: false,
};
handlePreferenceChange = (
@@ -80,6 +82,7 @@ class PreferencesPane extends React.PureComponent<Props> {
handleSourceCategoryChange = (sourceId: number, isActive: boolean): void => {
const { dispatch, activeSourceCategories } = this.props;
let newSourceCats: Array<number> = [];
if (isActive) {
newSourceCats = [...activeSourceCategories, sourceId];
} else {
@@ -208,7 +211,6 @@ class PreferencesPane extends React.PureComponent<Props> {
try {
localStorage.setItem("dice-enabled", newDiceEnabledSetting.toString());
Dice.setEnabled(newDiceEnabledSetting);
} catch (e) {}
dispatch(
@@ -228,8 +230,13 @@ class PreferencesPane extends React.PureComponent<Props> {
};
render() {
const { preferences, activeSourceCategories, ruleData, diceEnabled } =
this.props;
const {
preferences,
activeSourceCategories,
ruleData,
diceEnabled,
isVttView,
} = this.props;
const {
useHomebrewContent,
@@ -237,7 +244,6 @@ class PreferencesPane extends React.PureComponent<Props> {
hitPointType,
progressionType,
abilityScoreDisplayType,
sharingType,
privacyType,
ignoreCoinWeight,
enforceFeatRules,
@@ -288,12 +294,6 @@ class PreferencesPane extends React.PureComponent<Props> {
},
];
const sharingOptions: Array<HtmlSelectOption> = [
{ label: "Full", value: Constants.PreferenceSharingTypeEnum.FULL },
{ label: "Limited", value: Constants.PreferenceSharingTypeEnum.LIMITED },
// {label: 'Stat Block', value: Constants.PreferenceSharingTypeEnum.STAT_BLOCK},
];
let sourceToggles: Array<CheckboxInfo> = [];
let partneredSourceToggles: Array<CheckboxInfo> = [];
@@ -302,10 +302,11 @@ class PreferencesPane extends React.PureComponent<Props> {
return null;
}
const toggle: ToggleInfo = {
const toggle: CheckboxInfo = {
label: `${sourceCategory.name}`,
initiallyEnabled: activeSourceCategories.includes(sourceCategory.id),
onChange: this.handleSourceCategoryChange.bind(this, sourceCategory.id),
onChange: (e) =>
this.handleSourceCategoryChange(sourceCategory.id, e.target.checked),
sortOrder: sourceCategory.sortOrder,
};
@@ -319,96 +320,95 @@ class PreferencesPane extends React.PureComponent<Props> {
return (
<div className="ct-preferences-pane">
<Header>Preferences</Header>
<FormCheckBoxesField
heading="Sources"
<CheckboxGroup
className={styles.checkboxGroup}
title="Sources"
description={SourceCategoryDescription.official}
name="source-categories"
checkboxes={[
...sourceToggles,
{
label: "Homebrew",
initiallyEnabled: useHomebrewContent,
onChange: this.handlePreferenceChange.bind(
this,
"useHomebrewContent"
),
onChange: (e) =>
this.handlePreferenceChange(
"useHomebrewContent",
e.target.checked
),
sortOrder: 0,
},
...sourceToggles,
]}
themed={true}
showAccordion={true}
variant="sidebar"
darkMode={enableDarkMode}
variant="collapse"
accordionDefaultOpen={true}
themed
/>
<FormCheckBoxesField
heading="Partnered Content"
<CheckboxGroup
className={styles.checkboxGroup}
title="Partnered Content"
description={SourceCategoryDescription.partnered}
name="partnered-source-categories"
checkboxes={partneredSourceToggles}
checkUncheckAllEnabled={true}
themed={true}
accordionHeading="Categories"
variant="sidebar"
darkMode={enableDarkMode}
showAccordion={true}
variant="collapse"
themed
/>
<FormCheckBoxesField
heading="Underdark Mode"
<CheckboxGroup
className={styles.checkboxGroup}
title="Underdark Mode"
description="Enables dark mode for this character"
name="enable-dark-mode"
variant="inline"
data-testid="underdark-mode-toggle"
checkboxes={[
{
initiallyEnabled: enableDarkMode,
label: "Enable dark mode",
onChange: this.handlePreferenceChange.bind(
this,
"enableDarkMode"
),
onChange: (e) =>
this.handlePreferenceChange("enableDarkMode", e.target.checked),
},
]}
themed={true}
variant="sidebar"
darkMode={enableDarkMode}
themed
/>
<FormCheckBoxesField
heading="Dice Rolling"
description="Enables digital dice rolling for this character"
checkboxes={[
{
initiallyEnabled: diceEnabled,
label: "Enable Dice Rolling",
onChange: this.handleDiceToggle,
},
]}
themed={true}
variant="sidebar"
darkMode={enableDarkMode}
/>
<FormCheckBoxesField
heading="Optional Features"
description="Allow or restrict optional features for this character."
{!isVttView && (
<CheckboxGroup
className={styles.checkboxGroup}
title="Dice Rolling"
description="Enables digital dice rolling for all characters on this browser"
name="enable-dice-rolling"
variant="inline"
checkboxes={[
{
initiallyEnabled: diceEnabled,
label: "Enable Dice Rolling",
onChange: this.handleDiceToggle,
},
]}
themed
/>
)}
<CheckboxGroup
className={styles.checkboxGroup}
title="Optional Features"
description="Allow or restrict optional features for this character"
name="optional-features"
checkboxes={[
{
label: "Optional Class Features",
initiallyEnabled: enableOptionalClassFeatures,
onChangePromise:
onChangeConfirm:
this.handleOptionalClassFeaturesPreferenceChangePromise,
},
{
label: "Customize Your Origin",
initiallyEnabled: enableOptionalOrigins,
onChangePromise:
onChangeConfirm:
this.handleOptionalOriginsPreferenceChangePromise,
},
]}
themed={true}
variant="sidebar"
darkMode={enableDarkMode}
themed
/>
<PreferencesPaneSelectField
className={styles.selectField}
heading="Advancement Type"
description="Story-based character progression / XP-based character progression"
onChangePromise={this.handleProgressionPreferenceChangePromise}
@@ -418,6 +418,7 @@ class PreferencesPane extends React.PureComponent<Props> {
block={true}
/>
<PreferencesPaneSelectField
className={styles.selectField}
heading="Hit Point Type"
description="When leveling up, increase hit points by the fixed value for your chosen class or manually enter a rolled value"
onChangePromise={this.handleHitPointPreferenceChangePromise}
@@ -427,88 +428,93 @@ class PreferencesPane extends React.PureComponent<Props> {
block={true}
/>
<FormCheckBoxesField
heading="Use Prerequisites"
<CheckboxGroup
className={styles.checkboxGroup}
title="Use Prerequisites"
description="Allow or restrict choices based on rule prerequisites for the following for this character"
name="use-prerequisites"
checkboxes={[
{
label: "Feats",
initiallyEnabled: enforceFeatRules,
onChange: this.handlePreferenceChange.bind(
this,
"enforceFeatRules"
),
onChange: (e) =>
this.handlePreferenceChange(
"enforceFeatRules",
e.target.checked
),
},
{
label: "Multiclass Requirements",
initiallyEnabled: enforceMulticlassRules,
onChange: this.handlePreferenceChange.bind(
this,
"enforceMulticlassRules"
),
onChange: (e) =>
this.handlePreferenceChange(
"enforceMulticlassRules",
e.target.checked
),
},
]}
themed={true}
variant="sidebar"
darkMode={enableDarkMode}
/>
<FormCheckBoxesField
heading="Show level-scaled spells"
<CheckboxGroup
className={styles.checkboxGroup}
title="Show level-scaled spells"
description="Display and highlight available spells to cast with higher level spell slots"
name="show-scaled-spells"
variant="inline"
checkboxes={[
{
initiallyEnabled: showScaledSpells,
label: "Show Level-Scaled Spells",
onChange: this.handlePreferenceChange.bind(
this,
"showScaledSpells"
),
onChange: (e) =>
this.handlePreferenceChange(
"showScaledSpells",
e.target.checked
),
},
]}
themed={true}
variant="sidebar"
darkMode={enableDarkMode}
themed
/>
<PreferencesPaneSelectField
className={styles.selectField}
heading="Encumbrance Type"
description="Use the standard encumbrance rules / Disable the encumbrance display / Use the more detailed rules for encumbrance"
onChange={this.handleIntPreferenceChange.bind(
this,
"encumbranceType"
)}
onChange={(value) =>
this.handleIntPreferenceChange("encumbranceType", value)
}
initialOptionRemoved={true}
options={encumbranceOptions}
initialValue={encumbranceType}
block={true}
/>
<FormCheckBoxesField
heading="Ignore Coin Weight"
<CheckboxGroup
className={styles.checkboxGroup}
title="Ignore Coin Weight"
description="Coins do not count against your total weight carried (50 coins weigh 1 lb.)"
name="ignore-coin-weight"
variant="inline"
checkboxes={[
{
initiallyEnabled: ignoreCoinWeight,
label: "Ignore Coin Weight",
onChange: this.handlePreferenceChange.bind(
this,
"ignoreCoinWeight"
),
onChange: (e) =>
this.handlePreferenceChange(
"ignoreCoinWeight",
e.target.checked
),
},
]}
themed={true}
variant="sidebar"
darkMode={enableDarkMode}
themed
/>
<PreferencesPaneSelectField
className={styles.selectField}
heading="Ability Score/Modifier Display"
description="Reverse the arrangement of ability modifiers and scores"
onChange={this.handleIntPreferenceChange.bind(
this,
"abilityScoreDisplayType"
)}
onChange={(value) =>
this.handleIntPreferenceChange("abilityScoreDisplayType", value)
}
initialOptionRemoved={true}
options={abilityDisplayOptions}
initialValue={abilityScoreDisplayType}
@@ -516,21 +522,20 @@ class PreferencesPane extends React.PureComponent<Props> {
/>
<PrivacyTypeRadio
handleChange={(e) =>
this.handleChangePrivacy(parseInt(e?.target?.value))
}
darkMode={enableDarkMode}
className={styles.privacyTypeRadio}
initialValue={privacyType}
onChange={(e) =>
this.handleChangePrivacy(
HelperUtils.parseInputInt(e?.target?.value)
)
}
themed={true}
variant="sidebar"
/>
<div className="ct-preferences-pane__version">
<div className="ct-preferences-pane__version-label">Version:</div>
<div className="ct-preferences-pane__version-value">
{config.version}
</div>
</div>
<footer className={styles.footer}>
<strong>Version:</strong>
{config.version || "@dndbeyond/character-app@dev"}
</footer>
</div>
);
}
@@ -556,7 +561,15 @@ const PreferencesPaneContainer = (props) => {
const {
pane: { paneHistoryStart },
} = useSidebar();
return <PreferencesPane paneHistoryStart={paneHistoryStart} {...props} />;
const [searchParams] = useSearchParams();
const isVttView = searchParams.get("view") === "vtt";
return (
<PreferencesPane
paneHistoryStart={paneHistoryStart}
isVttView={isVttView}
{...props}
/>
);
};
export default connect(mapStateToProps)(PreferencesPaneContainer);
@@ -1,12 +1,12 @@
import { uniqueId } from "lodash";
import React from "react";
import { Select } from "@dndbeyond/character-components/es";
import {
HelperUtils,
HtmlSelectOption,
} from "@dndbeyond/character-rules-engine/es";
import { Select } from "~/components/Select";
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
interface Props {
@@ -141,12 +141,13 @@ export default class PreferencesPaneSelectField extends React.PureComponent<
<Select
options={options}
onChange={onChange ? this.handleChange : undefined}
onChangePromise={
onChangeConfirm={
onChangePromise ? this.handleChangePromise : undefined
}
value={value}
initialOptionRemoved={initialOptionRemoved}
hidePlaceholderOption={initialOptionRemoved}
placeholder={placeholder}
name={uId}
/>
</div>
</div>
@@ -1,62 +0,0 @@
import { orderBy } from "lodash";
import React from "react";
import { Toggle } from "~/components/Toggle";
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
import { ToggleInfo } from "./typings";
interface Props {
toggles: Array<ToggleInfo>;
heading: React.ReactNode;
description: string;
}
export default class PreferencesPaneTogglesField extends React.PureComponent<Props> {
static defaultProps = {
initiallyEnabled: false,
};
render() {
const { heading, description, toggles } = this.props;
if (!toggles.length) {
return null;
}
const sortedToggles = orderBy(toggles, "sortOrder");
return (
<div className="ct-preferences-pane__field ct-preferences-pane__field--toggle">
<div className="ct-preferences-pane__field-summary">
<Heading className="ct-preferences-pane__field-heading">
{heading}
</Heading>
{description && (
<div className="ct-preferences-pane__field-description">
{description}
</div>
)}
</div>
<div className="ct-preferences-pane__field-toggles">
{sortedToggles.map((toggle, idx) => (
<div className="ct-preferences-pane__field-toggle" key={idx}>
<div className="ct-preferences-pane__field-input">
<Toggle
checked={toggle.initiallyEnabled}
onClick={toggle.onChange}
onChangePromise={toggle.onChangePromise}
color="themed"
aria-label={toggle.label}
/>
</div>
<div className="ct-preferences-pane__field-label">
{toggle.label}
</div>
</div>
))}
</div>
</div>
);
}
}
@@ -1,5 +0,0 @@
import PreferencesPaneTogglesField from "./PreferencesPaneTogglesField";
export * from "./typings";
export default PreferencesPaneTogglesField;
export { PreferencesPaneTogglesField };
@@ -1,12 +1,10 @@
import PreferencesPane from "./PreferencesPane";
import PreferencesPaneSelectField from "./PreferencesPaneSelectField";
import PreferencesPaneToggleField from "./PreferencesPaneToggleField";
import PreferencesPaneTogglesField from "./PreferencesPaneTogglesField";
export default PreferencesPane;
export {
PreferencesPane,
PreferencesPaneSelectField,
PreferencesPaneToggleField,
PreferencesPaneTogglesField,
};
@@ -1,5 +1,4 @@
import { FC } from "react";
import { useDispatch } from "react-redux";
import {
Constants,
@@ -8,6 +7,7 @@ import {
import { Button } from "~/components/Button";
import { useSidebar } from "~/contexts/Sidebar";
import { useDispatch } from "~/hooks/useDispatch";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
import { PaneInitFailureContent } from "~/subApps/sheet/components/Sidebar/components/PaneInitFailureContent";
import {
@@ -1,7 +1,8 @@
import clsx from "clsx";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import { Select } from "@dndbeyond/character-components/es";
import {
characterActions,
CharacterValuesContract,
@@ -19,6 +20,8 @@ import {
ValueUtils,
} from "@dndbeyond/character-rules-engine/es";
import { Select } from "~/components/Select";
import formStyles from "~/subApps/builder/styles/form.module.css";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
@@ -26,6 +29,7 @@ import ProficiencyGroups from "../../../../CharacterSheet/components/Proficiency
import { SharedAppState } from "../../../stores/typings";
import ProficienciesPaneCustomProficiency from "./ProficienciesPaneCustomProficiency";
import ProficienciesPaneExistingProficiency from "./ProficienciesPaneExistingProficiency";
import styles from "./styles.module.css";
export const PROFICIENCY_TYPE = {
ARMOR: 1,
@@ -36,7 +40,9 @@ export const PROFICIENCY_TYPE = {
LANGUAGE_OTHER: 6,
};
interface Props extends DispatchProp {
// 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> {
customProficiencies: Array<CustomProficiencyContract>;
proficiencyGroups: Array<ProficiencyGroup>;
typeValueLookup: TypeValueLookup;
@@ -372,29 +378,31 @@ class ProficienciesPane extends React.PureComponent<Props, State> {
let subtypeOptions = this.getSubtypeOptions();
return (
<React.Fragment>
<>
<Heading>Add New Proficiencies</Heading>
<div className="ct-proficiencies-pane__add">
<div className="ct-proficiencies-pane__add-field ct-proficiencies-pane__add-field--type">
<label className={formStyles.label}>Proficiency Type</label>
<Select
className={styles.select}
options={typeOptions}
onChange={this.handleTypeChange}
value={type}
name="proficiency-type-select"
/>
{subtypeOptions.length > 0 && (
<>
<label className={clsx([formStyles.label, styles.subtype])}>
Available Proficiencies
</label>
<Select
options={typeOptions}
onChange={this.handleTypeChange}
value={type}
className={styles.subtype}
options={subtypeOptions}
onChange={this.handleSubtypeChange}
value={subtype}
name="proficiency-subtype-select"
/>
</div>
{subtypeOptions.length > 0 && (
<div className="ct-proficiencies-pane__add-field ct-proficiencies-pane__add-field--subtype">
<Select
options={subtypeOptions}
onChange={this.handleSubtypeChange}
value={subtype}
initialOptionRemoved={true}
resetAfterChoice={true}
/>
</div>
)}
</div>
</React.Fragment>
</>
)}
</>
);
};
@@ -1,5 +1,6 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import { Collapsible } from "@dndbeyond/character-components/es";
import {
@@ -28,7 +29,9 @@ import * as appEnvSelectors from "../../../selectors/appEnv";
import { SharedAppState } from "../../../stores/typings";
import { SenseManagePaneCustomizeItem } from "./SenseManagePaneCustomizeItem";
interface Props extends DispatchProp {
// 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> {
passivePerception: number | null;
passiveInvestigation: number | null;
passiveInsight: number | null;
@@ -1,5 +1,6 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
characterActions,
@@ -99,7 +100,9 @@ function generateSettingsContexts(
return SETTINGS_CONTEXTS;
}
interface Props extends DispatchProp {
// 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> {
// Figure out if this is right
preferences: CharacterPreferences;
identifiers: PaneIdentifiersSettingsContext;
@@ -1,5 +1,4 @@
import { FC, HTMLAttributes, useEffect, useRef, useState } from "react";
import { useDispatch } from "react-redux";
import {
LoadingPlaceholder,
@@ -12,6 +11,7 @@ import {
} from "@dndbeyond/character-rules-engine";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { useDispatch } from "~/hooks/useDispatch";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
import { ClipboardUtils } from "../../../utils";
@@ -1,6 +1,7 @@
import axios, { Canceler } from "axios";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
Checkbox,
@@ -22,9 +23,12 @@ import {
ShortModelInfoContract,
CharacterTheme,
} from "@dndbeyond/character-rules-engine/es";
import { RollRequest } from "@dndbeyond/dice";
import { IRollContext, RollType } from "@dndbeyond/dice";
import { GameLogContext } from "@dndbeyond/game-log-components";
import { RollTypes } from "@dndbeyond/pocket-dimension-dice/constants";
import {
RollContext,
RollDicePayload,
} from "@dndbeyond/pocket-dimension-dice/types";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
import { RestoreLifeManager } from "~/subApps/sheet/components/Sidebar/panes/HitPointsManagePane/RestoreLifeManager/RestoreLifeManager";
@@ -39,14 +43,16 @@ import {
import { SharedAppState } from "../../../stores/typings";
import { AppLoggerUtils } from "../../../utils";
interface Props extends DispatchProp {
// 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> {
classes: Array<CharClass>;
conModifier: number | null;
ruleData: RuleData;
isDead: boolean;
theme: CharacterTheme;
diceEnabled: boolean;
characterRollContext: IRollContext;
characterRollContext: RollContext;
hitPointInfo: HitPointInfo;
}
interface State {
@@ -224,7 +230,7 @@ class ShortRestPane extends React.PureComponent<Props, State> {
}
return (
<React.Fragment>
<>
<div className="ct-reset-pane__recover-sources">
{restMessage === null
? "Asking the server what will be reset..."
@@ -244,7 +250,7 @@ class ShortRestPane extends React.PureComponent<Props, State> {
/>
)}
</div>
</React.Fragment>
</>
);
};
@@ -270,9 +276,6 @@ class ShortRestPane extends React.PureComponent<Props, State> {
hitPointInfo,
} = this.props;
const [{ messageTargetOptions, defaultMessageTargetOption, userId }] =
this.context;
if (isDead) {
return null;
}
@@ -327,13 +330,11 @@ class ShortRestPane extends React.PureComponent<Props, State> {
ClassUtils.getMappingId(charClass)
] = ref;
}
return ref;
}}
className="ct-reset-pane__hitdie-manager-dice"
>
<DigitalDiceWrapper
useAdvancedMenu={false}
isContextMenuEnabled={false}
diceNotation={DiceUtils.renderDice({
...hitDice,
diceCount:
@@ -345,7 +346,9 @@ class ShortRestPane extends React.PureComponent<Props, State> {
ClassUtils.getMappingId(charClass)
] * conModifier,
})}
onRollResults={(rollRequest: RollRequest) => {
rollCallback={(
rollRequest: RollDicePayload["data"]
) => {
if (
healCharacterOnHitDieRoll &&
rollRequest.rolls[0]?.result?.total
@@ -375,17 +378,12 @@ class ShortRestPane extends React.PureComponent<Props, State> {
};
});
}}
rollType={RollType.Heal}
rollAction={"Hit Dice"}
diceEnabled={diceEnabled}
rollContext={characterRollContext}
rollTargetOptions={
messageTargetOptions
? Object.values(messageTargetOptions?.entities)
: undefined
}
rollTargetDefault={defaultMessageTargetOption}
userId={userId}
rollType={RollTypes.Heal}
action="Hit Dice"
isDiceEnabled={diceEnabled}
entityId={String(characterRollContext.entityId)}
entityType={String(characterRollContext.entityType)}
name={String(characterRollContext.name)}
>
{DiceUtils.renderDice({
...hitDice,
@@ -415,9 +413,9 @@ class ShortRestPane extends React.PureComponent<Props, State> {
if (isDead) {
return (
<React.Fragment>
<>
<RestoreLifeManager onSave={this.handleRestoreToLife} />
</React.Fragment>
</>
);
}
@@ -1,5 +1,6 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
Collapsible,
@@ -38,7 +39,9 @@ import ValueEditor from "../../../components/ValueEditor";
import { appEnvSelectors } from "../../../selectors";
import { SharedAppState } from "../../../stores/typings";
interface Props extends DispatchProp {
// 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> {
skills: Array<Skill>;
ruleData: RuleData;
entityValueLookup: EntityValueLookup;
@@ -168,7 +171,7 @@ class SkillPane extends React.PureComponent<Props, State> {
}
return (
<React.Fragment>
<>
{diceAdjustments.map((diceAdjustment, idx) => {
return (
<DiceAdjustmentSummary
@@ -180,7 +183,7 @@ class SkillPane extends React.PureComponent<Props, State> {
/>
);
})}
</React.Fragment>
</>
);
};
@@ -1,6 +1,7 @@
import { orderBy } from "lodash";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
Collapsible,
@@ -30,7 +31,9 @@ import { appEnvSelectors } from "../../../selectors";
import { SharedAppState } from "../../../stores/typings";
import { PaneIdentifierUtils } from "../../../utils";
interface Props extends DispatchProp {
// 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> {
skills: Array<Skill>;
customSkills: Array<Skill>;
ruleData: RuleData;
@@ -1,7 +1,8 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import { Collapsible, Select } from "@dndbeyond/character-components/es";
import { Collapsible } from "@dndbeyond/character-components/es";
import {
characterActions,
CharacterPreferences,
@@ -19,6 +20,7 @@ import {
import { HtmlContent } from "~/components/HtmlContent";
import { NumberDisplay } from "~/components/NumberDisplay";
import { Select } from "~/components/Select";
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
@@ -27,7 +29,9 @@ import * as appEnvSelectors from "../../../selectors/appEnv";
import { SharedAppState } from "../../../stores/typings";
import SpeedManagePaneCustomizeItem from "./SpeedManagePaneCustomizeItem";
interface Props extends DispatchProp {
// 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> {
movements: SpeedInfo;
customSpeedLookup: CustomSpeedLookup;
customSpeeds: Array<CustomSpeedContract>;
@@ -129,8 +133,9 @@ class SpeedManagePane extends React.PureComponent<Props> {
<Select
options={movementOptions}
onChange={this.handleMovementDisplayChange}
initialOptionRemoved={true}
value={preferences.primaryMovement}
name="movement-display"
hidePlaceholderOption
/>
</Collapsible>
);
@@ -154,7 +159,7 @@ class SpeedManagePane extends React.PureComponent<Props> {
};
render() {
const { movements, customSpeedLookup, ruleData, theme } = this.props;
const { movements, customSpeedLookup, ruleData } = this.props;
return (
<div className="ct-speed-manage-pane">
@@ -1,5 +1,6 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
Background,
@@ -21,7 +22,9 @@ import SuggestionTable from "../../../components/SuggestionTable";
import { ThemeButton } from "../../../components/common/Button";
import { SharedAppState } from "../../../stores/typings";
interface Props extends DispatchProp {
// 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> {
identifiers: PaneIdentifiersTrait;
background: Background | null;
traits: CharacterTraits;
@@ -255,14 +258,14 @@ class TraitPane extends React.PureComponent<Props, State> {
if (hasBeenEdited) {
if (isSaveDirty) {
calloutNode = (
<React.Fragment>
<>
<div className="ct-trait-pane__status ct-trait-pane__status--dirty">
<div className="ct-trait-pane__status-text">Unsaved changes</div>
</div>
<ThemeButton size="small" onClick={this.handleSave}>
Save
</ThemeButton>
</React.Fragment>
</>
);
} else {
calloutNode = (
@@ -1,4 +1,3 @@
import ArrowRightAltIcon from "@mui/icons-material/ArrowRightAlt";
import { has } from "lodash";
import React from "react";
import { connect } from "react-redux";
@@ -6,7 +5,6 @@ import { connect } from "react-redux";
import {
Collapsible,
DisabledLockSvg,
MarketplaceCta,
} from "@dndbeyond/character-components/es";
import {
CharacterTheme,
@@ -19,10 +17,12 @@ import {
VehicleComponentManager,
VehicleManager,
} from "@dndbeyond/character-rules-engine/es";
import LinkIcon from "@dndbeyond/fontawesome-cache/svgs/solid/arrow-up-right-from-square.svg";
import { EditableName } from "~/components/EditableName";
import { HtmlContent } from "~/components/HtmlContent";
import { Link } from "~/components/Link";
import { MarketplaceCta } from "~/components/MarketplaceCta";
import { Reference } from "~/components/Reference";
import { TagGroup } from "~/components/TagGroup";
import { useSidebar } from "~/contexts/Sidebar";
@@ -51,6 +51,7 @@ import {
PaneIdentifierUtils,
TypeScriptUtils,
} from "../../../utils";
import styles from "./styles.module.css";
interface Hack_DataForEditor {
name: string | null;
@@ -512,7 +513,7 @@ class VehiclePane extends React.PureComponent<Props, State> {
vehicle.isSpelljammer() || vehicle.isElementalAirship();
return (
<React.Fragment>
<>
{this.renderCustomize()}
{primaryComponentManageType ===
Constants.VehicleConfigurationPrimaryComponentManageTypeEnum
@@ -533,20 +534,16 @@ class VehiclePane extends React.PureComponent<Props, State> {
{this.renderTags()}
{this.renderDescription()}
<Link
className={styles.link}
href={vehicle.getUrl() ?? ""}
target="_blank"
rel="noopener noreferrer"
style={{
display: "flex",
alignItems: "center",
marginTop: "8px",
color: "primary.main",
}}
>
{vehicle.getDefinitionName()} Details Page <ArrowRightAltIcon />
{vehicle.getDefinitionName()} Details Page{" "}
<LinkIcon className={styles.linkIcon} />
</Link>
{this.renderActions()}
</React.Fragment>
</>
);
};
@@ -566,10 +563,10 @@ class VehiclePane extends React.PureComponent<Props, State> {
);
} else {
return (
<React.Fragment>
<>
<DisabledLockSvg className="ct-vehicle-pane__header-lock" />
{name}
</React.Fragment>
</>
);
}
};