fork refine

This commit is contained in:
Stefan Pejcic
2024-02-05 10:23:04 +01:00
parent 3fffde9a8f
commit 8496a83edb
3634 changed files with 715528 additions and 2 deletions

View File

@@ -0,0 +1,59 @@
import React from "react";
import { render, TestWrapper } from "@test";
import { ThemedHeader } from "./index";
import { AuthProvider, LegacyAuthProvider } from "@refinedev/core";
const mockLegacyAuthProvider: LegacyAuthProvider = {
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
getPermissions: () => Promise.resolve(["admin"]),
getUserIdentity: () =>
Promise.resolve({ name: "John Doe", avatar: "localhost:3000" }),
};
const mockAuthProvider: AuthProvider = {
login: () =>
Promise.resolve({
success: true,
}),
logout: () =>
Promise.resolve({
success: true,
}),
onError: () => Promise.resolve({}),
check: () =>
Promise.resolve({
authenticated: true,
}),
getIdentity: () =>
Promise.resolve({ name: "John Doe", avatar: "localhost:3000" }),
};
describe("ThemedHeader", () => {
it("should render successfull user name and avatar fallback in header", async () => {
const { findByText, getByTitle } = render(<ThemedHeader />, {
wrapper: TestWrapper({
authProvider: mockAuthProvider,
}),
});
await findByText("John Doe");
getByTitle("John Doe");
});
});
// NOTE: Will be removed in the refine v5
describe("ThemedHeader with legacyAuthProvider", () => {
it("should render successfull user name and avatar fallback in header", async () => {
const { findByText, getByTitle } = render(<ThemedHeader />, {
wrapper: TestWrapper({
legacyAuthProvider: mockLegacyAuthProvider,
}),
});
await findByText("John Doe");
getByTitle("John Doe");
});
});

View File

@@ -0,0 +1,58 @@
import React from "react";
import { useGetIdentity, useActiveAuthProvider } from "@refinedev/core";
import {
Avatar,
Group,
Header as MantineHeader,
Title,
useMantineTheme,
} from "@mantine/core";
import { RefineThemedLayoutHeaderProps } from "../types";
/**
* @deprecated It is recommended to use the improved `ThemedLayoutV2`. Review migration guidelines. https://refine.dev/docs/api-reference/mantine/components/mantine-themed-layout/#migrate-themedlayout-to-themedlayoutv2
*/
export const ThemedHeader: React.FC<RefineThemedLayoutHeaderProps> = () => {
const theme = useMantineTheme();
const authProvider = useActiveAuthProvider();
const { data: user } = useGetIdentity({
v3LegacyAuthProviderCompatible: Boolean(authProvider?.isLegacy),
});
const borderColor =
theme.colorScheme === "dark"
? theme.colors.dark[6]
: theme.colors.gray[2];
return (
<MantineHeader
zIndex={199}
height={64}
py={6}
px="sm"
sx={{
borderBottom: `1px solid ${borderColor}`,
}}
>
<Group
position="right"
align="center"
sx={{
height: "100%",
}}
>
<Title
order={6}
sx={{
cursor: "pointer",
}}
>
{user?.name}
</Title>
<Avatar src={user?.avatar} alt={user?.name} radius="xl" />
</Group>
</MantineHeader>
);
};

View File

@@ -0,0 +1,6 @@
import { layoutLayoutTests } from "@refinedev/ui-tests";
import { ThemedLayout } from "./index";
describe("ThemedLayout", () => {
layoutLayoutTests.bind(this)(ThemedLayout);
});

View File

