New source found from dndbeyond.com
This commit is contained in:
@@ -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>
|
||||
|
||||
+3
-3
@@ -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,
|
||||
};
|
||||
|
||||
+2
-2
@@ -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">
|
||||
|
||||
+2
-2
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+12
-24
@@ -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: "",
|
||||
};
|
||||
|
||||
+2
-2
@@ -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>;
|
||||
}
|
||||
}
|
||||
-9
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
+3
-3
@@ -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: "",
|
||||
|
||||
+2
-2
@@ -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">
|
||||
|
||||
+2
-2
@@ -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">
|
||||
|
||||
+7
-1
@@ -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 && (
|
||||
|
||||
+4
-4
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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">
|
||||
|
||||
+2
-2
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+8
-8
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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">
|
||||
|
||||
+2
-2
@@ -54,9 +54,9 @@ export default class VehicleBlockHeader extends React.PureComponent<Props> {
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
(<InlineSeparatedNodes nodes={metaTextSizeNodes} />)
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+6
-6
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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;
|
||||
|
||||
|
||||
+2
-2
@@ -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",
|
||||
|
||||
-137
@@ -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 };
|
||||
-266
@@ -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 };
|
||||
+2
-2
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-3
@@ -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) {
|
||||
|
||||
+3
-3
@@ -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>
|
||||
|
||||
+1
-104
@@ -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,
|
||||
};
|
||||
+31
-80
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user