misc: initial project favorite in grid view

This commit is contained in:
Sheen Capadngan
2024-06-28 17:40:34 +08:00
parent 7cdc47cd3a
commit 11d9166684
8 changed files with 272 additions and 47 deletions
+24
View File
@@ -7,6 +7,7 @@ import {
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries";
import { userKeys } from "./queries";
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types";
export const useAddUserToWsE2EE = () => {
@@ -88,3 +89,26 @@ export const useVerifyEmailVerificationCode = () => {
}
});
};
export const useUpdateUserProjectFavorites = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
orgId,
projectFavorites
}: {
orgId: string;
projectFavorites: string[];
}) => {
await apiRequest.put("/api/v1/user/me/project-favorites", {
orgId,
projectFavorites
});
return {};
},
onSuccess: () => {
queryClient.invalidateQueries(userKeys.userProjectFavorites);
}
});
};
+16
View File
@@ -22,11 +22,13 @@ export const userKeys = {
getUser: ["user"] as const,
getPrivateKey: ["user"] as const,
userAction: ["user-action"] as const,
userProjectFavorites: ["user-project-favorites"] as const,
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
myIp: ["ip"] as const,
myAPIKeys: ["api-keys"] as const,
myAPIKeysV2: ["api-keys-v2"] as const,
mySessions: ["sessions"] as const,
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
};
@@ -74,6 +76,14 @@ export const fetchUserAction = async (action: string) => {
return data.userAction || "";
};
export const fetchUserProjectFavorites = async (orgId: string) => {
const { data } = await apiRequest.get<{ projectFavorites: string[] }>(
`/api/v1/user/me/project-favorites?orgId=${orgId}`
);
return data.projectFavorites;
};
export const useRenameUser = () => {
const queryClient = useQueryClient();
@@ -122,6 +132,12 @@ export const fetchOrgUsers = async (orgId: string) => {
return data.users;
};
export const useGetUserProjectFavorites = (orgId: string) =>
useQuery({
queryKey: userKeys.userProjectFavorites,
queryFn: () => fetchUserProjectFavorites(orgId)
});
export const useGetOrgUsers = (orgId: string) =>
useQuery({
queryKey: userKeys.getOrgUsers(orgId),
+113 -42
View File
@@ -1,6 +1,6 @@
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import Head from "next/head";
@@ -8,7 +8,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { faSlack } from "@fortawesome/free-brands-svg-icons";
import { faFolderOpen } from "@fortawesome/free-regular-svg-icons";
import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons";
import {
faArrowRight,
faArrowUpRightFromSquare,
@@ -24,6 +24,7 @@ import {
faNetworkWired,
faPlug,
faPlus,
faStar as faSolidStar,
faUserPlus
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -62,6 +63,9 @@ import {
} from "@app/hooks/api";
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { Workspace } from "@app/hooks/api/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
import { usePopUp } from "@app/hooks/usePopUp";
const features = [
@@ -485,6 +489,8 @@ const OrganizationPage = withPermission(
const { currentOrg } = useOrganization();
const routerOrgId = String(router.query.id);
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === routerOrgId) || [];
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
const addUsersToProject = useAddUserToWsNonE2EE();
@@ -569,52 +575,117 @@ const OrganizationPage = withPermission(
const filteredWorkspaces = orgWorkspaces.filter((ws) =>
ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())
);
const favoriteWorkspaces = filteredWorkspaces.filter((ws) => projectFavorites?.includes(ws.id));
const nonFavoriteWorkspaces = filteredWorkspaces.filter((ws) =>
favoriteWorkspaces.every((entry) => entry.id !== ws.id)
);
const addProjectToFavorites = useCallback(
(projectId: string) => {
if (currentOrg?.id) {
updateUserProjectFavorites({
orgId: currentOrg?.id,
projectFavorites: [...(projectFavorites || []), projectId]
});
}
},
[currentOrg, projectFavorites]
);
const removeProjectFromFavorites = useCallback(
(projectId: string) => {
if (currentOrg?.id) {
updateUserProjectFavorites({
orgId: currentOrg?.id,
projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)]
});
}
},
[currentOrg, projectFavorites]
);
const renderProjectGridItem = (workspace: Workspace, isFavorite: boolean) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
<div
onClick={() => {
router.push(`/project/${workspace.id}/secrets/overview`);
localStorage.setItem("projectData.id", workspace.id);
}}
key={workspace.id}
className="min-w-72 flex h-40 cursor-pointer flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="flex flex-row justify-between">
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div>
{isFavorite ? (
<FontAwesomeIcon
icon={faSolidStar}
className="text-sm text-mineshaft-300"
onClick={(e) => {
e.stopPropagation();
removeProjectFromFavorites(workspace.id);
}}
/>
) : (
<FontAwesomeIcon
icon={faStar}
className="text-sm text-mineshaft-400"
onClick={(e) => {
e.stopPropagation();
addProjectToFavorites(workspace.id);
}}
/>
)}
</div>
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
{workspace.environments?.length || 0} environments
</div>
<button type="button">
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 transition-all hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200">
Explore{" "}
<FontAwesomeIcon
icon={faArrowRight}
className="pl-1.5 pr-0.5 duration-200 hover:pl-2 hover:pr-0"
/>
</div>
</button>
</div>
);
const projectsGridView = (
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{isWorkspaceLoading &&
Array.apply(0, Array(3)).map((_x, i) => (
<>
{favoriteWorkspaces.length > 0 && (
<>
<p className="mt-6 text-xl font-semibold text-white">Favorites</p>
<div
key={`workspace-cards-loading-${i + 1}`}
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
className={`b grid w-full grid-cols-1 gap-4 ${
nonFavoriteWorkspaces.length > 0 && "border-b border-mineshaft-600"
} py-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`}
>
<div className="mt-0 text-lg text-mineshaft-100">
<Skeleton className="w-3/4 bg-mineshaft-600" />
</div>
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
<div className="flex justify-end">
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
{favoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, true))}
</div>
))}
{filteredWorkspaces.map((workspace) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
<div
onClick={() => {
router.push(`/project/${workspace.id}/secrets/overview`);
localStorage.setItem("projectData.id", workspace.id);
}}
key={workspace.id}
className="min-w-72 group flex h-40 cursor-pointer flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div>
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
{workspace.environments?.length || 0} environments
</div>
<button type="button">
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 transition-all group-hover:border-primary-500/80 group-hover:bg-primary-800/20 group-hover:text-mineshaft-200">
Explore{" "}
<FontAwesomeIcon
icon={faArrowRight}
className="pl-1.5 pr-0.5 duration-200 group-hover:pl-2 group-hover:pr-0"
/>
</>
)}
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{isWorkspaceLoading &&
Array.apply(0, Array(3)).map((_x, i) => (
<div
key={`workspace-cards-loading-${i + 1}`}
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="mt-0 text-lg text-mineshaft-100">
<Skeleton className="w-3/4 bg-mineshaft-600" />
</div>
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
<div className="flex justify-end">
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
</div>
</button>
</div>
))}
</div>
))}
{nonFavoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, false))}
</div>
</>
);
const projectsListView = (