packages/core/src/hooks/menu/useMenu.tsx
import React from "react";
import { pickNotDeprecated, useUserFriendlyName } from "@definitions";
import { getParentResource } from "@definitions/helpers/router";
import { useParsed, useResource, useRouterContext, useTranslate } from "..";
import { useRouterType } from "../../contexts/router/picker";
import { createResourceKey } from "../../definitions/helpers/menu/create-resource-key";
import {
FlatTreeItem,
createTree,
} from "../../definitions/helpers/menu/create-tree";
import { useGetToPath } from "../router/use-get-to-path/index";
type UseMenuReturnType = {
defaultOpenKeys: string[];
selectedKey: string;
menuItems: TreeMenuItem[];
};
export type UseMenuProps = {
meta?: Record<string, any>;
hideOnMissingParameter?: boolean;
};
export type TreeMenuItem = FlatTreeItem & {
route?: string;
icon?: React.ReactNode;
label?: string;
children: TreeMenuItem[];
};
const getCleanPath = (pathname: string) => {
return pathname
.split("?")[0]
.split("#")[0]
.replace(/(.+)(\/$)/, "$1");
};
/**
* `useMenu` is used to get menu items of the default sidebar.
* These items include a link to dashboard page (if it exists) and links to the user defined resources
* (passed as children to {@link https://refine.dev/docs/core/components/refine-config `<Refine>`}).
* This hook can also be used to build custom menus, which is also used by default sidebar to show menu items.
*
* @see {@link https://refine.dev/docs/api-reference/core/hooks/ui/useMenu} for more details.
*/
export const useMenu = (
{ meta, hideOnMissingParameter }: UseMenuProps = {
hideOnMissingParameter: true,
},
): UseMenuReturnType => {
const translate = useTranslate();
const getToPath = useGetToPath();
const routerType = useRouterType();
const { resource, resources } = useResource();
const { pathname } = useParsed();
const { useLocation } = useRouterContext();
const { pathname: legacyPath } = useLocation();
const getFriendlyName = useUserFriendlyName();
const cleanPathname =
routerType === "legacy"
? getCleanPath(legacyPath)
: pathname
? getCleanPath(pathname)
: undefined;
const cleanRoute = `/${(cleanPathname ?? "").replace(/^\//, "")}`;
const selectedKey = resource
? createResourceKey(resource, resources, routerType === "legacy")
: cleanRoute ?? "";
const defaultOpenKeys = React.useMemo(() => {
if (!resource) return [];
let parent = getParentResource(resource, resources);
const keys = [createResourceKey(resource, resources)];
while (parent) {
keys.push(createResourceKey(parent, resources));
parent = getParentResource(parent, resources);
}
return keys;
}, []);
const prepareItem = React.useCallback(
(item: FlatTreeItem): TreeMenuItem | undefined => {
if (item?.meta?.hide ?? item?.options?.hide) return undefined;
if (!item?.list && item.children.length === 0) return undefined;
const composed = item.list
? getToPath({
resource: item,
action: "list",
legacy: routerType === "legacy",
meta,
})
: undefined;
if (
hideOnMissingParameter &&
composed &&
composed.match(/(\/|^):(.+?)(\/|$){1}/)
)
return undefined;
return {
...item,
route: composed,
icon: pickNotDeprecated(item.meta?.icon, item.options?.icon, item.icon),
label:
pickNotDeprecated(item?.meta?.label, item?.options?.label) ??
translate(
`${item.name}.${item.name}`,
getFriendlyName(item.name, "plural"),
),
};
},
[routerType, meta, getToPath, translate, hideOnMissingParameter],
);
const treeItems = React.useMemo(() => {
const treeMenuItems = createTree(resources, routerType === "legacy");
// add paths to items and their nodes recursively
const prepare = (items: TreeMenuItem[]): TreeMenuItem[] => {
return items.flatMap((item) => {
const preparedNodes = prepare(item.children);
const newItem = prepareItem({
...item,
children: preparedNodes,
});
if (!newItem) return [];
return [newItem];
});
};
return prepare(treeMenuItems);
}, [resources, routerType, prepareItem]);
return {
defaultOpenKeys,
selectedKey,
menuItems: treeItems,
};
};