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

Allow disabling basic/implicit actions (#28417)

* Allow disabling (deleting) basic actions

* Add confirmation modal

* Update how `menuItems` are built

* Use aria-label instead of a test ID

* Tweak confirmation modal copy
parent 92238be2
No related branches found
No related tags found
No related merge requests found
......@@ -9,6 +9,7 @@ import Link from "metabase/core/components/Link";
import Actions from "metabase/entities/actions";
import { parseTimestamp } from "metabase/lib/time";
import * as Urls from "metabase/lib/urls";
import { useConfirmation } from "metabase/hooks/use-confirmation";
import type { Card, WritebackAction } from "metabase-types/api";
import type { Dispatch, State } from "metabase-types/store";
......@@ -36,6 +37,7 @@ interface OwnProps {
interface DispatchProps {
onEnableImplicitActions: () => void;
onArchiveAction: (action: WritebackAction) => void;
onDeleteAction: (action: WritebackAction) => void;
}
interface ActionsLoaderProps {
......@@ -50,6 +52,8 @@ function mapDispatchToProps(dispatch: Dispatch, { model }: OwnProps) {
dispatch(Actions.actions.enableImplicitActionsForModel(model.id())),
onArchiveAction: (action: WritebackAction) =>
dispatch(Actions.objectActions.setArchived(action, true)),
onDeleteAction: (action: WritebackAction) =>
dispatch(Actions.actions.delete({ id: action.id })),
};
}
......@@ -58,26 +62,58 @@ function ModelActionDetails({
actions,
onEnableImplicitActions,
onArchiveAction,
onDeleteAction,
}: Props) {
const { show: askConfirmation, modalContent: confirmationModal } =
useConfirmation();
const database = model.database();
const hasActionsEnabled = database != null && database.hasActionsEnabled();
const canWrite = model.canWriteActions();
const hasImplicitActions = actions.some(action => action.type === "implicit");
const actionsSorted = useMemo(
() => _.sortBy(actions, mostRecentFirst),
[actions],
);
const implicitActions = useMemo(
() => actions.filter(action => action.type === "implicit"),
[actions],
);
const onDeleteImplicitActions = useCallback(() => {
askConfirmation({
title: t`Disable basic actions?`,
message: t`Disabling basic actions will also remove any buttons that use these actions. Are you sure you want to continue?`,
confirmButtonText: t`Disable`,
onConfirm: () => {
implicitActions.forEach(action => {
onDeleteAction(action);
});
},
});
}, [implicitActions, askConfirmation, onDeleteAction]);
const menuItems = useMemo(() => {
return [
{
const items = [];
const hasImplicitActions = implicitActions.length > 0;
if (hasImplicitActions) {
items.push({
title: t`Disable basic actions`,
icon: "bolt",
action: onDeleteImplicitActions,
});
} else {
items.push({
title: t`Create basic actions`,
icon: "bolt",
action: onEnableImplicitActions,
},
];
}, [onEnableImplicitActions]);
});
}
return items;
}, [implicitActions, onEnableImplicitActions, onDeleteImplicitActions]);
const renderActionListItem = useCallback(
(action: WritebackAction) => {
......@@ -104,13 +140,11 @@ function ModelActionDetails({
{canWrite && (
<ActionsHeader>
<Button as={Link} to={newActionUrl}>{t`New action`}</Button>
{!hasImplicitActions && (
<ActionMenu
triggerIcon="ellipsis"
items={menuItems}
triggerProps={ACTION_MENU_TRIGGER_PROPS}
/>
)}
<ActionMenu
triggerIcon="ellipsis"
items={menuItems}
triggerProps={{ "aria-label": t`Actions menu` }}
/>
</ActionsHeader>
)}
{database && !hasActionsEnabled && (
......@@ -128,6 +162,7 @@ function ModelActionDetails({
onCreateClick={onEnableImplicitActions}
/>
)}
{confirmationModal}
</Root>
);
}
......@@ -160,10 +195,6 @@ function mostRecentFirst(action: WritebackAction) {
return -createdAt.unix();
}
const ACTION_MENU_TRIGGER_PROPS = {
"data-testid": "new-action-menu",
};
export default _.compose(
Actions.loadList({
query: (state: State, { model }: OwnProps) => ({
......
......@@ -23,6 +23,7 @@ import {
import { checkNotNull } from "metabase/core/utils/types";
import { ActionsApi } from "metabase/services";
import Actions from "metabase/entities/actions";
import Models from "metabase/entities/questions";
import { ModalRoute } from "metabase/hoc/ModalRoute";
import { getMetadata } from "metabase/selectors/metadata";
......@@ -605,7 +606,7 @@ describe("ModelDetailPage", () => {
const action = createMockQueryAction({ model_id: model.id() });
await setupActions({ model, actions: [action] });
userEvent.click(screen.getByTestId("new-action-menu"));
userEvent.click(screen.getByLabelText("Actions menu"));
userEvent.click(screen.getByText("Create basic actions"));
await waitFor(() => {
......@@ -668,12 +669,11 @@ describe("ModelDetailPage", () => {
actions: createMockImplicitCUDActions(model.id()),
});
userEvent.click(screen.getByLabelText("Actions menu"));
expect(
screen.queryByText(/Create basic action/i),
).not.toBeInTheDocument();
expect(
screen.queryByTestId("new-action-menu"),
).not.toBeInTheDocument();
});
it("allows to archive a query action", async () => {
......@@ -709,6 +709,32 @@ describe("ModelDetailPage", () => {
expect(screen.queryByText("Archive")).not.toBeInTheDocument();
});
it("allows to disable implicit actions", async () => {
const deleteActionSpy = jest.spyOn(Actions.actions, "delete");
const model = getModel();
const actions = createMockImplicitCUDActions(model.id());
await setupActions({ model, actions });
userEvent.click(screen.getByLabelText("Actions menu"));
userEvent.click(screen.getByText("Disable basic actions"));
userEvent.click(screen.getByRole("button", { name: "Disable" }));
actions.forEach(action => {
expect(deleteActionSpy).toHaveBeenCalledWith({ id: action.id });
});
});
it("doesn't allow to disable implicit actions if they don't exist", async () => {
const model = getModel();
await setupActions({ model, actions: [] });
userEvent.click(screen.getByLabelText("Actions menu"));
expect(
screen.queryByText("Disable basic actions"),
).not.toBeInTheDocument();
});
});
describe("read-only permissions", () => {
......@@ -750,9 +776,7 @@ describe("ModelDetailPage", () => {
expect(
screen.queryByText("Create basic actions"),
).not.toBeInTheDocument();
expect(
screen.queryByTestId("new-action-menu"),
).not.toBeInTheDocument();
expect(screen.queryByTestId("actions-menu")).not.toBeInTheDocument();
});
it("doesn't allow to edit actions", async () => {
......
......@@ -8,6 +8,7 @@ import {
export function setupActionEndpoints(scope: Scope, action: WritebackAction) {
scope.get(`/api/action/${action.id}`).reply(200, action);
scope.put(`/api/action/${action.id}`).reply(200, action);
scope.delete(`/api/action/${action.id}`).reply(200, action);
}
export function setupActionsEndpoints(
......
......@@ -130,6 +130,17 @@ describe("scenarios > models > actions", () => {
});
cy.findByRole("listitem", { name: "Delete Order" }).should("not.exist");
cy.findByLabelText("Actions menu").click();
popover().findByText("Disable basic actions").click();
modal().within(() => {
cy.findByText("Disable basic actions?").should("be.visible");
cy.button("Disable").click();
});
cy.findByLabelText("Action list").should("not.exist");
cy.findByText("Create").should("not.exist");
cy.findByText("Update").should("not.exist");
cy.findByText("Delete").should("not.exist");
});
it("should allow to execute actions from the model page", () => {
......
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