From fb74875cdee5df1e19d98d78b79eb92737d80665 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 3 May 2024 23:12:44 +0530 Subject: [PATCH 001/199] [WEB-1181] chore: added a loader for page description (#4358) * chore: add loader for page description * chore: added skeleton loader * fix: title loader margin * chore: increased laoder width --- web/components/pages/editor/editor-body.tsx | 10 ++++--- web/components/pages/loaders/index.ts | 1 + .../pages/loaders/page-content-loader.tsx | 28 +++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 web/components/pages/loaders/page-content-loader.tsx diff --git a/web/components/pages/editor/editor-body.tsx b/web/components/pages/editor/editor-body.tsx index f1e38348b23..0ea7e6052c0 100644 --- a/web/components/pages/editor/editor-body.tsx +++ b/web/components/pages/editor/editor-body.tsx @@ -13,7 +13,7 @@ import { // types import { IUserLite, TPage } from "@plane/types"; // components -import { PageContentBrowser, PageEditorTitle } from "@/components/pages"; +import { PageContentBrowser, PageContentLoader, PageEditorTitle } from "@/components/pages"; // helpers import { cn } from "@/helpers/common.helper"; // hooks @@ -68,7 +68,7 @@ export const PageEditorBody: React.FC = observer((props) => { // derived values const workspaceId = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "" : ""; const pageTitle = pageStore?.name ?? ""; - const pageDescription = pageStore?.description_html ?? "

"; + const pageDescription = pageStore?.description_html; const { description_html, isContentEditable, updateTitle, isSubmitting, setIsSubmitting } = pageStore; const projectMemberIds = projectId ? getProjectMemberIds(projectId.toString()) : []; const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite); @@ -88,6 +88,8 @@ export const PageEditorBody: React.FC = observer((props) => { updateMarkings(description_html ?? "

"); }, [description_html, updateMarkings]); + if (pageDescription === undefined) return ; + return (
= observer((props) => { upload: fileService.getUploadFileFunction(workspaceSlug as string, setIsSubmitting), }} handleEditorReady={handleEditorReady} - initialValue={pageDescription} + initialValue={pageDescription ?? "

"} value={swrPageDetails?.description_html ?? "

"} ref={editorRef} containerClassName="p-0 pb-64" @@ -153,7 +155,7 @@ export const PageEditorBody: React.FC = observer((props) => { ) : (

"} handleEditorReady={handleReadOnlyEditorReady} containerClassName="p-0 pb-64 border-none" editorClassName="lg:px-10 pl-8" diff --git a/web/components/pages/loaders/index.ts b/web/components/pages/loaders/index.ts index f278fa38234..8760eede6c8 100644 --- a/web/components/pages/loaders/index.ts +++ b/web/components/pages/loaders/index.ts @@ -1 +1,2 @@ +export * from "./page-content-loader"; export * from "./page-loader"; diff --git a/web/components/pages/loaders/page-content-loader.tsx b/web/components/pages/loaders/page-content-loader.tsx new file mode 100644 index 00000000000..ec38c1bbe72 --- /dev/null +++ b/web/components/pages/loaders/page-content-loader.tsx @@ -0,0 +1,28 @@ +// ui +import { Loader } from "@plane/ui"; + +export const PageContentLoader = () => ( +
+
+ +
+ + + +
+
+ {Array.from(Array(4)).map((i) => ( +
+ +
+ + + +
+
+ ))} +
+
+
+
+); From f1fda4ae4a5b0e278dafdbcf387e46a546125fb7 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 6 May 2024 14:13:49 +0530 Subject: [PATCH 002/199] [WEB - 1122] fix: webhook for issues, issue comments, projects, cycles and modules. (#4330) * dev: update webhook logic for issues * dev: update issue webhooks for cycle and module * dev: webhook for comment * dev: issue attachment webhooks * dev: add logging * dev: add inbox issue webhooks * dev: update the webhook send task * dev: project webhooks for api * dev: webhooks update for projects, cycles and modules * dev: fix webhook on cycle and module create from external apis --- apiserver/plane/api/serializers/inbox.py | 4 + apiserver/plane/api/views/base.py | 35 -- apiserver/plane/api/views/cycle.py | 34 +- apiserver/plane/api/views/inbox.py | 18 +- apiserver/plane/api/views/issue.py | 25 +- apiserver/plane/api/views/module.py | 35 +- apiserver/plane/api/views/project.py | 36 ++- apiserver/plane/app/serializers/issue.py | 2 +- apiserver/plane/app/views/__init__.py | 2 +- apiserver/plane/app/views/base.py | 30 -- apiserver/plane/app/views/cycle/base.py | 34 +- apiserver/plane/app/views/cycle/issue.py | 7 +- apiserver/plane/app/views/inbox/base.py | 45 ++- apiserver/plane/app/views/issue/base.py | 4 +- apiserver/plane/app/views/issue/comment.py | 4 +- apiserver/plane/app/views/module/base.py | 40 ++- apiserver/plane/app/views/module/issue.py | 4 +- apiserver/plane/app/views/project/base.py | 32 +- .../plane/bgtasks/issue_activites_task.py | 43 ++- apiserver/plane/bgtasks/webhook_task.py | 302 ++++++++++++++---- 20 files changed, 545 insertions(+), 191 deletions(-) diff --git a/apiserver/plane/api/serializers/inbox.py b/apiserver/plane/api/serializers/inbox.py index 78bb74d13e4..a0c79235df2 100644 --- a/apiserver/plane/api/serializers/inbox.py +++ b/apiserver/plane/api/serializers/inbox.py @@ -1,9 +1,13 @@ # Module improts from .base import BaseSerializer +from .issue import IssueExpandSerializer from plane.db.models import InboxIssue class InboxIssueSerializer(BaseSerializer): + + issue_detail = IssueExpandSerializer(read_only=True, source="issue") + class Meta: model = InboxIssue fields = "__all__" diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/api/views/base.py index 13047eb78e8..1f6bd70afc2 100644 --- a/apiserver/plane/api/views/base.py +++ b/apiserver/plane/api/views/base.py @@ -19,7 +19,6 @@ # Module imports from plane.api.middleware.api_authentication import APIKeyAuthentication from plane.api.rate_limit import ApiKeyRateThrottle -from plane.bgtasks.webhook_task import send_webhook from plane.utils.exception_logger import log_exception from plane.utils.paginator import BasePaginator @@ -38,40 +37,6 @@ def initial(self, request, *args, **kwargs): timezone.deactivate() -class WebhookMixin: - webhook_event = None - bulk = False - - def finalize_response(self, request, response, *args, **kwargs): - response = super().finalize_response( - request, response, *args, **kwargs - ) - - # Check for the case should webhook be sent - if ( - self.webhook_event - and self.request.method in ["POST", "PATCH", "DELETE"] - and response.status_code in [200, 201, 204] - ): - url = request.build_absolute_uri() - parsed_url = urlparse(url) - # Extract the scheme and netloc - scheme = parsed_url.scheme - netloc = parsed_url.netloc - # Push the object to delay - send_webhook.delay( - event=self.webhook_event, - payload=response.data, - kw=self.kwargs, - action=self.request.method, - slug=self.workspace_slug, - bulk=self.bulk, - current_site=f"{scheme}://{netloc}", - ) - - return response - - class BaseAPIView(TimezoneMixin, APIView, BasePaginator): authentication_classes = [ APIKeyAuthentication, diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index d9c75ff41c6..6e1e5e057f9 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -5,6 +5,7 @@ from django.core import serializers from django.db.models import Count, F, Func, OuterRef, Q, Sum from django.utils import timezone +from django.core.serializers.json import DjangoJSONEncoder # Third party imports from rest_framework import status @@ -26,10 +27,11 @@ ) from plane.utils.analytics_plot import burndown_plot -from .base import BaseAPIView, WebhookMixin +from .base import BaseAPIView +from plane.bgtasks.webhook_task import model_activity -class CycleAPIEndpoint(WebhookMixin, BaseAPIView): +class CycleAPIEndpoint(BaseAPIView): """ This viewset automatically provides `list`, `create`, `retrieve`, `update` and `destroy` actions related to cycle. @@ -277,6 +279,16 @@ def post(self, request, slug, project_id): project_id=project_id, owned_by=request.user, ) + # Send the model activity + model_activity.delay( + model_name="cycle", + model_id=str(serializer.data["id"]), + requested_data=request.data, + current_instance=None, + actor_id=request.user.id, + slug=slug, + origin=request.META.get("HTTP_ORIGIN"), + ) return Response( serializer.data, status=status.HTTP_201_CREATED ) @@ -295,6 +307,11 @@ def patch(self, request, slug, project_id, pk): cycle = Cycle.objects.get( workspace__slug=slug, project_id=project_id, pk=pk ) + + current_instance = json.dumps( + CycleSerializer(cycle).data, cls=DjangoJSONEncoder + ) + if cycle.archived_at: return Response( {"error": "Archived cycle cannot be edited"}, @@ -344,6 +361,17 @@ def patch(self, request, slug, project_id, pk): status=status.HTTP_409_CONFLICT, ) serializer.save() + + # Send the model activity + model_activity.delay( + model_name="cycle", + model_id=str(serializer.data["id"]), + requested_data=request.data, + current_instance=current_instance, + actor_id=request.user.id, + slug=slug, + origin=request.META.get("HTTP_ORIGIN"), + ) return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -515,7 +543,7 @@ def delete(self, request, slug, project_id, cycle_id): return Response(status=status.HTTP_204_NO_CONTENT) -class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView): +class CycleIssueAPIEndpoint(BaseAPIView): """ This viewset automatically provides `list`, `create`, and `destroy` actions related to cycle issues. diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index 5e6e4a21589..8987e4f633c 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -154,6 +154,13 @@ def post(self, request, slug, project_id): state=state, ) + # create an inbox issue + inbox_issue = InboxIssue.objects.create( + inbox_id=inbox.id, + project_id=project_id, + issue=issue, + source=request.data.get("source", "in-app"), + ) # Create an Issue Activity issue_activity.delay( type="issue.activity.created", @@ -163,14 +170,7 @@ def post(self, request, slug, project_id): project_id=str(project_id), current_instance=None, epoch=int(timezone.now().timestamp()), - ) - - # create an inbox issue - inbox_issue = InboxIssue.objects.create( - inbox_id=inbox.id, - project_id=project_id, - issue=issue, - source=request.data.get("source", "in-app"), + inbox=str(inbox_issue.id), ) serializer = InboxIssueSerializer(inbox_issue) @@ -260,6 +260,7 @@ def patch(self, request, slug, project_id, issue_id): cls=DjangoJSONEncoder, ), epoch=int(timezone.now().timestamp()), + inbox=(inbox_issue.id), ) issue_serializer.save() else: @@ -327,6 +328,7 @@ def patch(self, request, slug, project_id, issue_id): epoch=int(timezone.now().timestamp()), notification=False, origin=request.META.get("HTTP_ORIGIN"), + inbox=str(inbox_issue.id), ) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 8d72ac5db72..a62278b1949 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -48,11 +48,10 @@ ProjectMember, ) -from .base import BaseAPIView, WebhookMixin +from .base import BaseAPIView - -class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView): +class WorkspaceIssueAPIEndpoint(BaseAPIView): """ This viewset provides `retrieveByIssueId` on workspace level @@ -60,12 +59,9 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView): model = Issue webhook_event = "issue" - permission_classes = [ - ProjectEntityPermission - ] + permission_classes = [ProjectEntityPermission] serializer_class = IssueSerializer - @property def project__identifier(self): return self.kwargs.get("project__identifier", None) @@ -91,7 +87,9 @@ def get_queryset(self): .order_by(self.kwargs.get("order_by", "-created_at")) ).distinct() - def get(self, request, slug, project__identifier=None, issue__identifier=None): + def get( + self, request, slug, project__identifier=None, issue__identifier=None + ): if issue__identifier and project__identifier: issue = Issue.issue_objects.annotate( sub_issues_count=Issue.issue_objects.filter( @@ -100,7 +98,11 @@ def get(self, request, slug, project__identifier=None, issue__identifier=None): .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") - ).get(workspace__slug=slug, project__identifier=project__identifier, sequence_id=issue__identifier) + ).get( + workspace__slug=slug, + project__identifier=project__identifier, + sequence_id=issue__identifier, + ) return Response( IssueSerializer( issue, @@ -110,7 +112,8 @@ def get(self, request, slug, project__identifier=None, issue__identifier=None): status=status.HTTP_200_OK, ) -class IssueAPIEndpoint(WebhookMixin, BaseAPIView): + +class IssueAPIEndpoint(BaseAPIView): """ This viewset automatically provides `list`, `create`, `retrieve`, `update` and `destroy` actions related to issue. @@ -652,7 +655,7 @@ def delete(self, request, slug, project_id, issue_id, pk): return Response(status=status.HTTP_204_NO_CONTENT) -class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView): +class IssueCommentAPIEndpoint(BaseAPIView): """ This viewset automatically provides `list`, `create`, `retrieve`, `update` and `destroy` actions related to comments of the particular issue. diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index 38744eaa545..eeb29dad244 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -5,6 +5,7 @@ from django.core import serializers from django.db.models import Count, F, Func, OuterRef, Prefetch, Q from django.utils import timezone +from django.core.serializers.json import DjangoJSONEncoder # Third party imports from rest_framework import status @@ -28,10 +29,11 @@ Project, ) -from .base import BaseAPIView, WebhookMixin +from .base import BaseAPIView +from plane.bgtasks.webhook_task import model_activity -class ModuleAPIEndpoint(WebhookMixin, BaseAPIView): +class ModuleAPIEndpoint(BaseAPIView): """ This viewset automatically provides `list`, `create`, `retrieve`, `update` and `destroy` actions related to module. @@ -163,6 +165,16 @@ def post(self, request, slug, project_id): status=status.HTTP_409_CONFLICT, ) serializer.save() + # Send the model activity + model_activity.delay( + model_name="module", + model_id=str(serializer.data["id"]), + requested_data=request.data, + current_instance=None, + actor_id=request.user.id, + slug=slug, + origin=request.META.get("HTTP_ORIGIN"), + ) module = Module.objects.get(pk=serializer.data["id"]) serializer = ModuleSerializer(module) return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -172,6 +184,11 @@ def patch(self, request, slug, project_id, pk): module = Module.objects.get( pk=pk, project_id=project_id, workspace__slug=slug ) + + current_instance = json.dumps( + ModuleSerializer(module).data, cls=DjangoJSONEncoder + ) + if module.archived_at: return Response( {"error": "Archived module cannot be edited"}, @@ -204,6 +221,18 @@ def patch(self, request, slug, project_id, pk): status=status.HTTP_409_CONFLICT, ) serializer.save() + + # Send the model activity + model_activity.delay( + model_name="module", + model_id=str(serializer.data["id"]), + requested_data=request.data, + current_instance=current_instance, + actor_id=request.user.id, + slug=slug, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -260,7 +289,7 @@ def delete(self, request, slug, project_id, pk): return Response(status=status.HTTP_204_NO_CONTENT) -class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView): +class ModuleIssueAPIEndpoint(BaseAPIView): """ This viewset automatically provides `list`, `create`, `retrieve`, `update` and `destroy` actions related to module issues. diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index fcb0cc4fb9e..019ab704eca 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -1,7 +1,11 @@ +# Python imports +import json + # Django imports from django.db import IntegrityError from django.db.models import Exists, F, Func, OuterRef, Prefetch, Q, Subquery from django.utils import timezone +from django.core.serializers.json import DjangoJSONEncoder # Third party imports from rest_framework import status @@ -23,11 +27,11 @@ State, Workspace, ) - -from .base import BaseAPIView, WebhookMixin +from plane.bgtasks.webhook_task import model_activity +from .base import BaseAPIView -class ProjectAPIEndpoint(WebhookMixin, BaseAPIView): +class ProjectAPIEndpoint(BaseAPIView): """Project Endpoints to create, update, list, retrieve and delete endpoint""" serializer_class = ProjectSerializer @@ -236,6 +240,17 @@ def post(self, request, slug): .filter(pk=serializer.data["id"]) .first() ) + # Model activity + model_activity.delay( + model_name="project", + model_id=str(project.id), + requested_data=request.data, + current_instance=None, + actor_id=request.user.id, + slug=slug, + origin=request.META.get("HTTP_ORIGIN"), + ) + serializer = ProjectSerializer(project) return Response( serializer.data, status=status.HTTP_201_CREATED @@ -265,7 +280,9 @@ def patch(self, request, slug, pk): try: workspace = Workspace.objects.get(slug=slug) project = Project.objects.get(pk=pk) - + current_instance = json.dumps( + ProjectSerializer(project).data, cls=DjangoJSONEncoder + ) if project.archived_at: return Response( {"error": "Archived project cannot be updated"}, @@ -303,6 +320,17 @@ def patch(self, request, slug, pk): .filter(pk=serializer.data["id"]) .first() ) + + model_activity.delay( + model_name="project", + model_id=str(project.id), + requested_data=request.data, + current_instance=current_instance, + actor_id=request.user.id, + slug=slug, + origin=request.META.get("HTTP_ORIGIN"), + ) + serializer = ProjectSerializer(project) return Response(serializer.data, status=status.HTTP_200_OK) return Response( diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 8c641b72097..b884d60a3b4 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -442,7 +442,7 @@ def validate_url(self, value): raise serializers.ValidationError("Invalid URL format.") # Check URL scheme - if not value.startswith(('http://', 'https://')): + if not value.startswith(("http://", "https://")): raise serializers.ValidationError("Invalid URL scheme.") return value diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 3d7603e240f..bb61aad3af4 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -30,7 +30,7 @@ from .oauth import OauthEndpoint -from .base import BaseAPIView, BaseViewSet, WebhookMixin +from .base import BaseAPIView, BaseViewSet from .workspace.base import ( WorkSpaceViewSet, diff --git a/apiserver/plane/app/views/base.py b/apiserver/plane/app/views/base.py index 1908cfdc951..c145409454a 100644 --- a/apiserver/plane/app/views/base.py +++ b/apiserver/plane/app/views/base.py @@ -19,7 +19,6 @@ from rest_framework.viewsets import ModelViewSet # Module imports -from plane.bgtasks.webhook_task import send_webhook from plane.utils.exception_logger import log_exception from plane.utils.paginator import BasePaginator @@ -38,35 +37,6 @@ def initial(self, request, *args, **kwargs): timezone.deactivate() -class WebhookMixin: - webhook_event = None - bulk = False - - def finalize_response(self, request, response, *args, **kwargs): - response = super().finalize_response( - request, response, *args, **kwargs - ) - - # Check for the case should webhook be sent - if ( - self.webhook_event - and self.request.method in ["POST", "PATCH", "DELETE"] - and response.status_code in [200, 201, 204] - ): - # Push the object to delay - send_webhook.delay( - event=self.webhook_event, - payload=response.data, - kw=self.kwargs, - action=self.request.method, - slug=self.workspace_slug, - bulk=self.bulk, - current_site=request.META.get("HTTP_ORIGIN"), - ) - - return response - - class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): model = None diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index dd9826c56b0..621c1dcb772 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -20,6 +20,7 @@ ) from django.db.models.functions import Coalesce from django.utils import timezone +from django.core.serializers.json import DjangoJSONEncoder # Third party imports from rest_framework import status @@ -47,10 +48,11 @@ from plane.utils.analytics_plot import burndown_plot # Module imports -from .. import BaseAPIView, BaseViewSet, WebhookMixin +from .. import BaseAPIView, BaseViewSet +from plane.bgtasks.webhook_task import model_activity -class CycleViewSet(WebhookMixin, BaseViewSet): +class CycleViewSet(BaseViewSet): serializer_class = CycleSerializer model = Cycle webhook_event = "cycle" @@ -412,6 +414,17 @@ def create(self, request, slug, project_id): ) .first() ) + + # Send the model activity + model_activity.delay( + model_name="cycle", + model_id=str(cycle["id"]), + requested_data=request.data, + current_instance=None, + actor_id=request.user.id, + slug=slug, + origin=request.META.get("HTTP_ORIGIN"), + ) return Response(cycle, status=status.HTTP_201_CREATED) return Response( serializer.errors, status=status.HTTP_400_BAD_REQUEST @@ -434,6 +447,11 @@ def partial_update(self, request, slug, project_id, pk): {"error": "Archived cycle cannot be updated"}, status=status.HTTP_400_BAD_REQUEST, ) + + current_instance = json.dumps( + CycleSerializer(cycle).data, cls=DjangoJSONEncoder + ) + request_data = request.data if ( @@ -487,6 +505,18 @@ def partial_update(self, request, slug, project_id, pk): "assignee_ids", "status", ).first() + + # Send the model activity + model_activity.delay( + model_name="cycle", + model_id=str(cycle["id"]), + requested_data=request.data, + current_instance=current_instance, + actor_id=request.user.id, + slug=slug, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(cycle, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apiserver/plane/app/views/cycle/issue.py b/apiserver/plane/app/views/cycle/issue.py index 9a029eb2570..fdc998f6dcd 100644 --- a/apiserver/plane/app/views/cycle/issue.py +++ b/apiserver/plane/app/views/cycle/issue.py @@ -23,7 +23,7 @@ from rest_framework import status # Module imports -from .. import BaseViewSet, WebhookMixin +from .. import BaseViewSet from plane.app.serializers import ( IssueSerializer, CycleIssueSerializer, @@ -40,7 +40,7 @@ from plane.utils.issue_filters import issue_filters from plane.utils.user_timezone_converter import user_timezone_converter -class CycleIssueViewSet(WebhookMixin, BaseViewSet): +class CycleIssueViewSet(BaseViewSet): serializer_class = CycleIssueSerializer model = CycleIssue @@ -254,6 +254,7 @@ def create(self, request, slug, project_id, cycle_id): update_cycle_issue_activity = [] # Iterate over each cycle_issue in cycle_issues for cycle_issue in cycle_issues: + old_cycle_id = cycle_issue.cycle_id # Update the cycle_issue's cycle_id cycle_issue.cycle_id = cycle_id # Add the modified cycle_issue to the records_to_update list @@ -261,7 +262,7 @@ def create(self, request, slug, project_id, cycle_id): # Record the update activity update_cycle_issue_activity.append( { - "old_cycle_id": str(cycle_issue.cycle_id), + "old_cycle_id": str(old_cycle_id), "new_cycle_id": str(cycle_id), "issue_id": str(cycle_issue.issue_id), } diff --git a/apiserver/plane/app/views/inbox/base.py b/apiserver/plane/app/views/inbox/base.py index 8e433a127cd..d688a885336 100644 --- a/apiserver/plane/app/views/inbox/base.py +++ b/apiserver/plane/app/views/inbox/base.py @@ -251,6 +251,16 @@ def create(self, request, slug, project_id): ) if serializer.is_valid(): serializer.save() + inbox_id = Inbox.objects.filter( + workspace__slug=slug, project_id=project_id + ).first() + # create an inbox issue + inbox_issue = InboxIssue.objects.create( + inbox_id=inbox_id.id, + project_id=project_id, + issue_id=serializer.data["id"], + source=request.data.get("source", "in-app"), + ) # Create an Issue Activity issue_activity.delay( type="issue.activity.created", @@ -262,16 +272,7 @@ def create(self, request, slug, project_id): epoch=int(timezone.now().timestamp()), notification=True, origin=request.META.get("HTTP_ORIGIN"), - ) - inbox_id = Inbox.objects.filter( - workspace__slug=slug, project_id=project_id - ).first() - # create an inbox issue - inbox_issue = InboxIssue.objects.create( - inbox_id=inbox_id.id, - project_id=project_id, - issue_id=serializer.data["id"], - source=request.data.get("source", "in-app"), + inbox=str(inbox_issue.id), ) inbox_issue = ( InboxIssue.objects.select_related("issue") @@ -339,7 +340,24 @@ def partial_update(self, request, slug, project_id, issue_id): # Get issue data issue_data = request.data.pop("issue", False) if bool(issue_data): - issue = Issue.objects.get( + issue = Issue.objects.annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ).get( pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id, @@ -379,6 +397,7 @@ def partial_update(self, request, slug, project_id, issue_id): epoch=int(timezone.now().timestamp()), notification=True, origin=request.META.get("HTTP_ORIGIN"), + inbox=str(inbox_issue.id), ) issue_serializer.save() else: @@ -444,6 +463,7 @@ def partial_update(self, request, slug, project_id, issue_id): epoch=int(timezone.now().timestamp()), notification=False, origin=request.META.get("HTTP_ORIGIN"), + inbox=(inbox_issue.id), ) inbox_issue = ( @@ -480,7 +500,8 @@ def partial_update(self, request, slug, project_id, issue_id): output_field=ArrayField(UUIDField()), ), ), - ).first() + ) + .first() ) serializer = InboxIssueDetailSerializer(inbox_issue).data return Response(serializer, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index 7a0e5d9b1eb..b1fd1a9bcc2 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -53,7 +53,7 @@ from plane.utils.user_timezone_converter import user_timezone_converter # Module imports -from .. import BaseAPIView, BaseViewSet, WebhookMixin +from .. import BaseAPIView, BaseViewSet class IssueListEndpoint(BaseAPIView): @@ -249,7 +249,7 @@ def get(self, request, slug, project_id): return Response(issues, status=status.HTTP_200_OK) -class IssueViewSet(WebhookMixin, BaseViewSet): +class IssueViewSet(BaseViewSet): def get_serializer_class(self): return ( IssueCreateSerializer diff --git a/apiserver/plane/app/views/issue/comment.py b/apiserver/plane/app/views/issue/comment.py index 0d61f132576..1698efef83f 100644 --- a/apiserver/plane/app/views/issue/comment.py +++ b/apiserver/plane/app/views/issue/comment.py @@ -11,7 +11,7 @@ from rest_framework import status # Module imports -from .. import BaseViewSet, WebhookMixin +from .. import BaseViewSet from plane.app.serializers import ( IssueCommentSerializer, CommentReactionSerializer, @@ -25,7 +25,7 @@ from plane.bgtasks.issue_activites_task import issue_activity -class IssueCommentViewSet(WebhookMixin, BaseViewSet): +class IssueCommentViewSet(BaseViewSet): serializer_class = IssueCommentSerializer model = IssueComment webhook_event = "issue_comment" diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 59f26a03646..5a987dad870 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -1,6 +1,7 @@ # Python imports import json +# Django Imports from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField from django.db.models import ( @@ -17,14 +18,14 @@ Value, ) from django.db.models.functions import Coalesce - -# Django Imports +from django.core.serializers.json import DjangoJSONEncoder from django.utils import timezone -from rest_framework import status # Third party imports +from rest_framework import status from rest_framework.response import Response +# Module imports from plane.app.permissions import ( ProjectEntityPermission, ProjectLitePermission, @@ -49,13 +50,11 @@ ) from plane.utils.analytics_plot import burndown_plot from plane.utils.user_timezone_converter import user_timezone_converter +from plane.bgtasks.webhook_task import model_activity +from .. import BaseAPIView, BaseViewSet -# Module imports -from .. import BaseAPIView, BaseViewSet, WebhookMixin - - -class ModuleViewSet(WebhookMixin, BaseViewSet): +class ModuleViewSet(BaseViewSet): model = Module permission_classes = [ ProjectEntityPermission, @@ -238,6 +237,16 @@ def create(self, request, slug, project_id): "updated_at", ) ).first() + # Send the model activity + model_activity.delay( + model_name="module", + model_id=str(module["id"]), + requested_data=request.data, + current_instance=None, + actor_id=request.user.id, + slug=slug, + origin=request.META.get("HTTP_ORIGIN"), + ) datetime_fields = ["created_at", "updated_at"] module = user_timezone_converter( module, datetime_fields, request.user.user_timezone @@ -422,6 +431,9 @@ def retrieve(self, request, slug, project_id, pk): def partial_update(self, request, slug, project_id, pk): module = self.get_queryset().filter(pk=pk) + current_instance = json.dumps( + ModuleSerializer(module).data, cls=DjangoJSONEncoder + ) if module.first().archived_at: return Response( @@ -464,6 +476,18 @@ def partial_update(self, request, slug, project_id, pk): "created_at", "updated_at", ).first() + + # Send the model activity + model_activity.delay( + model_name="module", + model_id=str(module["id"]), + requested_data=request.data, + current_instance=current_instance, + actor_id=request.user.id, + slug=slug, + origin=request.META.get("HTTP_ORIGIN"), + ) + datetime_fields = ["created_at", "updated_at"] module = user_timezone_converter( module, datetime_fields, request.user.user_timezone diff --git a/apiserver/plane/app/views/module/issue.py b/apiserver/plane/app/views/module/issue.py index e0fcb2d3c95..3e79e7ec789 100644 --- a/apiserver/plane/app/views/module/issue.py +++ b/apiserver/plane/app/views/module/issue.py @@ -16,7 +16,7 @@ from rest_framework import status # Module imports -from .. import BaseViewSet, WebhookMixin +from .. import BaseViewSet from plane.app.serializers import ( ModuleIssueSerializer, IssueSerializer, @@ -33,7 +33,7 @@ from plane.utils.issue_filters import issue_filters from plane.utils.user_timezone_converter import user_timezone_converter -class ModuleIssueViewSet(WebhookMixin, BaseViewSet): +class ModuleIssueViewSet(BaseViewSet): serializer_class = ModuleIssueSerializer model = ModuleIssue webhook_event = "module_issue" diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index d8791ae9bf0..6017a420fbd 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -1,5 +1,6 @@ # Python imports import boto3 +import json # Django imports from django.db import IntegrityError @@ -14,6 +15,7 @@ ) from django.conf import settings from django.utils import timezone +from django.core.serializers.json import DjangoJSONEncoder # Third Party imports from rest_framework.response import Response @@ -22,7 +24,7 @@ from rest_framework.permissions import AllowAny # Module imports -from plane.app.views.base import BaseViewSet, BaseAPIView, WebhookMixin +from plane.app.views.base import BaseViewSet, BaseAPIView from plane.app.serializers import ( ProjectSerializer, ProjectListSerializer, @@ -50,9 +52,10 @@ Issue, ) from plane.utils.cache import cache_response +from plane.bgtasks.webhook_task import model_activity -class ProjectViewSet(WebhookMixin, BaseViewSet): +class ProjectViewSet(BaseViewSet): serializer_class = ProjectListSerializer model = Project webhook_event = "project" @@ -334,6 +337,17 @@ def create(self, request, slug): .filter(pk=serializer.data["id"]) .first() ) + + model_activity.delay( + model_name="project", + model_id=str(project.id), + requested_data=request.data, + current_instance=None, + actor_id=request.user.id, + slug=slug, + origin=request.META.get("HTTP_ORIGIN"), + ) + serializer = ProjectListSerializer(project) return Response( serializer.data, status=status.HTTP_201_CREATED @@ -364,7 +378,9 @@ def partial_update(self, request, slug, pk=None): workspace = Workspace.objects.get(slug=slug) project = Project.objects.get(pk=pk) - + current_instance = json.dumps( + ProjectSerializer(project).data, cls=DjangoJSONEncoder + ) if project.archived_at: return Response( {"error": "Archived projects cannot be updated"}, @@ -402,6 +418,16 @@ def partial_update(self, request, slug, pk=None): .filter(pk=serializer.data["id"]) .first() ) + + model_activity.delay( + model_name="project", + model_id=str(project.id), + requested_data=request.data, + current_instance=current_instance, + actor_id=request.user.id, + slug=slug, + origin=request.META.get("HTTP_ORIGIN"), + ) serializer = ProjectListSerializer(project) return Response(serializer.data, status=status.HTTP_200_OK) return Response( diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 2d55d557964..007b3e48c96 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -31,6 +31,7 @@ ) from plane.settings.redis import redis_instance from plane.utils.exception_logger import log_exception +from plane.bgtasks.webhook_task import webhook_activity # Track Changes in name @@ -1296,7 +1297,7 @@ def create_issue_vote_activity( IssueActivity( issue_id=issue_id, actor_id=actor_id, - verb="created", + verb="updated", old_value=None, new_value=requested_data.get("vote"), field="vote", @@ -1365,7 +1366,7 @@ def create_issue_relation_activity( IssueActivity( issue_id=issue_id, actor_id=actor_id, - verb="created", + verb="updated", old_value="", new_value=f"{issue.project.identifier}-{issue.sequence_id}", field=requested_data.get("relation_type"), @@ -1380,7 +1381,7 @@ def create_issue_relation_activity( IssueActivity( issue_id=related_issue, actor_id=actor_id, - verb="created", + verb="updated", old_value="", new_value=f"{issue.project.identifier}-{issue.sequence_id}", field=( @@ -1606,6 +1607,7 @@ def issue_activity( subscriber=True, notification=False, origin=None, + inbox=None, ): try: issue_activities = [] @@ -1692,6 +1694,41 @@ def issue_activity( except Exception as e: log_exception(e) + for activity in issue_activities_created: + webhook_activity.delay( + event=( + "issue_comment" + if activity.field == "comment" + else "inbox_issue" if inbox else "issue" + ), + event_id=( + activity.issue_comment_id + if activity.field == "comment" + else inbox if inbox else activity.issue_id + ), + verb=activity.verb, + field=( + "description" + if activity.field == "comment" + else activity.field + ), + old_value=( + activity.old_value + if activity.old_value != "" + else None + ), + new_value=( + activity.new_value + if activity.new_value != "" + else None + ), + actor_id=activity.actor_id, + current_site=origin, + slug=activity.workspace.slug, + old_identifier=activity.old_identifier, + new_identifier=activity.new_identifier, + ) + if notification: notifications.delay( type=type, diff --git a/apiserver/plane/bgtasks/webhook_task.py b/apiserver/plane/bgtasks/webhook_task.py index 5ee0244c72a..d1e1cb34c84 100644 --- a/apiserver/plane/bgtasks/webhook_task.py +++ b/apiserver/plane/bgtasks/webhook_task.py @@ -25,6 +25,8 @@ ModuleIssueSerializer, ModuleSerializer, ProjectSerializer, + UserLiteSerializer, + InboxIssueSerializer, ) from plane.db.models import ( Cycle, @@ -37,6 +39,7 @@ User, Webhook, WebhookLog, + InboxIssue, ) from plane.license.utils.instance_value import get_email_configuration from plane.utils.exception_logger import log_exception @@ -49,6 +52,8 @@ "cycle_issue": CycleIssueSerializer, "module_issue": ModuleIssueSerializer, "issue_comment": IssueCommentSerializer, + "user": UserLiteSerializer, + "inbox_issue": InboxIssueSerializer, } MODEL_MAPPER = { @@ -59,6 +64,8 @@ "cycle_issue": CycleIssue, "module_issue": ModuleIssue, "issue_comment": IssueComment, + "user": User, + "inbox_issue": InboxIssue, } @@ -179,64 +186,6 @@ def webhook_task(self, webhook, slug, event, event_data, action, current_site): return -@shared_task() -def send_webhook(event, payload, kw, action, slug, bulk, current_site): - try: - webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True) - - if event == "project": - webhooks = webhooks.filter(project=True) - - if event == "issue": - webhooks = webhooks.filter(issue=True) - - if event == "module" or event == "module_issue": - webhooks = webhooks.filter(module=True) - - if event == "cycle" or event == "cycle_issue": - webhooks = webhooks.filter(cycle=True) - - if event == "issue_comment": - webhooks = webhooks.filter(issue_comment=True) - - if webhooks: - if action in ["POST", "PATCH"]: - if bulk and event in ["cycle_issue", "module_issue"]: - return - else: - event_data = [ - get_model_data( - event=event, - event_id=( - payload.get("id") - if isinstance(payload, dict) - else kw.get("pk") - ), - many=False, - ) - ] - - if action == "DELETE": - event_data = [{"id": kw.get("pk")}] - - for webhook in webhooks: - for data in event_data: - webhook_task.delay( - webhook=webhook.id, - slug=slug, - event=event, - event_data=data, - action=action, - current_site=current_site, - ) - - except Exception as e: - if settings.DEBUG: - print(e) - log_exception(e) - return - - @shared_task def send_webhook_deactivation_email( webhook_id, receiver_id, current_site, reason @@ -294,3 +243,240 @@ def send_webhook_deactivation_email( except Exception as e: log_exception(e) return + + +@shared_task( + bind=True, + autoretry_for=(requests.RequestException,), + retry_backoff=600, + max_retries=5, + retry_jitter=True, +) +def webhook_send_task( + self, + webhook, + slug, + event, + event_data, + action, + current_site, + activity, +): + try: + webhook = Webhook.objects.get(id=webhook, workspace__slug=slug) + + headers = { + "Content-Type": "application/json", + "User-Agent": "Autopilot", + "X-Plane-Delivery": str(uuid.uuid4()), + "X-Plane-Event": event, + } + + # # Your secret key + event_data = ( + json.loads(json.dumps(event_data, cls=DjangoJSONEncoder)) + if event_data is not None + else None + ) + + activity = ( + json.loads(json.dumps(activity, cls=DjangoJSONEncoder)) + if activity is not None + else None + ) + + action = { + "POST": "create", + "PATCH": "update", + "PUT": "update", + "DELETE": "delete", + }.get(action, action) + + payload = { + "event": event, + "action": action, + "webhook_id": str(webhook.id), + "workspace_id": str(webhook.workspace_id), + "data": event_data, + "activity": activity, + } + + # Use HMAC for generating signature + if webhook.secret_key: + hmac_signature = hmac.new( + webhook.secret_key.encode("utf-8"), + json.dumps(payload).encode("utf-8"), + hashlib.sha256, + ) + signature = hmac_signature.hexdigest() + headers["X-Plane-Signature"] = signature + + # Send the webhook event + response = requests.post( + webhook.url, + headers=headers, + json=payload, + timeout=30, + ) + + # Log the webhook request + WebhookLog.objects.create( + workspace_id=str(webhook.workspace_id), + webhook_id=str(webhook.id), + event_type=str(event), + request_method=str(action), + request_headers=str(headers), + request_body=str(payload), + response_status=str(response.status_code), + response_headers=str(response.headers), + response_body=str(response.text), + retry_count=str(self.request.retries), + ) + + except requests.RequestException as e: + # Log the failed webhook request + WebhookLog.objects.create( + workspace_id=str(webhook.workspace_id), + webhook_id=str(webhook.id), + event_type=str(event), + request_method=str(action), + request_headers=str(headers), + request_body=str(payload), + response_status=500, + response_headers="", + response_body=str(e), + retry_count=str(self.request.retries), + ) + # Retry logic + if self.request.retries >= self.max_retries: + Webhook.objects.filter(pk=webhook.id).update(is_active=False) + if webhook: + # send email for the deactivation of the webhook + send_webhook_deactivation_email( + webhook_id=webhook.id, + receiver_id=webhook.created_by_id, + reason=str(e), + current_site=current_site, + ) + return + raise requests.RequestException() + + except Exception as e: + if settings.DEBUG: + print(e) + log_exception(e) + return + + +@shared_task +def webhook_activity( + event, + verb, + field, + old_value, + new_value, + actor_id, + slug, + current_site, + event_id, + old_identifier, + new_identifier, +): + try: + webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True) + + if event == "project": + webhooks = webhooks.filter(project=True) + + if event == "issue": + webhooks = webhooks.filter(issue=True) + + if event == "module" or event == "module_issue": + webhooks = webhooks.filter(module=True) + + if event == "cycle" or event == "cycle_issue": + webhooks = webhooks.filter(cycle=True) + + if event == "issue_comment": + webhooks = webhooks.filter(issue_comment=True) + + for webhook in webhooks: + webhook_send_task.delay( + webhook=webhook.id, + slug=slug, + event=event, + event_data=get_model_data( + event=event, + event_id=event_id, + ), + action=verb, + current_site=current_site, + activity={ + "field": field, + "new_value": new_value, + "old_value": old_value, + "actor": get_model_data(event="user", event_id=actor_id), + "old_identifier": old_identifier, + "new_identifier": new_identifier, + }, + ) + return + except Exception as e: + if settings.DEBUG: + print(e) + log_exception(e) + return + + +@shared_task +def model_activity( + model_name, + model_id, + requested_data, + current_instance, + actor_id, + slug, + origin=None, +): + """Function takes in two json and computes differences between keys of both the json""" + if current_instance is None: + webhook_activity.delay( + event=model_name, + verb="created", + field=None, + old_value=None, + new_value=None, + actor_id=actor_id, + slug=slug, + current_site=origin, + event_id=model_id, + old_identifier=None, + new_identifier=None, + ) + return + + # Load the current instance + current_instance = ( + json.loads(current_instance) if current_instance is not None else None + ) + + # Loop through all keys in requested data and check the current value and requested value + for key in requested_data: + current_value = current_instance.get(key, None) + requested_value = requested_data.get(key, None) + if current_value != requested_value: + webhook_activity.delay( + event=model_name, + verb="updated", + field=key, + old_value=current_value, + new_value=requested_value, + actor_id=actor_id, + slug=slug, + current_site=origin, + event_id=model_id, + old_identifier=None, + new_identifier=None, + ) + + return From 653005bb3b75da0dda5d34ef4337206782f7ab54 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Mon, 6 May 2024 15:19:03 +0530 Subject: [PATCH 003/199] [WEB-1206] chore: bulk delete api logs (#4374) * chore: bulk delete api logs * chore: deletion time change --- apiserver/plane/bgtasks/api_logs_task.py | 15 +++++++++++++++ apiserver/plane/celery.py | 4 ++++ apiserver/plane/settings/common.py | 1 + 3 files changed, 20 insertions(+) create mode 100644 apiserver/plane/bgtasks/api_logs_task.py diff --git a/apiserver/plane/bgtasks/api_logs_task.py b/apiserver/plane/bgtasks/api_logs_task.py new file mode 100644 index 00000000000..038b939d54c --- /dev/null +++ b/apiserver/plane/bgtasks/api_logs_task.py @@ -0,0 +1,15 @@ +from django.utils import timezone +from datetime import timedelta +from plane.db.models import APIActivityLog +from celery import shared_task + + +@shared_task +def delete_api_logs(): + # Get the logs older than 30 days to delete + logs_to_delete = APIActivityLog.objects.filter( + created_at__lte=timezone.now() - timedelta(days=30) + ) + + # Delete the logs + logs_to_delete._raw_delete(logs_to_delete.db) diff --git a/apiserver/plane/celery.py b/apiserver/plane/celery.py index 056dfb16bc5..d3e742f14a9 100644 --- a/apiserver/plane/celery.py +++ b/apiserver/plane/celery.py @@ -32,6 +32,10 @@ "task": "plane.bgtasks.email_notification_task.stack_email_notification", "schedule": crontab(minute="*/5"), }, + "check-every-day-to-delete-api-logs": { + "task": "plane.bgtasks.api_logs_task.delete_api_logs", + "schedule": crontab(hour=0, minute=0), + }, } # Load task modules from all registered Django app configs. diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 06c6778d972..5a99f06efcc 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -293,6 +293,7 @@ "plane.bgtasks.exporter_expired_task", "plane.bgtasks.file_asset_task", "plane.bgtasks.email_notification_task", + "plane.bgtasks.api_logs_task", # management tasks "plane.bgtasks.dummy_data_task", ) From 463f4781aeb4d16e0a4ee147a562c6dfbea152ac Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Mon, 6 May 2024 15:22:45 +0530 Subject: [PATCH 004/199] [WEB-1137] fix: Firefox distorted vertical text (#4376) --- .../issues/issue-layouts/kanban/headers/group-by-card.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index afdd3351585..c84e7518083 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -111,8 +111,8 @@ export const HeaderGroupByCard: FC = observer((props) => {
Date: Mon, 6 May 2024 15:27:56 +0530 Subject: [PATCH 005/199] [WEB-1007] chore: invalid issue error empty state added (#4372) --- .../projects/[projectId]/issues/[issueId].tsx | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 27dfe92efa0..556337b859c 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -1,25 +1,32 @@ import React, { ReactElement, useEffect } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; +import { useTheme } from "next-themes"; import useSWR from "swr"; -// layouts +// ui import { Loader } from "@plane/ui"; -import { PageHead } from "@/components/core"; // components +import { EmptyState } from "@/components/common"; +import { PageHead } from "@/components/core"; import { ProjectIssueDetailsHeader } from "@/components/headers"; import { IssueDetailRoot } from "@/components/issues"; -// ui -// types -// store hooks +// hooks import { useApplication, useIssueDetail, useProject } from "@/hooks/store"; +// layouts import { AppLayout } from "@/layouts/app-layout"; +// types import { NextPageWithLayout } from "@/lib/types"; +// assets +import emptyIssueDark from "public/empty-state/search/issue-dark.webp"; +import emptyIssueLight from "public/empty-state/search/issues-light.webp"; const IssueDetailsPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; // hooks + const { resolvedTheme } = useTheme(); + // store hooks const { fetchIssue, issue: { getIssueById }, @@ -27,7 +34,11 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => { const { getProjectById } = useProject(); const { theme: themeStore } = useApplication(); // fetching issue details - const { isLoading, data: swrIssueDetails } = useSWR( + const { + isLoading, + data: swrIssueDetails, + error, + } = useSWR( workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null, workspaceSlug && projectId && issueId ? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString()) @@ -57,7 +68,17 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => { return ( <> - {issueLoader ? ( + {error ? ( + router.push(`/${workspaceSlug}/projects/${projectId}/issues`), + }} + /> + ) : issueLoader ? (
From 59f1cc1962d479930f5ac61589e56c17b5d3c1d3 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 6 May 2024 15:28:33 +0530 Subject: [PATCH 006/199] [WEB-1114] chore: recent activity message updated (#4371) --- web/components/core/activity.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index a70cd072792..2726ccdc3e9 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -724,6 +724,7 @@ const activityDetails: { )} + {activity.verb === "2" && ` from inbox by marking a duplicate issue.`} ), icon: