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
@@ -3,7 +3,6 @@ import React from "react";
import {
Collapsible,
DamageTypeIcon,
MarketplaceCta,
AoeTypeIcon,
ComponentConstants,
} from "@dndbeyond/character-components/es";
@@ -33,6 +32,7 @@ import {
import { HtmlContent } from "~/components/HtmlContent";
import { InfoItem } from "~/components/InfoItem";
import { MarketplaceCta } from "~/components/MarketplaceCta";
import { NumberDisplay } from "~/components/NumberDisplay";
import EditorBox from "../EditorBox";
@@ -77,8 +77,6 @@ export const ActionDetail = ({
theme,
inventoryLookup,
}: Props) => {
const infoItemProps = { role: "listItem", inline: true };
const handleRemoveCustomizations = () => {
if (onCustomizationsRemove) {
onCustomizationsRemove();
@@ -250,10 +248,10 @@ export const ActionDetail = ({
if (rangeInfo.range) {
rangeAreas.push(
<React.Fragment>
<>
<NumberDisplay type="distanceInFt" number={rangeInfo.range} />
{rangeInfo.longRange && <span>({rangeInfo.longRange})</span>}
</React.Fragment>
</>
);
}
@@ -263,7 +261,7 @@ export const ActionDetail = ({
aoeType = RuleDataUtils.getAoeType(rangeInfo.aoeType, ruleData);
}
rangeAreas.push(
<React.Fragment>
<>
<NumberDisplay type="distanceInFt" number={rangeInfo.aoeSize} />
{aoeType !== null && (
<span className={styles.rangeShape}>
@@ -278,7 +276,7 @@ export const ActionDetail = ({
/>
</span>
)}
</React.Fragment>
</>
);
}
}
@@ -300,14 +298,12 @@ export const ActionDetail = ({
}
return (
<React.Fragment>
<>
{damageDisplay !== null && (
<InfoItem label="Damage:" {...infoItemProps}>
{damageDisplay}
</InfoItem>
<InfoItem label="Damage:">{damageDisplay}</InfoItem>
)}
{damage.type !== null && damage.type.name !== null && (
<InfoItem label="Damage Type:" {...infoItemProps}>
<InfoItem label="Damage Type:">
<DamageTypeIcon
theme={theme}
type={
@@ -319,7 +315,7 @@ export const ActionDetail = ({
{damage.type.name}
</InfoItem>
)}
</React.Fragment>
</>
);
};
@@ -340,35 +336,35 @@ export const ActionDetail = ({
rangeAreas = getRangedInfoData();
} else {
rangeAreas.push(
<React.Fragment>
<>
<NumberDisplay
type="distanceInFt"
number={ActionUtils.getReach(action)}
/>{" "}
Reach
</React.Fragment>
</>
);
}
return (
<div className={styles.properties} role="list">
{activation !== null && activation.activationType && (
<InfoItem label="Action Type:" {...infoItemProps}>
<InfoItem label="Action Type:">
{ActivationUtils.renderActivation(activation, ruleData)}
</InfoItem>
)}
{attackSubtypeId && (
<InfoItem label="Attack Type:" {...infoItemProps}>
<InfoItem label="Attack Type:">
{ActionUtils.getAttackSubtypeName(action)}
</InfoItem>
)}
{requiresAttackRoll && (
<InfoItem label="To Hit:" {...infoItemProps}>
<InfoItem label="To Hit:">
<NumberDisplay type="signed" number={toHit} />
</InfoItem>
)}
{requiresSavingThrow && (
<InfoItem label="Attack/Save:" {...infoItemProps}>
<InfoItem label="Attack/Save:">
{saveStateId !== null
? RuleDataUtils.getStatNameById(saveStateId, ruleData)
: ""}{" "}
@@ -377,14 +373,14 @@ export const ActionDetail = ({
)}
{renderDamageProperties()}
{requiresAttackRoll && (
<InfoItem label="Stat:" {...infoItemProps}>
<InfoItem label="Stat:">
{statId === null
? "--"
: RuleDataUtils.getStatNameById(statId, ruleData)}
</InfoItem>
)}
{rangeAreas.length > 0 && (
<InfoItem label="Range/Area:" {...infoItemProps}>
<InfoItem label="Range/Area:">
{rangeAreas.map((node, idx) => (
<React.Fragment key={idx}>
{node}
@@ -393,16 +389,8 @@ export const ActionDetail = ({
))}
</InfoItem>
)}
{isProficient && (
<InfoItem label="Proficient:" {...infoItemProps}>
Yes
</InfoItem>
)}
{notes && (
<InfoItem label="Notes:" {...infoItemProps}>
{notes}
</InfoItem>
)}
{isProficient && <InfoItem label="Proficient:">Yes</InfoItem>}
{notes && <InfoItem label="Notes:">{notes}</InfoItem>}
</div>
);
};
@@ -422,22 +410,22 @@ export const ActionDetail = ({
return (
<div className={styles.properties} role="list">
{activation !== null && activation.activationType && (
<InfoItem label="Casting Time:" {...infoItemProps}>
<InfoItem label="Casting Time:">
{ActivationUtils.renderActivation(activation, ruleData)}
</InfoItem>
)}
{attackSubtypeId && (
<InfoItem label="Attack Type:" {...infoItemProps}>
<InfoItem label="Attack Type:">
{ActionUtils.getAttackSubtypeName(action)}
</InfoItem>
)}
{requiresAttackRoll && (
<InfoItem label="To Hit:" {...infoItemProps}>
<InfoItem label="To Hit:">
<NumberDisplay type="signed" number={toHit} />
</InfoItem>
)}
{requiresSavingThrow && (
<InfoItem label="Attack/Save:" {...infoItemProps}>
<InfoItem label="Attack/Save:">
{saveStateId !== null
? RuleDataUtils.getStatNameById(saveStateId, ruleData)
: ""}{" "}
@@ -446,14 +434,14 @@ export const ActionDetail = ({
)}
{renderDamageProperties()}
{requiresAttackRoll && (
<InfoItem label="Stat:" {...infoItemProps}>
<InfoItem label="Stat:">
{statId === null
? "--"
: RuleDataUtils.getStatNameById(statId, ruleData)}
</InfoItem>
)}
{rangeAreas.length > 0 && (
<InfoItem label="Range/Area:" {...infoItemProps}>
<InfoItem label="Range/Area:">
{rangeAreas.map((node, idx) => (
<React.Fragment key={idx}>
{node}
@@ -462,16 +450,8 @@ export const ActionDetail = ({
))}
</InfoItem>
)}
{isProficient && (
<InfoItem label="Proficient:" {...infoItemProps}>
Yes
</InfoItem>
)}
{notes && (
<InfoItem label="Notes:" {...infoItemProps}>
{notes}
</InfoItem>
)}
{isProficient && <InfoItem label="Proficient:">Yes</InfoItem>}
{notes && <InfoItem label="Notes:">{notes}</InfoItem>}
</div>
);
};
@@ -492,30 +472,30 @@ export const ActionDetail = ({
rangeAreas = getRangedInfoData();
} else {
rangeAreas.push(
<React.Fragment>
<>
<NumberDisplay
type="distanceInFt"
number={ActionUtils.getReach(action)}
/>{" "}
Reach
</React.Fragment>
</>
);
}
return (
<div className={styles.properties} role="list">
{activation !== null && activation.activationType && (
<InfoItem label={"Action Type:"} {...infoItemProps}>
<InfoItem label={"Action Type:"}>
{ActivationUtils.renderActivation(activation, ruleData)}
</InfoItem>
)}
{requiresAttackRoll && (
<InfoItem label="To Hit:" {...infoItemProps}>
<InfoItem label="To Hit:">
<NumberDisplay type="signed" number={toHit} />
</InfoItem>
)}
{requiresSavingThrow && (
<InfoItem label="Attack/Save:" {...infoItemProps}>
<InfoItem label="Attack/Save:">
{saveStateId !== null
? RuleDataUtils.getStatNameById(saveStateId, ruleData)
: ""}{" "}
@@ -524,14 +504,14 @@ export const ActionDetail = ({
)}
{renderDamageProperties()}
{requiresAttackRoll && (
<InfoItem label="Stat:" {...infoItemProps}>
<InfoItem label="Stat:">
{statId === null
? "--"
: RuleDataUtils.getStatNameById(statId, ruleData)}
</InfoItem>
)}
{rangeAreas.length > 0 && (
<InfoItem label="Range/Area:" {...infoItemProps}>
<InfoItem label="Range/Area:">
{rangeAreas.map((node, idx) => (
<React.Fragment key={idx}>
{node}
@@ -540,16 +520,8 @@ export const ActionDetail = ({
))}
</InfoItem>
)}
{isProficient && (
<InfoItem label="Proficient:" {...infoItemProps}>
Yes
</InfoItem>
)}
{notes && (
<InfoItem label="Notes:" {...infoItemProps}>
{notes}
</InfoItem>
)}
{isProficient && <InfoItem label="Proficient:">Yes</InfoItem>}
{notes && <InfoItem label="Notes:">{notes}</InfoItem>}
</div>
);
};
@@ -621,7 +593,6 @@ export const ActionDetail = ({
return (
<div className={styles.marketplaceCta}>
<MarketplaceCta
showImage={false}
sourceName={sourceName}
description={
"To unlock this infusion, check out the Marketplace to view purchase options."
@@ -67,9 +67,12 @@ export default function ClassSpellManager({
const isSpellbookSpellcaster = ClassUtils.isSpellbookSpellcaster(charClass);
const isSpellbookEmpty = knownSpellCount === 0 && activeCantripsCount === 0;
const spellCastingLearningStyle = ClassUtils.getSpellCastingLearningStyle(charClass);
const buttonAddText = Constants.SpellCastingLearningStyleAddText[spellCastingLearningStyle];
const removeButtonText = Constants.SpellCastingLearningStyleRemoveText[spellCastingLearningStyle];
const spellCastingLearningStyle =
ClassUtils.getSpellCastingLearningStyle(charClass);
const buttonAddText =
Constants.SpellCastingLearningStyleAddText[spellCastingLearningStyle];
const removeButtonText =
Constants.SpellCastingLearningStyleRemoveText[spellCastingLearningStyle];
const [state, setState] = useState({
collapsedGroups: {
activeSpells:
@@ -138,9 +141,10 @@ export default function ClassSpellManager({
}
}
const spellDisplayListType = spellCastingLearningStyle === Constants.SpellCastingLearningStyle.Prepared
? "Prepared"
: "Known";
const spellDisplayListType =
spellCastingLearningStyle === Constants.SpellCastingLearningStyle.Prepared
? "Prepared"
: "Known";
let spellsNode: React.ReactNode;
let spellsExtraNode: React.ReactNode;
let spellsClsNames: Array<string> = [
@@ -200,21 +204,19 @@ export default function ClassSpellManager({
return (
<div className="ct-class-spell-manager__empty">
{ClassUtils.isSpellbookSpellcaster(charClass) && (
<React.Fragment>
You currently have no prepared spells.
</React.Fragment>
<>You currently have no prepared spells.</>
)}
{ClassUtils.isPreparedSpellcaster(charClass) && (
<React.Fragment>
<>
You currently have no prepared spells. Learn and prepare spells from
your list of available spells below.
</React.Fragment>
</>
)}
{ClassUtils.isKnownSpellcaster(charClass) && (
<React.Fragment>
<>
You currently have no known spells. Learn spells from your list of
available spells below.
</React.Fragment>
</>
)}
</div>
);
@@ -251,7 +253,10 @@ export default function ClassSpellManager({
const { collapsedGroups } = state;
let heading: React.ReactNode = "Prepared Spells";
if (ClassUtils.isKnownSpellcaster(charClass) && spellCastingLearningStyle !== Constants.SpellCastingLearningStyle.Prepared) {
if (
ClassUtils.isKnownSpellcaster(charClass) &&
spellCastingLearningStyle !== Constants.SpellCastingLearningStyle.Prepared
) {
heading = "Known Spells";
}
@@ -307,7 +312,7 @@ export default function ClassSpellManager({
</div>
)}
{!isSpellbookEmpty && (
<React.Fragment>
<>
{renderSpellListSpellStatus()}
<SpellManager
charClassId={ClassUtils.getActiveId(charClass)}
@@ -325,7 +330,7 @@ export default function ClassSpellManager({
showCustomize={false}
proficiencyBonus={proficiencyBonus}
/>
</React.Fragment>
</>
)}
</Collapsible>
);
@@ -6,7 +6,6 @@ import {
CollapsibleHeading,
} from "@dndbeyond/character-components/es";
import {
CampaignUtils,
CharacterTheme,
Container,
ContainerUtils,
@@ -21,6 +20,7 @@ import {
import { ItemName } from "~/components/ItemName";
import { NumberDisplay } from "~/components/NumberDisplay";
import { ButtonWithMenu } from "~/subApps/sheet/components/Sidebar/components/ButtonWithMenu";
import CurrencyCollapsible from "../../../CharacterSheet/components/CurrencyCollapsible";
import { CurrencyErrorTypeEnum } from "../../containers/panes/CurrencyPane/CurrencyPaneConstants";
@@ -30,7 +30,6 @@ import { CustomItemCreator } from "../CustomItemCreator";
import EquipmentShop from "../EquipmentShop";
import ItemDetail from "../ItemDetail";
import { ItemSlotManager } from "../ItemSlotManager";
import { ThemeButtonWithMenu } from "../common/Button";
interface Props {
currentContainer: Container;
@@ -38,7 +37,6 @@ interface Props {
onItemMove?: (item: Item, containerDefinitionKey: string) => void;
onItemEquip?: (item: Item, uses: number) => void;
ruleData: RuleData;
proficiencyBonus: number;
inventory: Array<Item>;
containers: Array<Container>;
shopOpenInitially?: boolean;
@@ -68,7 +66,6 @@ export const ContainerActionsComponent: React.FC<Props> = ({
ruleData,
onItemMove,
onItemEquip,
proficiencyBonus,
inventory,
containers,
shopOpenInitially = false,
@@ -80,17 +77,6 @@ export const ContainerActionsComponent: React.FC<Props> = ({
handleAmountSet,
handleCurrencyChangeError,
}) => {
let showEquipmentShop = true;
if (
ContainerUtils.isShared(currentContainer) &&
partyInfo &&
CampaignUtils.isSharingStateInactive(
CampaignUtils.getSharingState(partyInfo)
)
) {
showEquipmentShop = false;
}
let headerNode: React.ReactNode = (
<CollapsibleHeaderContent
heading={
@@ -109,23 +95,21 @@ export const ContainerActionsComponent: React.FC<Props> = ({
return (
<div className="ct-container-manager">
{showEquipmentShop && (
<Collapsible
header="Add Items"
initiallyCollapsed={!shopOpenInitially}
overrideCollapsed={!shopOpenInitially}
>
<EquipmentShop
limitAddToCurrentContainer={currentContainer}
partyInfo={partyInfo}
containers={containers}
theme={theme}
ruleData={ruleData}
proficiencyBonus={proficiencyBonus}
/>
<CustomItemCreator containers={[currentContainer]} />
</Collapsible>
)}
<Collapsible
header="Add Items"
initiallyCollapsed={!shopOpenInitially}
overrideCollapsed={!shopOpenInitially}
>
<EquipmentShop
limitAddToCurrentContainer={currentContainer}
partyInfo={partyInfo}
containers={containers}
theme={theme}
ruleData={ruleData}
/>
<CustomItemCreator containers={[currentContainer]} />
</Collapsible>
{coinManager.canUseCointainers() && (
<CurrencyCollapsible
heading={`${ContainerUtils.getName(currentContainer)} Coin`}
@@ -179,20 +163,15 @@ export const ContainerActionsComponent: React.FC<Props> = ({
!isReadonly &&
partyInfo &&
inventoryManager.canMoveItem(item) ? (
<ThemeButtonWithMenu
<ButtonWithMenu
variant="outline"
placement="bottom-end"
showSingleOption={true}
containerEl={
document.querySelector(
".ct-sidebar__portal"
) as HTMLElement
}
groupedOptions={ContainerUtils.getGroupedOptions(
ItemUtils.getContainerDefinitionKey(item),
containers,
"Move To:",
CampaignUtils.getSharingState(partyInfo)
"Move To:"
)}
buttonStyle="outline"
onSelect={(definitionKey) => {
if (onItemMove) {
onItemMove(item, definitionKey);
@@ -200,7 +179,7 @@ export const ContainerActionsComponent: React.FC<Props> = ({
}}
>
Move
</ThemeButtonWithMenu>
</ButtonWithMenu>
) : null
}
/>
@@ -212,7 +191,6 @@ export const ContainerActionsComponent: React.FC<Props> = ({
ruleData={ruleData}
showCustomize={false}
showImage={false}
proficiencyBonus={proficiencyBonus}
/>
</Collapsible>
);
@@ -1,7 +1,5 @@
import React from "react";
import { LightLinkOutSvg } from "@dndbeyond/character-components/es";
export enum CtaBannerOptions {
NEW = "New",
BETA = "Beta",
@@ -112,19 +110,6 @@ export default class CtaPreferenceManager extends React.PureComponent<
</span>
</label>
</div>
{linkHref && (
<a
className="ct-cta-preference-manager__link-out"
href={linkHref}
rel="noopener noreferrer"
target="_blank"
>
<span className="ct-cta-preference-manager__link-out-label">
{linkTitle ? linkTitle : "Feedback Forum"}
</span>
<LightLinkOutSvg />
</a>
)}
</div>
);
}
@@ -6,12 +6,13 @@ import {
ContainerUtils,
} from "@dndbeyond/character-rules-engine/es";
import { ButtonWithMenu } from "~/subApps/sheet/components/Sidebar/components/ButtonWithMenu";
import { InventoryManagerContext } from "../../managers/InventoryManagerContext";
import { AppNotificationUtils } from "../../utils";
import CustomizeDataEditor from "../CustomizeDataEditor";
import EditorBox from "../EditorBox";
import SimpleQuantity from "../SimpleQuantity";
import { ThemeButtonWithMenu } from "../common/Button";
interface Props {
containers: Array<Container>;
@@ -76,7 +77,8 @@ export const CustomItemCreator: React.FC<Props> = ({ containers }) => {
/>
</div>
<div className="ct-custom-item-creator__action">
<ThemeButtonWithMenu
<ButtonWithMenu
placement="bottom-end"
onSelect={(containerDefinitionKey) =>
inventoryManager.handleCustomAdd(
{ customItem, containerDefinitionKey },
@@ -86,15 +88,14 @@ export const CustomItemCreator: React.FC<Props> = ({ containers }) => {
groupedOptions={ContainerUtils.getGroupedOptions(
null,
containers,
"Add To:",
inventoryManager.getSharingState()
"Add To:"
)}
>
Add{" "}
{customItem.quantity === 1
? " Item"
: ` ${customItem.quantity} Items`}
</ThemeButtonWithMenu>
</ButtonWithMenu>
</div>
</div>
</EditorBox>
@@ -4,12 +4,14 @@
*/
import React from "react";
import { Checkbox, Select } from "@dndbeyond/character-components/es";
import { Checkbox } from "@dndbeyond/character-components/es";
import {
HelperUtils,
HtmlSelectOption,
} from "@dndbeyond/character-rules-engine/es";
import { Select } from "~/components/Select";
import CustomizeDataEditorProperty from "./CustomizeDataEditorProperty";
import CustomizeDataEditorPropertyLabel from "./CustomizeDataEditorPropertyLabel";
import CustomizeDataEditorPropertyValue from "./CustomizeDataEditorPropertyValue";
@@ -109,6 +111,8 @@ class SelectEditor extends React.PureComponent<
options={options}
value={value}
onChange={this.handleChange}
name={propertyKey}
optionsWidth="x-large"
/>
</CustomizeDataEditorPropertyValue>
<CustomizeDataEditorPropertyLabel>
@@ -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 {
isBlock: boolean;
propertyKey: string;
}
export default class CustomizeDataEditorProperty extends React.PureComponent<Props> {
export default class CustomizeDataEditorProperty extends PureComponent<Props> {
static defaultProps = {
isBlock: false,
};
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class CustomizeDataEditorPropertyLabel extends React.PureComponent {
export default class CustomizeDataEditorPropertyLabel extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-customize-data-editor__property-label">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class CustomizeDataEditorPropertyValue extends React.PureComponent {
export default class CustomizeDataEditorPropertyValue extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-customize-data-editor__property-value">
@@ -48,7 +48,7 @@ export default class DiceAdjustmentSummary extends React.PureComponent<
let abilityNode: React.ReactNode;
if (statId) {
abilityNode = (
<React.Fragment>
<>
on{" "}
<span
className={`ct-dice-adjustment-summary__description--ability ${
@@ -59,16 +59,16 @@ export default class DiceAdjustmentSummary extends React.PureComponent<
>
{RuleDataUtils.getStatNameById(statId, ruleData)}
</span>
</React.Fragment>
</>
);
} else if (rollType === Constants.DiceAdjustmentRollTypeEnum.DEATH_SAVE) {
abilityNode = <React.Fragment>on Death Saves</React.Fragment>;
abilityNode = <>on Death Saves</>;
} else {
if (
!restriction &&
rollType === Constants.DiceAdjustmentRollTypeEnum.SAVE
) {
abilityNode = <React.Fragment>on saves</React.Fragment>;
abilityNode = <>on saves</>;
}
}
@@ -1,10 +1,10 @@
import clsx from "clsx";
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
interface Props {
className?: string,
interface Props extends PropsWithChildren {
className?: string;
}
export default class EditorBox extends React.PureComponent<Props> {
export default class EditorBox extends PureComponent<Props> {
render() {
const { children, className } = this.props;
@@ -27,7 +27,6 @@ interface Props {
pageSize: number;
// CAN THIS GO AS WELL!?
ruleData: RuleData;
proficiencyBonus: number;
containers: Array<Container>;
theme: CharacterTheme;
limitAddToCurrentContainer: Container | null;
@@ -252,7 +251,6 @@ export class EquipmentShop extends React.PureComponent<Props, State> {
const {
pageSize,
ruleData,
proficiencyBonus,
containers,
theme,
limitAddToCurrentContainer,
@@ -291,7 +289,6 @@ export class EquipmentShop extends React.PureComponent<Props, State> {
theme={theme}
item={item}
ruleData={ruleData}
proficiencyBonus={proficiencyBonus}
partyInfo={partyInfo}
/>
);
@@ -338,7 +335,7 @@ export class EquipmentShop extends React.PureComponent<Props, State> {
const sourceCategories = itemData?.sourceCategories || [];
return (
<React.Fragment>
<>
<ItemFilter
filterQuery={filterQuery}
onQueryChange={(value) =>
@@ -360,7 +357,7 @@ export class EquipmentShop extends React.PureComponent<Props, State> {
/>
{this.renderPagedListing(items)}
{this.renderPager(items, totalItems)}
</React.Fragment>
</>
);
};
@@ -5,25 +5,21 @@ import {
CollapsibleHeaderContent,
} from "@dndbeyond/character-components/es";
import {
CampaignUtils,
Container,
ContainerUtils,
CharacterTheme,
HelperUtils,
Item,
ItemUtils,
RuleData,
MenuOption,
PartyInfo,
Constants,
ItemManager,
} from "@dndbeyond/character-rules-engine/es";
import { ItemName } from "~/components/ItemName";
import { ButtonWithMenu } from "~/subApps/sheet/components/Sidebar/components/ButtonWithMenu";
import { AppNotificationUtils } from "../../../utils";
import ItemDetail from "../../ItemDetail";
import { ThemeButton, ThemeButtonWithMenu } from "../../common/Button";
import { ThemeButton } from "../../common/Button";
interface Props {
item: ItemManager;
@@ -31,7 +27,6 @@ interface Props {
minAddAmount: number;
maxAddAmount: number;
ruleData: RuleData;
proficiencyBonus: number;
containers: Array<Container>;
theme: CharacterTheme;
partyInfo: PartyInfo | null;
@@ -135,19 +130,18 @@ export default class EquipmentShopItem extends React.PureComponent<
heading={<ItemName item={item.item} />}
metaItems={metaItems}
callout={
<ThemeButtonWithMenu
<ButtonWithMenu
placement="bottom-end"
variant="outline"
groupedOptions={ContainerUtils.getGroupedOptions(
null,
containers,
"Add To:",
partyInfo
? CampaignUtils.getSharingState(partyInfo)
: Constants.PartyInventorySharingStateEnum.OFF
"Add To:"
)}
onSelect={this.handleAdd}
>
Add
</ThemeButtonWithMenu>
</ButtonWithMenu>
}
/>
);
@@ -160,7 +154,6 @@ export default class EquipmentShopItem extends React.PureComponent<
minAddAmount,
maxAddAmount,
ruleData,
proficiencyBonus,
theme,
containers,
partyInfo,
@@ -182,7 +175,6 @@ export default class EquipmentShopItem extends React.PureComponent<
showCustomize={false}
showActions={false}
showImage={false}
proficiencyBonus={proficiencyBonus}
/>
<div className="ct-equipment-shop__item-actions">
<div className="ct-equipment-shop__item-action">
@@ -223,22 +215,18 @@ export default class EquipmentShopItem extends React.PureComponent<
</div>
</div>
<div className="ct-equipment-shop__item-action">
<ThemeButtonWithMenu
<ButtonWithMenu
placement="bottom-end"
variant="outline"
onSelect={(definitionKey) => this.handleAdd(definitionKey)}
containerEl={
document.querySelector(".ct-sidebar__portal") as HTMLElement
}
groupedOptions={ContainerUtils.getGroupedOptions(
null,
containers,
"Add To:",
partyInfo
? CampaignUtils.getSharingState(partyInfo)
: Constants.PartyInventorySharingStateEnum.OFF
"Add To:"
)}
>
Add {totalAmount === 1 ? " Item" : ` ${totalAmount} Items`}
</ThemeButtonWithMenu>
</ButtonWithMenu>
</div>
</div>
</Collapsible>
@@ -1,16 +1,16 @@
import React from "react";
import { PureComponent, ErrorInfo, ReactNode, PropsWithChildren } from "react";
import { Link } from "~/components/Link";
import config from "../../../config";
import { ErrorUtils } from "../../utils";
interface Props {}
interface Props extends PropsWithChildren {}
interface State {
hasError: boolean;
errorId: string | null;
}
export default class ErrorBoundary extends React.PureComponent<Props, State> {
export default class ErrorBoundary extends PureComponent<Props, State> {
constructor(props) {
super(props);
this.state = {
@@ -25,7 +25,7 @@ export default class ErrorBoundary extends React.PureComponent<Props, State> {
};
}
componentDidCatch(error: Error, componentInfo: React.ErrorInfo) {
componentDidCatch(error: Error, componentInfo: ErrorInfo) {
console.error("ERROR >>>", error);
ErrorUtils.dispatchException(error, {
componentInfo,
@@ -36,7 +36,7 @@ export default class ErrorBoundary extends React.PureComponent<Props, State> {
window.history.back();
};
renderErrorUi = (): React.ReactNode => {
renderErrorUi = (): ReactNode => {
const { errorId } = this.state;
return (
@@ -122,7 +122,7 @@ export const ExtraRow: FC<Props> = ({
const total: number = totalHp + tempHp;
contentNode = (
<React.Fragment>
<>
<span className="ct-extra-row__hp-value ct-extra-row__hp-value--current">
{current}
</span>
@@ -130,7 +130,7 @@ export const ExtraRow: FC<Props> = ({
<span className="ct-extra-row__hp-value ct-extra-row__hp-value--total">
{total}
</span>
</React.Fragment>
</>
);
} else {
contentNode = (
@@ -179,7 +179,7 @@ export const ExtraRow: FC<Props> = ({
let contentNode: React.ReactNode = null;
if (data !== null) {
contentNode = (
<React.Fragment>
<>
<div className="ct-extra-row__speed-value">
<NumberDisplay
type="distanceInFt"
@@ -191,7 +191,7 @@ export const ExtraRow: FC<Props> = ({
{data.movementId !== Constants.MovementTypeEnum.WALK && (
<div className="ct-extra-row__speed-callout">{label}</div>
)}
</React.Fragment>
</>
);
} else {
contentNode = (
@@ -222,10 +222,10 @@ export const ExtraRow: FC<Props> = ({
}
return (
<React.Fragment>
<>
{armorClassText}
{armorClassInfo.showTooltip && renderAdditionalInfoTooltip()}
</React.Fragment>
</>
);
};
@@ -200,9 +200,9 @@ export default class HealthAdjuster extends React.PureComponent<Props, State> {
const { healingAmount, damageAmount } = this.state;
const { hitPointInfo } = this.props;
let healingDisplay: React.ReactText =
let healingDisplay =
healingAmount === null ? "" : Math.max(this.getDirtyHitPointsChange(), 0);
let damageDisplay: React.ReactText =
let damageDisplay =
damageAmount === null
? ""
: Math.abs(Math.min(this.getDirtyHitPointsChange(), 0));
@@ -2,7 +2,7 @@ import axios, { Canceler } from "axios";
import { orderBy } from "lodash";
import * as React from "react";
import { LoadingPlaceholder, Select } from "@dndbeyond/character-components/es";
import { LoadingPlaceholder } from "@dndbeyond/character-components/es";
import {
ApiAdapterPromise,
ApiAdapterRequestConfig,
@@ -32,6 +32,7 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { HtmlContent } from "~/components/HtmlContent";
import { Select } from "~/components/Select";
import DataLoadingStatusEnum from "../../constants/DataLoadingStatusEnum";
import { AppLoggerUtils, TypeScriptUtils } from "../../utils";
@@ -121,9 +122,6 @@ class InfusionChoiceManager extends React.PureComponent<Props, State> {
infusionChoices,
inventoryManager,
loadAvailableInfusions,
globalModifiers,
typeValueLookup,
ruleData,
onDefinitionsLoaded,
} = this.props;
@@ -406,11 +404,12 @@ class InfusionChoiceManager extends React.PureComponent<Props, State> {
contentNode = (
<Select
options={groups}
onChangePromise={this.handleChoiceItemChangePromise.bind(
onChangeConfirm={this.handleChoiceItemChangePromise.bind(
this,
infusionChoice
)}
value={selectedValue}
name="infusion-replicate-item-select"
/>
);
}
@@ -575,12 +574,13 @@ class InfusionChoiceManager extends React.PureComponent<Props, State> {
placeholder={`- Choose a Level ${InfusionChoiceUtils.getLevel(
infusionChoice
)} Option -`}
onChangePromise={onChangePromise.bind(this, choiceKey)}
onChangeConfirm={onChangePromise.bind(this, choiceKey)}
value={
knownInfusionDefinitionKey === null
? null
: DefinitionUtils.getDefinitionKeyId(knownInfusionDefinitionKey)
}
name="infusion-choice-select"
/>
{this.renderInfusionReplicateItemChoice(infusionChoice)}
{warningNode}
@@ -603,11 +603,11 @@ class InfusionChoiceManager extends React.PureComponent<Props, State> {
contentNode = <LoadingPlaceholder />;
} else {
contentNode = (
<React.Fragment>
<>
{infusionChoices.map((infusionChoice) =>
this.renderInfusionChoice(infusionChoice)
)}
</React.Fragment>
</>
);
}
@@ -7,11 +7,11 @@ import {
Damage,
DamageTypeIcon,
DataOriginName,
InfusionPreview,
} from "@dndbeyond/character-components/es";
import {
Action,
ActionUtils,
CampaignUtils,
CharacterTheme,
CharacterUtils,
Constants,
@@ -23,7 +23,7 @@ import {
EntityValueLookup,
FormatUtils,
Infusion,
InfusionChoiceLookup,
InfusionUtils,
InventoryManager,
Item,
ItemUtils,
@@ -41,6 +41,7 @@ import StarIcon from "@dndbeyond/fontawesome-cache/svgs/solid/star.svg";
import { HtmlContent } from "~/components/HtmlContent";
import { InfoItem } from "~/components/InfoItem";
import { InfusionPreview } from "~/components/InfusionPreview";
import { NumberDisplay } from "~/components/NumberDisplay";
import { Reference } from "~/components/Reference";
import { TagGroup } from "~/components/TagGroup";
@@ -56,8 +57,6 @@ import ValueEditor from "../ValueEditor";
import { RemoveButton } from "../common/Button";
import styles from "./styles.module.css";
const infoItemProps = { role: "listitem", inline: true };
interface Props {
item: Item;
weaponSpellDamageGroups: Array<WeaponSpellDamageGroup>;
@@ -80,9 +79,7 @@ interface Props {
onInfusionClick?: (infusion: Infusion) => void;
onMasteryActionClick?: (action: Action) => void;
entityValueLookup: EntityValueLookup;
infusionChoiceLookup: InfusionChoiceLookup;
isReadonly: boolean;
proficiencyBonus: number;
theme: CharacterTheme;
container: Container | null;
onPostRemoveNavigation?: PaneComponentEnum;
@@ -91,6 +88,8 @@ interface Props {
isCustomizeClosed?: boolean;
onCustomizeClick?: (isCollapsed: boolean) => void;
actions: Array<Action>;
onRemoveItem?: () => void;
characterId: number;
}
export class ItemDetail extends React.PureComponent<Props> {
static defaultProps = {
@@ -103,7 +102,6 @@ export class ItemDetail extends React.PureComponent<Props> {
showImage: true,
snippetData: null,
entityValueLookup: {},
infusionChoiceLookup: {},
isReadonly: false,
container: null,
};
@@ -245,7 +243,7 @@ export class ItemDetail extends React.PureComponent<Props> {
}
return (
<InfoItem label="Source:" {...infoItemProps}>
<InfoItem label="Source:">
<Reference
name={
RuleDataUtils.getSourceDataInfo(sourceId, ruleData)?.description ||
@@ -288,7 +286,7 @@ export class ItemDetail extends React.PureComponent<Props> {
} else if (isCustom) {
type = "Custom";
}
//TODO - itemManager exposes item.getIntroText() which should be used in the future to render the intro text and share logic with mobile
return (
<>
<div className="ct-item-detail__intro">
@@ -301,27 +299,26 @@ export class ItemDetail extends React.PureComponent<Props> {
})`}
{ItemUtils.isContainer(item) && ", container"}
</div>
{container && (
<div className="ct-item-detail__intro-subtitle">
{" "}
{!ItemUtils.isContainer(item)
<div className="ct-item-detail__intro-subtitle">
{container &&
(!ItemUtils.isContainer(item)
? ` In ${ContainerUtils.getName(container)}`
: ` In ${
ContainerUtils.isShared(container) ? "Party " : ""
}Inventory`}
<div>
{partyInfo &&
equippedEntityId &&
`${
inventoryManager.isEquippedToCurrentCharacter(item)
? "Equipped"
: `Equipped by ${inventoryManager.getEquippedCharacterName(
item
)}`
}`}
</div>
}Inventory`)}
<div>
{partyInfo &&
equippedEntityId &&
`${
inventoryManager.isEquippedToCurrentCharacter(item)
? "Equipped"
: `Equipped by ${inventoryManager.getEquippedCharacterName(
item
)}`
}`}
</div>
)}
{ItemUtils.isReplicated(item) && <span>Replicated Item</span>}
</div>
</>
);
};
@@ -552,24 +549,20 @@ export class ItemDetail extends React.PureComponent<Props> {
return (
<div className="ct-item-detail__customize" role="list">
<InfoItem label="Weight:" {...infoItemProps}>
<InfoItem label="Weight:">
{weight ? <NumberDisplay type="weightInLb" number={weight} /> : "--"}
</InfoItem>
<InfoItem label="Cost:" {...infoItemProps}>
<InfoItem label="Cost:">
{cost ? `${FormatUtils.renderLocaleNumber(cost)} gp` : "--"}
</InfoItem>
{notes && (
<InfoItem label="Notes:" {...infoItemProps}>
{notes}
</InfoItem>
)}
{notes && <InfoItem label="Notes:">{notes}</InfoItem>}
</div>
);
//`
};
renderGearProperties = (): React.ReactNode => {
const { item, theme } = this.props;
const { item } = this.props;
const bundleSize = ItemUtils.getBundleSize(item);
const isStackable = ItemUtils.isStackable(item);
@@ -581,11 +574,11 @@ export class ItemDetail extends React.PureComponent<Props> {
return (
<div className="ct-item-detail__customize" role="list">
<InfoItem label="Weight:" {...infoItemProps}>
<InfoItem label="Weight:">
{weight ? <NumberDisplay type="weightInLb" number={weight} /> : "--"}
</InfoItem>
{ItemUtils.isContainer(item) && (
<InfoItem label="Capacity:" {...infoItemProps}>
<InfoItem label="Capacity:">
{capacityWeight ? (
<NumberDisplay type="weightInLb" number={capacityWeight} />
) : (
@@ -593,24 +586,14 @@ export class ItemDetail extends React.PureComponent<Props> {
)}
</InfoItem>
)}
<InfoItem label="Cost:" {...infoItemProps}>
<InfoItem label="Cost:">
{cost ? `${FormatUtils.renderLocaleNumber(cost)} gp` : "--"}
</InfoItem>
{isStackable && bundleSize > 1 && (
<InfoItem label="Bundle Size:" {...infoItemProps}>
{bundleSize}
</InfoItem>
)}
{version !== null && (
<InfoItem label="Version:" {...infoItemProps}>
{version}
</InfoItem>
)}
{notes && (
<InfoItem label="Notes:" {...infoItemProps}>
{notes}
</InfoItem>
<InfoItem label="Bundle Size:">{bundleSize}</InfoItem>
)}
{version !== null && <InfoItem label="Version:">{version}</InfoItem>}
{notes && <InfoItem label="Notes:">{notes}</InfoItem>}
{this.getSourceReference(item)}
</div>
);
@@ -662,27 +645,23 @@ export class ItemDetail extends React.PureComponent<Props> {
return (
<div className="ct-item-detail__customize" role="list">
{proficiency && (
<InfoItem label="Proficient: " {...infoItemProps}>
Yes
</InfoItem>
)}
<InfoItem label="Attack Type:" {...infoItemProps}>
{proficiency && <InfoItem label="Proficient: ">Yes</InfoItem>}
<InfoItem label="Attack Type:">
{attackType === null ? "--" : attackTypeName}
</InfoItem>
{reach && (
<InfoItem label="Reach:" {...infoItemProps}>
<InfoItem label="Reach:">
<NumberDisplay type="distanceInFt" number={reach} />
</InfoItem>
)}
{showRange && (
<InfoItem label="Range:" {...infoItemProps}>
<InfoItem label="Range:">
<NumberDisplay type="distanceInFt" number={range} />
/
<NumberDisplay type="distanceInFt" number={longRange} />
</InfoItem>
)}
<InfoItem label="Damage:" {...infoItemProps}>
<InfoItem label="Damage:">
<div>
{damageDisplay}
{versatileDamageNode}
@@ -710,11 +689,11 @@ export class ItemDetail extends React.PureComponent<Props> {
</div>
))}
</InfoItem>
<InfoItem label="Damage Type:" {...infoItemProps}>
<InfoItem label="Damage Type:">
{damageType === null ? (
"--"
) : (
<React.Fragment>
<>
<DamageTypeIcon
theme={theme}
type={
@@ -724,37 +703,27 @@ export class ItemDetail extends React.PureComponent<Props> {
}
/>{" "}
{damageType}
</React.Fragment>
</>
)}
</InfoItem>
<InfoItem label="Weight:" {...infoItemProps}>
<InfoItem label="Weight:">
{weight ? <NumberDisplay type="weightInLb" number={weight} /> : "--"}
</InfoItem>
<InfoItem label="Cost:" {...infoItemProps}>
<InfoItem label="Cost:">
{cost ? `${FormatUtils.renderLocaleNumber(cost)} gp` : "--"}
</InfoItem>
{isStackable && bundleSize > 1 && (
<InfoItem label="Bundle Size:" {...infoItemProps}>
{bundleSize}
</InfoItem>
<InfoItem label="Bundle Size:">{bundleSize}</InfoItem>
)}
{properties && properties.length > 0 && (
<InfoItem label="Properties:" {...infoItemProps}>
<InfoItem label="Properties:">
{properties
.map((prop) => prop.name + (prop.notes ? ` (${prop.notes})` : ""))
.join(", ")}
</InfoItem>
)}
{version !== null && (
<InfoItem label="Version:" {...infoItemProps}>
{version}
</InfoItem>
)}
{notes && (
<InfoItem label="Notes:" {...infoItemProps}>
{notes}
</InfoItem>
)}
{version !== null && <InfoItem label="Version:">{version}</InfoItem>}
{notes && <InfoItem label="Notes:">{notes}</InfoItem>}
{this.getSourceReference(item)}
</div>
);
@@ -762,7 +731,7 @@ export class ItemDetail extends React.PureComponent<Props> {
};
renderArmorProperties = (): React.ReactNode => {
const { item, theme } = this.props;
const { item } = this.props;
const bundleSize = ItemUtils.getBundleSize(item);
const isStackable = ItemUtils.isStackable(item);
@@ -771,6 +740,7 @@ export class ItemDetail extends React.PureComponent<Props> {
const weight = ItemUtils.getWeight(item);
const cost = ItemUtils.getCost(item);
const notes = ItemUtils.getNotes(item);
const proficiency = ItemUtils.hasProficiency(item);
let armorClassDisplay: React.ReactNode = armorClass;
if (ItemUtils.isShield(item)) {
@@ -784,30 +754,19 @@ export class ItemDetail extends React.PureComponent<Props> {
return (
<div className="ct-item-detail__customize" role="list">
<InfoItem label="Armor Class:" {...infoItemProps}>
{armorClassDisplay}
</InfoItem>
<InfoItem label="Weight:" {...infoItemProps}>
{proficiency && <InfoItem label="Proficient: ">Yes</InfoItem>}
<InfoItem label="Armor Class:">{armorClassDisplay}</InfoItem>
<InfoItem label="Weight:">
{weight ? <NumberDisplay type="weightInLb" number={weight} /> : "--"}
</InfoItem>
<InfoItem label="Cost:" {...infoItemProps}>
<InfoItem label="Cost:">
{cost ? `${FormatUtils.renderLocaleNumber(cost)} gp` : "--"}
</InfoItem>
{isStackable && bundleSize > 1 && (
<InfoItem label="Bundle Size:" {...infoItemProps}>
{bundleSize}
</InfoItem>
)}
{version !== null && (
<InfoItem label="Version:" {...infoItemProps}>
{version}
</InfoItem>
)}
{notes && (
<InfoItem label="Notes:" {...infoItemProps}>
{notes}
</InfoItem>
<InfoItem label="Bundle Size:">{bundleSize}</InfoItem>
)}
{version !== null && <InfoItem label="Version:">{version}</InfoItem>}
{notes && <InfoItem label="Notes:">{notes}</InfoItem>}
{this.getSourceReference(item)}
</div>
);
@@ -958,25 +917,27 @@ export class ItemDetail extends React.PureComponent<Props> {
};
renderInfusionPreview = (): React.ReactNode => {
const {
item,
snippetData,
ruleData,
infusionChoiceLookup,
proficiencyBonus,
} = this.props;
const { item, partyInfo, characterId } = this.props;
const infusion = ItemUtils.getInfusion(item);
if (infusion && snippetData && infusionChoiceLookup) {
if (infusion) {
const itemMappingId = InfusionUtils.getInventoryMappingId(infusion);
const isInfusionOwner = partyInfo
? CampaignUtils.getPartyInventoryRestrictions(partyInfo).some(
(restriction) =>
restriction.type ===
Constants.PartyRestrictionTypeEnum.INFUSION &&
restriction.inventoryMappingId === itemMappingId &&
restriction.characterId === characterId
)
: true;
return (
<div className="ct-item-detail__infusion">
<InfusionPreview
infusion={infusion}
snippetData={snippetData}
ruleData={ruleData}
infusionChoiceLookup={infusionChoiceLookup}
onClick={this.handleInfusionClick}
proficiencyBonus={proficiencyBonus}
onClick={isInfusionOwner ? this.handleInfusionClick : undefined}
/>
</div>
);
@@ -999,6 +960,7 @@ export class ItemDetail extends React.PureComponent<Props> {
onCustomizeClick,
actions,
theme,
onRemoveItem,
} = this.props;
const isCustomized =
@@ -1050,6 +1012,7 @@ export class ItemDetail extends React.PureComponent<Props> {
<ItemDetailActions
onPostRemoveNavigation={onPostRemoveNavigation}
item={item}
onRemoveItem={onRemoveItem}
/>
)}
</div>
@@ -1059,11 +1022,12 @@ export class ItemDetail extends React.PureComponent<Props> {
const ItemDetailContainer = (props) => {
const { inventoryManager } = useContext(InventoryManagerContext);
const { actions } = useCharacterEngine();
const { actions, characterId } = useCharacterEngine();
return (
<ItemDetail
inventoryManager={inventoryManager}
actions={actions}
characterId={characterId}
{...props}
/>
);
@@ -0,0 +1,4 @@
import ItemSlotManager from "./ItemSlotManager";
export default ItemSlotManager;
export { ItemSlotManager };
@@ -52,7 +52,10 @@ export default class LoadingBlocker extends React.PureComponent<Props, State> {
}`}
>
<div className="ct-loading-blocker__logo" />
<AnimatedLoadingRingSvg className="ct-loading-blocker__anim" />
<AnimatedLoadingRingSvg
className="ct-loading-blocker__anim"
backgroundColor="#232323"
/>
</div>
);
}
@@ -0,0 +1,4 @@
import SimpleClassSpellList from "./SimpleClassSpellList";
export default SimpleClassSpellList;
export { SimpleClassSpellList };
@@ -1,5 +1,6 @@
import React, { useContext } from "react";
import { DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
ComponentConstants,
@@ -41,7 +42,9 @@ function isCastAvailableAtLevel(level, available) {
}
// TODO dispatch needs to be factored out
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> {
characterLevel: number;
spell: Spell;
spellSlots: Array<SpellSlotContract>;
@@ -452,7 +455,7 @@ export class SpellCaster extends React.PureComponent<Props, State> {
}
castActionsNode = (
<React.Fragment>
<>
<div className="ct-spell-caster__casting-action">
<ThemeButton
disabled={!isLimitedUseAvailable}
@@ -474,7 +477,7 @@ export class SpellCaster extends React.PureComponent<Props, State> {
)}
</ThemeButton>
</div>
</React.Fragment>
</>
);
} else {
const canSpellCastAtLevel = canCastAtLevel(
@@ -499,7 +502,7 @@ export class SpellCaster extends React.PureComponent<Props, State> {
SpellUtils.getCastLevelAvailableCount(castLevel, pactMagicSlots);
castActionsNode = (
<React.Fragment>
<>
{isSpellCastAvailableAtLevel && (
<div className="ct-spell-caster__casting-action">
<ThemeButton
@@ -527,7 +530,7 @@ export class SpellCaster extends React.PureComponent<Props, State> {
</ThemeButton>
</div>
)}
</React.Fragment>
</>
);
}
}
@@ -312,7 +312,7 @@ export default class SpellDetail extends React.PureComponent<Props> {
}
if (range.aoeValue) {
rangeAreas.push(
<React.Fragment>
<>
<NumberDisplay type="distanceInFt" number={range.aoeValue} />
<span className="ct-spell-detail__range-shape">
<AoeTypeIcon
@@ -325,7 +325,7 @@ export default class SpellDetail extends React.PureComponent<Props> {
themeMode={theme.isDarkMode ? "gray" : "dark"}
/>
</span>
</React.Fragment>
</>
);
}
}
@@ -367,7 +367,6 @@ export default class SpellDetail extends React.PureComponent<Props> {
);
}
const infoItemProps = { role: "listitem", inline: true };
const getSourceInfo = (sourceId) =>
sourceId ? RuleDataUtils.getSourceDataInfo(sourceId, ruleData) : null;
@@ -393,11 +392,11 @@ export default class SpellDetail extends React.PureComponent<Props> {
{this.renderCaster()}
{this.renderCustomize()}
<div className="ct-spell-detail__properties" role="list">
<InfoItem label="Casting Time:" {...infoItemProps}>
<InfoItem label="Casting Time:">
{castingTimeDisplay}
{castingTimeDescription ? `, ${castingTimeDescription}` : ""}
</InfoItem>
<InfoItem label="Range/Area:" {...infoItemProps}>
<InfoItem label="Range/Area:">
{rangeAreas.map((node, idx) => (
<React.Fragment key={idx}>
{node}
@@ -405,7 +404,7 @@ export default class SpellDetail extends React.PureComponent<Props> {
</React.Fragment>
))}
</InfoItem>
<InfoItem label="Components:" {...infoItemProps}>
<InfoItem label="Components:">
{componentsNode}
{componentsDescription && (
<span className="ct-spell-detail__components-description">
@@ -414,7 +413,7 @@ export default class SpellDetail extends React.PureComponent<Props> {
)}
</InfoItem>
{duration !== null && (
<InfoItem label="Duration:" {...infoItemProps}>
<InfoItem label="Duration:">
{DurationUtils.renderDuration(
duration,
SpellUtils.getConcentration(spell)
@@ -422,23 +421,15 @@ export default class SpellDetail extends React.PureComponent<Props> {
</InfoItem>
)}
{requiresSavingThrow && (
<InfoItem label="Attack/Save:" {...infoItemProps}>
<InfoItem label="Attack/Save:">
{SpellUtils.getSaveDcAbilityShortName(spell, ruleData)}{" "}
{attackSaveValue}
</InfoItem>
)}
{version !== null && (
<InfoItem label="Version:" {...infoItemProps}>
{version}
</InfoItem>
)}
{notes && (
<InfoItem label="Notes:" {...infoItemProps}>
{notes}
</InfoItem>
)}
{version !== null && <InfoItem label="Version:">{version}</InfoItem>}
{notes && <InfoItem label="Notes:">{notes}</InfoItem>}
{sourceId !== null && (
<InfoItem label="Source:" {...infoItemProps}>
<InfoItem label="Source:">
<Reference
name={getSourceInfo(sourceId)?.description || ""}
page={sourcePage}
@@ -183,9 +183,9 @@ export default function SpellManagerContainer({
const filteredSpells = getFilteredSpells(combinedSpells);
return (
<React.Fragment>
<>
{showFilters && (
<React.Fragment>
<>
<SpellFilter
spells={combinedSpells.map((manager) => manager.getSpell())}
sourceCategories={sourceCategories}
@@ -201,7 +201,7 @@ export default function SpellManagerContainer({
Looking for something not in the list below? Unlock all official
options in the <Link href="/marketplace">Marketplace</Link>.
</div>
</React.Fragment>
</>
)}
{filteredSpells.length === 0 && <div>No Results Found</div>}
{filteredSpells.map((spell, idx) => (
@@ -240,7 +240,7 @@ export default function SpellManagerContainer({
proficiencyBonus={proficiencyBonus}
/>
))}
</React.Fragment>
</>
);
}
@@ -13,7 +13,7 @@ import {
SpellSlotContract,
SpellUtils,
} from "@dndbeyond/character-rules-engine/es";
import { IRollContext } from "@dndbeyond/dice";
import { RollContext } from "@dndbeyond/pocket-dimension-dice/types";
import SpellsSpell from "../SpellsSpell";
@@ -44,7 +44,7 @@ interface Props {
theme: CharacterTheme;
dataOriginRefData: DataOriginRefData;
proficiencyBonus: number;
rollContext: IRollContext;
rollContext: RollContext;
}
export default class SpellsLevel extends React.PureComponent<Props> {
static defaultProps = {
@@ -1,4 +1,5 @@
import React, { useCallback, useContext, useEffect, useMemo } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Tooltip } from "@dndbeyond/character-common-components/es";
import {
DiceComponentUtils,
@@ -19,19 +20,16 @@ import {
Spell,
LeveledSpellManager,
} from "@dndbeyond/character-rules-engine/es";
import { RollTypes } from "@dndbeyond/pocket-dimension-dice/constants";
import {
Dice,
DiceEvent,
DiceTools,
IRollContext,
RollRequest,
RollType,
} from "@dndbeyond/dice";
import { GameLogContext } from "@dndbeyond/game-log-components";
RollContext,
RollDicePayload,
} from "@dndbeyond/pocket-dimension-dice/types";
import { ItemName } from "~/components/ItemName";
import { NumberDisplay } from "~/components/NumberDisplay";
import { SpellName } from "~/components/SpellName";
import { TypeScriptUtils } from "../../utils";
import SpellSlotChooser from "../SpellSlotChooser";
import { ThemeButton } from "../common/Button";
@@ -61,7 +59,7 @@ interface Props {
diceEnabled: boolean;
theme: CharacterTheme;
proficiencyBonus: number;
rollContext: IRollContext;
rollContext: RollContext;
}
function SpellsSpell({
@@ -89,9 +87,9 @@ function SpellsSpell({
() => new LeveledSpellManager({ spell: spellData }),
[spellData]
);
const [isSlotChooserOpen, setIsSlotChooserOpen] = React.useState(false);
const [isCriticalHit, setIsCriticalHit] = React.useState(false);
const spellsSpellNode = React.useRef<HTMLDivElement>(null);
const [isSlotChooserOpen, setIsSlotChooserOpen] = useState(false);
const [isCriticalHit, setIsCriticalHit] = useState(false);
const spellsSpellNode = useRef<HTMLDivElement>(null);
const handleSpellClick = (evt: React.MouseEvent): void => {
evt.nativeEvent.stopImmediatePropagation();
@@ -127,22 +125,11 @@ function SpellsSpell({
setIsSlotChooserOpen(false);
};
const handleRollResults = (result: RollRequest): void => {
const handleRollResults = (result: RollDicePayload["data"]): void => {
let wasCrit = DiceComponentUtils.isCriticalRoll(result);
setIsCriticalHit(wasCrit);
};
const diceEventHandler = useMemo(() => {
if (!spellsSpellNode.current) {
return () => {
/* NOOP */
};
}
return DiceComponentUtils.setupResetCritStateOnRoll(
spell.getName(),
spellsSpellNode.current
);
}, [spell]);
// TODO: use mui click away listener
const handleDocumentClick = useCallback((): void => {
handleSpellSlotChooserClose();
@@ -154,15 +141,11 @@ function SpellsSpell({
if (isSlotChooserOpen) {
document.removeEventListener("click", handleDocumentClick);
}
Dice.removeEventListener(DiceEvent.ROLL, diceEventHandler);
},
[isSlotChooserOpen, handleDocumentClick, diceEventHandler]
[isSlotChooserOpen, handleDocumentClick]
);
// Todo: consider making a separate component for this
const [{ messageTargetOptions, defaultMessageTargetOption, userId }] =
useContext(GameLogContext);
const renderAttackInfo = (): React.ReactNode => {
let toHit: number | null = null;
let saveDcValue: number | null = null;
@@ -182,27 +165,18 @@ function SpellsSpell({
return (
<div className="ct-spells-spell__tohit">
<DigitalDiceWrapper
diceNotation={DiceTools.CustomD20(toHit)}
rollType={RollType.ToHit}
rollAction={spell.getName()}
diceEnabled={diceEnabled}
themeColor={theme.themeColor}
onRollResults={handleRollResults}
rollContext={rollContext}
rollTargetOptions={
messageTargetOptions
? Object.values(messageTargetOptions.entities).filter(
TypeScriptUtils.isNotNullOrUndefined
)
: undefined
}
rollTargetDefault={defaultMessageTargetOption}
userId={Number(userId) || 0}
diceNotation={`1d20${
toHit > 0 ? `+${toHit}` : toHit !== 0 ? toHit : ""
}`}
rollType={RollTypes.ToHit}
action={spell.getName()}
isDiceEnabled={diceEnabled}
rollCallback={handleRollResults}
entityId={String(rollContext.entityId)}
entityType={String(rollContext.entityType)}
name={String(rollContext.name)}
>
<NumberDisplay
number={toHit}
type="signed"
/>
<NumberDisplay number={toHit} type="signed" />
</DigitalDiceWrapper>
</div>
);
@@ -312,7 +286,7 @@ function SpellsSpell({
actionNode = <span className="ct-spells-spell__at-will">At Will</span>;
} else {
actionNode = (
<React.Fragment>
<>
<ThemeButton
size={"small"}
onClick={
@@ -337,7 +311,7 @@ function SpellsSpell({
doesPactSlotExist={doesPactSlotExist}
/>
)}
</React.Fragment>
</>
);
}
let classNames: Array<string> = ["ct-spells-spell", className];
@@ -431,6 +405,11 @@ function SpellsSpell({
theme={theme}
isCriticalHit={isCriticalHit}
rollContext={rollContext}
rollCallback={() => {
if (isCriticalHit) {
setIsCriticalHit(false);
}
}}
/>
</div>
{showNotes && (
@@ -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 {
name: string;
className: string;
}
export default class Subsection extends React.PureComponent<Props> {
export default class Subsection extends PureComponent<Props> {
static defaultProps = {
className: "",
};
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class SubsectionFooter extends React.PureComponent {
export default class SubsectionFooter extends PureComponent<PropsWithChildren> {
render() {
const { children } = this.props;
@@ -1,9 +0,0 @@
import React from "react";
export default class SubsectionHeader extends React.PureComponent {
render() {
const { children } = this.props;
return <div className="ct-subsection__header">{children}</div>;
}
}
@@ -1,9 +0,0 @@
import React from "react";
export default class SubsectionHeaderContent extends React.PureComponent {
render() {
const { children } = this.props;
return <div className="ct-subsection__header-content">{children}</div>;
}
}
@@ -266,6 +266,7 @@ export default class ValueEditor extends React.PureComponent<Props, State> {
return (
<ValueEditorSelectProperty
{...baseProps}
key={baseProps.propertyKey}
options={RuleDataUtils.getStatOptions(ruleData)}
/>
);
@@ -274,6 +275,7 @@ export default class ValueEditor extends React.PureComponent<Props, State> {
return (
<ValueEditorSelectProperty
{...baseProps}
key={baseProps.propertyKey}
options={RuleDataUtils.getDieTypeOptions(ruleData)}
/>
);
@@ -284,6 +286,7 @@ export default class ValueEditor extends React.PureComponent<Props, State> {
return (
<ValueEditorSelectProperty
{...baseProps}
key={baseProps.propertyKey}
options={RuleDataUtils.getProficiencyLevelOptions(
ruleData
)}
@@ -294,6 +297,7 @@ export default class ValueEditor extends React.PureComponent<Props, State> {
return (
<ValueEditorSelectProperty
{...baseProps}
key={baseProps.propertyKey}
options={RuleDataUtils.getAlignmentOptions(ruleData)}
/>
);
@@ -302,6 +306,7 @@ export default class ValueEditor extends React.PureComponent<Props, State> {
return (
<ValueEditorSelectProperty
{...baseProps}
key={baseProps.propertyKey}
options={RuleDataUtils.getCreatureSizeOptions(ruleData)}
/>
);
@@ -310,6 +315,7 @@ export default class ValueEditor extends React.PureComponent<Props, State> {
return (
<ValueEditorSelectProperty
{...baseProps}
key={baseProps.propertyKey}
options={this.restrictOptions(
valueTypeId,
RuleDataUtils.getMonsterTypeOptions(ruleData)
@@ -323,6 +329,7 @@ export default class ValueEditor extends React.PureComponent<Props, State> {
return (
<ValueEditorNumberProperty
{...baseProps}
key={baseProps.propertyKey}
{...constraintProps}
/>
);
@@ -1,11 +1,11 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
interface Props {
interface Props extends PropsWithChildren {
isBlock: boolean;
propertyKey: number;
className: string;
}
export default class ValueEditorProperty extends React.PureComponent<Props> {
export default class ValueEditorProperty extends PureComponent<Props> {
static defaultProps = {
isBlock: false,
className: "",
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class ValueEditorPropertyLabel extends React.PureComponent {
export default class ValueEditorPropertyLabel extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-value-editor__property-label">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class ValueEditorPropertyValue extends React.PureComponent {
export default class ValueEditorPropertyValue extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-value-editor__property-value">
@@ -1,12 +1,14 @@
import React from "react";
import { Select } from "@dndbeyond/character-components/es";
import { HelperUtils, Constants } from "@dndbeyond/character-rules-engine/es";
import { Select } from "~/components/Select";
import ValueEditorProperty from "../ValueEditorProperty";
import ValueEditorPropertyLabel from "../ValueEditorPropertyLabel";
import ValueEditorPropertySource from "../ValueEditorPropertySource";
import ValueEditorPropertyValue from "../ValueEditorPropertyValue";
import styles from "./styles.module.css";
interface Props {
label: string;
@@ -103,11 +105,15 @@ export default class ValueEditorSelectProperty extends React.PureComponent<
<ValueEditorPropertyLabel>{label}</ValueEditorPropertyLabel>
<ValueEditorPropertyValue>
<Select
className={styles.select}
placeholder="--"
{...selectProps}
options={options}
value={value}
onChange={this.handleSelectChange}
optionsWidth="x-large"
name={propertyKey}
size="small"
/>
</ValueEditorPropertyValue>
{enableSource && (
@@ -33,14 +33,14 @@ export default class VehicleBlockAction extends React.PureComponent<Props> {
ammunitionText.push(text);
});
actionText.push(
<React.Fragment>
<>
<span className="ct-vehicle-block__action-ammo-label">
Ammunition:{" "}
</span>
<span className="ct-vehicle-block__action-ammo-description">
{ammunitionText.join(", ")}.
</span>
</React.Fragment>
</>
);
}
@@ -48,13 +48,13 @@ export default class VehicleBlockAction extends React.PureComponent<Props> {
//All vehicle && component actions descriptions are currently formatted html,
//should use action props derivers to generate the content displayed in this description in the future
actionText.push(
<React.Fragment>
<>
<HtmlContent
className="ct-vehicle-block__action-description"
html={description}
withoutTooltips
/>
</React.Fragment>
</>
);
}
@@ -1,8 +1,8 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
import VehicleBlockSectionHeader from "../VehicleBlockSectionHeader";
export default class VehicleBlockActionStationsShell extends React.PureComponent<{}> {
export default class VehicleBlockActionStationsShell extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-vehicle-block__action-stations">
@@ -41,7 +41,7 @@ export default class VehicleBlockActionSummary extends React.PureComponent<Props
if (sourceId !== null) {
nodes.push(
<React.Fragment>
<>
{"("}
<Reference
isDarkMode={theme?.isDarkMode}
@@ -50,7 +50,7 @@ export default class VehicleBlockActionSummary extends React.PureComponent<Props
chapter={sourceChapterNumber}
/>
{")."}
</React.Fragment>
</>
);
}
@@ -24,32 +24,32 @@ export default class VehicleBlockActions extends React.PureComponent<Props> {
}
return (
<React.Fragment>
<>
{reactions.length > 0 && (
<React.Fragment>
<>
<VehicleBlockSectionHeader label="Reactions" />
{reactions.map((action) => (
<VehicleBlockAction action={action} key={action.key} />
))}
</React.Fragment>
</>
)}
{bonusActions.length > 0 && (
<React.Fragment>
<>
<VehicleBlockSectionHeader label="Bonus Actions" />
{bonusActions.map((action) => (
<VehicleBlockAction action={action} key={action.key} />
))}
</React.Fragment>
</>
)}
{special.length > 0 && (
<React.Fragment>
<>
<VehicleBlockSectionHeader label="Special Actions" />
{special.map((action) => (
<VehicleBlockAction action={action} key={action.key} />
))}
</React.Fragment>
</>
)}
</React.Fragment>
</>
);
}
}
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class VehicleBlockComponentsShell extends React.PureComponent {
export default class VehicleBlockComponentsShell extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-vehicle-block__components-block">
@@ -54,9 +54,9 @@ export default class VehicleBlockHeader extends React.PureComponent<Props> {
}
return (
<React.Fragment>
<>
(<InlineSeparatedNodes nodes={metaTextSizeNodes} />)
</React.Fragment>
</>
);
};
@@ -57,15 +57,15 @@ export default class VehicleBlockPrimaryAttributes extends React.PureComponent<P
let showFullHitPointInfo: boolean = !shouldCoalesce;
currentHpNode = (
<React.Fragment>
<>
{showFullHitPointInfo && (
<React.Fragment>
<>
<span className={classNames.join(" ")}>{remainingHp}</span>
<span className="ct-vehicle-block-component__hp-sep">/</span>
</React.Fragment>
</>
)}
<span className={classNames.join(" ")}>{hitPointInfo.totalHp}</span>
</React.Fragment>
</>
);
}
@@ -233,7 +233,7 @@ export default class VehicleBlockPrimaryAttributes extends React.PureComponent<P
Constants.VehicleConfigurationDisplayTypeEnum.ELEMENTAL_AIRSHIP;
return (
<React.Fragment>
<>
{armorClassInfo !== null && this.renderArmorClassAttribute()}
{hitPointInfo !== null && (
<VehicleBlockAttribute
@@ -300,7 +300,7 @@ export default class VehicleBlockPrimaryAttributes extends React.PureComponent<P
displayColon={isElementalAirship}
/>
)}
</React.Fragment>
</>
);
}
}
@@ -1,13 +1,13 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
import { Constants } from "@dndbeyond/character-rules-engine/es";
import { GD_VehicleBlockShellProps } from "../../../utils/Component";
import VehicleBlockShellCap from "../VehicleBlockShellCap";
interface Props extends GD_VehicleBlockShellProps {}
interface Props extends GD_VehicleBlockShellProps, PropsWithChildren {}
export default class VehicleBlockShell extends React.PureComponent<Props> {
export default class VehicleBlockShell extends PureComponent<Props> {
render() {
const { children, displayType } = this.props;
@@ -53,7 +53,7 @@ export default class VehicleConditionTrackers extends React.PureComponent<Props>
//TODO handle standard conditions in future
return (
<React.Fragment>
<>
{specialConditions.map((condition, idx) => {
const levels = ConditionUtils.getDefinitionLevels(condition).map(
(level) => ConditionLevelUtils.getLevel(level)
@@ -89,7 +89,7 @@ export default class VehicleConditionTrackers extends React.PureComponent<Props>
/>
);
})}
</React.Fragment>
</>
);
}
}
@@ -1,10 +1,10 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
import { ThemeButton } from "../ThemeButton";
//Eventually remove and use character-components/Button/RemoveButton with theme override when needed
interface Props {
interface Props extends PropsWithChildren {
onClick?: () => void;
className?: string;
disabled?: boolean;
@@ -17,7 +17,7 @@ interface Props {
enableConfirm?: boolean;
isInteractive?: boolean;
}
export default class RemoveButton extends React.PureComponent<Props, {}> {
export default class RemoveButton extends PureComponent<Props, {}> {
static defaultProps = {
size: "small",
style: "outline",
@@ -1,9 +1,9 @@
import React from "react";
import { PureComponent, MouseEvent, PropsWithChildren } from "react";
import { Button } from "@dndbeyond/character-components/es";
interface Props {
onClick?: ((evt: React.MouseEvent<HTMLButtonElement>) => void) | (() => void);
interface Props extends PropsWithChildren {
onClick?: ((evt: MouseEvent<HTMLButtonElement>) => void) | (() => void);
className: string;
disabled?: boolean;
styledDisabled?: boolean;
@@ -17,7 +17,7 @@ interface Props {
confirmDuration?: number;
isInteractive?: boolean;
}
export default class ThemeButton extends React.PureComponent<Props, {}> {
export default class ThemeButton extends PureComponent<Props, {}> {
static defaultProps = {
className: "",
style: "filled",
@@ -1,137 +0,0 @@
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import List from "@mui/material/List";
import ListSubheader from "@mui/material/ListSubheader";
import MenuItem from "@mui/material/MenuItem";
import Popover from "@mui/material/Popover";
import React from "react";
import { GroupedMenuOption } from "@dndbeyond/character-rules-engine/es";
import { ThemeButton } from "..";
interface Props {
children: React.ReactNode;
groupedOptions: Array<GroupedMenuOption>;
className?: string;
size?: string;
buttonStyle?: string;
showSingleOption?: boolean;
onSelect: (containerDefinitionKey: string) => void;
containerEl?: HTMLElement;
}
export const ThemeButtonWithMenu: React.FC<Props> = ({
className,
children,
groupedOptions,
size = "small",
buttonStyle = "filled",
onSelect,
showSingleOption = false,
containerEl,
}) => {
const singleOption =
groupedOptions.length === 1 &&
groupedOptions[0].options?.length === 1 &&
!showSingleOption;
const classNames = [className, "ct-theme-button-with-menu"];
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
if (!singleOption) {
setAnchorEl(event.currentTarget);
} else if (singleOption) {
onSelect(groupedOptions[0].options[0]?.value);
}
};
const handleClose = (value?: string) => {
if (value) {
onSelect(value);
}
setAnchorEl(null);
};
return (
<span className={classNames.join(" ")}>
<ThemeButton
size={size}
style={buttonStyle}
onClick={(evt: React.MouseEvent<HTMLButtonElement>) => {
handleClick(evt);
}}
data-testid={`theme-button-with-menu-${children?.toString()}`
.toLowerCase()
.replace(/\s/g, "-")}
>
{children}{" "}
{!singleOption && (
<KeyboardArrowDownIcon sx={{ width: 8, height: 8 }} />
)}
</ThemeButton>
{!singleOption && (
<Popover
anchorEl={anchorEl}
container={containerEl}
keepMounted
open={Boolean(anchorEl)}
onClose={(evt: any) => {
evt.stopPropagation();
evt.nativeEvent?.stopImmediatePropagation();
handleClose();
}}
>
<List
sx={{ bgcolor: "background.default" }}
onClick={(evt) => {
evt.nativeEvent?.stopImmediatePropagation();
evt.stopPropagation();
}}
>
{groupedOptions.map((groupedOption) => (
<React.Fragment key={groupedOption.label}>
<ListSubheader
sx={{
lineHeight: "32px",
textTransform: "uppercase",
fontWeight: 700,
fontSize: "12px",
bgcolor: "background.default",
}}
data-testid={`theme-button-with-menu-subheader-${groupedOption.label}`
.toLowerCase()
.replace(/\s/g, "-")}
>
{groupedOption.label}
</ListSubheader>
{groupedOption.options.map((option) => {
return (
<MenuItem
sx={{ bgcolor: "background.default" }}
key={option.value}
onClick={(evt) => {
evt.stopPropagation();
evt.nativeEvent?.stopImmediatePropagation();
handleClose(option.value);
}}
data-testid={`theme-button-with-menu-item-${option.label}`
.toLowerCase()
.replace(/\s/g, "-")}
>
{option.label}
</MenuItem>
);
})}
</React.Fragment>
))}
</List>
</Popover>
)}
</span>
);
};
export default ThemeButtonWithMenu;
@@ -1,4 +0,0 @@
import { ThemeButtonWithMenu } from "./ThemeButtonWithMenu";
export default ThemeButtonWithMenu;
export { ThemeButtonWithMenu };
@@ -1,266 +0,0 @@
import clsx from "clsx";
import { orderBy } from "lodash";
import React from "react";
import { v4 as uuidv4 } from "uuid";
import { Checkbox } from "~/components/Checkbox";
import { HtmlContent } from "~/components/HtmlContent";
import { Accordion } from "../../../../../../components/Accordion";
import styles from "./styles.module.css";
import { CheckboxInfo } from "./typings";
export enum CheckboxVariant {
BUILDER = "builder",
SIDEBAR = "sidebar",
DEFAULT = "default",
}
interface Props {
checkboxes: Array<CheckboxInfo>;
heading: React.ReactNode;
description: React.ReactNode;
checkUncheckAllEnabled?: boolean;
onCheckUncheckAll?: CheckboxInfo;
showAccordion?: boolean;
accordionHeading?: string;
themed?: boolean;
darkMode?: boolean;
variant?: "builder" | "sidebar" | "default";
allText?: string;
}
export const FormCheckBoxesField: React.FC<Props> = ({
heading,
description,
checkboxes,
showAccordion,
checkUncheckAllEnabled,
onCheckUncheckAll,
themed,
darkMode,
accordionHeading = "Options",
variant = CheckboxVariant.DEFAULT,
allText = "All",
}) => {
const sortedChecks = orderBy(checkboxes, "sortOrder");
const allChecksEnabled = sortedChecks.every(
(checkbox) => checkbox.initiallyEnabled
);
if (!checkboxes.length) {
return null;
}
const renderSingleCheckbox = () => {
const [checkbox] = sortedChecks;
return (
<div
className={clsx([
styles.checkboxesContainer,
variant === CheckboxVariant.SIDEBAR &&
styles.checkboxesContainerSidebar,
])}
>
<div className={styles.summary}>
<div
className={clsx([
styles.summaryHeadingGroup,
styles.summaryHeadingGroupSingle,
])}
>
<div
className={clsx([
styles.summaryHeading,
variant === CheckboxVariant.SIDEBAR &&
styles.summaryHeadingSidebar,
darkMode && styles.summaryHeadingSidebarDark,
])}
>
{heading}
</div>
<div className={styles.checkbox}>
<Checkbox
id={uuidv4()}
checked={checkbox.initiallyEnabled}
aria-label={checkbox.label}
onClick={checkbox.onChange}
onChangePromise={checkbox.onChangePromise}
themed={themed}
darkMode={darkMode}
disabled={checkbox.enabled === false}
variant={variant}
/>
</div>
</div>
{description && (
<div
className={clsx([
styles.summaryDescription,
variant === CheckboxVariant.SIDEBAR &&
styles.summaryDescriptionSidebar,
darkMode && styles.darkMode,
])}
>
{description}
</div>
)}
</div>
</div>
);
};
const renderCheckboxes = () => {
return (
<div className={styles.container}>
{sortedChecks.map((checkbox, idx) => (
<div
className={styles.checkboxWrapper}
key={idx}
data-testid="content-preference-checkbox"
>
<div className={styles.group} key={idx}>
<div className={styles.checkbox}>
<Checkbox
id={uuidv4()}
checked={checkbox.initiallyEnabled}
aria-label={checkbox.label}
onClick={checkbox.onChange}
onChangePromise={checkbox.onChangePromise}
themed={themed}
darkMode={darkMode}
disabled={checkbox.enabled === false}
variant={variant}
/>
</div>
<div
className={clsx([
styles.groupLabel,
variant === CheckboxVariant.DEFAULT &&
styles.groupLabelDefault,
])}
>
{checkbox.label}
</div>
</div>
{checkbox.description && (
<HtmlContent
html={checkbox.description}
className={clsx([
styles.description,
variant === CheckboxVariant.DEFAULT &&
styles.descriptionDefault,
])}
/>
)}
</div>
))}
</div>
);
};
return checkboxes.length > 1 ||
variant === CheckboxVariant.BUILDER ||
variant === CheckboxVariant.DEFAULT ? (
<div
className={clsx(
[styles.checkboxesContainer],
variant === CheckboxVariant.SIDEBAR && styles.checkboxesContainerSidebar
)}
>
{((variant === CheckboxVariant.SIDEBAR && !showAccordion) ||
variant === CheckboxVariant.BUILDER ||
variant === CheckboxVariant.DEFAULT) && (
<div className={styles.summary}>
<div className={styles.summaryHeadingGroup}>
{checkUncheckAllEnabled &&
onCheckUncheckAll &&
variant === CheckboxVariant.DEFAULT && (
<Checkbox
id={uuidv4()}
checked={allChecksEnabled}
aria-label="Check/Uncheck all"
onClick={onCheckUncheckAll.onChange}
themed={themed}
darkMode={darkMode}
variant={variant}
/>
)}
<div
className={clsx([
styles.summaryHeading,
variant === CheckboxVariant.SIDEBAR &&
styles.summaryHeadingSidebar,
darkMode && styles.summaryHeadingSidebarDark,
variant === CheckboxVariant.DEFAULT &&
styles.summaryHeadingDefault,
])}
>
{heading}
</div>
</div>
{description && (
<div
className={clsx([
styles.summaryDescription,
variant === CheckboxVariant.SIDEBAR &&
styles.summaryDescriptionSidebar,
darkMode && styles.darkMode,
])}
>
{description}
</div>
)}
</div>
)}
{variant === CheckboxVariant.BUILDER && checkUncheckAllEnabled ? (
<div className={styles.summaryBuilder}>
<div className={styles.summaryHeadingGroup}>
{checkUncheckAllEnabled && onCheckUncheckAll && (
<Checkbox
id={uuidv4()}
checked={allChecksEnabled}
aria-label="Check/Uncheck all"
onClick={onCheckUncheckAll.onChange}
themed={themed}
darkMode={darkMode}
variant={variant}
/>
)}
<div className={styles.summaryHeadingAll}>{allText}</div>
</div>
</div>
) : null}
{showAccordion ? (
<Accordion
summary={
variant !== CheckboxVariant.SIDEBAR ? accordionHeading : heading
}
variant="text"
resetOpen={!allChecksEnabled}
className={clsx([
styles.accordion,
variant === CheckboxVariant.BUILDER && styles.accordionBuilder,
variant === CheckboxVariant.SIDEBAR && styles.accordionSidebar,
variant === CheckboxVariant.SIDEBAR &&
darkMode &&
styles.accordionHeadingDark,
])}
>
{variant === CheckboxVariant.SIDEBAR && (
<p className={clsx([styles.accordionSidebarDescription])}>
{description}
</p>
)}
{renderCheckboxes()}
</Accordion>
) : (
renderCheckboxes()
)}
</div>
) : (
renderSingleCheckbox()
);
};
@@ -1,134 +0,0 @@
import uniqueId from "lodash/uniqueId";
import React from "react";
import { Select } from "@dndbeyond/character-components/es";
import styles from "./styles.module.css";
interface Props {
initialValue: string | null;
onChange?: (value: string) => void;
onChangePromise?: (
newValue: string,
oldValue: string,
accept: () => void,
reject: () => void
) => void;
heading: React.ReactNode;
clsNames: Array<string>;
initialOptionRemoved: boolean;
options: Array<any>;
description?: React.ReactNode;
block: boolean;
placeholder?: string;
}
interface State {
value: string | null;
}
export default class FormSelectField extends React.PureComponent<Props, State> {
static defaultProps = {
initialValue: null,
clsNames: [],
initialOptionRemoved: false,
block: false,
};
constructor(props: Props) {
super(props);
this.state = {
value: props.initialValue,
};
}
componentDidUpdate(
prevProps: Readonly<Props>,
prevState: Readonly<State>,
snapshot?: any
): void {
const { initialValue } = this.props;
if (initialValue !== prevProps.initialValue) {
this.setState({
value: initialValue,
});
}
}
handleChange = (value: string): void => {
const { onChange } = this.props;
if (onChange) {
onChange(value);
}
this.setState({
value,
});
};
//TODO test that removing newValue from state still works in builder choices
handleChangePromise = (
newValue: string,
oldValue: string,
accept: () => void,
reject: () => void
): void => {
const { onChangePromise } = this.props;
if (onChangePromise) {
onChangePromise(newValue, oldValue, accept, reject);
}
};
render() {
const { value } = this.state;
const {
onChangePromise,
onChange,
block,
heading,
options,
clsNames,
initialOptionRemoved,
description,
placeholder,
} = this.props;
const uId: string = uniqueId("qry-");
let conClsNames: Array<string> = ["builder-field", "builder-field-select"];
if (clsNames.length) {
conClsNames = [...conClsNames, ...clsNames];
}
if (block) {
conClsNames.push("builder-field-select-block");
}
return (
<div className={conClsNames.join(" ")}>
<div className="builder-field-select-summary">
<label
className="builder-field-heading form-input-field-label"
htmlFor={uId}
>
{heading}
</label>
<div className={styles.summaryDescription}>{description}</div>
</div>
<div className="builder-field-select-input">
<Select
options={options}
onChange={onChange ? this.handleChange : undefined}
onChangePromise={
onChangePromise ? this.handleChangePromise : undefined
}
value={value}
initialOptionRemoved={initialOptionRemoved}
placeholder={placeholder}
/>
</div>
</div>
);
}
}
@@ -1,4 +0,0 @@
import FormSelectField from "./FormSelectField";
export default FormSelectField;
export { FormSelectField };
@@ -1,57 +0,0 @@
import React from "react";
import { Toggle } from "~/components/Toggle";
interface Props {
initiallyEnabled: boolean;
onChange?: (isEnabled?: boolean) => void;
onChangePromise?: (
newIsEnabled: boolean,
accept: () => void,
reject: () => void
) => void;
heading: React.ReactNode;
description?: React.ReactNode;
toggleLabel: string;
isReadOnly?: boolean;
}
export default class FormToggleField extends React.PureComponent<Props, {}> {
static defaultProps = {
initiallyEnabled: false,
};
render() {
const {
heading,
description,
initiallyEnabled,
onChange,
onChangePromise,
toggleLabel,
isReadOnly,
} = this.props;
return (
<div className="builder-field builder-field-toggle">
<div className="builder-field-toggle-summary">
<div className="builder-field-toggle-heading">{heading}</div>
{description && (
<div className="builder-field-toggle-description">
{description}
</div>
)}
</div>
<div className="builder-field-toggle-input">
{onChange && (
<Toggle
checked={initiallyEnabled}
onClick={onChange}
color="secondary"
aria-label={toggleLabel}
/>
)}
</div>
</div>
);
}
}
@@ -1,4 +0,0 @@
import FormToggleField from "./FormToggleField";
export default FormToggleField;
export { FormToggleField };
@@ -7,13 +7,13 @@ interface Props {
export default function InlineSeparatedNodes({ nodes, sep = ", " }: Props) {
return (
<React.Fragment>
<>
{nodes.map((node, idx) => (
<React.Fragment key={idx}>
{node}
{idx < nodes.length - 1 && sep}
</React.Fragment>
))}
</React.Fragment>
</>
);
}
@@ -1,17 +1,18 @@
import * as React from "react";
import { PureComponent, PropsWithChildren } from "react";
import LinkButton from "../LinkButton";
interface Props {
interface Props extends PropsWithChildren {
url: string;
className?: string;
block?: boolean;
size: "oversized" | "large" | "medium" | "small";
disabled?: boolean;
download?: string | boolean;
target?: string;
onClick?: (isDisabled: boolean) => void;
}
export default class BuilderLinkButton extends React.PureComponent<Props, {}> {
export default class BuilderLinkButton extends PureComponent<Props, {}> {
static defaultProps = {
className: "",
};
@@ -1,6 +1,6 @@
import React from "react";
import { MouseEvent, PureComponent, PropsWithChildren } from "react";
interface Props {
interface Props extends PropsWithChildren {
url: string;
className: string;
block: boolean;
@@ -10,7 +10,7 @@ interface Props {
onClick?: (isDisabled: boolean) => void;
target?: string;
}
export default class LinkButton extends React.PureComponent<Props, {}> {
export default class LinkButton extends PureComponent<Props, {}> {
static defaultProps = {
className: "",
clsNames: [],
@@ -18,7 +18,7 @@ export default class LinkButton extends React.PureComponent<Props, {}> {
disabled: false,
};
handleClick = (evt: React.MouseEvent) => {
handleClick = (evt: MouseEvent) => {
const { onClick, disabled } = this.props;
if (onClick) {
@@ -1,8 +1,8 @@
import * as React from "react";
import { PureComponent, PropsWithChildren } from "react";
import LinkButton from "../LinkButton";
interface Props {
interface Props extends PropsWithChildren {
url: string;
className?: string;
block?: boolean;
@@ -12,7 +12,7 @@ interface Props {
download?: string | boolean;
target?: string;
}
export default class ThemeLinkButton extends React.PureComponent<Props, {}> {
export default class ThemeLinkButton extends PureComponent<Props, {}> {
static defaultProps = {
className: "",
style: "filled",
@@ -29,14 +29,8 @@ export const ArmorListItem: FC<ArmorListItemProps> = ({
...props
}) => {
const actions = useEquipmentMethods();
const {
ruleData,
proficiencyBonus,
characterTheme,
itemUtils,
helperUtils,
containerLookup,
} = useCharacterEngine();
const { ruleData, characterTheme, itemUtils, helperUtils, containerLookup } =
useCharacterEngine();
const image = itemUtils.getAvatarUrl(item);
const altText = itemUtils.getName(item);
const container = helperUtils.lookupDataOrFallback(
@@ -74,7 +68,6 @@ export const ArmorListItem: FC<ArmorListItemProps> = ({
ruleData={ruleData}
showCustomize={false}
showActions={false}
proficiencyBonus={proficiencyBonus}
/>
<EquipmentListItemActions item={item} {...actions} {...props} />
</Accordion>
@@ -7,87 +7,6 @@ import {
ItemUtils,
} from "@dndbeyond/character-rules-engine/es";
import Collapsible, { CollapsibleHeader } from "../common/Collapsible";
export class EquipmentListItemHeaderAction extends React.PureComponent<{
item: Item;
heading: any;
metaItems: Array<string>;
imageUrl: string;
onUnequip?: (item: Item) => void;
onEquip?: (item: Item) => void;
unequipLabel: string;
equipLabel: string;
}> {
static defaultProps = {
showHeaderAction: false,
consumeLabel: "Consume",
};
handleUnequip = () => {
const { onUnequip, item } = this.props;
if (onUnequip) {
onUnequip(item);
}
};
handleEquip = () => {
const { onEquip, item } = this.props;
if (onEquip) {
onEquip(item);
}
};
render() {
const { item, heading, imageUrl, metaItems, unequipLabel, equipLabel } =
this.props;
const isEquipped = ItemUtils.isEquipped(item);
const isStackable = ItemUtils.isStackable(item);
const canEquip = ItemUtils.canEquip(item);
const quantity = ItemUtils.getQuantity(item);
const callout = (
<div className="equipment-list-item-callout">
{isStackable && (
<div className="equipment-list-item-callout-quantity">
<span className="equipment-list-item-callout-quantity-extra">
Qty
</span>
<span className="equipment-list-item-callout-quantity-value">
{quantity}
</span>
</div>
)}
{canEquip && (
<div className="equipment-list-item-callout-action">
<Button
onClick={isEquipped ? this.handleUnequip : this.handleEquip}
style={isEquipped ? "" : "outline"}
size="small"
>
{isEquipped ? unequipLabel : equipLabel}
</Button>
</div>
)}
</div>
);
return (
<CollapsibleHeader
imgSrc={imageUrl}
heading={heading}
metaItems={metaItems}
callout={callout}
/>
);
}
}
interface EquipmentListItemProps {
item: Item;
@@ -241,7 +160,7 @@ export class EquipmentListItemActions extends React.PureComponent<
handleAmountBlur = (evt) => {
const parsedValue = HelperUtils.parseInputInt(evt.target.value);
const clampedValue = HelperUtils.clampInt(parsedValue ? parsedValue : 0, 0);
const clampedValue = HelperUtils.clampInt(parsedValue ?? 0, 0);
this.handleQuantitySave(clampedValue);
};
@@ -394,25 +313,3 @@ export class EquipmentListItemActions extends React.PureComponent<
);
}
}
export default class EquipmentListItem extends React.PureComponent<{
header: any;
clsNames: Array<string>;
}> {
static defaultProps = {
clsNames: [],
};
render() {
let { header, children, clsNames } = this.props;
return (
<Collapsible
trigger={header}
clsNames={["equipment-list-item", ...clsNames]}
>
{children}
</Collapsible>
);
}
}
@@ -1,11 +0,0 @@
import EquipmentListItem, {
EquipmentListItemActions,
EquipmentListItemHeaderAction,
} from "./EquipmentListItem";
export default EquipmentListItem;
export {
EquipmentListItem,
EquipmentListItemActions,
EquipmentListItemHeaderAction,
};
@@ -3,7 +3,6 @@ import React from "react";
import { LoadingPlaceholder, Button } from "@dndbeyond/character-components/es";
import {
BaseItemDefinitionContract,
HelperUtils,
Item,
ItemUtils,
@@ -17,14 +16,14 @@ import {
ItemManager,
} from "@dndbeyond/character-rules-engine/es";
import { Accordion } from "~/components/Accordion";
import { AccordionSummary } from "~/components/AccordionSummary";
import { ItemFilter } from "~/components/ItemFilter";
import { ItemName } from "~/components/ItemName";
import { LegacyBadge } from "~/components/LegacyBadge";
import { AppLoggerUtils } from "../../../utils";
import ItemDetail from "../../ItemDetail";
import EquipmentListItem from "../EquipmentListItem";
import { CollapsibleHeader, CollapsibleHeading } from "../common/Collapsible";
import styles from "./styles.module.css";
//TODO: This component should be removed and replaced with the EquipmentShop component
export class EquipmentManagerShopItem extends React.PureComponent<
@@ -38,7 +37,6 @@ export class EquipmentManagerShopItem extends React.PureComponent<
minAddAmount: number;
maxAddAmount: number;
ruleData: RuleData;
proficiencyBonus: number;
containerDefinitionKey: string;
theme: CharacterTheme;
},
@@ -113,80 +111,28 @@ export class EquipmentManagerShopItem extends React.PureComponent<
);
};
renderButtons = () => {
const { item } = this.props;
return (
<div className="equipment-list-header-actions">
<Button size="small" onClick={this.handleAdd}>
Add
</Button>
</div>
);
};
renderHeader(item, metaItems, defaultImageUrl) {
const { theme } = this.props;
const { canAttune, avatarUrl } = item.definition;
const isLegacy = ItemUtils.isLegacy(item);
const heading = (
<CollapsibleHeading>
<div className="equipment-list-heading">
<span className="equipment-list-heading-text">
<ItemName item={item} />
</span>
<span className="equipment-list-heading-icons">
{canAttune ? (
<i className="equipment-list-heading-icon i-req-attunement" />
) : (
""
)}
</span>
{isLegacy && <LegacyBadge variant="margin-left" />}
</div>
</CollapsibleHeading>
);
let imageUrl = avatarUrl;
if (!imageUrl) {
imageUrl = defaultImageUrl;
}
return (
<CollapsibleHeader
imgSrc={imageUrl ? imageUrl : ""}
heading={heading}
metaItems={metaItems}
callout={this.renderButtons()}
/>
);
}
render() {
const { amount } = this.state;
const {
item,
minAddAmount,
maxAddAmount,
ruleData,
proficiencyBonus,
theme,
} = this.props;
const { item, minAddAmount, maxAddAmount, ruleData, theme } = this.props;
const avatarUrl = ItemUtils.getDefinitionAvatarUrl(item);
let metaItems = [ItemUtils.getType(item)];
let defaultImageUrl: string | null = null;
// Set default avatar for weapon
if (ItemUtils.isWeaponContract(item)) {
defaultImageUrl = RuleDataUtils.getDefaultWeaponImageUrl(ruleData);
} else if (ItemUtils.isGearContract(item)) {
}
// Set default avatar for gear
else if (ItemUtils.isGearContract(item)) {
const subType = ItemUtils.getSubType(item);
if (subType) {
metaItems.push(subType);
}
defaultImageUrl = RuleDataUtils.getDefaultGearImageUrl(ruleData);
} else if (ItemUtils.isArmorContract(item)) {
}
// Set default avatar for armor
else if (ItemUtils.isArmorContract(item)) {
const baseArmorName = ItemUtils.getBaseArmorName(item);
if (baseArmorName) {
metaItems.push(baseArmorName);
@@ -199,8 +145,22 @@ export class EquipmentManagerShopItem extends React.PureComponent<
(amount === null ? 1 : amount) * ItemUtils.getBundleSize(item);
return (
<EquipmentListItem
header={this.renderHeader(item, metaItems, defaultImageUrl)}
<Accordion
className={styles.accordion}
variant="paper"
summary={
<AccordionSummary
className={styles.accordionSummary}
title={<ItemName item={item} showLegacyBadge={true} />}
description={metaItems as string[]}
imageUrl={avatarUrl ?? defaultImageUrl ?? undefined}
callout={
<Button size="small" onClick={this.handleAdd}>
Add
</Button>
}
/>
}
>
<ItemDetail
theme={theme}
@@ -208,7 +168,6 @@ export class EquipmentManagerShopItem extends React.PureComponent<
ruleData={ruleData}
showCustomize={false}
showActions={false}
proficiencyBonus={proficiencyBonus}
/>
<div className="equipment-list-item-actions">
<div className="equipment-list-item-action">
@@ -251,7 +210,7 @@ export class EquipmentManagerShopItem extends React.PureComponent<
</Button>
</div>
</div>
</EquipmentListItem>
</Accordion>
);
}
}
@@ -266,7 +225,6 @@ interface EquipmentManagerShopProps {
containerDefinitionKey: string
) => void;
ruleData: RuleData;
proficiencyBonus: number;
containerDefinitionKey: string;
theme: CharacterTheme;
activeSourceCategories: Array<number>;
@@ -465,14 +423,8 @@ export default class EquipmentManagerShop extends React.PureComponent<
renderPagedListing = (filteredItems: Array<ItemManager>): React.ReactNode => {
const { currentPage } = this.state;
const {
pageSize,
onItemAdd,
ruleData,
proficiencyBonus,
containerDefinitionKey,
theme,
} = this.props;
const { pageSize, onItemAdd, ruleData, containerDefinitionKey, theme } =
this.props;
const pagedFilteredItems: Array<ItemManager> = filteredItems.slice(
0,
@@ -489,7 +441,6 @@ export default class EquipmentManagerShop extends React.PureComponent<
key={`${item.getMappingId()}-${idx}`}
onItemAdd={onItemAdd}
ruleData={ruleData}
proficiencyBonus={proficiencyBonus}
containerDefinitionKey={containerDefinitionKey}
/>
))
@@ -28,14 +28,8 @@ export const GearListItem: FC<GearListProps> = ({
unequipLabel,
...props
}) => {
const {
ruleData,
proficiencyBonus,
characterTheme,
itemUtils,
helperUtils,
containerLookup,
} = useCharacterEngine();
const { ruleData, characterTheme, itemUtils, helperUtils, containerLookup } =
useCharacterEngine();
const actions = useEquipmentMethods();
const image = itemUtils.getAvatarUrl(item);
const altText = itemUtils.getName(item);
@@ -81,7 +75,6 @@ export const GearListItem: FC<GearListProps> = ({
ruleData={ruleData}
showCustomize={false}
showActions={false}
proficiencyBonus={proficiencyBonus}
/>
<EquipmentListItemActions item={item} {...actions} {...props} />
</Accordion>
@@ -29,14 +29,8 @@ export const WeaponListItem: FC<WeaponListItemProps> = ({
...props
}) => {
const actions = useEquipmentMethods();
const {
ruleData,
proficiencyBonus,
characterTheme,
itemUtils,
helperUtils,
containerLookup,
} = useCharacterEngine();
const { ruleData, characterTheme, itemUtils, helperUtils, containerLookup } =
useCharacterEngine();
const image = itemUtils.getAvatarUrl(item);
const altText = itemUtils.getName(item);
const type = itemUtils.getType(item);
@@ -80,7 +74,6 @@ export const WeaponListItem: FC<WeaponListItemProps> = ({
ruleData={ruleData}
showCustomize={false}
showActions={false}
proficiencyBonus={proficiencyBonus}
/>
<EquipmentListItemActions item={item} {...actions} {...props} />
</Accordion>
@@ -1,228 +0,0 @@
import React from "react";
import {
DisabledChevronDownSvg,
DisabledChevronUpSvg,
} from "@dndbeyond/character-components/es";
export class CollapsibleHeading extends React.PureComponent<{
className: string;
}> {
static defaultProps = {
className: "",
};
render() {
const { className, children } = this.props;
return (
<div className={["collapsible-heading", className].join(" ")}>
{children}
</div>
);
}
}
export class CollapsibleHeaderCallout extends React.PureComponent<{
extra: any;
value: any;
}> {
render() {
let { extra, value } = this.props;
return (
<span className="collapsible-header-callout">
<span className="collapsible-header-callout-extra">{extra}</span>
<span className="collapsible-header-callout-value">{value}</span>
</span>
);
}
}
export class CollapsibleHeader extends React.PureComponent<{
clsIdent?: string;
imgSrc?: string;
imgKey?: string;
iconClsNames: Array<string>;
heading: any;
metaItems: Array<string>;
callout?: any;
}> {
static defaultProps = {
iconClsNames: [],
metaItems: [],
};
constructMetaItems() {
let { metaItems } = this.props;
if (!metaItems) {
return null;
}
return (
<div className="collapsible-header-meta">
{metaItems.map((item, idx) => {
return (
<span className="collapsible-header-meta-item" key={item + idx}>
{item}
</span>
);
})}
</div>
);
}
render() {
const { clsIdent, heading, callout, imgSrc, imgKey, iconClsNames } =
this.props;
let calloutNode, headingNode;
if (typeof heading === "string") {
headingNode = (
<CollapsibleHeading className="collapsible-heading">
{heading}
</CollapsibleHeading>
);
} else {
headingNode = heading;
}
if (typeof callout === "string") {
calloutNode = (
<div className="collapsible-heading-callout">{callout}</div>
);
} else {
calloutNode = (
<div className="collapsible-heading-callout">{callout}</div>
);
}
//TODO clean this up and use all of the same thing
let iconStyles = {};
let allIconClsNames = ["collapsible-header-icon"];
let hasIcon = false;
if (imgSrc) {
iconStyles["backgroundImage"] = `url(${imgSrc})`;
hasIcon = true;
} else if (imgKey) {
allIconClsNames.push(`collapsible-header-icon-${imgKey}`);
hasIcon = true;
} else if (iconClsNames.length) {
allIconClsNames = [...allIconClsNames, ...iconClsNames];
hasIcon = true;
}
//TODO fix name to conClsNames for consistency
let conClassNames = [`collapsible-header-el`];
if (clsIdent) {
conClassNames.push(`collapsible-header-el-${clsIdent}`);
}
return (
<div className={conClassNames.join(" ")}>
{hasIcon && (
<div className={allIconClsNames.join(" ")} style={iconStyles} />
)}
<div className="collapsible-header-info">
{headingNode}
{this.constructMetaItems()}
</div>
{calloutNode}
</div>
);
}
}
export class Collapsible extends React.PureComponent<
{
trigger: any;
clsNames: Array<string>;
initiallyCollapsed: boolean;
onChange?: (isCollapsed: boolean) => void;
onOpened?: () => void;
onClosed?: () => void;
},
{
collapsed: boolean;
mouseOverHeading: boolean;
}
> {
static defaultProps = {
clsNames: [],
initiallyCollapsed: true,
};
constructor(props) {
super(props);
this.state = {
collapsed: props.initiallyCollapsed,
mouseOverHeading: false,
};
}
handleUpdate = () => {
const { onChange, onOpened, onClosed } = this.props;
const { collapsed } = this.state;
if (collapsed) {
if (onClosed) {
onClosed();
}
} else {
if (onOpened) {
onOpened();
}
}
if (onChange) {
onChange(collapsed);
}
};
handleClick = (evt) => {
this.setState(
(prevState, preProps) => ({
collapsed: !prevState.collapsed,
}),
this.handleUpdate
);
};
render() {
const { collapsed } = this.state;
const { trigger, clsNames, children } = this.props;
let headerContent = trigger;
if (typeof headerContent == "string") {
headerContent = (
<div className="collapsible-heading">{headerContent}</div>
);
}
let conClassNames = ["collapsible"];
if (clsNames) {
conClassNames = [...clsNames, ...conClassNames];
}
conClassNames.push(
this.state.collapsed ? "collapsible-collapsed" : "collapsible-opened"
);
let headerClassNames = ["collapsible-header"];
if (this.state.mouseOverHeading) {
headerClassNames.push("collapsible-header-over");
}
return (
<div className={conClassNames.join(" ")}>
<div className={headerClassNames.join(" ")} onClick={this.handleClick}>
<div className="collapsible-header-content">{headerContent}</div>
{collapsed ? <DisabledChevronDownSvg /> : <DisabledChevronUpSvg />}
</div>
{collapsed ? null : <div className="collapsible-body">{children}</div>}
</div>
);
}
}
export default Collapsible;
@@ -1,13 +0,0 @@
import Collapsible, {
CollapsibleHeader,
CollapsibleHeaderCallout,
CollapsibleHeading,
} from "./Collapsible";
export default Collapsible;
export {
Collapsible,
CollapsibleHeader,
CollapsibleHeading,
CollapsibleHeaderCallout,
};
@@ -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>
</>
);
};

Some files were not shown because too many files have changed in this diff Show More