All files / src/pages/main-page/project-folders OwnedProjectsWorkspace.tsx

81.25% Statements 91/112
48.93% Branches 92/188
12.5% Functions 2/16
90.24% Lines 74/82

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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319                                                          103x             585x 585x 585x 585x 585x       585x   585x 585x                                             585x   585x   679x 585x           585x 94x   94x   773x       867x 782x     585x   679x 585x   76x       674x 779x 430x       283x 973x 585x   585x 76x               867x     585x 754x   585x   585x     190x     965x     585x 2925x 94x         89x 1170x     585x     679x 89x       585x       89x 674x     89x       674x 852x 585x   2340x 197x       782x                                     763x                                   1755x 585x           3510x 585x 585x           585x   4680x                                                   1001x     585x   585x 585x   937x 585x                                                                                       7605x 1755x 2340x 4680x      
import { Grid2x2, List, Plus } from "lucide-react";
import { motion } from "motion/react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { useLocalStorage } from "usehooks-ts";
import { Button } from "@/ahuora-design-system/ui/button";
import SortDropdown from "@/ahuora-design-system/ui/sort-dropdown";
import {
  ProjectRead,
  useCoreProjectsCreateMutation,
  useCoreProjectsListQuery,
} from "@/api/apiStore.gen";
import { useUserInfo } from "@/hooks/useUserInfo";
import CardGallery from "@/pages/main-page/components/CardGallery";
import { MainPageContents } from "@/pages/main-page/components/MainPageContentDefinitions";
import { ProjectPagination } from "@/pages/main-page/components/ProjectPagination";
import { buildProjectCardData } from "@/pages/main-page/components/projectCardData";
import {
  type ProjectViewMode,
  ROOT_PROJECT_FOLDER_ID,
} from "@/pages/main-page/project-folders/model";
import {
  CreateProjectFolderButton,
  ProjectFolderNavigation,
} from "@/pages/main-page/project-folders/ProjectFolderControls";
import { ProjectFolderGallery } from "@/pages/main-page/project-folders/ProjectFolderGallery";
import { useProjectFolderWorkspace } from "@/pages/main-page/project-folders/useProjectFolderWorkspace";
 
const ORDERING_BY_SORT = {
  "Recently Added": "recently_added",
  "Recently Edited": "recently_edited",
  "A-Z": "name",
} as const;
 
