Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 140 additions & 86 deletions src/app/(loading-group)/[organizationSlug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,21 @@

import { Button } from "@/components/ui/button";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { FunctionComponent } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import Page from "../../../components/Page";

import { useActiveOrg } from "../../../hooks/useActiveOrg";
import { browserApiClient } from "../../../services/devGuardApi";
import type {
Paged,
ProjectDTO,
SubGroupsAndAsset,
} from "../../../types/api/api";
import { UserRole } from "../../../types/api/api";
import type { Paged, ProjectDTO } from "../../../types/api/api";
import type { CreateProjectReq } from "../../../types/api/req";

import ListItem from "@/components/common/ListItem";
import Section from "@/components/common/Section";
import {
Dialog,
Expand All @@ -38,31 +41,28 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Form } from "@/components/ui/form";
import { useOrganizationMenu } from "@/hooks/useOrganizationMenu";
import { toast } from "sonner";

import CustomPagination from "@/components/common/CustomPagination";
import { ProjectForm } from "@/components/project/ProjectForm";
import Sort from "@/components/Sort";
import SubgroupsAndAssetsList, {
checkType,
} from "@/components/SubgroupsAndAssetsList";
import { Input } from "@/components/ui/input";
import { useCurrentUserRole } from "@/hooks/useUserRole";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useUpdateOrganization } from "@/context/OrganizationContext";
import { useSession } from "@/context/SessionContext";
import { useCurrentUserRole } from "@/hooks/useUserRole";
import { buildFilterSearchParams } from "@/utils/url";
import { debounce } from "lodash";
import { Loader2 } from "lucide-react";
import Link from "next/link";
import useSWR from "swr";
import Avatar from "../../../components/Avatar";
import EmptyParty from "../../../components/common/EmptyParty";
import ListRenderer from "../../../components/common/ListRenderer";
import Markdown from "../../../components/common/Markdown";
import { ProjectBadge } from "../../../components/common/ProjectTitle";
import { fetcher } from "../../../data-fetcher/fetcher";
import EmptyParty from "../../../components/common/EmptyParty";
import useRouterQuery from "../../../hooks/useRouterQuery";
import { useUpdateOrganization } from "@/context/OrganizationContext";
import { Badge } from "@/components/ui/badge";
import { buildFilterSearchParams } from "@/utils/url";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import Sort from "@/components/Sort";

const OrganizationHomePage: FunctionComponent = () => {
const [viewedProject, setViewedProject] = useState<"all" | "inactive">("all");
Expand All @@ -81,6 +81,9 @@ const OrganizationHomePage: FunctionComponent = () => {
mode: "onBlur",
});

const searchQuery = searchParams?.get("search") ?? "";
const isSearchActive = searchQuery.length >= 3;

