Skip to content
Snippets Groups Projects
Unverified Commit 98da65ac authored by Ryan Laurie's avatar Ryan Laurie Committed by GitHub
Browse files

Data apps: faster action adding (#25846)


* [Apps] Model Actions

WIP Adding migration and endpoints for model_action.

* WIP model action execution

Changing the execution endpoints again, still working on implicit action
execution tests. Need to also test the parameters fetch.

* Re-add execution route for dashcard action_id until Front End catches up

* Add name to model_action GET

* Hydrate model-action on dashcards

* Rename and combine hydration of model_action to action

* Implicit action execution support. Use slug columns as parameter ids

* Go through model-action when using GET /action

* FE integration fixes

* Move action execution to use new parameter shape

Now, parameters should be a map of parameter-id to value
Parameter-id should be mapped on the FE. So the action parameter-id not the unmapped
dashboard parameter-id.

* Bring ModelAction into copy code

* Generate target of implicit actions to match http actions

* Update action test to place action on model

* Add unique constraint to model_action for (card_id, slug)

* Add missing require

* model_action.slug needs to be varchar for mysql constraint

* Fix test from unique constraint

* Consistent ordering for tests

* Addressing Code Reviews and Scaffold changes

* Add type: implicit to implicit model actions

* Update src/metabase/api/model_action.clj

Co-authored-by: default avatarmetamben <103100869+metamben@users.noreply.github.com>

* Allow changing card_id with PUT to dashcard for model-actions

* Update tests after merge

* Fix implicit delete and add better test coverage

* Addressing review changes

Add PUT test
Add schema for dashboard-id and dashcard-id params
Fix select ordering in test

* save actions in models

* list model actions on detail page (#25648)

* Link Dashcards to model-based actions (#25770)

* Link dashcards to model-based actions

* address review comments

* Data apps model actions implicit creator (#25807)

* Link dashcards to model-based actions

* address review comments

* Link dashcards to model-based actions

* Allow implicit actions to be used like explicit ones

* address review comments

* update action types

* add action sidebar

* remove action selection from click behavior sidebar

* rework action picker

* add shortcut to create an action for a model

* update tests

* address review comments

Co-authored-by: default avatarCase Nelson <case@metabase.com>
Co-authored-by: default avatarmetamben <103100869+metamben@users.noreply.github.com>
Co-authored-by: default avatarAnton Kulyk <kuliks.anton@gmail.com>
parent 4c401fca
No related branches found
No related tags found
No related merge requests found
Showing
with 535 additions and 192 deletions
...@@ -4,7 +4,9 @@ import type { ...@@ -4,7 +4,9 @@ import type {
ParameterId, ParameterId,
Parameter, Parameter,
} from "metabase-types/types/Parameter"; } from "metabase-types/types/Parameter";
import type { CardId, SavedCard } from "metabase-types/types/Card"; import type { CardId, SavedCard } from "metabase-types/types/Card";
import type { WritebackAction } from "./writeback";
import type { Dataset } from "./dataset"; import type { Dataset } from "./dataset";
...@@ -41,6 +43,7 @@ export type DashboardOrderedCard = BaseDashboardOrderedCard & { ...@@ -41,6 +43,7 @@ export type DashboardOrderedCard = BaseDashboardOrderedCard & {
card: SavedCard; card: SavedCard;
parameter_mappings?: DashboardParameterMapping[] | null; parameter_mappings?: DashboardParameterMapping[] | null;
series?: SavedCard[]; series?: SavedCard[];
action?: WritebackAction;
}; };
export type DashboardParameterMapping = { export type DashboardParameterMapping = {
......
...@@ -6,7 +6,8 @@ import { ...@@ -6,7 +6,8 @@ import {
DashboardParameterMapping, DashboardParameterMapping,
} from "./dashboard"; } from "./dashboard";
import { WritebackAction } from "./writeback"; import { WritebackAction } from "./writeback";
import { FormType } from "./writeback-form-settings"; import { ActionDisplayType } from "./writeback-form-settings";
import { Card } from "./card";
export type DataAppId = number; export type DataAppId = number;
export type DataAppPage = Dashboard; export type DataAppPage = Dashboard;
...@@ -45,7 +46,6 @@ export type ActionParametersMapping = Pick< ...@@ -45,7 +46,6 @@ export type ActionParametersMapping = Pick<
export interface ActionDashboardCard export interface ActionDashboardCard
extends Omit<BaseDashboardOrderedCard, "parameter_mappings"> { extends Omit<BaseDashboardOrderedCard, "parameter_mappings"> {
action_id: number | null;
action?: WritebackAction; action?: WritebackAction;
card_id?: number; // model card id for the associated action card_id?: number; // model card id for the associated action
...@@ -54,6 +54,6 @@ export interface ActionDashboardCard ...@@ -54,6 +54,6 @@ export interface ActionDashboardCard
[key: string]: unknown; [key: string]: unknown;
"button.label"?: string; "button.label"?: string;
click_behavior?: ClickBehavior; click_behavior?: ClickBehavior;
actionDisplayType?: FormType; actionDisplayType?: ActionDisplayType;
}; };
} }
...@@ -30,7 +30,6 @@ export const createMockDashboardActionButton = ({ ...@@ -30,7 +30,6 @@ export const createMockDashboardActionButton = ({
...opts ...opts
}: Partial<ActionDashboardCard> = {}): ActionDashboardCard => ({ }: Partial<ActionDashboardCard> = {}): ActionDashboardCard => ({
id: 1, id: 1,
action_id: null,
parameter_mappings: null, parameter_mappings: null,
visualization_settings: merge( visualization_settings: merge(
{ {
......
...@@ -2,6 +2,7 @@ import { ...@@ -2,6 +2,7 @@ import {
Card, Card,
QueryAction, QueryAction,
WritebackAction, WritebackAction,
WritebackQueryAction,
QueryActionCard, QueryActionCard,
} from "metabase-types/api"; } from "metabase-types/api";
import { createMockCard } from "./card"; import { createMockCard } from "./card";
...@@ -25,7 +26,7 @@ export const createMockQueryAction = ({ ...@@ -25,7 +26,7 @@ export const createMockQueryAction = ({
...opts ...opts
}: Partial<WritebackAction & QueryAction> = {}): WritebackAction => { }: Partial<WritebackAction & QueryAction> = {}): WritebackAction => {
return { return {
id: 1, action_id: 1,
type: "query", type: "query",
card_id: card.id, card_id: card.id,
card, card,
...@@ -37,3 +38,30 @@ export const createMockQueryAction = ({ ...@@ -37,3 +38,30 @@ export const createMockQueryAction = ({
...opts, ...opts,
}; };
}; };
export const createMockImplictQueryAction = (
options: Partial<WritebackQueryAction>,
): WritebackQueryAction => ({
card_id: 1,
name: "",
description: "",
"updated-at": new Date().toISOString(),
"created-at": new Date().toISOString(),
slug: "",
parameters: [
{
id: "id",
target: ["variable", ["template-tag", "id"]],
type: "type/Integer",
},
{
id: "name",
target: ["variable", ["template-tag", "name"]],
type: "type/Text",
},
] as WritebackAction["parameters"],
type: "implicit",
visualization_settings: undefined,
card: createMockQueryActionCard(),
...options,
});
import type { ParameterId } from "./parameters"; import type { ParameterId } from "./parameters";
export type FormType = "inline" | "modal"; export type ActionDisplayType = "form" | "button";
export type FieldType = "string" | "number" | "date" | "category"; export type FieldType = "string" | "number" | "date" | "category";
export type DateInputType = "date" | "datetime" | "monthyear" | "quarteryear"; export type DateInputType = "date" | "datetime" | "monthyear" | "quarteryear";
...@@ -38,7 +38,7 @@ export interface FieldSettings { ...@@ -38,7 +38,7 @@ export interface FieldSettings {
export type FieldSettingsMap = Record<ParameterId, FieldSettings>; export type FieldSettingsMap = Record<ParameterId, FieldSettings>;
export interface ActionFormSettings { export interface ActionFormSettings {
name?: string; name?: string;
type: FormType; type: ActionDisplayType;
description?: string; description?: string;
fields: FieldSettingsMap; fields: FieldSettingsMap;
submitButtonLabel?: string; submitButtonLabel?: string;
......
...@@ -8,7 +8,8 @@ export interface WritebackParameter extends Parameter { ...@@ -8,7 +8,8 @@ export interface WritebackParameter extends Parameter {
export type WritebackActionType = "http" | "query" | "implicit"; export type WritebackActionType = "http" | "query" | "implicit";
export interface WritebackActionBase { export interface WritebackActionBase {
id: number; id?: number;
action_id?: number;
model_id?: number; model_id?: number;
slug?: string; slug?: string;
name: string; name: string;
......
...@@ -48,9 +48,10 @@ const EmptyState = ({ ...@@ -48,9 +48,10 @@ const EmptyState = ({
link, link,
illustrationElement, illustrationElement,
onActionClick, onActionClick,
className,
...rest ...rest
}) => ( }) => (
<div> <div className={className}>
<EmptyStateHeader> <EmptyStateHeader>
{illustrationElement && ( {illustrationElement && (
<EmptyStateIllustration>{illustrationElement}</EmptyStateIllustration> <EmptyStateIllustration>{illustrationElement}</EmptyStateIllustration>
......
import _ from "underscore"; import _ from "underscore";
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { color } from "metabase/lib/colors"; import { color, lighten } from "metabase/lib/colors";
import { space } from "metabase/styled-components/theme";
import { SidebarItem } from "metabase/dashboard/components/ClickBehaviorSidebar/SidebarItem"; import UnstyledEmptyState from "metabase/components/EmptyState";
export const ActionSidebarItem = styled(SidebarItem.Selectable)<{ export const ModelActionList = styled.div`
hasDescription?: boolean; margin-bottom: ${space(2)};
}>` &:not(:last-child) {
align-items: ${props => (props.hasDescription ? "flex-start" : "center")}; border-bottom: 1px solid ${color("border")};
margin-top: 2px; }
`; `;
export const ActionSidebarItemIcon = styled(SidebarItem.Icon)<{ export const ModelTitle = styled.h4`
isSelected?: boolean; color: ${color("text-dark")};
}>` margin-bottom: ${space(2)};
.Icon { display: flex;
color: ${props => align-items: center;
props.isSelected ? color("text-white") : color("brand")};
}
`; `;
export const ActionDescription = styled.span<{ isSelected?: boolean }>` export const ActionItem = styled.li`
width: 95%; padding-left: ${space(3)};
margin-top: 2px; margin-bottom: ${space(2)};
color: ${color("brand")};
cursor: pointer;
font-weight: bold;
&:hover: {
color: ${lighten("brand", 0.1)};
}
`;
color: ${props => export const EmptyState = styled(UnstyledEmptyState)`
props.isSelected ? color("text-white") : color("text-medium")}; margin-bottom: ${space(2)};
`; `;
export const ClickMappingsContainer = styled.div` export const EmptyModelStateContainer = styled.div`
margin-top: 1rem; padding-bottom: ${space(2)};
color: ${color("text-medium")};
text-align: center;
`; `;
import React, { useState } from "react"; import React from "react";
import { t } from "ttag"; import { t } from "ttag";
import _ from "underscore";
import Button from "metabase/core/components/Button"; import Icon from "metabase/components/Icon";
import EmptyState from "metabase/components/EmptyState";
import Actions from "metabase/entities/actions"; import Actions from "metabase/entities/actions";
import type { WritebackAction } from "metabase-types/api"; import Questions from "metabase/entities/questions";
import type { Card, WritebackAction } from "metabase-types/api";
import type { State } from "metabase-types/store"; import type { State } from "metabase-types/store";
import ModelPicker from "../ModelPicker"; import Link from "metabase/core/components/Link";
import ActionOptionItem from "./ActionOptionItem"; import Button from "metabase/core/components/Button";
import {
ModelTitle,
ActionItem,
ModelActionList,
EmptyState,
EmptyModelStateContainer,
} from "./ActionPicker.styled";
export default function ActionPicker({ export default function ActionPicker({
value, modelIds,
onChange, onClick,
}: { }: {
value: WritebackAction | undefined; modelIds: number[];
onChange: (value: WritebackAction) => void; onClick: (action: WritebackAction) => void;
}) { }) {
const [modelId, setModelId] = useState<number | undefined>(value?.model_id);
return ( return (
<div className="scroll-y"> <div className="scroll-y">
{!modelId ? ( {modelIds.map(modelId => (
<ModelPicker value={modelId} onChange={setModelId} /> <ConnectedModelActionPicker
) : ( key={modelId}
<> modelId={modelId}
<Button onClick={onClick}
icon="arrow_left" />
borderless ))}
onClick={() => setModelId(undefined)} {!modelIds.length && (
> <EmptyState
{t`Select Model`} message={t`No models found`}
</Button> action={t`Create new model`}
link={"/model/new"}
<ConnectedModelActionPicker />
modelId={modelId}
value={value}
onChange={onChange}
/>
</>
)} )}
</div> </div>
); );
} }
function ModelActionPicker({ function ModelActionPicker({
value, onClick,
onChange, model,
actions, actions,
}: { }: {
value: WritebackAction | undefined; onClick: (newValue: WritebackAction) => void;
onChange: (newValue: WritebackAction) => void; model: Card;
actions: WritebackAction[]; actions: WritebackAction[];
}) { }) {
if (!actions?.length) {
return (
<EmptyState
message={t`There are no actions for this model`}
action={t`Create new action`}
link={"/action/create"}
/>
);
}
return ( return (
<ul> <ModelActionList>
{actions?.map(action => ( <ModelTitle>
<ActionOptionItem <Icon name="model" size={16} className="mr2" />
name={action.name} {model.name}
description={action.description} </ModelTitle>
isSelected={action.id === value?.id} {actions?.length ? (
key={action.slug} <ul>
onClick={() => onChange(action)} {actions?.map(action => (
/> <ActionItem onClick={() => onClick(action)} key={action.id}>
))} {action.name}
</ul> </ActionItem>
))}
</ul>
) : (
<EmptyModelStateContainer>
<div>{t`There are no actions for this model`}</div>
<Button
as={Link}
to={`/action/create?model-id=${model.id}`}
borderless
>
{t`Create new action`}
</Button>
</EmptyModelStateContainer>
)}
</ModelActionList>
); );
} }
const ConnectedModelActionPicker = Actions.loadList({ const ConnectedModelActionPicker = _.compose(
query: (state: State, props: { modelId?: number | null }) => ({ Questions.load({
"model-id": props?.modelId, id: (state: State, props: { modelId?: number | null }) => props?.modelId,
entityAlias: "model",
}),
Actions.loadList({
query: (state: State, props: { modelId?: number | null }) => ({
"model-id": props?.modelId,
}),
}), }),
})(ModelActionPicker); )(ModelActionPicker);
import _ from "underscore"; import _ from "underscore";
import { t } from "ttag";
import { createAction } from "metabase/lib/redux"; import { createAction } from "metabase/lib/redux";
...@@ -85,22 +86,74 @@ export const addTextDashCardToDashboard = function ({ dashId }) { ...@@ -85,22 +86,74 @@ export const addTextDashCardToDashboard = function ({ dashId }) {
}); });
}; };
export const addActionDashCardToDashboard = ({ dashId }) => { const esitmateCardSize = (displayType, action) => {
const virtualActionsCard = { const BASE_HEIGHT = 3;
...createCard(), const HEIGHT_PER_FIELD = 1.5;
display: "action",
archived: false, if (displayType === "button") {
}; return { size_x: 2, size_y: 1 };
const dashcardOverrides = { }
card: virtualActionsCard,
size_x: 2, return {
size_y: 1, size_x: 6,
visualization_settings: { size_y: Math.round(
virtual_card: virtualActionsCard, BASE_HEIGHT + action.parameters.length * HEIGHT_PER_FIELD,
}, ),
}; };
return addDashCardToDashboard({
dashId: dashId,
dashcardOverrides: dashcardOverrides,
});
}; };
export const addActionToDashboard =
async ({ dashId, action, displayType }) =>
dispatch => {
const virtualActionsCard = {
...createCard(),
display: "action",
archived: false,
};
const dashcardOverrides = {
action,
card_id: action.model_id,
card: virtualActionsCard,
...esitmateCardSize(displayType, action),
visualization_settings: {
actionDisplayType: displayType ?? "button",
virtual_card: virtualActionsCard,
"button.label": action.name ?? action.id,
action_slug: action.slug,
},
};
dispatch(
addDashCardToDashboard({
dashId: dashId,
dashcardOverrides: dashcardOverrides,
}),
);
};
export const addLinkToDashboard =
async ({ dashId, clickBehavior }) =>
dispatch => {
const virtualActionsCard = {
...createCard(),
display: "action",
archived: false,
};
const dashcardOverrides = {
card: virtualActionsCard,
size_x: 2,
size_y: 1,
visualization_settings: {
virtual_card: virtualActionsCard,
"button.label": t`Link`,
click_behavior: clickBehavior,
actionDisplayType: "button",
},
};
dispatch(
addDashCardToDashboard({
dashId: dashId,
dashcardOverrides: dashcardOverrides,
}),
);
};
...@@ -77,7 +77,6 @@ export const saveDashboardAndCards = createThunkAction( ...@@ -77,7 +77,6 @@ export const saveDashboardAndCards = createThunkAction(
// mark isAdded because addcard doesn't record the position // mark isAdded because addcard doesn't record the position
return { return {
...result, ...result,
action_id: dc.action_id,
col: dc.col, col: dc.col,
row: dc.row, row: dc.row,
size_x: dc.size_x, size_x: dc.size_x,
......
import { createAction } from "metabase/lib/redux"; import { createAction } from "metabase/lib/redux";
import { SIDEBAR_NAME } from "metabase/dashboard/constants"; import { SIDEBAR_NAME } from "metabase/dashboard/constants";
import { getSidebar } from "../selectors";
export const SET_SIDEBAR = "metabase/dashboard/SET_SIDEBAR"; export const SET_SIDEBAR = "metabase/dashboard/SET_SIDEBAR";
export const setSidebar = createAction(SET_SIDEBAR); export const setSidebar = createAction(SET_SIDEBAR);
...@@ -20,6 +21,15 @@ export const showClickBehaviorSidebar = dashcardId => dispatch => { ...@@ -20,6 +21,15 @@ export const showClickBehaviorSidebar = dashcardId => dispatch => {
} }
}; };
export const toggleSidebar = name => (dispatch, getState) => {
const currentSidebarName = getSidebar(getState()).name;
if (currentSidebarName === name) {
dispatch(closeSidebar());
} else {
dispatch(setSidebar({ name }));
}
};
export const openAddQuestionSidebar = () => dispatch => { export const openAddQuestionSidebar = () => dispatch => {
dispatch( dispatch(
setSidebar({ setSidebar({
......
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
import { space } from "metabase/styled-components/theme";
export const Heading = styled.h4`
color: ${color("text-dark")};
font-size: 1.125rem;
`;
export const SidebarContent = styled.div`
padding: 1rem 2rem;
`;
export const BorderedSidebarContent = styled(SidebarContent)`
border-bottom: 1px solid ${color("border")};
`;
export const ClickBehaviorPickerText = styled.div`
color: ${color("text-medium")};
margin-bottom: ${space(2)};
margin-left: ${space(2)};
`;
export const BackButtonContainer = styled.div`
display: flex;
align-items: center;
cursor: pointer;
font-weight: bold;
color: ${color("text-medium")};
&:hover {
color: ${color("brand")};
}
`;
export const BackButtonIconContainer = styled.div`
padding: 4px 6px;
margin-right: 8px;
border: 1px solid ${color("border")};
border-radius: 4px;
`;
import React, { useState } from "react";
import _ from "underscore";
import { t } from "ttag";
import { connect } from "react-redux";
import type {
ActionDisplayType,
Dashboard,
WritebackAction,
Card,
CustomDestinationClickBehavior,
} from "metabase-types/api";
import type { State } from "metabase-types/store";
import Search from "metabase/entities/search";
import Sidebar from "metabase/dashboard/components/Sidebar";
import ActionPicker from "metabase/containers/ActionPicker";
import {
addActionToDashboard,
addLinkToDashboard,
closeSidebar,
} from "metabase/dashboard/actions";
import { ButtonOptions } from "./ButtonOptions";
import {
Heading,
SidebarContent,
BorderedSidebarContent,
} from "./AddActionSidebar.styled";
const mapDispatchToProps = {
addAction: addActionToDashboard,
addLink: addLinkToDashboard,
closeSidebar,
};
interface ActionSidebarProps {
dashboard: Dashboard;
addAction: ({
dashId,
action,
displayType,
}: {
dashId: number;
action: WritebackAction;
displayType: ActionDisplayType;
}) => void;
addLink: ({
dashId,
clickBehavior,
}: {
dashId: number;
clickBehavior: CustomDestinationClickBehavior;
}) => void;
closeSidebar: () => void;
models: Card[];
displayType: ActionDisplayType;
}
function AddActionSidebarFn({
dashboard,
addAction,
addLink,
closeSidebar,
models,
displayType,
}: ActionSidebarProps) {
const handleActionSelected = async (action: WritebackAction) => {
await addAction({ dashId: dashboard.id, action, displayType });
};
const modelIds = models?.map(model => model.id) ?? [];
const showButtonOptions = displayType === "button";
const showActionPicker = displayType === "form";
return (
<Sidebar>
<BorderedSidebarContent>
<Heading>
{t`Add a ${
displayType === "button" ? t`button` : t`form`
} to the page`}
</Heading>
</BorderedSidebarContent>
{showActionPicker && (
<SidebarContent>
<ActionPicker modelIds={modelIds} onClick={handleActionSelected} />
</SidebarContent>
)}
{showButtonOptions && (
<ButtonOptions
addLink={addLink}
closeSidebar={closeSidebar}
dashboard={dashboard}
ActionPicker={
<SidebarContent>
<ActionPicker
modelIds={modelIds}
onClick={handleActionSelected}
/>
</SidebarContent>
}
/>
)}
</Sidebar>
);
}
export const AddActionSidebar = _.compose(
Search.loadList({
query: (_state: State, props: any) => ({
models: ["dataset"],
collection: props.dashboard.collection_id,
}),
loadingAndErrorWrapper: false,
listName: "models",
}),
connect(null, mapDispatchToProps),
)(AddActionSidebarFn);
import React, { useState } from "react";
import { t } from "ttag";
import type {
Dashboard,
CustomDestinationClickBehavior,
DashboardOrderedCard,
} from "metabase-types/api";
import { clickBehaviorIsValid } from "metabase/lib/click-behavior";
import { BehaviorOption } from "metabase/dashboard/components/ClickBehaviorSidebar/TypeSelector/TypeSelector";
import LinkOptions from "metabase/dashboard/components/ClickBehaviorSidebar/LinkOptions";
import Icon from "metabase/components/Icon";
import {
ClickBehaviorPickerText,
SidebarContent,
BackButtonIconContainer,
BackButtonContainer,
BorderedSidebarContent,
} from "./AddActionSidebar.styled";
type ButtonType = "action" | "link" | null;
// this type depends on no properties being null, but many of the components that use it expect nulls while
// the object is buing constructed :(
const emptyClickBehavior: any = {
type: "link",
linkType: null,
targetId: null,
};
export const ButtonOptions = ({
addLink,
closeSidebar,
dashboard,
ActionPicker,
}: {
addLink: ({
dashId,
clickBehavior,
}: {
dashId: number;
clickBehavior: CustomDestinationClickBehavior;
}) => void;
closeSidebar: () => void;
dashboard: Dashboard;
ActionPicker: React.ReactNode;
}) => {
const [buttonType, setButtonType] = useState<ButtonType>(null);
const [linkClickBehavior, setLinkClickBehavior] =
useState<CustomDestinationClickBehavior>(emptyClickBehavior);
const showLinkPicker = buttonType === "link";
const showActionPicker = buttonType === "action";
const handleClickBehaviorChange = (
newClickBehavior: CustomDestinationClickBehavior,
) => {
if (newClickBehavior.type !== "link") {
return;
}
if (clickBehaviorIsValid(newClickBehavior)) {
addLink({ dashId: dashboard.id, clickBehavior: newClickBehavior });
closeSidebar();
return;
}
setLinkClickBehavior(newClickBehavior);
};
return (
<>
<ButtonTypePicker value={buttonType} onChange={setButtonType} />
{showLinkPicker && (
<LinkOptions
clickBehavior={linkClickBehavior}
dashcard={{ card: { display: "action" } } as DashboardOrderedCard} // necessary for LinkOptions to work
parameters={[]}
updateSettings={handleClickBehaviorChange as any}
/>
)}
{showActionPicker && ActionPicker}
</>
);
};
const ButtonTypePicker = ({
value,
onChange,
}: {
value: ButtonType;
onChange: (type: ButtonType) => void;
}) => {
if (value) {
return (
<BorderedSidebarContent>
<BackButtonContainer onClick={() => onChange(null)}>
<BackButtonIconContainer>
<Icon name="chevronleft" size={16} />
</BackButtonIconContainer>
<div>{t`Change button type`}</div>
</BackButtonContainer>
</BorderedSidebarContent>
);
}
return (
<SidebarContent>
<ClickBehaviorPickerText>
{t`What type of button do you want to add?`}
</ClickBehaviorPickerText>
<BehaviorOption
option={t`Perform action`}
selected={false}
icon="play"
hasNextStep
onClick={() => onChange("action")}
/>
<BehaviorOption
option={t`Go to a custom destination`}
selected={false}
icon="link"
hasNextStep
onClick={() => onChange("link")}
/>
</SidebarContent>
);
};
export * from "./AddActionSidebar";
import _ from "underscore"; import _ from "underscore";
import styled from "@emotion/styled"; 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 const ClickMappingsContainer = styled.div` export const ClickMappingsContainer = styled.div`
margin-top: 1rem; margin-top: 1rem;
`; `;
......
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { t } from "ttag";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { updateButtonActionMapping } from "metabase/dashboard/actions"; import { updateButtonActionMapping } from "metabase/dashboard/actions";
import { updateSettings } from "metabase/visualizations/lib/settings";
import ActionPicker from "metabase/containers/ActionPicker"; import ActionPicker from "metabase/containers/ActionPicker";
...@@ -15,13 +13,10 @@ import type { ...@@ -15,13 +13,10 @@ import type {
import type { State } from "metabase-types/store"; import type { State } from "metabase-types/store";
import type { UiParameter } from "metabase/parameters/types"; import type { UiParameter } from "metabase/parameters/types";
import { Heading, SidebarContent } from "../ClickBehaviorSidebar.styled"; import { SidebarContent } from "../ClickBehaviorSidebar.styled";
import ActionClickMappings from "./ActionClickMappings"; import ActionClickMappings from "./ActionClickMappings";
import { import { ClickMappingsContainer } from "./ActionOptions.styled";
ClickMappingsContainer,
ActionPickerWrapper,
} from "./ActionOptions.styled";
import { ensureParamsHaveNames } from "./utils"; import { ensureParamsHaveNames } from "./utils";
...@@ -55,26 +50,6 @@ function ActionOptions({ ...@@ -55,26 +50,6 @@ function ActionOptions({
}: ActionOptionsProps) { }: ActionOptionsProps) {
const selectedAction = dashcard.action; const selectedAction = dashcard.action;
const handleActionSelected = useCallback(
(action: WritebackAction) => {
onUpdateButtonActionMapping(dashcard.id, {
card_id: action.model_id,
action,
visualization_settings: updateSettings(
{
"button.label": action.name,
action_slug: action.slug, // :-( so hacky
},
dashcard.visualization_settings,
),
// Clean mappings from previous action
// as they're most likely going to be irrelevant
parameter_mappings: null,
});
},
[dashcard, onUpdateButtonActionMapping],
);
const handleParameterMappingChange = useCallback( const handleParameterMappingChange = useCallback(
(parameter_mappings: ActionParametersMapping[] | null) => { (parameter_mappings: ActionParametersMapping[] | null) => {
onUpdateButtonActionMapping(dashcard.id, { onUpdateButtonActionMapping(dashcard.id, {
...@@ -84,36 +59,23 @@ function ActionOptions({ ...@@ -84,36 +59,23 @@ function ActionOptions({
[dashcard, onUpdateButtonActionMapping], [dashcard, onUpdateButtonActionMapping],
); );
return ( if (!selectedAction) {
<ActionPickerWrapper> return null;
<ActionPicker value={selectedAction} onChange={handleActionSelected} /> }
{!!selectedAction && (
<ClickMappingsContainer>
<ActionClickMappings
action={{
...selectedAction,
parameters: ensureParamsHaveNames(selectedAction.parameters),
}}
dashcard={dashcard}
parameters={parameters}
onChange={handleParameterMappingChange}
/>
</ClickMappingsContainer>
)}
</ActionPickerWrapper>
);
}
function ActionOptionsContainer(props: ActionOptionsProps) {
return ( return (
<SidebarContent> <SidebarContent>
<Heading className="text-medium">{t`Pick an action`}</Heading> <ClickMappingsContainer>
<ActionOptions <ActionClickMappings
dashcard={props.dashcard} action={{
parameters={props.parameters} ...selectedAction,
onUpdateButtonActionMapping={props.onUpdateButtonActionMapping} parameters: ensureParamsHaveNames(selectedAction?.parameters ?? []),
/> }}
dashcard={dashcard}
parameters={parameters}
onChange={handleParameterMappingChange}
/>
</ClickMappingsContainer>
</SidebarContent> </SidebarContent>
); );
} }
...@@ -126,4 +88,4 @@ export default connect< ...@@ -126,4 +88,4 @@ export default connect<
>( >(
null, null,
mapDispatchToProps, mapDispatchToProps,
)(ActionOptionsContainer); )(ActionOptions);
import _ from "underscore"; import _ from "underscore";
import { humanize } from "metabase/lib/formatting";
import type { import type {
ActionDashboardCard, ActionDashboardCard,
...@@ -78,6 +79,6 @@ export function ensureParamsHaveNames( ...@@ -78,6 +79,6 @@ export function ensureParamsHaveNames(
): WritebackParameter[] { ): WritebackParameter[] {
return parameters.map(parameter => ({ return parameters.map(parameter => ({
...parameter, ...parameter,
name: parameter.name ?? parameter.id, name: parameter.name ?? humanize(parameter.id),
})); }));
} }
...@@ -63,7 +63,6 @@ describe("turnDashCardParameterMappingsIntoClickBehaviorMappings", () => { ...@@ -63,7 +63,6 @@ describe("turnDashCardParameterMappingsIntoClickBehaviorMappings", () => {
const action = createMockQueryAction({ parameters: actionParameters }); const action = createMockQueryAction({ parameters: actionParameters });
const dashCard = createMockDashboardActionButton({ const dashCard = createMockDashboardActionButton({
action, action,
action_id: action.id,
parameter_mappings: dashCardParameterMappings, parameter_mappings: dashCardParameterMappings,
}); });
return { action, dashCard }; return { action, dashCard };
......
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