@@ -0,0 +1,47 @@
import React from "react";
import { Box } from "@mantine/core";
import { RefineThemedLayoutProps } from "./types";
import { ThemedSider as DefaultSider } from "./sider";
import { ThemedHeader as DefaultHeader } from "./header";
/**
* @deprecated It is recommended to use the improved `ThemedLayoutV2`. Review migration guidelines. https://refine.dev/docs/api-reference/mantine/components/mantine-themed-layout/#migrate-themedlayout-to-themedlayoutv2
*/
export const ThemedLayout: React.FC<RefineThemedLayoutProps> = ({
Sider,
Header,
Title,
Footer,
OffLayoutArea,
children,
}) => {
const SiderToRender = Sider ?? DefaultSider;
const HeaderToRender = Header ?? DefaultHeader;
return (
<Box sx={{ display: "flex" }}>
<SiderToRender Title={Title} />
<Box
sx={{
display: "flex",
flexDirection: "column",
flex: 1,
overflow: "auto",
}}
>
<HeaderToRender />
<Box
component="main"
sx={(theme) => ({
padding: theme.spacing.sm,
})}
>
{children}
</Box>
{Footer && <Footer />}
</Box>
{OffLayoutArea && <OffLayoutArea />}
</Box>
);
};

View File

@@ -0,0 +1,359 @@
import React, { useState } from "react";
import {
CanAccess,
ITreeMenu,
useIsExistAuthentication,
useLink,
useLogout,
useMenu,
useActiveAuthProvider,
useRefineContext,
useRouterContext,
useRouterType,
useTitle,
useTranslate,
useWarnAboutChange,
} from "@refinedev/core";
import {
ActionIcon,
Box,
Drawer,
Navbar,
NavLink,
NavLinkStylesNames,
NavLinkStylesParams,
ScrollArea,
MediaQuery,
Button,
Tooltip,
TooltipProps,
Styles,
useMantineTheme,
Flex,
} from "@mantine/core";
import {
IconList,
IconMenu2,
IconIndentDecrease,
IconIndentIncrease,
IconPower,
IconDashboard,
} from "@tabler/icons";
import { RefineThemedLayoutSiderProps } from "../types";
import { ThemedTitle as DefaultTitle } from "@components";
const defaultNavIcon = <IconList size={20} />;
/**
* @deprecated It is recommended to use the improved `ThemedLayoutV2`. Review migration guidelines. https://refine.dev/docs/api-reference/mantine/components/mantine-themed-layout/#migrate-themedlayout-to-themedlayoutv2
*/
export const ThemedSider: React.FC<RefineThemedLayoutSiderProps> = ({
render,
meta,
Title: TitleFromProps,
}) => {
const theme = useMantineTheme();
const [collapsed, setCollapsed] = useState(false);
const [opened, setOpened] = useState(false);
const routerType = useRouterType();
const NewLink = useLink();
const { Link: LegacyLink } = useRouterContext();
const Link = routerType === "legacy" ? LegacyLink : NewLink;
const { defaultOpenKeys, menuItems, selectedKey } = useMenu({ meta });
const TitleFromContext = useTitle();
const isExistAuthentication = useIsExistAuthentication();
const t = useTranslate();
const { hasDashboard } = useRefineContext();
const authProvider = useActiveAuthProvider();
const { warnWhen, setWarnWhen } = useWarnAboutChange();
const { mutate: mutateLogout } = useLogout({
v3LegacyAuthProviderCompatible: Boolean(authProvider?.isLegacy),
});
const RenderToTitle = TitleFromProps ?? TitleFromContext ?? DefaultTitle;
const drawerWidth = () => {
if (collapsed) return 80;
return 200;
};
const borderColor =
theme.colorScheme === "dark"
? theme.colors.dark[6]
: theme.colors.gray[2];
const commonNavLinkStyles: Styles<NavLinkStylesNames, NavLinkStylesParams> =
{
root: {
display: "flex",
marginTop: "12px",
justifyContent: collapsed && !opened ? "center" : "flex-start",
},
icon: {
marginRight: collapsed && !opened ? 0 : 12,
},
body: {
display: collapsed && !opened ? "none" : "flex",
},
};
const commonTooltipProps: Partial<TooltipProps> = {
disabled: !collapsed || opened,
position: "right",
withinPortal: true,
withArrow: true,
arrowSize: 8,
arrowOffset: 12,
offset: 4,
};
const renderTreeView = (tree: ITreeMenu[], selectedKey?: string) => {
return tree.map((item) => {
const { icon, label, route, name, children } = item;
const isSelected = item.key === selectedKey;
const isParent = children.length > 0;
const additionalLinkProps = isParent
? {}
: { component: Link as any, to: route };
return (
<CanAccess
key={item.key}
resource={name.toLowerCase()}
action="list"
params={{
resource: item,
}}
>
<Tooltip label={label} {...commonTooltipProps}>
<NavLink
key={item.key}
label={collapsed && !opened ? null : label}
icon={icon ?? defaultNavIcon}
active={isSelected}
childrenOffset={collapsed && !opened ? 0 : 12}
defaultOpened={defaultOpenKeys.includes(
item.key || "",
)}
pl={collapsed || opened ? "12px" : "18px"}
styles={commonNavLinkStyles}
{...additionalLinkProps}
>
{isParent && renderTreeView(children, selectedKey)}
</NavLink>
</Tooltip>
</CanAccess>
);
});
};
const items = renderTreeView(menuItems, selectedKey);
const dashboard = hasDashboard ? (
<CanAccess resource="dashboard" action="list">
<Tooltip
label={t("dashboard.title", "Dashboard")}
{...commonTooltipProps}
>
<NavLink
key="dashboard"
label={
collapsed && !opened
? null
: t("dashboard.title", "Dashboard")
}
icon={<IconDashboard size={20} />}
component={Link as any}
to="/"
active={selectedKey === "/"}
styles={commonNavLinkStyles}
/>
</Tooltip>
</CanAccess>
) : null;
const handleLogout = () => {
if (warnWhen) {
const confirm = window.confirm(
t(
"warnWhenUnsavedChanges",
"Are you sure you want to leave? You have unsaved changes.",
),
);
if (confirm) {
setWarnWhen(false);
mutateLogout();
}
} else {
mutateLogout();
}
};
const logout = isExistAuthentication && (
<Tooltip label={t("buttons.logout", "Logout")} {...commonTooltipProps}>
<NavLink
key="logout"
label={
collapsed && !opened ? null : t("buttons.logout", "Logout")
}
icon={<IconPower size={20} />}
pl={collapsed || opened ? "12px" : "18px"}
onClick={handleLogout}
styles={commonNavLinkStyles}
/>
</Tooltip>
);
const renderSider = () => {
if (render) {
return render({
dashboard,
logout,
items,
collapsed,
});
}
return (
<>
{dashboard}
{items}
{logout}
</>
);
};
return (
<>
<MediaQuery largerThan="md" styles={{ display: "none" }}>
<Box
sx={{ position: "fixed", top: 16, left: 16, zIndex: 1199 }}
>
<ActionIcon
color="gray"
variant="filled"
size={32}
onClick={() => setOpened((prev) => !prev)}
>
<IconMenu2 />
</ActionIcon>
</Box>
</MediaQuery>
<MediaQuery largerThan="md" styles={{ display: "none" }}>
<Drawer
opened={opened}
onClose={() => setOpened(false)}
size={200}
zIndex={1200}
withCloseButton={false}
styles={{
drawer: {
overflow: "hidden",
},
}}
>
<Navbar.Section
pl={8}
sx={{
height: "64px",
display: "flex",
alignItems: "center",
paddingLeft: "10px",
borderBottom: `1px solid ${borderColor}`,
}}
>
<RenderToTitle collapsed={false} />
</Navbar.Section>
<Navbar.Section
component={ScrollArea}
grow
mx="-xs"
px="xs"
>
{renderSider()}
</Navbar.Section>
</Drawer>
</MediaQuery>
<MediaQuery smallerThan="md" styles={{ display: "none" }}>
<Box
sx={{
width: drawerWidth(),
transition: "width 200ms ease, min-width 200ms ease",
flexShrink: 0,
}}
/>
</MediaQuery>
<MediaQuery smallerThan="md" styles={{ display: "none" }}>
<Navbar
width={{ base: drawerWidth() }}
sx={{
overflow: "hidden",
transition: "width 200ms ease, min-width 200ms ease",
position: "fixed",
top: 0,
height: "100vh",
borderRight: 0,
zIndex: 199,
}}
>
<Flex
h="64px"
pl={collapsed ? 0 : "16px"}
align="center"
justify={collapsed ? "center" : "flex-start"}
sx={{
borderBottom: `1px solid ${borderColor}`,
}}
>
<RenderToTitle collapsed={collapsed} />
</Flex>
<Navbar.Section
grow
component={ScrollArea}
mx="-xs"
px="xs"
sx={{
".mantine-ScrollArea-viewport": {
borderRight: `1px solid ${borderColor}`,
borderBottom: `1px solid ${borderColor}`,
},
}}
>
{renderSider()}
</Navbar.Section>
<Navbar.Section
sx={{
borderRadius: 0,
borderRight: `1px solid ${borderColor}`,
}}
>
<Button
variant="subtle"
color="gray"
size="md"
fullWidth
sx={{
border: "none",
}}
onClick={() => setCollapsed((prev) => !prev)}
>
{collapsed ? (
<IconIndentIncrease size={16} />
) : (
<IconIndentDecrease size={16} />
)}
</Button>
</Navbar.Section>
</Navbar>
</MediaQuery>
</>
);
};

