Skip to content
Snippets Groups Projects
Unverified Commit b228f16b authored by Uladzimir Havenchyk's avatar Uladzimir Havenchyk Committed by GitHub
Browse files

refactor: split DashCardCardParameterMapper into smaller components to reduce complexity (#44526)

* Exrtract 'useResetParameterMapping'

* Extract 'DashCardCardParameterMapperButton'

* Extract 'DashCardCardParameterMapperContent'

* cleanup

* Drop if/else

* cleanup
parent 260f8b03
Branches
Tags
No related merge requests found
import { useState, useMemo, useCallback, useEffect } from "react";
import { connect } from "react-redux";
import { useMount, usePrevious } from "react-use";
import { t } from "ttag";
import _ from "underscore";
import { isActionDashCard } from "metabase/actions/utils";
import TippyPopover from "metabase/components/Popover/TippyPopover";
import { Ellipsified } from "metabase/core/components/Ellipsified";
import DeprecatedTooltip from "metabase/core/components/Tooltip";
import CS from "metabase/css/core/index.css";
import {
isNativeDashCard,
isVirtualDashCard,
getVirtualCardType,
showVirtualDashCardInfoText,
isQuestionDashCard,
} from "metabase/dashboard/utils";
import { useDispatch } from "metabase/lib/redux";
import ParameterTargetList from "metabase/parameters/components/ParameterTargetList";
getEditingParameter,
getDashcardParameterMappingOptions,
getParameterTarget,
getQuestionByCard,
} from "metabase/dashboard/selectors";
import { isNativeDashCard, isQuestionDashCard } from "metabase/dashboard/utils";
import type { ParameterMappingOption } from "metabase/parameters/utils/mapping-options";
import { getIsRecentlyAutoConnectedDashcard } from "metabase/redux/undo";
import { getMetadata } from "metabase/selectors/metadata";
import { Flex, Icon, Text, Transition, Tooltip, Box } from "metabase/ui";
import { Flex, Icon, Text, Transition } from "metabase/ui";
import {
MOBILE_HEIGHT_BY_DISPLAY_TYPE,
MOBILE_DEFAULT_CARD_HEIGHT,
} from "metabase/visualizations/shared/utils/sizes";
import type Question from "metabase-lib/v1/Question";
import {
getParameterSubType,
isDateParameter,
isTemporalUnitParameter,
} from "metabase-lib/v1/parameters/utils/parameter-type";
import { isDateParameter } from "metabase-lib/v1/parameters/utils/parameter-type";
import { isParameterVariableTarget } from "metabase-lib/v1/parameters/utils/targets";
import type {
Card,
CardId,
DashCardId,
DashboardCard,
Parameter,
ParameterId,
ParameterTarget,
} from "metabase-types/api";
import type { State } from "metabase-types/store";
import { resetParameterMapping, setParameterMapping } from "../../../actions";
import {
getEditingParameter,
getDashcardParameterMappingOptions,
getParameterTarget,
getQuestionByCard,
} from "../../../selectors";
import { getMappingOptionByTarget } from "../utils";
import {
Container,
CardLabel,
Header,
TargetButton,
TargetButtonText,
TextCardDefault,
CloseIconButton,
ChevrondownIcon,
KeyIcon,
Warning,
} from "./DashCardCardParameterMapper.styled";
import { DisabledNativeCardHelpText } from "./DisabledNativeCardHelpText";
function formatSelected({
name,
sectionName,
}: {
name: string;
sectionName?: string;
}) {
if (sectionName == null) {
// for native question variables or field literals we just display the name
return name;
}
return `${sectionName}.${name}`;
}
import { DashCardCardParameterMapperContent } from "./DashCardCardParameterMapperContent";
import { useResetParameterMapping } from "./hooks";
const mapStateToProps = (
state: State,
......@@ -90,7 +47,6 @@ const mapStateToProps = (
return {
editingParameter,
target: getParameterTarget(state, props),
metadata: getMetadata(state),
question: getQuestionByCard(state, props),
mappingOptions: getDashcardParameterMappingOptions(state, props),
isRecentlyAutoConnected: getIsRecentlyAutoConnectedDashcard(
......@@ -101,21 +57,11 @@ const mapStateToProps = (
};
};
const mapDispatchToProps = {
setParameterMapping,
};
interface DashcardCardParameterMapperProps {
card: Card;
dashcard: DashboardCard;
editingParameter: Parameter | null | undefined;
target: ParameterTarget | null | undefined;
setParameterMapping: (
parameterId: ParameterId,
dashcardId: DashCardId,
cardId: CardId,
target: ParameterTarget | null,
) => void;
isMobile: boolean;
// virtual cards will not have question
question?: Question;
......@@ -128,61 +74,22 @@ export function DashCardCardParameterMapper({
dashcard,
editingParameter,
target,
setParameterMapping,
isMobile,
question,
mappingOptions,
isRecentlyAutoConnected,
}: DashcardCardParameterMapperProps) {
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const prevParameter = usePrevious(editingParameter);
const dispatch = useDispatch();
const hasSeries =
isQuestionDashCard(dashcard) &&
dashcard.series &&
dashcard.series.length > 0;
const isQuestion = isQuestionDashCard(dashcard);
const hasSeries = isQuestion && dashcard.series && dashcard.series.length > 0;
const isDisabled = mappingOptions.length === 0 || isActionDashCard(dashcard);
const isVirtual = isVirtualDashCard(dashcard);
const virtualCardType = getVirtualCardType(dashcard);
const isNative = isQuestionDashCard(dashcard) && isNativeDashCard(dashcard);
const isTemporalUnit =
editingParameter != null && isTemporalUnitParameter(editingParameter);
const isNative = isQuestion && isNativeDashCard(dashcard);
useEffect(() => {
if (!prevParameter || !editingParameter) {
return;
}
if (
isNative &&
isDisabled &&
prevParameter.type !== editingParameter.type
) {
const subType = getParameterSubType(editingParameter);
const prevSubType = getParameterSubType(prevParameter);
if (prevSubType === "=" && subType !== "=") {
dispatch(resetParameterMapping(editingParameter.id, dashcard.id));
}
}
}, [
useResetParameterMapping({
editingParameter,
isNative,
isDisabled,
prevParameter,
editingParameter,
dispatch,
dashcard.id,
]);
const handleChangeTarget = useCallback(
(target: ParameterTarget | null) => {
if (editingParameter) {
setParameterMapping(editingParameter.id, dashcard.id, card.id, target);
}
},
[card.id, dashcard.id, editingParameter, setParameterMapping],
);
dashcardId: dashcard.id,
});
const selectedMappingOption = getMappingOptionByTarget(
mappingOptions,
......@@ -192,197 +99,32 @@ export function DashCardCardParameterMapper({
editingParameter ?? undefined,
);
const hasPermissionsToMap = useMemo(() => {
if (isVirtual) {
return true;
}
// virtual or action dashcard
if (!isQuestionDashCard(dashcard)) {
return true;
}
if (!question || !card.dataset_query) {
return false;
}
return question.canRunAdhocQuery();
}, [isVirtual, dashcard, card.dataset_query, question]);
const { buttonVariant, buttonTooltip, buttonText, buttonIcon } =
useMemo(() => {
if (!hasPermissionsToMap) {
return {
buttonVariant: "unauthed",
buttonTooltip: t`You don’t have permission to see this question’s columns.`,
buttonText: null,
buttonIcon: <KeyIcon name="key" />,
};
} else if (isDisabled && !isVirtual) {
return {
buttonVariant: "disabled",
buttonTooltip: t`This card doesn't have any fields or parameters that can be mapped to this parameter type.`,
buttonText: t`No valid fields`,
buttonIcon: null,
};
} else if (selectedMappingOption) {
return {
buttonVariant: "mapped",
buttonTooltip: null,
buttonText: formatSelected(selectedMappingOption),
buttonIcon: (
<CloseIconButton
role="button"
aria-label={t`Disconnect`}
onClick={e => {
handleChangeTarget(null);
e.stopPropagation();
}}
/>
),
};
} else if (target != null) {
return {
buttonVariant: "invalid",
buttonText: t`Unknown Field`,
buttonIcon: (
<CloseIconButton
onClick={e => {
handleChangeTarget(null);
e.stopPropagation();
}}
/>
),
};
} else {
return {
buttonVariant: "default",
buttonTooltip: null,
buttonText: t`Select…`,
buttonIcon: <ChevrondownIcon name="chevrondown" />,
};
}
}, [
hasPermissionsToMap,
isDisabled,
selectedMappingOption,
target,
handleChangeTarget,
isVirtual,
]);
const layoutHeight = isMobile
? MOBILE_HEIGHT_BY_DISPLAY_TYPE[dashcard.card.display] ||
MOBILE_DEFAULT_CARD_HEIGHT
: dashcard.size_y;
const headerContent = useMemo(() => {
if (layoutHeight > 2) {
if (isTemporalUnit) {
return t`Connect to`;
}
if (!isVirtual && !(isNative && isDisabled)) {
return t`Column to filter on`;
}
return t`Variable to map to`;
}
return null;
}, [layoutHeight, isTemporalUnit, isVirtual, isNative, isDisabled]);
const mappingInfoText =
(virtualCardType &&
{
heading: t`You can connect widgets to {{variables}} in heading cards.`,
text: t`You can connect widgets to {{variables}} in text cards.`,
link: t`You cannot connect variables to link cards.`,
action: t`Open this card's action settings to connect variables`,
placeholder: "",
}[virtualCardType]) ??
"";
const shouldShowAutoConnectHint =
isRecentlyAutoConnected && !!selectedMappingOption;
return (
<Container isSmall={!isMobile && dashcard.size_y < 2}>
{hasSeries && <CardLabel>{card.name}</CardLabel>}
{isVirtual && isDisabled ? (
showVirtualDashCardInfoText(dashcard, isMobile) ? (
<TextCardDefault>
<Icon name="info" size={12} className={CS.pr1} />
{mappingInfoText}
</TextCardDefault>
) : (
<TextCardDefault aria-label={mappingInfoText}>
<Icon
name="info"
size={16}
className={CS.textDarkHover}
tooltip={mappingInfoText}
/>
</TextCardDefault>
)
) : isNative && isDisabled && question && editingParameter ? (
<DisabledNativeCardHelpText
question={question}
parameter={editingParameter}
/>
) : (
<>
{headerContent && (
<Header>
<Ellipsified>{headerContent}</Ellipsified>
</Header>
)}
<Flex align="center" justify="center" gap="xs" pos="relative">
<DeprecatedTooltip tooltip={buttonTooltip}>
<TippyPopover
visible={
isDropdownVisible && !isDisabled && hasPermissionsToMap
}
onClickOutside={() => setIsDropdownVisible(false)}
placement="bottom-start"
content={
<ParameterTargetList
onChange={(target: ParameterTarget) => {
handleChangeTarget(target);
setIsDropdownVisible(false);
}}
target={target}
mappingOptions={mappingOptions}
/>
}
>
<TargetButton
variant={buttonVariant}
aria-label={buttonTooltip ?? undefined}
aria-haspopup="listbox"
aria-expanded={isDropdownVisible}
aria-disabled={isDisabled || !hasPermissionsToMap}
onClick={() => {
setIsDropdownVisible(true);
}}
onKeyDown={e => {
if (e.key === "Enter") {
setIsDropdownVisible(true);
}
}}
>
{buttonText && (
<TargetButtonText>
<Ellipsified>{buttonText}</Ellipsified>
</TargetButtonText>
)}
{buttonIcon}
</TargetButton>
</TippyPopover>
</DeprecatedTooltip>
{shouldShowAutoConnectHint &&
layoutHeight <= 3 &&
dashcard.size_x > 4 && <AutoConnectedAnimatedIcon />}
</Flex>
</>
)}
<DashCardCardParameterMapperContent
isNative={isNative}
isDisabled={isDisabled}
isMobile={isMobile}
dashcard={dashcard}
question={question}
editingParameter={editingParameter}
mappingOptions={mappingOptions}
isQuestion={isQuestion}
card={card}
selectedMappingOption={selectedMappingOption}
target={target}
shouldShowAutoConnectHint={shouldShowAutoConnectHint}
layoutHeight={layoutHeight}
/>
<Transition
mounted={shouldShowAutoConnectHint && layoutHeight > 3}
transition="fade"
......@@ -423,29 +165,6 @@ export function DashCardCardParameterMapper({
);
}
function AutoConnectedAnimatedIcon() {
const [mounted, setMounted] = useState(false);
useMount(() => {
setMounted(true);
});
return (
<Transition transition="fade" mounted={mounted} exitDuration={0}>
{styles => {
return (
<Box component="span" style={styles} pos="absolute" right={-20}>
<Tooltip label={t`Auto-connected`}>
<Icon name="sparkles" />
</Tooltip>
</Box>
);
}}
</Transition>
);
}
export const DashCardCardParameterMapperConnected = connect(
mapStateToProps,
mapDispatchToProps,
)(DashCardCardParameterMapper);
export const DashCardCardParameterMapperConnected = connect(mapStateToProps)(
DashCardCardParameterMapper,
);
import { useState, useMemo } from "react";
import { t } from "ttag";
import _ from "underscore";
import TippyPopover from "metabase/components/Popover/TippyPopover";
import { Ellipsified } from "metabase/core/components/Ellipsified";
import DeprecatedTooltip from "metabase/core/components/Tooltip";
import ParameterTargetList from "metabase/parameters/components/ParameterTargetList";
import type { ParameterMappingOption } from "metabase/parameters/utils/mapping-options";
import type Question from "metabase-lib/v1/Question";
import type { Card, ParameterTarget } from "metabase-types/api";
import {
TargetButton,
TargetButtonText,
CloseIconButton,
ChevrondownIcon,
KeyIcon,
} from "./DashCardCardParameterMapper.styled";
interface DashCardCardParameterMapperButtonProps {
isDisabled: boolean;
isVirtual: boolean;
isQuestion: boolean;
question: Question | undefined;
card: Card;
handleChangeTarget: (target: ParameterTarget | null) => void;
selectedMappingOption: ParameterMappingOption | undefined;
target: ParameterTarget | null | undefined;
mappingOptions: ParameterMappingOption[];
}
export const DashCardCardParameterMapperButton = ({
isDisabled,
handleChangeTarget,
isVirtual,
isQuestion,
question,
card,
selectedMappingOption,
target,
mappingOptions,
}: DashCardCardParameterMapperButtonProps) => {
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const hasPermissionsToMap = useMemo(() => {
if (isVirtual) {
return true;
}
if (!isQuestion) {
return true;
}
if (!question || !card.dataset_query) {
return false;
}
return question.canRunAdhocQuery();
}, [isVirtual, isQuestion, question, card.dataset_query]);
const { buttonVariant, buttonTooltip, buttonText, buttonIcon } =
useMemo(() => {
if (!hasPermissionsToMap) {
return {
buttonVariant: "unauthed",
buttonTooltip: t`You don’t have permission to see this question’s columns.`,
buttonText: null,
buttonIcon: <KeyIcon name="key" />,
};
}
if (isDisabled && !isVirtual) {
return {
buttonVariant: "disabled",
buttonTooltip: t`This card doesn't have any fields or parameters that can be mapped to this parameter type.`,
buttonText: t`No valid fields`,
buttonIcon: null,
};
}
if (selectedMappingOption) {
return {
buttonVariant: "mapped",
buttonTooltip: null,
buttonText: formatSelected(selectedMappingOption),
buttonIcon: (
<CloseIconButton
role="button"
aria-label={t`Disconnect`}
onClick={e => {
handleChangeTarget(null);
e.stopPropagation();
}}
/>
),
};
}
if (target != null) {
return {
buttonVariant: "invalid",
buttonText: t`Unknown Field`,
buttonIcon: (
<CloseIconButton
onClick={e => {
handleChangeTarget(null);
e.stopPropagation();
}}
/>
),
};
}
return {
buttonVariant: "default",
buttonTooltip: null,
buttonText: t`Select…`,
buttonIcon: <ChevrondownIcon name="chevrondown" />,
};
}, [
hasPermissionsToMap,
isDisabled,
isVirtual,
selectedMappingOption,
target,
handleChangeTarget,
]);
return (
<DeprecatedTooltip tooltip={buttonTooltip}>
<TippyPopover
visible={isDropdownVisible && !isDisabled && hasPermissionsToMap}
onClickOutside={() => setIsDropdownVisible(false)}
placement="bottom-start"
content={
<ParameterTargetList
onChange={(target: ParameterTarget) => {
handleChangeTarget(target);
setIsDropdownVisible(false);
}}
target={target}
mappingOptions={mappingOptions}
/>
}
>
<TargetButton
variant={buttonVariant}
aria-label={buttonTooltip ?? undefined}
aria-haspopup="listbox"
aria-expanded={isDropdownVisible}
aria-disabled={isDisabled || !hasPermissionsToMap}
onClick={() => {
setIsDropdownVisible(true);
}}
onKeyDown={e => {
if (e.key === "Enter") {
setIsDropdownVisible(true);
}
}}
>
{buttonText && (
<TargetButtonText>
<Ellipsified>{buttonText}</Ellipsified>
</TargetButtonText>
)}
{buttonIcon}
</TargetButton>
</TippyPopover>
</DeprecatedTooltip>
);
};
function formatSelected({
name,
sectionName,
}: {
name: string;
sectionName?: string;
}) {
if (sectionName == null) {
// for native question variables or field literals we just display the name
return name;
}
return `${sectionName}.${name}`;
}
import { useState, useMemo, useCallback } from "react";
import { useMount } from "react-use";
import { t } from "ttag";
import _ from "underscore";
import { Ellipsified } from "metabase/core/components/Ellipsified";
import CS from "metabase/css/core/index.css";
import { setParameterMapping } from "metabase/dashboard/actions/parameters";
import {
isVirtualDashCard,
getVirtualCardType,
showVirtualDashCardInfoText,
} from "metabase/dashboard/utils";
import { useDispatch } from "metabase/lib/redux";
import type { ParameterMappingOption } from "metabase/parameters/utils/mapping-options";
import { Flex, Icon, Transition, Tooltip, Box } from "metabase/ui";
import type Question from "metabase-lib/v1/Question";
import { isTemporalUnitParameter } from "metabase-lib/v1/parameters/utils/parameter-type";
import type {
Card,
DashboardCard,
Parameter,
ParameterTarget,
} from "metabase-types/api";
import { Header, TextCardDefault } from "./DashCardCardParameterMapper.styled";
import { DashCardCardParameterMapperButton } from "./DashCardCardParameterMapperButton";
import { DisabledNativeCardHelpText } from "./DisabledNativeCardHelpText";
interface DashCardCardParameterMapperContentProps {
isNative: boolean;
isDisabled: boolean;
isMobile: boolean;
isQuestion: boolean;
shouldShowAutoConnectHint: boolean;
dashcard: DashboardCard;
question: Question | undefined;
editingParameter: Parameter | null | undefined;
mappingOptions: ParameterMappingOption[];
card: Card;
selectedMappingOption: ParameterMappingOption | undefined;
target: ParameterTarget | null | undefined;
layoutHeight: number;
}
export const DashCardCardParameterMapperContent = ({
layoutHeight,
dashcard,
isNative,
isMobile,
isDisabled,
question,
editingParameter,
mappingOptions,
selectedMappingOption,
isQuestion,
card,
target,
shouldShowAutoConnectHint,
}: DashCardCardParameterMapperContentProps) => {
const isVirtual = isVirtualDashCard(dashcard);
const virtualCardType = getVirtualCardType(dashcard);
const isTemporalUnit =
editingParameter != null && isTemporalUnitParameter(editingParameter);
const dispatch = useDispatch();
const headerContent = useMemo(() => {
if (layoutHeight <= 2) {
return null;
}
if (isTemporalUnit) {
return t`Connect to`;
}
if (!isVirtual && !(isNative && isDisabled)) {
return t`Column to filter on`;
}
return t`Variable to map to`;
}, [layoutHeight, isTemporalUnit, isVirtual, isNative, isDisabled]);
const handleChangeTarget = useCallback(
(target: ParameterTarget | null) => {
if (editingParameter) {
dispatch(
setParameterMapping(
editingParameter.id,
dashcard.id,
card.id,
target,
),
);
}
},
[card.id, dashcard.id, dispatch, editingParameter],
);
const mappingInfoText =
(virtualCardType &&
{
heading: t`You can connect widgets to {{variables}} in heading cards.`,
text: t`You can connect widgets to {{variables}} in text cards.`,
link: t`You cannot connect variables to link cards.`,
action: t`Open this card's action settings to connect variables`,
placeholder: "",
}[virtualCardType]) ??
"";
if (isVirtual && isDisabled) {
return showVirtualDashCardInfoText(dashcard, isMobile) ? (
<TextCardDefault>
<Icon name="info" size={12} className={CS.pr1} />
{mappingInfoText}
</TextCardDefault>
) : (
<TextCardDefault aria-label={mappingInfoText}>
<Icon
name="info"
size={16}
className={CS.textDarkHover}
tooltip={mappingInfoText}
/>
</TextCardDefault>
);
}
if (isNative && isDisabled && question && editingParameter) {
return (
<DisabledNativeCardHelpText
question={question}
parameter={editingParameter}
/>
);
}
const shouldShowAutoConnectIcon =
shouldShowAutoConnectHint && layoutHeight <= 3 && dashcard.size_x > 4;
return (
<>
{headerContent && (
<Header>
<Ellipsified>{headerContent}</Ellipsified>
</Header>
)}
<Flex align="center" justify="center" gap="xs" pos="relative">
<DashCardCardParameterMapperButton
handleChangeTarget={handleChangeTarget}
isVirtual={isVirtual}
isQuestion={isQuestion}
isDisabled={isDisabled}
selectedMappingOption={selectedMappingOption}
question={question}
card={card}
target={target}
mappingOptions={mappingOptions}
/>
{shouldShowAutoConnectIcon && <AutoConnectedAnimatedIcon />}
</Flex>
</>
);
};
function AutoConnectedAnimatedIcon() {
const [mounted, setMounted] = useState(false);
useMount(() => {
setMounted(true);
});
return (
<Transition transition="fade" mounted={mounted} exitDuration={0}>
{styles => {
return (
<Box component="span" style={styles} pos="absolute" right={-20}>
<Tooltip label={t`Auto-connected`}>
<Icon name="sparkles" />
</Tooltip>
</Box>
);
}}
</Transition>
);
}
import { useEffect } from "react";
import { usePrevious } from "react-use";
import { resetParameterMapping } from "metabase/dashboard/actions/parameters";
import { useDispatch } from "metabase/lib/redux";
import { getParameterSubType } from "metabase-lib/v1/parameters/utils/parameter-type";
import type { DashCardId, Parameter } from "metabase-types/api";
export function useResetParameterMapping({
editingParameter,
isNative,
isDisabled,
dashcardId,
}: {
editingParameter: Parameter | null | undefined;
isNative: boolean;
isDisabled: boolean;
dashcardId: DashCardId;
}) {
const prevParameter = usePrevious(editingParameter);
const dispatch = useDispatch();
useEffect(() => {
if (!prevParameter || !editingParameter) {
return;
}
if (
isNative &&
isDisabled &&
prevParameter.type !== editingParameter.type
) {
const subType = getParameterSubType(editingParameter);
const prevSubType = getParameterSubType(prevParameter);
if (prevSubType === "=" && subType !== "=") {
dispatch(resetParameterMapping(editingParameter.id, dashcardId));
}
}
}, [
isNative,
isDisabled,
prevParameter,
editingParameter,
dispatch,
dashcardId,
]);
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment