diff --git a/ddb_main/components/FeatureChoice/FeatureChoice.tsx b/ddb_main/components/FeatureChoice/FeatureChoice.tsx deleted file mode 100644 index 995a26a..0000000 --- a/ddb_main/components/FeatureChoice/FeatureChoice.tsx +++ /dev/null @@ -1,306 +0,0 @@ -import { FC, HTMLAttributes } from "react"; - -import { BuilderChoiceTypeEnum } from "~/constants"; -import { useCharacterEngine } from "~/hooks/useCharacterEngine"; -import { useRuleData } from "~/hooks/useRuleData"; -import { useSource } from "~/hooks/useSource"; -import { - DetailChoice, - DetailChoiceFeat, -} from "~/tools/js/Shared/containers/DetailChoice"; -import { TypeScriptUtils } from "~/tools/js/Shared/utils"; -import { - CharClass, - Choice, - ClassDefinitionContract, - ClassFeature, - Feat, - FeatLookup, - FeatureChoiceOption, - HtmlSelectOptionGroup, - SourceData, -} from "~/types"; - -export interface FeatureChoiceProps extends HTMLAttributes { - choice: Choice; - charClass?: CharClass; - feature?: ClassFeature; - featsData: Feat[]; - subclassData?: ClassDefinitionContract[]; - onChoiceChange: ( - choiceId: string, - type: number, - value: any, - parentChoiceId: string | null - ) => void; - collapseDescription?: boolean; -} - -/** - * Component for rendering a features choice(s) - for species traits and class features - using DetailChoiceFeat for a selected feat's choices and DetailChoice for all others. - It is used in both the builder and character sheet sidebar panes. - */ -export const FeatureChoice: FC = ({ - charClass, - choice, - feature, - featsData, - subclassData, - onChoiceChange, - className, - collapseDescription, - ...props -}) => { - const { - choiceUtils, - featUtils, - helperUtils, - prerequisiteUtils, - classUtils, - featLookup, - preferences, - prerequisiteData, - choiceInfo, - ruleData, - entityRestrictionData, - } = useCharacterEngine(); - - const { ruleDataUtils } = useRuleData(); - - const { - getGroupedOptionsBySourceCategory, - getSimpleSourcedDefinitionContracts, - } = useSource(); - - const optionValue = choiceUtils.getOptionValue(choice); - const options = choiceUtils.getOptions(choice); - const type = choiceUtils.getType(choice); - const tagConstraints = choiceUtils.getTagConstraints(choice); - - let availableOptions: Array = []; - let availableGroupedOptions: HtmlSelectOptionGroup[] = []; - let detailChoiceDesc: string | null = null; - let subchoicesNode: React.ReactNode; - - const handleChoiceChange = ( - id: string, - type: number, - subType: number | null, - value: any, - parentChoiceId: string | null - ): void => { - onChoiceChange(id, type, value, parentChoiceId); - }; - - const getSubclassData = (): ClassDefinitionContract[] => { - if (!subclassData || !charClass) { - return []; - } - - let data: ClassDefinitionContract[] = [...subclassData]; - - let existingSubclass = classUtils.getSubclass(charClass); - if ( - existingSubclass !== null && - !data.some( - (classDefinition) => - existingSubclass !== null && - classDefinition.id === existingSubclass.id - ) - ) { - data.push(existingSubclass); - } - - return data; - }; - - const getAvailableFeatChoices = ( - existingFeatId: number | null, - featData: Feat[], - featLookup: FeatLookup - ): Feat[] => { - let data: Feat[] = [...featData]; - - if (existingFeatId !== null) { - let existingFeat = helperUtils.lookupDataOrFallback( - featLookup, - existingFeatId - ); - if ( - existingFeat !== null && - !data.some((feat) => featUtils.getId(feat) === existingFeatId) - ) { - data.push(existingFeat); - } - } - - return data; - }; - - const getSubclassSources = ( - subclass: ClassDefinitionContract - ): SourceData[] => { - if (subclass.sources === null) { - return []; - } - - return subclass.sources - .map((sourceMapping) => - helperUtils.lookupDataOrFallback( - ruleDataUtils.getSourceDataLookup(ruleData), - sourceMapping.sourceId - ) - ) - .filter(TypeScriptUtils.isNotNullOrUndefined); - }; - - switch (type) { - case BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION: - const availableFeats = getAvailableFeatChoices( - optionValue, - featsData, - featLookup - ); - const repeatableFeatTracker = new Set(); - - // Add selected feat to repeatable tracker if repeatable - const selectedFeat = optionValue ? featLookup[optionValue] : null; - if (selectedFeat && featUtils.isRepeatable(selectedFeat)) { - const parentId = featUtils.getRepeatableGroupId(selectedFeat); - if (parentId) { - repeatableFeatTracker.add(parentId); - } - } - - const filteredFeats = availableFeats.filter((feat) => { - const featId = featUtils.getId(feat); - const isRepeatable = featUtils.isRepeatable(feat); - - // If the feat is the currently selected, always include it - if (featId === optionValue) { - return true; - } - - // Always exclude all previous selected feats - if (featLookup[featId]) { - return false; - } - - // If the Feat does not meet the tag constraints, should they exist, exclude it - const tagCategories = featUtils.getCategories(feat); - if ( - tagConstraints && - !featUtils.doesSatisfyTagConstraints(tagCategories, tagConstraints) - ) { - return false; - } - - // Handle prerequisites when enforcing feat rules - if ( - preferences.enforceFeatRules && - !prerequisiteUtils.validatePrerequisiteGrouping( - featUtils.getPrerequisites(feat), - prerequisiteData - ) - ) { - return false; - } - - // Special handling for repeatable feats, there can be only one - if (isRepeatable) { - const parentId = featUtils.getRepeatableGroupId(feat); - - // If a feat from this repeatable group exist exclude all others - if (repeatableFeatTracker.has(parentId)) { - return false; - } - - repeatableFeatTracker.add(parentId); - } - - // If none of the exclusions above are met, include the feat - return true; - }); - - //Group available feats by source category - availableGroupedOptions = getGroupedOptionsBySourceCategory( - filteredFeats - .map((feat) => featUtils.getDefinition(feat)) - .filter(TypeScriptUtils.isNotNullOrUndefined) - ); - - if (selectedFeat && optionValue !== null) { - detailChoiceDesc = featUtils.getDescription(selectedFeat); - subchoicesNode = ; - } - break; - - case BuilderChoiceTypeEnum.SUB_CLASS_OPTION: - const subclassData = getSubclassData(); - - //Group available subclasses by source category - availableGroupedOptions = getGroupedOptionsBySourceCategory(subclassData); - - const chosenSubclass = subclassData.find( - (subclass) => subclass.id === optionValue - ); - if (chosenSubclass) { - detailChoiceDesc = ""; - let sources = getSubclassSources(chosenSubclass); - sources.forEach((source) => { - if (source.sourceCategory && source.sourceCategory.isToggleable) { - detailChoiceDesc += source.sourceCategory.description - ? source.sourceCategory.description - : ""; - } - }); - } - break; - case BuilderChoiceTypeEnum.ENTITY_SPELL_OPTION: - // Map over the options to mock parts of a SpellDefinitionContract. - const spellOptions = getSimpleSourcedDefinitionContracts(options); - availableGroupedOptions = getGroupedOptionsBySourceCategory( - spellOptions, - optionValue, - entityRestrictionData - ); - - // If there is a chosen spell, set detailChoiceDesc to its description. - const chosenSpell = spellOptions.find( - (spell) => spell.id === optionValue - ); - - if (chosenSpell) { - detailChoiceDesc = chosenSpell.description ?? ""; - } - - break; - - default: - availableOptions = options.map((option) => ({ - ...option, - value: option.id, - })); - } - - return ( -
- 0 - ? availableGroupedOptions - : availableOptions - } - onChange={handleChoiceChange} - description={detailChoiceDesc || ""} - choiceInfo={choiceInfo} - classId={charClass && classUtils.getId(charClass)} - showBackgroundProficiencyOptions={true} - collapseDescription={collapseDescription} - /> - {subchoicesNode} -
- ); -}; diff --git a/ddb_main/components/FeatureChoices/FeatureChoice/FeatureChoice.tsx b/ddb_main/components/FeatureChoices/FeatureChoice/FeatureChoice.tsx new file mode 100644 index 0000000..29e3f47 --- /dev/null +++ b/ddb_main/components/FeatureChoices/FeatureChoice/FeatureChoice.tsx @@ -0,0 +1,119 @@ +import { FC, HTMLAttributes } from "react"; +import { useDispatch } from "react-redux"; + +import { + characterActions, + ChoiceUtils, +} from "@dndbeyond/character-rules-engine"; + +import { useCharacterEngine } from "~/hooks/useCharacterEngine"; +import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice"; +import { CharClass, Choice, ClassDefinitionContract, Feat } from "~/types"; + +export interface FeatureChoiceProps extends HTMLAttributes { + choice: Choice; + charClass?: CharClass; + featsData: Feat[]; + subclassData?: ClassDefinitionContract[]; + onChoiceChange: ( + choiceId: string, + type: number, + value: any, + parentChoiceId: string | null + ) => void; + collapseDescription?: boolean; +} + +/** + * Component for rendering a features choice(s) - for species traits and class features - using DetailChoice for the choices and mapping over any feat sub choices if the choice from a species trait or class feature is a feat. + * It is used in both the builder and character sheet sidebar panes. + */ +export const FeatureChoice: FC = ({ + charClass, + choice, + featsData, + subclassData, + onChoiceChange, + className, + collapseDescription, + ...props +}) => { + const dispatch = useDispatch(); + const { + classUtils, + featLookup, + preferences, + prerequisiteData, + choiceInfo, + ruleData, + entityRestrictionData, + } = useCharacterEngine(); + + const handleChoiceChange = ( + id: string, + type: number, + value: any, + parentChoiceId: string | null + ): void => { + onChoiceChange(id, type, value, parentChoiceId); + }; + + const handleFeatChoiceChange = ( + featId: number, + id: string | null, + type: number, + value: any + ): void => { + if (id !== null) { + dispatch(characterActions.featChoiceSetRequest(featId, type, id, value)); + } + }; + + const { description, options, featSubChoices } = + ChoiceUtils.getSortedChoiceOptionsForFeaturesInfo( + choice, + featsData, + featLookup, + charClass || null, + subclassData || [], + preferences, + prerequisiteData, + ruleData, + entityRestrictionData + ); + + return ( +
+ + {featSubChoices && + featSubChoices.map((subChoice) => { + const id = ChoiceUtils.getId(subChoice.choice); + return ( + + handleFeatChoiceChange(subChoice.featId, id, type, value) + } + choiceInfo={choiceInfo} + showBackgroundProficiencyOptions={true} + description={subChoice.description} + /> + ); + })} +
+ ); +}; diff --git a/ddb_main/components/FeatureChoices/FeatureChoices.tsx b/ddb_main/components/FeatureChoices/FeatureChoices.tsx new file mode 100644 index 0000000..2d602c5 --- /dev/null +++ b/ddb_main/components/FeatureChoices/FeatureChoices.tsx @@ -0,0 +1,179 @@ +import clsx from "clsx"; +import { FC, HTMLAttributes, useEffect, useState } from "react"; +import { useSelector } from "react-redux"; + +import { + ApiAdapterUtils, + apiCreatorSelectors, + CharClass, + Choice, + ChoiceUtils, + ClassDefinitionContract, + ClassUtils, + Constants, + Feat, + FeatUtils, +} from "@dndbeyond/character-rules-engine"; + +import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading"; +import { appEnvSelectors } from "~/tools/js/Shared/selectors"; +import { AppLoggerUtils } from "~/tools/js/Shared/utils"; +import LoadingPlaceholder from "~/tools/js/smartComponents/LoadingPlaceholder"; +import { DataLoadingStatusEnum } from "~/tools/js/smartComponents/componentConstants"; + +import { FeatureChoice } from "./FeatureChoice"; +import styles from "./styles.module.css"; + +export interface FeatureChoicesProps extends HTMLAttributes { + choices: Array; + showHeading?: boolean; + onChoiceChange: ( + choiceId: string, + type: number, + value: any, + parentChoiceId: string | null + ) => void; + collapseDescription?: boolean; + charClass?: CharClass; + shouldFetch?: boolean; // Optional prop to control fetching behavior +} + +export const FeatureChoices: FC = ({ + className, + choices, + showHeading, + onChoiceChange, + collapseDescription, + charClass, + shouldFetch = true, // Default to true if not provided + ...props +}) => { + const isReadonly = useSelector(appEnvSelectors.getIsReadonly); + const loadAvailableFeats = useSelector( + apiCreatorSelectors.makeLoadAvailableFeats + ); + const loadAvailableSubclasses = useSelector( + apiCreatorSelectors.makeLoadAvailableSubclasses + ); + + const [subclassData, setSubclassData] = useState< + Array + >([]); + const [featsData, setFeatsData] = useState>([]); + const [featLoadingStatus, setFeatLoadingStatus] = + useState(DataLoadingStatusEnum.NOT_INITIALIZED); + const [subclassLoadingStatus, setSubclassLoadingStatus] = + useState(DataLoadingStatusEnum.NOT_INITIALIZED); + + const hasFeatChoice = choices.some( + (choice) => + ChoiceUtils.getType(choice) === + Constants.BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION + ); + + const hasSubclassChoice = choices.some( + (choice) => + ChoiceUtils.getType(choice) === + Constants.BuilderChoiceTypeEnum.SUB_CLASS_OPTION + ); + + useEffect(() => { + if ( + hasFeatChoice && + shouldFetch && + featLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED + ) { + setFeatLoadingStatus(DataLoadingStatusEnum.LOADING); + + loadAvailableFeats() + .then((response) => { + let featsData: Array = []; + + const data = ApiAdapterUtils.getResponseData(response); + if (data !== null) { + featsData = data.map((definition) => + FeatUtils.simulateFeat(definition) + ); + } + + setFeatsData(featsData); + setFeatLoadingStatus(DataLoadingStatusEnum.LOADED); + }) + .catch(AppLoggerUtils.handleAdhocApiError); + } + + if ( + charClass && + hasSubclassChoice && + shouldFetch && + subclassLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED + ) { + setSubclassLoadingStatus(DataLoadingStatusEnum.LOADING); + loadAvailableSubclasses(ClassUtils.getId(charClass)) + .then((response) => { + let subclassData: Array = []; + + const data = ApiAdapterUtils.getResponseData(response); + if (data !== null) { + subclassData = data; + } + + setSubclassData(subclassData); + setSubclassLoadingStatus(DataLoadingStatusEnum.LOADED); + }) + .catch(AppLoggerUtils.handleAdhocApiError); + } + }, [ + hasFeatChoice, + hasSubclassChoice, + subclassLoadingStatus, + featLoadingStatus, + charClass, + loadAvailableFeats, + loadAvailableSubclasses, + shouldFetch, + ]); + + const isDataLoaded = (): boolean => { + if (hasSubclassChoice) { + if (subclassLoadingStatus !== DataLoadingStatusEnum.LOADED) { + return false; + } + } + if (hasFeatChoice) { + if (featLoadingStatus !== DataLoadingStatusEnum.LOADED) { + return false; + } + } + + return true; + }; + + return !isReadonly && choices.length > 0 ? ( +
+ <> + {showHeading && ( + {`Option${ + choices.length > 1 ? "s" : "" + }`} + )} + {isDataLoaded() ? ( + choices.map((choice) => ( + + )) + ) : ( + + )} + +
+ ) : null; +}; diff --git a/ddb_main/packages/rules-engine/es/engine/Choice/utils.js b/ddb_main/packages/rules-engine/es/engine/Choice/utils.js index 1fdb467..ec3c679 100644 --- a/ddb_main/packages/rules-engine/es/engine/Choice/utils.js +++ b/ddb_main/packages/rules-engine/es/engine/Choice/utils.js @@ -1,11 +1,15 @@ import { orderBy } from 'lodash'; import { TypeScriptUtils } from "../../utils"; +import { ChoiceAccessors } from '.'; import { ClassAccessors } from '../Class'; -import { AbilityStatEnum, BuilderChoiceSubtypeEnum } from '../Core'; +import { AbilityStatEnum, BuilderChoiceSubtypeEnum, BuilderChoiceTypeEnum, } from '../Core'; import { DataOriginTypeEnum } from '../DataOrigin'; import { EntityUtils } from '../Entity'; +import { FeatAccessors, FeatUtils } from '../Feat'; import { HelperUtils } from '../Helper'; import { ModifierAccessors } from '../Modifier'; +import { PrerequisiteValidators } from '../Prerequisite'; +import { RuleDataAccessors } from '../RuleData'; import { SourceUtils } from '../Source'; import { SpellUtils } from '../Spell'; import { getDefaultSubtypes, getOptions, getOptionValue, isOptional } from './accessors'; @@ -33,8 +37,9 @@ export function isTodo(choice) { return !isOptional(choice) && getOptionValue(choice) === null; } // Given a Choice, return options for a select element that are potentially grouped by sourceCategory -//used in DetailChoiceFeat.tsx for feat choices -export function getSortedChoiceOptionsInfo(choice, ruleData, entityRestrictionData) { +//used in FeatPane and FeatDetail for feat choices +// and in getSortedChoiceOptionsForFeaturesInfo for feature choices that are feats and have subchoices +export function getSortedChoiceOptionsForFeatsInfo(featId, choice, ruleData, entityRestrictionData) { var _a; const options = getOptions(choice); let description; @@ -56,10 +61,126 @@ export function getSortedChoiceOptionsInfo(choice, ruleData, entityRestrictionDa featChoiceOptions = orderBy(featChoiceOptions, 'label'); } return { + featId, + choice, options: featChoiceOptions, description, }; } +// Given a Choice, return options for a select element that are potentially grouped by sourceCategory +//used in FeatureChoice for all choices +// extracted from FeatureChoice.tsx +// TODO needs to be broken down into smaller utils and simplified +export function getSortedChoiceOptionsForFeaturesInfo(choice, fetchedFeats, featLookup, charClass, fetchedSubclasses, characterPreferences, prerequisiteData, ruleData, entityRestrictionData) { + var _a, _b; + let description; + let availableOptions; + let featSubChoices; + const optionValue = ChoiceAccessors.getOptionValue(choice); + const options = ChoiceAccessors.getOptions(choice); + const type = ChoiceAccessors.getType(choice); + const tagConstraints = ChoiceAccessors.getTagConstraints(choice); + switch (type) { + case BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION: + const availableFeats = getAvailableFeatChoices(optionValue, fetchedFeats, featLookup); + const repeatableFeatTracker = new Set(); + // Add selected feat to repeatable tracker if repeatable + const selectedFeat = optionValue ? featLookup[optionValue] : null; + if (selectedFeat && FeatAccessors.isRepeatable(selectedFeat)) { + const parentId = FeatUtils.getRepeatableGroupId(selectedFeat); + if (parentId) { + repeatableFeatTracker.add(parentId); + } + } + const filteredFeats = availableFeats.filter((feat) => { + const featId = FeatAccessors.getId(feat); + const isRepeatable = FeatAccessors.isRepeatable(feat); + // If the feat is the currently selected, always include it + if (featId === optionValue) { + return true; + } + // Always exclude all previous selected feats + if (featLookup[featId]) { + return false; + } + // If the Feat does not meet the tag constraints, should they exist, exclude it + const tagCategories = FeatAccessors.getCategories(feat); + if (tagConstraints && !FeatUtils.doesSatisfyTagConstraints(tagCategories, tagConstraints)) { + return false; + } + // Handle prerequisites when enforcing feat rules + if (characterPreferences.enforceFeatRules && + !PrerequisiteValidators.validatePrerequisiteGrouping(FeatAccessors.getPrerequisites(feat), prerequisiteData)) { + return false; + } + // Special handling for repeatable feats, there can be only one + if (isRepeatable) { + const parentId = FeatUtils.getRepeatableGroupId(feat); + // If a feat from this repeatable group exist exclude all others + if (repeatableFeatTracker.has(parentId)) { + return false; + } + repeatableFeatTracker.add(parentId); + } + // If none of the exclusions above are met, include the feat + return true; + }); + //Group available feats by source category + availableOptions = SourceUtils.getGroupedOptionsBySourceCategory(filteredFeats + .map((feat) => FeatAccessors.getDefinition(feat)) + .filter(TypeScriptUtils.isNotNullOrUndefined), ruleData); + if (selectedFeat && optionValue !== null) { + description = (_a = FeatAccessors.getDescription(selectedFeat)) !== null && _a !== void 0 ? _a : ''; + const featChoices = FeatAccessors.getChoices(selectedFeat); + if (featChoices.length > 0) { + featSubChoices = featChoices.map((featChoice) => { + return getSortedChoiceOptionsForFeatsInfo(optionValue, featChoice, ruleData, entityRestrictionData); + }); + } + // subchoicesNode = ; + } + break; + case BuilderChoiceTypeEnum.SUB_CLASS_OPTION: + const subclassData = charClass ? getAvailableSubclassChoices(charClass, fetchedSubclasses) : []; + //Group available subclasses by source category + availableOptions = SourceUtils.getGroupedOptionsBySourceCategory(subclassData, ruleData); + const chosenSubclass = subclassData.find((subclass) => subclass.id === optionValue); + if (chosenSubclass) { + description = ''; + const sources = chosenSubclass.sources + ? chosenSubclass.sources + .map((sourceMapping) => HelperUtils.lookupDataOrFallback(RuleDataAccessors.getSourceDataLookup(ruleData), sourceMapping.sourceId)) + .filter(TypeScriptUtils.isNotNullOrUndefined) + : []; + sources.forEach((source) => { + var _a, _b; + if ((_a = source.sourceCategory) === null || _a === void 0 ? void 0 : _a.isToggleable) { + description += source.sourceCategory.description ? source.sourceCategory.description : ''; + } + description += (_b = chosenSubclass.description) !== null && _b !== void 0 ? _b : ''; + }); + } + break; + case BuilderChoiceTypeEnum.ENTITY_SPELL_OPTION: + // Map over the options to mock parts of a SpellDefinitionContract. + const spellOptions = SourceUtils.getSimpleSourcedDefinitionContracts(options); + availableOptions = SourceUtils.getGroupedOptionsBySourceCategory(spellOptions, ruleData, optionValue, entityRestrictionData); + // If there is a chosen spell, set detailChoiceDesc to its description. + const chosenSpell = spellOptions.find((spell) => spell.id === optionValue); + if (chosenSpell) { + description = (_b = chosenSpell.description) !== null && _b !== void 0 ? _b : ''; + } + break; + default: + availableOptions = options.map((option) => (Object.assign(Object.assign({}, option), { value: option.id }))); + } + return { + choice, + options: availableOptions, + featSubChoices, + description, + }; +} // filter Choice options based on modifiers and default subtypes that identify when a user has made a choice from another source. We filter those options out or we append the source data origin name next to the option label export function getRemainingOptions(options, modifiers, defaultSubtypes, showBackgroundProficiencyOptions, optionValue) { let remainingSubtypes = []; @@ -209,3 +330,21 @@ export function getSortedRenderOptions(options, classId, subType, entityRestrict ? orderBy(renderOptions, 'sortOrder') : orderBy(renderOptions, 'label'); } +//Taken from FeatureChoice.tsx +function getAvailableSubclassChoices(charClass, fetchedSubclasses) { + const existingSubclass = ClassAccessors.getSubclass(charClass); + //if there is an existing subclass that isn't already in the fetched subclasses, append it to the list + if (existingSubclass && !fetchedSubclasses.some((subclass) => subclass.id === existingSubclass.id)) { + return [...fetchedSubclasses, existingSubclass]; + } + return fetchedSubclasses; +} +//Taken from FeatureChoice.tsx +function getAvailableFeatChoices(existingFeatId, fetchedFeats, featLookup) { + const existingFeat = existingFeatId ? HelperUtils.lookupDataOrFallback(featLookup, existingFeatId) : null; + // If there is an existing feat that isn't already in the fetched feats, append it to the list + if (existingFeat !== null && !fetchedFeats.some((feat) => FeatAccessors.getId(feat) === existingFeatId)) { + return [...fetchedFeats, existingFeat]; + } + return fetchedFeats; +} diff --git a/ddb_main/packages/rules-engine/es/engine/Class/derivers.js b/ddb_main/packages/rules-engine/es/engine/Class/derivers.js index 784d8dd..6ee5f53 100644 --- a/ddb_main/packages/rules-engine/es/engine/Class/derivers.js +++ b/ddb_main/packages/rules-engine/es/engine/Class/derivers.js @@ -171,8 +171,9 @@ export function deriveReplacementWeaponAbilityStats(classModifiers) { classModifiers.forEach((modifier) => { if (ModifierValidators.isEnablesAbilityStatModifier(modifier)) { const statId = ModifierAccessors.getEntityId(modifier) || 0; - if (statId && !replacementWeaponAbilityStats.includes(statId)) { - replacementWeaponAbilityStats.push(statId); + const dataOrigin = ModifierAccessors.getDataOrigin(modifier); + if (statId) { + replacementWeaponAbilityStats.push({ statId, dataOrigin }); } } }); diff --git a/ddb_main/packages/rules-engine/es/sagas/character/handlers.js b/ddb_main/packages/rules-engine/es/sagas/character/handlers.js index 48f4d5a..1a22d89 100644 --- a/ddb_main/packages/rules-engine/es/sagas/character/handlers.js +++ b/ddb_main/packages/rules-engine/es/sagas/character/handlers.js @@ -343,6 +343,11 @@ export function* handleAdhocFeatCreate(action) { * @param action */ export function* handleAdhocFeatRemove(action) { + const featLookup = yield select(rulesEngineSelectors.getFeatLookup); + const currentFeat = HelperUtils.lookupDataOrFallback(featLookup, action.payload.id, null); + if (currentFeat !== null) { + yield call(apiRemoveSpellsBySpellListIds, FeatAccessors.getSpellListIds(currentFeat)); + } const data = yield call(SagaHelpers.getApiRequestData, ApiRequests.deleteCharacterFeatAdHoc, action.payload); yield call(handleDataUpdates, data); } @@ -457,7 +462,13 @@ export function* handleRaceChoose(action) { const { race } = action.payload; const existingRace = yield select(rulesEngineSelectors.getRace); if (existingRace !== null) { - yield call(apiRemoveSpellsBySpellListIds, RaceAccessors.getSpellListIds(existingRace)); + const spellListIdsToRemove = RaceAccessors.getSpellListIds(existingRace); + const racialTraitsToRemove = RaceAccessors.getRacialTraits(existingRace); + const featsFromRacialTraits = racialTraitsToRemove.map((trait) => RacialTraitAccessors.getFeats(trait)).flat(); + featsFromRacialTraits.forEach((feat) => { + spellListIdsToRemove.push(...FeatAccessors.getSpellListIds(feat)); + }); + yield call(apiRemoveSpellsBySpellListIds, spellListIdsToRemove); const existingOptionalOrigins = yield select(rulesEngineSelectors.getOptionalOrigins); const optionalOriginsIdsToRemove = existingOptionalOrigins.map(OptionalOriginAccessors.getRacialTraitId); yield call(apiRemoveOptionalOriginsCollection, { @@ -1309,15 +1320,21 @@ export function* handleCurrencyTransactionSet(action) { */ export function* handleRacialTraitChoiceSetRequest(action) { const { racialTraitId, choiceId } = action.payload; - yield call(apiRacialTraitChoiceSet, action); const race = yield select(rulesEngineSelectors.getRace); - if (race) { - const racialTrait = RaceAccessors.getRacialTraits(race).find((racialTrait) => RacialTraitAccessors.getId(racialTrait) === racialTraitId); - if (racialTrait) { - const racialTraitChoice = RacialTraitAccessors.getChoices(racialTrait).find((choice) => ChoiceAccessors.getId(choice) === choiceId); - if (racialTraitChoice) { - yield call(autoUpdateChoices, racialTraitChoice); - } + const racialTrait = race + ? RaceAccessors.getRacialTraits(race).find((racialTrait) => RacialTraitAccessors.getId(racialTrait) === racialTraitId) + : null; + // Remove any spells that are associated with the feat from the racial trait choices + const feats = racialTrait ? RacialTraitAccessors.getFeats(racialTrait) : []; + const featSpellListIds = feats + .map(FeatAccessors.getSpellListIds) + .reduce((acc, ids) => acc.concat(ids), []); + yield call(apiRemoveSpellsBySpellListIds, featSpellListIds); + yield call(apiRacialTraitChoiceSet, action); + if (racialTrait) { + const racialTraitChoice = RacialTraitAccessors.getChoices(racialTrait).find((choice) => ChoiceAccessors.getId(choice) === choiceId); + if (racialTraitChoice) { + yield call(autoUpdateChoices, racialTraitChoice); } } } @@ -1343,6 +1360,7 @@ export function* handleClassFeatureChoiceSetRequest(action) { const { classId, classFeatureId, choiceId, choiceType, optionValue } = action.payload; let oldClasses = yield select(rulesEngineSelectors.getClasses); let oldCharClass = oldClasses.find((charClass) => ClassAccessors.getActiveId(charClass) === classId); + const spellListIdsToRemove = []; if (choiceType === BuilderChoiceTypeEnum.SUB_CLASS_OPTION && oldCharClass) { const subclass = ClassAccessors.getSubclass(oldCharClass); if (subclass !== null) { @@ -1350,9 +1368,19 @@ export function* handleClassFeatureChoiceSetRequest(action) { .filter((classFeature) => ClassFeatureAccessors.getClassId(classFeature) === subclass.id) .map(ClassFeatureAccessors.getSpellListIds) .reduce((acc, ids) => acc.concat(ids), []); - yield call(apiRemoveSpellsBySpellListIds, spellListIds); + spellListIdsToRemove.push(...spellListIds); } } + if (choiceType === BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION && oldCharClass) { + const classFeature = ClassAccessors.getClassFeatures(oldCharClass).find((classFeature) => ClassFeatureAccessors.getId(classFeature) === classFeatureId); + const featsFromClassFeature = classFeature ? ClassFeatureAccessors.getFeats(classFeature) : []; + featsFromClassFeature.forEach((feat) => { + const spellListIds = FeatAccessors.getSpellListIds(feat); + spellListIdsToRemove.push(...spellListIds); + }); + } + //remove any spells that are associated with the spell lists from the subclass or feat choices + yield call(apiRemoveSpellsBySpellListIds, spellListIdsToRemove); yield call(apiClassFeatureChoiceSet, action); const classes = yield select(rulesEngineSelectors.getClasses); const charClass = classes.find((charClass) => !!oldCharClass && ClassAccessors.getId(charClass) === ClassAccessors.getId(oldCharClass)); @@ -1453,12 +1481,21 @@ export function* handleClassLevelSetRequest(action) { const oldClassId = ClassAccessors.getId(oldCharClass); const removedClassFeatures = ClassAccessors.getActiveClassFeatures(oldCharClass).filter((classFeature) => ClassFeatureAccessors.getRequiredLevel(classFeature) > level); const isRemovingSubclass = removedClassFeatures.some((feature) => ClassFeatureAccessors.getChoices(feature).some((choice) => ChoiceAccessors.getType(choice) === BuilderChoiceTypeEnum.SUB_CLASS_OPTION)); + const spellListIdsFromFeatsToRemove = []; + removedClassFeatures.forEach((classFeature) => { + const feats = ClassFeatureAccessors.getFeats(classFeature); + feats.forEach((feat) => { + const spellListIds = FeatAccessors.getSpellListIds(feat); + spellListIdsFromFeatsToRemove.push(...spellListIds); + }); + }); const removedSpellListIds = ClassAccessors.getActiveClassFeatures(oldCharClass) .filter((classFeature) => ClassFeatureAccessors.getRequiredLevel(classFeature) > level || (isRemovingSubclass && ClassFeatureAccessors.getClassId(classFeature) !== oldClassId)) .map(ClassFeatureAccessors.getSpellListIds) .reduce((acc, spellListIds) => acc.concat(spellListIds), []); - yield call(apiRemoveSpellsBySpellListIds, removedSpellListIds); + // Remove spells associated with the spell lists from the removed class features + yield call(apiRemoveSpellsBySpellListIds, [...removedSpellListIds, ...spellListIdsFromFeatsToRemove]); } } yield call(apiClassLevelSet, action); @@ -1582,7 +1619,21 @@ function* updateBackgroundResult(action, data) { export function* handleBackgroundSetRequest(action) { const existingBackground = yield select(rulesEngineSelectors.getBackgroundInfo); if (existingBackground !== null) { - yield call(apiRemoveSpellsBySpellListIds, BackgroundAccessors.getSpellListIds(existingBackground)); + //start an array of spellListIds to remove with any spellListIds from the existing background + const spellListIdsToRemove = BackgroundAccessors.getSpellListIds(existingBackground); + //check for any granted or chosen feats on the background and get their spellListIds + const featLookup = yield select(rulesEngineSelectors.getFeatLookup); + const chosenFeatIds = BackgroundAccessors.getFeatLists(existingBackground) + .map((featList) => featList.chosenFeatId) + .filter(TypeScriptUtils.isNotNullOrUndefined); + chosenFeatIds.forEach((featId) => { + const feat = HelperUtils.lookupDataOrFallback(featLookup, featId); + if (feat) { + spellListIdsToRemove.push(...FeatAccessors.getSpellListIds(feat)); + } + }); + //remove any spellListIds that were collected + yield call(apiRemoveSpellsBySpellListIds, spellListIdsToRemove); } yield put(callCommitAction(characterActions.backgroundHasCustomSet, false)); const data = yield call(SagaHelpers.getApiRequestData, ApiRequests.putCharacterBackground, action.payload); diff --git a/ddb_main/subApps/sheet/components/Sidebar/helpers/getActiveEntryComponent.ts b/ddb_main/subApps/sheet/components/Sidebar/helpers/getActiveEntryComponent.ts index b597e3f..d7ac682 100644 --- a/ddb_main/subApps/sheet/components/Sidebar/helpers/getActiveEntryComponent.ts +++ b/ddb_main/subApps/sheet/components/Sidebar/helpers/getActiveEntryComponent.ts @@ -1,5 +1,6 @@ import { CreaturePane } from "~/subApps/sheet/components/Sidebar/panes/CreaturePane"; import { GameLogPane } from "~/subApps/sheet/components/Sidebar/panes/GameLogPane"; +import { SpeciesTraitPane } from "~/subApps/sheet/components/Sidebar/panes/SpeciesTraitPane"; import BlessingPane from "~/tools/js/Shared/containers/panes/BlessingPane"; import CharacterSpellPane from "~/tools/js/Shared/containers/panes/CharacterSpellPane"; import ClassSpellPane from "~/tools/js/Shared/containers/panes/ClassSpellPane"; @@ -34,7 +35,6 @@ import { ShareUrlPane } from "~/tools/js/Shared/containers/panes/ShareUrlPane"; import ShortRestPane from "~/tools/js/Shared/containers/panes/ShortRestPane"; import SkillPane from "~/tools/js/Shared/containers/panes/SkillPane"; import SkillsPane from "~/tools/js/Shared/containers/panes/SkillsPane"; -import SpeciesTraitPane from "~/tools/js/Shared/containers/panes/SpeciesTraitPane"; import SpeedManagePane from "~/tools/js/Shared/containers/panes/SpeedManagePane"; import SpellManagePane from "~/tools/js/Shared/containers/panes/SpellManagePane"; import StartingEquipmentPane from "~/tools/js/Shared/containers/panes/StartingEquipmentPane"; diff --git a/ddb_main/subApps/sheet/components/Sidebar/panes/ClassFeaturePane/ClassFeaturePane.tsx b/ddb_main/subApps/sheet/components/Sidebar/panes/ClassFeaturePane/ClassFeaturePane.tsx index f5f25a1..aac1629 100644 --- a/ddb_main/subApps/sheet/components/Sidebar/panes/ClassFeaturePane/ClassFeaturePane.tsx +++ b/ddb_main/subApps/sheet/components/Sidebar/panes/ClassFeaturePane/ClassFeaturePane.tsx @@ -1,45 +1,27 @@ -import { - FC, - HTMLAttributes, - useContext, - useEffect, - useRef, - useState, -} from "react"; +import { FC, HTMLAttributes, useContext } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { - ApiAdapterUtils, - FeatManager, -} from "@dndbeyond/character-rules-engine"; +import { FeatManager } from "@dndbeyond/character-rules-engine"; -import { FeatureChoice } from "~/components/FeatureChoice"; -import { BuilderChoiceTypeEnum } from "~/constants"; +import { FeatureChoices } from "~/components/FeatureChoices"; import { useSidebar } from "~/contexts/Sidebar"; import { useCharacterEngine } from "~/hooks/useCharacterEngine"; import { Header } from "~/subApps/sheet/components/Sidebar/components/Header"; import { Preview } from "~/subApps/sheet/components/Sidebar/components/Preview"; -import DataLoadingStatusEnum from "~/tools/js/Shared/constants/DataLoadingStatusEnum"; +import { ClassFeatureSnippet } from "~/tools/js/CharacterSheet/components/FeatureSnippet"; import { CharacterFeaturesManagerContext } from "~/tools/js/Shared/managers/CharacterFeaturesManagerContext"; -import { apiCreatorSelectors } from "~/tools/js/Shared/selectors"; -import { AppLoggerUtils, PaneIdentifierUtils } from "~/tools/js/Shared/utils"; -import LoadingPlaceholder from "~/tools/js/smartComponents/LoadingPlaceholder"; +import { appEnvSelectors } from "~/tools/js/Shared/selectors"; +import { PaneIdentifierUtils } from "~/tools/js/Shared/utils"; import { Action, CharClass, - ClassDefinitionContract, ClassFeature, - Feat, InfusionChoice, Spell, } from "~/types"; -import { ClassFeatureSnippet } from "../../../../../../tools/js/CharacterSheet/components/FeatureSnippet"; -import { appEnvSelectors } from "../../../../../../tools/js/Shared/selectors"; -import { Heading } from "../../components/Heading"; import { PaneInitFailureContent } from "../../components/PaneInitFailureContent"; import { PaneComponentEnum, PaneIdentifiersClassFeature } from "../../types"; -import styles from "./styles.module.css"; interface Props extends HTMLAttributes { identifiers: PaneIdentifiersClassFeature | null; @@ -61,8 +43,6 @@ export const ClassFeaturePane: FC = ({ identifiers, ...props }) => { proficiencyBonus, originRef: dataOriginRefData, characterTheme: theme, - choiceUtils, - featUtils, helperUtils, } = useCharacterEngine(); @@ -76,23 +56,6 @@ export const ClassFeaturePane: FC = ({ identifiers, ...props }) => { const isReadonly = useSelector(appEnvSelectors.getIsReadonly); - const loadAvailableFeats = useSelector( - apiCreatorSelectors.makeLoadAvailableFeats - ); - const loadAvailableSubclasses = useSelector( - apiCreatorSelectors.makeLoadAvailableSubclasses - ); - - const [subclassData, setSubclassData] = useState< - Array - >([]); - const [featsData, setFeatsData] = useState>([]); - const [featLoadingStatus, setFeatLoadingStatus] = - useState(DataLoadingStatusEnum.NOT_INITIALIZED); - const [subclassLoadingStatus, setSubclassLoadingStatus] = - useState(DataLoadingStatusEnum.NOT_INITIALIZED); - const currentCharClass = useRef(null); - const charClass = identifiers?.classMappingId ? classes.find( (charClass) => @@ -105,98 +68,6 @@ export const ClassFeaturePane: FC = ({ identifiers, ...props }) => { (feature) => identifiers?.id === classFeatureUtils.getId(feature) ) ?? null; - const featureChoices = classFeature - ? classFeatureUtils.getChoices(classFeature) - : []; - - const hasFeatChoice = featureChoices.some( - (choice) => - choiceUtils.getType(choice) === BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION - ); - - const hasSubclassChoice = featureChoices.some( - (choice) => - choiceUtils.getType(choice) === BuilderChoiceTypeEnum.SUB_CLASS_OPTION - ); - - useEffect(() => { - const isSameClass = ( - c1: CharClass | null, - c2: CharClass | null - ): boolean => { - if (!c1 || !c2) return false; - return classUtils.getId(c1) === classUtils.getId(c2); - }; - - if ( - hasFeatChoice && - featLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED - ) { - setFeatLoadingStatus(DataLoadingStatusEnum.LOADING); - - loadAvailableFeats({ - // cancelToken: new axios.CancelToken((c) => { - // this.loadFeatsCanceler = c; - // }), - }) - .then((response) => { - let featsData: Array = []; - - const data = ApiAdapterUtils.getResponseData(response); - if (data !== null) { - featsData = data.map((definition) => - featUtils.simulateFeat(definition) - ); - } - - setFeatsData(featsData); - setFeatLoadingStatus(DataLoadingStatusEnum.LOADED); - // this.loadFeatsCanceler = null; - }) - .catch(AppLoggerUtils.handleAdhocApiError); - } - - if ( - charClass && - !isSameClass(charClass, currentCharClass.current) && // Only pull data if the class has changed from previous fetch - hasSubclassChoice && - subclassLoadingStatus !== DataLoadingStatusEnum.LOADING // Check if its not already fetching data - ) { - currentCharClass.current = charClass; //Update ref of fetched class - setSubclassLoadingStatus(DataLoadingStatusEnum.LOADING); - - loadAvailableSubclasses(classUtils.getId(charClass), { - // cancelToken: new axios.CancelToken((c) => { - // this.loadSubclassesCanceler = c; - // }), - }) - .then((response) => { - let subclassData: Array = []; - - const data = ApiAdapterUtils.getResponseData(response); - if (data !== null) { - subclassData = data; - } - - setSubclassData(subclassData); - setSubclassLoadingStatus(DataLoadingStatusEnum.LOADED); - // this.loadSubclassesCanceler = null; - }) - .catch(AppLoggerUtils.handleAdhocApiError); - } - }, [ - hasFeatChoice, - hasSubclassChoice, - subclassLoadingStatus, - featLoadingStatus, - charClass, - classUtils, - featUtils, - loadAvailableFeats, - loadAvailableSubclasses, - currentCharClass, - ]); - const handleInfusionChoiceShow = (infusionChoice: InfusionChoice): void => { const choiceKey = infusionChoiceUtils.getKey(infusionChoice); if (choiceKey !== null) { @@ -302,21 +173,6 @@ export const ClassFeaturePane: FC = ({ identifiers, ...props }) => { ); }; - const isDataLoaded = (): boolean => { - if (hasSubclassChoice) { - if (subclassLoadingStatus !== DataLoadingStatusEnum.LOADED) { - return false; - } - } - if (hasFeatChoice) { - if (featLoadingStatus !== DataLoadingStatusEnum.LOADED) { - return false; - } - } - - return true; - }; - if (!charClass || !classFeature) { return ; } @@ -356,30 +212,13 @@ export const ClassFeaturePane: FC = ({ identifiers, ...props }) => { onFeatClick={handleFeatShow} featuresManager={characterFeaturesManager} /> - {!isReadonly && featureChoices.length > 0 && ( - <> - {`Option${ - featureChoices.length > 1 ? "s" : "" - }`} - {isDataLoaded() ? ( - featureChoices.map((choice) => ( - - )) - ) : ( - - )} - - )} + ); }; diff --git a/ddb_main/subApps/sheet/components/Sidebar/panes/FeatPane/FeatPane.tsx b/ddb_main/subApps/sheet/components/Sidebar/panes/FeatPane/FeatPane.tsx index 6cca15f..5983132 100644 --- a/ddb_main/subApps/sheet/components/Sidebar/panes/FeatPane/FeatPane.tsx +++ b/ddb_main/subApps/sheet/components/Sidebar/panes/FeatPane/FeatPane.tsx @@ -1,18 +1,17 @@ import { FC, HTMLAttributes, useContext } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { Accordion } from "~/components/Accordion"; +import { ChoiceUtils } from "@dndbeyond/character-rules-engine"; + import { Button } from "~/components/Button"; import { HelperTextAccordion } from "~/components/HelperTextAccordion"; -import { HtmlContent } from "~/components/HtmlContent"; import { useSidebar } from "~/contexts/Sidebar"; import { useCharacterEngine } from "~/hooks/useCharacterEngine"; import { FeatFeatureSnippet } from "~/tools/js/CharacterSheet/components/FeatureSnippet"; -import { DetailChoiceFeat } from "~/tools/js/Shared/containers/DetailChoice"; +import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice"; import { CharacterFeaturesManagerContext } from "~/tools/js/Shared/managers/CharacterFeaturesManagerContext"; import { appEnvSelectors } from "~/tools/js/Shared/selectors"; import { PaneIdentifierUtils } from "~/tools/js/Shared/utils"; -import { DdbBadgeSvg } from "~/tools/js/smartComponents/Svg"; import { Action, Spell } from "~/types"; import { Header } from "../../components/Header"; @@ -38,6 +37,8 @@ export const FeatPane: FC = ({ identifiers, ...props }) => { originRef: dataOriginRefData, characterTheme: theme, entityUtils, + entityRestrictionData, + choiceInfo, } = useCharacterEngine(); const { characterFeaturesManager } = useContext( @@ -115,6 +116,18 @@ export const FeatPane: FC = ({ identifiers, ...props }) => { } }; + const handleChoiceChange = ( + id: string | null, + type: number, + value: any + ): void => { + if (feat && id !== null) { + dispatch( + characterActions.featChoiceSetRequest(feat.getId(), type, id, value) + ); + } + }; + if (feat === null) { return ; } @@ -163,7 +176,31 @@ export const FeatPane: FC = ({ identifiers, ...props }) => { /> {!isReadonly && feat.getChoices().length > 0 && (
- + {feat.getChoices().map((choice) => { + const id = ChoiceUtils.getId(choice); + + const { description, options: featChoiceOptions } = + ChoiceUtils.getSortedChoiceOptionsForFeatsInfo( + feat.getId(), + choice, + ruleData, + entityRestrictionData + ); + + return ( + + ); + })}
)} {!feat.isHiddenFeat() && diff --git a/ddb_main/subApps/sheet/components/Sidebar/panes/FeatsManagePane/FeatDetail/FeatDetail.tsx b/ddb_main/subApps/sheet/components/Sidebar/panes/FeatsManagePane/FeatDetail/FeatDetail.tsx index 856b904..74fca64 100644 --- a/ddb_main/subApps/sheet/components/Sidebar/panes/FeatsManagePane/FeatDetail/FeatDetail.tsx +++ b/ddb_main/subApps/sheet/components/Sidebar/panes/FeatsManagePane/FeatDetail/FeatDetail.tsx @@ -1,9 +1,15 @@ import { FC, HTMLAttributes } from "react"; +import { useDispatch } from "react-redux"; -import { FeatManager } from "@dndbeyond/character-rules-engine/es"; +import { + characterActions, + ChoiceUtils, + FeatManager, +} from "@dndbeyond/character-rules-engine/es"; import { HtmlContent } from "~/components/HtmlContent"; -import { DetailChoiceFeat } from "~/tools/js/Shared/containers/DetailChoice"; +import { useCharacterEngine } from "~/hooks/useCharacterEngine"; +import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice"; import styles from "./styles.module.css"; @@ -12,9 +18,28 @@ interface Props extends HTMLAttributes { } export const FeatDetail: FC = ({ featManager, className, ...props }) => { + const dispatch = useDispatch(); + const { ruleData, entityRestrictionData, choiceInfo } = useCharacterEngine(); + const prerequisiteDescription = featManager.getPrerequisiteDescription(); const featDescription = featManager.getDescription(); + const handleChoiceChange = ( + id: string | null, + type: number, + value: any + ): void => { + if (featManager && id !== null) { + dispatch( + characterActions.featChoiceSetRequest( + featManager.getId(), + type, + id, + value + ) + ); + } + }; return (
{prerequisiteDescription && ( @@ -27,7 +52,31 @@ export const FeatDetail: FC = ({ featManager, className, ...props }) => { )} {featManager.getChoices().length > 0 && (
- + {featManager.getChoices().map((choice) => { + const id = ChoiceUtils.getId(choice); + + const { description, options: featChoiceOptions } = + ChoiceUtils.getSortedChoiceOptionsForFeatsInfo( + featManager.getId(), + choice, + ruleData, + entityRestrictionData + ); + + return ( + + ); + })}
)}
diff --git a/ddb_main/subApps/sheet/components/Sidebar/panes/SpeciesTraitPane/SpeciesTraitPane.tsx b/ddb_main/subApps/sheet/components/Sidebar/panes/SpeciesTraitPane/SpeciesTraitPane.tsx new file mode 100644 index 0000000..2967454 --- /dev/null +++ b/ddb_main/subApps/sheet/components/Sidebar/panes/SpeciesTraitPane/SpeciesTraitPane.tsx @@ -0,0 +1,193 @@ +import { FC, HTMLAttributes, useContext } from "react"; +import { useDispatch, useSelector } from "react-redux"; + +import { + Action, + ActionUtils, + characterActions, + FeatManager, + HelperUtils, + RaceUtils, + RacialTrait, + RacialTraitUtils, + Spell, + SpellUtils, +} from "@dndbeyond/character-rules-engine"; + +import { FeatureChoices } from "~/components/FeatureChoices"; +import { useSidebar } from "~/contexts/Sidebar"; +import { useCharacterEngine } from "~/hooks/useCharacterEngine"; +import { Header } from "~/subApps/sheet/components/Sidebar/components/Header"; +import { Preview } from "~/subApps/sheet/components/Sidebar/components/Preview"; +import { + PaneComponentEnum, + PaneIdentifiersRacialTrait, +} from "~/subApps/sheet/components/Sidebar/types"; +import { SpeciesTraitFeatureSnippet } from "~/tools/js/CharacterSheet/components/FeatureSnippet"; +import { CharacterFeaturesManagerContext } from "~/tools/js/Shared/managers/CharacterFeaturesManagerContext"; +import { appEnvSelectors } from "~/tools/js/Shared/selectors"; +import { PaneIdentifierUtils } from "~/tools/js/Shared/utils"; + +import { PaneInitFailureContent } from "../../components/PaneInitFailureContent"; + +interface Props extends HTMLAttributes { + identifiers: PaneIdentifiersRacialTrait | null; +} + +export const SpeciesTraitPane: FC = ({ identifiers, ...props }) => { + const dispatch = useDispatch(); + const { + race: species, + feats, + snippetData, + ruleData, + abilityLookup, + originRef: dataOriginRefData, + proficiencyBonus, + characterTheme: theme, + } = useCharacterEngine(); + + const { characterFeaturesManager } = useContext( + CharacterFeaturesManagerContext + ); + const isReadonly = useSelector(appEnvSelectors.getIsReadonly); + + const { + pane: { paneHistoryPush, paneHistoryStart }, + } = useSidebar(); + + const speciesTrait = species + ? RaceUtils.getVisibleRacialTraits(species).find( + (trait) => identifiers?.id === RacialTraitUtils.getId(trait) + ) + : null; + + const handleActionUseSet = (action: Action, uses: number): void => { + const id = ActionUtils.getId(action); + const entityTypeId = ActionUtils.getEntityTypeId(action); + if (id !== null && entityTypeId !== null) { + dispatch( + characterActions.actionUseSet( + id, + entityTypeId, + uses, + ActionUtils.getDataOriginType(action) + ) + ); + } + }; + + const handleSpellUseSet = (spell: Spell, uses: number): void => { + const mappingId = SpellUtils.getMappingId(spell); + const mappingEntityTypeId = SpellUtils.getMappingEntityTypeId(spell); + if (mappingId !== null && mappingEntityTypeId !== null) { + dispatch( + characterActions.spellUseSet( + mappingId, + mappingEntityTypeId, + uses, + SpellUtils.getDataOriginType(spell) + ) + ); + } + }; + + const handleSpellDetailShow = (spell: Spell): void => { + const mappingId = SpellUtils.getMappingId(spell); + if (mappingId !== null) { + paneHistoryPush( + PaneComponentEnum.CHARACTER_SPELL_DETAIL, + PaneIdentifierUtils.generateCharacterSpell(mappingId) + ); + } + }; + + const handleSpeciesTraitShow = (speciesTrait: RacialTrait): void => { + paneHistoryStart( + PaneComponentEnum.SPECIES_TRAIT_DETAIL, + PaneIdentifierUtils.generateRacialTrait( + RacialTraitUtils.getId(speciesTrait) + ) + ); + }; + + const handleActionShow = (action: Action): void => { + const mappingId = ActionUtils.getMappingId(action); + const mappingEntityTypeId = ActionUtils.getMappingEntityTypeId(action); + if (mappingId !== null && mappingEntityTypeId !== null) { + paneHistoryPush( + PaneComponentEnum.ACTION, + PaneIdentifierUtils.generateAction(mappingId, mappingEntityTypeId) + ); + } + }; + + const handleFeatShow = (feat: FeatManager): void => { + paneHistoryPush( + PaneComponentEnum.FEAT_DETAIL, + PaneIdentifierUtils.generateFeat(feat.getId()) + ); + }; + + const handleChoiceChange = ( + choiceId: string, + type: number, + value: any + ): void => { + if (!species || !speciesTrait) { + return; + } + + dispatch( + characterActions.racialTraitChoiceSetRequest( + RacialTraitUtils.getId(speciesTrait), + type, + choiceId, + HelperUtils.parseInputInt(value) + ) + ); + }; + + if (species === null || !speciesTrait) { + return ; + } + + const portrait = RaceUtils.getPortraitAvatarUrl(species); + + return ( +
+
} + > + {RacialTraitUtils.getName(speciesTrait)} +
+ + +
+ ); +}; diff --git a/ddb_main/tools/js/CharacterBuilder/containers/pages/ClassesManage/ClassManagerFeature/ClassManagerFeature.tsx b/ddb_main/tools/js/CharacterBuilder/containers/pages/ClassesManage/ClassManagerFeature/ClassManagerFeature.tsx index 5cbcbca..9207d35 100644 --- a/ddb_main/tools/js/CharacterBuilder/containers/pages/ClassesManage/ClassManagerFeature/ClassManagerFeature.tsx +++ b/ddb_main/tools/js/CharacterBuilder/containers/pages/ClassesManage/ClassManagerFeature/ClassManagerFeature.tsx @@ -1,11 +1,8 @@ -import axios, { Canceler } from "axios"; import React from "react"; -import { LoadingPlaceholder } from "@dndbeyond/character-components/es"; import { ApiAdapterPromise, ApiAdapterRequestConfig, - ApiAdapterUtils, ApiResponse, CharacterPreferences, CharClass, @@ -18,10 +15,8 @@ import { Constants, DefinitionPool, EntitledEntity, - Feat, FeatDefinitionContract, FeatLookup, - FeatUtils, FormatUtils, HelperUtils, InfusionChoice, @@ -35,20 +30,16 @@ import { OptionalClassFeatureUtils, PrerequisiteData, RuleData, - RuleDataUtils, - SourceData, TypeValueLookup, } from "@dndbeyond/character-rules-engine/es"; import { Accordion } from "~/components/Accordion"; import { CollapsibleContent } from "~/components/CollapsibleContent"; -import { FeatureChoice } from "~/components/FeatureChoice"; +import { FeatureChoices } from "~/components/FeatureChoices"; import { Link } from "~/components/Link"; import { GrantedFeat } from "~/tools/js/CharacterBuilder/components/GrantedFeat/GrantedFeat"; import InfusionChoiceManager from "../../../../../Shared/components/InfusionChoiceManager"; -import DataLoadingStatusEnum from "../../../../../Shared/constants/DataLoadingStatusEnum"; -import { AppLoggerUtils, TypeScriptUtils } from "../../../../../Shared/utils"; interface Props { isActive: boolean; @@ -122,13 +113,7 @@ interface Props { inventoryManager: InventoryManager; } interface State { - featData: Array; - subclassData: Array; - hasFeatChoice: boolean; - hasSubclassChoice: boolean; collapsibleOpened: boolean; - featLoadingStatus: DataLoadingStatusEnum; - subclassLoadingStatus: DataLoadingStatusEnum; } export default class ClassManagerFeature extends React.PureComponent< Props, @@ -139,157 +124,16 @@ export default class ClassManagerFeature extends React.PureComponent< featLookup: {}, }; - loadFeatsCanceler: null | Canceler = null; - loadSubclassesCanceler: null | Canceler = null; - constructor(props: Props) { super(props); this.state = { - featData: [], - subclassData: [], - hasFeatChoice: false, - hasSubclassChoice: false, collapsibleOpened: false, - featLoadingStatus: DataLoadingStatusEnum.NOT_INITIALIZED, - subclassLoadingStatus: DataLoadingStatusEnum.NOT_INITIALIZED, }; } - componentDidMount(): void { - this.setState( - { - hasFeatChoice: this.hasFeatChoice(this.props), - hasSubclassChoice: this.hasSubclassChoice(this.props), - }, - () => { - this.conditionallyLoadFeatData(); - } - ); - } - - componentDidUpdate( - prevProps: Readonly, - prevState: Readonly, - snapshot?: any - ): void { + hasSubclassChoice = (): boolean => { const { feature } = this.props; - const { collapsibleOpened } = this.state; - - if (feature !== prevProps.feature) { - this.setState( - { - hasFeatChoice: this.hasFeatChoice(this.props), - hasSubclassChoice: this.hasSubclassChoice(this.props), - }, - () => { - this.conditionallyLoadFeatData(); - } - ); - } - if (collapsibleOpened && !prevState.collapsibleOpened) { - this.conditionallyLoadFeatData(); - } - } - - conditionallyLoadFeatData = (): void => { - const { loadAvailableFeats, loadAvailableSubclasses, charClass } = - this.props; - const { - hasFeatChoice, - hasSubclassChoice, - featLoadingStatus, - subclassLoadingStatus, - collapsibleOpened, - } = this.state; - - if ( - hasFeatChoice && - collapsibleOpened && - featLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED - ) { - this.setState({ - featLoadingStatus: DataLoadingStatusEnum.LOADING, - }); - - loadAvailableFeats({ - cancelToken: new axios.CancelToken((c) => { - this.loadFeatsCanceler = c; - }), - }) - .then((response) => { - let featData: Array = []; - - const data = ApiAdapterUtils.getResponseData(response); - if (data !== null) { - featData = data.map((definition) => - FeatUtils.simulateFeat(definition) - ); - } - - this.setState({ - featData, - featLoadingStatus: DataLoadingStatusEnum.LOADED, - }); - this.loadFeatsCanceler = null; - }) - .catch(AppLoggerUtils.handleAdhocApiError); - } - - if ( - hasSubclassChoice && - collapsibleOpened && - subclassLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED - ) { - this.setState({ - subclassLoadingStatus: DataLoadingStatusEnum.LOADING, - }); - - loadAvailableSubclasses(ClassUtils.getId(charClass), { - cancelToken: new axios.CancelToken((c) => { - this.loadSubclassesCanceler = c; - }), - }) - .then((response) => { - let subclassData: Array = []; - - const data = ApiAdapterUtils.getResponseData(response); - if (data !== null) { - subclassData = data; - } - - this.setState({ - subclassData, - subclassLoadingStatus: DataLoadingStatusEnum.LOADED, - }); - this.loadSubclassesCanceler = null; - }) - .catch(AppLoggerUtils.handleAdhocApiError); - } - }; - - componentWillUnmount(): void { - if (this.loadFeatsCanceler !== null) { - this.loadFeatsCanceler(); - } - if (this.loadSubclassesCanceler !== null) { - this.loadSubclassesCanceler(); - } - } - - hasFeatChoice = (props: Props): boolean => { - const { feature } = props; - - const choices = ClassFeatureUtils.getChoices(feature); - return choices.some( - (choice) => - ChoiceUtils.getType(choice) === - Constants.BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION - ); - }; - - hasSubclassChoice = (props: Props): boolean => { - const { feature } = props; const choices = ClassFeatureUtils.getChoices(feature); return choices.some( @@ -299,28 +143,6 @@ export default class ClassManagerFeature extends React.PureComponent< ); }; - isDataLoaded = (): boolean => { - const { - hasSubclassChoice, - hasFeatChoice, - subclassLoadingStatus, - featLoadingStatus, - } = this.state; - - if (hasSubclassChoice) { - if (subclassLoadingStatus !== DataLoadingStatusEnum.LOADED) { - return false; - } - } - if (hasFeatChoice) { - if (featLoadingStatus !== DataLoadingStatusEnum.LOADED) { - return false; - } - } - - return true; - }; - handleCollapsibleOpened = (id: string, state: boolean): void => { this.setState({ collapsibleOpened: state, @@ -436,46 +258,6 @@ export default class ClassManagerFeature extends React.PureComponent< ); }; - getSubclassSources = ( - subclass: ClassDefinitionContract - ): Array => { - const { ruleData } = this.props; - - if (subclass.sources === null) { - return []; - } - - return subclass.sources - .map((sourceMapping) => - HelperUtils.lookupDataOrFallback( - RuleDataUtils.getSourceDataLookup(ruleData), - sourceMapping.sourceId - ) - ) - .filter(TypeScriptUtils.isNotNullOrUndefined); - }; - - getSubclassData = (): Array => { - const { subclassData } = this.state; - const { charClass } = this.props; - - let data: Array = [...subclassData]; - - let existingSubclass = ClassUtils.getSubclass(charClass); - if ( - existingSubclass !== null && - !data.some( - (classDefinition) => - existingSubclass !== null && - classDefinition.id === existingSubclass.id - ) - ) { - data.push(existingSubclass); - } - - return data; - }; - getClassFeatureDescription = (): string => { const { feature, isStartingClass } = this.props; @@ -493,33 +275,6 @@ export default class ClassManagerFeature extends React.PureComponent< return description === null ? "" : description; }; - renderChoices = (): React.ReactNode => { - const { isActive, feature, charClass } = this.props; - const { featData } = this.state; - - if (!isActive) { - return null; - } - - const choices = ClassFeatureUtils.getChoices(feature); - - if (choices === null) { - return null; - } - - return choices.map((choice) => ( - - )); - }; - getMetaItems = () => { const { feature, optionalClassFeatureLookup, definitionPool } = this.props; const choiceCount = this.getTotalChoiceCount(); @@ -590,7 +345,7 @@ export default class ClassManagerFeature extends React.PureComponent< feature, inventoryManager, } = this.props; - const { hasSubclassChoice, hasFeatChoice } = this.state; + const { collapsibleOpened } = this.state; // If the class feature grants feats, render those instead of the class feature. if (feature.featLists.length > 0) { @@ -604,22 +359,11 @@ export default class ClassManagerFeature extends React.PureComponent< } const conClsNames: Array = ["class-manager-feature-name"]; - let contentNode: React.ReactNode; if (isActive) { if (this.getTotalTodoCount() > 0) { conClsNames.push("collapsible-todo"); } - - if (hasFeatChoice || hasSubclassChoice) { - if (this.isDataLoaded()) { - contentNode = this.renderChoices(); - } else { - contentNode = ; - } - } else { - contentNode = this.renderChoices(); - } } const hasChoices = this.getTotalTodoCount() > 0; const accordionId = ClassFeatureUtils.getId(feature).toString(); @@ -636,13 +380,20 @@ export default class ClassManagerFeature extends React.PureComponent< {this.getClassFeatureDescription()} - {isActive && hasSubclassChoice && ( + {isActive && this.hasSubclassChoice() && (
Looking for something not in the list below? Unlock all official options in the Marketplace.
)} - {contentNode} + {isActive && ( + + )} {isActive && ( { handleChoiceChange = ( choiceId: string, choiceType: number, - choiceSubType: number | null, optionValue: any ): void => { const { dispatch } = this.props; diff --git a/ddb_main/tools/js/CharacterBuilder/containers/pages/SpeciesManage/SpeciesManageSpeciesTrait/SpeciesManageSpeciesTrait.tsx b/ddb_main/tools/js/CharacterBuilder/containers/pages/SpeciesManage/SpeciesManageSpeciesTrait/SpeciesManageSpeciesTrait.tsx index e9d0478..775789b 100644 --- a/ddb_main/tools/js/CharacterBuilder/containers/pages/SpeciesManage/SpeciesManageSpeciesTrait/SpeciesManageSpeciesTrait.tsx +++ b/ddb_main/tools/js/CharacterBuilder/containers/pages/SpeciesManage/SpeciesManageSpeciesTrait/SpeciesManageSpeciesTrait.tsx @@ -1,11 +1,8 @@ -import axios, { Canceler } from "axios"; import React from "react"; -import { LoadingPlaceholder } from "@dndbeyond/character-components/es"; import { ApiAdapterPromise, ApiAdapterRequestConfig, - ApiAdapterUtils, ApiResponse, CharacterPreferences, ChoiceData, @@ -13,10 +10,8 @@ import { Constants, CoreUtils, DefinitionPool, - Feat, FeatDefinitionContract, FeatLookup, - FeatUtils, HelperUtils, OptionalOriginLookup, OptionalOriginUtils, @@ -26,12 +21,9 @@ import { } from "@dndbeyond/character-rules-engine/es"; import { Accordion } from "~/components/Accordion"; -import { FeatureChoice } from "~/components/FeatureChoice"; +import { FeatureChoices } from "~/components/FeatureChoices"; import { HtmlContent } from "~/components/HtmlContent"; -import DataLoadingStatusEnum from "../../../../../Shared/constants/DataLoadingStatusEnum"; -import { AppLoggerUtils } from "../../../../../Shared/utils"; - interface Props { speciesTrait: RacialTrait; choiceInfo: ChoiceData; @@ -52,10 +44,7 @@ interface Props { speciesName?: string | null; } interface State { - featData: Array; - hasFeatChoice: boolean; collapsibleOpened: boolean; - loadingStatus: DataLoadingStatusEnum; } export default class SpeciesManageSpeciesTrait extends React.PureComponent< Props, @@ -65,115 +54,10 @@ export default class SpeciesManageSpeciesTrait extends React.PureComponent< super(props); this.state = { - featData: [], - hasFeatChoice: false, collapsibleOpened: false, - loadingStatus: DataLoadingStatusEnum.NOT_INITIALIZED, }; } - loadFeatsCanceler: null | Canceler = null; - - componentDidMount(): void { - this.setState( - { - hasFeatChoice: this.hasFeatChoice(this.props), - }, - () => { - this.conditionallyLoadFeatData(); - } - ); - } - - componentDidUpdate( - prevProps: Readonly, - prevState: Readonly, - snapshot?: any - ): void { - const { speciesTrait } = this.props; - const { collapsibleOpened } = this.state; - - if (speciesTrait !== prevProps.speciesTrait) { - this.setState( - { - hasFeatChoice: this.hasFeatChoice(this.props), - }, - () => { - this.conditionallyLoadFeatData(); - } - ); - } - if (collapsibleOpened && !prevState.collapsibleOpened) { - this.conditionallyLoadFeatData(); - } - } - - componentWillUnmount(): void { - if (this.loadFeatsCanceler !== null) { - this.loadFeatsCanceler(); - } - } - - conditionallyLoadFeatData = (): void => { - const { loadAvailableFeats } = this.props; - const { hasFeatChoice, loadingStatus, collapsibleOpened } = this.state; - - if ( - hasFeatChoice && - collapsibleOpened && - loadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED - ) { - this.setState({ - loadingStatus: DataLoadingStatusEnum.LOADING, - }); - - loadAvailableFeats({ - cancelToken: new axios.CancelToken((c) => { - this.loadFeatsCanceler = c; - }), - }) - .then((response) => { - let featData: Array = []; - - const data = ApiAdapterUtils.getResponseData(response); - if (data !== null) { - featData = data.map((definition) => - FeatUtils.simulateFeat(definition) - ); - } - - this.setState({ - featData, - loadingStatus: DataLoadingStatusEnum.LOADED, - }); - this.loadFeatsCanceler = null; - }) - .catch(AppLoggerUtils.handleAdhocApiError); - } - }; - - getFeatData = (existingFeatId: number | null): Array => { - const { featData } = this.state; - const { featLookup } = this.props; - - let data: Array = [...featData]; - - if (existingFeatId !== null) { - let existingFeat = HelperUtils.lookupDataOrFallback( - featLookup, - existingFeatId - ); - if ( - existingFeat !== null && - !data.some((feat) => FeatUtils.getId(feat) === existingFeatId) - ) { - data.push(existingFeat); - } - } - - return data; - }; - getSummary = () => { const { speciesTrait, speciesName } = this.props; // Get base name for the species trait @@ -277,29 +161,9 @@ export default class SpeciesManageSpeciesTrait extends React.PureComponent< } }; - renderChoices = (): React.ReactNode => { - const { speciesTrait } = this.props; - const { featData } = this.state; - - const choices = RacialTraitUtils.getChoices(speciesTrait); - - if (choices === null) { - return null; - } - - return choices.map((choice) => ( - - )); - }; - render() { const { speciesTrait } = this.props; - const { hasFeatChoice, loadingStatus } = this.state; + const { collapsibleOpened } = this.state; const description = RacialTraitUtils.getDescription(speciesTrait); const choices = RacialTraitUtils.getChoices(speciesTrait); @@ -314,17 +178,6 @@ export default class SpeciesManageSpeciesTrait extends React.PureComponent< conClsNames.push("collapsible-todo"); } - let contentNode: React.ReactNode; - if (hasFeatChoice) { - if (loadingStatus === DataLoadingStatusEnum.LOADED) { - contentNode = this.renderChoices(); - } else { - contentNode = ; - } - } else { - contentNode = this.renderChoices(); - } - return ( - {contentNode} + ); } diff --git a/ddb_main/tools/js/CharacterSheet/components/FeatureSnippet/ClassFeatureSnippet/ClassFeatureSnippet.tsx b/ddb_main/tools/js/CharacterSheet/components/FeatureSnippet/ClassFeatureSnippet/ClassFeatureSnippet.tsx index 2170245..33cad48 100644 --- a/ddb_main/tools/js/CharacterSheet/components/FeatureSnippet/ClassFeatureSnippet/ClassFeatureSnippet.tsx +++ b/ddb_main/tools/js/CharacterSheet/components/FeatureSnippet/ClassFeatureSnippet/ClassFeatureSnippet.tsx @@ -64,12 +64,19 @@ export default class ClassFeatureSnippet extends React.PureComponent { }; renderDescription = (): React.ReactNode => { - const { feature, showDescription } = this.props; + const { feature, showDescription, charClass } = this.props; + const isStartingClass = ClassUtils.isStartingClass(charClass); + const multiClassDescription = + ClassFeatureUtils.getMultiClassDescription(feature); + + // If the class feature is accessible, show the description or multi-class description, otherwise show a "Check out the Marketplace" message. const description: string | null = AccessUtils.isAccessible( ClassFeatureUtils.getAccessType(feature) ) - ? ClassFeatureUtils.getDescription(feature) + ? !isStartingClass && multiClassDescription + ? multiClassDescription + : ClassFeatureUtils.getDescription(feature) : "Check out the Marketplace to unlock this Class Feature."; return showDescription diff --git a/ddb_main/tools/js/CharacterSheet/components/FeatureSnippet/SpeciesTraitFeatureSnippet/SpeciesTraitFeatureSnippet.tsx b/ddb_main/tools/js/CharacterSheet/components/FeatureSnippet/SpeciesTraitFeatureSnippet/SpeciesTraitFeatureSnippet.tsx index a7e518a..4f0293c 100644 --- a/ddb_main/tools/js/CharacterSheet/components/FeatureSnippet/SpeciesTraitFeatureSnippet/SpeciesTraitFeatureSnippet.tsx +++ b/ddb_main/tools/js/CharacterSheet/components/FeatureSnippet/SpeciesTraitFeatureSnippet/SpeciesTraitFeatureSnippet.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { RacialTraitAccessors } from "@dndbeyond/character-rules-engine/es/engine/RacialTrait"; + import { AbilityLookup, BaseFeat, @@ -16,7 +16,6 @@ import { CharacterFeaturesManager, } from "@dndbeyond/character-rules-engine/es"; - import { DataOriginTypeEnum } from "~/constants"; import { FeatureSnippet } from "~/subApps/sheet/components/FeatureSnippet"; @@ -83,7 +82,7 @@ export default class SpeciesTraitFeatureSnippet extends React.PureComponent { /> )} - {replacementWeaponStats.map((statId) => ( -
+ {replacementWeaponStats.map((replacementInfo) => ( +
- this.handleReplaceAbilityStatChange(enabled, statId) + this.handleReplaceAbilityStatChange( + enabled, + replacementInfo.statId + ) + } + label={ + <> + Use{" "} + {RuleDataUtils.getStatNameById( + replacementInfo.statId, + ruleData, + true + )}{" "} + + (From + + {replacementInfo.dataOrigin.primary.definition.name} + + ) + + } - label={`Use ${RuleDataUtils.getStatNameById( - statId, - ruleData, - true - )}`} />
))} diff --git a/ddb_main/tools/js/Shared/containers/DetailChoice/DetailChoice.tsx b/ddb_main/tools/js/Shared/containers/DetailChoice/DetailChoice.tsx index 19a25e8..1d803d0 100644 --- a/ddb_main/tools/js/Shared/containers/DetailChoice/DetailChoice.tsx +++ b/ddb_main/tools/js/Shared/containers/DetailChoice/DetailChoice.tsx @@ -1,3 +1,4 @@ +import clsx from "clsx"; import { FC, ReactNode } from "react"; import { @@ -18,6 +19,7 @@ import Collapsible from "~/tools/js/smartComponents/Collapsible"; import { Select } from "~/tools/js/smartComponents/legacy"; // TODO this needs to not extend builder contract and options typing needs to be fixed +// TODO scss to styles.module.css interface Props extends Omit { choice: Choice; description?: string; @@ -31,7 +33,6 @@ interface Props extends Omit { onChange?: ( id: string, type: number, - subType: number | null, value: any, parentChoiceId: string | null ) => void; @@ -61,12 +62,17 @@ export const DetailChoice: FC = ({ const { entityRestrictionData, ruleData } = useCharacterEngine(); const { languages } = useRuleData(); + const hasTodo = + (ChoiceUtils.isInfinite(choice) && !ChoiceUtils.isOptionSelected(choice)) || + ChoiceUtils.isTodo(choice); + const handleChoiceChange = (value: string): void => { if (onChange && id !== null) { - onChange(id, type, subType, value, parentChoiceId); + onChange(id, type, value, parentChoiceId); } }; + //Don't render the choice if hideWhenOnlyDefaultSelected is true and the choice is only default selected. if ( hideWhenOnlyDefaultSelected && ChoiceUtils.isOnlyDefaultSelected(choice) @@ -74,14 +80,6 @@ export const DetailChoice: FC = ({ return null; } - let classNames: Array = ["detail-choice", className]; - if ( - (ChoiceUtils.isInfinite(choice) && !ChoiceUtils.isOptionSelected(choice)) || - ChoiceUtils.isTodo(choice) - ) { - classNames.push("detail-choice--todo"); - } - // Use the description prop or, if a chosenOption is found and a description wasn't passed in, use the chosenOption description instead. let choiceDescription = description; @@ -94,10 +92,6 @@ export const DetailChoice: FC = ({ choiceDescription = description || chosenOption["description"]; } - if (parentChoiceId !== null) { - classNames.push("detail-choice--child"); - } - let choiceNode: ReactNode; if (choiceDescription) { switch (type) { @@ -142,7 +136,14 @@ export const DetailChoice: FC = ({ } return ( -
+