From 790e16e093c850e142903de6bc34b06129741d7f Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 24 Jul 2024 03:41:07 +0530 Subject: [PATCH 01/16] chore: workspace user favorites --- apiserver/plane/app/serializers/__init__.py | 2 + apiserver/plane/app/serializers/favorite.py | 109 ++++++++++++++++++ apiserver/plane/app/urls/workspace.py | 17 +++ apiserver/plane/app/views/__init__.py | 5 + .../plane/app/views/workspace/favorite.py | 70 +++++++++++ apiserver/plane/db/models/favorite.py | 11 +- 6 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 apiserver/plane/app/serializers/favorite.py create mode 100644 apiserver/plane/app/views/workspace/favorite.py diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 0d87acde74d..83981ab53c6 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -121,3 +121,5 @@ from .webhook import WebhookSerializer, WebhookLogSerializer from .dashboard import DashboardSerializer, WidgetSerializer + +from .favorite import UserFavoriteSerializer diff --git a/apiserver/plane/app/serializers/favorite.py b/apiserver/plane/app/serializers/favorite.py new file mode 100644 index 00000000000..b8400bd09ac --- /dev/null +++ b/apiserver/plane/app/serializers/favorite.py @@ -0,0 +1,109 @@ +from rest_framework import serializers + +from plane.db.models import ( + UserFavorite, + Cycle, + Module, + Issue, + IssueView, + Page, + Project, +) + + +class ProjectFavoriteLiteSerializer(serializers.ModelSerializer): + + class Meta: + model = Project + fields = [ + "id", + "name", + "logo_props", + ] + + +class PageFavoriteLiteSerializer(serializers.ModelSerializer): + + class Meta: + model = Page + fields = [ + "id", + "name", + "logo_props", + ] + + +class CycleFavoriteLiteSerializer(serializers.ModelSerializer): + + class Meta: + model = Cycle + fields = [ + "id", + "name", + "logo_props", + ] + + +class ModuleFavoriteLiteSerializer(serializers.ModelSerializer): + + class Meta: + model = Module + fields = [ + "id", + "name", + "logo_props", + ] + + +class ViewFavoriteSerializer(serializers.ModelSerializer): + + class Meta: + model = IssueView + fields = ["id", "name", "logo_props"] + + +def get_entity_model_and_serializer(entity_type): + entity_map = { + "cycle": (Cycle, CycleFavoriteLiteSerializer), + "issue": (Issue, None), + "module": (Module, ModuleFavoriteLiteSerializer), + "view": (IssueView, ViewFavoriteSerializer), + "page": (Page, PageFavoriteLiteSerializer), + "project": (Project, ProjectFavoriteLiteSerializer), + "folder": (None, None), + } + return entity_map.get(entity_type, (None, None)) + + +class UserFavoriteSerializer(serializers.ModelSerializer): + entity_data = serializers.SerializerMethodField() + + class Meta: + model = UserFavorite + fields = [ + "id", + "entity_type", + "entity_identifier", + "entity_data", + "name", + "is_folder", + "sequence", + "parent", + "workspace", + ] + read_only_fields = ["workspace", "created_by", "updated_by"] + + def get_entity_data(self, obj): + entity_type = obj.entity_type + entity_identifier = obj.entity_identifier + + entity_model, entity_serializer = get_entity_model_and_serializer( + entity_type + ) + if entity_model and entity_serializer: + try: + entity = entity_model.objects.get(pk=entity_identifier) + return entity_serializer(entity).data + except entity_model.DoesNotExist: + return None + return None diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index 8b21bb9e1b2..3f1e000e473 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -25,6 +25,8 @@ ExportWorkspaceUserActivityEndpoint, WorkspaceModulesEndpoint, WorkspaceCyclesEndpoint, + WorkspaceFavoriteEndpoint, + WorkspaceFavoriteGroupEndpoint, ) @@ -237,4 +239,19 @@ WorkspaceCyclesEndpoint.as_view(), name="workspace-cycles", ), + path( + "workspaces//user-favorites/", + WorkspaceFavoriteEndpoint.as_view(), + name="workspace-user-favorites", + ), + path( + "workspaces//user-favorites//", + WorkspaceFavoriteEndpoint.as_view(), + name="workspace-user-favorites", + ), + path( + "workspaces//user-favorites//group/", + WorkspaceFavoriteGroupEndpoint.as_view(), + name="workspace-user-favorites-groups", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index bb722de527b..0babdf5d849 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -40,6 +40,11 @@ ExportWorkspaceUserActivityEndpoint, ) +from .workspace.favorite import ( + WorkspaceFavoriteEndpoint, + WorkspaceFavoriteGroupEndpoint, +) + from .workspace.member import ( WorkSpaceMemberViewSet, TeamMemberViewSet, diff --git a/apiserver/plane/app/views/workspace/favorite.py b/apiserver/plane/app/views/workspace/favorite.py new file mode 100644 index 00000000000..29cfb3f9134 --- /dev/null +++ b/apiserver/plane/app/views/workspace/favorite.py @@ -0,0 +1,70 @@ +# Third party modules +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.views.base import BaseAPIView +from plane.db.models import UserFavorite, Workspace +from plane.app.serializers import UserFavoriteSerializer +from plane.app.permissions import WorkspaceEntityPermission + + +class WorkspaceFavoriteEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get(self, request, slug): + favorites = UserFavorite.objects.filter( + user=request.user, + workspace__slug=slug, + parent__isnull=True, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + serializer = UserFavoriteSerializer(favorites, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def post(self, request, slug): + workspace = Workspace.objects.get(slug=slug) + serializer = UserFavoriteSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(user_id=request.user.id, workspace=workspace) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def patch(self, request, slug, favorite_id): + favorite = UserFavorite.objects.get( + user=request.user, workspace__slug=slug, pk=favorite_id + ) + serializer = UserFavoriteSerializer( + favorite, data=request.data, partial=True + ) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, slug, favorite_id): + favorite = UserFavorite.objects.get( + user=request.user, workspace__slug=slug, pk=favorite_id + ) + favorite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class WorkspaceFavoriteGroupEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get(self, request, slug, favorite_id): + favorites = UserFavorite.objects.filter( + user=request.user, + workspace__slug=slug, + parent_id=favorite_id, + project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, + ) + serializer = UserFavoriteSerializer(favorites, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/db/models/favorite.py b/apiserver/plane/db/models/favorite.py index 2ea1014bc82..ebfe67dfef0 100644 --- a/apiserver/plane/db/models/favorite.py +++ b/apiserver/plane/db/models/favorite.py @@ -39,9 +39,14 @@ class Meta: def save(self, *args, **kwargs): if self._state.adding: - largest_sequence = UserFavorite.objects.filter( - workspace=self.project.workspace - ).aggregate(largest=models.Max("sequence"))["largest"] + if self.project: + largest_sequence = UserFavorite.objects.filter( + workspace=self.project.workspace + ).aggregate(largest=models.Max("sequence"))["largest"] + else: + largest_sequence = UserFavorite.objects.filter( + workspace=self.workspace, + ).aggregate(largest=models.Max("sequence"))["largest"] if largest_sequence is not None: self.sequence = largest_sequence + 10000 From 9d1ce25acb16bc381fd2bac8511ba7863eafaa35 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Thu, 25 Jul 2024 19:14:08 +0530 Subject: [PATCH 02/16] chore: added project id in entity type --- apiserver/plane/app/serializers/favorite.py | 26 ++++----------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/apiserver/plane/app/serializers/favorite.py b/apiserver/plane/app/serializers/favorite.py index b8400bd09ac..cf4056ec5ce 100644 --- a/apiserver/plane/app/serializers/favorite.py +++ b/apiserver/plane/app/serializers/favorite.py @@ -15,51 +15,35 @@ class ProjectFavoriteLiteSerializer(serializers.ModelSerializer): class Meta: model = Project - fields = [ - "id", - "name", - "logo_props", - ] + fields = ["id", "name", "logo_props", "project_id"] class PageFavoriteLiteSerializer(serializers.ModelSerializer): class Meta: model = Page - fields = [ - "id", - "name", - "logo_props", - ] + fields = ["id", "name", "logo_props", "project_id"] class CycleFavoriteLiteSerializer(serializers.ModelSerializer): class Meta: model = Cycle - fields = [ - "id", - "name", - "logo_props", - ] + fields = ["id", "name", "logo_props", "project_id"] class ModuleFavoriteLiteSerializer(serializers.ModelSerializer): class Meta: model = Module - fields = [ - "id", - "name", - "logo_props", - ] + fields = ["id", "name", "logo_props", "project_id"] class ViewFavoriteSerializer(serializers.ModelSerializer): class Meta: model = IssueView - fields = ["id", "name", "logo_props"] + fields = ["id", "name", "logo_props", "project_id"] def get_entity_model_and_serializer(entity_type): From 827e11f58e87735c6114a26b1951873273bc64f7 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Thu, 25 Jul 2024 19:17:15 +0530 Subject: [PATCH 03/16] chore: removed the extra key --- apiserver/plane/app/serializers/favorite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/app/serializers/favorite.py b/apiserver/plane/app/serializers/favorite.py index cf4056ec5ce..89a909411a4 100644 --- a/apiserver/plane/app/serializers/favorite.py +++ b/apiserver/plane/app/serializers/favorite.py @@ -15,7 +15,7 @@ class ProjectFavoriteLiteSerializer(serializers.ModelSerializer): class Meta: model = Project - fields = ["id", "name", "logo_props", "project_id"] + fields = ["id", "name", "logo_props"] class PageFavoriteLiteSerializer(serializers.ModelSerializer): From 9ee459f0b3050ed418defa518ea4741408a343cc Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 26 Jul 2024 13:48:28 +0530 Subject: [PATCH 04/16] chore: removed the project member filter --- apiserver/plane/app/serializers/favorite.py | 7 +++++++ apiserver/plane/app/views/workspace/favorite.py | 2 -- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/app/serializers/favorite.py b/apiserver/plane/app/serializers/favorite.py index 89a909411a4..5696fb6bc42 100644 --- a/apiserver/plane/app/serializers/favorite.py +++ b/apiserver/plane/app/serializers/favorite.py @@ -19,11 +19,18 @@ class Meta: class PageFavoriteLiteSerializer(serializers.ModelSerializer): + project_id = serializers.SerializerMethodField() class Meta: model = Page fields = ["id", "name", "logo_props", "project_id"] + def get_project_id(self, obj): + project = ( + obj.projects.first() + ) # This gets the first project related to the Page + return project.id if project else None + class CycleFavoriteLiteSerializer(serializers.ModelSerializer): diff --git a/apiserver/plane/app/views/workspace/favorite.py b/apiserver/plane/app/views/workspace/favorite.py index 29cfb3f9134..0ee29369fdb 100644 --- a/apiserver/plane/app/views/workspace/favorite.py +++ b/apiserver/plane/app/views/workspace/favorite.py @@ -19,8 +19,6 @@ def get(self, request, slug): user=request.user, workspace__slug=slug, parent__isnull=True, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, ) serializer = UserFavoriteSerializer(favorites, many=True) return Response(serializer.data, status=status.HTTP_200_OK) From 1054263d13f729460af1c08c22b3fa097bde17a1 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 26 Jul 2024 14:18:00 +0530 Subject: [PATCH 05/16] chore: updated the project permission layer --- apiserver/plane/app/views/workspace/favorite.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apiserver/plane/app/views/workspace/favorite.py b/apiserver/plane/app/views/workspace/favorite.py index 0ee29369fdb..797103b8a0a 100644 --- a/apiserver/plane/app/views/workspace/favorite.py +++ b/apiserver/plane/app/views/workspace/favorite.py @@ -2,6 +2,9 @@ from rest_framework import status from rest_framework.response import Response +# Django modules +from django.db.models import Q + # Module imports from plane.app.views.base import BaseAPIView from plane.db.models import UserFavorite, Workspace @@ -15,10 +18,18 @@ class WorkspaceFavoriteEndpoint(BaseAPIView): ] def get(self, request, slug): + # the second filter is to check if the user is a member of the project favorites = UserFavorite.objects.filter( user=request.user, workspace__slug=slug, parent__isnull=True, + ).filter( + Q(project__isnull=True) + | ( + Q(project__isnull=False) + & Q(project__project_projectmember__member=request.user) + & Q(project__project_projectmember__is_active=True) + ) ) serializer = UserFavoriteSerializer(favorites, many=True) return Response(serializer.data, status=status.HTTP_200_OK) From d8f13b3dd307582f116379bbdad16510d2bcbd8e Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 26 Jul 2024 14:22:58 +0530 Subject: [PATCH 06/16] chore: updated the workspace group favorite filter --- apiserver/plane/app/views/workspace/favorite.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/app/views/workspace/favorite.py b/apiserver/plane/app/views/workspace/favorite.py index 797103b8a0a..2a9bf9a1268 100644 --- a/apiserver/plane/app/views/workspace/favorite.py +++ b/apiserver/plane/app/views/workspace/favorite.py @@ -72,8 +72,13 @@ def get(self, request, slug, favorite_id): user=request.user, workspace__slug=slug, parent_id=favorite_id, - project__project_projectmember__member=request.user, - project__project_projectmember__is_active=True, + ).filter( + Q(project__isnull=True) + | ( + Q(project__isnull=False) + & Q(project__project_projectmember__member=request.user) + & Q(project__project_projectmember__is_active=True) + ) ) serializer = UserFavoriteSerializer(favorites, many=True) return Response(serializer.data, status=status.HTTP_200_OK) From 9454e4d16ec9b93ab5e0875a4824c75a3e532c81 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 29 Jul 2024 13:03:42 +0530 Subject: [PATCH 07/16] fix: project favorite toggle --- apiserver/plane/app/serializers/favorite.py | 3 ++- apiserver/plane/app/views/workspace/favorite.py | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/app/serializers/favorite.py b/apiserver/plane/app/serializers/favorite.py index 5696fb6bc42..8e0beda1060 100644 --- a/apiserver/plane/app/serializers/favorite.py +++ b/apiserver/plane/app/serializers/favorite.py @@ -80,7 +80,8 @@ class Meta: "is_folder", "sequence", "parent", - "workspace", + "workspace_id", + "project_id", ] read_only_fields = ["workspace", "created_by", "updated_by"] diff --git a/apiserver/plane/app/views/workspace/favorite.py b/apiserver/plane/app/views/workspace/favorite.py index 2a9bf9a1268..68239bfe6b4 100644 --- a/apiserver/plane/app/views/workspace/favorite.py +++ b/apiserver/plane/app/views/workspace/favorite.py @@ -38,7 +38,11 @@ def post(self, request, slug): workspace = Workspace.objects.get(slug=slug) serializer = UserFavoriteSerializer(data=request.data) if serializer.is_valid(): - serializer.save(user_id=request.user.id, workspace=workspace) + serializer.save( + user_id=request.user.id, + workspace=workspace, + project_id=request.data.get("project_id", None), + ) return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) From 40776186043c233b1cae93a69888b99904faeda3 Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 29 Jul 2024 16:59:06 +0530 Subject: [PATCH 08/16] chore: Fav feature --- packages/types/src/favourite/favourite.d.ts | 14 + packages/types/src/favourite/index.d.ts | 1 + packages/types/src/index.d.ts | 1 + .../ui/src/icons/favourite-folder-icon.tsx | 36 +++ packages/ui/src/icons/index.ts | 1 + .../[workspaceSlug]/(projects)/sidebar.tsx | 8 +- .../sidebar/favourites/favourite-folder.tsx | 262 ++++++++++++++++++ .../sidebar/favourites/favourite-item.tsx | 123 ++++++++ .../sidebar/favourites/favourites-menu.tsx | 138 +++++++++ .../sidebar/favourites/new-fav-folder.tsx | 101 +++++++ .../workspace/sidebar/projects-list.tsx | 251 +++++++---------- web/core/constants/event-tracker.ts | 4 +- web/core/hooks/store/use-favourite.ts | 10 + .../layouts/auth-layout/workspace-wrapper.tsx | 8 + .../services/favourite/favourite.service.ts | 58 ++++ web/core/services/favourite/index.ts | 1 + web/core/store/cycle.store.ts | 8 +- web/core/store/favourite.store.ts | 233 ++++++++++++++++ web/core/store/module.store.ts | 8 +- web/core/store/pages/page.ts | 26 +- web/core/store/project-view.store.ts | 8 +- web/core/store/project/project.store.ts | 9 +- web/core/store/root.store.ts | 3 + 23 files changed, 1136 insertions(+), 176 deletions(-) create mode 100644 packages/types/src/favourite/favourite.d.ts create mode 100644 packages/types/src/favourite/index.d.ts create mode 100644 packages/ui/src/icons/favourite-folder-icon.tsx create mode 100644 web/core/components/workspace/sidebar/favourites/favourite-folder.tsx create mode 100644 web/core/components/workspace/sidebar/favourites/favourite-item.tsx create mode 100644 web/core/components/workspace/sidebar/favourites/favourites-menu.tsx create mode 100644 web/core/components/workspace/sidebar/favourites/new-fav-folder.tsx create mode 100644 web/core/hooks/store/use-favourite.ts create mode 100644 web/core/services/favourite/favourite.service.ts create mode 100644 web/core/services/favourite/index.ts create mode 100644 web/core/store/favourite.store.ts diff --git a/packages/types/src/favourite/favourite.d.ts b/packages/types/src/favourite/favourite.d.ts new file mode 100644 index 00000000000..0c8c2dc1a75 --- /dev/null +++ b/packages/types/src/favourite/favourite.d.ts @@ -0,0 +1,14 @@ +export type IFavourite = { + id: string; + name: string; + entity_type: string; + entity_data: { + name: string; + }; + is_folder: boolean; + sort_order: number; + parent: string | null; + entity_identifier?: string | null; + children: IFavourite[]; + project_id: string | null; +}; diff --git a/packages/types/src/favourite/index.d.ts b/packages/types/src/favourite/index.d.ts new file mode 100644 index 00000000000..57cbf00a459 --- /dev/null +++ b/packages/types/src/favourite/index.d.ts @@ -0,0 +1 @@ +export * from "./favourite"; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 353aeaf08da..51299823d76 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -28,3 +28,4 @@ export * from "./common"; export * from "./pragmatic"; export * from "./publish"; export * from "./workspace-notifications"; +export * from "./favourite"; diff --git a/packages/ui/src/icons/favourite-folder-icon.tsx b/packages/ui/src/icons/favourite-folder-icon.tsx new file mode 100644 index 00000000000..68649e23929 --- /dev/null +++ b/packages/ui/src/icons/favourite-folder-icon.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const FavouriteFolderIcon: React.FC = ({ + className = "text-current", + color = "#a3a3a3", + ...rest +}) => ( + + + + +); diff --git a/packages/ui/src/icons/index.ts b/packages/ui/src/icons/index.ts index b1861502950..a243873ae56 100644 --- a/packages/ui/src/icons/index.ts +++ b/packages/ui/src/icons/index.ts @@ -24,3 +24,4 @@ export * from "./info-icon"; export * from "./dropdown-icon"; export * from "./intake"; export * from "./user-activity-icon"; +export * from "./favourite-folder-icon"; diff --git a/web/app/[workspaceSlug]/(projects)/sidebar.tsx b/web/app/[workspaceSlug]/(projects)/sidebar.tsx index b1a9e32fc7c..e57090fb8d9 100644 --- a/web/app/[workspaceSlug]/(projects)/sidebar.tsx +++ b/web/app/[workspaceSlug]/(projects)/sidebar.tsx @@ -10,6 +10,7 @@ import { SidebarWorkspaceMenu, } from "@/components/workspace"; // helpers +import { SidebarFavouritesMenu } from "@/components/workspace/sidebar/favourites/favourites-menu"; import { cn } from "@/helpers/common.helper"; // hooks import { useAppTheme } from "@/hooks/store"; @@ -41,7 +42,6 @@ export const AppSidebar: FC = observer(() => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [windowSize]); - return (
= observer(() => { "opacity-0": !sidebarCollapsed, })} /> + +
diff --git a/web/core/components/workspace/sidebar/favourites/favourite-folder.tsx b/web/core/components/workspace/sidebar/favourites/favourite-folder.tsx new file mode 100644 index 00000000000..f9622619e9f --- /dev/null +++ b/web/core/components/workspace/sidebar/favourites/favourite-folder.tsx @@ -0,0 +1,262 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; + +import { useParams } from "next/navigation"; +import { PenSquare, Star, MoreHorizontal, ChevronRight } from "lucide-react"; +import { Disclosure, Transition } from "@headlessui/react"; +// ui +import { IFavourite } from "@plane/types"; +import { CustomMenu, Tooltip, DropIndicator, setToast, TOAST_TYPE, FavouriteFolderIcon } from "@plane/ui"; + +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useAppTheme } from "@/hooks/store"; +import { useFavourite } from "@/hooks/store/use-favourite"; +import { usePlatformOS } from "@/hooks/use-platform-os"; +// constants +import { FavouriteItem } from "./favourite-item"; +import { NewFavouriteFolder } from "./new-fav-folder"; +type Props = { + isLastChild: boolean; + favourite: IFavourite; +}; + +export const FavouriteFolder: React.FC = (props) => { + const { isLastChild, favourite } = props; + // store hooks + const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); + + const { isMobile } = usePlatformOS(); + const { deleteFavourite, moveFavourite, getGroupedFavourites } = useFavourite(); + const { workspaceSlug } = useParams(); + // states + const [isMenuActive, setIsMenuActive] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const [instruction, setInstruction] = useState<"DRAG_OVER" | "DRAG_BELOW" | undefined>(undefined); + const [folderToRename, setFolderToRename] = useState(null); + // refs + const actionSectionRef = useRef(null); + const elementRef = useRef(null); + + !favourite.children && getGroupedFavourites(workspaceSlug.toString(), favourite.id); + + const handleRemoveFromFavorites = () => { + console.log(favourite.id, "handleRemoveFromFavorites"); + deleteFavourite(workspaceSlug.toString(), favourite.id) + .then((res) => { + console.log(res, "res"); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Favourite removed successfully.", + }); + }) + .catch((err) => { + Object.keys(err.data).map((key) => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: err.data[key], + }); + }); + }); + }; + + const handleOnDrop = (source: string, destination: string) => { + moveFavourite(workspaceSlug.toString(), source, { + parent: destination, + }) + .then((res) => { + console.log(res, "res"); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Favourite moved successfully.", + }); + }) + .catch((err) => { + console.log(err, "err"); + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Failed to move favourite.", + }); + }); + }; + + useEffect(() => { + const element = elementRef.current; + + if (!element) return; + + return combine( + dropTargetForElements({ + element, + getData: () => ({ type: "PARENT", id: favourite.id }), + onDragEnter: () => { + setIsDragging(true); + }, + onDragLeave: () => { + setIsDragging(false); + }, + onDragStart: () => { + setIsDragging(true); + }, + onDrop: ({ self, source }) => { + setIsDragging(false); + const sourceId = source?.data?.id as string | undefined; + const destinationId = self?.data?.id as string | undefined; + + if (sourceId === destinationId) return; + if (!sourceId || !destinationId) return; + + handleOnDrop(sourceId, destinationId); + }, + }) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [elementRef.current, isDragging, favourite.id, handleOnDrop]); + + // useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); + // useOutsideClickDetector(projectRef, () => projectRef?.current?.classList?.remove(HIGHLIGHT_CLASS)); + + return folderToRename ? ( + + ) : ( + <> + + {({ open }) => ( +
+ +
+ {isSidebarCollapsed ? ( +
+ +
+ +
+
+
+ ) : ( + <> + +
+ +
+ +
+

{favourite.name}

+
+
+
+ setIsMenuActive(!isMenuActive)} + > + + + } + className={cn( + "opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto", + { + "opacity-100 pointer-events-auto": isMenuActive, + } + )} + customButtonClassName="grid place-items-center" + placement="bottom-start" + > + + + + Remove from favorites + + + setFolderToRename(favourite.id)}> +
+ + Rename Folder +
+
+
+ + + + + )} +
+ {favourite.children && favourite.children.length > 0 && ( + + + {favourite.children.map((child) => ( + + ))} + + + )} + {isLastChild && } +
+ )} +
+ + ); +}; diff --git a/web/core/components/workspace/sidebar/favourites/favourite-item.tsx b/web/core/components/workspace/sidebar/favourites/favourite-item.tsx new file mode 100644 index 00000000000..367d0d8dc0b --- /dev/null +++ b/web/core/components/workspace/sidebar/favourites/favourite-item.tsx @@ -0,0 +1,123 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { Briefcase, FileText, Layers } from "lucide-react"; +// ui +import { IFavourite } from "@plane/types"; +import { ContrastIcon, DiceIcon, DragHandle, FavouriteFolderIcon, LayersIcon, Tooltip } from "@plane/ui"; +// components +import { SidebarNavItem } from "@/components/sidebar"; + +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useAppTheme } from "@/hooks/store"; +import { usePlatformOS } from "@/hooks/use-platform-os"; +// plane web components + +export const FavouriteItem = observer(({ favourite }: { favourite: IFavourite }) => { + // store hooks + const { sidebarCollapsed } = useAppTheme(); + const { isMobile } = usePlatformOS(); + //state + const [isDragging, setIsDragging] = useState(false); + + // router params + const { workspaceSlug } = useParams(); + // derived values + + //ref + const elementRef = useRef(null); + const dragHandleRef = useRef(null); + + const getIcon = (type: string) => { + const className = `flex-shrink-0 size-4 stroke-[1.5]`; + + switch (type) { + case "page": + return ; + case "project": + return ; + case "view": + return ; + case "module": + return ; + case "cycle": + return ; + case "issue": + return ; + case "folder": + return ; + default: + return ; + } + }; + + useEffect(() => { + const element = elementRef.current; + + if (!element) return; + + return combine( + draggable({ + element, + dragHandle: element, + canDrag: () => true, + getInitialData: () => ({ id: favourite.id, type: "CHILD" }), + onDragStart: () => { + setIsDragging(true); + }, + onDrop: () => { + setIsDragging(false); + }, + }) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [elementRef?.current, isDragging]); + + return ( + + +
+ + + + + {getIcon(favourite.entity_type)} + + {!sidebarCollapsed && ( +

+ {favourite.entity_data ? favourite.entity_data.name : favourite.name} +

+ )} +
+
+ + ); +}); diff --git a/web/core/components/workspace/sidebar/favourites/favourites-menu.tsx b/web/core/components/workspace/sidebar/favourites/favourites-menu.tsx new file mode 100644 index 00000000000..2df3f87cdd2 --- /dev/null +++ b/web/core/components/workspace/sidebar/favourites/favourites-menu.tsx @@ -0,0 +1,138 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react"; +import { ChevronRight, FolderPlus } from "lucide-react"; +import { Disclosure, Transition } from "@headlessui/react"; +// ui +import { Tooltip } from "@plane/ui"; +// constants + +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useAppTheme } from "@/hooks/store"; +import { useFavourite } from "@/hooks/store/use-favourite"; +import useLocalStorage from "@/hooks/use-local-storage"; +import { usePlatformOS } from "@/hooks/use-platform-os"; +// plane web components +import { FavouriteFolder } from "./favourite-folder"; +import { FavouriteItem } from "./favourite-item"; +import { NewFavouriteFolder } from "./new-fav-folder"; +export const SidebarFavouritesMenu = observer(() => { + //state + const [createNewFolder, setCreateNewFolder] = useState(false); + const [isScrolled, setIsScrolled] = useState(false); // scroll animation state + + // store hooks + const { sidebarCollapsed } = useAppTheme(); + const { favouriteIds, favouriteMap } = useFavourite(); + + const { isMobile } = usePlatformOS(); + + // local storage + const { setValue: toggleFavouriteMenu, storedValue } = useLocalStorage("is_favourite_menu_open", true); + // derived values + const isFavouriteMenuOpen = !!storedValue; + // refs + const containerRef = useRef(null); + + useEffect(() => { + if (sidebarCollapsed) toggleFavouriteMenu(true); + }, [sidebarCollapsed, toggleFavouriteMenu]); + + /** + * Implementing scroll animation styles based on the scroll length of the container + */ + useEffect(() => { + const handleScroll = () => { + if (containerRef.current) { + const scrollTop = containerRef.current.scrollTop; + setIsScrolled(scrollTop > 0); + } + }; + const currentContainerRef = containerRef.current; + if (currentContainerRef) { + currentContainerRef.addEventListener("scroll", handleScroll); + } + return () => { + if (currentContainerRef) { + currentContainerRef.removeEventListener("scroll", handleScroll); + } + }; + }, [containerRef]); + return ( +
+ + {!sidebarCollapsed && ( + + toggleFavouriteMenu(!isFavouriteMenuOpen)} className="flex-1 text-start"> + MY FAVOURITES + + + setCreateNewFolder((state) => !state)} + className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform")} + /> + toggleFavouriteMenu(!isFavouriteMenuOpen)} + className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", { + "rotate-90": isFavouriteMenuOpen, + })} + /> + + + )} + + {isFavouriteMenuOpen && ( + + {createNewFolder && } + {favouriteIds + .filter((id) => !favouriteMap[id].parent) + .map((id) => ( + + {favouriteMap[id].is_folder ? ( + + ) : ( + + )} + + ))} + + )} + + +
+ ); +}); diff --git a/web/core/components/workspace/sidebar/favourites/new-fav-folder.tsx b/web/core/components/workspace/sidebar/favourites/new-fav-folder.tsx new file mode 100644 index 00000000000..40c98606e68 --- /dev/null +++ b/web/core/components/workspace/sidebar/favourites/new-fav-folder.tsx @@ -0,0 +1,101 @@ +import { useEffect, useRef } from "react"; +import { useParams } from "next/navigation"; +import { Controller, SubmitHandler, useForm } from "react-hook-form"; +import { FavouriteFolderIcon, Input, setToast, TOAST_TYPE } from "@plane/ui"; +import { useFavourite } from "@/hooks/store/use-favourite"; +import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; + +type TForm = { + name: string; + entity_type: string; + parent: string | null; + project_id: string | null; + is_folder: boolean; +}; +type TProps = { + setCreateNewFolder: (value: boolean | string) => void; + actionType: "create" | "rename"; + defaultName?: string; + favouriteId?: string; +}; +export const NewFavouriteFolder = (props: TProps) => { + const { setCreateNewFolder, actionType, defaultName, favouriteId } = props; + const { workspaceSlug } = useParams(); + const { addFavourite, updateFavourite } = useFavourite(); + + // ref + const ref = useRef(null); + + // form info + const { handleSubmit, control, setValue, setFocus } = useForm({ + reValidateMode: "onChange", + defaultValues: { + name: defaultName, + }, + }); + + const handleAddNewFolder: SubmitHandler = (formData) => { + formData = { + entity_type: "folder", + is_folder: true, + name: formData.name, + parent: null, + project_id: null, + }; + addFavourite(workspaceSlug.toString(), formData) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Favourite created successfully.", + }); + }) + .catch((err) => { + Object.keys(err.data).map((key) => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: err.data[key], + }); + }); + }); + setCreateNewFolder(false); + setValue("name", ""); + }; + + const handleRenameFolder: SubmitHandler = (formData) => { + if (!favouriteId) return; + const payload = { + name: formData.name, + }; + updateFavourite(workspaceSlug.toString(), favouriteId, payload).then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Favourite updated successfully.", + }); + }); + setCreateNewFolder(false); + setValue("name", ""); + }; + + useEffect(() => { + setFocus("name"); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useOutsideClickDetector(ref, () => { + setCreateNewFolder(false); + }); + return ( +
+ +
+ } + /> + +
+ ); +}; diff --git a/web/core/components/workspace/sidebar/projects-list.tsx b/web/core/components/workspace/sidebar/projects-list.tsx index e1a067d8c09..5515f010583 100644 --- a/web/core/components/workspace/sidebar/projects-list.tsx +++ b/web/core/components/workspace/sidebar/projects-list.tsx @@ -5,7 +5,7 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -import { Briefcase, ChevronRight, LucideIcon, Plus, Star } from "lucide-react"; +import { Briefcase, ChevronRight, Plus } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; // types import { IProject } from "@plane/types"; @@ -25,14 +25,10 @@ import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUser } export const SidebarProjectsList: FC = observer(() => { // get local storage data for isFavoriteProjectsListOpen and isAllProjectsListOpen - const isFavProjectsListOpenInLocalStorage = localStorage.getItem("isFavoriteProjectsListOpen"); const isAllProjectsListOpenInLocalStorage = localStorage.getItem("isAllProjectsListOpen"); // states - const [isFavoriteProjectsListOpen, setIsFavoriteProjectsListOpen] = useState( - isFavProjectsListOpenInLocalStorage === "true" - ); + const [isAllProjectsListOpen, setIsAllProjectsListOpen] = useState(isAllProjectsListOpenInLocalStorage === "true"); - const [isFavoriteProjectCreate, setIsFavoriteProjectCreate] = useState(false); const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); const [isScrolled, setIsScrolled] = useState(false); // scroll animation state // refs @@ -44,12 +40,7 @@ export const SidebarProjectsList: FC = observer(() => { const { membership: { currentWorkspaceRole }, } = useUser(); - const { - getProjectById, - joinedProjectIds: joinedProjects, - favoriteProjectIds: favoriteProjects, - updateProjectView, - } = useProject(); + const { getProjectById, joinedProjectIds: joinedProjects, updateProjectView } = useProject(); // router params const { workspaceSlug } = useParams(); // auth @@ -132,49 +123,18 @@ export const SidebarProjectsList: FC = observer(() => { ); }, [containerRef]); - const toggleListDisclosure = (isOpen: boolean, type: "all" | "favorite") => { - if (type === "all") { - setIsAllProjectsListOpen(isOpen); - localStorage.setItem("isAllProjectsListOpen", isOpen.toString()); - } else { - setIsFavoriteProjectsListOpen(isOpen); - localStorage.setItem("isFavoriteProjectsListOpen", isOpen.toString()); - } + const toggleListDisclosure = (isOpen: boolean) => { + setIsAllProjectsListOpen(isOpen); + localStorage.setItem("isAllProjectsListOpen", isOpen.toString()); }; - const projectSections: { - key: "all" | "favorite"; - type: "FAVORITES" | "JOINED"; - title: string; - icon: LucideIcon; - projects: string[]; - isOpen: boolean; - }[] = [ - { - key: "favorite", - type: "FAVORITES", - title: "FAVORITES", - icon: Star, - projects: favoriteProjects, - isOpen: isFavoriteProjectsListOpen, - }, - { - key: "all", - type: "JOINED", - title: "YOUR PROJECTS", - icon: Briefcase, - projects: joinedProjects, - isOpen: isAllProjectsListOpen, - }, - ]; - return ( <> {workspaceSlug && ( setIsProjectModalOpen(false)} - setToFavorite={isFavoriteProjectCreate} + setToFavorite={false} workspaceSlug={workspaceSlug.toString()} /> )} @@ -184,123 +144,106 @@ export const SidebarProjectsList: FC = observer(() => { "border-t border-custom-sidebar-border-300": isScrolled, })} > - {projectSections.map((section, index) => { - if (!section.projects || section.projects.length === 0) return; - - return ( + <> + <> - - <> -
+ toggleListDisclosure(!isAllProjectsListOpen)} + > + + <> + {isCollapsed ? ( + + ) : ( + YOUR PROJECTS + )} + + + + {!isCollapsed && ( +
+ {isAuthorizedUser && ( + + + )} - > toggleListDisclosure(!section.isOpen, section.key)} + className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0" + onClick={() => toggleListDisclosure(!isAllProjectsListOpen)} > - - <> - {isCollapsed ? ( - - ) : ( - {section.title} - )} - - + - {!isCollapsed && ( -
- {isAuthorizedUser && ( - - - - )} - toggleListDisclosure(!section.isOpen, section.key)} - > - - -
- )}
- + + {isAllProjectsListOpen && ( + - {section.isOpen && ( - - {section.projects.map((projectId, index) => ( - handleCopyText(projectId)} - projectListType={section.type} - disableDrag={section.key === "favorite"} - disableDrop={section.key === "favorite"} - isLastChild={index === section.projects.length - 1} - handleOnProjectDrop={handleOnProjectDrop} - /> - ))} - - )} - - - -
+ {joinedProjects.map((projectId, index) => ( + handleCopyText(projectId)} + projectListType={"JOINED"} + disableDrag={false} + disableDrop={false} + isLastChild={index === joinedProjects.length - 1} + handleOnProjectDrop={handleOnProjectDrop} + /> + ))} + + )} +
- ); - })} + + + {isAuthorizedUser && joinedProjects?.length === 0 && ( + + + {getIcon()} - return ( - - -
- - - - - {getIcon()} - - {!sidebarCollapsed && ( -

- {favourite.entity_data ? favourite.entity_data.name : favourite.name} -

- )} -
-
- - ); -}); + handleRemoveFromFavorites(favourite)}> + + + Remove from favorites + + + +
+ + + ); + } +); diff --git a/web/core/components/workspace/sidebar/favourites/favourites-menu.tsx b/web/core/components/workspace/sidebar/favourites/favourites-menu.tsx index 3b3a9eeeb58..c48ac50fac2 100644 --- a/web/core/components/workspace/sidebar/favourites/favourites-menu.tsx +++ b/web/core/components/workspace/sidebar/favourites/favourites-menu.tsx @@ -2,10 +2,12 @@ import React, { useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; import { ChevronRight, FolderPlus } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; // ui -import { Tooltip } from "@plane/ui"; +import { IFavourite } from "@plane/types"; +import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui"; // constants // helpers @@ -26,17 +28,37 @@ export const SidebarFavouritesMenu = observer(() => { // store hooks const { sidebarCollapsed } = useAppTheme(); - const { favouriteIds, favouriteMap } = useFavourite(); + const { favouriteIds, favouriteMap, deleteFavourite } = useFavourite(); + const { workspaceSlug } = useParams(); const { isMobile } = usePlatformOS(); // local storage - const { setValue: toggleFavouriteMenu, storedValue } = useLocalStorage("is_favourite_menu_open", true); + const { setValue: toggleFavouriteMenu, storedValue } = useLocalStorage("is_favourite_menu_open", false); // derived values const isFavouriteMenuOpen = !!storedValue; // refs const containerRef = useRef(null); + const handleRemoveFromFavorites = (favourite: IFavourite) => { + deleteFavourite(workspaceSlug.toString(), favourite.id) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Favourite removed successfully.", + }); + }) + .catch((err) => { + Object.keys(err.data).map((key) => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: err.data[key], + }); + }); + }); + }; useEffect(() => { if (sidebarCollapsed) toggleFavouriteMenu(true); }, [sidebarCollapsed, toggleFavouriteMenu]); @@ -64,8 +86,9 @@ export const SidebarFavouritesMenu = observer(() => { return (
@@ -79,7 +102,10 @@ export const SidebarFavouritesMenu = observer(() => { setCreateNewFolder((state) => !state)} + onClick={() => { + setCreateNewFolder(true); + !isFavouriteMenuOpen && toggleFavouriteMenu(!isFavouriteMenuOpen); + }} className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform")} /> { isMobile={isMobile} > {favouriteMap[id].is_folder ? ( - + ) : ( - + )} ))} From 11dcce2bf3ba02002ca38aea6f84826b402b9a82 Mon Sep 17 00:00:00 2001 From: gakshita Date: Tue, 30 Jul 2024 12:30:34 +0530 Subject: [PATCH 11/16] fix: nomenclature --- .../favourite.d.ts => favorite/favorite.d.ts} | 4 +- packages/types/src/favorite/index.d.ts | 1 + packages/types/src/favourite/index.d.ts | 1 - packages/types/src/index.d.ts | 2 +- ...lder-icon.tsx => favorite-folder-icon.tsx} | 6 +- packages/ui/src/icons/index.ts | 2 +- .../[workspaceSlug]/(projects)/sidebar.tsx | 4 +- .../favorite-folder.tsx} | 60 ++--- .../favorite-item.tsx} | 40 +-- .../favorites-menu.tsx} | 67 +++-- .../new-fav-folder.tsx | 24 +- web/core/constants/event-tracker.ts | 4 +- web/core/hooks/store/use-favorite.ts | 10 + web/core/hooks/store/use-favourite.ts | 10 - .../layouts/auth-layout/workspace-wrapper.tsx | 10 +- .../favorite.service.ts} | 24 +- web/core/services/favorite/index.ts | 1 + web/core/services/favourite/index.ts | 1 - web/core/store/cycle.store.ts | 4 +- web/core/store/favorite.store.ts | 233 ++++++++++++++++++ web/core/store/favourite.store.ts | 233 ------------------ web/core/store/module.store.ts | 4 +- web/core/store/pages/page.ts | 6 +- web/core/store/project-view.store.ts | 4 +- web/core/store/project/project.store.ts | 4 +- web/core/store/root.store.ts | 6 +- 26 files changed, 379 insertions(+), 386 deletions(-) rename packages/types/src/{favourite/favourite.d.ts => favorite/favorite.d.ts} (80%) create mode 100644 packages/types/src/favorite/index.d.ts delete mode 100644 packages/types/src/favourite/index.d.ts rename packages/ui/src/icons/{favourite-folder-icon.tsx => favorite-folder-icon.tsx} (90%) rename web/core/components/workspace/sidebar/{favourites/favourite-folder.tsx => favorites/favorite-folder.tsx} (84%) rename web/core/components/workspace/sidebar/{favourites/favourite-item.tsx => favorites/favorite-item.tsx} (79%) rename web/core/components/workspace/sidebar/{favourites/favourites-menu.tsx => favorites/favorites-menu.tsx} (68%) rename web/core/components/workspace/sidebar/{favourites => favorites}/new-fav-folder.tsx (77%) create mode 100644 web/core/hooks/store/use-favorite.ts delete mode 100644 web/core/hooks/store/use-favourite.ts rename web/core/services/{favourite/favourite.service.ts => favorite/favorite.service.ts} (61%) create mode 100644 web/core/services/favorite/index.ts delete mode 100644 web/core/services/favourite/index.ts create mode 100644 web/core/store/favorite.store.ts delete mode 100644 web/core/store/favourite.store.ts diff --git a/packages/types/src/favourite/favourite.d.ts b/packages/types/src/favorite/favorite.d.ts similarity index 80% rename from packages/types/src/favourite/favourite.d.ts rename to packages/types/src/favorite/favorite.d.ts index 0c8c2dc1a75..ae7bf2da887 100644 --- a/packages/types/src/favourite/favourite.d.ts +++ b/packages/types/src/favorite/favorite.d.ts @@ -1,4 +1,4 @@ -export type IFavourite = { +export type IFavorite = { id: string; name: string; entity_type: string; @@ -9,6 +9,6 @@ export type IFavourite = { sort_order: number; parent: string | null; entity_identifier?: string | null; - children: IFavourite[]; + children: IFavorite[]; project_id: string | null; }; diff --git a/packages/types/src/favorite/index.d.ts b/packages/types/src/favorite/index.d.ts new file mode 100644 index 00000000000..e11ce8f3c1f --- /dev/null +++ b/packages/types/src/favorite/index.d.ts @@ -0,0 +1 @@ +export * from "./favorite"; diff --git a/packages/types/src/favourite/index.d.ts b/packages/types/src/favourite/index.d.ts deleted file mode 100644 index 57cbf00a459..00000000000 --- a/packages/types/src/favourite/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./favourite"; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 51299823d76..6dfddc6b638 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -28,4 +28,4 @@ export * from "./common"; export * from "./pragmatic"; export * from "./publish"; export * from "./workspace-notifications"; -export * from "./favourite"; +export * from "./favorite"; diff --git a/packages/ui/src/icons/favourite-folder-icon.tsx b/packages/ui/src/icons/favorite-folder-icon.tsx similarity index 90% rename from packages/ui/src/icons/favourite-folder-icon.tsx rename to packages/ui/src/icons/favorite-folder-icon.tsx index 68649e23929..03ebba71b75 100644 --- a/packages/ui/src/icons/favourite-folder-icon.tsx +++ b/packages/ui/src/icons/favorite-folder-icon.tsx @@ -2,11 +2,7 @@ import * as React from "react"; import { ISvgIcons } from "./type"; -export const FavouriteFolderIcon: React.FC = ({ - className = "text-current", - color = "#a3a3a3", - ...rest -}) => ( +export const FavoriteFolderIcon: React.FC = ({ className = "text-current", color = "#a3a3a3", ...rest }) => ( = observer(() => { "opacity-0": !sidebarCollapsed, })} /> - +
void; + favorite: IFavorite; + handleRemoveFromFavorites: (favorite: IFavorite) => void; }; -export const FavouriteFolder: React.FC = (props) => { - const { isLastChild, favourite, handleRemoveFromFavorites } = props; +export const FavoriteFolder: React.FC = (props) => { + const { isLastChild, favorite, handleRemoveFromFavorites } = props; // store hooks const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); const { isMobile } = usePlatformOS(); - const { moveFavourite, getGroupedFavourites } = useFavourite(); + const { moveFavorite, getGroupedFavorites } = useFavorite(); const { workspaceSlug } = useParams(); // states const [isMenuActive, setIsMenuActive] = useState(false); @@ -45,10 +45,10 @@ export const FavouriteFolder: React.FC = (props) => { const actionSectionRef = useRef(null); const elementRef = useRef(null); - !favourite.children && getGroupedFavourites(workspaceSlug.toString(), favourite.id); + !favorite.children && getGroupedFavorites(workspaceSlug.toString(), favorite.id); const handleOnDrop = (source: string, destination: string) => { - moveFavourite(workspaceSlug.toString(), source, { + moveFavorite(workspaceSlug.toString(), source, { parent: destination, }) .then((res) => { @@ -56,7 +56,7 @@ export const FavouriteFolder: React.FC = (props) => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", - message: "Favourite moved successfully.", + message: "Favorite moved successfully.", }); }) .catch((err) => { @@ -64,7 +64,7 @@ export const FavouriteFolder: React.FC = (props) => { setToast({ type: TOAST_TYPE.ERROR, title: "Error!", - message: "Failed to move favourite.", + message: "Failed to move favorite.", }); }); }; @@ -77,7 +77,7 @@ export const FavouriteFolder: React.FC = (props) => { return combine( dropTargetForElements({ element, - getData: () => ({ type: "PARENT", id: favourite.id }), + getData: () => ({ type: "PARENT", id: favorite.id }), onDragEnter: () => { setIsDragging(true); }, @@ -100,20 +100,20 @@ export const FavouriteFolder: React.FC = (props) => { }) ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [elementRef.current, isDragging, favourite.id, handleOnDrop]); + }, [elementRef.current, isDragging, favorite.id, handleOnDrop]); useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); return folderToRename ? ( - ) : ( <> - + {({ open }) => (
= (props) => { >
- +
) : ( <> = (props) => { })} >
- +
-

{favourite.name}

+

{favorite.name}

@@ -185,13 +185,13 @@ export const FavouriteFolder: React.FC = (props) => { customButtonClassName="grid place-items-center" placement="bottom-start" > - handleRemoveFromFavorites(favourite)}> + handleRemoveFromFavorites(favorite)}> Remove from favorites - setFolderToRename(favourite.id)}> + setFolderToRename(favorite.id)}>
Rename Folder @@ -217,7 +217,7 @@ export const FavouriteFolder: React.FC = (props) => { )}
- {favourite.children && favourite.children.length > 0 && ( + {favorite.children && favorite.children.length > 0 && ( = (props) => { leaveTo="transform scale-95 opacity-0" > - {favourite.children.map((child) => ( - ( + ))} diff --git a/web/core/components/workspace/sidebar/favourites/favourite-item.tsx b/web/core/components/workspace/sidebar/favorites/favorite-item.tsx similarity index 79% rename from web/core/components/workspace/sidebar/favourites/favourite-item.tsx rename to web/core/components/workspace/sidebar/favorites/favorite-item.tsx index 37a61ab0d6d..02467a5eab4 100644 --- a/web/core/components/workspace/sidebar/favourites/favourite-item.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-item.tsx @@ -8,8 +8,8 @@ import Link from "next/link"; import { useParams } from "next/navigation"; import { Briefcase, FileText, Layers, MoreHorizontal, Star } from "lucide-react"; // ui -import { IFavourite } from "@plane/types"; -import { ContrastIcon, CustomMenu, DiceIcon, DragHandle, FavouriteFolderIcon, LayersIcon, Tooltip } from "@plane/ui"; +import { IFavorite } from "@plane/types"; +import { ContrastIcon, CustomMenu, DiceIcon, DragHandle, FavoriteFolderIcon, LayersIcon, Tooltip } from "@plane/ui"; // components import { SidebarNavItem } from "@/components/sidebar"; @@ -20,13 +20,13 @@ import { useAppTheme } from "@/hooks/store"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import { usePlatformOS } from "@/hooks/use-platform-os"; -export const FavouriteItem = observer( +export const FavoriteItem = observer( ({ - favourite, + favorite, handleRemoveFromFavorites, }: { - favourite: IFavourite; - handleRemoveFromFavorites: (favourite: IFavourite) => void; + favorite: IFavorite; + handleRemoveFromFavorites: (favorite: IFavorite) => void; }) => { // store hooks const { sidebarCollapsed } = useAppTheme(); @@ -47,7 +47,7 @@ export const FavouriteItem = observer( const getIcon = () => { const className = `flex-shrink-0 size-4 stroke-[1.5]`; - switch (favourite.entity_type) { + switch (favorite.entity_type) { case "page": return ; case "project": @@ -61,24 +61,24 @@ export const FavouriteItem = observer( case "issue": return ; case "folder": - return ; + return ; default: return ; } }; const getLink = () => { - switch (favourite.entity_type) { + switch (favorite.entity_type) { case "project": - return `/${workspaceSlug}/projects/${favourite.project_id}/issues`; + return `/${workspaceSlug}/projects/${favorite.project_id}/issues`; case "cycle": - return `/${workspaceSlug}/projects/${favourite.project_id}/cycles/${favourite.entity_identifier}`; + return `/${workspaceSlug}/projects/${favorite.project_id}/cycles/${favorite.entity_identifier}`; case "module": - return `/${workspaceSlug}/projects/${favourite.project_id}/modules/${favourite.entity_identifier}`; + return `/${workspaceSlug}/projects/${favorite.project_id}/modules/${favorite.entity_identifier}`; case "view": - return `/${workspaceSlug}/projects/${favourite.project_id}/views/${favourite.entity_identifier}`; + return `/${workspaceSlug}/projects/${favorite.project_id}/views/${favorite.entity_identifier}`; case "page": - return `/${workspaceSlug}/projects/${favourite.project_id}/pages/${favourite.entity_identifier}`; + return `/${workspaceSlug}/projects/${favorite.project_id}/pages/${favorite.entity_identifier}`; default: return `/${workspaceSlug}`; } @@ -94,7 +94,7 @@ export const FavouriteItem = observer( element, dragHandle: element, canDrag: () => true, - getInitialData: () => ({ id: favourite.id, type: "CHILD" }), + getInitialData: () => ({ id: favorite.id, type: "CHILD" }), onDragStart: () => { setIsDragging(true); }, @@ -110,13 +110,13 @@ export const FavouriteItem = observer( return (
@@ -125,7 +125,7 @@ export const FavouriteItem = observer( className={cn( "hidden group-hover/project-item:flex items-center justify-center absolute top-1/2 -left-3 -translate-y-1/2 rounded text-custom-sidebar-text-400 cursor-grab", { - "cursor-not-allowed opacity-60": favourite.sort_order === null, + "cursor-not-allowed opacity-60": favorite.sort_order === null, "cursor-grabbing": isDragging, "!hidden": sidebarCollapsed, } @@ -140,7 +140,7 @@ export const FavouriteItem = observer( {!sidebarCollapsed && ( - {favourite.entity_data ? favourite.entity_data.name : favourite.name} + {favorite.entity_data ? favorite.entity_data.name : favorite.name} )} @@ -163,7 +163,7 @@ export const FavouriteItem = observer( customButtonClassName="grid place-items-center" placement="bottom-start" > - handleRemoveFromFavorites(favourite)}> + handleRemoveFromFavorites(favorite)}> Remove from favorites diff --git a/web/core/components/workspace/sidebar/favourites/favourites-menu.tsx b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx similarity index 68% rename from web/core/components/workspace/sidebar/favourites/favourites-menu.tsx rename to web/core/components/workspace/sidebar/favorites/favorites-menu.tsx index c48ac50fac2..42deb3b5a67 100644 --- a/web/core/components/workspace/sidebar/favourites/favourites-menu.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx @@ -6,7 +6,7 @@ import { useParams } from "next/navigation"; import { ChevronRight, FolderPlus } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; // ui -import { IFavourite } from "@plane/types"; +import { IFavorite } from "@plane/types"; import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui"; // constants @@ -14,39 +14,39 @@ import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui"; import { cn } from "@/helpers/common.helper"; // hooks import { useAppTheme } from "@/hooks/store"; -import { useFavourite } from "@/hooks/store/use-favourite"; +import { useFavorite } from "@/hooks/store/use-favorite"; import useLocalStorage from "@/hooks/use-local-storage"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components -import { FavouriteFolder } from "./favourite-folder"; -import { FavouriteItem } from "./favourite-item"; -import { NewFavouriteFolder } from "./new-fav-folder"; -export const SidebarFavouritesMenu = observer(() => { +import { FavoriteFolder } from "./favorite-folder"; +import { FavoriteItem } from "./favorite-item"; +import { NewFavoriteFolder } from "./new-fav-folder"; +export const SidebarFavoritesMenu = observer(() => { //state const [createNewFolder, setCreateNewFolder] = useState(null); const [isScrolled, setIsScrolled] = useState(false); // scroll animation state // store hooks const { sidebarCollapsed } = useAppTheme(); - const { favouriteIds, favouriteMap, deleteFavourite } = useFavourite(); + const { favoriteIds, favoriteMap, deleteFavorite } = useFavorite(); const { workspaceSlug } = useParams(); const { isMobile } = usePlatformOS(); // local storage - const { setValue: toggleFavouriteMenu, storedValue } = useLocalStorage("is_favourite_menu_open", false); + const { setValue: toggleFavoriteMenu, storedValue } = useLocalStorage("is_favorite_menu_open", false); // derived values - const isFavouriteMenuOpen = !!storedValue; + const isFavoriteMenuOpen = !!storedValue; // refs const containerRef = useRef(null); - const handleRemoveFromFavorites = (favourite: IFavourite) => { - deleteFavourite(workspaceSlug.toString(), favourite.id) + const handleRemoveFromFavorites = (favorite: IFavorite) => { + deleteFavorite(workspaceSlug.toString(), favorite.id) .then(() => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", - message: "Favourite removed successfully.", + message: "Favorite removed successfully.", }); }) .catch((err) => { @@ -60,8 +60,8 @@ export const SidebarFavouritesMenu = observer(() => { }); }; useEffect(() => { - if (sidebarCollapsed) toggleFavouriteMenu(true); - }, [sidebarCollapsed, toggleFavouriteMenu]); + if (sidebarCollapsed) toggleFavoriteMenu(true); + }, [sidebarCollapsed, toggleFavoriteMenu]); /** * Implementing scroll animation styles based on the scroll length of the container @@ -88,7 +88,7 @@ export const SidebarFavouritesMenu = observer(() => { ref={containerRef} className={cn("-mr-3 -ml-4 pl-4", { "border-t border-custom-sidebar-border-300": isScrolled, - "vertical-scrollbar h-full !overflow-y-scroll scrollbar-sm": isFavouriteMenuOpen, + "vertical-scrollbar h-full !overflow-y-scroll scrollbar-sm": isFavoriteMenuOpen, })} > @@ -97,28 +97,28 @@ export const SidebarFavouritesMenu = observer(() => { as="button" className="group/workspace-button w-full px-2 py-1.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90 rounded text-xs font-semibold" > - toggleFavouriteMenu(!isFavouriteMenuOpen)} className="flex-1 text-start"> - MY FAVOURITES + toggleFavoriteMenu(!isFavoriteMenuOpen)} className="flex-1 text-start"> + MY FAVORITES { setCreateNewFolder(true); - !isFavouriteMenuOpen && toggleFavouriteMenu(!isFavouriteMenuOpen); + !isFavoriteMenuOpen && toggleFavoriteMenu(!isFavoriteMenuOpen); }} className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform")} /> toggleFavouriteMenu(!isFavouriteMenuOpen)} + onClick={() => toggleFavoriteMenu(!isFavoriteMenuOpen)} className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", { - "rotate-90": isFavouriteMenuOpen, + "rotate-90": isFavoriteMenuOpen, })} /> )} { leaveFrom="transform scale-100 opacity-100" leaveTo="transform scale-95 opacity-0" > - {isFavouriteMenuOpen && ( + {isFavoriteMenuOpen && ( { })} static > - {createNewFolder && } - {favouriteIds - .filter((id) => !favouriteMap[id].parent) + {createNewFolder && } + {favoriteIds + .filter((id) => !favoriteMap[id].parent) .map((id, index) => ( - {favouriteMap[id].is_folder ? ( - ) : ( - + )} ))} diff --git a/web/core/components/workspace/sidebar/favourites/new-fav-folder.tsx b/web/core/components/workspace/sidebar/favorites/new-fav-folder.tsx similarity index 77% rename from web/core/components/workspace/sidebar/favourites/new-fav-folder.tsx rename to web/core/components/workspace/sidebar/favorites/new-fav-folder.tsx index 72981c53e01..da049ee3d22 100644 --- a/web/core/components/workspace/sidebar/favourites/new-fav-folder.tsx +++ b/web/core/components/workspace/sidebar/favorites/new-fav-folder.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef } from "react"; import { useParams } from "next/navigation"; import { Controller, SubmitHandler, useForm } from "react-hook-form"; -import { FavouriteFolderIcon, Input, setToast, TOAST_TYPE } from "@plane/ui"; -import { useFavourite } from "@/hooks/store/use-favourite"; +import { FavoriteFolderIcon, Input, setToast, TOAST_TYPE } from "@plane/ui"; +import { useFavorite } from "@/hooks/store/use-favorite"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; type TForm = { @@ -16,12 +16,12 @@ type TProps = { setCreateNewFolder: (value: boolean | string | null) => void; actionType: "create" | "rename"; defaultName?: string; - favouriteId?: string; + favoriteId?: string; }; -export const NewFavouriteFolder = (props: TProps) => { - const { setCreateNewFolder, actionType, defaultName, favouriteId } = props; +export const NewFavoriteFolder = (props: TProps) => { + const { setCreateNewFolder, actionType, defaultName, favoriteId } = props; const { workspaceSlug } = useParams(); - const { addFavourite, updateFavourite } = useFavourite(); + const { addFavorite, updateFavorite } = useFavorite(); // ref const ref = useRef(null); @@ -42,12 +42,12 @@ export const NewFavouriteFolder = (props: TProps) => { parent: null, project_id: null, }; - addFavourite(workspaceSlug.toString(), formData) + addFavorite(workspaceSlug.toString(), formData) .then(() => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", - message: "Favourite created successfully.", + message: "Favorite created successfully.", }); }) .catch((err) => { @@ -64,15 +64,15 @@ export const NewFavouriteFolder = (props: TProps) => { }; const handleRenameFolder: SubmitHandler = (formData) => { - if (!favouriteId) return; + if (!favoriteId) return; const payload = { name: formData.name, }; - updateFavourite(workspaceSlug.toString(), favouriteId, payload).then(() => { + updateFavorite(workspaceSlug.toString(), favoriteId, payload).then(() => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", - message: "Favourite updated successfully.", + message: "Favorite updated successfully.", }); }); setCreateNewFolder(false); @@ -88,7 +88,7 @@ export const NewFavouriteFolder = (props: TProps) => { }); return (
- +
{ + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useFavorites must be used within StoreProvider"); + return context.favorite; +}; diff --git a/web/core/hooks/store/use-favourite.ts b/web/core/hooks/store/use-favourite.ts deleted file mode 100644 index 86ac36072ae..00000000000 --- a/web/core/hooks/store/use-favourite.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useContext } from "react"; -// mobx store -import { StoreContext } from "@/lib/store-context"; -import { IFavouriteStore } from "@/store/favourite.store"; - -export const useFavourite = (): IFavouriteStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useFavourites must be used within StoreProvider"); - return context.favourite; -}; diff --git a/web/core/layouts/auth-layout/workspace-wrapper.tsx b/web/core/layouts/auth-layout/workspace-wrapper.tsx index a31ac9cc177..37be07fcecc 100644 --- a/web/core/layouts/auth-layout/workspace-wrapper.tsx +++ b/web/core/layouts/auth-layout/workspace-wrapper.tsx @@ -12,7 +12,7 @@ import { LogOut } from "lucide-react"; import { Button, TOAST_TYPE, setToast, Tooltip } from "@plane/ui"; import { LogoSpinner } from "@/components/common"; import { useMember, useProject, useUser, useWorkspace } from "@/hooks/store"; -import { useFavourite } from "@/hooks/store/use-favourite"; +import { useFavorite } from "@/hooks/store/use-favorite"; import { usePlatformOS } from "@/hooks/use-platform-os"; // images import PlaneBlackLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png"; @@ -32,7 +32,7 @@ export const WorkspaceAuthWrapper: FC = observer((props) // store hooks const { membership, signOut, data: currentUser } = useUser(); const { fetchProjects } = useProject(); - const { fetchFavourite } = useFavourite(); + const { fetchFavorite } = useFavorite(); const { workspace: { fetchWorkspaceMembers }, } = useMember(); @@ -70,10 +70,10 @@ export const WorkspaceAuthWrapper: FC = observer((props) : null, { revalidateIfStale: false, revalidateOnFocus: false } ); - // fetch workspace favourite + // fetch workspace favorite useSWR( - workspaceSlug && currentWorkspace ? `WORKSPACE_FAVOURITE_${workspaceSlug}` : null, - workspaceSlug && currentWorkspace ? () => fetchFavourite(workspaceSlug.toString()) : null, + workspaceSlug && currentWorkspace ? `WORKSPACE_FAVORITE_${workspaceSlug}` : null, + workspaceSlug && currentWorkspace ? () => fetchFavorite(workspaceSlug.toString()) : null, { revalidateIfStale: false, revalidateOnFocus: false } ); diff --git a/web/core/services/favourite/favourite.service.ts b/web/core/services/favorite/favorite.service.ts similarity index 61% rename from web/core/services/favourite/favourite.service.ts rename to web/core/services/favorite/favorite.service.ts index 3eb81471dba..77be926f013 100644 --- a/web/core/services/favourite/favourite.service.ts +++ b/web/core/services/favorite/favorite.service.ts @@ -1,17 +1,17 @@ -import type { IFavourite } from "@plane/types"; +import type { IFavorite } from "@plane/types"; // helpers import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; // types -export class FavouriteService extends APIService { +export class FavoriteService extends APIService { constructor() { super(API_BASE_URL); } - async addFavourite(workspaceSlug: string, data: Partial): Promise { - console.log("addFavourite", data); + async addFavorite(workspaceSlug: string, data: Partial): Promise { + console.log("addFavorite", data); return this.post(`/api/workspaces/${workspaceSlug}/user-favorites/`, data) .then((response) => response?.data) .catch((error) => { @@ -19,24 +19,24 @@ export class FavouriteService extends APIService { }); } - async updateFavourite(workspaceSlug: string, favouriteId: string, data: Partial): Promise { - console.log("updateFavourite", data); - return this.patch(`/api/workspaces/${workspaceSlug}/user-favorites/${favouriteId}/`, data) + async updateFavorite(workspaceSlug: string, favoriteId: string, data: Partial): Promise { + console.log("updateFavorite", data); + return this.patch(`/api/workspaces/${workspaceSlug}/user-favorites/${favoriteId}/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async deleteFavourite(workspaceSlug: string, favouriteId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/user-favorites/${favouriteId}/`) + async deleteFavorite(workspaceSlug: string, favoriteId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/user-favorites/${favoriteId}/`) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async getFavourites(workspaceSlug: string): Promise { + async getFavorites(workspaceSlug: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/user-favorites/`, { params: { all: true, @@ -48,8 +48,8 @@ export class FavouriteService extends APIService { }); } - async getGroupedFavourites(workspaceSlug: string, favouriteId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/user-favorites/${favouriteId}/group/`) + async getGroupedFavorites(workspaceSlug: string, favoriteId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/user-favorites/${favoriteId}/group/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/web/core/services/favorite/index.ts b/web/core/services/favorite/index.ts new file mode 100644 index 00000000000..41df23a178d --- /dev/null +++ b/web/core/services/favorite/index.ts @@ -0,0 +1 @@ +export * from "./favorite.service"; diff --git a/web/core/services/favourite/index.ts b/web/core/services/favourite/index.ts deleted file mode 100644 index 0408c0ea1e2..00000000000 --- a/web/core/services/favourite/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./favourite.service"; diff --git a/web/core/store/cycle.store.ts b/web/core/store/cycle.store.ts index 4ddf8fac2dc..2e5c880246d 100644 --- a/web/core/store/cycle.store.ts +++ b/web/core/store/cycle.store.ts @@ -552,7 +552,7 @@ export class CycleStore implements ICycleStore { if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], true); }); // updating through api. - const response = await this.rootStore.favourite.addFavourite(workspaceSlug.toString(), { + const response = await this.rootStore.favorite.addFavorite(workspaceSlug.toString(), { entity_type: "cycle", entity_identifier: cycleId, project_id: projectId, @@ -579,7 +579,7 @@ export class CycleStore implements ICycleStore { runInAction(() => { if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], false); }); - const response = await this.rootStore.favourite.removeFavouriteEntity(workspaceSlug, cycleId); + const response = await this.rootStore.favorite.removeFavoriteEntity(workspaceSlug, cycleId); return response; } catch (error) { runInAction(() => { diff --git a/web/core/store/favorite.store.ts b/web/core/store/favorite.store.ts new file mode 100644 index 00000000000..69cee7814bd --- /dev/null +++ b/web/core/store/favorite.store.ts @@ -0,0 +1,233 @@ +import { uniqBy } from "lodash"; +import set from "lodash/set"; +import { action, observable, makeObservable, runInAction } from "mobx"; +import { IFavorite } from "@plane/types"; +import { FavoriteService } from "@/services/favorite"; + +export interface IFavoriteStore { + // observables + + favoriteIds: string[]; + favoriteMap: { + [favoriteId: string]: IFavorite; + }; + entityMap: { + [entityId: string]: IFavorite; + }; + // computed actions + // actions + fetchFavorite: (workspaceSlug: string) => Promise; + // CRUD actions + addFavorite: (workspaceSlug: string, data: Partial) => Promise; + updateFavorite: (workspaceSlug: string, favoriteId: string, data: Partial) => Promise; + deleteFavorite: (workspaceSlug: string, favoriteId: string) => Promise; + getGroupedFavorites: (workspaceSlug: string, favoriteId: string) => Promise; + moveFavorite: (workspaceSlug: string, favoriteId: string, data: Partial) => Promise; + removeFavoriteEntity: (workspaceSlug: string, entityId: string) => Promise; +} + +export class FavoriteStore implements IFavoriteStore { + // observables + favoriteIds: string[] = []; + favoriteMap: { + [favoriteId: string]: IFavorite; + } = {}; + entityMap: { + [entityId: string]: IFavorite; + } = {}; + // service + favoriteService; + + constructor() { + makeObservable(this, { + // observable + favoriteMap: observable, + entityMap: observable, + favoriteIds: observable, + // action + fetchFavorite: action, + // CRUD actions + addFavorite: action, + getGroupedFavorites: action, + moveFavorite: action, + removeFavoriteEntity: action, + }); + this.favoriteService = new FavoriteService(); + } + + /** + * Creates a favorite in the workspace and adds it to the store + * @param workspaceSlug + * @param data + * @returns Promise + */ + addFavorite = async (workspaceSlug: string, data: Partial) => { + try { + data = { ...data, parent: null, is_folder: data.entity_type === "folder" }; + const response = await this.favoriteService.addFavorite(workspaceSlug, data); + runInAction(() => { + set(this.favoriteMap, [response.id], response); + response.entity_identifier && set(this.entityMap, [response.entity_identifier], response); + this.favoriteIds = [response.id, ...this.favoriteIds]; + }); + return response; + } catch (error) { + console.log("Failed to create favorite from favorite store"); + throw error; + } + }; + + /** + * Updates a favorite in the workspace and updates the store + * @param workspaceSlug + * @param favoriteId + * @param data + * @returns Promise + */ + updateFavorite = async (workspaceSlug: string, favoriteId: string, data: Partial) => { + try { + const response = await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, data); + runInAction(() => { + set(this.favoriteMap, [response.id], response); + }); + return response; + } catch (error) { + console.log("Failed to update favorite from favorite store"); + throw error; + } + }; + + /** + * Moves a favorite in the workspace and updates the store + * @param workspaceSlug + * @param favoriteId + * @param data + * @returns Promise + */ + moveFavorite = async (workspaceSlug: string, favoriteId: string, data: Partial) => { + try { + const response = await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, data); + runInAction(() => { + // add the favorite to the new parent + if (!data.parent) return; + set(this.favoriteMap, [data.parent], { + ...this.favoriteMap[data.parent], + children: [response, ...this.favoriteMap[data.parent].children], + }); + + // remove the favorite from the old parent + const oldParent = this.favoriteMap[favoriteId].parent; + if (oldParent) { + set(this.favoriteMap, [oldParent], { + ...this.favoriteMap[oldParent], + children: this.favoriteMap[oldParent].children.filter((child) => child.id !== favoriteId), + }); + } + + // add parent of the favorite + set(this.favoriteMap, [favoriteId], { + ...this.favoriteMap[favoriteId], + parent: data.parent, + }); + }); + } catch (error) { + console.log("Failed to move favorite from favorite store"); + throw error; + } + }; + + /** + * Deletes a favorite from the workspace and updates the store + * @param workspaceSlug + * @param favoriteId + * @returns Promise + */ + deleteFavorite = async (workspaceSlug: string, favoriteId: string) => { + try { + await this.favoriteService.deleteFavorite(workspaceSlug, favoriteId); + runInAction(() => { + const parent = this.favoriteMap[favoriteId].parent; + if (parent) { + set(this.favoriteMap, [parent], { + ...this.favoriteMap[parent], + children: this.favoriteMap[parent].children.filter((child) => child.id !== favoriteId), + }); + } + delete this.favoriteMap[favoriteId]; + this.favoriteIds = this.favoriteIds.filter((id) => id !== favoriteId); + }); + } catch (error) { + console.log("Failed to delete favorite from favorite store"); + throw error; + } + }; + + /** + * Removes a favorite entity from the workspace and updates the store + * @param workspaceSlug + * @param entityId + * @returns Promise + */ + removeFavoriteEntity = async (workspaceSlug: string, entityId: string) => { + try { + const favoriteId = this.entityMap[entityId].id; + await this.deleteFavorite(workspaceSlug, favoriteId); + runInAction(() => { + delete this.entityMap[entityId]; + }); + } catch (error) { + console.log("Failed to remove favorite entity from favorite store"); + throw error; + } + }; + /** + * get Grouped Favorites + * @param workspaceSlug + * @param favoriteId + * @returns Promise + */ + getGroupedFavorites = async (workspaceSlug: string, favoriteId: string) => { + try { + const response = await this.favoriteService.getGroupedFavorites(workspaceSlug, favoriteId); + runInAction(() => { + // add children to the favorite + set(this.favoriteMap, [favoriteId], { ...this.favoriteMap[favoriteId], children: response }); + // add the favorites to the map + response.forEach((favorite) => { + set(this.favoriteMap, [favorite.id], favorite); + this.favoriteIds.push(favorite.id); + this.favoriteIds = uniqBy(this.favoriteIds, (id) => id); + favorite.entity_identifier && set(this.entityMap, [favorite.entity_identifier], favorite); + }); + }); + + return response; + } catch (error) { + console.log("Failed to get grouped favorites from favorite store"); + throw error; + } + }; + + /** + * get Workspace favorite using workspace slug + * @param workspaceSlug + * @returns Promise + * + */ + fetchFavorite = async (workspaceSlug: string) => { + try { + const favorites = await this.favoriteService.getFavorites(workspaceSlug); + runInAction(() => { + favorites.forEach((favorite) => { + set(this.favoriteMap, [favorite.id], favorite); + this.favoriteIds.push(favorite.id); + favorite.entity_identifier && set(this.entityMap, [favorite.entity_identifier], favorite); + }); + }); + return favorites; + } catch (error) { + console.log("Failed to fetch favorites from workspace store"); + throw error; + } + }; +} diff --git a/web/core/store/favourite.store.ts b/web/core/store/favourite.store.ts deleted file mode 100644 index a01ca1efc8c..00000000000 --- a/web/core/store/favourite.store.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { uniqBy } from "lodash"; -import set from "lodash/set"; -import { action, observable, makeObservable, runInAction } from "mobx"; -import { IFavourite } from "@plane/types"; -import { FavouriteService } from "@/services/favourite"; - -export interface IFavouriteStore { - // observables - - favouriteIds: string[]; - favouriteMap: { - [favouriteId: string]: IFavourite; - }; - entityMap: { - [entityId: string]: IFavourite; - }; - // computed actions - // actions - fetchFavourite: (workspaceSlug: string) => Promise; - // CRUD actions - addFavourite: (workspaceSlug: string, data: Partial) => Promise; - updateFavourite: (workspaceSlug: string, favouriteId: string, data: Partial) => Promise; - deleteFavourite: (workspaceSlug: string, favouriteId: string) => Promise; - getGroupedFavourites: (workspaceSlug: string, favouriteId: string) => Promise; - moveFavourite: (workspaceSlug: string, favouriteId: string, data: Partial) => Promise; - removeFavouriteEntity: (workspaceSlug: string, entityId: string) => Promise; -} - -export class FavouriteStore implements IFavouriteStore { - // observables - favouriteIds: string[] = []; - favouriteMap: { - [favouriteId: string]: IFavourite; - } = {}; - entityMap: { - [entityId: string]: IFavourite; - } = {}; - // service - favouriteService; - - constructor() { - makeObservable(this, { - // observable - favouriteMap: observable, - entityMap: observable, - favouriteIds: observable, - // action - fetchFavourite: action, - // CRUD actions - addFavourite: action, - getGroupedFavourites: action, - moveFavourite: action, - removeFavouriteEntity: action, - }); - this.favouriteService = new FavouriteService(); - } - - /** - * Creates a favourite in the workspace and adds it to the store - * @param workspaceSlug - * @param data - * @returns Promise - */ - addFavourite = async (workspaceSlug: string, data: Partial) => { - try { - data = { ...data, parent: null, is_folder: data.entity_type === "folder" }; - const response = await this.favouriteService.addFavourite(workspaceSlug, data); - runInAction(() => { - set(this.favouriteMap, [response.id], response); - response.entity_identifier && set(this.entityMap, [response.entity_identifier], response); - this.favouriteIds = [response.id, ...this.favouriteIds]; - }); - return response; - } catch (error) { - console.log("Failed to create favourite from favourite store"); - throw error; - } - }; - - /** - * Updates a favourite in the workspace and updates the store - * @param workspaceSlug - * @param favouriteId - * @param data - * @returns Promise - */ - updateFavourite = async (workspaceSlug: string, favouriteId: string, data: Partial) => { - try { - const response = await this.favouriteService.updateFavourite(workspaceSlug, favouriteId, data); - runInAction(() => { - set(this.favouriteMap, [response.id], response); - }); - return response; - } catch (error) { - console.log("Failed to update favourite from favourite store"); - throw error; - } - }; - - /** - * Moves a favourite in the workspace and updates the store - * @param workspaceSlug - * @param favouriteId - * @param data - * @returns Promise - */ - moveFavourite = async (workspaceSlug: string, favouriteId: string, data: Partial) => { - try { - const response = await this.favouriteService.updateFavourite(workspaceSlug, favouriteId, data); - runInAction(() => { - // add the favourite to the new parent - if (!data.parent) return; - set(this.favouriteMap, [data.parent], { - ...this.favouriteMap[data.parent], - children: [response, ...this.favouriteMap[data.parent].children], - }); - - // remove the favourite from the old parent - const oldParent = this.favouriteMap[favouriteId].parent; - if (oldParent) { - set(this.favouriteMap, [oldParent], { - ...this.favouriteMap[oldParent], - children: this.favouriteMap[oldParent].children.filter((child) => child.id !== favouriteId), - }); - } - - // add parent of the favourite - set(this.favouriteMap, [favouriteId], { - ...this.favouriteMap[favouriteId], - parent: data.parent, - }); - }); - } catch (error) { - console.log("Failed to move favourite from favourite store"); - throw error; - } - }; - - /** - * Deletes a favourite from the workspace and updates the store - * @param workspaceSlug - * @param favouriteId - * @returns Promise - */ - deleteFavourite = async (workspaceSlug: string, favouriteId: string) => { - try { - await this.favouriteService.deleteFavourite(workspaceSlug, favouriteId); - runInAction(() => { - const parent = this.favouriteMap[favouriteId].parent; - if (parent) { - set(this.favouriteMap, [parent], { - ...this.favouriteMap[parent], - children: this.favouriteMap[parent].children.filter((child) => child.id !== favouriteId), - }); - } - delete this.favouriteMap[favouriteId]; - this.favouriteIds = this.favouriteIds.filter((id) => id !== favouriteId); - }); - } catch (error) { - console.log("Failed to delete favourite from favourite store"); - throw error; - } - }; - - /** - * Removes a favourite entity from the workspace and updates the store - * @param workspaceSlug - * @param entityId - * @returns Promise - */ - removeFavouriteEntity = async (workspaceSlug: string, entityId: string) => { - try { - const favouriteId = this.entityMap[entityId].id; - await this.deleteFavourite(workspaceSlug, favouriteId); - runInAction(() => { - delete this.entityMap[entityId]; - }); - } catch (error) { - console.log("Failed to remove favourite entity from favourite store"); - throw error; - } - }; - /** - * get Grouped Favourites - * @param workspaceSlug - * @param favouriteId - * @returns Promise - */ - getGroupedFavourites = async (workspaceSlug: string, favouriteId: string) => { - try { - const response = await this.favouriteService.getGroupedFavourites(workspaceSlug, favouriteId); - runInAction(() => { - // add children to the favourite - set(this.favouriteMap, [favouriteId], { ...this.favouriteMap[favouriteId], children: response }); - // add the favourites to the map - response.forEach((favorite) => { - set(this.favouriteMap, [favorite.id], favorite); - this.favouriteIds.push(favorite.id); - this.favouriteIds = uniqBy(this.favouriteIds, (id) => id); - favorite.entity_identifier && set(this.entityMap, [favorite.entity_identifier], favorite); - }); - }); - - return response; - } catch (error) { - console.log("Failed to get grouped favourites from favourite store"); - throw error; - } - }; - - /** - * get Workspace favourite using workspace slug - * @param workspaceSlug - * @returns Promise - * - */ - fetchFavourite = async (workspaceSlug: string) => { - try { - const favourites = await this.favouriteService.getFavourites(workspaceSlug); - runInAction(() => { - favourites.forEach((favourite) => { - set(this.favouriteMap, [favourite.id], favourite); - this.favouriteIds.push(favourite.id); - favourite.entity_identifier && set(this.entityMap, [favourite.entity_identifier], favourite); - }); - }); - return favourites; - } catch (error) { - console.log("Failed to fetch favourites from workspace store"); - throw error; - } - }; -} diff --git a/web/core/store/module.store.ts b/web/core/store/module.store.ts index ede31936a23..e469139c8be 100644 --- a/web/core/store/module.store.ts +++ b/web/core/store/module.store.ts @@ -486,7 +486,7 @@ export class ModulesStore implements IModuleStore { runInAction(() => { set(this.moduleMap, [moduleId, "is_favorite"], true); }); - await this.rootStore.favourite.addFavourite(workspaceSlug.toString(), { + await this.rootStore.favorite.addFavorite(workspaceSlug.toString(), { entity_type: "module", entity_identifier: moduleId, project_id: projectId, @@ -513,7 +513,7 @@ export class ModulesStore implements IModuleStore { runInAction(() => { set(this.moduleMap, [moduleId, "is_favorite"], false); }); - await this.rootStore.favourite.removeFavouriteEntity(workspaceSlug, moduleId); + await this.rootStore.favorite.removeFavoriteEntity(workspaceSlug, moduleId); } catch (error) { console.error("Failed to remove module from favorites in module store", error); runInAction(() => { diff --git a/web/core/store/pages/page.ts b/web/core/store/pages/page.ts index 06e58fa5113..81d403d99e2 100644 --- a/web/core/store/pages/page.ts +++ b/web/core/store/pages/page.ts @@ -479,8 +479,8 @@ export class Page implements IPage { runInAction(() => { this.is_favorite = true; }); - await this.rootStore.favourite - .addFavourite(workspaceSlug.toString(), { + await this.rootStore.favorite + .addFavorite(workspaceSlug.toString(), { entity_type: "page", entity_identifier: this.id, project_id: projectId, @@ -505,7 +505,7 @@ export class Page implements IPage { this.is_favorite = false; }); - await this.rootStore.favourite.removeFavouriteEntity(workspaceSlug, this.id).catch((error) => { + await this.rootStore.favorite.removeFavoriteEntity(workspaceSlug, this.id).catch((error) => { runInAction(() => { this.is_favorite = pageIsFavorite; }); diff --git a/web/core/store/project-view.store.ts b/web/core/store/project-view.store.ts index 20c6b0a5b52..b31bc23a789 100644 --- a/web/core/store/project-view.store.ts +++ b/web/core/store/project-view.store.ts @@ -358,7 +358,7 @@ export class ProjectViewStore implements IProjectViewStore { runInAction(() => { set(this.viewMap, [viewId, "is_favorite"], true); }); - await this.rootStore.favourite.addFavourite(workspaceSlug.toString(), { + await this.rootStore.favorite.addFavorite(workspaceSlug.toString(), { entity_type: "view", entity_identifier: viewId, project_id: projectId, @@ -385,7 +385,7 @@ export class ProjectViewStore implements IProjectViewStore { runInAction(() => { set(this.viewMap, [viewId, "is_favorite"], false); }); - await this.rootStore.favourite.removeFavouriteEntity(workspaceSlug, viewId); + await this.rootStore.favorite.removeFavoriteEntity(workspaceSlug, viewId); } catch (error) { console.error("Failed to remove view from favorites in view store", error); runInAction(() => { diff --git a/web/core/store/project/project.store.ts b/web/core/store/project/project.store.ts index 1a529684982..c706503dea6 100644 --- a/web/core/store/project/project.store.ts +++ b/web/core/store/project/project.store.ts @@ -279,7 +279,7 @@ export class ProjectStore implements IProjectStore { runInAction(() => { set(this.projectMap, [projectId, "is_favorite"], true); }); - const response = await this.rootStore.favourite.addFavourite(workspaceSlug.toString(), { + const response = await this.rootStore.favorite.addFavorite(workspaceSlug.toString(), { entity_type: "project", entity_identifier: projectId, project_id: projectId, @@ -304,7 +304,7 @@ export class ProjectStore implements IProjectStore { try { const currentProject = this.getProjectById(projectId); if (!currentProject.is_favorite) return; - const response = await this.rootStore.favourite.removeFavouriteEntity(workspaceSlug.toString(), projectId); + const response = await this.rootStore.favorite.removeFavoriteEntity(workspaceSlug.toString(), projectId); runInAction(() => { set(this.projectMap, [projectId, "is_favorite"], false); diff --git a/web/core/store/root.store.ts b/web/core/store/root.store.ts index 57f6de68ce4..1bab15b34d1 100644 --- a/web/core/store/root.store.ts +++ b/web/core/store/root.store.ts @@ -6,7 +6,7 @@ import { CycleFilterStore, ICycleFilterStore } from "./cycle_filter.store"; import { DashboardStore, IDashboardStore } from "./dashboard.store"; import { IProjectEstimateStore, ProjectEstimateStore } from "./estimates/project-estimate.store"; import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store"; -import { FavouriteStore, IFavouriteStore } from "./favourite.store"; +import { FavoriteStore, IFavoriteStore } from "./favorite.store"; import { GlobalViewStore, IGlobalViewStore } from "./global-view.store"; import { IProjectInboxStore, ProjectInboxStore } from "./inbox/project-inbox.store"; import { InstanceStore, IInstanceStore } from "./instance.store"; @@ -53,7 +53,7 @@ export class CoreRootStore { projectEstimate: IProjectEstimateStore; multipleSelect: IMultipleSelectStore; workspaceNotification: IWorkspaceNotificationStore; - favourite: IFavouriteStore; + favorite: IFavoriteStore; constructor() { this.router = new RouterStore(); @@ -80,7 +80,7 @@ export class CoreRootStore { this.projectPages = new ProjectPageStore(this); this.projectEstimate = new ProjectEstimateStore(this); this.workspaceNotification = new WorkspaceNotificationStore(this); - this.favourite = new FavouriteStore(); + this.favorite = new FavoriteStore(); } resetOnSignOut() { From 049b96e3331ca865407ee7e07445ae4bdad89919 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 31 Jul 2024 20:40:18 +0530 Subject: [PATCH 12/16] chore: hard delete favorites --- apiserver/plane/app/views/cycle/base.py | 2 +- apiserver/plane/app/views/module/base.py | 2 +- apiserver/plane/app/views/page/base.py | 2 +- apiserver/plane/app/views/project/base.py | 2 +- apiserver/plane/app/views/view/base.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 1258aa6086d..db2a73d4c95 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -1154,7 +1154,7 @@ def destroy(self, request, slug, project_id, cycle_id): workspace__slug=slug, entity_identifier=cycle_id, ) - cycle_favorite.delete() + cycle_favorite.delete(soft=False) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 4d1203f077a..c6861fe4bf7 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -840,7 +840,7 @@ def destroy(self, request, slug, project_id, module_id): entity_type="module", entity_identifier=module_id, ) - module_favorite.delete() + module_favorite.delete(soft=False) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py index b0d56064972..9757b16acbc 100644 --- a/apiserver/plane/app/views/page/base.py +++ b/apiserver/plane/app/views/page/base.py @@ -401,7 +401,7 @@ def destroy(self, request, slug, project_id, pk): entity_identifier=pk, entity_type="page", ) - page_favorite.delete() + page_favorite.delete(soft=False) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index bca8236a98b..22181dacff9 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -599,7 +599,7 @@ def destroy(self, request, slug, project_id): user=request.user, workspace__slug=slug, ) - project_favorite.delete() + project_favorite.delete(soft=False) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py index 236a051aaa9..9881088317d 100644 --- a/apiserver/plane/app/views/view/base.py +++ b/apiserver/plane/app/views/view/base.py @@ -474,5 +474,5 @@ def destroy(self, request, slug, project_id, view_id): entity_type="view", entity_identifier=view_id, ) - view_favorite.delete() + view_favorite.delete(soft=False) return Response(status=status.HTTP_204_NO_CONTENT) From d51953f3ca78f64ef0beaffccb8c03bb61f8ae39 Mon Sep 17 00:00:00 2001 From: gakshita Date: Thu, 1 Aug 2024 12:24:25 +0530 Subject: [PATCH 13/16] fix: review changes --- .../sidebar/favorites/favorites-menu.tsx | 12 +++-- .../sidebar/favorites/new-fav-folder.tsx | 12 +++-- .../services/favorite/favorite.service.ts | 2 - web/core/store/favorite.store.ts | 44 +++++++++---------- 4 files changed, 30 insertions(+), 40 deletions(-) diff --git a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx index 42deb3b5a67..ced7de0f024 100644 --- a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx @@ -49,13 +49,11 @@ export const SidebarFavoritesMenu = observer(() => { message: "Favorite removed successfully.", }); }) - .catch((err) => { - Object.keys(err.data).map((key) => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: err.data[key], - }); + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Something went wrong!", }); }); }; diff --git a/web/core/components/workspace/sidebar/favorites/new-fav-folder.tsx b/web/core/components/workspace/sidebar/favorites/new-fav-folder.tsx index da049ee3d22..e17a4c78777 100644 --- a/web/core/components/workspace/sidebar/favorites/new-fav-folder.tsx +++ b/web/core/components/workspace/sidebar/favorites/new-fav-folder.tsx @@ -50,13 +50,11 @@ export const NewFavoriteFolder = (props: TProps) => { message: "Favorite created successfully.", }); }) - .catch((err) => { - Object.keys(err.data).map((key) => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: err.data[key], - }); + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Something went wrong!", }); }); setCreateNewFolder(false); diff --git a/web/core/services/favorite/favorite.service.ts b/web/core/services/favorite/favorite.service.ts index 77be926f013..8a4963cdf3f 100644 --- a/web/core/services/favorite/favorite.service.ts +++ b/web/core/services/favorite/favorite.service.ts @@ -11,7 +11,6 @@ export class FavoriteService extends APIService { } async addFavorite(workspaceSlug: string, data: Partial): Promise { - console.log("addFavorite", data); return this.post(`/api/workspaces/${workspaceSlug}/user-favorites/`, data) .then((response) => response?.data) .catch((error) => { @@ -20,7 +19,6 @@ export class FavoriteService extends APIService { } async updateFavorite(workspaceSlug: string, favoriteId: string, data: Partial): Promise { - console.log("updateFavorite", data); return this.patch(`/api/workspaces/${workspaceSlug}/user-favorites/${favoriteId}/`, data) .then((response) => response?.data) .catch((error) => { diff --git a/web/core/store/favorite.store.ts b/web/core/store/favorite.store.ts index 69cee7814bd..e35aaa0ca86 100644 --- a/web/core/store/favorite.store.ts +++ b/web/core/store/favorite.store.ts @@ -72,7 +72,7 @@ export class FavoriteStore implements IFavoriteStore { }); return response; } catch (error) { - console.log("Failed to create favorite from favorite store"); + console.error("Failed to create favorite from favorite store"); throw error; } }; @@ -92,7 +92,7 @@ export class FavoriteStore implements IFavoriteStore { }); return response; } catch (error) { - console.log("Failed to update favorite from favorite store"); + console.error("Failed to update favorite from favorite store"); throw error; } }; @@ -110,28 +110,23 @@ export class FavoriteStore implements IFavoriteStore { runInAction(() => { // add the favorite to the new parent if (!data.parent) return; - set(this.favoriteMap, [data.parent], { - ...this.favoriteMap[data.parent], - children: [response, ...this.favoriteMap[data.parent].children], - }); + set(this.favoriteMap, [data.parent, "children"], [response, ...this.favoriteMap[data.parent].children]); // remove the favorite from the old parent const oldParent = this.favoriteMap[favoriteId].parent; if (oldParent) { - set(this.favoriteMap, [oldParent], { - ...this.favoriteMap[oldParent], - children: this.favoriteMap[oldParent].children.filter((child) => child.id !== favoriteId), - }); + set( + this.favoriteMap, + [oldParent, "children"], + this.favoriteMap[oldParent].children.filter((child) => child.id !== favoriteId) + ); } // add parent of the favorite - set(this.favoriteMap, [favoriteId], { - ...this.favoriteMap[favoriteId], - parent: data.parent, - }); + set(this.favoriteMap, [favoriteId, "parent"], data.parent); }); } catch (error) { - console.log("Failed to move favorite from favorite store"); + console.error("Failed to move favorite from favorite store"); throw error; } }; @@ -148,16 +143,17 @@ export class FavoriteStore implements IFavoriteStore { runInAction(() => { const parent = this.favoriteMap[favoriteId].parent; if (parent) { - set(this.favoriteMap, [parent], { - ...this.favoriteMap[parent], - children: this.favoriteMap[parent].children.filter((child) => child.id !== favoriteId), - }); + set( + this.favoriteMap, + [parent, "children"], + this.favoriteMap[parent].children.filter((child) => child.id !== favoriteId) + ); } delete this.favoriteMap[favoriteId]; this.favoriteIds = this.favoriteIds.filter((id) => id !== favoriteId); }); } catch (error) { - console.log("Failed to delete favorite from favorite store"); + console.error("Failed to delete favorite from favorite store"); throw error; } }; @@ -176,7 +172,7 @@ export class FavoriteStore implements IFavoriteStore { delete this.entityMap[entityId]; }); } catch (error) { - console.log("Failed to remove favorite entity from favorite store"); + console.error("Failed to remove favorite entity from favorite store"); throw error; } }; @@ -191,7 +187,7 @@ export class FavoriteStore implements IFavoriteStore { const response = await this.favoriteService.getGroupedFavorites(workspaceSlug, favoriteId); runInAction(() => { // add children to the favorite - set(this.favoriteMap, [favoriteId], { ...this.favoriteMap[favoriteId], children: response }); + set(this.favoriteMap, [favoriteId, "children"], response); // add the favorites to the map response.forEach((favorite) => { set(this.favoriteMap, [favorite.id], favorite); @@ -203,7 +199,7 @@ export class FavoriteStore implements IFavoriteStore { return response; } catch (error) { - console.log("Failed to get grouped favorites from favorite store"); + console.error("Failed to get grouped favorites from favorite store"); throw error; } }; @@ -226,7 +222,7 @@ export class FavoriteStore implements IFavoriteStore { }); return favorites; } catch (error) { - console.log("Failed to fetch favorites from workspace store"); + console.error("Failed to fetch favorites from workspace store"); throw error; } }; From eff77c3bfe0bbc8bb44cbe317ddf1f88a67ab018 Mon Sep 17 00:00:00 2001 From: gakshita Date: Thu, 1 Aug 2024 12:31:31 +0530 Subject: [PATCH 14/16] fix: added optimistic addition to the store --- web/core/store/cycle.store.ts | 1 + web/core/store/favorite.store.ts | 20 ++++++++++++++++++-- web/core/store/module.store.ts | 1 + web/core/store/pages/page.ts | 1 + web/core/store/project-view.store.ts | 1 + web/core/store/project/project.store.ts | 1 + 6 files changed, 23 insertions(+), 2 deletions(-) diff --git a/web/core/store/cycle.store.ts b/web/core/store/cycle.store.ts index 2e5c880246d..736881e6de5 100644 --- a/web/core/store/cycle.store.ts +++ b/web/core/store/cycle.store.ts @@ -556,6 +556,7 @@ export class CycleStore implements ICycleStore { entity_type: "cycle", entity_identifier: cycleId, project_id: projectId, + entity_data: { name: this.cycleMap[cycleId].name || "" }, }); return response; } catch (error) { diff --git a/web/core/store/favorite.store.ts b/web/core/store/favorite.store.ts index e35aaa0ca86..f6a56e450d9 100644 --- a/web/core/store/favorite.store.ts +++ b/web/core/store/favorite.store.ts @@ -1,6 +1,7 @@ import { uniqBy } from "lodash"; import set from "lodash/set"; import { action, observable, makeObservable, runInAction } from "mobx"; +import { v4 as uuidv4 } from "uuid"; import { IFavorite } from "@plane/types"; import { FavoriteService } from "@/services/favorite"; @@ -62,16 +63,31 @@ export class FavoriteStore implements IFavoriteStore { * @returns Promise */ addFavorite = async (workspaceSlug: string, data: Partial) => { + const id = uuidv4(); + data = { ...data, parent: null, is_folder: data.entity_type === "folder" }; + try { - data = { ...data, parent: null, is_folder: data.entity_type === "folder" }; + // optimistic addition + runInAction(() => { + set(this.favoriteMap, [id], data); + data.entity_identifier && set(this.entityMap, [data.entity_identifier], data); + this.favoriteIds = [id, ...this.favoriteIds]; + }); const response = await this.favoriteService.addFavorite(workspaceSlug, data); + + // overwrite the temp id runInAction(() => { + delete this.favoriteMap[id]; set(this.favoriteMap, [response.id], response); response.entity_identifier && set(this.entityMap, [response.entity_identifier], response); - this.favoriteIds = [response.id, ...this.favoriteIds]; + this.favoriteIds = [response.id, ...this.favoriteIds.filter((favId) => favId !== id)]; }); return response; } catch (error) { + delete this.favoriteMap[id]; + data.entity_identifier && delete this.entityMap[data.entity_identifier]; + this.favoriteIds = this.favoriteIds.filter((favId) => favId !== id); + console.error("Failed to create favorite from favorite store"); throw error; } diff --git a/web/core/store/module.store.ts b/web/core/store/module.store.ts index e469139c8be..2c095341928 100644 --- a/web/core/store/module.store.ts +++ b/web/core/store/module.store.ts @@ -490,6 +490,7 @@ export class ModulesStore implements IModuleStore { entity_type: "module", entity_identifier: moduleId, project_id: projectId, + entity_data: { name: this.moduleMap[moduleId].name || "" }, }); } catch (error) { console.error("Failed to add module to favorites in module store", error); diff --git a/web/core/store/pages/page.ts b/web/core/store/pages/page.ts index 81d403d99e2..6a7ba74c312 100644 --- a/web/core/store/pages/page.ts +++ b/web/core/store/pages/page.ts @@ -484,6 +484,7 @@ export class Page implements IPage { entity_type: "page", entity_identifier: this.id, project_id: projectId, + entity_data: { name: this.name || "" }, }) .catch((error) => { runInAction(() => { diff --git a/web/core/store/project-view.store.ts b/web/core/store/project-view.store.ts index b31bc23a789..60bef412490 100644 --- a/web/core/store/project-view.store.ts +++ b/web/core/store/project-view.store.ts @@ -362,6 +362,7 @@ export class ProjectViewStore implements IProjectViewStore { entity_type: "view", entity_identifier: viewId, project_id: projectId, + entity_data: { name: this.viewMap[viewId].name || "" }, }); } catch (error) { console.error("Failed to add view to favorites in view store", error); diff --git a/web/core/store/project/project.store.ts b/web/core/store/project/project.store.ts index c706503dea6..f4d6d3e1a3f 100644 --- a/web/core/store/project/project.store.ts +++ b/web/core/store/project/project.store.ts @@ -283,6 +283,7 @@ export class ProjectStore implements IProjectStore { entity_type: "project", entity_identifier: projectId, project_id: projectId, + entity_data: { name: this.projectMap[projectId].name || "" }, }); return response; } catch (error) { From c4f3f9ae21fe054314ec8e834ffb745ccadde7ab Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Thu, 1 Aug 2024 13:00:07 +0530 Subject: [PATCH 15/16] chore: user favorite hard delete --- apiserver/plane/app/views/workspace/favorite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/app/views/workspace/favorite.py b/apiserver/plane/app/views/workspace/favorite.py index 68239bfe6b4..d4fe6a622fd 100644 --- a/apiserver/plane/app/views/workspace/favorite.py +++ b/apiserver/plane/app/views/workspace/favorite.py @@ -62,7 +62,7 @@ def delete(self, request, slug, favorite_id): favorite = UserFavorite.objects.get( user=request.user, workspace__slug=slug, pk=favorite_id ) - favorite.delete() + favorite.delete(soft=False) return Response(status=status.HTTP_204_NO_CONTENT) From e70007112257080c8092fe4684b8b783685be249 Mon Sep 17 00:00:00 2001 From: gakshita Date: Thu, 1 Aug 2024 22:45:20 +0530 Subject: [PATCH 16/16] fix: linting fixed --- .../components/workspace/sidebar/favorites/favorite-folder.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx b/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx index 9e599d38982..3096ae7099b 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx @@ -88,6 +88,7 @@ export const FavoriteFolder: React.FC = (props) => { setIsDragging(true); }, onDrop: ({ self, source }) => { + setInstruction(undefined); setIsDragging(false); const sourceId = source?.data?.id as string | undefined; const destinationId = self?.data?.id as string | undefined;