All files / src/pages/flowsheet-page/flowsheet/Canvas BreadCrumbs.tsx

73.23% Statements 52/71
64.7% Branches 33/51
44.44% Functions 4/9
75.43% Lines 43/57

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158                              103x 103x   960x             960x 960x   960x 960x 960x   960x 11x 11x             11x 11x   11x   3615x       960x     2740x   126x 960x 1086x 126x       1086x 1212x 4520x       2348x 2348x   2348x 3284x 2348x   2348x 245x   245x                                                               245x 245x         245x 245x 245x   245x 245x 133x                                                       10127x 2838x 3083x          
import { useReactFlow } from "@xyflow/react";
import { MoreHorizontal } from "lucide-react";
import { useSearchParams } from "react-router-dom";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/ahuora-design-system/ui/dropdown-menu";
import { Separator } from "@/ahuora-design-system/ui/separator";
import type { Breadcrumbs } from "@/api/apiStore.gen";
import { useBreadcrumbs } from "@/hooks/flowsheetObjects";
import { useFlowsheetRevisionPreview } from "@/hooks/flowsheetRevisionPreview";
import { ContentTypes } from "../LeftSideBar/LeftSideBarTabDefinitions";
 
const MAX_VISIBLE_BREADCRUMBS = 8; // Total visible breadcrumbs
const CONTEXT_AROUND_SELECTED = 3; // Number of items to show before and after the selected breadcrumb
 
const BreadcrumbButton = ({
  crumb,
  index,
}: {
  crumb: Breadcrumbs;
  index: number;
}) => {
  const [searchParams, setSearchParams] = useSearchParams();
  const { isPreview } = useFlowsheetRevisionPreview();
 
  const label = crumb.name;
  const isSelected = crumb.groupId === +searchParams.get("parentGroup")!;
  const reactFlow = useReactFlow();
 
  const handleBreadcrumbClick = (crumb: Breadcrumbs) => {
    const newSearchParams = new URLSearchParams(searchParams);
    newSearchParams.set("parentGroup", crumb.groupId.toString());
    if (isPreview) {
      // Historical navigation stays on the read-only canvas; object details
      // are a working-state surface and may expose unsupported actions.
      newSearchParams.delete("object");
      newSearchParams.set("content", ContentTypes.flowsheet);
    } else {
      newSearchParams.set("object", crumb.simulationObjectId.toString());
      newSearchParams.set("content", ContentTypes.objectDetails);
    }
    setSearchParams(newSearchParams);
    // reactFlow.fitView();
  };
 
  return (
    <div
      className={`flex items-center gap-1 px-2 py-1 cursor-pointer rounded-md ${
        isSelected ? "bg-secondary" : "hover:bg-secondary"
      }`}
      onClick={() => handleBreadcrumbClick(crumb)}
    >
      <div
        className={`h-1.5 w-1.5 rotate-45 ${index === 0 ? "bg-sky-500" : "bg-emerald-500"}`}
      />
      <span
        className="text-xs font-normal text-card-foreground truncate max-w-[100px]"
        title={label}
      >
        {label?.length > 10 ? `${label?.slice(0, 10)}...` : label}
      </span>
    </div>
  );
};
 
const BreadCrumbs = () => {
  const breadcrumbs = useBreadcrumbs();
 
  const [searchParams] = useSearchParams();
  const currentGroupId = +(searchParams.get("parentGroup") || 0);
  const showDropdown = breadcrumbs.length > MAX_VISIBLE_BREADCRUMBS;
 
  const currentIndex = breadcrumbs.indexOf(currentGroupId);
  let visibleBreadcrumbs: Breadcrumbs[] = [];
  if (!showDropdown) {
    visibleBreadcrumbs = breadcrumbs;
  } else {
    // Always include the first and last breadcrumbs
    const firstBreadcrumb = breadcrumbs[0];
    const lastBreadcrumb = breadcrumbs[breadcrumbs.length - 1];
 
    // Calculate range around the selected breadcrumb
    const startIndex = Math.max(1, currentIndex - CONTEXT_AROUND_SELECTED); // Start after the first breadcrumb
    const endIndex = Math.min(
      breadcrumbs.length - 1,
      currentIndex + CONTEXT_AROUND_SELECTED + 1,
    ); // End before the last breadcrumb
 
    const visibleRange = breadcrumbs.slice(startIndex, endIndex);
 
    // Ensure breadcrumbs fit within the max limit, including first and last
    visibleBreadcrumbs = [firstBreadcrumb, ...visibleRange, lastBreadcrumb];
 
    // If too many breadcrumbs, adjust by trimming the middle
    if (visibleBreadcrumbs.length > MAX_VISIBLE_BREADCRUMBS) {
      const excess = visibleBreadcrumbs.length - MAX_VISIBLE_BREADCRUMBS;
      const middleIndex = Math.floor(visibleRange.length / 2);
      visibleBreadcrumbs = [
        firstBreadcrumb,
        ...visibleRange.slice(0, middleIndex - Math.ceil(excess / 2)),
        ...visibleRange.slice(middleIndex + Math.floor(excess / 2)),
        lastBreadcrumb,
      ];
    }
  }
 
  // Ensure no duplicates
  visibleBreadcrumbs = Array.from(new Set(visibleBreadcrumbs));
  const hiddenBreadcrumbs = !showDropdown
    ? []
    : visibleBreadcrumbs.filter((crumb) => !visibleBreadcrumbs.includes(crumb));
 
  return (
    <nav
      aria-label="Breadcrumb"
      className="grid grid-cols-1 gap-2 p-2 rounded-md bg-card shadow border select-none"
    >
      <div className="flex flex-wrap items-center gap-2">
        {visibleBreadcrumbs.map((crumb, index) => (
          <div key={index} className="flex items-center">
            <BreadcrumbButton crumb={crumb} index={index} />
            {index < visibleBreadcrumbs.length - 1 && (
              <>
                {hiddenBreadcrumbs.length > 0 &&
                index ===
                  visibleBreadcrumbs.length - CONTEXT_AROUND_SELECTED - 1 ? (
                  <DropdownMenu>
                    <DropdownMenuTrigger className="px-1 hover:bg-muted rounded">
                      <MoreHorizontal className="h-4 w-4 text-muted-foreground" />
                    </DropdownMenuTrigger>
                    <DropdownMenuContent
                      align="start"
                      className="bg-card border"
                    >
                      {hiddenBreadcrumbs.map((hiddenCrumb, hiddenIndex) => (
                        <DropdownMenuItem key={hiddenCrumb}>
                          <BreadcrumbButton crumb={hiddenCrumb} index={-1} />
                        </DropdownMenuItem>
                      ))}
                    </DropdownMenuContent>
                  </DropdownMenu>
                ) : (
                  <Separator orientation="vertical" className="bg-muted" />
                )}
              </>
            )}
          </div>
        ))}
      </div>
    </nav>
  );
};
 
export default BreadCrumbs;