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,
};