New source found from dndbeyond.com

This commit is contained in:
David Kruger 2025-06-18 01:00:16 -07:00
parent 451d940294
commit 0b403376c5
30 changed files with 496 additions and 418 deletions

View File

@ -2,7 +2,6 @@ import clsx from "clsx";
import { FC, HTMLAttributes, useEffect, useState } from "react"; import { FC, HTMLAttributes, useEffect, useState } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { Tooltip } from "@dndbeyond/character-common-components/es";
import { import {
SpellUtils, SpellUtils,
EntityUtils, EntityUtils,
@ -12,6 +11,7 @@ import {
characterEnvSelectors, characterEnvSelectors,
} from "@dndbeyond/character-rules-engine/es"; } from "@dndbeyond/character-rules-engine/es";
import { Tooltip } from "~/components/Tooltip";
import { useCharacterTheme } from "~/contexts/CharacterTheme"; import { useCharacterTheme } from "~/contexts/CharacterTheme";
import { useUnpropagatedClick } from "~/hooks/useUnpropagatedClick"; import { useUnpropagatedClick } from "~/hooks/useUnpropagatedClick";
@ -77,23 +77,29 @@ export const SpellName: FC<Props> = ({
{...props} {...props}
> >
{showExpandedType && expandedInfoText !== "" && ( {showExpandedType && expandedInfoText !== "" && (
<Tooltip <>
title={expandedInfoText} <span
className={styles.expanded} data-tooltip-id="expandedInfoText"
isDarkMode={isDarkMode} data-tooltip-content={expandedInfoText}
> className={styles.expanded}
+ >
</Tooltip> +
</span>
<Tooltip id="expandedInfoText" />
</>
)} )}
{SpellUtils.getName(spell)} {SpellUtils.getName(spell)}
{SpellUtils.isCustomized(spell) && ( {SpellUtils.isCustomized(spell) && (
<Tooltip <>
title="Spell is Customized" <span
className={styles.customized} data-tooltip-id="spellIsCustomized"
isDarkMode={isDarkMode} data-tooltip-content="Spell is Customized"
> className={styles.customized}
* >
</Tooltip> *
</span>
<Tooltip id="spellIsCustomized" />
</>
)} )}
{showIcons && SpellUtils.getConcentration(spell) && ( {showIcons && SpellUtils.getConcentration(spell) && (
<ConcentrationIcon <ConcentrationIcon

View File

@ -216,9 +216,6 @@ export const useCharacterEngine = () => ({
featOption: useSelector(s.getFeatOptionLookup), featOption: useSelector(s.getFeatOptionLookup),
featChoice: useSelector(s.getFeatChoiceLookup), featChoice: useSelector(s.getFeatChoiceLookup),
gearWeaponItems: useSelector(s.getGearWeaponItems), gearWeaponItems: useSelector(s.getGearWeaponItems),
globalBackgroundSpellListIds: useSelector(s.getGlobalBackgroundSpellListIds),
globalRaceSpellListIds: useSelector(s.getGlobalRaceSpellListIds),
globalSpellListIds: useSelector(s.getGlobalSpellListIds),
hasInitiativeAdvantage: useSelector(s.getHasInitiativeAdvantage), hasInitiativeAdvantage: useSelector(s.getHasInitiativeAdvantage),
hasMaxAttunedItems: useSelector(s.hasMaxAttunedItems), hasMaxAttunedItems: useSelector(s.hasMaxAttunedItems),
hasSpells: useSelector(s.hasSpells), hasSpells: useSelector(s.hasSpells),

View File

@ -450,3 +450,6 @@ export function getFeatListContracts(contract) {
var _a, _b; var _a, _b;
return (_b = (_a = contract === null || contract === void 0 ? void 0 : contract.definition) === null || _a === void 0 ? void 0 : _a.grantedFeats) !== null && _b !== void 0 ? _b : []; return (_b = (_a = contract === null || contract === void 0 ? void 0 : contract.definition) === null || _a === void 0 ? void 0 : _a.grantedFeats) !== null && _b !== void 0 ? _b : [];
} }
export function getFeatLists(background) {
return background.featLists;
}

View File

@ -218,7 +218,7 @@ export function getDataOriginRefName(ref, refData, defaultValue, tryParent = fal
break; break;
} }
case DataOriginTypeEnum.UNKNOWN: { case DataOriginTypeEnum.UNKNOWN: {
extraDisplay = defaultValue !== null && defaultValue !== void 0 ? defaultValue : 'No Origin'; extraDisplay = defaultValue !== null && defaultValue !== void 0 ? defaultValue : 'Unknown Origin';
break; break;
} }
default: default:

View File

@ -281,3 +281,7 @@ export function getDefinitionRepeatableParentId(feat) {
export function getRepeatableParentId(feat) { export function getRepeatableParentId(feat) {
return getDefinitionRepeatableParentId(feat); return getDefinitionRepeatableParentId(feat);
} }
export function getSpellListIds(feat) {
var _a, _b;
return (_b = (_a = getDefinition(feat)) === null || _a === void 0 ? void 0 : _a.spellListIds) !== null && _b !== void 0 ? _b : [];
}

View File

@ -14,7 +14,7 @@ import { ModifierGenerators } from '../Modifier';
import { OptionAccessors, OptionGenerators } from '../Option'; import { OptionAccessors, OptionGenerators } from '../Option';
import { RaceAccessors } from '../Race'; import { RaceAccessors } from '../Race';
import { SpellGenerators } from '../Spell'; import { SpellGenerators } from '../Spell';
import { getComponentId, getComponentTypeId, getCreatureRules, getEntityTypeId, getId, getModifiers, getOptions, getUniqueKey, } from './accessors'; import { getComponentId, getComponentTypeId, getCreatureRules, getEntityTypeId, getId, getModifiers, getOptions, getSpellListIds, getUniqueKey, } from './accessors';
/** /**
* *
* @param feat * @param feat
@ -342,3 +342,20 @@ export function generateDataOriginPairedFeats(feats) {
} }
return lookup; return lookup;
} }
/**
* Generates a list of spell list IDs from the feats.
* This is used to determine which spell lists are available globally.
*
* @param feats - Array of Feat objects
* @returns Array of spell list IDs
*/
export function generateGlobalFeatsSpellListIds(feats) {
const spellListIds = [];
feats.forEach((feat) => {
const spellListIdsForFeat = getSpellListIds(feat);
if (spellListIdsForFeat.length > 0) {
spellListIds.push(...spellListIdsForFeat);
}
});
return spellListIds;
}

View File

@ -79,6 +79,13 @@ export function getDescription(prerequisiteGrouping) {
export function getSubType(prerequisite) { export function getSubType(prerequisite) {
return prerequisite.subType; return prerequisite.subType;
} }
/**
*
* @param prerequisite
*/
export function getShouldExclude(prerequisite) {
return prerequisite.shouldExclude;
}
/** /**
* @param prerequisite * @param prerequisite
*/ */

View File

@ -1,6 +1,6 @@
import { AbilityAccessors } from '../Ability'; import { AbilityAccessors } from '../Ability';
import { FormatUtils } from '../Format'; import { FormatUtils } from '../Format';
import { getEntityId, getFriendlySubtypeName, getPrerequisites, getSubType, getType, getValue } from './accessors'; import { getEntityId, getFriendlySubtypeName, getPrerequisites, getSubType, getType, getValue, getShouldExclude, } from './accessors';
import { PrerequisiteSubTypeEnum, PrerequisiteTypeEnum } from './constants'; import { PrerequisiteSubTypeEnum, PrerequisiteTypeEnum } from './constants';
import { validatePrerequisite } from './validators'; import { validatePrerequisite } from './validators';
/** /**
@ -13,19 +13,19 @@ export function getPrerequisiteFailure(prerequisite, prerequisiteData) {
case PrerequisiteTypeEnum.ABILITY_SCORE: case PrerequisiteTypeEnum.ABILITY_SCORE:
return getPrerequisiteFailureAbilityScore(prerequisite, prerequisiteData); return getPrerequisiteFailureAbilityScore(prerequisite, prerequisiteData);
case PrerequisiteTypeEnum.PROFICIENCY: case PrerequisiteTypeEnum.PROFICIENCY:
return getPrerequisiteFailureProficiency(prerequisite, prerequisiteData); return getPrerequisiteFailureProficiency(prerequisite);
case PrerequisiteTypeEnum.SPECIES: case PrerequisiteTypeEnum.SPECIES:
return getPrerequisiteFailureRace(prerequisite, prerequisiteData); return getPrerequisiteFailureRace(prerequisite);
case PrerequisiteTypeEnum.SIZE: case PrerequisiteTypeEnum.SIZE:
return getPrerequisiteFailureSize(prerequisite, prerequisiteData); return getPrerequisiteFailureSize(prerequisite);
case PrerequisiteTypeEnum.SPECIES_OPTION: case PrerequisiteTypeEnum.SPECIES_OPTION:
return getPrerequisiteFailureSubrace(prerequisite, prerequisiteData); return getPrerequisiteFailureSubrace(prerequisite);
case PrerequisiteTypeEnum.LEVEL: case PrerequisiteTypeEnum.LEVEL:
return getPrerequisiteFailureLevel(prerequisite, prerequisiteData); return getPrerequisiteFailureLevel(prerequisite);
case PrerequisiteTypeEnum.CLASS: case PrerequisiteTypeEnum.CLASS:
return getPrerequisiteFailureClass(prerequisite, prerequisiteData); return getPrerequisiteFailureClass(prerequisite);
case PrerequisiteTypeEnum.FEAT: case PrerequisiteTypeEnum.FEAT:
return getPrerequisiteFailureFeat(prerequisite, prerequisiteData); return getPrerequisiteFailureFeat(prerequisite);
case PrerequisiteTypeEnum.CLASS_FEATURE: case PrerequisiteTypeEnum.CLASS_FEATURE:
return getPrerequisiteFailureClassFeature(prerequisite); return getPrerequisiteFailureClassFeature(prerequisite);
case PrerequisiteTypeEnum.CUSTOM_VALUE: case PrerequisiteTypeEnum.CUSTOM_VALUE:
@ -39,7 +39,7 @@ export function getPrerequisiteFailure(prerequisite, prerequisiteData) {
* @param prerequisite * @param prerequisite
* @param prerequisiteData * @param prerequisiteData
*/ */
export function getPrerequisiteFailureLevel(prerequisite, prerequisiteData) { export function getPrerequisiteFailureLevel(prerequisite) {
let requiredDescription = ''; let requiredDescription = '';
switch (getSubType(prerequisite)) { switch (getSubType(prerequisite)) {
case PrerequisiteSubTypeEnum.CHARACTER_LEVEL: case PrerequisiteSubTypeEnum.CHARACTER_LEVEL:
@ -56,6 +56,7 @@ export function getPrerequisiteFailureLevel(prerequisite, prerequisiteData) {
requiredChoice: getFriendlySubtypeName(prerequisite), requiredChoice: getFriendlySubtypeName(prerequisite),
requiredDescription, requiredDescription,
}, },
shouldExclude: !!getShouldExclude(prerequisite),
}; };
} }
/** /**
@ -81,6 +82,7 @@ export function getPrerequisiteFailureAbilityScore(prerequisite, prerequisiteDat
requiredChoice: getFriendlySubtypeName(prerequisite), requiredChoice: getFriendlySubtypeName(prerequisite),
requiredDescription: `${getFriendlySubtypeName(prerequisite)} ${getValue(prerequisite)}+`, requiredDescription: `${getFriendlySubtypeName(prerequisite)} ${getValue(prerequisite)}+`,
}, },
shouldExclude: !!getShouldExclude(prerequisite),
}; };
} }
/** /**
@ -88,13 +90,14 @@ export function getPrerequisiteFailureAbilityScore(prerequisite, prerequisiteDat
* @param prerequisite * @param prerequisite
* @param prerequisiteData * @param prerequisiteData
*/ */
export function getPrerequisiteFailureProficiency(prerequisite, prerequisiteData) { export function getPrerequisiteFailureProficiency(prerequisite) {
return { return {
type: PrerequisiteTypeEnum.PROFICIENCY, type: PrerequisiteTypeEnum.PROFICIENCY,
data: { data: {
requiredChoice: getFriendlySubtypeName(prerequisite), requiredChoice: getFriendlySubtypeName(prerequisite),
requiredDescription: `${getFriendlySubtypeName(prerequisite)} Proficiency`, requiredDescription: `${getFriendlySubtypeName(prerequisite)} Proficiency`,
}, },
shouldExclude: !!getShouldExclude(prerequisite),
}; };
} }
/** /**
@ -102,13 +105,14 @@ export function getPrerequisiteFailureProficiency(prerequisite, prerequisiteData
* @param prerequisite * @param prerequisite
* @param prerequisiteData * @param prerequisiteData
*/ */
export function getPrerequisiteFailureRace(prerequisite, prerequisiteData) { export function getPrerequisiteFailureRace(prerequisite) {
return { return {
type: PrerequisiteTypeEnum.SPECIES, type: PrerequisiteTypeEnum.SPECIES,
data: { data: {
requiredChoice: getFriendlySubtypeName(prerequisite), requiredChoice: getFriendlySubtypeName(prerequisite),
requiredDescription: getFriendlySubtypeName(prerequisite), requiredDescription: getFriendlySubtypeName(prerequisite),
}, },
shouldExclude: !!getShouldExclude(prerequisite),
}; };
} }
/** /**
@ -116,13 +120,14 @@ export function getPrerequisiteFailureRace(prerequisite, prerequisiteData) {
* @param prerequisite * @param prerequisite
* @param prerequisiteData * @param prerequisiteData
*/ */
export function getPrerequisiteFailureSubrace(prerequisite, prerequisiteData) { export function getPrerequisiteFailureSubrace(prerequisite) {
return { return {
type: PrerequisiteTypeEnum.SPECIES_OPTION, type: PrerequisiteTypeEnum.SPECIES_OPTION,
data: { data: {
requiredChoice: getFriendlySubtypeName(prerequisite), requiredChoice: getFriendlySubtypeName(prerequisite),
requiredDescription: getFriendlySubtypeName(prerequisite), requiredDescription: getFriendlySubtypeName(prerequisite),
}, },
shouldExclude: !!getShouldExclude(prerequisite),
}; };
} }
/** /**
@ -130,13 +135,14 @@ export function getPrerequisiteFailureSubrace(prerequisite, prerequisiteData) {
* @param prerequisite * @param prerequisite
* @param prerequisiteData * @param prerequisiteData
*/ */
export function getPrerequisiteFailureSize(prerequisite, prerequisiteData) { export function getPrerequisiteFailureSize(prerequisite) {
return { return {
type: PrerequisiteTypeEnum.SIZE, type: PrerequisiteTypeEnum.SIZE,
data: { data: {
requiredChoice: getFriendlySubtypeName(prerequisite), requiredChoice: getFriendlySubtypeName(prerequisite),
requiredDescription: `${getFriendlySubtypeName(prerequisite)} Size`, requiredDescription: `${getFriendlySubtypeName(prerequisite)} Size`,
}, },
shouldExclude: !!getShouldExclude(prerequisite),
}; };
} }
/** /**
@ -144,13 +150,14 @@ export function getPrerequisiteFailureSize(prerequisite, prerequisiteData) {
* @param prerequisite * @param prerequisite
* @param prerequisiteData * @param prerequisiteData
*/ */
export function getPrerequisiteFailureClass(prerequisite, prerequisiteData) { export function getPrerequisiteFailureClass(prerequisite) {
return { return {
type: PrerequisiteTypeEnum.CLASS, type: PrerequisiteTypeEnum.CLASS,
data: { data: {
requiredChoice: getFriendlySubtypeName(prerequisite), requiredChoice: getFriendlySubtypeName(prerequisite),
requiredDescription: `${getFriendlySubtypeName(prerequisite)}`, requiredDescription: `${getFriendlySubtypeName(prerequisite)}`,
}, },
shouldExclude: !!getShouldExclude(prerequisite),
}; };
} }
export const getPrerequisiteFailureClassFeature = (prerequisite) => ({ export const getPrerequisiteFailureClassFeature = (prerequisite) => ({
@ -159,19 +166,21 @@ export const getPrerequisiteFailureClassFeature = (prerequisite) => ({
requiredChoice: prerequisite.friendlySubTypeName, requiredChoice: prerequisite.friendlySubTypeName,
requiredDescription: prerequisite.friendlySubTypeName, requiredDescription: prerequisite.friendlySubTypeName,
}, },
shouldExclude: !!getShouldExclude(prerequisite),
}); });
/** /**
* *
* @param prerequisite * @param prerequisite
* @param prerequisiteData * @param prerequisiteData
*/ */
export function getPrerequisiteFailureFeat(prerequisite, prerequisiteData) { export function getPrerequisiteFailureFeat(prerequisite) {
return { return {
type: PrerequisiteTypeEnum.FEAT, type: PrerequisiteTypeEnum.FEAT,
data: { data: {
requiredChoice: getFriendlySubtypeName(prerequisite), requiredChoice: getFriendlySubtypeName(prerequisite),
requiredDescription: `${getFriendlySubtypeName(prerequisite)}`, requiredDescription: `${getFriendlySubtypeName(prerequisite)}`,
}, },
shouldExclude: !!getShouldExclude(prerequisite),
}; };
} }
/** /**
@ -198,14 +207,3 @@ export function getPrerequisiteGroupingFailures(prerequisiteGrouping, prerequisi
} }
return groupingFailures.filter((group) => group.length); return groupingFailures.filter((group) => group.length);
} }
/**
* Get all prerequisites of a specific type
* @param type
* @param prereqGroups
*/
export function getPrereqsByType(type, prereqGroups) {
return prereqGroups.reduce((acc, prereq) => {
const featMappings = getPrerequisites(prereq).filter((mapping) => getType(mapping) === type);
return acc.concat(featMappings);
}, []);
}

View File

@ -2,9 +2,8 @@ import { AbilityAccessors } from '../Ability';
import { ClassAccessors } from '../Class'; import { ClassAccessors } from '../Class';
import { HelperUtils } from '../Helper'; import { HelperUtils } from '../Helper';
import { RaceAccessors } from '../Race'; import { RaceAccessors } from '../Race';
import { getEntityId, getEntityKey, getEntityTypeId, getPrerequisites, getSubType, getType, getValue, } from './accessors'; import { getEntityId, getEntityKey, getEntityTypeId, getPrerequisites, getShouldExclude, getSubType, getType, getValue, } from './accessors';
import { PrerequisiteSubTypeEnum, PrerequisiteTypeEnum } from './constants'; import { PrerequisiteSubTypeEnum, PrerequisiteTypeEnum } from './constants';
import { getPrereqsByType } from './utils';
/** /**
* *
* @param prerequisiteGrouping * @param prerequisiteGrouping
@ -63,9 +62,12 @@ export function validatePrerequisite(prerequisite, prerequisiteData) {
return true; return true;
} }
export function validatePrerequisiteClassFeature(prerequisite, prerequisiteData) { export function validatePrerequisiteClassFeature(prerequisite, prerequisiteData) {
const shouldExclude = getShouldExclude(prerequisite);
if (!prerequisite.entityId) if (!prerequisite.entityId)
return true; return true;
return Object.prototype.hasOwnProperty.call(prerequisiteData.classFeatureLookup, prerequisite.entityId); // check if class feature exists in the class feature lookup
const hasClassFeature = Object.prototype.hasOwnProperty.call(prerequisiteData.classFeatureLookup, prerequisite.entityId);
return shouldExclude ? !hasClassFeature : hasClassFeature;
} }
/** /**
* *
@ -73,10 +75,13 @@ export function validatePrerequisiteClassFeature(prerequisite, prerequisiteData)
* @param prerequisiteData * @param prerequisiteData
*/ */
export function validatePrerequisiteLevel(prerequisite, prerequisiteData) { export function validatePrerequisiteLevel(prerequisite, prerequisiteData) {
const shouldExclude = getShouldExclude(prerequisite);
switch (getSubType(prerequisite)) { switch (getSubType(prerequisite)) {
case PrerequisiteSubTypeEnum.CHARACTER_LEVEL: case PrerequisiteSubTypeEnum.CHARACTER_LEVEL:
const value = getValue(prerequisite); const value = getValue(prerequisite);
return prerequisiteData.characterLevel >= (value ? value : 0); // check if the character level is greater than or equal to the value
const characterLevelBeatsValue = prerequisiteData.characterLevel >= (value || 0);
return shouldExclude ? !characterLevelBeatsValue : characterLevelBeatsValue;
default: default:
// not implemented // not implemented
} }
@ -101,6 +106,12 @@ export function validatePrerequisiteAbilityScore(prerequisite, prerequisiteData)
if (value === null) { if (value === null) {
return true; return true;
} }
const shouldExclude = getShouldExclude(prerequisite);
// if shouldExclude is true, we want to check if the ability score is less than the value
if (shouldExclude) {
return abilityScore < value;
}
// if shouldExclude is false, we want to check if the ability score is greater than or equal to the value
return abilityScore >= value; return abilityScore >= value;
} }
/** /**
@ -109,7 +120,10 @@ export function validatePrerequisiteAbilityScore(prerequisite, prerequisiteData)
* @param prerequisiteData * @param prerequisiteData
*/ */
export function validatePrerequisiteProficiency(prerequisite, prerequisiteData) { export function validatePrerequisiteProficiency(prerequisite, prerequisiteData) {
return prerequisiteData.proficiencyLookup.hasOwnProperty(getEntityKey(prerequisite)); const shouldExclude = getShouldExclude(prerequisite);
//check if the proficiency exists in the proficiency lookup
const hasProficiency = prerequisiteData.proficiencyLookup.hasOwnProperty(getEntityKey(prerequisite));
return shouldExclude ? !hasProficiency : hasProficiency;
} }
/** /**
* *
@ -120,10 +134,11 @@ export function validatePrerequisiteRace(prerequisite, prerequisiteData) {
if (!prerequisiteData.race) { if (!prerequisiteData.race) {
return true; return true;
} }
const shouldExclude = getShouldExclude(prerequisite);
// check if race is a base race // check if race is a base race
if (RaceAccessors.getBaseRaceId(prerequisiteData.race) === getEntityId(prerequisite) && if (RaceAccessors.getBaseRaceId(prerequisiteData.race) === getEntityId(prerequisite) &&
RaceAccessors.getBaseRaceTypeId(prerequisiteData.race) === getEntityTypeId(prerequisite)) { RaceAccessors.getBaseRaceTypeId(prerequisiteData.race) === getEntityTypeId(prerequisite)) {
return true; return !shouldExclude;
} }
// check if race is specific race // check if race is specific race
return validatePrerequisiteEntityRace(prerequisite, prerequisiteData); return validatePrerequisiteEntityRace(prerequisite, prerequisiteData);
@ -149,8 +164,11 @@ function validatePrerequisiteEntityRace(prerequisite, prerequisiteData) {
if (!prerequisiteData.race) { if (!prerequisiteData.race) {
return true; return true;
} }
return (RaceAccessors.getEntityRaceId(prerequisiteData.race) === getEntityId(prerequisite) && const shouldExclude = getShouldExclude(prerequisite);
RaceAccessors.getEntityRaceTypeId(prerequisiteData.race) === getEntityTypeId(prerequisite)); // check if the race type and id match the prerequisite
const isMatch = RaceAccessors.getEntityRaceId(prerequisiteData.race) === getEntityId(prerequisite) &&
RaceAccessors.getEntityRaceTypeId(prerequisiteData.race) === getEntityTypeId(prerequisite);
return shouldExclude ? !isMatch : isMatch;
} }
/** /**
* *
@ -165,7 +183,10 @@ export function validatePrerequisiteSize(prerequisite, prerequisiteData) {
if (!sizeInfo) { if (!sizeInfo) {
return true; return true;
} }
return sizeInfo.id === getEntityId(prerequisite) && sizeInfo.entityTypeId === getEntityTypeId(prerequisite); const shouldExclude = getShouldExclude(prerequisite);
// check if size matches the prerequisite size
const isMatch = sizeInfo.id === getEntityId(prerequisite) && sizeInfo.entityTypeId === getEntityTypeId(prerequisite);
return shouldExclude ? !isMatch : isMatch;
} }
/** /**
* *
@ -177,24 +198,10 @@ export function validatePrerequisiteClass(prerequisite, prerequisiteData) {
if (entityId === null) { if (entityId === null) {
return true; return true;
} }
return Object.values(prerequisiteData.baseClassLookup).some((charClass) => ClassAccessors.getId(charClass) === entityId); const shouldExclude = getShouldExclude(prerequisite);
} // check if class exists in the base class lookup
/** const hasClass = Object.values(prerequisiteData.baseClassLookup).some((charClass) => ClassAccessors.getId(charClass) === entityId);
* @param prerequisiteGrouping return shouldExclude ? !hasClass : hasClass;
* @param prerequisiteData
*/
export function validatePrerequisiteGroupingFeat(prerequisiteGrouping, prerequisiteData) {
if (prerequisiteGrouping === null) {
return true;
}
const mappings = getPrereqsByType(PrerequisiteTypeEnum.FEAT, prerequisiteGrouping);
let isValid = true;
mappings.forEach((prerequisite) => {
if (!validatePrerequisiteFeat(prerequisite, prerequisiteData)) {
isValid = false;
}
});
return isValid;
} }
/** /**
* *
@ -206,5 +213,8 @@ export function validatePrerequisiteFeat(prerequisite, prerequisiteData) {
if (entityId === null) { if (entityId === null) {
return true; return true;
} }
return !!HelperUtils.lookupDataOrFallback(prerequisiteData.featLookup, entityId); const shouldExclude = getShouldExclude(prerequisite);
// check if feat exists in the feat lookup
const hasFeat = !!HelperUtils.lookupDataOrFallback(prerequisiteData.featLookup, entityId);
return shouldExclude ? !hasFeat : hasFeat;
} }

View File

@ -638,7 +638,7 @@ export function generateActiveCharacterSpells(spells, inventoryInfusionLookup, c
* @param optionalClassFeatures * @param optionalClassFeatures
* @param definitionPool * @param definitionPool
*/ */
export function generateSpellListDataOriginLookup(race, classes, background, optionalOrigins, optionalClassFeatures, definitionPool) { export function generateSpellListDataOriginLookup(race, classes, background, optionalOrigins, optionalClassFeatures, definitionPool, feats) {
let lookup = {}; let lookup = {};
if (race !== null) { if (race !== null) {
RaceAccessors.getDefinitionRacialTraits(race).forEach((feature) => { RaceAccessors.getDefinitionRacialTraits(race).forEach((feature) => {
@ -662,6 +662,12 @@ export function generateSpellListDataOriginLookup(race, classes, background, opt
}); });
}); });
}); });
feats.forEach((feat) => {
const uniqueKey = FeatAccessors.getUniqueKey(feat);
FeatAccessors.getSpellListIds(feat).forEach((spellListId) => {
lookup[spellListId] = DataOriginGenerators.generateDataOriginRef(DataOriginTypeEnum.FEAT, uniqueKey);
});
});
optionalOrigins.forEach((optionalOrigin) => { optionalOrigins.forEach((optionalOrigin) => {
const definitionKey = OptionalOriginAccessors.getDefinitionKey(optionalOrigin); const definitionKey = OptionalOriginAccessors.getDefinitionKey(optionalOrigin);
if (!definitionKey) { if (!definitionKey) {
@ -697,6 +703,6 @@ export function generateSpellListDataOriginLookup(race, classes, background, opt
* @param raceSpellListIds * @param raceSpellListIds
* @param backgroundSpellListIds * @param backgroundSpellListIds
*/ */
export function generateGlobalSpellListIds(raceSpellListIds, backgroundSpellListIds) { export function generateGlobalSpellListIds(raceSpellListIds, backgroundSpellListIds, featSpellListIds) {
return [...raceSpellListIds, ...backgroundSpellListIds]; return [...raceSpellListIds, ...backgroundSpellListIds, ...featSpellListIds];
} }

View File

@ -1,3 +1,4 @@
import sortBy from 'lodash/sortBy';
import { characterActions } from "../actions"; import { characterActions } from "../actions";
import { ChoiceUtils } from "../engine/Choice"; import { ChoiceUtils } from "../engine/Choice";
import { DisplayConfigurationTypeEnum } from "../engine/Core"; import { DisplayConfigurationTypeEnum } from "../engine/Core";
@ -70,10 +71,54 @@ export class FeatManager extends BaseManager {
// user-facing categories from technical-only tags. // user-facing categories from technical-only tags.
this.getCategories = () => FeatAccessors.getCategories(this.feat); this.getCategories = () => FeatAccessors.getCategories(this.feat);
//Utils //Utils
//Gets all the prereq Failures for this feat - returns an array of arrays, where each inner array is a grouping of failures
this.getPrerequisiteFailures = () => { this.getPrerequisiteFailures = () => {
const prerequisiteData = rulesEngineSelectors.getPrerequisiteData(this.state); const prerequisiteData = rulesEngineSelectors.getPrerequisiteData(this.state);
return PrerequisiteUtils.getPrerequisiteGroupingFailures(this.getPrerequisites(), prerequisiteData); return PrerequisiteUtils.getPrerequisiteGroupingFailures(this.getPrerequisites(), prerequisiteData);
}; };
// Creates a string that summarizes the prerequisite failures for this feat
this.getPrerequisiteFailuresText = () => {
//get all the prerequisite failures, sorted so that if every group failure is a missing prereq, they come first
const failures = sortBy(this.getPrerequisiteFailures(), (failureGroup) => failureGroup.every((f) => !f.shouldExclude) ? 0 : 1);
// Create an intro string for the summary
let intro = '';
// Check if all failures are either missing or prohibited
const hasOnlyMissingFailures = failures.every((failureGroup) => failureGroup.every((f) => !f.shouldExclude));
const hasOnlyProhibitedFailures = failures.every((failureGroup) => failureGroup.every((f) => f.shouldExclude));
// Determine the intro text based on the type of failures
if (hasOnlyMissingFailures) {
intro = 'Missing: ';
}
else if (hasOnlyProhibitedFailures) {
intro = 'Prohibits: ';
}
else {
intro = 'Requires: ';
}
// Each failure is sorted so that prohibited failures come last, and then combined into a single string
const combinedStrings = failures
.map((failure) => sortBy(failure, (f) => (f.shouldExclude ? 1 : 0))
.map((failureAnd) => {
if (failureAnd.shouldExclude && !hasOnlyProhibitedFailures && !hasOnlyMissingFailures) {
return `Prohibits ${failureAnd.data.requiredDescription}`;
}
return `${failureAnd.data.requiredDescription}`;
})
.reduce((acc, desc, idx) => {
let connector = '';
if (idx < failure.length - 2) {
connector = ', ';
}
else if (idx < failure.length - 1) {
connector = ' and ';
}
acc += `${desc}${connector}`;
return acc;
}, ''))
.join(' or ');
// return the intro text followed by the combined strings
return `${intro}${combinedStrings}`;
};
this.getRepeatableGroupId = () => FeatUtils.getRepeatableGroupId(this.feat); this.getRepeatableGroupId = () => FeatUtils.getRepeatableGroupId(this.feat);
this.isRepeatableFeatParent = () => this.isRepeatable() && this.getRepeatableParentId() === null; this.isRepeatableFeatParent = () => this.isRepeatable() && this.getRepeatableParentId() === null;
this.getDefinitionKey = () => { this.getDefinitionKey = () => {

View File

@ -144,7 +144,7 @@ function* filter(action) {
* *
* @param action * @param action
*/ */
function* executePostAction(action) { export function* executePostAction(action) {
if (isPostAction(action.meta)) { if (isPostAction(action.meta)) {
for (let i = 0; i < action.meta.postAction.type.length; i++) { for (let i = 0; i < action.meta.postAction.type.length; i++) {
yield put({ yield put({
@ -184,7 +184,7 @@ function* executeSyncTransactionDeactivate(action, transactionInitiatorId) {
* *
* @param action * @param action
*/ */
function* executeApi(action) { export function* executeApi(action) {
let apiLookup = { let apiLookup = {
// ACTION // ACTION
[types.ACTION_USE_SET]: apiShared.putCharacterActionLimitedUse, [types.ACTION_USE_SET]: apiShared.putCharacterActionLimitedUse,
@ -392,7 +392,7 @@ function* executeApi(action) {
yield call(apiRequest, apiPayload); yield call(apiRequest, apiPayload);
} }
} }
function* executeHandler(action) { export function* executeHandler(action) {
let handlerLookup = { let handlerLookup = {
//ACTION //ACTION
[types.ACTION_CUSTOMIZATIONS_DELETE]: sagaHandlers.handleActionCustomizationsDelete, [types.ACTION_CUSTOMIZATIONS_DELETE]: sagaHandlers.handleActionCustomizationsDelete,

View File

@ -97,7 +97,7 @@ function* filter(action) {
* *
* @param action * @param action
*/ */
function* executePostAction(action) { export function* executePostAction(action) {
if (isPostAction(action.meta)) { if (isPostAction(action.meta)) {
for (let i = 0; i < action.meta.postAction.type.length; i++) { for (let i = 0; i < action.meta.postAction.type.length; i++) {
yield put({ yield put({
@ -137,7 +137,7 @@ function* executeSyncTransactionDeactivate(action, transactionInitiatorId) {
* *
* @param action * @param action
*/ */
function* executeApi(action) { export function* executeApi(action) {
let apiLookup = { let apiLookup = {
//KNOWN_INFUSION //KNOWN_INFUSION
[types.KNOWN_INFUSION_MAPPING_SET]: apiShared.putCharacterKnownInfusion, [types.KNOWN_INFUSION_MAPPING_SET]: apiShared.putCharacterKnownInfusion,
@ -207,7 +207,7 @@ function* executeApi(action) {
yield call(apiRequest, apiPayload); yield call(apiRequest, apiPayload);
} }
} }
function* executeHandler(action) { export function* executeHandler(action) {
let handlerLookup = { let handlerLookup = {
//KNOWN_INFUSION //KNOWN_INFUSION
[types.KNOWN_INFUSION_MAPPING_CREATE]: sagaHandlers.handleKnownInfusionCreate, [types.KNOWN_INFUSION_MAPPING_CREATE]: sagaHandlers.handleKnownInfusionCreate,

View File

@ -68,6 +68,7 @@ export const getSpellListDataOriginLookup = createSelector([
characterSelectors.getOptionalOrigins, characterSelectors.getOptionalOrigins,
characterSelectors.getOptionalClassFeatures, characterSelectors.getOptionalClassFeatures,
serviceDataSelectors.getDefinitionPool, serviceDataSelectors.getDefinitionPool,
characterSelectors.getFeats,
], SpellGenerators.generateSpellListDataOriginLookup); ], SpellGenerators.generateSpellListDataOriginLookup);
/** /**
* @returns {Record<string, Array<ActionContract>>} * @returns {Record<string, Array<ActionContract>>}
@ -400,7 +401,11 @@ export const getGlobalBackgroundSpellListIds = createSelector([getBackgroundInfo
/** /**
* @returns {Array<number>} * @returns {Array<number>}
*/ */
export const getGlobalSpellListIds = createSelector([getGlobalRaceSpellListIds, getGlobalBackgroundSpellListIds], SpellGenerators.generateGlobalSpellListIds); export const getGlobalFeatsSpellListIds = createSelector([getFeats], FeatGenerators.generateGlobalFeatsSpellListIds);
/**
* @returns {Array<number>}
*/
export const getGlobalSpellListIds = createSelector([getGlobalRaceSpellListIds, getGlobalBackgroundSpellListIds, getGlobalFeatsSpellListIds], SpellGenerators.generateGlobalSpellListIds);
/** /**
* @returns {DecorationInfo} * @returns {DecorationInfo}
*/ */

View File

@ -1,5 +1,5 @@
import clsx from "clsx"; import clsx from "clsx";
import { FC, HTMLAttributes, useMemo, useState } from "react"; import { FC, FocusEvent, useMemo, useState } from "react";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { import {
@ -19,8 +19,8 @@ import {
HP_BONUS_VALUE, HP_BONUS_VALUE,
HP_OVERRIDE_MAX_VALUE, HP_OVERRIDE_MAX_VALUE,
} from "~/subApps/sheet/constants"; } from "~/subApps/sheet/constants";
import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField";
import { InputField } from "../InputField";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
export interface HpManageModalProps export interface HpManageModalProps
@ -65,52 +65,55 @@ export const HpManageModal: FC<HpManageModalProps> = ({
reset(); reset();
}; };
const transformBaseHp = (value: string): number => { const handleBaseHp = (e: FocusEvent<HTMLInputElement>) => {
let parsedNumber = HelperUtils.parseInputInt( let parsedNumber = HelperUtils.parseInputInt(
value, e.target.value,
RuleDataUtils.getMinimumHpTotal(ruleData) RuleDataUtils.getMinimumHpTotal(ruleData)
); );
return HelperUtils.clampInt(
const clampedValue = HelperUtils.clampInt(
parsedNumber, parsedNumber,
RuleDataUtils.getMinimumHpTotal(ruleData), RuleDataUtils.getMinimumHpTotal(ruleData),
HP_BASE_MAX_VALUE HP_BASE_MAX_VALUE
); );
setBaseHp(clampedValue);
}; };
const transformBonusHp = (value: string): number | null => { const handleBonusHp = (e: FocusEvent<HTMLInputElement>) => {
let parsedNumber = HelperUtils.parseInputInt(value); let parsedNumber = HelperUtils.parseInputInt(e.target.value);
if (parsedNumber === null) {
return parsedNumber; if (parsedNumber === null) return parsedNumber;
}
return HelperUtils.clampInt( const clampedValue = HelperUtils.clampInt(
parsedNumber, parsedNumber,
HP_BONUS_VALUE.MIN, HP_BONUS_VALUE.MIN,
HP_BONUS_VALUE.MAX HP_BONUS_VALUE.MAX
); );
setBonusHp(clampedValue);
}; };
const transformOverrideHp = (value: string): number | null => { const handleOverrideHp = (e: FocusEvent<HTMLInputElement>) => {
let parsedNumber = HelperUtils.parseInputInt(value, null); let parsedNumber = HelperUtils.parseInputInt(e.target.value, null);
if (parsedNumber === null) { if (parsedNumber === null) {
setOverrideHp(null);
return parsedNumber; return parsedNumber;
} }
return HelperUtils.clampInt(
const clampedValue = HelperUtils.clampInt(
parsedNumber, parsedNumber,
RuleDataUtils.getMinimumHpTotal(ruleData) RuleDataUtils.getMinimumHpTotal(ruleData)
); );
};
const handleOverrideHpUpdate = (value: number | null): void => { const clampedOverride = HelperUtils.clampInt(
const newOverride = clampedValue,
value === null RuleDataUtils.getMinimumHpTotal(ruleData),
? null HP_OVERRIDE_MAX_VALUE
: HelperUtils.clampInt( );
value,
RuleDataUtils.getMinimumHpTotal(ruleData),
HP_OVERRIDE_MAX_VALUE
);
setOverrideHp(newOverride); setOverrideHp(clampedOverride);
}; };
const totalHp = useMemo(() => { const totalHp = useMemo(() => {
@ -148,35 +151,33 @@ export const HpManageModal: FC<HpManageModalProps> = ({
<span className={styles.baseHpValue}>{baseHp}</span> <span className={styles.baseHpValue}>{baseHp}</span>
</p> </p>
) : ( ) : (
<FormInputField <InputField
className={styles.inputField}
label="Rolled HP" label="Rolled HP"
initialValue={baseHp} initialValue={baseHp}
type="number" type="number"
onBlur={(value) => setBaseHp(value as number)} onBlur={handleBaseHp}
transformValueOnBlur={transformBaseHp}
/> />
)} )}
<FormInputField <InputField
className={styles.inputField}
label="HP Modifier" label="HP Modifier"
initialValue={bonusHp} initialValue={bonusHp}
type="number" type="number"
placeholder={"--"} placeholder="--"
onBlur={(value) => setBonusHp(value as number)} onBlur={handleBonusHp}
transformValueOnBlur={transformBonusHp}
/> />
<FormInputField <InputField
inputAttributes={ className={styles.inputField}
{
min: RuleDataUtils.getMinimumHpTotal(ruleData),
max: HP_OVERRIDE_MAX_VALUE,
} as HTMLAttributes<HTMLInputElement>
}
label="Override HP" label="Override HP"
initialValue={overrideHp} initialValue={overrideHp}
type="number" type="number"
placeholder={"--"} placeholder={"--"}
onBlur={handleOverrideHpUpdate} inputProps={{
transformValueOnBlur={transformOverrideHp} min: RuleDataUtils.getMinimumHpTotal(ruleData),
max: HP_OVERRIDE_MAX_VALUE,
}}
onBlur={handleOverrideHp}
/> />
</div> </div>

View File

@ -0,0 +1,132 @@
import clsx from "clsx";
import {
ChangeEvent,
FC,
FocusEvent,
HTMLAttributes,
HTMLInputTypeAttribute,
useEffect,
useState,
} from "react";
import { v4 as uuidv4 } from "uuid";
import styles from "./styles.module.css";
interface InputProps extends HTMLAttributes<HTMLInputElement> {
min?: number;
max?: number;
autoComplete?: string;
}
export interface InputFieldProps extends HTMLAttributes<HTMLInputElement> {
errorMessage?: string;
initialValue?: string | number | null;
label: string;
maxLength?: number;
placeholder?: string;
type?: HTMLInputTypeAttribute;
inputProps?: InputProps;
}
export const InputField: FC<InputFieldProps> = ({
className,
errorMessage,
initialValue = "",
inputProps,
label,
maxLength,
onBlur,
onChange,
onFocus,
placeholder,
type = "text",
...props
}) => {
// Set default error message if not provided
errorMessage ||= `The maximum length is ${maxLength} characters.`;
// Set up component state and a shared id
const [value, setValue] = useState(initialValue);
const [showError, setShowError] = useState(false);
const id = props.id ?? `input-field-${uuidv4()}`;
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const valueLength = e.target.value.length;
if (maxLength) {
// If the value length exceeds maxLength, show error
if (valueLength > maxLength) {
setShowError(true);
//return; Prevent further processing if maxLength exceeded
return;
}
// If the value length is equal to maxLength, show error
if (valueLength === maxLength) {
setShowError(true);
}
// If the value length is less than maxLength, hide error
if (valueLength < maxLength) {
setShowError(false);
}
}
// Update component state
setValue(e.target.value);
// Call the provided onChange handler if it exists
if (onChange) onChange(e);
return;
};
const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
const intValue = parseInt(e.target.value);
// If entered value is below the minimum, reset to minimum
if (!isNaN(intValue) && inputProps?.min && intValue < inputProps?.min) {
setValue(inputProps.min);
}
// If entered value is above the maximum and no initialValue exists, reset to maximum
if (!isNaN(intValue) && inputProps?.max && intValue > inputProps?.max) {
setValue(inputProps.max);
}
// Call the provided onBlur handler if it exists
if (onBlur) onBlur(e);
// Reset error state on blur
setShowError(false);
};
const handleFocus = (e: FocusEvent<HTMLInputElement>) => {
// Call the provided onFocus handler if it exists
if (onFocus) onFocus(e);
// Check for maxLength error
const valueLength = e.target.value.length;
if (maxLength && valueLength > maxLength - 1) setShowError(true);
};
useEffect(() => {
// Update the value when initialValue changes
setValue(initialValue);
// Reset error state when initial value changes
setShowError(false);
}, [initialValue]);
const inputAttrs = { id, type, placeholder };
return (
<div className={clsx([styles.inputField, className])} {...props}>
<label className={styles.label} htmlFor={id}>
{label}
</label>
<input
className={styles.input}
value={value ?? ""}
onBlur={handleBlur}
onChange={handleChange}
onFocus={handleFocus}
{...inputAttrs}
{...inputProps}
/>
{showError && <span className={styles.error}>{errorMessage}</span>}
</div>
);
};

View File

@ -16,11 +16,11 @@ import {
import { useCharacterEngine } from "~/hooks/useCharacterEngine"; import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { builderActions } from "~/tools/js/CharacterBuilder/actions"; import { builderActions } from "~/tools/js/CharacterBuilder/actions";
import { builderSelectors } from "~/tools/js/CharacterBuilder/selectors"; import { builderSelectors } from "~/tools/js/CharacterBuilder/selectors";
import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField";
import { PortraitManager } from "~/tools/js/Shared/containers/panes/DecoratePane"; import { PortraitManager } from "~/tools/js/Shared/containers/panes/DecoratePane";
import Tooltip from "~/tools/js/commonComponents/Tooltip"; import Tooltip from "~/tools/js/commonComponents/Tooltip";
import { CharacterAvatarPortrait } from "~/tools/js/smartComponents/CharacterAvatar"; import { CharacterAvatarPortrait } from "~/tools/js/smartComponents/CharacterAvatar";
import { InputField } from "../InputField";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
interface Props extends HTMLAttributes<HTMLDivElement> { interface Props extends HTMLAttributes<HTMLDivElement> {
@ -56,7 +56,7 @@ export const PortraitName: FC<Props> = ({
setCurrentSuggestedNames(suggestedNames); setCurrentSuggestedNames(suggestedNames);
}, [suggestedNames]); }, [suggestedNames]);
const handleNameFieldSet = (value: string | null): void => { const handleNameFieldSet = (value: string) => {
let updatedName = value ?? ""; let updatedName = value ?? "";
// If the flag useDefaultCharacterName is true and value is falsy, set the name to the DefaultCharacterName // If the flag useDefaultCharacterName is true and value is falsy, set the name to the DefaultCharacterName
@ -137,18 +137,16 @@ export const PortraitName: FC<Props> = ({
</> </>
)} )}
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<FormInputField <InputField
label="Character Name" label="Character Name"
onBlur={handleNameFieldSet}
initialValue={localValue} initialValue={localValue}
inputAttributes={
{
spellCheck: false,
autoComplete: "off",
} as HTMLAttributes<HTMLInputElement>
}
maxLength={InputLimits.characterNameMaxLength} maxLength={InputLimits.characterNameMaxLength}
maxLengthErrorMsg={CharacterNameLimitMsg} errorMessage={CharacterNameLimitMsg}
onBlur={(e) => handleNameFieldSet(e.target.value)}
inputProps={{
spellCheck: false,
autoComplete: "off",
}}
/> />
<Button <Button
className={styles.suggestionButton} className={styles.suggestionButton}

View File

@ -11,7 +11,6 @@ import Collapsible, {
CollapsibleHeaderContent, CollapsibleHeaderContent,
} from "~/tools/js/smartComponents/Collapsible"; } from "~/tools/js/smartComponents/Collapsible";
import { TodoIcon } from "~/tools/js/smartComponents/Icons"; import { TodoIcon } from "~/tools/js/smartComponents/Icons";
import PrerequisiteFailureSummary from "~/tools/js/smartComponents/PrerequisiteFailureSummary";
import { CharacterTheme } from "~/types"; import { CharacterTheme } from "~/types";
import { import {
@ -110,10 +109,9 @@ export const Feat: FC<Props> = ({
<Reference name={feat.getPrimarySourceName()} /> <Reference name={feat.getPrimarySourceName()} />
</span> </span>
{showFailures && ( {showFailures && (
<PrerequisiteFailureSummary <div className={styles.failures}>
failures={feat.getPrerequisiteFailures()} {feat.getPrerequisiteFailuresText()}
className={styles.failures} </div>
/>
)} )}
</div> </div>
} }

View File

@ -1,9 +1,13 @@
import { FocusEvent } from "react";
import { import {
AbilityManager, AbilityManager,
HelperUtils, HelperUtils,
} from "@dndbeyond/character-rules-engine/es"; } from "@dndbeyond/character-rules-engine/es";
import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField";
import { InputField } from "~/subApps/builder/components/InputField";
import styles from "./styles.module.css";
interface Props { interface Props {
abilities: Array<AbilityManager>; abilities: Array<AbilityManager>;
@ -16,14 +20,19 @@ export default function AbilityScoreManagerManual({
minScore = 3, minScore = 3,
maxScore = 18, maxScore = 18,
}: Props) { }: Props) {
const handleTransformValueOnBlur = (value: string): number | null => { const handleBlur = (
const parsedValue = HelperUtils.parseInputInt(value); e: FocusEvent<HTMLInputElement>,
abilityScore: AbilityManager
) => {
// Get the int value from the input
const parsedValue = HelperUtils.parseInputInt(e.target.value);
// Create a clamped variable to hold the clamped value
let clampedValue: number | null = null; let clampedValue: number | null = null;
if (parsedValue !== null) { // If the value is valid, clamp it to the min and max scores
if (parsedValue !== null)
clampedValue = HelperUtils.clampInt(parsedValue, minScore, maxScore); clampedValue = HelperUtils.clampInt(parsedValue, minScore, maxScore);
} // Set the score change from the clampped value
abilityScore.handleScoreChange(clampedValue);
return clampedValue;
}; };
return ( return (
@ -35,11 +44,17 @@ export default function AbilityScoreManagerManual({
key={abilityScore.getId()} key={abilityScore.getId()}
data-stat-id={abilityScore.getId()} data-stat-id={abilityScore.getId()}
> >
<FormInputField <InputField
transformValueOnBlur={handleTransformValueOnBlur} className={styles.input}
onBlur={(value) => abilityScore.handleScoreChange(Number(value))} type="number"
label={abilityScore.getLabel() ?? ""} label={abilityScore.getLabel() ?? ""}
initialValue={abilityScore.getBaseScore()} initialValue={abilityScore.getBaseScore()}
inputProps={{
min: minScore,
max: maxScore,
autoComplete: "off",
}}
onBlur={(e) => handleBlur(e, abilityScore)}
/> />
<div className="ability-score-manager-stat-total"> <div className="ability-score-manager-stat-total">
Total: {abilityScore.getTotalScore() ?? "--"} Total: {abilityScore.getTotalScore() ?? "--"}

View File

@ -1,6 +1,6 @@
import axios, { Canceler } from "axios"; import axios, { Canceler } from "axios";
import { orderBy } from "lodash"; import { orderBy } from "lodash";
import React, { HTMLAttributes, useContext } from "react"; import React, { FocusEvent, HTMLAttributes, useContext } from "react";
import { DispatchProp } from "react-redux"; import { DispatchProp } from "react-redux";
import { LoadingPlaceholder, Select } from "@dndbeyond/character-components/es"; import { LoadingPlaceholder, Select } from "@dndbeyond/character-components/es";
@ -52,12 +52,12 @@ import { HtmlContent } from "~/components/HtmlContent";
import { Link } from "~/components/Link"; import { Link } from "~/components/Link";
import { useSource } from "~/hooks/useSource"; import { useSource } from "~/hooks/useSource";
import { EditorWithDialog } from "~/subApps/builder/components/EditorWithDialog"; import { EditorWithDialog } from "~/subApps/builder/components/EditorWithDialog";
import { InputField } from "~/subApps/builder/components/InputField";
import { RouteKey } from "~/subApps/builder/constants"; import { RouteKey } from "~/subApps/builder/constants";
import { import {
ModalData, ModalData,
useModalManager, useModalManager,
} from "~/subApps/builder/contexts/ModalManager"; } from "~/subApps/builder/contexts/ModalManager";
import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField";
import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice"; import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice";
import { toastMessageActions } from "../../../../Shared/actions"; import { toastMessageActions } from "../../../../Shared/actions";
@ -195,7 +195,7 @@ class CustomBackgroundManager extends React.PureComponent<
<div className="custom-background-property-value"> <div className="custom-background-property-value">
<input <input
type="text" type="text"
defaultValue={name ? name : ""} defaultValue={name || ""}
onBlur={this.handleTextInputBlur.bind(this, "name")} onBlur={this.handleTextInputBlur.bind(this, "name")}
/> />
</div> </div>
@ -204,7 +204,7 @@ class CustomBackgroundManager extends React.PureComponent<
<div className="custom-background-property custom-background-property-textarea"> <div className="custom-background-property custom-background-property-textarea">
<div className="custom-background-property-value"> <div className="custom-background-property-value">
<textarea <textarea
defaultValue={description ? description : ""} defaultValue={description || ""}
onBlur={this.handleTextInputBlur.bind(this, "description")} onBlur={this.handleTextInputBlur.bind(this, "description")}
/> />
</div> </div>
@ -471,35 +471,35 @@ class DescriptionManage extends React.PureComponent<Props, State> {
dispatch(characterActions.traitSet(traitType, content)); dispatch(characterActions.traitSet(traitType, content));
}; };
handleHairChange = (value: string): void => { handleHairChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch(characterActions.hairSet(value)); dispatch(characterActions.hairSet(e.target.value));
}; };
handleSkinChange = (value: string): void => { handleSkinChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch(characterActions.skinSet(value)); dispatch(characterActions.skinSet(e.target.value));
}; };
handleEyesChange = (value: string): void => { handleEyesChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch(characterActions.eyesSet(value)); dispatch(characterActions.eyesSet(e.target.value));
}; };
handleHeightChange = (value: string): void => { handleHeightChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch(characterActions.heightSet(value)); dispatch(characterActions.heightSet(e.target.value));
}; };
handleWeightChange = (value: string): void => { handleWeightChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props; const { dispatch } = this.props;
let parsedValue = HelperUtils.parseInputInt(value); let parsedValue = this.handleTransformValueToNumberOnBlur(e.target.value);
dispatch(characterActions.weightSet(parsedValue)); dispatch(characterActions.weightSet(parsedValue));
}; };
handleAgeChange = (value: string): void => { handleAgeChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props; const { dispatch } = this.props;
let parsedValue = HelperUtils.parseInputInt(value); let parsedValue = this.handleTransformValueToNumberOnBlur(e.target.value);
dispatch(characterActions.ageSet(parsedValue)); dispatch(characterActions.ageSet(parsedValue));
}; };
@ -517,9 +517,9 @@ class DescriptionManage extends React.PureComponent<Props, State> {
return clampedValue; return clampedValue;
}; };
handleGenderChange = (value: string): void => { handleGenderChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch(characterActions.genderSet(value)); dispatch(characterActions.genderSet(e.target.value));
}; };
handleBackgroundChangePromise = ( handleBackgroundChangePromise = (
@ -608,9 +608,9 @@ class DescriptionManage extends React.PureComponent<Props, State> {
dispatch(characterActions.lifestyleSet(HelperUtils.parseInputInt(value))); dispatch(characterActions.lifestyleSet(HelperUtils.parseInputInt(value)));
}; };
handleFaithChange = (value: string): void => { handleFaithChange = (e: FocusEvent<HTMLInputElement>): void => {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch(characterActions.faithSet(value)); dispatch(characterActions.faithSet(e.target.value));
}; };
handleCustomBackgroundSave = (properties: any): void => { handleCustomBackgroundSave = (properties: any): void => {
@ -716,7 +716,7 @@ class DescriptionManage extends React.PureComponent<Props, State> {
<DetailChoice <DetailChoice
{...choice} {...choice}
choice={choice} choice={choice}
key={choiceId ? choiceId : ""} key={choiceId || ""}
options={availableOptions} options={availableOptions}
onChange={this.handleChoiceChange} onChange={this.handleChoiceChange}
choiceInfo={choiceInfo} choiceInfo={choiceInfo}
@ -1035,7 +1035,7 @@ class DescriptionManage extends React.PureComponent<Props, State> {
) : ( ) : (
<HtmlContent <HtmlContent
className="description-manage-background-description" className="description-manage-background-description"
html={shortDescription ? shortDescription : ""} html={shortDescription || ""}
withoutTooltips withoutTooltips
/> />
)} )}
@ -1325,17 +1325,17 @@ class DescriptionManage extends React.PureComponent<Props, State> {
{alignmentDescriptionNode} {alignmentDescriptionNode}
</div> </div>
<div className="description-manage-faith"> <div className="description-manage-faith">
<FormInputField <InputField
label="Faith" label="Faith"
initialValue={faith} initialValue={faith}
onBlur={this.handleFaithChange} onBlur={this.handleFaithChange}
inputAttributes={ maxLength={512}
inputProps={
{ {
spellCheck: false, spellCheck: false,
autoComplete: "off", autoComplete: "off",
} as HTMLAttributes<HTMLInputElement> } as HTMLAttributes<HTMLInputElement>
} }
maxLength={512}
/> />
</div> </div>
<div className="description-manage-lifestyle"> <div className="description-manage-lifestyle">
@ -1367,47 +1367,52 @@ class DescriptionManage extends React.PureComponent<Props, State> {
]} ]}
variant="paper" variant="paper"
> >
<FormInputField <InputField
className={styles.inputField}
label="Hair" label="Hair"
initialValue={hair} initialValue={hair}
onBlur={this.handleHairChange} onBlur={this.handleHairChange}
maxLength={50} maxLength={50}
/> />
<FormInputField <InputField
className={styles.inputField}
label="Skin" label="Skin"
initialValue={skin} initialValue={skin}
onBlur={this.handleSkinChange} onBlur={this.handleSkinChange}
maxLength={50} maxLength={50}
/> />
<FormInputField <InputField
className={styles.inputField}
label="Eyes" label="Eyes"
initialValue={eyes} initialValue={eyes}
onBlur={this.handleEyesChange} onBlur={this.handleEyesChange}
maxLength={50} maxLength={50}
/> />
<FormInputField <InputField
className={styles.inputField}
label="Height" label="Height"
initialValue={height} initialValue={height}
onBlur={this.handleHeightChange} onBlur={this.handleHeightChange}
maxLength={50} maxLength={50}
/> />
<FormInputField <InputField
className={styles.inputField}
label="Weight (lbs)" label="Weight (lbs)"
type={"number"} type="number"
initialValue={weight} initialValue={weight}
onBlur={this.handleWeightChange} onBlur={this.handleWeightChange}
transformValueOnBlur={this.handleTransformValueToNumberOnBlur} inputProps={numberInputAttributes}
inputAttributes={numberInputAttributes}
/> />
<FormInputField <InputField
className={styles.inputField}
label="Age (Years)" label="Age (Years)"
type={"number"} type="number"
initialValue={age} initialValue={age}
onBlur={this.handleAgeChange} onBlur={this.handleAgeChange}
transformValueOnBlur={this.handleTransformValueToNumberOnBlur} inputProps={numberInputAttributes}
inputAttributes={numberInputAttributes}
/> />
<FormInputField <InputField
className={styles.inputField}
label="Gender" label="Gender"
initialValue={gender} initialValue={gender}
onBlur={this.handleGenderChange} onBlur={this.handleGenderChange}

View File

@ -1,5 +1,10 @@
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
import React, { ChangeEvent, HTMLAttributes, ReactNode } from "react"; import React, {
ChangeEvent,
FocusEvent,
HTMLAttributes,
ReactNode,
} from "react";
import { DispatchProp } from "react-redux"; import { DispatchProp } from "react-redux";
import { import {
@ -24,12 +29,12 @@ import {
import { Dice } from "@dndbeyond/dice"; import { Dice } from "@dndbeyond/dice";
import { SourceCategoryDescription } from "~/constants"; import { SourceCategoryDescription } from "~/constants";
import { InputField } from "~/subApps/builder/components/InputField";
import { RouteKey } from "~/subApps/builder/constants"; import { RouteKey } from "~/subApps/builder/constants";
import { import {
ModalData, ModalData,
useModalManager, useModalManager,
} from "~/subApps/builder/contexts/ModalManager"; } from "~/subApps/builder/contexts/ModalManager";
import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField";
import UserRoles from "~/tools/js/Shared/constants/UserRoles"; import UserRoles from "~/tools/js/Shared/constants/UserRoles";
import PrivacyTypeRadio from "~/tools/js/smartComponents/PrivacyTypeRadio"; import PrivacyTypeRadio from "~/tools/js/smartComponents/PrivacyTypeRadio";
@ -723,66 +728,67 @@ class HomeBasicInfo extends React.PureComponent<Props> {
this.forceUpdate(); this.forceUpdate();
}} }}
/> />
<FormInputField <InputField
label="Long Description" label="Long Description"
initialValue={premadeInfo.definition.longDescription} initialValue={premadeInfo.definition.longDescription}
inputAttributes={inputAttributes} inputProps={inputAttributes}
onBlur={(value: string) => { onBlur={(e: FocusEvent<HTMLInputElement>) => {
premadeInfo.definition.longDescription = value; premadeInfo.definition.longDescription = e.target.value;
this.handlePremadeInfoChanged(premadeInfo); this.handlePremadeInfoChanged(premadeInfo);
}} }}
/> />
<FormInputField <InputField
label="Short Description" label="Short Description"
initialValue={premadeInfo.definition.shortDescription} initialValue={premadeInfo.definition.shortDescription}
inputAttributes={inputAttributes} inputProps={inputAttributes}
onBlur={(value: string) => { onBlur={(e: FocusEvent<HTMLInputElement>) => {
premadeInfo.definition.shortDescription = value; premadeInfo.definition.shortDescription = e.target.value;
this.handlePremadeInfoChanged(premadeInfo); this.handlePremadeInfoChanged(premadeInfo);
}} }}
/> />
<FormInputField <InputField
label="Image Url" label="Image Url"
initialValue={premadeInfo.definition.imageUrl} initialValue={premadeInfo.definition.imageUrl}
inputAttributes={inputAttributes} inputProps={inputAttributes}
onBlur={(value: string) => { onBlur={(e: FocusEvent<HTMLInputElement>) => {
premadeInfo.definition.imageUrl = value; premadeInfo.definition.imageUrl = e.target.value;
this.handlePremadeInfoChanged(premadeInfo); this.handlePremadeInfoChanged(premadeInfo);
}} }}
/> />
<FormInputField <InputField
label="Image Alt Text" label="Image Alt Text"
initialValue={premadeInfo.definition.imageAltText} initialValue={premadeInfo.definition.imageAltText}
inputAttributes={inputAttributes} inputProps={inputAttributes}
onBlur={(value: string) => { onBlur={(e: FocusEvent<HTMLInputElement>) => {
premadeInfo.definition.imageAltText = value; premadeInfo.definition.imageAltText = e.target.value;
this.handlePremadeInfoChanged(premadeInfo); this.handlePremadeInfoChanged(premadeInfo);
}} }}
/> />
<FormInputField <InputField
label="Mobile Image Url" label="Mobile Image Url"
initialValue={premadeInfo.definition.mobileImageUrl} initialValue={premadeInfo.definition.mobileImageUrl}
inputAttributes={inputAttributes} inputProps={inputAttributes}
onBlur={(value: string) => { onBlur={(e: FocusEvent<HTMLInputElement>) => {
premadeInfo.definition.mobileImageUrl = value; premadeInfo.definition.mobileImageUrl = e.target.value;
this.handlePremadeInfoChanged(premadeInfo); this.handlePremadeInfoChanged(premadeInfo);
}} }}
/> />
<FormInputField <InputField
label="Mobile Image Accessibility" label="Mobile Image Accessibility"
initialValue={premadeInfo.definition.mobileImageAccessibility} initialValue={premadeInfo.definition.mobileImageAccessibility}
inputAttributes={inputAttributes} inputProps={inputAttributes}
onBlur={(value: string) => { onBlur={(e: FocusEvent<HTMLInputElement>) => {
premadeInfo.definition.mobileImageAccessibility = value; premadeInfo.definition.mobileImageAccessibility =
e.target.value;
this.handlePremadeInfoChanged(premadeInfo); this.handlePremadeInfoChanged(premadeInfo);
}} }}
/> />
<FormInputField <InputField
label="Theme Color Hex Code" label="Theme Color Hex Code"
initialValue={premadeInfo.definition.themeColor} initialValue={premadeInfo.definition.themeColor}
inputAttributes={inputAttributes} inputProps={inputAttributes}
onBlur={(value: string) => { onBlur={(e: FocusEvent<HTMLInputElement>) => {
premadeInfo.definition.themeColor = value; premadeInfo.definition.themeColor = e.target.value;
this.handlePremadeInfoChanged(premadeInfo); this.handlePremadeInfoChanged(premadeInfo);
}} }}
/> />

View File

@ -62,8 +62,6 @@ interface Props {
theme?: CharacterTheme; theme?: CharacterTheme;
} }
const infoItemProps = { role: "listItem", inline: true };
export const ActionDetail = ({ export const ActionDetail = ({
showCustomize = true, showCustomize = true,
largePoolMinAmount = 11, largePoolMinAmount = 11,
@ -79,6 +77,8 @@ export const ActionDetail = ({
theme, theme,
inventoryLookup, inventoryLookup,
}: Props) => { }: Props) => {
const infoItemProps = { role: "listItem", inline: true };
const handleRemoveCustomizations = () => { const handleRemoveCustomizations = () => {
if (onCustomizationsRemove) { if (onCustomizationsRemove) {
onCustomizationsRemove(); onCustomizationsRemove();
@ -427,17 +427,17 @@ export const ActionDetail = ({
</InfoItem> </InfoItem>
)} )}
{attackSubtypeId && ( {attackSubtypeId && (
<InfoItem label="Attack Type:"> <InfoItem label="Attack Type:" {...infoItemProps}>
{ActionUtils.getAttackSubtypeName(action)} {ActionUtils.getAttackSubtypeName(action)}
</InfoItem> </InfoItem>
)} )}
{requiresAttackRoll && ( {requiresAttackRoll && (
<InfoItem label="To Hit:"> <InfoItem label="To Hit:" {...infoItemProps}>
<NumberDisplay type="signed" number={toHit} /> <NumberDisplay type="signed" number={toHit} />
</InfoItem> </InfoItem>
)} )}
{requiresSavingThrow && ( {requiresSavingThrow && (
<InfoItem label="Attack/Save:"> <InfoItem label="Attack/Save:" {...infoItemProps}>
{saveStateId !== null {saveStateId !== null
? RuleDataUtils.getStatNameById(saveStateId, ruleData) ? RuleDataUtils.getStatNameById(saveStateId, ruleData)
: ""}{" "} : ""}{" "}
@ -446,14 +446,14 @@ export const ActionDetail = ({
)} )}
{renderDamageProperties()} {renderDamageProperties()}
{requiresAttackRoll && ( {requiresAttackRoll && (
<InfoItem label="Stat:"> <InfoItem label="Stat:" {...infoItemProps}>
{statId === null {statId === null
? "--" ? "--"
: RuleDataUtils.getStatNameById(statId, ruleData)} : RuleDataUtils.getStatNameById(statId, ruleData)}
</InfoItem> </InfoItem>
)} )}
{rangeAreas.length > 0 && ( {rangeAreas.length > 0 && (
<InfoItem label="Range/Area:"> <InfoItem label="Range/Area:" {...infoItemProps}>
{rangeAreas.map((node, idx) => ( {rangeAreas.map((node, idx) => (
<React.Fragment key={idx}> <React.Fragment key={idx}>
{node} {node}
@ -462,8 +462,16 @@ export const ActionDetail = ({
))} ))}
</InfoItem> </InfoItem>
)} )}
{isProficient && <InfoItem label="Proficient:">Yes</InfoItem>} {isProficient && (
{notes && <InfoItem label="Notes:">{notes}</InfoItem>} <InfoItem label="Proficient:" {...infoItemProps}>
Yes
</InfoItem>
)}
{notes && (
<InfoItem label="Notes:" {...infoItemProps}>
{notes}
</InfoItem>
)}
</div> </div>
); );
}; };

View File

@ -92,7 +92,7 @@ export default function SpellManagerContainer({
const getData = useCallback( const getData = useCallback(
async function getData() { async function getData() {
let classSpellMap = await spellsManager.getSpellShoppe(); let classSpellMap = await spellsManager.getSpellShoppe(true);
if (!classSpellMap[charClassId] || shouldFetch) { if (!classSpellMap[charClassId] || shouldFetch) {
classSpellMap = await spellsManager.getSpellShoppe(true); classSpellMap = await spellsManager.getSpellShoppe(true);
} }

View File

@ -1,139 +0,0 @@
import clsx from "clsx";
import {
ChangeEvent,
FC,
FocusEvent,
HTMLAttributes,
useEffect,
useRef,
useState,
} from "react";
import { v4 as uuidv4 } from "uuid";
import { useMaxLengthErrorHandling } from "~/hooks/useErrorHandling/useMaxLengthErrorHandling";
export interface FormInputFieldProps
extends Omit<
HTMLAttributes<HTMLDivElement>,
"onChange" | "onBlur" | "onFocus"
> {
label: string;
type?: string;
placeholder?: string;
initialValue?: string | number | null;
inputAttributes?: HTMLAttributes<HTMLInputElement>;
maxLength?: number;
maxLengthErrorMsg?: string;
variant?: "default" | "small";
// Handlers
onChange?: (value: string | number | null) => void;
onBlur?: (value: string | number | null) => void;
onFocus?: (value: string | number | null) => void;
transformValueOnChange?: (value: string) => string | number | null;
transformValueOnBlur?: (value: string) => string | number | null;
transformValueOnFocus?: (value: string) => string | number | null;
}
export const FormInputField: FC<FormInputFieldProps> = ({
label, // req
type = "text",
placeholder = "",
initialValue = "",
inputAttributes = {},
maxLength,
maxLengthErrorMsg = "",
...handlers
}) => {
const prevInitialValue = useRef(initialValue);
const initialLengthErrorState = maxLength
? (initialValue?.toString().length ?? 0) >= maxLength
: false;
const { MaxLengthErrorMessage, hideError, handleMaxLengthErrorMsg } =
useMaxLengthErrorHandling(
initialLengthErrorState,
maxLength ?? null,
maxLengthErrorMsg
);
const guid = `FIF_${uuidv4()}`;
const [value, setValue] = useState(initialValue);
const handleOnChange = (evt: ChangeEvent<HTMLInputElement>): void => {
const { onChange = null, transformValueOnChange = null } = handlers;
const { value: targetElementValue } = evt.target;
const value = transformValueOnChange
? transformValueOnChange?.(targetElementValue)
: targetElementValue;
onChange?.(value);
setValue(value);
if (value !== null) {
handleMaxLengthErrorMsg(value.toString());
}
};
const handleOnBlur = (evt: FocusEvent<HTMLInputElement>) => {
const { onBlur = null, transformValueOnBlur = null } = handlers;
const { value: targetElementValue } = evt.target;
const value = transformValueOnBlur
? transformValueOnBlur?.(targetElementValue)
: targetElementValue;
onBlur?.(value);
if (value !== targetElementValue) {
setValue(value);
}
// Always hide any length warnings when leaving the field since you can't go over the limit anyways
hideError();
};
const handleOnFocus = (evt: FocusEvent<HTMLInputElement>) => {
const { onFocus = null, transformValueOnFocus = null } = handlers;
const { value: targetElementValue } = evt.target;
const value = transformValueOnFocus
? transformValueOnFocus?.(targetElementValue)
: targetElementValue;
onFocus?.(value);
if (value !== targetElementValue) {
setValue(value);
}
if (value !== null) {
handleMaxLengthErrorMsg(value.toString());
}
};
useEffect(() => {
if (initialValue !== prevInitialValue.current) {
setValue(initialValue ?? "");
prevInitialValue.current = initialValue;
}
}, [initialValue]);
return (
<div className={clsx(["builder-field", "form-input-field"])}>
<span className="builder-field-label">
<label
className="builder-field-heading form-input-field-label"
htmlFor={guid}
>
{label}
</label>
</span>
<span className="builder-field-input">
<input
className="builder-field-value"
id={guid}
type={type}
placeholder={placeholder}
onChange={handleOnChange}
onBlur={handleOnBlur}
onFocus={handleOnFocus}
value={value ?? ""}
maxLength={maxLength || undefined}
{...inputAttributes}
/>
</span>
<MaxLengthErrorMessage />
</div>
);
};

View File

@ -1,44 +0,0 @@
import clsx from "clsx";
import * as React from "react";
import { PrerequisiteFailure } from "@dndbeyond/character-rules-engine/es";
interface Props {
className: string;
failures: Array<Array<PrerequisiteFailure>>;
}
export default class PrerequisiteFailureSummary extends React.PureComponent<
Props,
{}
> {
static defaultProps = {
className: "",
};
render() {
const { failures, className } = this.props;
let strings: string = failures
.map((failure) =>
failure
.map((failureAnd) => failureAnd.data.requiredDescription)
.reduce((acc, desc, idx) => {
let connector: string = "";
if (idx < failure.length - 2) {
connector = ", ";
} else if (idx < failure.length - 1) {
connector = " and ";
}
acc += `${desc}${connector}`;
return acc;
}, "")
)
.join(" or ");
return (
<div className={clsx(["ddbc-prerequisite-failure-summary", className])}>
Missing: {strings.length > 0 ? strings : "Unknown"}
</div>
);
}
}

View File

@ -1,4 +0,0 @@
import PrerequisiteFailureSummary from "./PrerequisiteFailureSummary";
export default PrerequisiteFailureSummary;
export { PrerequisiteFailureSummary };

View File

@ -1,2 +1,2 @@
// extracted by mini-css-extract-plugin // extracted by mini-css-extract-plugin
export default {"hpManageModal":"styles_hpManageModal__jXAcr","total":"styles_total__1a5fd","totalLabel":"styles_totalLabel__K0+aD","totalValue":"styles_totalValue__wZrWG","controls":"styles_controls__EvlpE","baseHp":"styles_baseHp__cA4rA","baseHpLabel":"styles_baseHpLabel__nc41x","baseHpValue":"styles_baseHpValue__tBbMl","hpSources":"styles_hpSources__lqYJK","info":"styles_info__Gkkvg","help":"styles_help__JapXf","infoGroup":"styles_infoGroup__IBi4u","infoLabel":"styles_infoLabel__jjH0g"}; export default {"hpManageModal":"styles_hpManageModal__jXAcr","total":"styles_total__1a5fd","totalLabel":"styles_totalLabel__K0+aD","totalValue":"styles_totalValue__wZrWG","controls":"styles_controls__EvlpE","baseHp":"styles_baseHp__cA4rA","baseHpLabel":"styles_baseHpLabel__nc41x","baseHpValue":"styles_baseHpValue__tBbMl","hpSources":"styles_hpSources__lqYJK","info":"styles_info__Gkkvg","help":"styles_help__JapXf","infoGroup":"styles_infoGroup__IBi4u","infoLabel":"styles_infoLabel__jjH0g","inputField":"styles_inputField__PbrUj"};

View File

@ -0,0 +1,2 @@
// extracted by mini-css-extract-plugin
export default {"label":"styles_label__FtMSp","input":"styles_input__2OhV7","error":"styles_error__kxBCf"};

View File

@ -0,0 +1,2 @@
// extracted by mini-css-extract-plugin
export default {"input":"styles_input__YM9-A"};

View File

@ -1,2 +1,2 @@
// extracted by mini-css-extract-plugin // extracted by mini-css-extract-plugin
export default {"accordionGroup":"styles_accordionGroup__ijLDx"}; export default {"accordionGroup":"styles_accordionGroup__ijLDx","inputField":"styles_inputField__WePfs"};