pankod/refine

View on GitHub
packages/core/src/hooks/menu/useMenu.tsx

Summary

Maintainability
B
4 hrs
Test Coverage
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,
  };
};