Grabbed dndbeyond's source code
``` ~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js ```
This commit is contained in:
+358
@@ -0,0 +1,358 @@
|
||||
import axios, { Canceler } from "axios";
|
||||
import React from "react";
|
||||
|
||||
import { LoadingPlaceholder } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
ApiAdapterPromise,
|
||||
ApiAdapterRequestConfig,
|
||||
ApiAdapterUtils,
|
||||
ApiResponse,
|
||||
EntitledEntity,
|
||||
RacialTraitDefinitionContract,
|
||||
DefinitionPool,
|
||||
DefinitionPoolUtils,
|
||||
Constants,
|
||||
DefinitionUtils,
|
||||
RacialTrait,
|
||||
RacialTraitUtils,
|
||||
OptionalOrigin,
|
||||
Race,
|
||||
RaceUtils,
|
||||
OptionalOriginUtils,
|
||||
OptionalOriginLookup,
|
||||
HelperUtils,
|
||||
RuleData,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { Link } from "~/components/Link";
|
||||
|
||||
import DataLoadingStatusEnum from "../../../../../Shared/constants/DataLoadingStatusEnum";
|
||||
import { AppLoggerUtils, TypeScriptUtils } from "../../../../../Shared/utils";
|
||||
import { OptionalFeature } from "../../../../components/OptionalFeature";
|
||||
import { AffectedFeatureInfo } from "../../../../components/OptionalFeature/OptionalFeature";
|
||||
import { PageSubHeader } from "../../../../components/PageSubHeader";
|
||||
|
||||
interface OptionalOriginManagerProps {
|
||||
species: Race;
|
||||
definitionPool: DefinitionPool;
|
||||
optionalOriginLookup: OptionalOriginLookup;
|
||||
loadAvailableOptionalSpeciesTraits?: (
|
||||
additionalConfig?: Partial<ApiAdapterRequestConfig>
|
||||
) => ApiAdapterPromise<
|
||||
ApiResponse<EntitledEntity<RacialTraitDefinitionContract>>
|
||||
>;
|
||||
onDefinitionsLoaded?: (
|
||||
definitionData: EntitledEntity<RacialTraitDefinitionContract>
|
||||
) => void;
|
||||
onSelection: (
|
||||
definitionKey: string,
|
||||
affectedSpeciesTraitDefinitionKey: string | null
|
||||
) => void;
|
||||
onChangeReplacementPromise?: (
|
||||
definitionKey: string,
|
||||
newAffectedDefinitionKey: string | null,
|
||||
oldAffectedDefinitionKey: string | null,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
) => void;
|
||||
onRemoveSelectionPromise?: (
|
||||
definitionKey: string,
|
||||
newIsEnabled: boolean,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
) => void;
|
||||
ruleData: RuleData;
|
||||
}
|
||||
interface OptionalOriginManagerState {
|
||||
loadingStatus: DataLoadingStatusEnum;
|
||||
}
|
||||
export class OptionalOriginManager extends React.PureComponent<
|
||||
OptionalOriginManagerProps,
|
||||
OptionalOriginManagerState
|
||||
> {
|
||||
loadOptionalOriginsCanceler: null | Canceler = null;
|
||||
|
||||
constructor(props: OptionalOriginManagerProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
loadingStatus: DataLoadingStatusEnum.NOT_LOADED,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { loadAvailableOptionalSpeciesTraits, onDefinitionsLoaded } =
|
||||
this.props;
|
||||
|
||||
const { loadingStatus } = this.state;
|
||||
|
||||
if (
|
||||
loadAvailableOptionalSpeciesTraits &&
|
||||
loadingStatus === DataLoadingStatusEnum.NOT_LOADED
|
||||
) {
|
||||
this.setState({
|
||||
loadingStatus: DataLoadingStatusEnum.LOADING,
|
||||
});
|
||||
|
||||
loadAvailableOptionalSpeciesTraits({
|
||||
cancelToken: new axios.CancelToken((c) => {
|
||||
this.loadOptionalOriginsCanceler = c;
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
let data = ApiAdapterUtils.getResponseData(response);
|
||||
if (data?.definitionData.length && onDefinitionsLoaded) {
|
||||
onDefinitionsLoaded(data);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loadingStatus: DataLoadingStatusEnum.LOADED,
|
||||
});
|
||||
this.loadOptionalOriginsCanceler = null;
|
||||
})
|
||||
.catch(AppLoggerUtils.handleAdhocApiError);
|
||||
} else {
|
||||
this.setState({
|
||||
loadingStatus: DataLoadingStatusEnum.LOADED,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if (this.loadOptionalOriginsCanceler !== null) {
|
||||
this.loadOptionalOriginsCanceler();
|
||||
}
|
||||
}
|
||||
|
||||
getAffectedFeatureInfos = (origin: RacialTrait): AffectedFeatureInfo[] => {
|
||||
const { definitionPool, species } = this.props;
|
||||
|
||||
return RacialTraitUtils.getAffectedFeatureDefinitionKeys(origin)
|
||||
.map((definitionKey: string) =>
|
||||
RacialTraitUtils.simulateRacialTrait(definitionKey, definitionPool)
|
||||
)
|
||||
.filter(TypeScriptUtils.isNotNullOrUndefined)
|
||||
.map((affectedSpeciesTrait: RacialTrait) => {
|
||||
const definitionKey =
|
||||
RacialTraitUtils.getDefinitionKey(affectedSpeciesTrait);
|
||||
let disabled = RaceUtils.getOptionalOrigins(species).some(
|
||||
(originMapping: OptionalOrigin) =>
|
||||
OptionalOriginUtils.getDefinitionKey(originMapping) !==
|
||||
RacialTraitUtils.getDefinitionKey(origin) &&
|
||||
OptionalOriginUtils.getAffectedRacialTraitDefinitionKey(
|
||||
originMapping
|
||||
) === definitionKey
|
||||
);
|
||||
|
||||
return {
|
||||
name: RacialTraitUtils.getName(affectedSpeciesTrait),
|
||||
definitionKey,
|
||||
disabled,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
renderOptionalOriginCta = (): React.ReactNode => {
|
||||
return (
|
||||
<div className="ct-optional-origin-manager__content">
|
||||
<div className="ct-optional-origin-manager__content--empty">
|
||||
<span>
|
||||
You currently have no available custom origins for this character
|
||||
</span>
|
||||
<div className="ct-character-tools__marketplace-callout">
|
||||
Unlock all official options in the{" "}
|
||||
<Link href="/marketplace">Marketplace</Link> or create them for{" "}
|
||||
<Link href="/my-creations">Homebrew</Link> Species.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loadingStatus } = this.state;
|
||||
const {
|
||||
definitionPool,
|
||||
species,
|
||||
optionalOriginLookup,
|
||||
onChangeReplacementPromise,
|
||||
onRemoveSelectionPromise,
|
||||
onSelection,
|
||||
} = this.props;
|
||||
|
||||
let contentNode: React.ReactNode;
|
||||
if (loadingStatus !== DataLoadingStatusEnum.LOADED) {
|
||||
contentNode = <LoadingPlaceholder />;
|
||||
} else {
|
||||
let availableOptionalOrigins: RacialTrait[] =
|
||||
DefinitionPoolUtils.getTypedDefinitionList(
|
||||
Constants.DefinitionTypeEnum.RACIAL_TRAIT,
|
||||
definitionPool
|
||||
)
|
||||
.map((speciesTraitDef) =>
|
||||
RacialTraitUtils.simulateRacialTrait(
|
||||
DefinitionUtils.getDefinitionKey(speciesTraitDef),
|
||||
definitionPool
|
||||
)
|
||||
)
|
||||
.filter(TypeScriptUtils.isNotNullOrUndefined)
|
||||
.filter((speciesTrait) =>
|
||||
RacialTraitUtils.isValidRaceRacialTrait(species, speciesTrait)
|
||||
)
|
||||
.filter(
|
||||
(speciesTrait) =>
|
||||
RacialTraitUtils.getFeatureType(speciesTrait) !==
|
||||
Constants.FeatureTypeEnum.GRANTED
|
||||
);
|
||||
|
||||
let availableOptionalOriginLookup = HelperUtils.generateNonNullLookup(
|
||||
availableOptionalOrigins,
|
||||
RacialTraitUtils.getDefinitionKey
|
||||
);
|
||||
|
||||
const unEntitledOptionalSpeciesTraits: RacialTrait[] = [];
|
||||
Object.keys(optionalOriginLookup).forEach((definitionKey) => {
|
||||
if (!availableOptionalOriginLookup.hasOwnProperty(definitionKey)) {
|
||||
const speciesTrait = RacialTraitUtils.simulateRacialTrait(
|
||||
definitionKey,
|
||||
definitionPool
|
||||
);
|
||||
if (
|
||||
speciesTrait &&
|
||||
RacialTraitUtils.isValidRaceRacialTrait(species, speciesTrait)
|
||||
) {
|
||||
unEntitledOptionalSpeciesTraits.push(speciesTrait);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const consolidatedOptionalOrigins: RacialTrait[] = [
|
||||
...availableOptionalOrigins,
|
||||
...unEntitledOptionalSpeciesTraits,
|
||||
].filter(
|
||||
(speciesTrait) =>
|
||||
!RacialTraitUtils.deriveHideInContext(
|
||||
speciesTrait,
|
||||
Constants.AppContextTypeEnum.BUILDER
|
||||
)
|
||||
);
|
||||
|
||||
if (!consolidatedOptionalOrigins.length) {
|
||||
contentNode = this.renderOptionalOriginCta();
|
||||
} else {
|
||||
const replacementOrigins: RacialTrait[] = [];
|
||||
const additionalOrigins: RacialTrait[] = [];
|
||||
consolidatedOptionalOrigins.forEach((speciesTrait) => {
|
||||
switch (RacialTraitUtils.getFeatureType(speciesTrait)) {
|
||||
case Constants.FeatureTypeEnum.ADDITIONAL:
|
||||
additionalOrigins.push(speciesTrait);
|
||||
break;
|
||||
case Constants.FeatureTypeEnum.REPLACEMENT:
|
||||
replacementOrigins.push(speciesTrait);
|
||||
break;
|
||||
default:
|
||||
//not implemented
|
||||
}
|
||||
});
|
||||
|
||||
contentNode = (
|
||||
<div className="ct-optional-origin-manager__content">
|
||||
{replacementOrigins.length > 0 && (
|
||||
<React.Fragment>
|
||||
<PageSubHeader>Replacement Traits</PageSubHeader>
|
||||
{RaceUtils.deriveOrderedRacialTraits(replacementOrigins).map(
|
||||
(origin: RacialTrait) => {
|
||||
const optionalOriginMapping =
|
||||
HelperUtils.lookupDataOrFallback(
|
||||
optionalOriginLookup,
|
||||
RacialTraitUtils.getDefinitionKey(origin)
|
||||
);
|
||||
const affectedFeatureDefinitionKey = optionalOriginMapping
|
||||
? OptionalOriginUtils.getAffectedRacialTraitDefinitionKey(
|
||||
optionalOriginMapping
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<OptionalFeature
|
||||
key={RacialTraitUtils.getDefinitionKey(origin)}
|
||||
name={`Origin ${RacialTraitUtils.getName(origin)}`}
|
||||
description={RacialTraitUtils.getDescription(origin)}
|
||||
featureType={RacialTraitUtils.getFeatureType(origin)}
|
||||
definitionKey={RacialTraitUtils.getDefinitionKey(
|
||||
origin
|
||||
)}
|
||||
affectedFeatureDefinitionKey={
|
||||
affectedFeatureDefinitionKey
|
||||
}
|
||||
isSelected={!!optionalOriginMapping ?? false}
|
||||
onSelection={onSelection}
|
||||
onChangeReplacementPromise={onChangeReplacementPromise}
|
||||
onRemoveSelectionPromise={onRemoveSelectionPromise}
|
||||
affectedFeatures={this.getAffectedFeatureInfos(origin)}
|
||||
accessType={RacialTraitUtils.getAccessType(origin)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{additionalOrigins.length > 0 && (
|
||||
<React.Fragment>
|
||||
<PageSubHeader>Additional Traits</PageSubHeader>
|
||||
{RaceUtils.deriveOrderedRacialTraits(additionalOrigins).map(
|
||||
(origin: RacialTrait) => {
|
||||
const optionalOriginMapping =
|
||||
HelperUtils.lookupDataOrFallback(
|
||||
optionalOriginLookup,
|
||||
RacialTraitUtils.getDefinitionKey(origin)
|
||||
);
|
||||
const affectedFeatureDefinitionKey = optionalOriginMapping
|
||||
? OptionalOriginUtils.getAffectedRacialTraitDefinitionKey(
|
||||
optionalOriginMapping
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<OptionalFeature
|
||||
key={RacialTraitUtils.getDefinitionKey(origin)}
|
||||
name={RacialTraitUtils.getName(origin)}
|
||||
description={RacialTraitUtils.getDescription(origin)}
|
||||
featureType={RacialTraitUtils.getFeatureType(origin)}
|
||||
definitionKey={RacialTraitUtils.getDefinitionKey(
|
||||
origin
|
||||
)}
|
||||
affectedFeatureDefinitionKey={
|
||||
affectedFeatureDefinitionKey
|
||||
}
|
||||
isSelected={!!optionalOriginMapping ?? false}
|
||||
onSelection={onSelection}
|
||||
onChangeReplacementPromise={onChangeReplacementPromise}
|
||||
onRemoveSelectionPromise={onRemoveSelectionPromise}
|
||||
affectedFeatures={this.getAffectedFeatureInfos(origin)}
|
||||
accessType={RacialTraitUtils.getAccessType(origin)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ct-optional-origin-manager">
|
||||
<div className="ct-optional-origin-manager__intro">
|
||||
The following options allow you to customize aspects of your character
|
||||
such as ability scores, languages, and certain proficiencies to fit
|
||||
the origin you have in mind.
|
||||
</div>
|
||||
{contentNode}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OptionalOriginManager;
|
||||
@@ -0,0 +1,541 @@
|
||||
import React from "react";
|
||||
import { DispatchProp } from "react-redux";
|
||||
import { NavigateFunction, useNavigate } from "react-router-dom";
|
||||
|
||||
import {
|
||||
ApiAdapterPromise,
|
||||
ApiAdapterRequestConfig,
|
||||
ApiResponse,
|
||||
characterActions,
|
||||
CharacterPreferences,
|
||||
ChoiceData,
|
||||
ClassSpellListSpellsLookup,
|
||||
Constants,
|
||||
DefinitionPool,
|
||||
DefinitionUtils,
|
||||
EntitledEntity,
|
||||
FeatDefinitionContract,
|
||||
FeatLookup,
|
||||
HelperUtils,
|
||||
OptionalOriginLookup,
|
||||
OptionalOriginUtils,
|
||||
PrerequisiteData,
|
||||
Race,
|
||||
RaceUtils,
|
||||
RacialTraitDefinitionContract,
|
||||
RacialTraitUtils,
|
||||
RuleData,
|
||||
RuleDataUtils,
|
||||
rulesEngineSelectors,
|
||||
serviceDataActions,
|
||||
serviceDataSelectors,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { HelperTextAccordion } from "~/components/HelperTextAccordion";
|
||||
import { HtmlContent } from "~/components/HtmlContent";
|
||||
import { SummaryList } from "~/components/SummaryList";
|
||||
import { TabList } from "~/components/TabList";
|
||||
import { RouteKey } from "~/subApps/builder/constants";
|
||||
import {
|
||||
ModalData,
|
||||
useModalManager,
|
||||
} from "~/subApps/builder/contexts/ModalManager";
|
||||
|
||||
import { SimpleClassSpellList } from "../../../../Shared/components/SimpleClassSpellList";
|
||||
import { apiCreatorSelectors } from "../../../../Shared/selectors";
|
||||
import Page from "../../../components/Page";
|
||||
import { PageBody } from "../../../components/PageBody";
|
||||
import PageHeader from "../../../components/PageHeader";
|
||||
import SpeciesTraitList from "../../../components/SpeciesTraitList";
|
||||
import { navigationConfig } from "../../../config";
|
||||
import ConnectedBuilderPage from "../ConnectedBuilderPage";
|
||||
import { OptionalOriginManager } from "./OptionalOriginManager";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
ruleData: RuleData;
|
||||
preferences: CharacterPreferences;
|
||||
prerequisiteData: PrerequisiteData;
|
||||
choiceInfo: ChoiceData;
|
||||
species: Race | null;
|
||||
featLookup: FeatLookup;
|
||||
definitionPool: DefinitionPool;
|
||||
optionalOriginLookup: OptionalOriginLookup;
|
||||
classSpellListSpellsLookup: ClassSpellListSpellsLookup;
|
||||
navigate: NavigateFunction;
|
||||
characterId: number;
|
||||
loadAvailableOptionalSpeciesTraits: (
|
||||
additionalConfig?: Partial<ApiAdapterRequestConfig>
|
||||
) => ApiAdapterPromise<
|
||||
ApiResponse<EntitledEntity<RacialTraitDefinitionContract>>
|
||||
>;
|
||||
loadAvailableFeats: (
|
||||
additionalConfig?: Partial<ApiAdapterRequestConfig>
|
||||
) => ApiAdapterPromise<ApiResponse<FeatDefinitionContract[]>>;
|
||||
createModal: (modal: ModalData) => void;
|
||||
}
|
||||
|
||||
class SpeciesManage extends React.PureComponent<Props> {
|
||||
handleChangeSpecies = (): void => {
|
||||
const { navigate, characterId } = this.props;
|
||||
navigate(
|
||||
navigationConfig
|
||||
.getRouteDefPath(RouteKey.RACE_CHOOSE)
|
||||
.replace(":characterId", characterId)
|
||||
);
|
||||
};
|
||||
|
||||
handleSpeciesTraitChoiceChange = (
|
||||
speciesTraitId,
|
||||
choiceId,
|
||||
choiceType,
|
||||
optionValue
|
||||
): void => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(
|
||||
characterActions.racialTraitChoiceSetRequest(
|
||||
speciesTraitId,
|
||||
choiceType,
|
||||
choiceId,
|
||||
optionValue
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
handleDefinitionsLoaded = (
|
||||
entitledData: EntitledEntity<RacialTraitDefinitionContract>
|
||||
): void => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(
|
||||
serviceDataActions.definitionPoolAdd(
|
||||
entitledData.definitionData,
|
||||
entitledData.accessTypes
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
handleOptionalOriginSelection = (
|
||||
definitionKey: string,
|
||||
affectedSpeciesTraitDefinitionKey: string | null
|
||||
): void => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
const speciesTraitId =
|
||||
DefinitionUtils.hack__getDefinitionKeyId(definitionKey);
|
||||
if (speciesTraitId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const affectedSpeciesTraitId = affectedSpeciesTraitDefinitionKey
|
||||
? DefinitionUtils.hack__getDefinitionKeyId(
|
||||
affectedSpeciesTraitDefinitionKey
|
||||
)
|
||||
: null;
|
||||
|
||||
dispatch(
|
||||
characterActions.optionalOriginCreate(
|
||||
speciesTraitId,
|
||||
affectedSpeciesTraitId
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
handleRemoveOptionalOriginSelectionPromise = (
|
||||
definitionKey: string,
|
||||
newIsEnabled: boolean,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
): void => {
|
||||
const {
|
||||
dispatch,
|
||||
createModal,
|
||||
optionalOriginLookup,
|
||||
classSpellListSpellsLookup,
|
||||
} = this.props;
|
||||
|
||||
const optionalOrigin = HelperUtils.lookupDataOrFallback(
|
||||
optionalOriginLookup,
|
||||
definitionKey
|
||||
);
|
||||
if (optionalOrigin === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const optionalSpeciesTrait =
|
||||
OptionalOriginUtils.getRacialTrait(optionalOrigin);
|
||||
if (optionalSpeciesTrait === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const speciesTraitId = OptionalOriginUtils.getRacialTraitId(optionalOrigin);
|
||||
const spellListIds =
|
||||
OptionalOriginUtils.getRemoveMappingSpellListIds(optionalOrigin);
|
||||
const hasSpellsToRemove = spellListIds.some((id) =>
|
||||
classSpellListSpellsLookup.hasOwnProperty(id)
|
||||
);
|
||||
|
||||
if (!hasSpellsToRemove) {
|
||||
dispatch(characterActions.optionalOriginDestroy(speciesTraitId));
|
||||
accept();
|
||||
} else {
|
||||
createModal({
|
||||
content: (
|
||||
<div>
|
||||
<p>
|
||||
You are about to remove{" "}
|
||||
<strong>{RacialTraitUtils.getName(optionalSpeciesTrait)}</strong>{" "}
|
||||
from your character.
|
||||
</p>
|
||||
<p>
|
||||
After doing so, the following spells provided by this feature will
|
||||
be removed from your character:
|
||||
</p>
|
||||
<SimpleClassSpellList
|
||||
spellListIds={spellListIds}
|
||||
classSpellListSpellsLookup={classSpellListSpellsLookup}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
props: {
|
||||
heading: "Customized Origin Warning",
|
||||
variant: "remove",
|
||||
size: "fit-content",
|
||||
confirmButtonText: "Remove",
|
||||
onConfirm: () => {
|
||||
dispatch(characterActions.optionalOriginDestroy(speciesTraitId));
|
||||
accept();
|
||||
},
|
||||
onClose: () => {
|
||||
reject();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleOnChangeOptionalOriginReplacementPromise = (
|
||||
definitionKey: string,
|
||||
newAffectedDefinitionKey: string | null,
|
||||
oldAffectedDefinitionKey: string | null,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
): void => {
|
||||
const {
|
||||
dispatch,
|
||||
createModal,
|
||||
optionalOriginLookup,
|
||||
classSpellListSpellsLookup,
|
||||
} = this.props;
|
||||
|
||||
if (newAffectedDefinitionKey === oldAffectedDefinitionKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const optionalOrigin = HelperUtils.lookupDataOrFallback(
|
||||
optionalOriginLookup,
|
||||
definitionKey
|
||||
);
|
||||
if (optionalOrigin === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const optionalSpeciesTrait =
|
||||
OptionalOriginUtils.getRacialTrait(optionalOrigin);
|
||||
if (optionalSpeciesTrait === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const speciesTraitId = OptionalOriginUtils.getRacialTraitId(optionalOrigin);
|
||||
const newAffectedSpeciesTraitId: number | null = newAffectedDefinitionKey
|
||||
? DefinitionUtils.hack__getDefinitionKeyId(newAffectedDefinitionKey)
|
||||
: null;
|
||||
|
||||
const spellListIds =
|
||||
OptionalOriginUtils.getUpdateMappingSpellListIdsToRemove(optionalOrigin, {
|
||||
affectedRacialTraitId: newAffectedSpeciesTraitId,
|
||||
});
|
||||
const hasSpellsToRemove = spellListIds.some((id) =>
|
||||
classSpellListSpellsLookup.hasOwnProperty(id)
|
||||
);
|
||||
|
||||
if (!hasSpellsToRemove) {
|
||||
dispatch(
|
||||
characterActions.optionalOriginSetRequest(
|
||||
speciesTraitId,
|
||||
newAffectedSpeciesTraitId
|
||||
)
|
||||
);
|
||||
accept();
|
||||
} else {
|
||||
createModal({
|
||||
content: (
|
||||
<div>
|
||||
<p>
|
||||
You are about to change the Species Trait to be replaced by{" "}
|
||||
<strong>{RacialTraitUtils.getName(optionalSpeciesTrait)}</strong>
|
||||
</p>
|
||||
<p>
|
||||
After doing so, the following spells provided by this feature will
|
||||
be removed from your character:
|
||||
</p>
|
||||
<SimpleClassSpellList
|
||||
spellListIds={spellListIds}
|
||||
classSpellListSpellsLookup={classSpellListSpellsLookup}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
props: {
|
||||
heading: "Customized Origin Warning",
|
||||
variant: "remove",
|
||||
size: "fit-content",
|
||||
onConfirm: () => {
|
||||
dispatch(
|
||||
characterActions.optionalOriginSetRequest(
|
||||
speciesTraitId,
|
||||
newAffectedSpeciesTraitId
|
||||
)
|
||||
);
|
||||
accept();
|
||||
},
|
||||
onClose: () => {
|
||||
reject();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderSpeciesTraitsGroup = (): React.ReactNode => {
|
||||
const {
|
||||
species,
|
||||
loadAvailableFeats,
|
||||
choiceInfo,
|
||||
prerequisiteData,
|
||||
preferences,
|
||||
featLookup,
|
||||
optionalOriginLookup,
|
||||
definitionPool,
|
||||
} = this.props;
|
||||
|
||||
if (!species) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const speciesTraits = RaceUtils.getVisibleRacialTraits(species);
|
||||
const filteredSpeciesTraits =
|
||||
RacialTraitUtils.filterRacialTraitsByDisplayConfigurationType(
|
||||
speciesTraits,
|
||||
[
|
||||
Constants.DisplayConfigurationTypeEnum.RACIAL_TRAIT,
|
||||
Constants.DisplayConfigurationTypeEnum.LANGUAGE,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="race-detail-secondary">
|
||||
{filteredSpeciesTraits.length > 0 && (
|
||||
<div className="race-detail-racial-traits">
|
||||
<SpeciesTraitList
|
||||
speciesTraits={filteredSpeciesTraits}
|
||||
featLookup={featLookup}
|
||||
loadAvailableFeats={loadAvailableFeats}
|
||||
handleSpeciesTraitChoiceChange={
|
||||
this.handleSpeciesTraitChoiceChange
|
||||
}
|
||||
choiceInfo={choiceInfo}
|
||||
prerequisiteData={prerequisiteData}
|
||||
preferences={preferences}
|
||||
optionalOriginLookup={optionalOriginLookup}
|
||||
definitionPool={definitionPool}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderSpeciesTraitsContent = () => {
|
||||
const { preferences } = this.props;
|
||||
|
||||
if (preferences.enableOptionalOrigins) {
|
||||
return this.renderTabList();
|
||||
}
|
||||
|
||||
return this.renderSpeciesTraitsGroup();
|
||||
};
|
||||
|
||||
renderTabList = () => {
|
||||
const {
|
||||
species,
|
||||
optionalOriginLookup,
|
||||
loadAvailableOptionalSpeciesTraits,
|
||||
definitionPool,
|
||||
ruleData,
|
||||
} = this.props;
|
||||
|
||||
if (!species) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TabList
|
||||
className="manage-race-tabs"
|
||||
variant="collapse"
|
||||
tabs={[
|
||||
{
|
||||
label: "Species Traits",
|
||||
content: this.renderSpeciesTraitsGroup(),
|
||||
id: "traits",
|
||||
},
|
||||
{
|
||||
label: "Origin Manager",
|
||||
content: (
|
||||
<OptionalOriginManager
|
||||
species={species}
|
||||
definitionPool={definitionPool}
|
||||
optionalOriginLookup={optionalOriginLookup}
|
||||
onDefinitionsLoaded={this.handleDefinitionsLoaded}
|
||||
loadAvailableOptionalSpeciesTraits={
|
||||
loadAvailableOptionalSpeciesTraits
|
||||
}
|
||||
onSelection={this.handleOptionalOriginSelection}
|
||||
onChangeReplacementPromise={
|
||||
this.handleOnChangeOptionalOriginReplacementPromise
|
||||
}
|
||||
onRemoveSelectionPromise={
|
||||
this.handleRemoveOptionalOriginSelectionPromise
|
||||
}
|
||||
ruleData={ruleData}
|
||||
/>
|
||||
),
|
||||
id: "Origin Manager",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderInformationCollapsible = (): React.ReactNode => {
|
||||
const { ruleData, species } = this.props;
|
||||
if (species === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const definitionKey = DefinitionUtils.hack__generateDefinitionKey(
|
||||
RaceUtils.getEntityRaceTypeId(species),
|
||||
RaceUtils.getEntityRaceId(species)
|
||||
);
|
||||
const builderText = RuleDataUtils.getBuilderHelperTextByDefinitionKeys(
|
||||
[definitionKey],
|
||||
ruleData,
|
||||
Constants.DisplayConfigurationTypeEnum.RACIAL_TRAIT
|
||||
);
|
||||
|
||||
return <HelperTextAccordion builderHelperText={builderText} />;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { species, ruleData } = this.props;
|
||||
|
||||
//TODO render something else if species doesnt exist
|
||||
if (!species) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fullName = RaceUtils.getFullName(species);
|
||||
const portraitAvatarUrl = RaceUtils.getPortraitAvatarUrl(species);
|
||||
const description = RaceUtils.getDescription(species);
|
||||
const moreDetailsUrl = RaceUtils.getMoreDetailsUrl(species);
|
||||
const previewUrl: string | null =
|
||||
portraitAvatarUrl ?? RuleDataUtils.getDefaultRaceImageUrl(ruleData);
|
||||
|
||||
const descriptionClassNames: string[] = ["race-detail-description"];
|
||||
|
||||
const calledOutSpeciesTraits = RaceUtils.getCalledOutRacialTraits(species);
|
||||
if (calledOutSpeciesTraits.length > 0) {
|
||||
descriptionClassNames.push("race-detail-description--hide-traits");
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageBody>
|
||||
<div className="manage-race race-detail">
|
||||
<div className="race-detail-primary">
|
||||
<div className="race-detail-aside">
|
||||
<div className="race-detail-preview">
|
||||
<img
|
||||
className="race-detail-preview-img"
|
||||
src={previewUrl ? previewUrl : ""}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="manage-race-chosen-action"
|
||||
onClick={this.handleChangeSpecies}
|
||||
>
|
||||
Change Species
|
||||
</div>
|
||||
</div>
|
||||
<PageHeader>{fullName}</PageHeader>
|
||||
<HtmlContent
|
||||
className={descriptionClassNames.join(" ")}
|
||||
html={description ? description : ""}
|
||||
withoutTooltips
|
||||
/>
|
||||
{calledOutSpeciesTraits.length > 0 && (
|
||||
<SummaryList
|
||||
list={calledOutSpeciesTraits}
|
||||
title="Species Traits"
|
||||
className={styles.summaryList}
|
||||
/>
|
||||
)}
|
||||
<div className="race-detail-more">
|
||||
<a
|
||||
className="race-detail-more-link"
|
||||
href={moreDetailsUrl ? moreDetailsUrl : ""}
|
||||
>
|
||||
{fullName} Details Page
|
||||
</a>
|
||||
</div>
|
||||
{this.renderInformationCollapsible()}
|
||||
</div>
|
||||
{this.renderSpeciesTraitsContent()}
|
||||
</div>
|
||||
</PageBody>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SpeciesManageWithHooks = (props) => {
|
||||
const navigate = useNavigate();
|
||||
const { createModal } = useModalManager();
|
||||
return (
|
||||
<SpeciesManage {...props} navigate={navigate} createModal={createModal} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectedBuilderPage(
|
||||
SpeciesManageWithHooks,
|
||||
RouteKey.RACE_MANAGE,
|
||||
(state) => {
|
||||
return {
|
||||
characterId: rulesEngineSelectors.getId(state),
|
||||
species: rulesEngineSelectors.getRace(state),
|
||||
choiceInfo: rulesEngineSelectors.getChoiceInfo(state),
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
prerequisiteData: rulesEngineSelectors.getPrerequisiteData(state),
|
||||
preferences: rulesEngineSelectors.getCharacterPreferences(state),
|
||||
featLookup: rulesEngineSelectors.getFeatLookup(state),
|
||||
definitionPool: serviceDataSelectors.getDefinitionPool(state),
|
||||
loadAvailableOptionalSpeciesTraits:
|
||||
apiCreatorSelectors.makeLoadAvailableOptionalRacialTraits(state),
|
||||
loadAvailableFeats: apiCreatorSelectors.makeLoadAvailableFeats(state),
|
||||
optionalOriginLookup: rulesEngineSelectors.getOptionalOriginLookup(state),
|
||||
classSpellListSpellsLookup:
|
||||
rulesEngineSelectors.getClassSpellListSpellsLookup(state),
|
||||
};
|
||||
}
|
||||
);
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
import axios, { Canceler } from "axios";
|
||||
import React from "react";
|
||||
|
||||
import { LoadingPlaceholder } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
ApiAdapterPromise,
|
||||
ApiAdapterRequestConfig,
|
||||
ApiAdapterUtils,
|
||||
ApiResponse,
|
||||
CharacterPreferences,
|
||||
ChoiceData,
|
||||
ChoiceUtils,
|
||||
Constants,
|
||||
CoreUtils,
|
||||
DefinitionPool,
|
||||
Feat,
|
||||
FeatDefinitionContract,
|
||||
FeatLookup,
|
||||
FeatUtils,
|
||||
HelperUtils,
|
||||
OptionalOriginLookup,
|
||||
OptionalOriginUtils,
|
||||
PrerequisiteData,
|
||||
RacialTrait,
|
||||
RacialTraitUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { Accordion } from "~/components/Accordion";
|
||||
import { FeatureChoice } from "~/components/FeatureChoice";
|
||||
import { HtmlContent } from "~/components/HtmlContent";
|
||||
|
||||
import DataLoadingStatusEnum from "../../../../../Shared/constants/DataLoadingStatusEnum";
|
||||
import { AppLoggerUtils } from "../../../../../Shared/utils";
|
||||
|
||||
interface Props {
|
||||
speciesTrait: RacialTrait;
|
||||
choiceInfo: ChoiceData;
|
||||
preferences: CharacterPreferences;
|
||||
prerequisiteData: PrerequisiteData;
|
||||
featLookup: FeatLookup;
|
||||
onChoiceChange: (
|
||||
speciesTraitId: number,
|
||||
choiceId: string,
|
||||
type: number,
|
||||
value: any
|
||||
) => void;
|
||||
loadAvailableFeats: (
|
||||
additionalConfig?: Partial<ApiAdapterRequestConfig>
|
||||
) => ApiAdapterPromise<ApiResponse<Array<FeatDefinitionContract>>>;
|
||||
optionalOriginLookup: OptionalOriginLookup;
|
||||
definitionPool: DefinitionPool;
|
||||
speciesName?: string | null;
|
||||
}
|
||||
interface State {
|
||||
featData: Array<Feat>;
|
||||
hasFeatChoice: boolean;
|
||||
collapsibleOpened: boolean;
|
||||
loadingStatus: DataLoadingStatusEnum;
|
||||
}
|
||||
export default class SpeciesManageSpeciesTrait extends React.PureComponent<
|
||||
Props,
|
||||
State
|
||||
> {
|
||||
constructor(props: Props) {
|
||||
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<Props>,
|
||||
prevState: Readonly<State>,
|
||||
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<Feat> = [];
|
||||
|
||||
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<Feat> => {
|
||||
const { featData } = this.state;
|
||||
const { featLookup } = this.props;
|
||||
|
||||
let data: Array<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;
|
||||
};
|
||||
|
||||
getSummary = () => {
|
||||
const { speciesTrait, speciesName } = this.props;
|
||||
// Get base name for the species trait
|
||||
let name = RacialTraitUtils.getName(speciesTrait);
|
||||
// Get the feature type for the species trait
|
||||
const featureType = RacialTraitUtils.getFeatureType(speciesTrait);
|
||||
// Get the display configuration for the species trait
|
||||
const displayConfiguration =
|
||||
RacialTraitUtils.getDisplayConfiguration(speciesTrait);
|
||||
// Determine if we should add the species name to the name
|
||||
const addName = displayConfiguration
|
||||
? CoreUtils.getDisplayConfigurationValue(
|
||||
Constants.DisplayConfigurationTypeEnum.RACIAL_TRAIT,
|
||||
displayConfiguration
|
||||
) === Constants.DisplayConfigurationValueEnum.ON
|
||||
: false;
|
||||
// Add the species name to the name if necessary
|
||||
if (speciesName && addName) name = `${name} (${speciesName})`;
|
||||
// Add "origin" to the name if it's an optional origin replacement feature
|
||||
if (featureType === Constants.FeatureTypeEnum.REPLACEMENT)
|
||||
name = `Origin ${name}`;
|
||||
// Return constructed name
|
||||
return name;
|
||||
};
|
||||
|
||||
getMetaItems = () => {
|
||||
const { speciesTrait, optionalOriginLookup, definitionPool } = this.props;
|
||||
const displayConfiguration =
|
||||
RacialTraitUtils.getDisplayConfiguration(speciesTrait);
|
||||
const choices = RacialTraitUtils.getChoices(speciesTrait);
|
||||
let metaItems: Array<string> = [];
|
||||
if (choices.length) {
|
||||
metaItems.push(
|
||||
`${choices.length} Choice${choices.length !== 1 ? "s" : ""}`
|
||||
);
|
||||
}
|
||||
const featureType = RacialTraitUtils.getFeatureType(speciesTrait);
|
||||
const addOrigin = displayConfiguration
|
||||
? CoreUtils.getDisplayConfigurationValue(
|
||||
Constants.DisplayConfigurationTypeEnum.RACIAL_TRAIT,
|
||||
displayConfiguration
|
||||
) === Constants.DisplayConfigurationValueEnum.OFF
|
||||
: false;
|
||||
if (featureType !== Constants.FeatureTypeEnum.GRANTED || addOrigin) {
|
||||
metaItems.push("Origin");
|
||||
}
|
||||
if (featureType === Constants.FeatureTypeEnum.REPLACEMENT) {
|
||||
const optionalOrigin = HelperUtils.lookupDataOrFallback(
|
||||
optionalOriginLookup,
|
||||
RacialTraitUtils.getDefinitionKey(speciesTrait)
|
||||
);
|
||||
if (optionalOrigin) {
|
||||
const affectedDefinitionKey =
|
||||
OptionalOriginUtils.getAffectedRacialTraitDefinitionKey(
|
||||
optionalOrigin
|
||||
);
|
||||
if (affectedDefinitionKey) {
|
||||
const replacedSpeciesTrait = RacialTraitUtils.simulateRacialTrait(
|
||||
affectedDefinitionKey,
|
||||
definitionPool
|
||||
);
|
||||
if (replacedSpeciesTrait) {
|
||||
metaItems.push(
|
||||
`Replaces ${RacialTraitUtils.getName(replacedSpeciesTrait)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return metaItems;
|
||||
};
|
||||
|
||||
hasFeatChoice = (props: Props): boolean => {
|
||||
const { speciesTrait } = props;
|
||||
|
||||
const choices = RacialTraitUtils.getChoices(speciesTrait);
|
||||
return choices.some(
|
||||
(choice) =>
|
||||
ChoiceUtils.getType(choice) ===
|
||||
Constants.BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION
|
||||
);
|
||||
};
|
||||
|
||||
handleCollapsibleOpened = (id: string, state: boolean): void => {
|
||||
this.setState({
|
||||
collapsibleOpened: state,
|
||||
});
|
||||
};
|
||||
|
||||
handleChoiceChange = (choiceId: string, type: number, value: any): void => {
|
||||
const { onChoiceChange, speciesTrait } = this.props;
|
||||
|
||||
if (onChoiceChange) {
|
||||
onChoiceChange(
|
||||
RacialTraitUtils.getId(speciesTrait),
|
||||
choiceId,
|
||||
type,
|
||||
HelperUtils.parseInputInt(value)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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) => (
|
||||
<FeatureChoice
|
||||
choice={choice}
|
||||
featsData={featData}
|
||||
onChoiceChange={this.handleChoiceChange}
|
||||
key={ChoiceUtils.getId(choice)}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { speciesTrait } = this.props;
|
||||
const { hasFeatChoice, loadingStatus } = this.state;
|
||||
|
||||
const description = RacialTraitUtils.getDescription(speciesTrait);
|
||||
const choices = RacialTraitUtils.getChoices(speciesTrait);
|
||||
const accordionId = RacialTraitUtils.getId(speciesTrait).toString();
|
||||
|
||||
const hasTodoChoices: boolean = choices.some(
|
||||
(choice) => ChoiceUtils.getOptionValue(choice) === null
|
||||
);
|
||||
|
||||
const conClsNames: Array<string> = ["race-detail-racial-trait-name"];
|
||||
if (hasTodoChoices) {
|
||||
conClsNames.push("collapsible-todo");
|
||||
}
|
||||
|
||||
let contentNode: React.ReactNode;
|
||||
if (hasFeatChoice) {
|
||||
if (loadingStatus === DataLoadingStatusEnum.LOADED) {
|
||||
contentNode = this.renderChoices();
|
||||
} else {
|
||||
contentNode = <LoadingPlaceholder />;
|
||||
}
|
||||
} else {
|
||||
contentNode = this.renderChoices();
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
id={accordionId}
|
||||
summary={this.getSummary()}
|
||||
summaryMetaItems={this.getMetaItems()}
|
||||
variant="paper"
|
||||
showAlert={hasTodoChoices}
|
||||
handleIsOpen={this.handleCollapsibleOpened}
|
||||
>
|
||||
<HtmlContent
|
||||
className="race-detail-racial-trait-description"
|
||||
html={description ? description : ""}
|
||||
withoutTooltips
|
||||
/>
|
||||
{contentNode}
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import SpeciesManageSpeciesTrait from "./SpeciesManageSpeciesTrait";
|
||||
|
||||
export default SpeciesManageSpeciesTrait;
|
||||
export { SpeciesManageSpeciesTrait };
|
||||
@@ -0,0 +1,5 @@
|
||||
import SpeciesManage from "./SpeciesManage";
|
||||
import SpeciesManageSpeciesTrait from "./SpeciesManageSpeciesTrait";
|
||||
|
||||
export default SpeciesManage;
|
||||
export { SpeciesManage, SpeciesManageSpeciesTrait };
|
||||
Reference in New Issue
Block a user