/** Own the Home page folder navigation, APIs, and project pagination boundary. */
export function OwnedProjectsWorkspace() {
  const nav = useNavigate();
  const { data: userInfo } = useUserInfo();
  const [createProject] = useCoreProjectsCreateMutation();
  const [viewMode, setViewMode] = useLocalStorage<ProjectViewMode>(
    "page-view-mode",
    "grid",
  );
  const [projectPageByFolder, setProjectPageByFolder] = useState<
    Record<string, number>
  >({});
  const [sortOrder] = useLocalStorage(
    MainPageContents.home.sortStorageKey,
    MainPageContents.home.sortStorageValue,
  );
  const {
    currentFolderId,
    isMissing,
    isUnavailable,
    isLoading,
    canOrganize,
    folderIndex,
    currentFolders,
    navigateToFolder,
    createFolder,
    renameFolder,
    binFolder,
    moveProject,
    pendingProjectMoveIds,
    refetchFolders,
    folderPage,
    folderPageCount,
    folderCount,
    setFolderPage,
  } = useProjectFolderWorkspace({ enabled: true });
  const ordering =
    ORDERING_BY_SORT[sortOrder as keyof typeof ORDERING_BY_SORT] ??
    "recently_edited";
  const paginationKey = `${currentFolderId?.toString() ?? "root"}:${ordering}`;
  const projectPage = projectPageByFolder[paginationKey] ?? 1;
  const {
    currentData: projectResponse,
    isFetching: projectsLoading,
    isError: isProjectError,
    refetch: refetchProjects,
  } = useCoreProjectsListQuery(
    {
      type: "owned",
      folder: isUnavailable
        ? undefined
        : (currentFolderId?.toString() ?? "root"),
      isBinned: false,
      ordering,
      page: projectPage,
    },
    { skip: isLoading },
  );
 
  const setProjectPage = (page: number) => {
    setProjectPageByFolder((pages) => ({ ...pages, [paginationKey]: page }));
  };
  const openProject = (project: ProjectRead) => {
    if (project.active_flowsheet) {
      nav(`/project/${project.active_flowsheet}/flowsheet`);
    } else {
      toast.error("Project has no default flowsheet.");
    }
  };
  const projects = (projectResponse?.results ?? []).map((project) =>
    buildProjectCardData(project, {
      type: MainPageContents.home.type,
      folderId: project.folder ?? ROOT_PROJECT_FOLDER_ID,
      onMissingFlowsheet: () => openProject(project),
    }),
  );
  const activeProjectPage = Math.min(projectPage, projectResponse?.pages ?? 1);
 
  const createNewProject = () => {
    createProject({ project: { folder: currentFolderId } })
      .unwrap()
      .then(openProject)
      .catch((error) => {
        toast.error(
          "Failed to create project: " + (error.data?.detail || error.message),
        );
      });
  };
 
  return (
    <>
      <h1 className="text-3xl mt-16">Kia Ora, {userInfo?.name}!</h1>
      <div className="sticky top-0 z-20 -mx-[5%] flex items-center justify-between bg-background px-[5%] py-4">
        <h1 className="text-xl">My Projects</h1>
        <div className="flex gap-3">
          <CreateProjectFolderButton
            currentFolderId={currentFolderId}
            currentFolderLabel={
              currentFolderId === ROOT_PROJECT_FOLDER_ID
                ? "My Projects"
                : (folderIndex.foldersById.get(currentFolderId)?.name ??
                  "the current folder")
            }
            onCreate={createFolder}
            disabled={isLoading || isUnavailable || isMissing}
          />
          <Button asChild>
            <motion.button
              type="button"
              onClick={createNewProject}
              aria-label="Create a new Project"
              whileHover={{ scale: 1.05 }}
              whileTap={{ scale: 0.99 }}
              className="gap-2"
            >
              <Plus className="icon-small" />
              New Project
            </motion.button>
          </Button>
          <Button
            variant="tertiary"
            size="navIcon"
            aria-label={
              viewMode === "grid"
                ? "Switch to list view"
                : "Switch to grid view"
            }
            onClick={() =>
              setViewMode((mode) => (mode === "grid" ? "list" : "grid"))
            }
          >
            {viewMode === "grid" ? (
              <List className="icon-large" />
            ) : (
              <Grid2x2 className="icon-large" />
            )}
          </Button>
          <SortDropdown currentContent={MainPageContents.home} />
        </div>
      </div>
      {isLoading && (
        <p role="status" className="text-sm text-muted-foreground">
          Loading folders...
        </p>
      )}
      {isUnavailable && (
        <div
          role="alert"
          className="flex items-center justify-between gap-4 rounded-md border border-border bg-muted/30 px-4 py-3"
        >
          <p className="text-sm text-muted-foreground">
            Folders could not be loaded. You can still open the projects shown
            here.
          </p>
          <Button
            type="button"
            variant="outline"
            size="sm"
            onClick={refetchFolders}
          >
            Retry
          </Button>
        </div>
      )}
      {isMissing && (
        <div
          role="status"
          className="flex items-center justify-between gap-4 rounded-md border border-border bg-muted/30 px-4 py-3"
        >
          <p className="text-sm text-muted-foreground">
            That folder is no longer available. Showing My Projects instead.
          </p>
          <Button
            type="button"
            variant="outline"
            size="sm"
            onClick={() => navigateToFolder(ROOT_PROJECT_FOLDER_ID)}
          >
            Back to My Projects
          </Button>
        </div>
      )}
      {!isUnavailable && !isMissing && (
        <ProjectFolderNavigation
          currentFolderId={currentFolderId}
          folderIndex={folderIndex}
          onNavigate={navigateToFolder}
        />
      )}
      <div>
        <ProjectFolderGallery
          folders={currentFolders}
          viewMode={viewMode}
          onOpen={navigateToFolder}
          onRename={renameFolder}
          onBin={binFolder}
          onDropProject={canOrganize ? moveProject : undefined}
          pendingProjectMoveIds={pendingProjectMoveIds}
        />
        {folderPageCount > 1 && (
          <div className="mt-3 flex items-center justify-between text-sm text-muted-foreground">
            <span>{folderCount} folders</span>
            <div className="flex items-center gap-2">
              <Button
                variant="outline"
                size="sm"
                disabled={folderPage === 1}
                onClick={() => setFolderPage(folderPage - 1)}
              >
                Previous folders
              </Button>
              <span>
                Page {folderPage} of {folderPageCount}
              </span>
              <Button
                variant="outline"
                size="sm"
                disabled={folderPage === folderPageCount}
                onClick={() => setFolderPage(folderPage + 1)}
              >
                Next folders
              </Button>
            </div>
          </div>
        )}
        <div className="mt-5 flex flex-col gap-5">
          <div className="flex items-center justify-between">
            <h2 className="text-sm font-medium">Projects</h2>
            <p className="text-xs text-muted-foreground">
              {projectResponse?.count ?? 0}{" "}
              {projectResponse?.count === 1 ? "project" : "projects"}
            </p>
          </div>
          {isProjectError ? (
            <div
              role="alert"
              className="flex items-center justify-between gap-4 rounded-md border border-border bg-muted/30 px-4 py-3"
            >
              <p className="text-sm text-muted-foreground">
                Projects could not be loaded.
              </p>
              <Button
                type="button"
                variant="outline"
                size="sm"
                onClick={refetchProjects}
              >
                Retry
              </Button>
            </div>
          ) : projectsLoading && !projectResponse ? (
            <p role="status" className="text-sm text-muted-foreground">
              Loading projects...
            </p>
          ) : projects.length === 0 ? (
            <p className="italic text-muted-foreground">
              {currentFolders.length > 0
                ? "No projects in this folder"
                : "No projects or folders here"}
            </p>
          ) : (
            <>
              <CardGallery
                data={projects}
                viewMode={viewMode}
                onMoveProject={canOrganize ? moveProject : undefined}
                pendingProjectMoveIds={pendingProjectMoveIds}
              />
              <ProjectPagination
                currentPage={activeProjectPage}
                pageCount={projectResponse?.pages ?? 1}
                totalProjects={projectResponse?.count ?? 0}
                currentPageSize={projects.length}
                pageSize={projectResponse?.page_size ?? 20}
                onPageChange={setProjectPage}
              />
            </>
          )}
        </div>
      </div>
    </>
  );
}