import { ReactNode, useMemo, useState } from "react"; import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons"; import { faArrowDownAZ, faArrowUpZA, faBorderAll, faCheckCircle, faList, faMagnifyingGlass, faPlus, faSearch, faStar as faSolidStar } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, IconButton, Input, Lottie, Pagination, Skeleton, Tooltip } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { getProjectHomePage, getProjectLottieIcon, getProjectTitle } from "@app/helpers/project"; import { getUserTablePreference, PreferenceKey, setUserTablePreference } from "@app/helpers/userTablePreferences"; import { usePagination, useResetPageHelper } from "@app/hooks"; import { useGetUserProjects } from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { Project, ProjectType } from "@app/hooks/api/projects/types"; import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; import { ProjectListToggle, ProjectListView } from "@app/pages/organization/ProjectsPage/components/ProjectListToggle"; type Props = { onAddNewProject: () => void; onUpgradePlan: () => void; isAddingProjectsAllowed: boolean; projectListView: ProjectListView; onProjectListViewChange: (value: ProjectListView) => void; }; enum ProjectOrderBy { Name = "name" } enum ProjectsViewMode { GRID = "grid", LIST = "list" } export const MyProjectView = ({ onAddNewProject, onUpgradePlan, isAddingProjectsAllowed, projectListView, onProjectListViewChange }: Props) => { const navigate = useNavigate(); const { currentOrg } = useOrganization(); const [projectTypeFilter, setProjectTypeFilter] = useState>>( {} ); const { data: workspaces = [], isPending: isWorkspaceLoading } = useGetUserProjects(); const { setPage, perPage, setPerPage, page, offset, limit, toggleOrderDirection, orderDirection } = usePagination(ProjectOrderBy.Name, { initPerPage: getUserTablePreference("myProjectsTable", PreferenceKey.PerPage, 24) }); const isTableFilteredByType = Boolean(Object.values(projectTypeFilter).some((el) => el)); const handlePerPageChange = (newPerPage: number) => { setPerPage(newPerPage); setUserTablePreference("myProjectsTable", PreferenceKey.PerPage, newPerPage); }; const { data: projectFavorites, isPending: isProjectFavoritesLoading } = useGetUserProjectFavorites(currentOrg?.id); const [projectsViewMode, setProjectsViewMode] = useState( (localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID ); const [searchFilter, setSearchFilter] = useState(""); const isProjectViewLoading = isWorkspaceLoading || isProjectFavoritesLoading; const isWorkspaceEmpty = !isProjectViewLoading && workspaces?.length === 0; const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites(); const filteredWorkspaces = useMemo( () => workspaces .filter((ws) => { if (isTableFilteredByType && !projectTypeFilter?.[ws.type]) { return false; } return ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()); }) .sort((a, b) => orderDirection === OrderByDirection.ASC ? a.name.toLowerCase().localeCompare(b.name.toLowerCase()) : b.name.toLowerCase().localeCompare(a.name.toLowerCase()) ), [searchFilter, orderDirection, workspaces, projectTypeFilter] ); useResetPageHelper({ setPage, offset, totalCount: filteredWorkspaces.length }); const { workspacesWithFaveProp } = useMemo(() => { const workspacesWithFav = filteredWorkspaces .map((w): Project & { isFavorite: boolean } => ({ ...w, isFavorite: Boolean(projectFavorites?.includes(w.id)) })) .sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite)) .slice(offset, limit * page); return { workspacesWithFaveProp: workspacesWithFav }; }, [filteredWorkspaces, projectFavorites, offset, limit, page]); const handleToggleFilterByProjectType = (type: ProjectType) => { setProjectTypeFilter((state) => { return { ...(state || {}), [type]: !state?.[type] }; }); }; const addProjectToFavorites = async (projectId: string) => { try { if (currentOrg?.id) { await updateUserProjectFavorites({ orgId: currentOrg?.id, projectFavorites: [...(projectFavorites || []), projectId] }); } } catch { createNotification({ text: "Failed to add project to favorites.", type: "error" }); } }; const removeProjectFromFavorites = async (projectId: string) => { try { if (currentOrg?.id) { await updateUserProjectFavorites({ orgId: currentOrg?.id, projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)] }); } } catch { createNotification({ text: "Failed to remove project from favorites.", type: "error" }); } }; const renderProjectGridItem = (workspace: Project, isFavorite: boolean) => ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
{ navigate({ to: getProjectHomePage(workspace.type, workspace.environments), params: { projectId: workspace.id } }); }} key={workspace.id} className="cursor-pointer overflow-clip rounded border border-l-[4px] border-mineshaft-600 border-l-mineshaft-400 bg-mineshaft-800 p-4 transition-transform duration-100 hover:scale-[103%] hover:border-l-primary hover:bg-mineshaft-700" >

