Skip to content
Snippets Groups Projects
Unverified Commit b3a868c0 authored by Anton Kulyk's avatar Anton Kulyk Committed by GitHub
Browse files

Refactor `ClickBehaviorSidebar` components (#25089)

* Untangle `ClickBehaviorSidebarHeader`

* Untangle `ClickBehaviorSidebarMainView`

* Untangle `Column`

* Move `LinkOptions` to its own directory

* Untangle `LinkOptions`

* Move `TableClickBehaviorView` to its own directory

* Untangle `TableClickBehaviorView`

* Untangle `QuestionDashboardPicker`

* Extract `SidebarItem` component

* Remove `SidebarItemClasses` and `SidebarItemStyle`

* Fix `QuestionDashboardPicker` sidebar item component

* Move components into their own folders

* Extract more code into `SidebarItem`

* Extract styled components

* Untangle main content rendering

* Remove redundant div

* Fix padding

* Clean up `ClickBehaviorSidebar` props

* Fix missing prop

* Fix incorrect `disabled` condition

* Convert sidebar into functional component

* Fix `hasSelectedColumn` used before define
parent f2d0c6c2
No related merge requests found
Showing
with 826 additions and 517 deletions
/* eslint-disable react/prop-types */
import React from "react";
import React, { useCallback } from "react";
import { t } from "ttag";
import _ from "underscore";
import Icon from "metabase/components/Icon";
import { color } from "metabase/lib/colors";
import Actions from "metabase/entities/actions";
import ClickMappings from "metabase/dashboard/components/ClickMappings";
import { SidebarItemWrapper, SidebarItemStyle } from "./SidebarItem";
import { SidebarItem } from "../SidebarItem";
import { Heading, SidebarContent } from "../ClickBehaviorSidebar.styled";
import {
Heading,
SidebarContent,
SidebarIconWrapper,
} from "./ClickBehaviorSidebar.styled";
ActionSidebarItem,
ActionSidebarItemIcon,
ActionDescription,
} from "./ActionOptions.styled";
const ActionOption = ({ name, description, isSelected, onClick }) => {
return (
<SidebarItemWrapper
<ActionSidebarItem
onClick={onClick}
style={{
...SidebarItemStyle,
backgroundColor: isSelected ? color("brand") : "transparent",
color: isSelected ? color("white") : "inherit",
alignItems: description ? "flex-start" : "center",
marginTop: "2px",
}}
isSelected={isSelected}
hasDescription={!!description}
>
<SidebarIconWrapper>
<Icon
name="bolt"
color={isSelected ? color("text-white") : color("brand")}
/>
</SidebarIconWrapper>
<ActionSidebarItemIcon name="bolt" isSelected={isSelected} />
<div>
<h4>{name}</h4>
{description && (
<span
className={isSelected ? "text-white" : "text-medium"}
style={{ width: "95%", marginTop: "2px" }}
>
{description}
</span>
)}
<SidebarItem.Name>{name}</SidebarItem.Name>
{description && <ActionDescription>{description}</ActionDescription>}
</div>
</SidebarItemWrapper>
</ActionSidebarItem>
);
};
function ActionOptions({ dashcard, clickBehavior, updateSettings }) {
const handleActionSelected = useCallback(
action => {
updateSettings({
type: clickBehavior.type,
emitter_id: clickBehavior.emitter_id,
action: action.id,
});
},
[clickBehavior, updateSettings],
);
return (
<SidebarContent>
<Heading className="text-medium">{t`Pick an action`}</Heading>
......@@ -68,13 +58,7 @@ function ActionOptions({ dashcard, clickBehavior, updateSettings }) {
name={action.name}
description={action.description}
isSelected={clickBehavior.action === action.id}
onClick={() =>
updateSettings({
type: clickBehavior.type,
action: action.id,
emitter_id: clickBehavior.emitter_id,
})
}
onClick={() => handleActionSelected(action)}
/>
))}
{selectedAction && (
......
import _ from "underscore";
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
import { SidebarItem } from "../SidebarItem";
export const ActionSidebarItem = styled(SidebarItem.Selectable)<{
hasDescription?: boolean;
}>`
align-items: ${props => (props.hasDescription ? "flex-start" : "center")};
margin-top: 2px;
`;
export const ActionSidebarItemIcon = styled(SidebarItem.Icon)<{
isSelected?: boolean;
}>`
.Icon {
color: ${props =>
props.isSelected ? color("text-white") : color("brand")};
}
`;
export const ActionDescription = styled.span<{ isSelected?: boolean }>`
width: 95%;
margin-top: 2px;
color: ${props =>
props.isSelected ? color("text-white") : color("text-medium")};
`;
export { default } from "./ActionOptions";
/* eslint-disable react/prop-types */
import React from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { getIn } from "icepick";
import {
......@@ -8,6 +8,9 @@ import {
} from "metabase/lib/click-behavior";
import { keyForColumn } from "metabase/lib/dataset";
import { useOnMount } from "metabase/hooks/use-on-mount";
import { usePrevious } from "metabase/hooks/use-previous";
import Sidebar from "metabase/dashboard/components/Sidebar";
import ClickBehaviorSidebarHeader from "./ClickBehaviorSidebarHeader";
......@@ -16,188 +19,208 @@ import TableClickBehaviorView from "./TableClickBehaviorView";
import TypeSelector from "./TypeSelector";
import { SidebarContent } from "./ClickBehaviorSidebar.styled";
class ClickBehaviorSidebar extends React.Component {
state = {
showTypeSelector: null,
selectedColumn: null,
originalVizSettings: null,
originalColumnVizSettings: null,
};
componentDidUpdate(prevProps, prevState) {
if (this.props.dashcard.id !== prevProps.dashcard.id) {
this.setState({
originalVizSettings: this.props.dashcard.visualization_settings,
});
}
if (
this.props.dashcard.id !== prevProps.dashcard.id &&
this.state.selectedColumn != null
) {
this.unsetSelectedColumn();
function getClickBehaviorForColumn(dashcard, column) {
return getIn(dashcard, [
"visualization_settings",
"column_settings",
keyForColumn(column),
"click_behavior",
]);
}
function shouldShowTypeSelector(clickBehavior) {
return !clickBehavior || clickBehavior.type == null;
}
function ClickBehaviorSidebar({
dashboard,
dashcard,
dashcardData,
parameters,
hideClickBehaviorSidebar,
onUpdateDashCardColumnSettings,
onUpdateDashCardVisualizationSettings,
onReplaceAllDashCardVisualizationSettings,
}) {
const [isTypeSelectorVisible, setTypeSelectorVisible] = useState(null);
const [selectedColumn, setSelectedColumn] = useState(null);
const [originalVizSettings, setOriginalVizSettings] = useState(null);
const [originalColumnVizSettings, setOriginalColumnVizSettings] =
useState(null);
const previousDashcard = usePrevious(dashcard);
const hasSelectedColumn = selectedColumn != null;
const clickBehavior = useMemo(() => {
if (isTableDisplay(dashcard) && !hasSelectedColumn) {
return;
}
if (
this.props.dashcard.id !== prevProps.dashcard.id ||
this.state.selectedColumn !== prevState.selectedColumn
) {
this.showTypeSelectorIfNeeded();
if (hasSelectedColumn) {
return getClickBehaviorForColumn(dashcard, selectedColumn);
} else {
const curr = this.getClickBehavior() || {};
const prev = this.getClickBehavior(prevProps) || {};
if (curr.type !== prev.type && curr.type != null) {
// move to next screen if the type was just changed
this.setState({ showTypeSelector: false });
}
}
}
componentDidMount() {
this.showTypeSelectorIfNeeded();
if (this.props.dashcard) {
this.setState({
originalVizSettings: this.props.dashcard.visualization_settings,
});
return getIn(dashcard, ["visualization_settings", "click_behavior"]);
}
}
}, [dashcard, selectedColumn, hasSelectedColumn]);
const isValidClickBehavior = useMemo(
() => clickBehaviorIsValid(clickBehavior),
[clickBehavior],
);
const handleChangeSettings = useCallback(
nextClickBehavior => {
const { id } = dashcard;
if (selectedColumn == null) {
onUpdateDashCardVisualizationSettings(id, {
click_behavior: nextClickBehavior,
});
} else {
onUpdateDashCardColumnSettings(id, keyForColumn(selectedColumn), {
click_behavior: nextClickBehavior,
});
}
setSelectedColumn = selectedColumn => {
const originalColumnVizSettings = this.getClickBehaviorForColumn(
this.props,
const changedType = nextClickBehavior.type !== clickBehavior?.type;
if (changedType) {
// move to next screen
setTypeSelectorVisible(false);
}
},
[
dashcard,
clickBehavior,
selectedColumn,
);
this.setState({ selectedColumn, originalColumnVizSettings });
};
unsetSelectedColumn = () => {
if (!clickBehaviorIsValid(this.getClickBehavior())) {
this.updateSettings(this.state.originalColumnVizSettings);
onUpdateDashCardColumnSettings,
onUpdateDashCardVisualizationSettings,
],
);
const handleColumnSelected = useCallback(
column => {
const originalColumnVizSettings = getClickBehaviorForColumn(
dashcard,
column,
);
setSelectedColumn(column);
setOriginalColumnVizSettings(originalColumnVizSettings);
},
[dashcard],
);
const handleUnsetSelectedColumn = useCallback(() => {
if (!isValidClickBehavior) {
handleChangeSettings(originalColumnVizSettings);
}
this.setState({ originalColumnVizSettings: null, selectedColumn: null });
};
getClickBehavior(props = this.props) {
const { dashcard } = props;
const { selectedColumn } = this.state;
if (isTableDisplay(dashcard) && selectedColumn == null) {
return undefined;
setOriginalColumnVizSettings(null);
setSelectedColumn(null);
}, [isValidClickBehavior, originalColumnVizSettings, handleChangeSettings]);
const handleCancel = useCallback(() => {
onReplaceAllDashCardVisualizationSettings(dashcard.id, originalVizSettings);
hideClickBehaviorSidebar();
}, [
dashcard,
originalVizSettings,
hideClickBehaviorSidebar,
onReplaceAllDashCardVisualizationSettings,
]);
useOnMount(() => {
if (shouldShowTypeSelector(clickBehavior)) {
setTypeSelectorVisible(true);
}
if (selectedColumn == null) {
return getIn(dashcard, ["visualization_settings", "click_behavior"]);
} else {
return this.getClickBehaviorForColumn(props, selectedColumn);
}
}
getClickBehaviorForColumn(props, column) {
return getIn(props.dashcard, [
"visualization_settings",
"column_settings",
keyForColumn(column),
"click_behavior",
]);
}
getColumns() {
const { dashcard, dashcardData } = this.props;
return getIn(dashcardData, [dashcard.card_id, "data", "cols"]);
}
showTypeSelectorIfNeeded() {
const { type } = this.getClickBehavior() || {};
this.setState({ showTypeSelector: type == null });
}
updateSettings = (
click_behavior,
{ props = this.props, state = this.state } = {},
) => {
const { selectedColumn } = state;
const { id } = props.dashcard;
if (selectedColumn == null) {
props.onUpdateDashCardVisualizationSettings(id, { click_behavior });
} else {
props.onUpdateDashCardColumnSettings(id, keyForColumn(selectedColumn), {
click_behavior,
});
if (dashcard) {
setOriginalVizSettings(dashcard.visualization_settings);
}
};
});
handleCancel = () => {
this.props.onReplaceAllDashCardVisualizationSettings(
this.props.dashcard.id,
this.state.originalVizSettings,
);
this.props.hideClickBehaviorSidebar();
};
render() {
const { dashboard, dashcard, parameters, hideClickBehaviorSidebar } =
this.props;
const { selectedColumn } = this.state;
const clickBehavior = this.getClickBehavior() || { type: "menu" };
useEffect(() => {
if (!previousDashcard) {
return;
}
if (isTableDisplay(dashcard) && selectedColumn == null) {
if (dashcard.id !== previousDashcard.id) {
setOriginalVizSettings(dashcard.visualization_settings);
if (hasSelectedColumn) {
handleUnsetSelectedColumn();
}
}
}, [
dashcard,
previousDashcard,
hasSelectedColumn,
handleUnsetSelectedColumn,
]);
const renderContent = useCallback(() => {
const finalClickBehavior = clickBehavior || { type: "menu" };
if (isTableDisplay(dashcard) && !hasSelectedColumn) {
const columns = getIn(dashcardData, [dashcard.card_id, "data", "cols"]);
return (
<TableClickBehaviorView
columns={this.getColumns()}
columns={columns}
dashcard={dashcard}
getClickBehaviorForColumn={column =>
this.getClickBehaviorForColumn(this.props, column)
getClickBehaviorForColumn(dashcard, column)
}
canClose={clickBehaviorIsValid(clickBehavior)}
onColumnClick={this.setSelectedColumn}
onCancel={this.handleCancel}
onClose={hideClickBehaviorSidebar}
onColumnClick={handleColumnSelected}
/>
);
}
const { showTypeSelector } = this.state;
if (showTypeSelector === null) {
return null;
if (isTypeSelectorVisible) {
return (
<SidebarContent>
<TypeSelector
clickBehavior={finalClickBehavior}
dashcard={dashcard}
parameters={parameters}
updateSettings={handleChangeSettings}
moveToNextPage={() => setTypeSelectorVisible(false)}
/>
</SidebarContent>
);
}
return (
<Sidebar
onClose={hideClickBehaviorSidebar}
onCancel={this.handleCancel}
closeIsDisabled={!clickBehaviorIsValid(clickBehavior)}
>
<ClickBehaviorSidebarHeader
dashcard={dashcard}
selectedColumn={selectedColumn}
hasSelectedColumn={selectedColumn != null}
onUnsetColumn={this.unsetSelectedColumn}
/>
<div>
{showTypeSelector ? (
<SidebarContent>
<TypeSelector
clickBehavior={clickBehavior}
dashcard={dashcard}
parameters={this.props.parameters}
updateSettings={this.updateSettings}
moveToNextPage={() =>
this.setState({ showTypeSelector: false })
}
/>
</SidebarContent>
) : (
<ClickBehaviorSidebarMainView
clickBehavior={clickBehavior}
dashboard={dashboard}
dashcard={dashcard}
parameters={parameters}
handleShowTypeSelector={() =>
this.setState({ showTypeSelector: true })
}
updateSettings={this.updateSettings}
/>
)}
</div>
</Sidebar>
<ClickBehaviorSidebarMainView
clickBehavior={finalClickBehavior}
dashboard={dashboard}
dashcard={dashcard}
parameters={parameters}
handleShowTypeSelector={() => setTypeSelectorVisible(true)}
updateSettings={handleChangeSettings}
/>
);
}
}, [
dashboard,
dashcard,
dashcardData,
clickBehavior,
parameters,
hasSelectedColumn,
isTypeSelectorVisible,
handleChangeSettings,
handleColumnSelected,
]);
return (
<Sidebar
onClose={hideClickBehaviorSidebar}
onCancel={handleCancel}
closeIsDisabled={!isValidClickBehavior}
>
<ClickBehaviorSidebarHeader
dashcard={dashcard}
selectedColumn={selectedColumn}
hasSelectedColumn={hasSelectedColumn}
onUnsetColumn={handleUnsetSelectedColumn}
/>
<div>{renderContent()}</div>
</Sidebar>
);
}
export default ClickBehaviorSidebar;
import styled from "@emotion/styled";
import { color, darken } from "metabase/lib/colors";
import { color } from "metabase/lib/colors";
export const SidebarItem = styled.div`
display: flex;
align-items: center;
width: 100%;
`;
export const CloseIconContainer = styled.span`
margin-left: auto;
padding: 1rem;
border-left: 1px solid ${darken("brand", 0.2)};
`;
import { SidebarItem } from "./SidebarItem";
export const Heading = styled.h4`
color: ${color("text-dark")};
......@@ -26,7 +16,7 @@ export const SidebarContent = styled.div`
`;
export const SidebarContentBordered = styled(SidebarContent)`
padding-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid ${color("border")};
`;
......@@ -37,16 +27,7 @@ export const SidebarHeader = styled.div`
margin-bottom: 16px;
`;
export const SidebarIconWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
width: 36px;
height: 36px;
margin-right: 10px;
border: 1px solid #f2f2f2;
border-radius: 8px;
export const SelectedClickBehaviorItemIcon = styled(SidebarItem.Icon)`
border-color: transparent;
padding-left: 12px;
`;
/* eslint-disable react/prop-types */
import React from "react";
import { jt } from "ttag";
import Icon from "metabase/components/Icon";
import { Heading, SidebarHeader } from "./ClickBehaviorSidebar.styled";
function ClickBehaviorSidebarHeader({
dashcard,
selectedColumn,
hasSelectedColumn,
onUnsetColumn,
}) {
return (
<SidebarHeader>
{!hasSelectedColumn ? (
<Heading>{jt`Click behavior for ${(
<span className="text-brand">{dashcard.card.name}</span>
)}`}</Heading>
) : (
<div
onClick={onUnsetColumn}
className="flex align-center text-brand-hover cursor-pointer"
>
<div
className="bordered"
style={{
marginRight: 8,
paddingTop: 4,
paddingBottom: 4,
paddingRight: 6,
paddingLeft: 6,
borderRadius: 4,
}}
>
<Icon name="chevronleft" className="text-medium" size={12} />
</div>
<Heading>
{jt`Click behavior for ${(
<span className="text-brand">{selectedColumn.display_name}</span>
)}`}
</Heading>
</div>
)}
</SidebarHeader>
);
}
export default ClickBehaviorSidebarHeader;
/* eslint-disable react/prop-types */
import React, { useCallback } from "react";
import { t, jt } from "ttag";
import Icon from "metabase/components/Icon";
import { isTableDisplay } from "metabase/lib/click-behavior";
import { Heading, SidebarHeader } from "../ClickBehaviorSidebar.styled";
import {
ColumnClickBehaviorHeader,
ChevronIconContainer,
ItemName,
} from "./ClickBehaviorSidebarHeader.styled";
function DefaultHeader({ children }) {
return (
<Heading>{jt`Click behavior for ${(
<ItemName>{children}</ItemName>
)}`}</Heading>
);
}
function ClickBehaviorSidebarHeader({
dashcard,
selectedColumn,
hasSelectedColumn,
onUnsetColumn,
}) {
const renderContent = useCallback(() => {
if (isTableDisplay(dashcard)) {
if (hasSelectedColumn) {
return (
<ColumnClickBehaviorHeader onClick={onUnsetColumn}>
<ChevronIconContainer>
<Icon name="chevronleft" size={12} />
</ChevronIconContainer>
<DefaultHeader>{selectedColumn.display_name}</DefaultHeader>
</ColumnClickBehaviorHeader>
);
}
return <Heading>{t`On-click behavior for each column`}</Heading>;
}
return <DefaultHeader>{dashcard.card.name}</DefaultHeader>;
}, [dashcard, selectedColumn, hasSelectedColumn, onUnsetColumn]);
return <SidebarHeader>{renderContent()}</SidebarHeader>;
}
export default ClickBehaviorSidebarHeader;
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
import Icon from "metabase/components/Icon";
export const ItemName = styled.span`
color: ${color("brand")};
`;
export const ColumnClickBehaviorHeader = styled.div`
display: flex;
align-items: center;
cursor: pointer;
&:hover {
color: ${color("brand")};
}
`;
export const ChevronIconContainer = styled.div`
padding: 4px 6px;
margin-right: 8px;
border: 1px solid ${color("border")};
border-radius: 4px;
${Icon.Root} {
color: ${color("text-medium")};
}
`;
export { default } from "./ClickBehaviorSidebarHeader";
/* eslint-disable react/prop-types */
import React from "react";
import Icon from "metabase/components/Icon";
import { color } from "metabase/lib/colors";
import { clickBehaviorOptions, getClickBehaviorOptionName } from "./utils";
import ActionOptions from "./ActionOptions";
import CrossfilterOptions from "./CrossfilterOptions";
import LinkOptions from "./LinkOptions";
import { SidebarItemWrapper } from "./SidebarItem";
import {
CloseIconContainer,
SidebarContentBordered,
SidebarIconWrapper,
} from "./ClickBehaviorSidebar.styled";
function ClickBehaviorSidebarMainView({
clickBehavior,
dashboard,
dashcard,
parameters,
handleShowTypeSelector,
updateSettings,
}) {
return (
<div>
<SidebarContentBordered>
<SidebarItemWrapper
onClick={handleShowTypeSelector}
style={{
backgroundColor: color("brand"),
color: color("white"),
}}
>
<SidebarIconWrapper
style={{ borderColor: "transparent", paddingLeft: 12 }}
>
<Icon
name={
clickBehaviorOptions.find(o => o.value === clickBehavior.type)
.icon
}
/>
</SidebarIconWrapper>
<div className="flex align-center full">
<h4>{getClickBehaviorOptionName(clickBehavior.type, dashcard)}</h4>
<CloseIconContainer>
<Icon name="close" size={12} />
</CloseIconContainer>
</div>
</SidebarItemWrapper>
</SidebarContentBordered>
{clickBehavior.type === "link" ? (
<LinkOptions
clickBehavior={clickBehavior}
dashcard={dashcard}
parameters={parameters}
updateSettings={updateSettings}
/>
) : clickBehavior.type === "crossfilter" ? (
<CrossfilterOptions
clickBehavior={clickBehavior}
dashboard={dashboard}
dashcard={dashcard}
updateSettings={updateSettings}
/>
) : clickBehavior.type === "action" ? (
<ActionOptions
clickBehavior={clickBehavior}
dashcard={dashcard}
parameters={parameters}
updateSettings={updateSettings}
/>
) : null}
</div>
);
}
export default ClickBehaviorSidebarMainView;
/* eslint-disable react/prop-types */
import React from "react";
import { clickBehaviorOptions, getClickBehaviorOptionName } from "../utils";
import ActionOptions from "../ActionOptions";
import CrossfilterOptions from "../CrossfilterOptions";
import LinkOptions from "../LinkOptions";
import { SidebarItem } from "../SidebarItem";
import {
SidebarContentBordered,
SelectedClickBehaviorItemIcon,
} from "../ClickBehaviorSidebar.styled";
function ClickBehaviorOptions({
clickBehavior,
dashboard,
dashcard,
parameters,
updateSettings,
}) {
if (clickBehavior.type === "link") {
return (
<LinkOptions
clickBehavior={clickBehavior}
dashcard={dashcard}
parameters={parameters}
updateSettings={updateSettings}
/>
);
}
if (clickBehavior.type === "crossfilter") {
return (
<CrossfilterOptions
clickBehavior={clickBehavior}
dashboard={dashboard}
dashcard={dashcard}
updateSettings={updateSettings}
/>
);
}
if (clickBehavior.type === "action") {
return (
<ActionOptions
clickBehavior={clickBehavior}
dashcard={dashcard}
parameters={parameters}
updateSettings={updateSettings}
/>
);
}
return null;
}
function ClickBehaviorSidebarMainView({
clickBehavior,
dashboard,
dashcard,
parameters,
handleShowTypeSelector,
updateSettings,
}) {
const clickBehaviorOptionName = getClickBehaviorOptionName(
clickBehavior.type,
dashcard,
);
const { icon: clickBehaviorIcon } = clickBehaviorOptions.find(
o => o.value === clickBehavior.type,
);
return (
<>
<SidebarContentBordered>
<SidebarItem.Selectable
onClick={handleShowTypeSelector}
isSelected
padded={false}
>
<SelectedClickBehaviorItemIcon name={clickBehaviorIcon} />
<SidebarItem.Content>
<SidebarItem.Name>{clickBehaviorOptionName}</SidebarItem.Name>
<SidebarItem.CloseIcon />
</SidebarItem.Content>
</SidebarItem.Selectable>
</SidebarContentBordered>
<ClickBehaviorOptions
clickBehavior={clickBehavior}
dashboard={dashboard}
dashcard={dashcard}
parameters={parameters}
updateSettings={updateSettings}
/>
</>
);
}
export default ClickBehaviorSidebarMainView;
export { default } from "./ClickBehaviorSidebarMainView";
/* eslint-disable react/prop-types */
import React from "react";
import { t } from "ttag";
import _ from "underscore";
import Button from "metabase/core/components/Button";
import Icon from "metabase/components/Icon";
import InputBlurChange from "metabase/components/InputBlurChange";
import ModalContent from "metabase/components/ModalContent";
import ModalWithTrigger from "metabase/components/ModalWithTrigger";
import { color } from "metabase/lib/colors";
import {
isTableDisplay,
clickBehaviorIsValid,
} from "metabase/lib/click-behavior";
import CustomLinkText from "./CustomLinkText";
import LinkOption from "./LinkOption";
import ValuesYouCanReference from "./ValuesYouCanReference";
import QuestionDashboardPicker from "./QuestionDashboardPicker";
import { SidebarItemWrapper } from "./SidebarItem";
import {
CloseIconContainer,
SidebarContent,
SidebarIconWrapper,
} from "./ClickBehaviorSidebar.styled";
function LinkOptions({ clickBehavior, updateSettings, dashcard, parameters }) {
const linkTypeOptions = [
{ type: "dashboard", icon: "dashboard", name: t`Dashboard` },
{ type: "question", icon: "bar", name: t`Saved question` },
{ type: "url", icon: "link", name: t`URL` },
];
return (
<SidebarContent>
<p className="text-medium mt3 mb1">{t`Link to`}</p>
<div>
{clickBehavior.linkType == null ? (
linkTypeOptions.map(({ type, icon, name }, index) => (
<LinkOption
key={name}
option={name}
icon={icon}
onClick={() =>
updateSettings({ type: clickBehavior.type, linkType: type })
}
/>
))
) : clickBehavior.linkType === "url" ? (
<ModalWithTrigger
isInitiallyOpen={clickBehavior.linkTemplate == null}
triggerElement={
<SidebarItemWrapper
style={{
backgroundColor: color("brand"),
color: color("white"),
}}
>
<SidebarIconWrapper
style={{ borderColor: "transparent", marginLeft: 8 }}
>
<Icon name="link" />
</SidebarIconWrapper>
<div className="flex align-center full">
<h4 className="pr1">
{clickBehavior.linkTemplate
? clickBehavior.linkTemplate
: t`URL`}
</h4>
<CloseIconContainer
onClick={() =>
updateSettings({
type: clickBehavior.type,
linkType: null,
})
}
>
<Icon name="close" size={12} />
</CloseIconContainer>
</div>
</SidebarItemWrapper>
}
>
{({ onClose }) => (
<ModalContent
title={t`Enter a URL to link to`}
onClose={clickBehavior.targetId != null ? onClose : null}
>
<div className="mb1">{t`You can insert the value of a column or dashboard filter using its name, like this: {{some_column}}`}</div>
<InputBlurChange
autoFocus
className="input block full"
placeholder={t`e.g. http://acme.com/id/\{\{user_id\}\}`}
value={clickBehavior.linkTemplate}
onChange={e =>
updateSettings({
...clickBehavior,
linkTemplate: e.target.value,
})
}
/>
{isTableDisplay(dashcard) && (
<CustomLinkText
updateSettings={updateSettings}
clickBehavior={clickBehavior}
/>
)}
<ValuesYouCanReference
dashcard={dashcard}
parameters={parameters}
/>
<div className="flex">
<Button
primary
onClick={() => onClose()}
className="ml-auto mt2"
disabled={!clickBehaviorIsValid(clickBehavior)}
>{t`Done`}</Button>
</div>
</ModalContent>
)}
</ModalWithTrigger>
) : (
<div></div>
)}
</div>
<div className="mt1">
{clickBehavior.linkType != null && clickBehavior.linkType !== "url" && (
<div>
<QuestionDashboardPicker
dashcard={dashcard}
clickBehavior={clickBehavior}
updateSettings={updateSettings}
/>
{isTableDisplay(dashcard) && (
<div>
<CustomLinkText
updateSettings={updateSettings}
clickBehavior={clickBehavior}
/>
<ValuesYouCanReference
dashcard={dashcard}
parameters={parameters}
/>
</div>
)}
</div>
)}
</div>
</SidebarContent>
);
}
export default LinkOptions;
......@@ -5,7 +5,7 @@ import _ from "underscore";
import InputBlurChange from "metabase/components/InputBlurChange";
import { Heading } from "./ClickBehaviorSidebar.styled";
import { Heading } from "../ClickBehaviorSidebar.styled";
const CustomLinkText = ({ clickBehavior, updateSettings }) => {
return (
......
/* eslint-disable react/prop-types */
import React, { useCallback } from "react";
import { t } from "ttag";
import _ from "underscore";
import InputBlurChange from "metabase/components/InputBlurChange";
import ModalContent from "metabase/components/ModalContent";
import ModalWithTrigger from "metabase/components/ModalWithTrigger";
import {
isTableDisplay,
clickBehaviorIsValid,
} from "metabase/lib/click-behavior";
import CustomLinkText from "./CustomLinkText";
import { SidebarItem } from "../SidebarItem";
import ValuesYouCanReference from "./ValuesYouCanReference";
import { CustomURLPickerIcon, CustomURLPickerName } from "./LinkOptions.styled";
import { FormDescription, DoneButton } from "./CustomURLPicker.styled";
function CustomURLPicker({
clickBehavior,
updateSettings,
dashcard,
parameters,
}) {
const hasLinkTemplate = clickBehavior.linkTemplate != null;
const canSelect = clickBehaviorIsValid(clickBehavior);
const handleLinkTemplateChange = useCallback(
e => {
updateSettings({
...clickBehavior,
linkTemplate: e.target.value,
});
},
[clickBehavior, updateSettings],
);
const handleReset = useCallback(() => {
updateSettings({
type: clickBehavior.type,
linkType: null,
});
}, [clickBehavior, updateSettings]);
return (
<ModalWithTrigger
isInitiallyOpen={!hasLinkTemplate}
triggerElement={
<SidebarItem.Selectable isSelected padded={false}>
<CustomURLPickerIcon name="link" />
<SidebarItem.Content>
<CustomURLPickerName>
{hasLinkTemplate ? clickBehavior.linkTemplate : t`URL`}
</CustomURLPickerName>
<SidebarItem.CloseIcon onClick={handleReset} />
</SidebarItem.Content>
</SidebarItem.Selectable>
}
>
{({ onClose }) => (
<ModalContent
title={t`Enter a URL to link to`}
onClose={hasLinkTemplate ? onClose : null}
>
<FormDescription>
{t`You can insert the value of a column or dashboard filter using its name, like this: {{some_column}}`}
</FormDescription>
<InputBlurChange
autoFocus
value={clickBehavior.linkTemplate}
placeholder={t`e.g. http://acme.com/id/\{\{user_id\}\}`}
onChange={handleLinkTemplateChange}
className="input block full"
/>
{isTableDisplay(dashcard) && (
<CustomLinkText
updateSettings={updateSettings}
clickBehavior={clickBehavior}
/>
)}
<ValuesYouCanReference dashcard={dashcard} parameters={parameters} />
<DoneButton
primary
onClick={onClose}
disabled={!canSelect}
>{t`Done`}</DoneButton>
</ModalContent>
)}
</ModalWithTrigger>
);
}
export default CustomURLPicker;
import styled from "@emotion/styled";
import Button from "metabase/core/components/Button";
export const FormDescription = styled.span`
margin-bottom: 1rem;
`;
export const DoneButton = styled(Button)`
margin-left: auto;
margin-top: 2rem;
`;
......@@ -2,22 +2,17 @@
import React from "react";
import _ from "underscore";
import Icon from "metabase/components/Icon";
import { color } from "metabase/lib/colors";
import { SidebarItemWrapper, SidebarItemStyle } from "./SidebarItem";
import { SidebarIconWrapper } from "./ClickBehaviorSidebar.styled";
import { SidebarItem } from "../SidebarItem";
const LinkOption = ({ option, icon, onClick }) => (
<SidebarItemWrapper onClick={onClick} style={{ ...SidebarItemStyle }}>
<SidebarIconWrapper>
<Icon name={icon} color={color("brand")} />
</SidebarIconWrapper>
<SidebarItem onClick={onClick}>
<SidebarItem.Icon name={icon} color={color("brand")} />
<div>
<h4>{option}</h4>
<SidebarItem.Name>{option}</SidebarItem.Name>
</div>
</SidebarItemWrapper>
</SidebarItem>
);
export default LinkOption;
/* eslint-disable react/prop-types */
import React from "react";
import { t } from "ttag";
import _ from "underscore";
import { isTableDisplay } from "metabase/lib/click-behavior";
import CustomLinkText from "./CustomLinkText";
import QuestionDashboardPicker from "./QuestionDashboardPicker";
import { SidebarContent } from "../ClickBehaviorSidebar.styled";
import CustomURLPicker from "./CustomURLPicker";
import LinkOption from "./LinkOption";
import ValuesYouCanReference from "./ValuesYouCanReference";
function LinkTypeOptions({ onSelect }) {
const linkTypeOptions = [
{ type: "dashboard", icon: "dashboard", name: t`Dashboard` },
{ type: "question", icon: "bar", name: t`Saved question` },
{ type: "url", icon: "link", name: t`URL` },
];
return (
<>
{linkTypeOptions.map(({ type, icon, name }) => (
<LinkOption
key={name}
option={name}
icon={icon}
onClick={() => onSelect(type)}
/>
))}
</>
);
}
function LinkOptions({ clickBehavior, updateSettings, dashcard, parameters }) {
const hasSelectedLinkType = clickBehavior.linkType != null;
const handleSelectLinkType = type =>
updateSettings({ type: clickBehavior.type, linkType: type });
return (
<SidebarContent>
<p className="text-medium mt3 mb1">{t`Link to`}</p>
<div>
{!hasSelectedLinkType ? (
<LinkTypeOptions onSelect={handleSelectLinkType} />
) : clickBehavior.linkType === "url" ? (
<CustomURLPicker
clickBehavior={clickBehavior}
updateSettings={updateSettings}
dashcard={dashcard}
parameters={parameters}
/>
) : (
<div></div>
)}
</div>
<div className="mt1">
{hasSelectedLinkType && clickBehavior.linkType !== "url" && (
<div>
<QuestionDashboardPicker
dashcard={dashcard}
clickBehavior={clickBehavior}
updateSettings={updateSettings}
/>
{isTableDisplay(dashcard) && (
<div>
<CustomLinkText
updateSettings={updateSettings}
clickBehavior={clickBehavior}
/>
<ValuesYouCanReference
dashcard={dashcard}
parameters={parameters}
/>
</div>
)}
</div>
)}
</div>
</SidebarContent>
);
}
export default LinkOptions;
import styled from "@emotion/styled";
import { SidebarItem } from "../SidebarItem";
import { sidebarItemPaddingStyle } from "../SidebarItem/SidebarItem.styled";
export const LinkTargetEntityPickerContent = styled.div`
display: flex;
align-items: center;
width: 100%;
${sidebarItemPaddingStyle};
`;
export const CustomURLPickerIcon = styled(SidebarItem.Icon)`
border-color: transparent;
margin-left: 8px;
`;
export const CustomURLPickerName = styled(SidebarItem.Name)`
padding-right: 1rem;
`;
export const SelectedEntityPickerIcon = styled(SidebarItem.Icon)`
border-color: transparent;
`;
export const SelectedEntityPickerContent = styled(SidebarItem.Content)`
font-weight: bold;
`;
/* eslint-disable react/prop-types */
import React, { useCallback } from "react";
import { t } from "ttag";
import _ from "underscore";
import Icon from "metabase/components/Icon";
import ModalContent from "metabase/components/ModalContent";
import ModalWithTrigger from "metabase/components/ModalWithTrigger";
import Dashboards from "metabase/entities/dashboards";
import Questions from "metabase/entities/questions";
import DashboardPicker from "metabase/containers/DashboardPicker";
import QuestionPicker from "metabase/containers/QuestionPicker";
import ClickMappings, {
clickTargetObjectType,
} from "metabase/dashboard/components/ClickMappings";
import { SidebarItem } from "../SidebarItem";
import { Heading } from "../ClickBehaviorSidebar.styled";
import {
LinkTargetEntityPickerContent,
SelectedEntityPickerIcon,
SelectedEntityPickerContent,
} from "./LinkOptions.styled";
function PickerControl({ isDash, clickBehavior, onCancel }) {
const Entity = isDash ? Dashboards : Questions;
const renderLabel = useCallback(() => {
const hasSelectedTarget = clickBehavior.targetId != null;
if (hasSelectedTarget) {
return <Entity.Name id={clickBehavior.targetId} />;
}
return isDash ? t`Pick a dashboard...` : t`Pick a question...`;
}, [isDash, clickBehavior]);
return (
<SidebarItem.Selectable isSelected padded={false}>
<LinkTargetEntityPickerContent>
<SelectedEntityPickerIcon name={isDash ? "dashboard" : "bar"} />
<SelectedEntityPickerContent>
{renderLabel()}
<Icon name="chevrondown" size={12} className="ml-auto" />
</SelectedEntityPickerContent>
</LinkTargetEntityPickerContent>
<SidebarItem.CloseIcon onClick={onCancel} />
</SidebarItem.Selectable>
);
}
function getTargetClickMappingsHeading(entity) {
return {
dashboard: t`Pass values to this dashboard's filters (optional)`,
native: t`Pass values to this question's variables (optional)`,
gui: t`Pass values to filter this question (optional)`,
}[clickTargetObjectType(entity)];
}
function TargetClickMappings({
isDash,
clickBehavior,
dashcard,
updateSettings,
}) {
const Entity = isDash ? Dashboards : Questions;
return (
<Entity.Loader id={clickBehavior.targetId}>
{({ object }) => (
<div className="pt1">
<Heading>{getTargetClickMappingsHeading(object)}</Heading>
<ClickMappings
object={object}
dashcard={dashcard}
isDash={isDash}
clickBehavior={clickBehavior}
updateSettings={updateSettings}
/>
</div>
)}
</Entity.Loader>
);
}
function QuestionDashboardPicker({ dashcard, clickBehavior, updateSettings }) {
const isDash = clickBehavior.linkType === "dashboard";
const hasSelectedTarget = clickBehavior.targetId != null;
const Picker = isDash ? DashboardPicker : QuestionPicker;
const handleSelectLinkTargetEntityId = useCallback(
targetId => {
const nextSettings = { ...clickBehavior, targetId };
const isNewTargetEntity = targetId !== clickBehavior.targetId;
if (isNewTargetEntity) {
// For new target question/dashboard,
// parameter mappings for the previous link target question/dashboard
// don't make sense and have to be reset
nextSettings.parameterMapping = {};
}
updateSettings(nextSettings);
},
[clickBehavior, updateSettings],
);
const handleResetLinkTargetType = useCallback(() => {
updateSettings({
type: clickBehavior.type,
linkType: null,
});
}, [clickBehavior, updateSettings]);
const pickerModalTitle = isDash
? t`Pick a dashboard to link to`
: t`Pick a question to link to`;
return (
<div>
<div className="pb1">
<ModalWithTrigger
triggerElement={
<PickerControl
isDash={isDash}
clickBehavior={clickBehavior}
onCancel={handleResetLinkTargetType}
/>
}
isInitiallyOpen={!hasSelectedTarget}
>
{({ onClose }) => (
<ModalContent
title={pickerModalTitle}
onClose={hasSelectedTarget ? onClose : null}
>
<Picker
value={clickBehavior.targetId}
onChange={targetId => {
handleSelectLinkTargetEntityId(targetId);
onClose();
}}
/>
</ModalContent>
)}
</ModalWithTrigger>
</div>
{hasSelectedTarget && (
<TargetClickMappings
isDash={isDash}
clickBehavior={clickBehavior}
dashcard={dashcard}
updateSettings={updateSettings}
/>
)}
</div>
);
}
export default QuestionDashboardPicker;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment