diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx
index 72351df88..7fc10df13 100644
--- a/frontend/src/layouts/AppLayout/AppLayout.tsx
+++ b/frontend/src/layouts/AppLayout/AppLayout.tsx
@@ -5,13 +5,14 @@
/* eslint-disable no-var */
/* eslint-disable func-names */
-import { useEffect, useMemo } from "react";
+import { useCallback, useEffect, useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
+import { faStar } from "@fortawesome/free-regular-svg-icons";
import {
faAngleDown,
faArrowLeft,
@@ -23,7 +24,8 @@ import {
faInfo,
faMobile,
faPlus,
- faQuestion
+ faQuestion,
+ faStar as faSolidStar
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
@@ -72,6 +74,8 @@ import {
useRegisterUserAction,
useSelectOrganization
} from "@app/hooks/api";
+import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
+import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
import { CreateOrgModal } from "@app/views/Org/components";
@@ -122,6 +126,11 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { workspaces, currentWorkspace } = useWorkspace();
const { orgs, currentOrg } = useOrganization();
+ const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
+ const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
+ const nonFavoriteWorkspaces = workspaces.filter((w) => !projectFavorites?.includes(w.id));
+ const favoriteWorkspaces = workspaces.filter((w) => projectFavorites?.includes(w.id));
+
const { user } = useUser();
const { subscription } = useSubscription();
const workspaceId = currentWorkspace?.id || "";
@@ -271,6 +280,30 @@ export const AppLayout = ({ children }: LayoutProps) => {
}
};
+ 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]
+ );
+
return (
<>
@@ -451,19 +484,47 @@ export const AppLayout = ({ children }: LayoutProps) => {
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
>
- {workspaces
+ {[...favoriteWorkspaces, ...nonFavoriteWorkspaces]
.filter((ws) => ws.orgId === currentOrg?.id)
.map(({ id, name }) => (
-
- {name}
-
+
+
+ {name}
+
+
+
+ {projectFavorites?.includes(id) ? (
+ {
+ e.stopPropagation();
+ removeProjectFromFavorites(id);
+ }}
+ />
+ ) : (
+ {
+ e.stopPropagation();
+ addProjectToFavorites(id);
+ }}
+ />
+ )}
+
+
))}
diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx
index 5ff425c37..14fd6b5b0 100644
--- a/frontend/src/pages/org/[id]/overview/index.tsx
+++ b/frontend/src/pages/org/[id]/overview/index.tsx
@@ -619,7 +619,7 @@ const OrganizationPage = withPermission(
{isFavorite ? (
{
e.stopPropagation();
removeProjectFromFavorites(workspace.id);
@@ -628,7 +628,7 @@ const OrganizationPage = withPermission(
) : (
{
e.stopPropagation();
addProjectToFavorites(workspace.id);
@@ -651,6 +651,49 @@ const OrganizationPage = withPermission(
);
+ const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => (
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
+ {
+ router.push(`/project/${workspace.id}/secrets/overview`);
+ localStorage.setItem("projectData.id", workspace.id);
+ }}
+ key={workspace.id}
+ className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
+ index === 0 && "rounded-t-md"
+ } ${index === filteredWorkspaces.length - 1 && "rounded-b-md border-b"}`}
+ >
+
+
+
+ {workspace.environments?.length || 0} environments
+
+ {isFavorite ? (
+
{
+ e.stopPropagation();
+ removeProjectFromFavorites(workspace.id);
+ }}
+ />
+ ) : (
+ {
+ e.stopPropagation();
+ addProjectToFavorites(workspace.id);
+ }}
+ />
+ )}
+
+
+ );
+
const projectsGridView = (
<>
{favoriteWorkspaces.length > 0 && (
@@ -701,29 +744,13 @@ const OrganizationPage = withPermission(
))}
- {filteredWorkspaces.map((workspace, ind) => (
- // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
- {
- router.push(`/project/${workspace.id}/secrets/overview`);
- localStorage.setItem("projectData.id", workspace.id);
- }}
- key={workspace.id}
- className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
- ind === 0 && "rounded-t-md"
- } ${ind === filteredWorkspaces.length - 1 && "rounded-b-md border-b"}`}
- >
-
-
-
- {workspace.environments?.length || 0} environments
-
-
-
- ))}
+ {[...favoriteWorkspaces, ...nonFavoriteWorkspaces].map((workspace, ind) =>
+ renderProjectListItem(
+ workspace,
+ favoriteWorkspaces.some((w) => w.id === workspace.id),
+ ind
+ )
+ )}
);