From 1acaae9d5e5950b59b5889bf09b59db14ff44b3c Mon Sep 17 00:00:00 2001 From: Sangeetha Date: Mon, 6 Jan 2025 17:36:10 +0530 Subject: [PATCH] [WEB-3038]feat: home preferences (#6308) * WIP * WIP * WIP * WIP * Create home preference if not exist * chore: handled the unique state name validation (#6299) * fix: changed the response structure (#6301) * [WEB-1964]chore: cycles actions restructuring (#6298) * chore: cycles quick actions restructuring * chore: added additional actions to cycle list actions * chore: cycle quick action structure * chore: added additional actions to cycle list actions * chore: added end cycle hook * fix: updated end cycle export --------- Co-authored-by: gurusinath * fix: active cycle graph tooltip and endpoint validation (#6306) * [WEB-2870]feat: language support (#6215) * fix: adding language support package * fix: language support implementation using mobx * fix: adding more languages for support * fix: profile settings translations * feat: added language support for sidebar and user settings * feat: added language support for deactivation modal * fix: added project sync after transfer issues (#6200) * code refactor and improvement (#6203) * chore: package code refactoring * chore: component restructuring and refactor * chore: comment create improvement * refactor: enhance workspace and project wrapper modularity (#6207) * [WEB-2678]feat: added functionality to add labels directly from dropdown (#6211) * enhancement:added functionality to add features directly from dropdown * fix: fixed import order * fix: fixed lint errors * chore: added common component for project activity (#6212) * chore: added common component for project activity * fix: added enum * fix: added enum for initiatives * - Do not clear temp files that are locked. (#6214) - Handle edge cases in sync workspace * fix: labels empty state for drop down (#6216) * refactor: remove cn helper function from the editor package (#6217) * * feat: added language support to issue create modal in sidebar * fix: project activity type * * fix: added missing translations * fix: modified translation for plurals * fix: fixed spanish translation * dev: language type error in space user profile types * fix: type fixes * chore: added alpha tag --------- Co-authored-by: sriram veeraghanta Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: Prateek Shourya Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Co-authored-by: Satish Gandham Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Co-authored-by: gurusinath * feat: introduced stacked bar chart and tree map chart. (#6305) * feat: add issue attachment external endpoint (#6307) * [PE-97] chore: re-order pages options (#6303) * chore: re-order pages dropdown options * chore: re-order pages dropdown options * fix: remove localdb tracing * [WEB-2937] feat: home recent activies list endpoint (#6295) * Crud for wuick links * Validate quick link existence * Add custom method for destroy and retrieve * Add List method * Remove print statements * List all the workspace quick links * feat: endpoint to get recently active items * Resolve conflicts * Resolve conflicts * Add filter to only list required entities * Return required fields * Add filter * Add filter * fix: remove emoji edit for uneditable pages (#6304) * Removed duplicate imports * feat: patch api * Enable sort order to be updatable * Return key name only insert missing keys use serializer to return data * Remove random generation of sort_order * Remove name field Remove random generation of sort_order --------- Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Co-authored-by: Vamsi Krishna <46787868+mathalav55@users.noreply.github.com> Co-authored-by: gurusinath Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: sriram veeraghanta Co-authored-by: Prateek Shourya Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Co-authored-by: Satish Gandham Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com> --- apiserver/plane/app/serializers/__init__.py | 3 +- apiserver/plane/app/serializers/workspace.py | 72 +++++++++++++------ apiserver/plane/app/urls/workspace.py | 33 +++++---- apiserver/plane/app/views/__init__.py | 1 + .../plane/app/views/workspace/preference.py | 72 +++++++++++++++++++ apiserver/plane/db/models/__init__.py | 1 + 6 files changed, 149 insertions(+), 33 deletions(-) create mode 100644 apiserver/plane/app/views/workspace/preference.py diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 134eee891..796e68b40 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -20,7 +20,8 @@ from .workspace import ( WorkspaceMemberMeSerializer, WorkspaceUserPropertiesSerializer, WorkspaceUserLinkSerializer, - WorkspaceRecentVisitSerializer + WorkspaceRecentVisitSerializer, + WorkspaceHomePreferenceSerializer, ) from .project import ( ProjectSerializer, diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index ad45cf1a8..53368a6fc 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -16,10 +16,11 @@ from plane.db.models import ( WorkspaceUserProperties, WorkspaceUserLink, UserRecentVisit, - Issue, - Page, + Issue, + Page, Project, - ProjectMember + ProjectMember, + WorkspaceHomePreference, ) from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS @@ -27,6 +28,7 @@ from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS from django.core.validators import URLValidator from django.core.exceptions import ValidationError + class WorkSpaceSerializer(DynamicBaseSerializer): owner = UserLiteSerializer(read_only=True) total_members = serializers.IntegerField(read_only=True) @@ -119,6 +121,7 @@ class WorkspaceUserPropertiesSerializer(BaseSerializer): fields = "__all__" read_only_fields = ["workspace", "user"] + class WorkspaceUserLinkSerializer(BaseSerializer): class Meta: model = WorkspaceUserLink @@ -129,7 +132,7 @@ class WorkspaceUserLinkSerializer(BaseSerializer): url = data.get("url", "") if url and not url.startswith(("http://", "https://")): data["url"] = "http://" + url - + return super().to_internal_value(data) def validate_url(self, value): @@ -141,18 +144,29 @@ class WorkspaceUserLinkSerializer(BaseSerializer): return value + class IssueRecentVisitSerializer(serializers.ModelSerializer): project_identifier = serializers.SerializerMethodField() class Meta: model = Issue - fields = ["name", "state", "priority", "assignees", "type", "sequence_id", "project_id", "project_identifier"] + fields = [ + "name", + "state", + "priority", + "assignees", + "type", + "sequence_id", + "project_id", + "project_identifier", + ] def get_project_identifier(self, obj): project = obj.project return project.identifier if project else None + class ProjectMemberSerializer(BaseSerializer): member = UserLiteSerializer(read_only=True) @@ -160,55 +174,66 @@ class ProjectMemberSerializer(BaseSerializer): model = ProjectMember fields = ["member"] + class ProjectRecentVisitSerializer(serializers.ModelSerializer): - project_members = serializers.SerializerMethodField() - + project_members = serializers.SerializerMethodField() + class Meta: model = Project fields = ["id", "name", "logo_props", "project_members", "identifier"] def get_project_members(self, obj): - members = ProjectMember.objects.filter(project_id=obj.id).select_related('member') + members = ProjectMember.objects.filter(project_id=obj.id).select_related( + "member" + ) serializer = ProjectMemberSerializer(members, many=True) return serializer.data - + + class PageRecentVisitSerializer(serializers.ModelSerializer): project_id = serializers.SerializerMethodField() project_identifier = serializers.SerializerMethodField() class Meta: model = Page - fields = ["id", "name", "logo_props", "project_id", "owned_by", "project_identifier"] + fields = [ + "id", + "name", + "logo_props", + "project_id", + "owned_by", + "project_identifier", + ] def get_project_id(self, obj): - return obj.project_id if hasattr(obj, 'project_id') else obj.projects.values_list('id', flat=True).first() - + return ( + obj.project_id + if hasattr(obj, "project_id") + else obj.projects.values_list("id", flat=True).first() + ) + def get_project_identifier(self, obj): project = obj.projects.first() return project.identifier if project else None + def get_entity_model_and_serializer(entity_type): entity_map = { "issue": (Issue, IssueRecentVisitSerializer), "page": (Page, PageRecentVisitSerializer), - "project": (Project, ProjectRecentVisitSerializer) + "project": (Project, ProjectRecentVisitSerializer), } return entity_map.get(entity_type, (None, None)) + class WorkspaceRecentVisitSerializer(BaseSerializer): entity_data = serializers.SerializerMethodField() class Meta: model = UserRecentVisit - fields = [ - "id", - "entity_name", - "entity_identifier", - "entity_data", - "visited_at" - ] + fields = ["id", "entity_name", "entity_identifier", "entity_data", "visited_at"] read_only_fields = ["workspace", "owner", "created_by", "updated_by"] def get_entity_data(self, obj): @@ -225,3 +250,10 @@ class WorkspaceRecentVisitSerializer(BaseSerializer): except entity_model.DoesNotExist: return None return None + + +class WorkspaceHomePreferenceSerializer(BaseSerializer): + class Meta: + model = WorkspaceHomePreference + fields = ["key", "is_enabled", "sort_order"] + read_only_fields = ["worspace", "created_by", "update_by"] diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index f8cdbbd5b..2bb3d5b1d 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -28,7 +28,8 @@ from plane.app.views import ( WorkspaceFavoriteGroupEndpoint, WorkspaceDraftIssueViewSet, QuickLinkViewSet, - UserRecentVisitViewSet + UserRecentVisitViewSet, + WorkspacePreferenceViewSet, ) @@ -215,25 +216,33 @@ urlpatterns = [ WorkspaceDraftIssueViewSet.as_view({"post": "create_draft_to_issue"}), name="workspace-drafts-issues", ), - # quick link path( "workspaces//quick-links/", QuickLinkViewSet.as_view({"get": "list", "post": "create"}), - name="workspace-quick-links" + name="workspace-quick-links", ), path( - "workspaces//quick-links//", - QuickLinkViewSet.as_view({ - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy" - }), - name="workspace-quick-links" + "workspaces//quick-links//", + QuickLinkViewSet.as_view( + {"get": "retrieve", "patch": "partial_update", "delete": "destroy"} + ), + name="workspace-quick-links", + ), + # Widgets + path( + "workspaces//home-preferences/", + WorkspacePreferenceViewSet.as_view(), + name="workspace-home-preference", + ), + path( + "workspaces//home-preferences//", + WorkspacePreferenceViewSet.as_view(), + name="workspace-home-preference", ), path( "workspaces//recent-visits/", UserRecentVisitViewSet.as_view({"get": "list"}), - name="workspace-recent-visits" - ) + name="workspace-recent-visits", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 19dbd4d8f..53c9c0a72 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -41,6 +41,7 @@ from .workspace.base import ( from .workspace.draft import WorkspaceDraftIssueViewSet +from .workspace.preference import WorkspacePreferenceViewSet from .workspace.favorite import ( WorkspaceFavoriteEndpoint, WorkspaceFavoriteGroupEndpoint, diff --git a/apiserver/plane/app/views/workspace/preference.py b/apiserver/plane/app/views/workspace/preference.py new file mode 100644 index 000000000..21c06321c --- /dev/null +++ b/apiserver/plane/app/views/workspace/preference.py @@ -0,0 +1,72 @@ +# Module imports +from ..base import BaseAPIView +from plane.db.models.workspace import WorkspaceHomePreference +from plane.app.permissions import allow_permission, ROLE +from plane.db.models import Workspace +from plane.app.serializers.workspace import WorkspaceHomePreferenceSerializer + +# Third party imports +from rest_framework.response import Response +from rest_framework import status + + +class WorkspacePreferenceViewSet(BaseAPIView): + model = WorkspaceHomePreference + + def get_serializer_class(self): + return WorkspaceHomePreferenceSerializer + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") + def get(self, request, slug): + workspace = Workspace.objects.get(slug=slug) + + get_preference = WorkspaceHomePreference.objects.filter( + user=request.user, workspace_id=workspace.id + ) + + create_preference_keys = [] + + keys = [key for key, _ in WorkspaceHomePreference.HomeWidgetKeys.choices] + + for preference in keys: + if preference not in get_preference.values_list("key", flat=True): + create_preference_keys.append(preference) + + preference = WorkspaceHomePreference.objects.bulk_create( + [ + WorkspaceHomePreference( + key=key, user=request.user, workspace=workspace + ) + for key in create_preference_keys + ], + batch_size=10, + ignore_conflicts=True, + ) + preference = WorkspaceHomePreference.objects.filter( + user=request.user, workspace_id=workspace.id + ) + + return Response( + preference.values("key", "is_enabled", "config", "sort_order"), + status=status.HTTP_200_OK, + ) + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") + def patch(self, request, slug, key): + preference = WorkspaceHomePreference.objects.filter( + key=key, workspace__slug=slug + ).first() + + if preference: + serializer = WorkspaceHomePreferenceSerializer( + preference, 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) + + return Response( + {"detail": "Preference not found"}, status=status.HTTP_400_BAD_REQUEST + ) diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 216e445e6..09b372fdd 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -69,6 +69,7 @@ from .workspace import ( WorkspaceTheme, WorkspaceUserProperties, WorkspaceUserLink, + WorkspaceHomePreference ) from .favorite import UserFavorite