const queryWithState = useMemo(() => {
const p = buildFilterSearchParams(searchParams);
const state = searchParams?.get("state");
Expand All @@ -95,30 +98,38 @@ const OrganizationHomePage: FunctionComponent = () => {

const stillOnPage = useRef(true);
const pushQuery = useRouterQuery();

const swrUrl = isSearchActive
? `/organizations/${decodeURIComponent(activeOrg.slug)}/projects/search?${queryWithState.toString()}`
: `/organizations/${decodeURIComponent(activeOrg.slug)}/projects/?${queryWithState.toString()}`;

const {
isLoading,
data: projects,
error,
mutate,
} = useSWR<Paged<ProjectDTO>>(
activeOrg
? `/organizations/${decodeURIComponent(activeOrg.slug)}/projects/?${queryWithState.toString()}`
: null,
async (url: string) =>
fetcher<Paged<ProjectDTO>>(url).then((res) => {
return res;
}),
);
} = useSWR<Paged<SubGroupsAndAsset>>(swrUrl, async (url: string) => {
const data = await fetcher<Paged<ProjectDTO>>(url);
// we need to transform the data to add the resourceType field to each item, so that we can distinguish between projects and assets in the SubgroupsAndAssetsList component
return {
...data,
data: data.data.map((item) => ({
...item,
resourceType: "project",
})),
} as Paged<SubGroupsAndAsset>;
});

const debouncedHandleSearch = useCallback(
debounce((e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value === "") {
pushQuery({ search: undefined, page: "1" });
} else if (e.target.value.length >= 3) {
pushQuery({ search: e.target.value, page: "1" });
const value = e.target.value;
if (value === "") {
pushQuery({ search: undefined, page: 1 });
} else if (value.length >= 3) {
pushQuery({ search: value, page: 1 });
}
}, 500),
[],
[pushQuery],
);
Comment thread
refoo0 marked this conversation as resolved.

const handleSetTabValue = (value: string) => {
Expand Down Expand Up @@ -172,6 +183,61 @@ const OrganizationHomePage: FunctionComponent = () => {
mutate();
};

const handleLazyDataFetching = useCallback(
async (projectSlug: string, projectId: string) => {
const base = `/organizations/${decodeURIComponent(activeOrg.slug)}/projects/${decodeURIComponent(projectSlug)}/resources?parentId=${projectId}`;

const resp = await browserApiClient(base);
if (resp.ok) {
const data = await resp.json();
const subGroupsAndAsset = data as Paged<SubGroupsAndAsset>;

mutate(
(prev) => {
if (!prev) return prev;
// traverse the whole tree, find the correct project and update it with the new data
const recursiveFn = (
item: SubGroupsAndAsset,
): SubGroupsAndAsset => {
const { asset, subgroup } = checkType(item);
if (asset != null) {
return asset;
}

if (subgroup.id === projectId) {
return {
...subgroup,
subGroupsAndAsset: subGroupsAndAsset.data,
};
}

return {
...subgroup,
subGroupsAndAsset:
subgroup?.subGroupsAndAsset?.map(recursiveFn),
};
};

return {
...prev,
data: prev.data.map(recursiveFn) as Array<
ProjectDTO & {
resourceType: "project";
}
>,
};
},
{ revalidate: false },
);
} else {
toast.error(
"Failed to load subgroups and assets. Please try again later.",
);
}
},
[activeOrg.slug, mutate],
);

useEffect(() => {
// trigger a sync on page load - if the org has an external entity provider
if (activeOrg.externalEntityProviderId) {
Expand Down Expand Up @@ -269,17 +335,24 @@ const OrganizationHomePage: FunctionComponent = () => {
forceVertical
title="Groups"
>
<Tabs
defaultValue="all"
value={viewedProject}
onValueChange={handleSetTabValue}
>
<TabsList>
<TabsTrigger value="all">Groups</TabsTrigger>
<TabsTrigger value="inactive">Inactive</TabsTrigger>
</TabsList>
</Tabs>

<div className="flex items-center gap-4">
<Tabs
defaultValue="all"
value={viewedProject}
onValueChange={handleSetTabValue}
className={`${isSearchActive ? "pointer-events-none disabled" : ""}`}
>
<TabsList>
<TabsTrigger value="all">Groups</TabsTrigger>
<TabsTrigger value="inactive">Inactive</TabsTrigger>
</TabsList>
</Tabs>
{isSearchActive && (
<span className="text-xs text-yellow-600 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-950 border border-yellow-200 dark:border-yellow-800 rounded px-2 py-1">
Filter and sorting options are disabled while searching
</span>
)}
</div>
<div className="flex gap-2">
<Sort
sortOptions={[
Expand All @@ -293,55 +366,36 @@ const OrganizationHomePage: FunctionComponent = () => {
className="h-11"
onChange={debouncedHandleSearch}
defaultValue={searchParams?.get("search") || ""}
placeholder="Search for projects"
placeholder="Search for projects (min. 3 characters)..."
/>
</div>
<ListRenderer
isLoading={isLoading}
error={error}
data={projects?.data}
Empty={<EmptyParty title={"No groups found"} description="" />}
renderItem={(project) => (
<Link
key={project.id}
href={`/${activeOrg.slug}/projects/${project.slug}`}
className="flex flex-col gap-2 hover:no-underline"
>
<ListItem
reactOnHover
Title={
<div className="flex flex-row items-center gap-2">
<Avatar {...project} />
<span>{project.name}</span>
{project.state === "deleted" && (
<Badge variant={"destructive"}>Pending deletion</Badge>
)}
<div id="group-and-project-list">
<ListRenderer
isLoading={isLoading}
error={error}
data={projects?.data}
Empty={<EmptyParty title={"No groups found"} description="" />}
renderItem={(project) => {
return (
<div key={project.id} className="flex flex-col">
<div className="flex flex-col gap-2">
<SubgroupsAndAssetsList
project={
project as ProjectDTO & { resourceType: "project" }
}
onFetchData={handleLazyDataFetching}
subgroupsWithAssets={
(project as ProjectDTO & { resourceType: "project" })
.subGroupsAndAsset
}
projectSlug={project.slug}
/>
</div>
}
Description={
<div className="flex flex-col">
<span>
<Markdown
components={{
a: (props: React.ComponentPropsWithoutRef<"a">) => (
<span>{props.children}</span>
),
}}
>
{project.description}
</Markdown>
</span>
{project.type !== "default" && (
<div className="flex mt-4 flex-row items-center gap-2">
<ProjectBadge type={project.type} />
</div>
)}
</div>
}
/>
</Link>
)}
/>
</div>
);
}}
/>
</div>
</Section>
{projects && (
<div className="mt-4">
Expand Down
Loading
Loading