View File

@@ -0,0 +1,6 @@
import { layoutTitleTests } from "@refinedev/ui-tests";
import { ThemedTitle } from "./index";
describe("ThemedTitleTitle", () => {
layoutTitleTests.bind(this)(ThemedTitle);
});

View File

@@ -0,0 +1,76 @@
import React from "react";
import { useRouterContext, useRouterType, useLink } from "@refinedev/core";
import { Center, Text, useMantineTheme } from "@mantine/core";
import { RefineLayoutThemedTitleProps } from "../types";
const defaultText = "refine Project";
const defaultIcon = (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
data-testid="refine-logo"
>
<path
d="M12 9C13.6569 9 15 7.65685 15 6C15 4.34315 13.6569 3 12 3C10.3431 3 9 4.34315 9 6C9 7.65685 10.3431 9 12 9Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M24 12C24 18.6274 18.6274 24 12 24C5.37258 24 0 18.6274 0 12C0 5.37258 5.37258 0 12 0C18.6274 0 24 5.37258 24 12ZM8 6C8 3.79086 9.79086 2 12 2C14.2091 2 16 3.79086 16 6V18C16 20.2091 14.2091 22 12 22C9.79086 22 8 20.2091 8 18V6Z"
fill="currentColor"
/>
</svg>
);
/**
* @deprecated It is recommended to use the improved `ThemedLayoutV2`. Review migration guidelines. https://refine.dev/docs/api-reference/mantine/components/mantine-themed-layout/#migrate-themedlayout-to-themedlayoutv2
*/
export const ThemedTitle: React.FC<RefineLayoutThemedTitleProps> = ({
collapsed,
icon = defaultIcon,
text = defaultText,
wrapperStyles = {},
}) => {
const theme = useMantineTheme();
const routerType = useRouterType();
const Link = useLink();
const { Link: LegacyLink } = useRouterContext();
const ActiveLink = routerType === "legacy" ? LegacyLink : Link;
return (
<ActiveLink to="/" style={{ all: "unset" }}>
<Center
style={{
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: collapsed ? "center" : "flex-start",
gap: "8px",
...wrapperStyles,
}}
>
<Text
lh={0}
fz="inherit"
color={theme.colorScheme === "dark" ? "brand.5" : "brand.6"}
>
{icon}
</Text>
{!collapsed && (
<Text
fz="inherit"
color={theme.colorScheme === "dark" ? "white" : "black"}
>
{text}
</Text>
)}
</Center>
</ActiveLink>
);
};

View File

@@ -0,0 +1,13 @@
import type {
RefineThemedLayoutSiderProps,
RefineThemedLayoutHeaderProps,
RefineThemedLayoutProps,
RefineLayoutThemedTitleProps,
} from "@refinedev/ui-types";
export type {
RefineThemedLayoutSiderProps,
RefineThemedLayoutHeaderProps,
RefineThemedLayoutProps,
RefineLayoutThemedTitleProps,
};