Skip to content

Commit

Permalink
feat(core): button hooks (#5808)
Browse files Browse the repository at this point in the history
* chore(ui-tests): disable invalidation test from ui tests

* chore(core): accept mutation mode value in useMutationMode

* feat(core): add button hooks

* refactor(antd): refactor logics of buttons

* refactor(chakra-ui): refactor logics of buttons

* refactor(mantine): refactor logics of buttons

* refactor(mui): refactor logics of buttons

* chore: add changesets
  • Loading branch information
aliemir committed Apr 2, 2024
1 parent 9a080df commit 10ba9c3
Show file tree
Hide file tree
Showing 58 changed files with 1,769 additions and 1,974 deletions.
5 changes: 5 additions & 0 deletions .changeset/fair-avocados-cross.md
@@ -0,0 +1,5 @@
---
"@refinedev/core": patch
---

chore: improved `useMutationMode` hooks usage by accepting explicit values to be passed for `mutationMode` and `undoableTimeout`, handling the precedence of the values inside the hook rather than outside to avoid repetition
20 changes: 20 additions & 0 deletions .changeset/smart-yaks-run.md
@@ -0,0 +1,20 @@
---
"@refinedev/core": patch
---

feat: added headless button hooks

We've added a new set of hooks to make it easier to create and manage UI buttons of Refine. There's a hook for each type of button which previously had duplicated logic across the codebase between UI integrations of Refine. Now all these buttons will be powered by the same hooks maintained in the `@refinedev/core` package to ensure consistency and reduce duplication.

New Hooks:

- `useListButton`: A navigation button that navigates to the list page of a resource.
- `useCreateButton`: A navigation button that navigates to the create page of a resource.
- `useShowButton`: A navigation button that navigates to the show page of a record.
- `useEditButton`: A navigation button that navigates to the edit page of a record.
- `useCloneButton`: A navigation button that navigates to the clone page of a record.
- `useRefreshButton`: A button that triggers an invalidation of the cache of a record.
- `useDeleteButton`: A button that triggers a delete mutation on a record.
- `useSaveButton`: A button to be used inside a form to trigger a save mutation.
- `useExportButton`: A button to be used with `useExport` to trigger an export bulk data of a resource.
- `useImportButton`: A button to be used with `useImport` to trigger an import bulk data for a resource.
10 changes: 10 additions & 0 deletions .changeset/smooth-clouds-rule.md
@@ -0,0 +1,10 @@
---
"@refinedev/antd": patch
"@refinedev/chakra-ui": patch
"@refinedev/mantine": patch
"@refinedev/mui": patch
---

refactor: moved internal logic of buttons to respective hooks from `@refinedev/core`

We've moved the internal logic of buttons to their respective hooks in the `@refinedev/core` package to ensure consistency and reduce duplication. This change will make it easier to manage and maintain the buttons across different UI integrations of Refine. This will also benefit the users who want to customize the buttons via `swizzle` option or create their own buttons withouth having to duplicate the logic.
5 changes: 5 additions & 0 deletions .changeset/tiny-seas-hug.md
@@ -0,0 +1,5 @@
---
"@refinedev/ui-tests": patch
---

chore: updated refresh button tests to be more UI focused and hand off the logic to the `@refinedev/core`'s `useRefreshButton` hook
83 changes: 16 additions & 67 deletions packages/antd/src/components/buttons/clone/index.tsx
@@ -1,22 +1,13 @@
import React, { useContext } from "react";
import React from "react";
import { Button } from "antd";
import { PlusSquareOutlined } from "@ant-design/icons";
import {
useCan,
useNavigation,
useTranslate,
useResource,
useRouterContext,
useRouterType,
useLink,
AccessControlContext,
} from "@refinedev/core";
import { useCloneButton } from "@refinedev/core";
import {
RefineButtonTestIds,
RefineButtonClassNames,
} from "@refinedev/ui-types";

import { CloneButtonProps } from "../types";
import type { CloneButtonProps } from "../types";

/**
* `<CloneButton>` uses Ant Design's {@link https://ant.design/components/button/ `<Button> component`}.
Expand All @@ -36,63 +27,21 @@ export const CloneButton: React.FC<CloneButtonProps> = ({
onClick,
...rest
}) => {
const accessControlContext = useContext(AccessControlContext);

const accessControlEnabled =
accessControl?.enabled ??
accessControlContext.options.buttons.enableAccessControl;

const hideIfUnauthorized =
accessControl?.hideIfUnauthorized ??
accessControlContext.options.buttons.hideIfUnauthorized;

const { cloneUrl: generateCloneUrl } = useNavigation();
const routerType = useRouterType();
const Link = useLink();
const { Link: LegacyLink } = useRouterContext();

const ActiveLink = routerType === "legacy" ? LegacyLink : Link;

const translate = useTranslate();

const { id, resource } = useResource(
resourceNameFromProps ?? propResourceNameOrRouteName,
);

const { data } = useCan({
resource: resource?.name,
action: "create",
params: { id: recordItemId ?? id, resource },
queryOptions: {
enabled: accessControlEnabled,
},
const { to, LinkComponent, label, disabled, hidden, title } = useCloneButton({
id: recordItemId,
resource: resourceNameFromProps ?? propResourceNameOrRouteName,
accessControl,
meta,
});

const createButtonDisabledTitle = () => {
if (data?.can) return "";
if (data?.reason) return data.reason;

return translate(
"buttons.notAccessTitle",
"You don't have permission to access",
);
};

const cloneUrl =
resource && (recordItemId || id)
? generateCloneUrl(resource, recordItemId! ?? id!, meta)
: "";

if (accessControlEnabled && hideIfUnauthorized && !data?.can) {
return null;
}
if (hidden) return null;

return (
<ActiveLink
to={cloneUrl}
<LinkComponent
to={to}
replace={false}
onClick={(e: React.PointerEvent<HTMLButtonElement>) => {
if (data?.can === false) {
if (disabled) {
e.preventDefault();
return;
}
Expand All @@ -104,14 +53,14 @@ export const CloneButton: React.FC<CloneButtonProps> = ({
>
<Button
icon={<PlusSquareOutlined />}
disabled={data?.can === false}
title={createButtonDisabledTitle()}
disabled={disabled}
title={title}
data-testid={RefineButtonTestIds.CloneButton}
className={RefineButtonClassNames.CloneButton}
{...rest}
>
{!hideText && (children ?? translate("buttons.clone", "Clone"))}
{!hideText && (children ?? label)}
</Button>
</ActiveLink>
</LinkComponent>
);
};
83 changes: 17 additions & 66 deletions packages/antd/src/components/buttons/create/index.tsx
@@ -1,22 +1,13 @@
import React, { useContext } from "react";
import React from "react";
import { Button } from "antd";
import { PlusSquareOutlined } from "@ant-design/icons";
import {
useNavigation,
useTranslate,
useCan,
useResource,
useRouterContext,
useRouterType,
useLink,
AccessControlContext,
} from "@refinedev/core";
import { useCreateButton } from "@refinedev/core";
import {
RefineButtonClassNames,
RefineButtonTestIds,
} from "@refinedev/ui-types";

import { CreateButtonProps } from "../types";
import type { CreateButtonProps } from "../types";

/**
* <CreateButton> uses Ant Design's {@link https://ant.design/components/button/ `<Button> component`}.
Expand All @@ -35,62 +26,22 @@ export const CreateButton: React.FC<CreateButtonProps> = ({
onClick,
...rest
}) => {
const accessControlContext = useContext(AccessControlContext);

const accessControlEnabled =
accessControl?.enabled ??
accessControlContext.options.buttons.enableAccessControl;

const hideIfUnauthorized =
accessControl?.hideIfUnauthorized ??
accessControlContext.options.buttons.hideIfUnauthorized;

const translate = useTranslate();
const routerType = useRouterType();
const Link = useLink();
const { Link: LegacyLink } = useRouterContext();

const ActiveLink = routerType === "legacy" ? LegacyLink : Link;

const { createUrl: generateCreateUrl } = useNavigation();

const { resource } = useResource(
resourceNameFromProps ?? propResourceNameOrRouteName,
);

const { data } = useCan({
resource: resource?.name,
action: "create",
queryOptions: {
enabled: accessControlEnabled,
const { hidden, disabled, label, title, LinkComponent, to } = useCreateButton(
{
resource: resourceNameFromProps ?? propResourceNameOrRouteName,
accessControl,
meta,
},
params: {
resource,
},
});

const createButtonDisabledTitle = () => {
if (data?.can) return "";
if (data?.reason) return data.reason;

return translate(
"buttons.notAccessTitle",
"You don't have permission to access",
);
};

const createUrl = resource ? generateCreateUrl(resource, meta) : "";
);

if (accessControlEnabled && hideIfUnauthorized && !data?.can) {
return null;
}
if (hidden) return null;

return (
<ActiveLink
to={createUrl}
<LinkComponent
to={to}
replace={false}
onClick={(e: React.PointerEvent<HTMLButtonElement>) => {
if (data?.can === false) {
if (disabled) {
e.preventDefault();
return;
}
Expand All @@ -102,15 +53,15 @@ export const CreateButton: React.FC<CreateButtonProps> = ({
>
<Button
icon={<PlusSquareOutlined />}
disabled={data?.can === false}
title={createButtonDisabledTitle()}
disabled={disabled}
title={title}
data-testid={RefineButtonTestIds.CreateButton}
className={RefineButtonClassNames.CreateButton}
type="primary"
{...rest}
>
{!hideText && (children ?? translate("buttons.create", "Create"))}
{!hideText && (children ?? label)}
</Button>
</ActiveLink>
</LinkComponent>
);
};

0 comments on commit 10ba9c3

Please sign in to comment.