diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ActionOptions.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ActionOptions/ActionOptions.jsx similarity index 52% rename from frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ActionOptions.jsx rename to frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ActionOptions/ActionOptions.jsx index 4b130367bc8c47ee1cc973576ab80e28933c52eb..9049cd33d5ed50feb3b0729c0c2665481591a1b4 100644 --- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ActionOptions.jsx +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ActionOptions/ActionOptions.jsx @@ -1,57 +1,47 @@ /* 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 && ( diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ActionOptions/ActionOptions.styled.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ActionOptions/ActionOptions.styled.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6da6ac09579ab6a29e116482b140e8357c05e00d --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ActionOptions/ActionOptions.styled.tsx @@ -0,0 +1,30 @@ +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")}; +`; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ActionOptions/index.ts b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ActionOptions/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b0646d30acc523f1d7b026fd211f813dfda9af64 --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ActionOptions/index.ts @@ -0,0 +1 @@ +export { default } from "./ActionOptions"; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.jsx index 0df61c7c95ddbefcfdad33ebf947e1711b93ace7..ecb4489ca92f316c7451537bc1b91824dfe08248 100644 --- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.jsx +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.jsx @@ -1,5 +1,5 @@ /* 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; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.styled.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.styled.tsx index dfbaf52d3b11fbef5b3e2b9d554259ff7bc30e5e..87e3a2b60f7f118ccece94309256dcd475ab9cb5 100644 --- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.styled.tsx +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.styled.tsx @@ -1,17 +1,7 @@ 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; `; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader.jsx deleted file mode 100644 index 5169dff7f91ce465d98da7929a9917e862cdd37a..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader.jsx +++ /dev/null @@ -1,50 +0,0 @@ -/* 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; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/ClickBehaviorSidebarHeader.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/ClickBehaviorSidebarHeader.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9eabc99fdc0cf93dd7b54cbcdc939c8c76831f8a --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/ClickBehaviorSidebarHeader.jsx @@ -0,0 +1,50 @@ +/* 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; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/ClickBehaviorSidebarHeader.styled.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/ClickBehaviorSidebarHeader.styled.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cb3f2bc41998d5ebbc341111d6c68536f587eca2 --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/ClickBehaviorSidebarHeader.styled.tsx @@ -0,0 +1,30 @@ +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")}; + } +`; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/index.ts b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..abd89c2220ec2364456c32ac983b984c903a8dbb --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/index.ts @@ -0,0 +1 @@ +export { default } from "./ClickBehaviorSidebarHeader"; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarMainView.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarMainView.jsx deleted file mode 100644 index ed78e51933734928732b4c56dd1438bde81aba6a..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarMainView.jsx +++ /dev/null @@ -1,82 +0,0 @@ -/* 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; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarMainView/ClickBehaviorSidebarMainView.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarMainView/ClickBehaviorSidebarMainView.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4d7e44e0fc85c400d23fb22e14fb5c2280db3291 --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarMainView/ClickBehaviorSidebarMainView.jsx @@ -0,0 +1,97 @@ +/* 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; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarMainView/index.ts b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarMainView/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f8a2003216ad1bd7d07f1c0a9d4aad8135d77720 --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarMainView/index.ts @@ -0,0 +1 @@ +export { default } from "./ClickBehaviorSidebarMainView"; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/Column.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/Column.jsx deleted file mode 100644 index 88e24452ea234d885d97d3eb44f7c89827d62d1c..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/Column.jsx +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from "react"; -import { t, jt, ngettext, msgid } from "ttag"; -import _ from "underscore"; - -import Icon from "metabase/components/Icon"; - -import { color } from "metabase/lib/colors"; -import { getIconForField } from "metabase/lib/schema_metadata"; - -import Dashboards from "metabase/entities/dashboards"; -import Questions from "metabase/entities/questions"; - -import { SidebarItemWrapper, SidebarItemStyle } from "./SidebarItem"; -import { SidebarIconWrapper } from "./ClickBehaviorSidebar.styled"; - -const LinkTargetName = ({ clickBehavior: { linkType, targetId } }) => ( - <span> - {linkType === "url" ? ( - t`URL` - ) : linkType === "question" ? ( - <span> - {'"'} - <Questions.Name id={targetId} /> - {'"'} - </span> - ) : linkType === "dashboard" ? ( - <span> - {'"'} - <Dashboards.Name id={targetId} /> - {'"'} - </span> - ) : ( - "Unknown" - )} - </span> -); - -const Column = ({ column, clickBehavior, onClick }) => ( - <SidebarItemWrapper onClick={onClick} style={{ ...SidebarItemStyle }}> - <SidebarIconWrapper> - <Icon name={getIconForField(column)} color={color("brand")} size={18} /> - </SidebarIconWrapper> - <div> - <h4> - {clickBehavior && clickBehavior.type === "crossfilter" - ? (n => - ngettext( - msgid`${column.display_name} updates ${n} filter`, - `${column.display_name} updates ${n} filters`, - n, - ))(Object.keys(clickBehavior.parameterMapping || {}).length) - : clickBehavior && clickBehavior.type === "link" - ? jt`${column.display_name} goes to ${( - <LinkTargetName clickBehavior={clickBehavior} /> - )}` - : column.display_name} - </h4> - </div> - </SidebarItemWrapper> -); - -export default Column; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOption.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOption.jsx deleted file mode 100644 index 28b915a31c74809f6363b59123c9ab63842ab091..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOption.jsx +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable react/prop-types */ -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"; - -const LinkOption = ({ option, icon, onClick }) => ( - <SidebarItemWrapper onClick={onClick} style={{ ...SidebarItemStyle }}> - <SidebarIconWrapper> - <Icon name={icon} color={color("brand")} /> - </SidebarIconWrapper> - <div> - <h4>{option}</h4> - </div> - </SidebarItemWrapper> -); - -export default LinkOption; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions.jsx deleted file mode 100644 index 6d3452c7bfe6d5e9a6d477d357a341a1a7f3a7c4..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions.jsx +++ /dev/null @@ -1,156 +0,0 @@ -/* 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; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/CustomLinkText.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/CustomLinkText.jsx similarity index 92% rename from frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/CustomLinkText.jsx rename to frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/CustomLinkText.jsx index 60491052fa35080ddb72ba496ac75f107e96ce4a..1d8967f8ab651905c92888cda7560f546150c7e6 100644 --- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/CustomLinkText.jsx +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/CustomLinkText.jsx @@ -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 ( diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/CustomURLPicker.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/CustomURLPicker.jsx new file mode 100644 index 0000000000000000000000000000000000000000..df176e2208f14af4f16d2651f657022dd8d0411b --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/CustomURLPicker.jsx @@ -0,0 +1,96 @@ +/* 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; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/CustomURLPicker.styled.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/CustomURLPicker.styled.tsx new file mode 100644 index 0000000000000000000000000000000000000000..81a0ad54479feb6894a256202e4294c94d339072 --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/CustomURLPicker.styled.tsx @@ -0,0 +1,12 @@ +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; +`; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkOption.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkOption.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4118259d526586525abbf90f0596bf3a87d7866f --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkOption.jsx @@ -0,0 +1,18 @@ +/* eslint-disable react/prop-types */ +import React from "react"; +import _ from "underscore"; + +import { color } from "metabase/lib/colors"; + +import { SidebarItem } from "../SidebarItem"; + +const LinkOption = ({ option, icon, onClick }) => ( + <SidebarItem onClick={onClick}> + <SidebarItem.Icon name={icon} color={color("brand")} /> + <div> + <SidebarItem.Name>{option}</SidebarItem.Name> + </div> + </SidebarItem> +); + +export default LinkOption; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkOptions.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkOptions.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c7766ec14a91900f3373b7a94c314be1a94581f5 --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkOptions.jsx @@ -0,0 +1,87 @@ +/* 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; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkOptions.styled.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkOptions.styled.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3a7ab40def5db9b2c73a54e0dca57eaeb45673ac --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/LinkOptions.styled.tsx @@ -0,0 +1,28 @@ +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; +`; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/QuestionDashboardPicker.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/QuestionDashboardPicker.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a35fbc5ad680084ec2e4cc1be667ebc67b113a5a --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/QuestionDashboardPicker.jsx @@ -0,0 +1,158 @@ +/* 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; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ValuesYouCanReference.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/ValuesYouCanReference.jsx similarity index 100% rename from frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ValuesYouCanReference.jsx rename to frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/ValuesYouCanReference.jsx diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/index.ts b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..8eaa4084ec9b2ddb538f98ae0d7b7778600eb7eb --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions/index.ts @@ -0,0 +1 @@ +export { default } from "./LinkOptions"; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/QuestionDashboardPicker.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/QuestionDashboardPicker.jsx deleted file mode 100644 index 4eb7f61d3634f07eb8f68ee4205f4a5b22ef37a5..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/QuestionDashboardPicker.jsx +++ /dev/null @@ -1,141 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from "react"; -import { t } from "ttag"; -import _ from "underscore"; -import cx from "classnames"; - -import Icon from "metabase/components/Icon"; -import ModalContent from "metabase/components/ModalContent"; -import ModalWithTrigger from "metabase/components/ModalWithTrigger"; - -import { color } from "metabase/lib/colors"; - -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 { SidebarItemClasses, SidebarItemStyle } from "./SidebarItem"; -import { - CloseIconContainer, - Heading, - SidebarIconWrapper, - SidebarItem, -} from "./ClickBehaviorSidebar.styled"; - -function QuestionDashboardPicker({ dashcard, clickBehavior, updateSettings }) { - const isDash = clickBehavior.linkType === "dashboard"; - const Entity = isDash ? Dashboards : Questions; - const Picker = isDash ? DashboardPicker : QuestionPicker; - return ( - <div> - <div className="pb1"> - <ModalWithTrigger - triggerElement={ - <div - className={cx(SidebarItemClasses, "overflow-hidden")} - style={{ - marginLeft: SidebarItemStyle.marginLeft, - marginRight: SidebarItemStyle.marginRight, - backgroundColor: color("brand"), - color: color("white"), - }} - > - <SidebarItem - style={{ - paddingLeft: SidebarItemStyle.paddingLeft, - paddingRight: SidebarItemStyle.paddingRight, - paddingTop: SidebarItemStyle.paddingTop, - paddingBottom: SidebarItemStyle.paddingBottom, - }} - > - <SidebarIconWrapper style={{ borderColor: "transparent" }}> - <Icon name={isDash ? "dashboard" : "bar"} /> - </SidebarIconWrapper> - <div className="flex align-center full text-bold"> - {clickBehavior.targetId == null ? ( - isDash ? ( - t`Pick a dashboard...` - ) : ( - t`Pick a question...` - ) - ) : ( - <Entity.Name id={clickBehavior.targetId} /> - )} - <Icon name="chevrondown" size={12} className="ml-auto" /> - </div> - </SidebarItem> - <CloseIconContainer - onClick={() => - updateSettings({ - type: clickBehavior.type, - linkType: null, - }) - } - > - <Icon name="close" size={12} /> - </CloseIconContainer> - </div> - } - isInitiallyOpen={clickBehavior.targetId == null} - > - {({ onClose }) => ( - <ModalContent - title={ - isDash - ? t`Pick a dashboard to link to` - : t`Pick a question to link to` - } - onClose={clickBehavior.targetId != null ? onClose : null} - > - <Picker - value={clickBehavior.targetId} - onChange={targetId => { - updateSettings({ - ...clickBehavior, - ...(targetId !== clickBehavior.targetId - ? { parameterMapping: {} } - : {}), - targetId, - }); - onClose(); - }} - /> - </ModalContent> - )} - </ModalWithTrigger> - </div> - {clickBehavior.targetId != null && ( - <Entity.Loader id={clickBehavior.targetId}> - {({ object }) => ( - <div className="pt1"> - <Heading> - { - { - 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(object)] - } - </Heading> - <ClickMappings - object={object} - dashcard={dashcard} - isDash={isDash} - clickBehavior={clickBehavior} - updateSettings={updateSettings} - /> - </div> - )} - </Entity.Loader> - )} - </div> - ); -} - -export default QuestionDashboardPicker; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/SidebarItem.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/SidebarItem.jsx deleted file mode 100644 index 487b46aaa700449c416188ddaec6f6e73a535954..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/SidebarItem.jsx +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from "react"; -import cx from "classnames"; - -export const SidebarItemClasses = - "border-brand-hover bordered border-transparent rounded flex align-center cursor-pointer overflow-hidden"; - -export const SidebarItemStyle = { - paddingTop: 8, - paddingBottom: 8, - paddingLeft: 12, - paddingRight: 12, -}; - -export const SidebarItemWrapper = ({ children, onClick, style, disabled }) => ( - <div - className={cx(SidebarItemClasses, { disabled })} - onClick={!disabled && onClick} - style={{ - ...style, - }} - > - {children} - </div> -); diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/SidebarItem/SidebarItem.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/SidebarItem/SidebarItem.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7b2b89371673cd5656ae1d777961bf34785f8b43 --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/SidebarItem/SidebarItem.jsx @@ -0,0 +1,45 @@ +/* eslint-disable react/prop-types */ +import React from "react"; + +import Icon from "metabase/components/Icon"; + +import { + Name, + Content, + IconContainer, + CloseIconContainer, + BaseSidebarItemRoot, + SelectableSidebarItemRoot, +} from "./SidebarItem.styled"; + +function ItemIcon({ className, ...props }) { + return ( + <IconContainer className={className}> + <Icon {...props} /> + </IconContainer> + ); +} + +function CloseIcon({ className, onClick }) { + return ( + <CloseIconContainer className={className} onClick={onClick}> + <Icon name="close" size={12} /> + </CloseIconContainer> + ); +} + +export function SidebarItem({ as = BaseSidebarItemRoot, ...props }) { + const Element = as; + return <Element {...props} />; +} + +function SelectableSidebarItem(props) { + return <SidebarItem {...props} as={SelectableSidebarItemRoot} />; +} + +SidebarItem.Selectable = SelectableSidebarItem; + +SidebarItem.Content = Content; +SidebarItem.Name = Name; +SidebarItem.Icon = ItemIcon; +SidebarItem.CloseIcon = CloseIcon; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/SidebarItem/SidebarItem.styled.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/SidebarItem/SidebarItem.styled.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7cc6d2a295784eb1e4add342c6f941e6295fde8d --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/SidebarItem/SidebarItem.styled.tsx @@ -0,0 +1,72 @@ +import styled from "@emotion/styled"; +import { css } from "@emotion/react"; +import { color, darken } from "metabase/lib/colors"; + +const disabledStyle = css` + pointer-events: none; + opacity: 0.4; +`; + +export const sidebarItemPaddingStyle = css` + padding: 8px 12px; +`; + +export const BaseSidebarItemRoot = styled.div<{ + disabled?: boolean; + padded?: boolean; +}>` + display: flex; + align-items: center; + + overflow: hidden; + + border: 1px solid transparent; + border-radius: 8px; + + cursor: pointer; + + ${({ disabled }) => disabled && disabledStyle} + + ${({ padded = true }) => padded && sidebarItemPaddingStyle} + + &:hover { + border-color: ${color("brand")}; + } +`; + +export const SelectableSidebarItemRoot = styled(BaseSidebarItemRoot)<{ + isSelected: boolean; +}>` + background-color: ${props => + props.isSelected ? color("brand") : "transparent"}; + + color: ${props => (props.isSelected ? color("white") : "inherit")}; +`; + +export const Content = styled.div` + display: flex; + align-items: center; + width: 100%; +`; + +export const Name = styled.h4``; + +export const IconContainer = 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 CloseIconContainer = styled.span` + margin-left: auto; + padding: 1rem; + border-left: 1px solid ${darken("brand", 0.2)}; +`; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/SidebarItem/index.ts b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/SidebarItem/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3ccdc2d36e064444b6fb8e5493de8147bef7b3c9 --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/SidebarItem/index.ts @@ -0,0 +1 @@ +export * from "./SidebarItem"; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView.jsx deleted file mode 100644 index 10b9e5dedeaa2a45d25788deca5c3954b93366c3..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView.jsx +++ /dev/null @@ -1,72 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from "react"; -import { t } from "ttag"; -import _ from "underscore"; - -import { hasActionsMenu } from "metabase/lib/click-behavior"; - -import Sidebar from "metabase/dashboard/components/Sidebar"; - -import Column from "./Column"; -import { Heading, SidebarHeader } from "./ClickBehaviorSidebar.styled"; - -function TableClickBehaviorView({ - columns, - dashcard, - getClickBehaviorForColumn, - canClose, - onColumnClick, - onCancel, - onClose, -}) { - return ( - <Sidebar onClose={onClose} onCancel={onCancel} closeIsDisabled={!canClose}> - <SidebarHeader> - <Heading className="text-paragraph">{t`On-click behavior for each column`}</Heading> - </SidebarHeader> - <div> - {_.chain(columns) - .map(column => ({ - column, - clickBehavior: getClickBehaviorForColumn(column), - })) - .groupBy(({ clickBehavior }) => { - const { type = "actionMenu" } = clickBehavior || {}; - return type; - }) - .pairs() - .sortBy(([linkType]) => - ["link", "crossfilter", "actionMenu"].indexOf(linkType), - ) - .map(([linkType, columnsWithClickBehavior]) => ( - <div key={linkType} className="mb2 px4"> - <h5 className="text-uppercase text-medium my1"> - { - { - link: t`Go to custom destination`, - crossfilter: t`Update a dashboard filter`, - actionMenu: hasActionsMenu(dashcard) - ? t`Open the actions menu` - : t`Do nothing`, - }[linkType] - } - </h5> - {columnsWithClickBehavior.map( - ({ column, clickBehavior }, index) => ( - <Column - key={index} - column={column} - clickBehavior={clickBehavior} - onClick={() => onColumnClick(column)} - /> - ), - )} - </div> - )) - .value()} - </div> - </Sidebar> - ); -} - -export default TableClickBehaviorView; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/Column.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/Column.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b1bc33628526799e2c1777378f67e7e27dbcb16d --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/Column.jsx @@ -0,0 +1,87 @@ +/* eslint-disable react/prop-types */ +import React from "react"; +import { t, jt, ngettext, msgid } from "ttag"; +import _ from "underscore"; + +import { color } from "metabase/lib/colors"; +import { getIconForField } from "metabase/lib/schema_metadata"; + +import Dashboards from "metabase/entities/dashboards"; +import Questions from "metabase/entities/questions"; + +import { SidebarItem } from "../SidebarItem"; + +function Quoted({ children }) { + return ( + <span> + {'"'} + {children} + {'"'} + </span> + ); +} + +const LinkTargetName = ({ clickBehavior: { linkType, targetId } }) => { + if (linkType === "url") { + return t`URL`; + } + if (linkType === "question") { + return ( + <Quoted> + <Questions.Name id={targetId} /> + </Quoted> + ); + } + if (linkType === "dashboard") { + return ( + <Quoted> + <Dashboards.Name id={targetId} /> + </Quoted> + ); + } + return t`Unknown`; +}; + +function ClickBehaviorDescription({ column, clickBehavior }) { + if (!clickBehavior) { + return column.display_name; + } + + if (clickBehavior.type === "crossfilter") { + const parameters = Object.keys(clickBehavior.parameterMapping || {}); + return (n => + ngettext( + msgid`${column.display_name} updates ${n} filter`, + `${column.display_name} updates ${n} filters`, + n, + ))(parameters.length); + } + + if (clickBehavior.type === "link") { + return jt`${column.display_name} goes to ${( + <LinkTargetName clickBehavior={clickBehavior} /> + )}`; + } + + return column.display_name; +} + +const Column = ({ column, clickBehavior, onClick }) => ( + <SidebarItem onClick={onClick}> + <SidebarItem.Icon + name={getIconForField(column)} + color={color("brand")} + size={18} + /> + <div> + <SidebarItem.Name> + <ClickBehaviorDescription + column={column} + clickBehavior={clickBehavior} + /> + </SidebarItem.Name> + </div> + </SidebarItem> +); + +export default Column; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/TableClickBehaviorView.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/TableClickBehaviorView.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f06c17c788a39d20efabc727992edf7f389785d8 --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/TableClickBehaviorView.jsx @@ -0,0 +1,82 @@ +/* eslint-disable react/prop-types */ +import React, { useMemo, useCallback } from "react"; +import { t } from "ttag"; +import _ from "underscore"; + +import { hasActionsMenu } from "metabase/lib/click-behavior"; + +import Column from "./Column"; + +const COLUMN_SORTING_ORDER_BY_CLICK_BEHAVIOR_TYPE = [ + "link", + "crossfilter", + "actionMenu", +]; + +function explainClickBehaviorType(type, dashcard) { + return { + link: t`Go to custom destination`, + crossfilter: t`Update a dashboard filter`, + actionMenu: hasActionsMenu(dashcard) + ? t`Open the actions menu` + : t`Do nothing`, + }[type]; +} + +function TableClickBehaviorView({ + columns, + dashcard, + getClickBehaviorForColumn, + onColumnClick, +}) { + const groupedColumns = useMemo(() => { + const withClickBehaviors = columns.map(column => ({ + column, + clickBehavior: getClickBehaviorForColumn(column), + })); + const groupedByClickBehavior = _.groupBy( + withClickBehaviors, + ({ clickBehavior }) => { + return clickBehavior?.type || "actionMenu"; + }, + ); + + const pairs = _.pairs(groupedByClickBehavior); + return _.sortBy(pairs, ([type]) => + COLUMN_SORTING_ORDER_BY_CLICK_BEHAVIOR_TYPE.indexOf(type), + ); + }, [columns, getClickBehaviorForColumn]); + + const renderColumn = useCallback( + ({ column, clickBehavior }, index) => { + return ( + <Column + key={index} + column={column} + clickBehavior={clickBehavior} + onClick={() => onColumnClick(column)} + /> + ); + }, + [onColumnClick], + ); + + const renderColumnGroup = useCallback( + group => { + const [clickBehaviorType, columnsWithClickBehavior] = group; + return ( + <div key={clickBehaviorType} className="mb2 px4"> + <h5 className="text-uppercase text-medium my1"> + {explainClickBehaviorType(clickBehaviorType, dashcard)} + </h5> + {columnsWithClickBehavior.map(renderColumn)} + </div> + ); + }, + [dashcard, renderColumn], + ); + + return <>{groupedColumns.map(renderColumnGroup)}</>; +} + +export default TableClickBehaviorView; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/index.ts b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5d88342da82f84cddad301aef3bd1d0a320eae42 --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/index.ts @@ -0,0 +1 @@ +export { default } from "./TableClickBehaviorView"; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TypeSelector.jsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TypeSelector/TypeSelector.jsx similarity index 51% rename from frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TypeSelector.jsx rename to frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TypeSelector/TypeSelector.jsx index 68a2a69bf59fb43ae94d9bbe875e05591da9f1e8..38afb286b30b22cf4d39992776a85fded9ec5fe5 100644 --- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TypeSelector.jsx +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TypeSelector/TypeSelector.jsx @@ -1,14 +1,13 @@ /* eslint-disable react/prop-types */ -import React from "react"; +import React, { useCallback } from "react"; import _ from "underscore"; import Icon from "metabase/components/Icon"; - import { color } from "metabase/lib/colors"; +import { clickBehaviorOptions, getClickBehaviorOptionName } from "../utils"; +import { SidebarItem } from "../SidebarItem"; -import { clickBehaviorOptions, getClickBehaviorOptionName } from "./utils"; -import { SidebarItemWrapper, SidebarItemStyle } from "./SidebarItem"; -import { SidebarIconWrapper } from "./ClickBehaviorSidebar.styled"; +import { BehaviorOptionIcon } from "./TypeSelector.styled"; const BehaviorOption = ({ option, @@ -18,30 +17,25 @@ const BehaviorOption = ({ selected, disabled, }) => ( - <SidebarItemWrapper - style={{ - ...SidebarItemStyle, - backgroundColor: selected ? color("brand") : "transparent", - color: selected ? color("white") : "inherit", - }} + <SidebarItem.Selectable + isSelected={selected} onClick={onClick} disabled={disabled} > - <SidebarIconWrapper style={{ borderColor: selected && "transparent" }}> - <Icon - name={selected ? "check" : icon} - color={selected ? color("white") : color("brand")} - /> - </SidebarIconWrapper> - <div className="flex align-center full"> - <h4>{option}</h4> + <BehaviorOptionIcon + name={selected ? "check" : icon} + color={selected ? color("white") : color("brand")} + isSelected={selected} + /> + <SidebarItem.Content> + <SidebarItem.Name>{option}</SidebarItem.Name> {hasNextStep && ( <span className="ml-auto"> <Icon name="chevronright" size={12} /> </span> )} - </div> - </SidebarItemWrapper> + </SidebarItem.Content> + </SidebarItem.Selectable> ); function TypeSelector({ @@ -51,23 +45,28 @@ function TypeSelector({ parameters, moveToNextPage, }) { + const handleSelect = useCallback( + value => { + if (value !== clickBehavior.type) { + updateSettings(value === "menu" ? undefined : { type: value }); + } else if (value !== "menu") { + moveToNextPage(); + } + }, + [clickBehavior, updateSettings, moveToNextPage], + ); + return ( <div> {clickBehaviorOptions.map(({ value, icon }) => ( <div key={value} className="mb1"> <BehaviorOption - onClick={() => { - if (value !== clickBehavior.type) { - updateSettings(value === "menu" ? undefined : { type: value }); - } else if (value !== "menu") { - moveToNextPage(); // if it didn't change, we need to advance here rather than in `componentDidUpdate` - } - }} - icon={icon} option={getClickBehaviorOptionName(value, dashcard)} - hasNextStep={value !== "menu"} selected={clickBehavior.type === value} disabled={value === "crossfilter" && parameters.length === 0} + onClick={() => handleSelect(value)} + icon={icon} + hasNextStep={value !== "menu"} /> </div> ))} diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TypeSelector/TypeSelector.styled.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TypeSelector/TypeSelector.styled.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8f1f7cfb79fdb471b35c9c028c781df839554e06 --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TypeSelector/TypeSelector.styled.tsx @@ -0,0 +1,14 @@ +import styled from "@emotion/styled"; +import { color } from "metabase/lib/colors"; +import { SidebarItem } from "../SidebarItem"; + +export const BehaviorOptionIcon = styled(SidebarItem.Icon)<{ + isSelected?: boolean; +}>` + border-color: ${props => + props.isSelected ? "transparent" : color("border")}; + + .Icon { + color: ${props => (props.isSelected ? color("white") : color("brand"))}; + } +`; diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TypeSelector/index.ts b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TypeSelector/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..7eb68b6df3574394846114471e29367dd8378e2e --- /dev/null +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TypeSelector/index.ts @@ -0,0 +1 @@ +export { default } from "./TypeSelector";