{workspace.name}

{getProjectTitle(workspace.type)}

{isFavorite ? ( { e.stopPropagation(); removeProjectFromFavorites(workspace.id); }} /> ) : ( { e.stopPropagation(); addProjectToFavorites(workspace.id); }} /> )}

{workspace.description || "No description"}

); const renderProjectListItem = (workspace: Project, isFavorite: boolean, index: number) => ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
{ navigate({ to: getProjectHomePage(workspace.type, workspace.environments), params: { projectId: workspace.id } }); }} key={workspace.id} className={`group flex min-w-72 cursor-pointer border-l border-r border-t border-mineshaft-600 bg-mineshaft-800 px-6 py-3 hover:bg-mineshaft-700 ${ index === 0 && "rounded-t-md" }`} >

{workspace.name}

{getProjectTitle(workspace.type)}{" "} {workspace.description ? `- ${workspace.description}` : ""}

{isFavorite ? ( { e.stopPropagation(); removeProjectFromFavorites(workspace.id); }} /> ) : ( { e.stopPropagation(); addProjectToFavorites(workspace.id); }} /> )}
); let projectsComponents: ReactNode; if (filteredWorkspaces.length || isProjectViewLoading) { switch (projectsViewMode) { case ProjectsViewMode.GRID: projectsComponents = (
{isProjectViewLoading && Array.apply(0, Array(3)).map((_x, i) => (
))} {!isProjectViewLoading && ( <> {workspacesWithFaveProp.map((workspace) => renderProjectGridItem(workspace, workspace.isFavorite) )} )}
); break; case ProjectsViewMode.LIST: default: projectsComponents = (
{isProjectViewLoading && Array.apply(0, Array(3)).map((_x, i) => (
))} {!isProjectViewLoading && workspacesWithFaveProp.map((workspace, ind) => renderProjectListItem(workspace, workspace.isFavorite, ind) )}
); break; } } else if (workspaces.length && searchFilter) { projectsComponents = (
No projects match search...
); } else if (filteredWorkspaces.length === 0 && isTableFilteredByType) { projectsComponents = (
No projects match filters...
); } return (
setSearchFilter(e.target.value)} leftIcon={} />
Filter By Project Type {Object.values(ProjectType).map((el) => ( { e.preventDefault(); handleToggleFilterByProjectType(el); }} icon={projectTypeFilter?.[el] && } iconPos="right" >
{getProjectTitle(el)}
))}
{ localStorage.setItem("projectsViewMode", ProjectsViewMode.GRID); setProjectsViewMode(ProjectsViewMode.GRID); }} ariaLabel="grid" size="xs" className={`${ projectsViewMode === ProjectsViewMode.GRID ? "bg-mineshaft-500" : "bg-transparent" } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} > { localStorage.setItem("projectsViewMode", ProjectsViewMode.LIST); setProjectsViewMode(ProjectsViewMode.LIST); }} ariaLabel="list" size="xs" className={`${ projectsViewMode === ProjectsViewMode.LIST ? "bg-mineshaft-500" : "bg-transparent" } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} >
{(isOldProjectV1Allowed) => ( {(isAllowed) => ( )} )}
{projectsComponents} {!isProjectViewLoading && Boolean(filteredWorkspaces.length) && ( )} {isWorkspaceEmpty && !isTableFilteredByType && (
You are not part of any projects in this organization yet. When you are, they will appear here.
Create a new project, or ask other organization members to give you necessary permissions.
)}
); };