From c81848591322c9512e80d4651c61a34d1faa6db2 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Sat, 6 May 2023 19:55:16 +0530 Subject: [PATCH 01/51] dev: initialize inbox --- apiserver/plane/db/models/inbox.py | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 apiserver/plane/db/models/inbox.py diff --git a/apiserver/plane/db/models/inbox.py b/apiserver/plane/db/models/inbox.py new file mode 100644 index 00000000000..4768e4d32c6 --- /dev/null +++ b/apiserver/plane/db/models/inbox.py @@ -0,0 +1,32 @@ +# Django imports +from django.db import models + +# Module imports +from plane.db.models import ProjectBaseModel + + +class Inbox(ProjectBaseModel): + name = models.CharField(max_length=255) + description = models.TextField(verbose_name="Inbox Description", blank=True) + is_default = models.BooleanField(default=False) + + def __str__(self): + """Return name of the Inbox""" + return f"{self.name} <{self.project.name}>" + + class Meta: + unique_together = ["name", "project"] + verbose_name = "Inbox" + verbose_name_plural = "Inboxes" + db_table = "inboxes" + ordering = ("name",) + + +class InboxIssue(ProjectBaseModel): + inbox = models.ForeignKey( + "db.Inbox", related_name="issue_inbox", on_delete=models.CASCADE + ) + issue = models.ForeignKey( + "db.Issue", related_name="issue_inbox", on_delete=models.CASCADE + ) + status = models.IntegerField() \ No newline at end of file From f04ded6fd77e54e330797e14aa7c9ab496abc221 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 8 May 2023 00:34:50 +0530 Subject: [PATCH 02/51] dev: inbox and inbox issues models, views and serializers --- apiserver/plane/api/serializers/__init__.py | 8 +- apiserver/plane/api/serializers/inbox.py | 26 +++++ apiserver/plane/api/urls.py | 48 +++++++++ apiserver/plane/api/views/__init__.py | 2 + apiserver/plane/api/views/inbox.py | 109 ++++++++++++++++++++ apiserver/plane/db/models/__init__.py | 2 + apiserver/plane/db/models/inbox.py | 14 ++- 7 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 apiserver/plane/api/serializers/inbox.py create mode 100644 apiserver/plane/api/views/inbox.py diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 79014c53db7..62f08301e36 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -69,4 +69,10 @@ from .page import PageSerializer, PageBlockSerializer, PageFavoriteSerializer -from .estimate import EstimateSerializer, EstimatePointSerializer, EstimateReadSerializer +from .estimate import ( + EstimateSerializer, + EstimatePointSerializer, + EstimateReadSerializer, +) + +from .inbox import InboxSerializer, InboxIssueSerializer diff --git a/apiserver/plane/api/serializers/inbox.py b/apiserver/plane/api/serializers/inbox.py new file mode 100644 index 00000000000..01734da3d90 --- /dev/null +++ b/apiserver/plane/api/serializers/inbox.py @@ -0,0 +1,26 @@ +# Module imports +from .base import BaseSerializer +from .issue import IssueFlatSerializer +from plane.db.models import Inbox, InboxIssue + + +class InboxSerializer(BaseSerializer): + class Meta: + model = Inbox + fields = "__all__" + read_only_fields = [ + "project", + "workspace", + ] + + +class InboxIssueSerializer(BaseSerializer): + issue_detail = IssueFlatSerializer(source="issue", read_only=True) + + class Meta: + model = InboxIssue + fields = "__all__" + read_only_fields = [ + "project", + "workspace", + ] diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index a88744b4a7e..7d81f364ccc 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -148,6 +148,10 @@ # Release Notes ReleaseNotesEndpoint, ## End Release Notes + # Inbox + InboxViewSet, + InboxIssueViewSet, + ## End Inbox ) @@ -1285,4 +1289,48 @@ name="release-notes", ), ## End Release Notes + # Inbox + path( + "workspaces//projects//inboxes/", + InboxViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="inbox", + ), + path( + "workspaces//projects//inboxes//", + InboxViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="inbox", + ), + path( + "workspaces//projects//inboxes//inbox-issues/", + InboxIssueViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="inbox-issue", + ), + path( + "workspaces//projects//inboxes//inbox-issues//", + InboxIssueViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="inbox-issue", + ), + ## End Inbox ] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 536fd83bfd2..7192e02eb25 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -140,3 +140,5 @@ from .release import ReleaseNotesEndpoint + +from .inbox import InboxViewSet, InboxIssueViewSet diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py new file mode 100644 index 00000000000..aec28be5b2e --- /dev/null +++ b/apiserver/plane/api/views/inbox.py @@ -0,0 +1,109 @@ +# Third party imports +from rest_framework import status +from rest_framework.response import Response +from sentry_sdk import capture_exception + +# Module imports +from .base import BaseViewSet +from plane.api.permissions import ProjectBasePermission +from plane.db.models import Project, Inbox, InboxIssue +from plane.api.serializers import ( + InboxSerializer, + InboxIssueSerializer, + IssueCreateSerializer, +) + + +class InboxViewSet(BaseViewSet): + permission_classes = [ + ProjectBasePermission, + ] + + serializer_class = InboxSerializer + model = Inbox + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + ) + + def perform_create(self, serializer): + serializer.save(project_id=self.kwargs.get("project_id")) + + def destroy(self, request, slug, project_id, pk): + try: + inbox = Inbox.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + + if inbox.is_default: + return Response( + {"error": "You cannot delete the default inbox"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + inbox.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wronf please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class InboxIssueViewSet(BaseViewSet): + permission_classes = [ + ProjectBasePermission, + ] + serializer_class = InboxIssueSerializer + model = InboxIssue + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + inbox_id=self.kwargs.get("inbox_id"), + ) + .select_related("issue", "workspace", "project") + ) + + def create(self, request, slug, project_id, inbox_id): + try: + project = Project.objects.get(workspace__slug=slug, pk=project_id) + issue_serializer = IssueCreateSerializer( + data=request.data.get("issue"), context={"project": project} + ) + + if issue_serializer.is_valid(): + issue_serializer.save() + inbox_issue = InboxIssue.objects.create( + issue_id=issue_serializer.data["id"], + inbox_id=inbox_id, + project_id=project_id, + ) + inbox_serializer = InboxIssueSerializer(inbox_issue) + + return Response( + { + "issue": issue_serializer.data, + "inbox_issue": inbox_serializer.data, + }, + status=status.HTTP_201_CREATED, + ) + return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index e32d768e034..e97aacb44a4 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -67,3 +67,5 @@ from .page import Page, PageBlock, PageFavorite, PageLabel from .estimate import Estimate, EstimatePoint + +from .inbox import Inbox, InboxIssue \ No newline at end of file diff --git a/apiserver/plane/db/models/inbox.py b/apiserver/plane/db/models/inbox.py index 4768e4d32c6..0ec6a836743 100644 --- a/apiserver/plane/db/models/inbox.py +++ b/apiserver/plane/db/models/inbox.py @@ -29,4 +29,16 @@ class InboxIssue(ProjectBaseModel): issue = models.ForeignKey( "db.Issue", related_name="issue_inbox", on_delete=models.CASCADE ) - status = models.IntegerField() \ No newline at end of file + status = models.IntegerField( + choices=((-1, "Rejected"), (0, "Snoozed"), (1, "Accepted")), default=0 + ) + + class Meta: + verbose_name = "InboxIssue" + verbose_name_plural = "InboxIssues" + db_table = "inbox_issues" + ordering = ("-created_at",) + + def __str__(self): + """Return name of the Issue""" + return f"{self.issue.name} <{self.inbox.name}>" From 500184f90ff2dd6e8924596cfaa8669d7a6f1b3e Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 8 May 2023 13:35:11 +0530 Subject: [PATCH 03/51] dev: issue object filter for inbox --- apiserver/plane/api/views/cycle.py | 6 +++--- apiserver/plane/api/views/importer.py | 2 +- apiserver/plane/api/views/issue.py | 14 ++++++-------- apiserver/plane/api/views/module.py | 6 +++--- apiserver/plane/api/views/state.py | 2 +- apiserver/plane/db/models/issue.py | 14 ++++++++++++++ 6 files changed, 28 insertions(+), 16 deletions(-) diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index f61a9348707..08833062104 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -225,7 +225,7 @@ def get_queryset(self): super() .get_queryset() .annotate( - sub_issues_count=Issue.objects.filter(parent=OuterRef("issue_id")) + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -249,9 +249,9 @@ def list(self, request, slug, project_id, cycle_id): group_by = request.GET.get("group_by", False) filters = issue_filters(request.query_params, "GET") issues = ( - Issue.objects.filter(issue_cycle__cycle_id=cycle_id) + Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id) .annotate( - sub_issues_count=Issue.objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") diff --git a/apiserver/plane/api/views/importer.py b/apiserver/plane/api/views/importer.py index b9a7fe0c5b1..2fb594f2505 100644 --- a/apiserver/plane/api/views/importer.py +++ b/apiserver/plane/api/views/importer.py @@ -241,7 +241,7 @@ def delete(self, request, slug, service, pk): ) # Delete all imported Issues imported_issues = importer.imported_data.get("issues", []) - Issue.objects.filter(id__in=imported_issues).delete() + Issue.issue_objects.filter(id__in=imported_issues).delete() # Delete all imported Labels imported_labels = importer.imported_data.get("labels", []) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 987677bb253..981d1afc6d5 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -120,9 +120,7 @@ def perform_destroy(self, instance): def get_queryset(self): return ( - super() - .get_queryset() - .annotate( + Issue.issue_objects.annotate( sub_issues_count=Issue.objects.filter(parent=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) @@ -219,7 +217,7 @@ def create(self, request, slug, project_id): def retrieve(self, request, slug, project_id, pk=None): try: - issue = Issue.objects.get( + issue = Issue.issue_objects.get( workspace__slug=slug, project_id=project_id, pk=pk ) return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) @@ -234,7 +232,7 @@ class UserWorkSpaceIssues(BaseAPIView): def get(self, request, slug): try: issues = ( - Issue.objects.filter(assignees__in=[request.user], workspace__slug=slug) + Issue.issue_objects.filter(assignees__in=[request.user], workspace__slug=slug) .annotate( sub_issues_count=Issue.objects.filter(parent=OuterRef("id")) .order_by() @@ -282,7 +280,7 @@ class WorkSpaceIssuesEndpoint(BaseAPIView): def get(self, request, slug): try: issues = ( - Issue.objects.filter(workspace__slug=slug) + Issue.issue_objects.filter(workspace__slug=slug) .filter(project__project_projectmember__member=self.request.user) .order_by("-created_at") ) @@ -581,7 +579,7 @@ class SubIssuesEndpoint(BaseAPIView): def get(self, request, slug, project_id, issue_id): try: sub_issues = ( - Issue.objects.filter( + Issue.issue_objects.filter( parent_id=issue_id, workspace__slug=slug, project_id=project_id ) .select_related("project") @@ -636,7 +634,7 @@ def post(self, request, slug, project_id, issue_id): status=status.HTTP_400_BAD_REQUEST, ) - sub_issues = Issue.objects.filter(id__in=sub_issue_ids) + sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids) for sub_issue in sub_issues: sub_issue.parent = parent_issue diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index 8f0cabeaf35..7c285c24244 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -201,7 +201,7 @@ def get_queryset(self): super() .get_queryset() .annotate( - sub_issues_count=Issue.objects.filter(parent=OuterRef("issue")) + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -226,9 +226,9 @@ def list(self, request, slug, project_id, module_id): group_by = request.GET.get("group_by", False) filters = issue_filters(request.query_params, "GET") issues = ( - Issue.objects.filter(issue_module__module_id=module_id) + Issue.issue_objects.filter(issue_module__module_id=module_id) .annotate( - sub_issues_count=Issue.objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py index b217a662d06..29cba7a74ff 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/api/views/state.py @@ -89,7 +89,7 @@ def destroy(self, request, slug, project_id, pk): ) # Check for any issues in the state - issue_exist = Issue.objects.filter(state=pk).exists() + issue_exist = Issue.issue_objects.filter(state=pk).exists() if issue_exist: return Response( diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index fed946a613c..e27d83540bd 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -17,6 +17,17 @@ # TODO: Handle identifiers for Bulk Inserts - nk +class IssueManager(models.Manager): + def get_queryset(self): + return ( + super() + .get_queryset() + .filter( + models.Q(issue_inbox__status=1) | models.Q(issue_inbox__isnull=True) + ) + ) + + class Issue(ProjectBaseModel): PRIORITY_CHOICES = ( ("urgent", "Urgent"), @@ -68,6 +79,9 @@ class Issue(ProjectBaseModel): sort_order = models.FloatField(default=65535) completed_at = models.DateTimeField(null=True) + objects = models.Manager() + issue_objects = IssueManager() + class Meta: verbose_name = "Issue" verbose_name_plural = "Issues" From f38358f88a9f64db6c5f7acc9cbabecb2d3a5360 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 8 May 2023 16:48:39 +0530 Subject: [PATCH 04/51] dev: filter for search issues --- apiserver/plane/api/views/search.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apiserver/plane/api/views/search.py b/apiserver/plane/api/views/search.py index 823a1fcc8c7..f0262471909 100644 --- a/apiserver/plane/api/views/search.py +++ b/apiserver/plane/api/views/search.py @@ -57,7 +57,7 @@ def filter_issues(self, query, slug, project_id): else: q |= Q(**{f"{field}__icontains": query}) return ( - Issue.objects.filter( + Issue.issue_objects.filter( q, project__project_projectmember__member=self.request.user, workspace__slug=slug, @@ -218,11 +218,11 @@ def get(self, request, slug, project_id): ) if parent == "true" and issue_id: - issue = Issue.objects.get(pk=issue_id) + issue = Issue.issue_objects.get(pk=issue_id) issues = issues.filter( ~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True ).exclude( - pk__in=Issue.objects.filter(parent__isnull=False).values_list( + pk__in=Issue.issue_objects.filter(parent__isnull=False).values_list( "parent_id", flat=True ) ) From 515fd707fb4c3f27ec4bdbcb0c0d3fc8193c839f Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 8 May 2023 16:59:15 +0530 Subject: [PATCH 05/51] dev: inbox snooze and duplicates --- apiserver/plane/api/views/inbox.py | 5 +++++ apiserver/plane/db/models/inbox.py | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index aec28be5b2e..c2622429afd 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -1,3 +1,7 @@ +# Django import +from django.utils import timezone +from django.db.models import Q + # Third party imports from rest_framework import status from rest_framework.response import Response @@ -69,6 +73,7 @@ def get_queryset(self): super() .get_queryset() .filter( + Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"), inbox_id=self.kwargs.get("inbox_id"), diff --git a/apiserver/plane/db/models/inbox.py b/apiserver/plane/db/models/inbox.py index 0ec6a836743..ed1ece776ee 100644 --- a/apiserver/plane/db/models/inbox.py +++ b/apiserver/plane/db/models/inbox.py @@ -30,7 +30,12 @@ class InboxIssue(ProjectBaseModel): "db.Issue", related_name="issue_inbox", on_delete=models.CASCADE ) status = models.IntegerField( - choices=((-1, "Rejected"), (0, "Snoozed"), (1, "Accepted")), default=0 + choices=((-1, "Rejected"), (0, "Snoozed"), (1, "Accepted"), (2, "Duplicate")), + default=0, + ) + snoozed_till = models.DateTimeField(null=True) + duplicate_to = models.ForeignKey( + "db.Issue", related_name="inbox_duplicate", on_delete=models.CASCADE ) class Meta: From 5f0583331e8a26a9867dbcdd9e919969ff8017c1 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 10 May 2023 16:06:17 +0530 Subject: [PATCH 06/51] dev: set duplicate to null by default --- apiserver/plane/db/models/inbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/db/models/inbox.py b/apiserver/plane/db/models/inbox.py index ed1ece776ee..18ab11ac97a 100644 --- a/apiserver/plane/db/models/inbox.py +++ b/apiserver/plane/db/models/inbox.py @@ -35,7 +35,7 @@ class InboxIssue(ProjectBaseModel): ) snoozed_till = models.DateTimeField(null=True) duplicate_to = models.ForeignKey( - "db.Issue", related_name="inbox_duplicate", on_delete=models.CASCADE + "db.Issue", related_name="inbox_duplicate", on_delete=models.SET_NULL, null=True ) class Meta: From 25368089d2495cf90d46f72eaf21eed827ec7599 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain Date: Thu, 11 May 2023 13:57:42 +0530 Subject: [PATCH 07/51] feat: inbox ui and services --- apps/app/components/inbox/index.ts | 0 apps/app/components/issues/modal.tsx | 81 ++-- apps/app/constants/fetch-keys.ts | 4 + apps/app/contexts/project-member.context.tsx | 4 +- apps/app/hooks/use-issues-view.tsx | 5 +- .../project-authorization-wrapper.tsx | 5 +- .../workspace-authorization-wrapper.tsx | 3 +- .../projects/[projectId]/inbox/[inboxId].tsx | 410 ++++++++++++++++++ apps/app/services/inbox.service.ts | 134 ++++++ apps/app/services/track-event.service.ts | 24 + apps/app/types/inbox.d.ts | 28 ++ apps/app/types/index.d.ts | 3 +- 12 files changed, 670 insertions(+), 31 deletions(-) create mode 100644 apps/app/components/inbox/index.ts create mode 100644 apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx create mode 100644 apps/app/services/inbox.service.ts create mode 100644 apps/app/types/inbox.d.ts diff --git a/apps/app/components/inbox/index.ts b/apps/app/components/inbox/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx index b2eb45e2860..dcf2e01cb2c 100644 --- a/apps/app/components/issues/modal.tsx +++ b/apps/app/components/issues/modal.tsx @@ -10,6 +10,7 @@ import { Dialog, Transition } from "@headlessui/react"; import projectService from "services/project.service"; import modulesService from "services/modules.service"; import issuesService from "services/issues.service"; +import inboxServices from "services/inbox.service"; // hooks import useUser from "hooks/use-user"; import useToast from "hooks/use-toast"; @@ -31,6 +32,7 @@ import { MODULE_ISSUES_WITH_PARAMS, CYCLE_DETAILS, MODULE_DETAILS, + INBOX_ISSUES, } from "constants/fetch-keys"; export interface IssuesModalProps { @@ -53,7 +55,7 @@ export const CreateUpdateIssueModal: React.FC = ({ const [activeProject, setActiveProject] = useState(null); const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { workspaceSlug, projectId, cycleId, moduleId, inboxId } = router.query; const { params } = useIssuesView(); @@ -126,33 +128,60 @@ export const CreateUpdateIssueModal: React.FC = ({ }; const createIssue = async (payload: Partial) => { - await issuesService - .createIssues(workspaceSlug as string, activeProject ?? "", payload) - .then((res) => { - mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); - - if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle); - if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module); - - if (!createMore) handleClose(); - - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", + if (inboxId) + await inboxServices + .createInboxIssue(workspaceSlug as string, projectId as string, inboxId as string, { + issue: payload, + }) + .then((res) => { + mutate(INBOX_ISSUES(inboxId as string)); + if (!createMore) handleClose(); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue created successfully.", + }); + + if (payload.assignees_list?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE); + + if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be created. Please try again.", + }); }); - - if (payload.assignees_list?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE); - - if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Issue could not be created. Please try again.", + else + await issuesService + .createIssues(workspaceSlug as string, activeProject ?? "", payload) + .then((res) => { + mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); + + if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle); + if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module); + + if (!createMore) handleClose(); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue created successfully.", + }); + + if (payload.assignees_list?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE); + + if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be created. Please try again.", + }); }); - }); }; const updateIssue = async (payload: Partial) => { diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index a4773e23357..a5f9c5ec5cf 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -114,6 +114,10 @@ export const VIEWS_LIST = (projectId: string) => `VIEWS_LIST_${projectId.toUpper export const VIEW_ISSUES = (viewId: string) => `VIEW_ISSUES_${viewId.toUpperCase()}`; export const VIEW_DETAILS = (viewId: string) => `VIEW_DETAILS_${viewId.toUpperCase()}`; +export const INBOX_LIST = (projectId: string) => `VIEWS_LIST_${projectId.toUpperCase()}`; +export const INBOX_ISSUES = (inboxId: string) => `VIEW_ISSUES_${inboxId.toUpperCase()}`; +export const INBOX_DETAILS = (inboxId: string) => `VIEW_DETAILS_${inboxId.toUpperCase()}`; + // Issues export const ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId.toUpperCase()}`; export const SUB_ISSUES = (issueId: string) => `SUB_ISSUES_${issueId.toUpperCase()}`; diff --git a/apps/app/contexts/project-member.context.tsx b/apps/app/contexts/project-member.context.tsx index 60cb8954f66..db5e45e77e8 100644 --- a/apps/app/contexts/project-member.context.tsx +++ b/apps/app/contexts/project-member.context.tsx @@ -31,7 +31,9 @@ export const ProjectMemberProvider: React.FC = (props) => { const { children } = props; const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + // const { workspaceSlug, projectId } = router.query; + const projectId = "f5462100-2156-41a9-af13-50d76077b01d"; + const workspaceSlug = "laland"; const { data: memberDetails, error } = useSWR( workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null, diff --git a/apps/app/hooks/use-issues-view.tsx b/apps/app/hooks/use-issues-view.tsx index 57b584db426..a62c85312a9 100644 --- a/apps/app/hooks/use-issues-view.tsx +++ b/apps/app/hooks/use-issues-view.tsx @@ -40,7 +40,10 @@ const useIssuesView = () => { } = useContext(issueViewContext); const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { cycleId, moduleId } = router.query; + + const projectId = "f5462100-2156-41a9-af13-50d76077b01d"; + const workspaceSlug = "laland"; const params: any = { order_by: orderBy, diff --git a/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx b/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx index 61da09887c2..c93f81fd8cc 100644 --- a/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx +++ b/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx @@ -54,7 +54,10 @@ const ProjectAuthorizationWrapped: React.FC = ({ const [toggleSidebar, setToggleSidebar] = useState(false); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + // const { workspaceSlug, projectId } = router.query; + + const projectId = "f5462100-2156-41a9-af13-50d76077b01d"; + const workspaceSlug = "laland"; const { issueView } = useIssuesView(); diff --git a/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx b/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx index 90dcbcf131a..ad83702f3f7 100644 --- a/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx +++ b/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx @@ -51,7 +51,8 @@ export const WorkspaceAuthorizationLayout: React.FC = ({ const [toggleSidebar, setToggleSidebar] = useState(false); const router = useRouter(); - const { workspaceSlug } = router.query; + // const { workspaceSlug } = router.query; + const workspaceSlug = "laland"; const { data: workspaceMemberMe, error } = useSWR( workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug as string) : null, diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx new file mode 100644 index 00000000000..9e6673010e8 --- /dev/null +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -0,0 +1,410 @@ +import Router, { useRouter } from "next/router"; + +import useSWR, { mutate } from "swr"; + +// services +import projectService from "services/project.service"; +// layouts +import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; +// contexts +import { IssueViewContextProvider } from "contexts/issue-view.context"; +// helper +import { truncateText } from "helpers/string.helper"; +// components +import { IssuesFilterView, IssuesView } from "components/core"; +// ui +import { CustomMenu, PrimaryButton } from "components/ui"; +import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; +// icons +import { PlusIcon, ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline"; +// types +import type { NextPage } from "next"; +// fetch-keys +import { + INBOX_ISSUES, + ISSUE_DETAILS, + PROJECT_DETAILS, + PROJECT_ISSUES_ACTIVITY, + SUB_ISSUES, +} from "constants/fetch-keys"; +import { useState, useEffect, useCallback, useMemo, useRef } from "react"; +import useIssuesView from "hooks/use-issues-view"; +import { + AddComment, + IssueActivitySection, + IssueAttachmentUpload, + IssueAttachments, + IssueDescriptionForm, + IssueDetailsSidebar, + SubIssuesList, +} from "components/issues"; +import issuesService from "services/issues.service"; +import inboxServices from "services/inbox.service"; +import Link from "next/link"; +import { IIssue } from "types"; +import { useForm } from "react-hook-form"; + +const defaultValues = { + name: "", + description: "", + description_html: "", + estimate_point: null, + state: "", + assignees_list: [], + priority: "low", + blockers_list: [], + blocked_list: [], + target_date: new Date().toString(), + issue_cycle: null, + issue_module: null, + labels_list: [], +}; + +const ProjectIssues: NextPage = () => { + const router = useRouter(); + + const [isCreateInboxModalOpen, setIsCreateInboxModalOpen] = useState(false); + + const { workspaceSlug, projectId, inboxId, issueId } = router.query; + + const activeIssueRef = useRef(null); + + const { reset, control, watch } = useForm({ + defaultValues, + }); + + const { data: inboxIssues } = useSWR( + workspaceSlug && projectId && inboxId ? INBOX_ISSUES(inboxId as string) : null, + workspaceSlug && projectId && inboxId + ? () => + inboxServices.getInboxIssues( + workspaceSlug as string, + projectId as string, + inboxId as string + ) + : null + ); + + console.log("inboxIssues: ", inboxIssues); + + const { data: projectDetails } = useSWR( + workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, + workspaceSlug && projectId + ? () => projectService.getProject(workspaceSlug as string, projectId as string) + : null + ); + + const { + data: issueDetails, + mutate: mutateIssueDetails, + error: issueDetailError, + } = useSWR( + workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, + workspaceSlug && projectId && issueId + ? () => + issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string) + : null + ); + + const { data: siblingIssues } = useSWR( + workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null, + workspaceSlug && projectId && issueDetails?.parent + ? () => + issuesService.subIssues( + workspaceSlug as string, + projectId as string, + issueDetails.parent ?? "" + ) + : null + ); + + const submitChanges = useCallback( + async (formData: Partial) => { + if (!workspaceSlug || !projectId || !issueId) return; + + mutate( + ISSUE_DETAILS(issueId as string), + (prevData: IIssue) => ({ + ...prevData, + ...formData, + }), + false + ); + + const payload = { ...formData }; + await issuesService + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) + .then((res) => { + mutateIssueDetails(); + mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); + }) + .catch((e) => { + console.error(e); + }); + }, + [workspaceSlug, issueId, projectId, mutateIssueDetails] + ); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (!inboxIssues) return; + + if (!issueId) return; + + const currentIssueIndex = inboxIssues.findIndex((issue) => issue.issue === issueId); + + switch (e.key) { + case "ArrowUp": + Router.push({ + pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, + query: { + issueId: + currentIssueIndex === 0 + ? inboxIssues[inboxIssues.length - 1].issue + : inboxIssues[currentIssueIndex - 1].issue, + }, + }); + if (activeIssueRef.current) { + activeIssueRef.current.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "center", + }); + } + break; + case "ArrowDown": + Router.push({ + pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, + query: { + issueId: + currentIssueIndex === inboxIssues.length - 1 + ? inboxIssues[0].issue + : inboxIssues[currentIssueIndex + 1].issue, + }, + }); + + break; + default: + break; + } + }, + [workspaceSlug, projectId, issueId, inboxId, inboxIssues] + ); + + useEffect(() => { + document.addEventListener("keydown", onKeyDown); + + return () => { + document.removeEventListener("keydown", onKeyDown); + }; + }, [onKeyDown]); + + useEffect(() => { + if (!inboxIssues || inboxIssues.length === 0) return; + + if (!workspaceSlug || !projectId || !inboxId) return; + + Router.push({ + pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, + query: { + issueId: inboxIssues[0].issue, + }, + }); + }, [inboxIssues, workspaceSlug, projectId, inboxId]); + + return ( + + + + + + } + right={ +
+ { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + + Add Issue + +
+ } + > +
+
+
+

Inbox

+
+ + +
+
+
+ + +
+ {(inboxIssues?.findIndex((issue) => issue.issue === issueId) ?? 0) + 1} of{" "} + {inboxIssues?.length} +
+
+
actions button comes here
+
+ +
+ {issueDetails && ( +
+ {issueDetails?.parent && issueDetails.parent !== "" ? ( +
+ + + + + {issueDetails.project_detail.identifier}- + {issueDetails.parent_detail?.sequence_id} + + + {issueDetails.parent_detail?.name.substring(0, 50)} + + + + + + {siblingIssues && siblingIssues.length > 0 ? ( + siblingIssues.map((issue: IIssue) => ( + + + + {issueDetails.project_detail.identifier}-{issue.sequence_id} + + + + )) + ) : ( + + No other sibling issues + + )} + +
+ ) : null} + +
+ +
+
+ )} + + {!issueDetails && issueDetailError && ( +
Add accept button to accept inbox issue
+ )} + + {issueDetails && ( + <> +
+

Attachments

+
+ + +
+
+
+

Comments/Activity

+ + +
+ + )} +
+
+ + {issueDetails && ( +
+ +
+ )} +
+ + + ); +}; + +export default ProjectIssues; diff --git a/apps/app/services/inbox.service.ts b/apps/app/services/inbox.service.ts new file mode 100644 index 00000000000..61e12f87170 --- /dev/null +++ b/apps/app/services/inbox.service.ts @@ -0,0 +1,134 @@ +import APIService from "services/api.service"; +import trackEventServices from "services/track-event.service"; + +const { NEXT_PUBLIC_API_BASE_URL } = process.env; + +const trackEvent = + process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; + +// types +import type { IInboxIssue, IInbox } from "types"; + +class InboxServices extends APIService { + constructor() { + super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + } + + async getInboxes(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getInboxById(workspaceSlug: string, projectId: string, inboxId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getInboxIssues( + workspaceSlug: string, + projectId: string, + inboxId: string + ): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getInboxIssueById( + workspaceSlug: string, + projectId: string, + inboxId: string, + inboxIssueId: string + ): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async createInboxIssue( + workspaceSlug: string, + projectId: string, + inboxId: string, + data: any + ): Promise { + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`, + data + ) + .then((response) => { + if (trackEvent) trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_CREATE"); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + + // async updateInbox( + // workspaceSlug: string, + // projectId: string, + // inboxId: string, + // data: IInboxForm + // ): Promise { + // return this.put( + // `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}`, + // data + // ) + // .then((response) => { + // if (trackEvent) trackEventServices.trackInboxEvent(response?.data, "INBOX_UPDATE"); + // return response?.data; + // }) + // .catch((error) => { + // throw error?.response?.data; + // }); + // } + + // async patchInbox( + // workspaceSlug: string, + // projectId: string, + // inboxId: string, + // data: Partial + // ): Promise { + // return this.patch( + // `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}`, + // data + // ) + + // .then((response) => { + // if (trackEvent) trackEventServices.trackInboxEvent(response?.data, "INBOX_UPDATE"); + // return response?.data; + // }) + // .catch((error) => { + // throw error?.response?.data; + // }); + // } + + // async deleteInbox(workspaceSlug: string, projectId: string, inboxId: string): Promise { + // return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}`) + // .then((response) => { + // if (trackEvent) trackEventServices.trackInboxEvent(response?.data, "INBOX_DELETE"); + // return response?.data; + // }) + // .catch((error) => { + // throw error?.response?.data; + // }); + // } +} + +const inboxServices = new InboxServices(); + +export default inboxServices; diff --git a/apps/app/services/track-event.service.ts b/apps/app/services/track-event.service.ts index 54de7119b2c..bc1c55fd219 100644 --- a/apps/app/services/track-event.service.ts +++ b/apps/app/services/track-event.service.ts @@ -79,6 +79,8 @@ type GptEventType = "ASK_GPT" | "USE_GPT_RESPONSE_IN_ISSUE" | "USE_GPT_RESPONSE_ type IssueEstimateEventType = "ESTIMATE_CREATE" | "ESTIMATE_UPDATE" | "ESTIMATE_DELETE"; +type InboxEventType = "INBOX_CREATE" | "INBOX_UPDATE" | "INBOX_DELETE" | "INBOX_ISSUE_CREATE"; + type ImporterEventType = | "GITHUB_IMPORTER_CREATE" | "GITHUB_IMPORTER_DELETE" @@ -615,6 +617,28 @@ class TrackEventServices extends APIService { }, }); } + + // TODO: add types to the data + async trackInboxEvent(data: any, eventName: InboxEventType): Promise { + let payload: any; + if (eventName !== "INBOX_DELETE") + payload = { + ...data, + // change payload according to requirement + }; + else payload = data; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: { + ...payload, + }, + }, + }); + } } const trackEventServices = new TrackEventServices(); diff --git a/apps/app/types/inbox.d.ts b/apps/app/types/inbox.d.ts new file mode 100644 index 00000000000..a57dad359b1 --- /dev/null +++ b/apps/app/types/inbox.d.ts @@ -0,0 +1,28 @@ +export interface IInboxIssue { + id: string; + issue_detail: IInboxIssue; + created_at: Date; + updated_at: Date; + status: number; + snoozed_till: null; + created_by: string; + updated_by: string; + project: string; + workspace: string; + inbox: string; + issue: string; + duplicate_to: null; +} + +export interface IInboxIssue { + id: string; + name: string; + description: string; + priority: null; + start_date: null; + target_date: null; + sequence_id: number; + sort_order: number; +} + +export interface IInbox extends any {} diff --git a/apps/app/types/index.d.ts b/apps/app/types/index.d.ts index 259cb1d4139..d8203bf019d 100644 --- a/apps/app/types/index.d.ts +++ b/apps/app/types/index.d.ts @@ -10,8 +10,9 @@ export * from "./views"; export * from "./integration"; export * from "./pages"; export * from "./ai"; -export * from "./estimate" +export * from "./estimate"; export * from "./importer"; +export * from "./inbox"; export type NestedKeyOf = { [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object From 07faca38fc13590b39238b89d49e1d3e401af83d Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 15 May 2023 13:46:20 +0530 Subject: [PATCH 08/51] feat: project detail in inbox --- apiserver/plane/api/serializers/inbox.py | 4 ++++ apiserver/plane/api/views/inbox.py | 1 + 2 files changed, 5 insertions(+) diff --git a/apiserver/plane/api/serializers/inbox.py b/apiserver/plane/api/serializers/inbox.py index 01734da3d90..9e7c9e197f1 100644 --- a/apiserver/plane/api/serializers/inbox.py +++ b/apiserver/plane/api/serializers/inbox.py @@ -1,10 +1,13 @@ # Module imports from .base import BaseSerializer from .issue import IssueFlatSerializer +from .project import ProjectLiteSerializer from plane.db.models import Inbox, InboxIssue class InboxSerializer(BaseSerializer): + project_detail = ProjectLiteSerializer(source="project", read_only=True) + class Meta: model = Inbox fields = "__all__" @@ -16,6 +19,7 @@ class Meta: class InboxIssueSerializer(BaseSerializer): issue_detail = IssueFlatSerializer(source="issue", read_only=True) + project_detail = ProjectLiteSerializer(source="project", read_only=True) class Meta: model = InboxIssue diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index c2622429afd..6439a1eed0c 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -34,6 +34,7 @@ def get_queryset(self): workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"), ) + .select_related("workspace", "project") ) def perform_create(self, serializer): From 8e6dc97ef39e2ce92c58b7fbe9d7a571b945ef95 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain Date: Mon, 15 May 2023 18:03:32 +0530 Subject: [PATCH 09/51] style: layout, popover, icons, sidebar --- apps/app/components/icons/inbox-icon.tsx | 24 + apps/app/components/icons/index.ts | 2 + .../icons/stacked-layers-horizontal-icon.tsx | 24 + .../components/inbox/inbox-action-headers.tsx | 122 +++++ .../app/components/inbox/inbox-issue-card.tsx | 46 ++ .../components/inbox/inbox-main-content.tsx | 229 +++++++++ apps/app/components/inbox/index.ts | 3 + .../app/components/inbox/select-duplicate.tsx | 10 +- .../components/inbox/select-snooze-till.tsx | 91 ---- apps/app/contexts/project-member.context.tsx | 4 +- .../project-authorization-wrapper.tsx | 5 +- .../workspace-authorization-wrapper.tsx | 3 +- .../projects/[projectId]/inbox/[inboxId].tsx | 457 ++++-------------- apps/app/public/empty-state/empty-inbox.svg | 57 +++ apps/app/types/inbox.d.ts | 4 +- 15 files changed, 612 insertions(+), 469 deletions(-) create mode 100644 apps/app/components/icons/inbox-icon.tsx create mode 100644 apps/app/components/icons/stacked-layers-horizontal-icon.tsx create mode 100644 apps/app/components/inbox/inbox-action-headers.tsx create mode 100644 apps/app/components/inbox/inbox-issue-card.tsx create mode 100644 apps/app/components/inbox/inbox-main-content.tsx delete mode 100644 apps/app/components/inbox/select-snooze-till.tsx create mode 100644 apps/app/public/empty-state/empty-inbox.svg diff --git a/apps/app/components/icons/inbox-icon.tsx b/apps/app/components/icons/inbox-icon.tsx new file mode 100644 index 00000000000..6013c88616b --- /dev/null +++ b/apps/app/components/icons/inbox-icon.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const InboxIcon: React.FC = ({ + width = "24", + height = "24", + color = "#858E96", + className, +}) => ( + + + +); diff --git a/apps/app/components/icons/index.ts b/apps/app/components/icons/index.ts index b802121d5f8..11cd5811677 100644 --- a/apps/app/components/icons/index.ts +++ b/apps/app/components/icons/index.ts @@ -75,3 +75,5 @@ export * from "./video-file-icon"; export * from "./audio-file-icon"; export * from "./command-icon"; export * from "./color-picker-icon"; +export * from "./inbox-icon"; +export * from "./stacked-layers-horizontal-icon"; diff --git a/apps/app/components/icons/stacked-layers-horizontal-icon.tsx b/apps/app/components/icons/stacked-layers-horizontal-icon.tsx new file mode 100644 index 00000000000..2d5f75ba993 --- /dev/null +++ b/apps/app/components/icons/stacked-layers-horizontal-icon.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const StackedLayersHorizontalIcon: React.FC = ({ + width = "24", + height = "24", + className, + color = "#858e96", +}) => ( + + + +); diff --git a/apps/app/components/inbox/inbox-action-headers.tsx b/apps/app/components/inbox/inbox-action-headers.tsx new file mode 100644 index 00000000000..4874ad46d28 --- /dev/null +++ b/apps/app/components/inbox/inbox-action-headers.tsx @@ -0,0 +1,122 @@ +import { useState } from "react"; + +// icons +import { InboxIcon, StackedLayersHorizontalIcon } from "components/icons"; +import { + ChevronDownIcon, + ChevronUpIcon, + CheckCircleIcon, + ClockIcon, + XCircleIcon, +} from "@heroicons/react/24/outline"; + +// headless ui +import { Popover } from "@headlessui/react"; + +// react-datepicker +import DatePicker from "react-datepicker"; + +// components +import { PrimaryButton, SecondaryButton } from "components/ui"; + +type Props = { + issueCount: number; + currentIssueIndex: number; + onAccept: () => void; + onDecline: () => void; + onMarkAsDuplicate: () => void; + onSnooze: (date: Date | string) => void; +}; + +export const InboxActionHeader: React.FC = (props) => { + const { issueCount, currentIssueIndex, onAccept, onDecline, onMarkAsDuplicate, onSnooze } = props; + + const [date, setDate] = useState(new Date()); + + return ( +
+
+ +

Inbox

+
+ +
+
+ + +
+ {currentIssueIndex + 1}/{issueCount} +
+
+
+ + + Accept + + + + Mark as duplicate + + + + + + Snooze + + + + {({ close }) => ( +
+ { + if (!val) return; + setDate(val); + }} + dateFormat="dd-MM-yyyy" + inline + /> + { + close(); + onSnooze(date); + }} + > + Snooze + +
+ )} +
+
+ + + Decline + +
+
+
+ ); +}; diff --git a/apps/app/components/inbox/inbox-issue-card.tsx b/apps/app/components/inbox/inbox-issue-card.tsx new file mode 100644 index 00000000000..be6df0743eb --- /dev/null +++ b/apps/app/components/inbox/inbox-issue-card.tsx @@ -0,0 +1,46 @@ +// icons +import { getPriorityIcon } from "components/icons"; +import { CalendarIcon } from "@heroicons/react/24/outline"; + +// types +import type { IInboxIssue } from "types"; + +type Props = { + issue: IInboxIssue; + active: boolean; +}; + +export const InboxIssueCard: React.FC = (props) => { + const { issue, active } = props; + + return ( +
+
+

PL-152

+

{issue.issue_detail.name}

+
+
+ {getPriorityIcon( + issue.issue_detail.priority ?? "None", + "text-sm rounded bg-orange-200 bg-opacity-50 w-6 h-6 flex justify-center items-center text-orange-600" + )} + {issue.snoozed_till && ( +
+ +

+ {new Date(issue.snoozed_till).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + })} +

+
+ )} +
+
+ ); +}; diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx new file mode 100644 index 00000000000..b1d9fa5b172 --- /dev/null +++ b/apps/app/components/inbox/inbox-main-content.tsx @@ -0,0 +1,229 @@ +import { useCallback, useEffect } from "react"; + +import Link from "next/link"; +import Image from "next/image"; +import { useRouter } from "next/router"; + +import useSWR, { mutate } from "swr"; + +// react hook form +import { Control, UseFormReset, UseFormWatch } from "react-hook-form"; +// services +import issuesService from "services/issues.service"; +// ui +import { CustomMenu, PrimaryButton, Spinner } from "components/ui"; +// fetch-keys +import { + INBOX_ISSUES, + ISSUE_DETAILS, + PROJECT_ISSUES_ACTIVITY, + SUB_ISSUES, +} from "constants/fetch-keys"; + +import { + AddComment, + IssueActivitySection, + IssueAttachmentUpload, + IssueAttachments, + IssueDescriptionForm, + IssueDetailsSidebar, + SubIssuesList, +} from "components/issues"; + +// types +import type { IIssue } from "types"; + +type Props = { + onAccept: () => void; + control: Control; + watch: UseFormWatch; + reset: UseFormReset; +}; + +export const InboxMainContent: React.FC = (props) => { + const { onAccept, watch, control, reset } = props; + + const router = useRouter(); + const { workspaceSlug, projectId, issueId, inboxId } = router.query; + + const { + data: issueDetails, + mutate: mutateIssueDetails, + error: issueDetailError, + } = useSWR( + workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, + workspaceSlug && projectId && issueId + ? () => + issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string) + : null + ); + + const { data: siblingIssues } = useSWR( + workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null, + workspaceSlug && projectId && issueDetails?.parent + ? () => + issuesService.subIssues( + workspaceSlug as string, + projectId as string, + issueDetails.parent ?? "" + ) + : null + ); + + useEffect(() => { + if (!issueDetails || !issueId) return; + + mutate(PROJECT_ISSUES_ACTIVITY(issueId.toString())); + reset({ + ...issueDetails, + blockers_list: + issueDetails.blockers_list ?? + issueDetails.blocker_issues?.map((issue) => issue.blocker_issue_detail?.id), + blocked_list: + issueDetails.blocks_list ?? + issueDetails.blocked_issues?.map((issue) => issue.blocked_issue_detail?.id), + assignees_list: + issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id), + labels_list: issueDetails.labels_list ?? issueDetails.labels, + labels: issueDetails.labels_list ?? issueDetails.labels, + }); + }, [issueDetails, reset, issueId]); + + const submitChanges = useCallback( + async (formData: Partial) => { + if (!workspaceSlug || !projectId || !issueId || !inboxId) return; + + mutate( + ISSUE_DETAILS(issueId as string), + (prevData: IIssue) => ({ + ...prevData, + ...formData, + }), + false + ); + + const payload = { ...formData }; + await issuesService + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) + .then((res) => { + mutateIssueDetails(); + mutate(INBOX_ISSUES(inboxId.toString())); + mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); + }) + .catch((e) => { + console.error(e); + }); + }, + [workspaceSlug, issueId, projectId, mutateIssueDetails, inboxId] + ); + + if (!issueDetails && !issueDetailError) + return ( +
+ +
+ ); + + if (!issueDetails && issueDetailError) + return ( +
+
+ Empty Inbox +
+
+

+ You haven{"'"}t accepted Inbox issue. +

+

+ You have to accept the issue to view, add files, change states and more. +

+ Accept Inbox Issue +
+
+ ); + + if (issueDetails) + return ( +
+
+
+ {issueDetails?.parent && issueDetails.parent !== "" ? ( +
+ + + + + {issueDetails.project_detail.identifier}- + {issueDetails.parent_detail?.sequence_id} + + + {issueDetails.parent_detail?.name.substring(0, 50)} + + + + + + {siblingIssues && siblingIssues.length > 0 ? ( + siblingIssues.map((issue: IIssue) => ( + + + + {issueDetails.project_detail.identifier}-{issue.sequence_id} + + + + )) + ) : ( + + No other sibling issues + + )} + +
+ ) : null} + +
+ +
+ +
+

Attachments

+
+ + +
+
+
+

Comments/Activity

+ + +
+
+
+ +
+ +
+
+ ); + + return null; +}; diff --git a/apps/app/components/inbox/index.ts b/apps/app/components/inbox/index.ts index cf731ebb455..45dbb930103 100644 --- a/apps/app/components/inbox/index.ts +++ b/apps/app/components/inbox/index.ts @@ -1,2 +1,5 @@ export * from "./select-duplicate"; export * from "./select-snooze-till"; +export * from "./inbox-issue-card"; +export * from "./inbox-action-headers"; +export * from "./inbox-main-content"; diff --git a/apps/app/components/inbox/select-duplicate.tsx b/apps/app/components/inbox/select-duplicate.tsx index c519b7564e0..c150ae19db3 100644 --- a/apps/app/components/inbox/select-duplicate.tsx +++ b/apps/app/components/inbox/select-duplicate.tsx @@ -67,9 +67,9 @@ export const SelectDuplicateInboxIssueModal: React.FC = (props) => { (query === "" ? issues : issues?.filter((issue) => issue.name.includes(query))) ?? []; return ( -
-
- setQuery("")} appear> + setQuery("")} appear> +
+
= (props) => {
- +
-
+ ); }; diff --git a/apps/app/components/inbox/select-snooze-till.tsx b/apps/app/components/inbox/select-snooze-till.tsx deleted file mode 100644 index bd33e36f89b..00000000000 --- a/apps/app/components/inbox/select-snooze-till.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useState } from "react"; - -// react-datepicker -import DatePicker from "react-datepicker"; -// headless ui -import { Dialog, Transition } from "@headlessui/react"; -// hooks -import useToast from "hooks/use-toast"; -// ui -import { PrimaryButton, SecondaryButton } from "components/ui"; - -type Props = { - isOpen: boolean; - onClose: () => void; - onSubmit: (snoozeTill: Date | string) => void; -}; - -export const SelectSnoozeTillModal: React.FC = (props) => { - const { isOpen, onClose, onSubmit } = props; - - const [dateValue, setDateValue] = useState(new Date()); - - const { setToastAlert } = useToast(); - - const handleClose = () => { - onClose(); - }; - - const handleSubmit = () => { - if (!dateValue) - return setToastAlert({ - title: "Error", - type: "error", - }); - onSubmit(dateValue); - handleClose(); - }; - - return ( -
-
- - - -
- - -
- - -

Select issue

-
- { - if (val) setDateValue(val); - }} - dateFormat="dd-MM-yyyy" - inline - /> -
- -
- Cancel - Snooze till -
-
-
-
-
-
-
-
- ); -}; diff --git a/apps/app/contexts/project-member.context.tsx b/apps/app/contexts/project-member.context.tsx index db5e45e77e8..60cb8954f66 100644 --- a/apps/app/contexts/project-member.context.tsx +++ b/apps/app/contexts/project-member.context.tsx @@ -31,9 +31,7 @@ export const ProjectMemberProvider: React.FC = (props) => { const { children } = props; const router = useRouter(); - // const { workspaceSlug, projectId } = router.query; - const projectId = "f5462100-2156-41a9-af13-50d76077b01d"; - const workspaceSlug = "laland"; + const { workspaceSlug, projectId } = router.query; const { data: memberDetails, error } = useSWR( workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null, diff --git a/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx b/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx index 3a6ea2c1406..969cace9a0f 100644 --- a/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx +++ b/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx @@ -56,10 +56,7 @@ const ProjectAuthorizationWrapped: React.FC = ({ const [analyticsModal, setAnalyticsModal] = useState(false); const router = useRouter(); - // const { workspaceSlug, projectId } = router.query; - - const projectId = "f5462100-2156-41a9-af13-50d76077b01d"; - const workspaceSlug = "laland"; + const { workspaceSlug, projectId } = router.query; const { issueView } = useIssuesView(); diff --git a/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx b/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx index d6825cc67f0..5029131cb07 100644 --- a/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx +++ b/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx @@ -52,8 +52,7 @@ export const WorkspaceAuthorizationLayout: React.FC = ({ const [analyticsModal, setAnalyticsModal] = useState(false); const router = useRouter(); - // const { workspaceSlug } = router.query; - const workspaceSlug = "laland"; + const { workspaceSlug } = router.query; const { data: workspaceMemberMe, error } = useSWR( workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug as string) : null, diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index 85f38da989c..f03bf67577a 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useRef } from "react"; +import { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import Router, { useRouter } from "next/router"; @@ -9,41 +9,29 @@ import useSWR, { mutate } from "swr"; import { useForm } from "react-hook-form"; // services import inboxServices from "services/inbox.service"; -import issuesService from "services/issues.service"; import projectService from "services/project.service"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // contexts import { IssueViewContextProvider } from "contexts/issue-view.context"; // components -import { SelectDuplicateInboxIssueModal, SelectSnoozeTillModal } from "components/inbox"; +import { + InboxIssueCard, + InboxActionHeader, + InboxMainContent, + SelectDuplicateInboxIssueModal, +} from "components/inbox"; // helper import { truncateText } from "helpers/string.helper"; // ui -import { CustomMenu, PrimaryButton, Spinner, SecondaryButton } from "components/ui"; +import { PrimaryButton } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons -import { PlusIcon, ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline"; +import { PlusIcon } from "@heroicons/react/24/outline"; // types import type { NextPage } from "next"; // fetch-keys -import { - INBOX_ISSUES, - ISSUE_DETAILS, - PROJECT_DETAILS, - PROJECT_ISSUES_ACTIVITY, - SUB_ISSUES, -} from "constants/fetch-keys"; - -import { - AddComment, - IssueActivitySection, - IssueAttachmentUpload, - IssueAttachments, - IssueDescriptionForm, - IssueDetailsSidebar, - SubIssuesList, -} from "components/issues"; +import { INBOX_ISSUES, ISSUE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys"; // types import type { IIssue } from "types"; @@ -69,17 +57,14 @@ const ProjectIssues: NextPage = () => { const { workspaceSlug, projectId, inboxId, issueId } = router.query; - const activeIssueRef = useRef(null); - const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false); - const [selectSnoozeModalOpen, setSelectSnoozeModalOpen] = useState(false); const { reset, control, watch } = useForm({ defaultValues, }); const { data: inboxIssues } = useSWR( - workspaceSlug && projectId && inboxId ? INBOX_ISSUES(inboxId as string) : null, + workspaceSlug && projectId && inboxId ? INBOX_ISSUES(inboxId.toString()) : null, workspaceSlug && projectId && inboxId ? () => inboxServices.getInboxIssues( @@ -97,57 +82,6 @@ const ProjectIssues: NextPage = () => { : null ); - const { - data: issueDetails, - mutate: mutateIssueDetails, - error: issueDetailError, - } = useSWR( - workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, - workspaceSlug && projectId && issueId - ? () => - issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string) - : null - ); - - const { data: siblingIssues } = useSWR( - workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null, - workspaceSlug && projectId && issueDetails?.parent - ? () => - issuesService.subIssues( - workspaceSlug as string, - projectId as string, - issueDetails.parent ?? "" - ) - : null - ); - - const submitChanges = useCallback( - async (formData: Partial) => { - if (!workspaceSlug || !projectId || !issueId) return; - - mutate( - ISSUE_DETAILS(issueId as string), - (prevData: IIssue) => ({ - ...prevData, - ...formData, - }), - false - ); - - const payload = { ...formData }; - await issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) - .then((res) => { - mutateIssueDetails(); - mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); - }) - .catch((e) => { - console.error(e); - }); - }, - [workspaceSlug, issueId, projectId, mutateIssueDetails] - ); - const onKeyDown = useCallback( (e: KeyboardEvent) => { if (!inboxIssues) return; @@ -167,13 +101,6 @@ const ProjectIssues: NextPage = () => { : inboxIssues[currentIssueIndex - 1].issue, }, }); - if (activeIssueRef.current) { - activeIssueRef.current.scrollIntoView({ - behavior: "smooth", - block: "center", - inline: "center", - }); - } break; case "ArrowDown": Router.push({ @@ -215,25 +142,6 @@ const ProjectIssues: NextPage = () => { }); }, [inboxIssues, workspaceSlug, projectId, inboxId]); - useEffect(() => { - if (!issueDetails) return; - - mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); - reset({ - ...issueDetails, - blockers_list: - issueDetails.blockers_list ?? - issueDetails.blocker_issues?.map((issue) => issue.blocker_issue_detail?.id), - blocked_list: - issueDetails.blocks_list ?? - issueDetails.blocked_issues?.map((issue) => issue.blocked_issue_detail?.id), - assignees_list: - issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id), - labels_list: issueDetails.labels_list ?? issueDetails.labels, - labels: issueDetails.labels_list ?? issueDetails.labels, - }); - }, [issueDetails, reset, issueId]); - return ( {
} > -
-
-
-

Inbox

-
-
+
+ issue.issue === issueId) ?? 0} + issueCount={inboxIssues?.length ?? 0} + onAccept={() => { + inboxServices + .markInboxStatus( + workspaceSlug!.toString(), + projectId!.toString(), + inboxId!.toString(), + inboxIssues?.find((inboxIssue) => inboxIssue.issue === issueId)?.id!, + { + status: 1, + } + ) + .then(() => { + mutate(ISSUE_DETAILS(issueId as string), undefined); + }); + }} + onDecline={() => { + inboxServices + .markInboxStatus( + workspaceSlug!.toString(), + projectId!.toString(), + inboxId!.toString(), + inboxIssues?.find((inboxIssue) => inboxIssue.issue === issueId)?.id!, + { + status: -1, + } + ) + .then(() => { + reset(defaultValues); + mutate(ISSUE_DETAILS(issueId as string), undefined); + }); + }} + onMarkAsDuplicate={() => { + setSelectDuplicateIssue(true); + }} + onSnooze={(date) => { + inboxServices + .markInboxStatus( + workspaceSlug!.toString(), + projectId!.toString(), + inboxId!.toString(), + inboxIssues?.find((inboxIssue) => inboxIssue.issue === issueId)?.id!, + { + status: 0, + snoozed_till: new Date(date), + } + ) + .then(() => { + reset(defaultValues); + mutate(ISSUE_DETAILS(issueId as string), undefined); + }); + }} + /> + +
+ - -
-
-
- - -
- {(inboxIssues?.findIndex((issue) => issue.issue === issueId) ?? 0) + 1} of{" "} - {inboxIssues?.length} -
-
-
- { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.issue === issueId)?.id!, - { - status: 1, - } - ) - .then(() => { - mutateIssueDetails(); - }); - }} - > - Accept - - { - setSelectDuplicateIssue(true); - }} - > - Mark as duplicate - - { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.issue === issueId)?.id!, - { - status: -1, - } - ) - .then(() => { - mutateIssueDetails(); - }); - }} - > - Decline - - { - setSelectSnoozeModalOpen(true); - }} - > - Snooze - -
-
- -
- {!issueDetails && !issueDetailError && ( -
- -
- )} - - {issueDetails && ( -
-
-
- {issueDetails?.parent && issueDetails.parent !== "" ? ( -
- - - - - {issueDetails.project_detail.identifier}- - {issueDetails.parent_detail?.sequence_id} - - - {issueDetails.parent_detail?.name.substring(0, 50)} - - - - - - {siblingIssues && siblingIssues.length > 0 ? ( - siblingIssues.map((issue: IIssue) => ( - - - - {issueDetails.project_detail.identifier}-{issue.sequence_id} - - - - )) - ) : ( - - No other sibling issues - - )} - -
- ) : null} - -
- -
- -
-

Attachments

-
- - -
-
-
-

Comments/Activity

- - -
-
-
- -
- -
-
- )} - - {!issueDetails && issueDetailError && ( -
- { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.issue === issueId)?.id!, - { - status: 1, - } - ) - .then(() => { - mutateIssueDetails(); - }); - }} - > - Accept Inbox Issue - -

You have to accept the issue to view it.

-
- )} +
+ { + inboxServices + .markInboxStatus( + workspaceSlug!.toString(), + projectId!.toString(), + inboxId!.toString(), + inboxIssues?.find((inboxIssue) => inboxIssue.issue === issueId)?.id!, + { + status: 1, + } + ) + .then(() => { + reset(defaultValues); + mutate(ISSUE_DETAILS(issueId as string), undefined); + }); + }} + />
@@ -523,39 +280,15 @@ const ProjectIssues: NextPage = () => { } ) .then(() => { - mutateIssueDetails(); + reset(defaultValues); setSelectDuplicateIssue(false); + mutate(ISSUE_DETAILS(issueId as string), undefined); }) .catch(() => { setSelectDuplicateIssue(false); }); }} /> - - setSelectSnoozeModalOpen(false)} - onSubmit={(snoozeTill) => { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.issue === issueId)?.id!, - { - status: 0, - snoozed_till: new Date(snoozeTill), - } - ) - .then(() => { - mutateIssueDetails(); - setSelectSnoozeModalOpen(false); - }) - .catch(() => { - setSelectSnoozeModalOpen(false); - }); - }} - /> ); diff --git a/apps/app/public/empty-state/empty-inbox.svg b/apps/app/public/empty-state/empty-inbox.svg new file mode 100644 index 00000000000..39d7ca5e269 --- /dev/null +++ b/apps/app/public/empty-state/empty-inbox.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/app/types/inbox.d.ts b/apps/app/types/inbox.d.ts index 0c936904799..b3435a67530 100644 --- a/apps/app/types/inbox.d.ts +++ b/apps/app/types/inbox.d.ts @@ -4,14 +4,14 @@ export interface IInboxIssue { created_at: Date; updated_at: Date; status: number; - snoozed_till: null; + snoozed_till: Date | null; created_by: string; updated_by: string; project: string; workspace: string; inbox: string; issue: string; - duplicate_to: null; + duplicate_to: string | null; } export interface IInboxIssue { From 7a97b612cad23def0913ba2a36077dd021fbe207 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 17 May 2023 12:06:40 +0530 Subject: [PATCH 10/51] dev: default inbox for project and pending issues count --- apiserver/plane/api/serializers/inbox.py | 5 ++++- apiserver/plane/api/views/inbox.py | 7 ++++++- apiserver/plane/api/views/project.py | 3 +++ apiserver/plane/db/models/inbox.py | 4 ++-- apiserver/plane/db/models/project.py | 1 + 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/api/serializers/inbox.py b/apiserver/plane/api/serializers/inbox.py index 9e7c9e197f1..e6100c79d38 100644 --- a/apiserver/plane/api/serializers/inbox.py +++ b/apiserver/plane/api/serializers/inbox.py @@ -1,3 +1,6 @@ +# Third party frameworks +from rest_framework import serializers + # Module imports from .base import BaseSerializer from .issue import IssueFlatSerializer @@ -7,7 +10,7 @@ class InboxSerializer(BaseSerializer): project_detail = ProjectLiteSerializer(source="project", read_only=True) - + pending_issue_count = serializers.IntegerField(read_only=True) class Meta: model = Inbox fields = "__all__" diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index 6439a1eed0c..c38731117b2 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -1,6 +1,6 @@ # Django import from django.utils import timezone -from django.db.models import Q +from django.db.models import Q, Count # Third party imports from rest_framework import status @@ -33,6 +33,11 @@ def get_queryset(self): .filter( workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"), + ).annotate( + pending_issue_count=Count( + "issue_inbox", + filter=Q(issue_inbox__status=-2), + ) ) .select_related("workspace", "project") ) diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 3851e31c145..7b6804915ad 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -46,6 +46,7 @@ ProjectMemberInvite, User, ProjectIdentifier, + Inbox, ) from plane.bgtasks.project_invitation_task import project_invitation @@ -214,6 +215,8 @@ def partial_update(self, request, slug, pk=None): ) if serializer.is_valid(): + if serializer.data["inbox_view"]: + Inbox.objects.get_or_create(name=f"{project.name} Inbox", project=project, is_default=True) serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apiserver/plane/db/models/inbox.py b/apiserver/plane/db/models/inbox.py index 18ab11ac97a..d106e4f650d 100644 --- a/apiserver/plane/db/models/inbox.py +++ b/apiserver/plane/db/models/inbox.py @@ -30,8 +30,8 @@ class InboxIssue(ProjectBaseModel): "db.Issue", related_name="issue_inbox", on_delete=models.CASCADE ) status = models.IntegerField( - choices=((-1, "Rejected"), (0, "Snoozed"), (1, "Accepted"), (2, "Duplicate")), - default=0, + choices=((-2, "Pending"), (-1, "Rejected"), (0, "Snoozed"), (1, "Accepted"), (2, "Duplicate")), + default=-2, ) snoozed_till = models.DateTimeField(null=True) duplicate_to = models.ForeignKey( diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index 04435cadf2c..cd5f6e8723f 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -68,6 +68,7 @@ class Project(BaseModel): cycle_view = models.BooleanField(default=True) issue_views_view = models.BooleanField(default=True) page_view = models.BooleanField(default=True) + inbox_view = models.BooleanField(default=False) cover_image = models.URLField(blank=True, null=True, max_length=800) estimate = models.ForeignKey( "db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True From 29604696abcb0dc0eb6be90c5f0386f0edbc08c1 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 17 May 2023 12:17:22 +0530 Subject: [PATCH 11/51] dev: fix exception when creating default inbox --- apiserver/plane/api/views/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 7b6804915ad..14d2d51da67 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -215,9 +215,9 @@ def partial_update(self, request, slug, pk=None): ) if serializer.is_valid(): + serializer.save() if serializer.data["inbox_view"]: Inbox.objects.get_or_create(name=f"{project.name} Inbox", project=project, is_default=True) - serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) From 286566e9cad7dc05ea04aaac1b91f3046f8ae293 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain Date: Wed, 17 May 2023 17:06:44 +0530 Subject: [PATCH 12/51] fix: empty state for inbox --- .../projects/[projectId]/inbox/[inboxId].tsx | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index 85b55e9a148..6cc6aaf582a 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -142,6 +142,42 @@ const ProjectIssues: NextPage = () => { }); }, [inboxIssues, workspaceSlug, projectId, inboxId]); + if (inboxIssues && inboxIssues.length === 0) + return ( + + + + + + } + right={ +
+ { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + + Add Issue + +
+ } + > +
+

+ No inbox issues found in this inbox. +

+
+
+
+ ); + return ( Date: Wed, 17 May 2023 17:33:28 +0530 Subject: [PATCH 13/51] dev: auto issue state updation when rejected or marked duplicate --- apiserver/plane/api/views/inbox.py | 41 ++++++++++++++++++++++++++++-- apiserver/plane/db/models/issue.py | 5 +++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index c38731117b2..2c8cefbdd3b 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -10,7 +10,7 @@ # Module imports from .base import BaseViewSet from plane.api.permissions import ProjectBasePermission -from plane.db.models import Project, Inbox, InboxIssue +from plane.db.models import Project, Inbox, InboxIssue, Issue, State from plane.api.serializers import ( InboxSerializer, InboxIssueSerializer, @@ -33,7 +33,8 @@ def get_queryset(self): .filter( workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"), - ).annotate( + ) + .annotate( pending_issue_count=Count( "issue_inbox", filter=Q(issue_inbox__status=-2), @@ -118,3 +119,39 @@ def create(self, request, slug, project_id, inbox_id): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + def partial_update(self, request, slug, project_id, inbox_id, pk): + try: + inbox_issue = InboxIssue.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + ) + serializer = InboxIssueSerializer( + inbox_issue, data=request.data, partial=True + ) + + if serializer.is_valid(): + serializer.save() + # Update the issue state if the issue is rejected or marked as duplicate + if serializer.data["status"] in [-1, 2]: + issue = Issue.objects.get( + pk=inbox_issue.issue_id, + workspace__slug=slug, + project_id=project_id, + ) + state = State.objects.filter(group="cancelled").first() + if state is not None: + issue.state = state + issue.save() + return Response(serializer.data, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except InboxIssue.DoesNotExist: + return Response( + {"error": "Inbox Issue does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index e27d83540bd..d72831be74c 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -23,7 +23,10 @@ def get_queryset(self): super() .get_queryset() .filter( - models.Q(issue_inbox__status=1) | models.Q(issue_inbox__isnull=True) + models.Q(issue_inbox__status=1) + | models.Q(issue_inbox__status=-1) + | models.Q(issue_inbox__status=2) + | models.Q(issue_inbox__isnull=True) ) ) From f33fb1a8894ef120980cb123f3b6869817640ca3 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 22 May 2023 19:21:57 +0530 Subject: [PATCH 14/51] fix: inbox update status --- apiserver/plane/api/views/inbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index 2c8cefbdd3b..72de01cc552 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -142,7 +142,7 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): if state is not None: issue.state = state issue.save() - return Response(serializer.data, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except InboxIssue.DoesNotExist: return Response( From ff700d1159c7d228bd1696f9152c7df3adc09059 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain Date: Tue, 23 May 2023 17:37:54 +0530 Subject: [PATCH 15/51] fix: hydrating chose with old values filters workflow --- .../components/inbox/inbox-action-headers.tsx | 76 +++++++++++++++++-- .../app/components/inbox/inbox-issue-card.tsx | 6 +- .../app/components/inbox/select-duplicate.tsx | 26 ++++--- .../projects/[projectId]/inbox/[inboxId].tsx | 51 +++++++++++-- .../projects/[projectId]/issues/index.tsx | 4 +- 5 files changed, 139 insertions(+), 24 deletions(-) diff --git a/apps/app/components/inbox/inbox-action-headers.tsx b/apps/app/components/inbox/inbox-action-headers.tsx index 5da34ca95aa..6e2a7476f66 100644 --- a/apps/app/components/inbox/inbox-action-headers.tsx +++ b/apps/app/components/inbox/inbox-action-headers.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; // icons import { InboxIcon, StackedLayersHorizontalIcon } from "components/icons"; @@ -17,11 +17,17 @@ import { Popover } from "@headlessui/react"; import DatePicker from "react-datepicker"; // components -import { PrimaryButton, SecondaryButton } from "components/ui"; +import { PrimaryButton, SecondaryButton, MultiLevelDropdown } from "components/ui"; + +// types +import type { IInboxIssue } from "types"; type Props = { issueCount: number; currentIssueIndex: number; + filter: any; + setFilter: (value: any) => void; + inboxIssue?: IInboxIssue; onAccept: () => void; onDecline: () => void; onMarkAsDuplicate: () => void; @@ -29,15 +35,73 @@ type Props = { }; export const InboxActionHeader: React.FC = (props) => { - const { issueCount, currentIssueIndex, onAccept, onDecline, onMarkAsDuplicate, onSnooze } = props; + const { + issueCount, + currentIssueIndex, + onAccept, + onDecline, + onMarkAsDuplicate, + onSnooze, + filter, + setFilter, + inboxIssue, + } = props; const [date, setDate] = useState(new Date()); + useEffect(() => { + if (!inboxIssue?.snoozed_till) return; + setDate(new Date(inboxIssue.snoozed_till)); + }, [inboxIssue]); + return (
-
- -

Inbox

+
+
+ +

Inbox

+
+
+ { + setFilter(value); + }} + direction="left" + options={[ + { + id: "all", + label: "All", + value: "all", + selected: filter === "all", + }, + { + id: "snooze", + label: "Snooze", + value: "snooze", + selected: filter === "snooze", + }, + { + id: "mark_as_duplicate", + label: "Duplicate", + value: "duplicate", + selected: filter === "duplicate", + }, + { + id: "accepted", + label: "Accepted", + value: "accepted", + selected: filter === "accepted", + }, + { + id: "declined", + label: "Declined", + value: "declined", + selected: filter === "declined", + }, + ]} + /> +
diff --git a/apps/app/components/inbox/inbox-issue-card.tsx b/apps/app/components/inbox/inbox-issue-card.tsx index 9669894fbde..c862a388cea 100644 --- a/apps/app/components/inbox/inbox-issue-card.tsx +++ b/apps/app/components/inbox/inbox-issue-card.tsx @@ -21,8 +21,10 @@ export const InboxIssueCard: React.FC = (props) => { }`} >
-

- {issue.project_detail.identifier}-{issue.issue_detail.sequence_id} +

+ + {issue.project_detail.identifier}-{issue.issue_detail.sequence_id} +

{issue.issue_detail.name}

diff --git a/apps/app/components/inbox/select-duplicate.tsx b/apps/app/components/inbox/select-duplicate.tsx index c150ae19db3..fdf7034df95 100644 --- a/apps/app/components/inbox/select-duplicate.tsx +++ b/apps/app/components/inbox/select-duplicate.tsx @@ -1,6 +1,5 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; -import Link from "next/link"; import { useRouter } from "next/router"; import useSWR from "swr"; @@ -16,21 +15,20 @@ import issuesServices from "services/issues.service"; // ui import { PrimaryButton, SecondaryButton } from "components/ui"; // icons -import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline"; -import { BlockerIcon, LayerDiagonalIcon } from "components/icons"; -// types -import { IIssue, UserAuth } from "types"; +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { LayerDiagonalIcon } from "components/icons"; // fetch-keys import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; type Props = { isOpen: boolean; + value?: string | null; onClose: () => void; onSubmit: (issueId: string) => void; }; export const SelectDuplicateInboxIssueModal: React.FC = (props) => { - const { isOpen, onClose, onSubmit } = props; + const { isOpen, onClose, onSubmit, value } = props; const [query, setQuery] = useState(""); const [selectedItem, setSelectedItem] = useState(""); @@ -38,17 +36,27 @@ export const SelectDuplicateInboxIssueModal: React.FC = (props) => { const { setToastAlert } = useToast(); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, issueId } = router.query; const { data: issues } = useSWR( workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null, workspaceSlug && projectId - ? () => issuesServices.getIssues(workspaceSlug as string, projectId as string) + ? () => + issuesServices + .getIssues(workspaceSlug as string, projectId as string) + .then((res) => res.filter((issue) => issue.id !== issueId)) : null ); + useEffect(() => { + if (!value) { + setSelectedItem(""); + return; + } else setSelectedItem(value); + }, [value]); + const handleClose = () => { onClose(); }; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index 6cc6aaf582a..a027cea2336 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -24,7 +24,7 @@ import { // helper import { truncateText } from "helpers/string.helper"; // ui -import { PrimaryButton } from "components/ui"; +import { PrimaryButton, Spinner } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons import { PlusIcon } from "@heroicons/react/24/outline"; @@ -57,13 +57,18 @@ const ProjectIssues: NextPage = () => { const { workspaceSlug, projectId, inboxId, issueId } = router.query; + const [filters, setFilters] = useState("all"); const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false); const { reset, control, watch } = useForm({ defaultValues, }); - const { data: inboxIssues, mutate: inboxIssuesMutate } = useSWR( + const { + data: inboxIssues, + mutate: inboxIssuesMutate, + error: inboxIssuesError, + } = useSWR( workspaceSlug && projectId && inboxId ? INBOX_ISSUES(inboxId.toString()) : null, workspaceSlug && projectId && inboxId ? () => @@ -84,9 +89,7 @@ const ProjectIssues: NextPage = () => { const onKeyDown = useCallback( (e: KeyboardEvent) => { - if (!inboxIssues) return; - - if (!issueId) return; + if (!inboxIssues || !issueId) return; const currentIssueIndex = inboxIssues.findIndex((issue) => issue.issue === issueId); @@ -142,6 +145,40 @@ const ProjectIssues: NextPage = () => { }); }, [inboxIssues, workspaceSlug, projectId, inboxId]); + if (!inboxIssues && !inboxIssuesError) + return ( + + + + + + } + right={ +
+ { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + + Add Issue + +
+ } + > +
+ +
+
+
+ ); + if (inboxIssues && inboxIssues.length === 0) return ( @@ -206,6 +243,9 @@ const ProjectIssues: NextPage = () => { >
issue.issue === issueId)} currentIssueIndex={inboxIssues?.findIndex((issue) => issue.issue === issueId) ?? 0} issueCount={inboxIssues?.length ?? 0} onAccept={() => { @@ -327,6 +367,7 @@ const ProjectIssues: NextPage = () => { setSelectDuplicateIssue(false)} + value={inboxIssues?.find((inboxIssue) => inboxIssue.issue === issueId)?.duplicate_to} onSubmit={(dupIssueId: string) => { inboxServices .markInboxStatus( diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx index a832cd98375..61323813a15 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx @@ -68,7 +68,7 @@ const ProjectIssues: NextPage = () => { > Analytics - {projectDetails?.inbox_view && ( + {projectDetails && projectDetails.inbox_view && ( { outline > Inbox - {inboxList?.[0]?.pending_issue_count !== 0 && ( + {inboxList && inboxList?.[0]?.pending_issue_count !== 0 && ( {inboxList?.[0]?.pending_issue_count} From 16dbd950a10048c4fc2540f14696e48aa93ff907 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Tue, 23 May 2023 18:54:17 +0530 Subject: [PATCH 16/51] feat: inbox issue filtering --- apiserver/plane/api/views/inbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index 72de01cc552..cf6a48df207 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -76,7 +76,7 @@ class InboxIssueViewSet(BaseViewSet): model = InboxIssue def get_queryset(self): - return ( + return self.filter_queryset( super() .get_queryset() .filter( From 6d95ce30c2dd03c708f7e24b74fc3b95ca988d6c Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Tue, 23 May 2023 19:24:51 +0530 Subject: [PATCH 17/51] fix: issue inbox filtering --- apiserver/plane/api/views/inbox.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index cf6a48df207..b3bb0ca3fde 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -75,6 +75,8 @@ class InboxIssueViewSet(BaseViewSet): serializer_class = InboxIssueSerializer model = InboxIssue + filterset_fields = ["status",] + def get_queryset(self): return self.filter_queryset( super() From d1ed92529bf66d03fe3958cfc2ea588c2ba267f3 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain Date: Wed, 24 May 2023 11:42:04 +0530 Subject: [PATCH 18/51] feat: filter inbox issues --- .../components/inbox/inbox-action-headers.tsx | 24 ++++++++++--------- apps/app/constants/fetch-keys.ts | 3 ++- .../projects/[projectId]/inbox/[inboxId].tsx | 7 +++--- apps/app/services/inbox.service.ts | 6 +++-- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/apps/app/components/inbox/inbox-action-headers.tsx b/apps/app/components/inbox/inbox-action-headers.tsx index 6e2a7476f66..112d44e75d7 100644 --- a/apps/app/components/inbox/inbox-action-headers.tsx +++ b/apps/app/components/inbox/inbox-action-headers.tsx @@ -65,39 +65,41 @@ export const InboxActionHeader: React.FC = (props) => { { - setFilter(value); + setFilter({ + status: value, + }); }} direction="left" options={[ { id: "all", label: "All", - value: "all", - selected: filter === "all", + value: null, + selected: filter === null, }, { id: "snooze", label: "Snooze", - value: "snooze", - selected: filter === "snooze", + value: 0, + selected: filter === 0, }, { id: "mark_as_duplicate", label: "Duplicate", - value: "duplicate", - selected: filter === "duplicate", + value: 2, + selected: filter === 2, }, { id: "accepted", label: "Accepted", - value: "accepted", - selected: filter === "accepted", + value: 1, + selected: filter === 1, }, { id: "declined", label: "Declined", - value: "declined", - selected: filter === "declined", + value: -1, + selected: filter === -1, }, ]} /> diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 40b2c33cdca..f8fb19ce96c 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -122,7 +122,8 @@ export const VIEW_ISSUES = (viewId: string, params: any) => { }; export const INBOX_LIST = (projectId: string) => `VIEWS_LIST_${projectId.toUpperCase()}`; -export const INBOX_ISSUES = (inboxId: string) => `VIEW_ISSUES_${inboxId.toUpperCase()}`; +export const INBOX_ISSUES = (inboxId: string, params?: any) => + `VIEW_ISSUES_${inboxId.toUpperCase()}_${JSON.stringify(params)}`; export const INBOX_DETAILS = (inboxId: string) => `VIEW_DETAILS_${inboxId.toUpperCase()}`; // Issues diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index a027cea2336..7eea852e21e 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -57,7 +57,7 @@ const ProjectIssues: NextPage = () => { const { workspaceSlug, projectId, inboxId, issueId } = router.query; - const [filters, setFilters] = useState("all"); + const [filters, setFilters] = useState(null); const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false); const { reset, control, watch } = useForm({ @@ -69,13 +69,14 @@ const ProjectIssues: NextPage = () => { mutate: inboxIssuesMutate, error: inboxIssuesError, } = useSWR( - workspaceSlug && projectId && inboxId ? INBOX_ISSUES(inboxId.toString()) : null, + workspaceSlug && projectId && inboxId ? INBOX_ISSUES(inboxId.toString(), filters) : null, workspaceSlug && projectId && inboxId ? () => inboxServices.getInboxIssues( workspaceSlug as string, projectId as string, - inboxId as string + inboxId as string, + filters ) : null ); diff --git a/apps/app/services/inbox.service.ts b/apps/app/services/inbox.service.ts index 0198f153b41..1247375f0ee 100644 --- a/apps/app/services/inbox.service.ts +++ b/apps/app/services/inbox.service.ts @@ -33,10 +33,12 @@ class InboxServices extends APIService { async getInboxIssues( workspaceSlug: string, projectId: string, - inboxId: string + inboxId: string, + params?: any ): Promise { return this.get( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues` + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues`, + { params } ) .then((response) => response?.data) .catch((error) => { From e437f1d7560138dca01dce54e5995d73a11cfa09 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain Date: Wed, 7 Jun 2023 18:55:29 +0530 Subject: [PATCH 19/51] refactor: analytics, border colors --- .../components/inbox/inbox-action-headers.tsx | 6 ++-- .../app/components/inbox/inbox-issue-card.tsx | 2 +- .../components/inbox/inbox-main-content.tsx | 6 ++-- apps/app/components/issues/modal.tsx | 12 +++----- .../projects/[projectId]/inbox/[inboxId].tsx | 4 +-- .../[projectId]/settings/features.tsx | 11 ++++--- apps/app/services/track-event.service.ts | 29 ++++++++++--------- 7 files changed, 37 insertions(+), 33 deletions(-) diff --git a/apps/app/components/inbox/inbox-action-headers.tsx b/apps/app/components/inbox/inbox-action-headers.tsx index 112d44e75d7..3334ef4d57d 100644 --- a/apps/app/components/inbox/inbox-action-headers.tsx +++ b/apps/app/components/inbox/inbox-action-headers.tsx @@ -55,7 +55,7 @@ export const InboxActionHeader: React.FC = (props) => { }, [inboxIssue]); return ( -
+
@@ -110,7 +110,7 @@ export const InboxActionHeader: React.FC = (props) => {
-
+
{currentIssueIndex + 1}/{issueCount}
-
- - - - - Snooze - - - - {({ close }) => ( -
- { - if (!val) return; - setDate(val); - }} - dateFormat="dd-MM-yyyy" - inline - /> - { - close(); - onSnooze(date); - }} - > - Snooze - -
- )} -
-
- - - Mark as duplicate - - - - Accept - - - - Decline - -
+ {isAllowed && ( +
+ + + + + Snooze + + + + {({ close }) => ( +
+ { + if (!val) return; + setDate(val); + }} + dateFormat="dd-MM-yyyy" + inline + /> + { + close(); + onSnooze(date); + }} + > + Snooze + +
+ )} +
+
+ + + Mark as duplicate + + + + Accept + + + + Decline + +
+ )}
); diff --git a/apps/app/components/inbox/inbox-issue-card.tsx b/apps/app/components/inbox/inbox-issue-card.tsx index a6e74437af7..d7f33bc3265 100644 --- a/apps/app/components/inbox/inbox-issue-card.tsx +++ b/apps/app/components/inbox/inbox-issue-card.tsx @@ -21,23 +21,35 @@ export const InboxIssueCard: React.FC = (props) => { }`} >
-

- - {issue.project_detail.identifier}-{issue.issue_detail.sequence_id} - +

+ {issue.project_detail.identifier}-{issue.sequence_id}

-

{issue.issue_detail.name}

+
{issue.name}
- {getPriorityIcon( - issue.issue_detail.priority ?? "None", - "text-sm rounded bg-orange-200 bg-opacity-50 w-6 h-6 flex justify-center items-center text-orange-600" - )} - {issue.snoozed_till && ( +
+ {getPriorityIcon( + issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None", + "text-sm" + )} +
+ {issue.issue_inbox.snoozed_till && (

- {new Date(issue.snoozed_till).toLocaleDateString("en-US", { + {new Date(issue.issue_inbox.snoozed_till).toLocaleDateString("en-US", { month: "short", day: "numeric", })} diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx index fa49527c2e2..072b9511f72 100644 --- a/apps/app/components/inbox/inbox-main-content.tsx +++ b/apps/app/components/inbox/inbox-main-content.tsx @@ -1,7 +1,5 @@ import { useCallback, useEffect } from "react"; -import Link from "next/link"; -import Image from "next/image"; import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; @@ -10,36 +8,29 @@ import useSWR, { mutate } from "swr"; import { Control, UseFormReset, UseFormWatch } from "react-hook-form"; // services import issuesService from "services/issues.service"; +import inboxServices from "services/inbox.service"; // ui -import { CustomMenu, PrimaryButton, Spinner } from "components/ui"; +import { Spinner } from "components/ui"; // hooks import useUser from "hooks/use-user"; // fetch-keys import { INBOX_ISSUES, + INBOX_ISSUE_DETAILS, ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, - SUB_ISSUES, } from "constants/fetch-keys"; -import { - AddComment, - IssueActivitySection, - IssueAttachmentUpload, - IssueAttachments, - IssueDescriptionForm, - IssueDetailsSidebar, - SubIssuesList, -} from "components/issues"; +import { IssueDescriptionForm, IssueDetailsSidebar } from "components/issues"; // types -import type { IIssue } from "types"; +import type { IInboxIssue, IIssue } from "types"; type Props = { onAccept: () => void; - control: Control; - watch: UseFormWatch; - reset: UseFormReset; + control: Control; + watch: UseFormWatch; + reset: UseFormReset; status?: -2 | -1 | 0 | 1 | 2; }; @@ -47,7 +38,7 @@ export const InboxMainContent: React.FC = (props) => { const { onAccept, watch, control, reset, status } = props; const router = useRouter(); - const { workspaceSlug, projectId, issueId, inboxId } = router.query; + const { workspaceSlug, projectId, inboxId, issueId } = router.query; const { user } = useUser(); @@ -55,26 +46,17 @@ export const InboxMainContent: React.FC = (props) => { data: issueDetails, mutate: mutateIssueDetails, error: issueDetailError, - } = useSWR( - workspaceSlug && projectId && issueId && status && status === 1 - ? ISSUE_DETAILS(issueId as string) + } = useSWR( + workspaceSlug && projectId && inboxId && issueId + ? INBOX_ISSUE_DETAILS(inboxId.toString(), issueId.toString()) : null, - workspaceSlug && projectId && issueId && status && status === 1 + workspaceSlug && projectId && inboxId && issueId ? () => - issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string) - : null - ); - - const { data: siblingIssues } = useSWR( - workspaceSlug && projectId && issueDetails?.parent && status === 1 - ? SUB_ISSUES(issueDetails.parent) - : null, - workspaceSlug && projectId && issueDetails?.parent && status === 1 - ? () => - issuesService.subIssues( - workspaceSlug as string, - projectId as string, - issueDetails.parent ?? "" + inboxServices.getInboxIssueById( + workspaceSlug.toString(), + projectId.toString(), + inboxId.toString(), + issueId.toString() ) : null ); @@ -82,19 +64,8 @@ export const InboxMainContent: React.FC = (props) => { useEffect(() => { if (!issueDetails || !issueId) return; - mutate(PROJECT_ISSUES_ACTIVITY(issueId.toString())); reset({ - ...issueDetails, - blockers_list: - issueDetails.blockers_list ?? - issueDetails.blocker_issues?.map((issue) => issue.blocker_issue_detail?.id), - blocked_list: - issueDetails.blocks_list ?? - issueDetails.blocked_issues?.map((issue) => issue.blocked_issue_detail?.id), - assignees_list: - issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id), - labels_list: issueDetails.labels_list ?? issueDetails.labels, - labels: issueDetails.labels_list ?? issueDetails.labels, + ...issueDetails.issue_detail, }); }, [issueDetails, reset, issueId]); @@ -102,12 +73,16 @@ export const InboxMainContent: React.FC = (props) => { async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueId || !inboxId) return; - mutate( - ISSUE_DETAILS(issueId.toString()), - (prevData) => ({ - ...prevData, - ...formData, - }), + mutate( + INBOX_ISSUE_DETAILS(inboxId.toString(), issueId.toString()), + (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + ...formData, + }; + }, false ); @@ -132,24 +107,6 @@ export const InboxMainContent: React.FC = (props) => { [workspaceSlug, issueId, projectId, mutateIssueDetails, inboxId, user] ); - if (status !== undefined && status !== 1) - return ( -

-
- Empty Inbox -
-
-

- You haven{"'"}t accepted Inbox issue. -

-

- You have to accept the issue to view, add files, change states and more. -

- Accept Inbox Issue -
-
- ); - if (!issueDetails && !issueDetailError) return (
@@ -162,81 +119,25 @@ export const InboxMainContent: React.FC = (props) => { ); diff --git a/apps/app/components/inbox/index.ts b/apps/app/components/inbox/index.ts index cd0f30e19ec..833e06f8c93 100644 --- a/apps/app/components/inbox/index.ts +++ b/apps/app/components/inbox/index.ts @@ -1,3 +1,4 @@ +export * from "./filters-dropdown"; export * from "./select-duplicate"; export * from "./inbox-issue-card"; export * from "./inbox-action-headers"; diff --git a/apps/app/components/issues/description-form.tsx b/apps/app/components/issues/description-form.tsx index 2a61e85a633..c64e5c02a54 100644 --- a/apps/app/components/issues/description-form.tsx +++ b/apps/app/components/issues/description-form.tsx @@ -28,7 +28,11 @@ export interface IssueDescriptionFormValues { } export interface IssueDetailsProps { - issue: IIssue; + issue: { + name: string; + description: string; + description_html: string; + }; handleFormSubmit: (value: IssueDescriptionFormValues) => Promise; } diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx index eb3130adee8..7deed4d2b95 100644 --- a/apps/app/components/issues/modal.tsx +++ b/apps/app/components/issues/modal.tsx @@ -142,6 +142,42 @@ export const CreateUpdateIssueModal: React.FC = ({ }); }; + const addIssueToInbox = async (payload: Partial) => { + if (!workspaceSlug || !projectId || !inboxId) return; + + await inboxServices + .createInboxIssue( + workspaceSlug.toString(), + projectId.toString(), + inboxId.toString(), + { + issue: { ...payload, source: "web" }, + }, + user + ) + .then((res) => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue created successfully.", + }); + + mutate(calendarFetchKey); + + if (!createMore) handleClose(); + + if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be created. Please try again.", + }); + mutate(INBOX_ISSUES(inboxId.toString())); + }); + }; + const calendarFetchKey = cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams) : moduleId @@ -160,34 +196,9 @@ export const CreateUpdateIssueModal: React.FC = ({ const createIssue = async (payload: Partial) => { if (!workspaceSlug || !projectId) return; - if (inboxId) { - await inboxServices - .createInboxIssue(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), { - issue: payload, - }) - .then((res) => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - - mutate(calendarFetchKey); - - if (!createMore) handleClose(); - if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Issue could not be created. Please try again.", - }); - mutate(INBOX_ISSUES(inboxId.toString())); - }); - } else { - if (!workspaceSlug) return; + if (inboxId) await addIssueToInbox(payload); + else await issuesService .createIssues(workspaceSlug as string, activeProject ?? "", payload, user) .then(async (res) => { @@ -218,7 +229,6 @@ export const CreateUpdateIssueModal: React.FC = ({ message: "Issue could not be created. Please try again.", }); }); - } }; const updateIssue = async (payload: Partial) => { diff --git a/apps/app/components/views/select-filters.tsx b/apps/app/components/views/select-filters.tsx index 1b698634690..3351be66714 100644 --- a/apps/app/components/views/select-filters.tsx +++ b/apps/app/components/views/select-filters.tsx @@ -20,7 +20,7 @@ import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fe import { PRIORITIES } from "constants/project"; type Props = { - filters: IIssueFilterOptions | IQuery; + filters: Partial | IQuery; onSelect: (option: any) => void; direction?: "left" | "right"; height?: "sm" | "md" | "rg" | "lg"; diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 490cfbbad9e..5625b710db7 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -23,6 +23,19 @@ const paramsToKey = (params: any) => { return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}`; }; +const inboxParamsToKey = (params: any) => { + const { priority, inbox_status } = params; + + let priorityKey = priority ? priority.split(",") : []; + let inboxStatusKey = inbox_status ? inbox_status.split(",") : []; + + // sorting each keys in ascending order + priorityKey = priorityKey.sort().join("_"); + inboxStatusKey = inboxStatusKey.sort().join("_"); + + return `${priorityKey}_${inboxStatusKey}`; +}; + export const CURRENT_USER = "CURRENT_USER"; export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS"; export const USER_WORKSPACES = "USER_WORKSPACES"; @@ -124,10 +137,18 @@ export const VIEW_ISSUES = (viewId: string, params: any) => { return `VIEW_ISSUES_${viewId.toUpperCase()}_${paramsKey.toUpperCase()}`; }; -export const INBOX_LIST = (projectId: string) => `VIEWS_LIST_${projectId.toUpperCase()}`; -export const INBOX_ISSUES = (inboxId: string, params?: any) => - `VIEW_ISSUES_${inboxId.toUpperCase()}_${JSON.stringify(params)}`; -export const INBOX_DETAILS = (inboxId: string) => `VIEW_DETAILS_${inboxId.toUpperCase()}`; +// inbox +export const INBOX_LIST = (projectId: string) => `INBOX_LIST_${projectId.toUpperCase()}`; +export const INBOX_DETAILS = (inboxId: string) => `INBOX_DETAILS_${inboxId.toUpperCase()}`; +export const INBOX_ISSUES = (inboxId: string, params?: any) => { + if (!params) return `INBOX_ISSUES_${inboxId.toUpperCase()}`; + + const paramsKey = inboxParamsToKey(params); + + return `INBOX_ISSUES_${inboxId.toUpperCase()}_${paramsKey.toUpperCase()}`; +}; +export const INBOX_ISSUE_DETAILS = (inboxId: string, issueId: string) => + `INBOX_ISSUE_DETAILS_${inboxId.toUpperCase()}_${issueId.toUpperCase()}`; // Issues export const ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId.toUpperCase()}`; diff --git a/apps/app/constants/inbox.ts b/apps/app/constants/inbox.ts new file mode 100644 index 00000000000..fa2befeb5f5 --- /dev/null +++ b/apps/app/constants/inbox.ts @@ -0,0 +1,7 @@ +export const STATUS: { [key: string]: number } = { + Pending: -2, + Rejected: -1, + Snoozed: 0, + Accepted: 1, + Duplicate: 2, +}; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index a3545673a98..1d8f1304b2e 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -20,6 +20,7 @@ import { InboxActionHeader, InboxMainContent, SelectDuplicateInboxIssueModal, + FiltersDropdown, } from "components/inbox"; // helper import { truncateText } from "helpers/string.helper"; @@ -29,12 +30,11 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons import { PlusIcon } from "@heroicons/react/24/outline"; // types +import type { IInboxFilterOptions, IInboxIssue } from "types"; import type { NextPage } from "next"; // fetch-keys -import { INBOX_ISSUES, ISSUE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys"; - -// types -import type { IIssue } from "types"; +import { INBOX_DETAILS, INBOX_ISSUES, ISSUE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys"; +import useUser from "hooks/use-user"; const defaultValues = { name: "", @@ -53,30 +53,44 @@ const defaultValues = { }; const ProjectIssues: NextPage = () => { - const router = useRouter(); + const [filters, setFilters] = useState>({}); + const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false); + const router = useRouter(); const { workspaceSlug, projectId, inboxId, issueId } = router.query; - const [filters, setFilters] = useState(null); - const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false); + const { user } = useUser(); - const { reset, control, watch } = useForm({ + const { reset, control, watch } = useForm({ defaultValues, }); - const { - data: inboxIssues, - mutate: inboxIssuesMutate, - error: inboxIssuesError, - } = useSWR( - workspaceSlug && projectId && inboxId ? INBOX_ISSUES(inboxId.toString(), filters) : null, + const params = { + priority: filters?.priority ? filters?.priority.join(",") : undefined, + inbox_status: filters?.inbox_status ? filters?.inbox_status.join(",") : undefined, + }; + + const { data: inboxDetails } = useSWR( + workspaceSlug && projectId && inboxId ? INBOX_DETAILS(inboxId.toString()) : null, + workspaceSlug && projectId && inboxId + ? () => + inboxServices.getInboxById( + workspaceSlug.toString(), + projectId.toString(), + inboxId.toString() + ) + : null + ); + + const { data: inboxIssues, mutate: inboxIssuesMutate } = useSWR( + workspaceSlug && projectId && inboxId ? INBOX_ISSUES(inboxId.toString(), params) : null, workspaceSlug && projectId && inboxId ? () => inboxServices.getInboxIssues( workspaceSlug as string, projectId as string, inboxId as string, - filters + params ) : null ); @@ -92,7 +106,7 @@ const ProjectIssues: NextPage = () => { (e: KeyboardEvent) => { if (!inboxIssues || !issueId) return; - const currentIssueIndex = inboxIssues.findIndex((issue) => issue.issue === issueId); + const currentIssueIndex = inboxIssues.findIndex((issue) => issue.id === issueId); switch (e.key) { case "ArrowUp": @@ -101,8 +115,8 @@ const ProjectIssues: NextPage = () => { query: { issueId: currentIssueIndex === 0 - ? inboxIssues[inboxIssues.length - 1].issue - : inboxIssues[currentIssueIndex - 1].issue, + ? inboxIssues[inboxIssues.length - 1].id + : inboxIssues[currentIssueIndex - 1].id, }, }); break; @@ -112,8 +126,8 @@ const ProjectIssues: NextPage = () => { query: { issueId: currentIssueIndex === inboxIssues.length - 1 - ? inboxIssues[0].issue - : inboxIssues[currentIssueIndex + 1].issue, + ? inboxIssues[0].id + : inboxIssues[currentIssueIndex + 1].id, }, }); @@ -141,80 +155,16 @@ const ProjectIssues: NextPage = () => { Router.push({ pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, query: { - issueId: inboxIssues[0].issue, + issueId: inboxIssues[0].id, }, }); }, [inboxIssues, workspaceSlug, projectId, inboxId]); - if (!inboxIssues && !inboxIssuesError) - return ( - - - - - - } - right={ -
- { - const e = new KeyboardEvent("keydown", { key: "c" }); - document.dispatchEvent(e); - }} - > - - Add Issue - -
- } - > -
- -
-
-
- ); + useEffect(() => { + if (!inboxDetails || filters) return; - if (inboxIssues && inboxIssues.length === 0) - return ( - - - - - - } - right={ -
- { - const e = new KeyboardEvent("keydown", { key: "c" }); - document.dispatchEvent(e); - }} - > - - Add Issue - -
- } - > -
-

- No inbox issues found in this inbox. -

-
-
-
- ); + setFilters(inboxDetails.view_props); + }, [inboxDetails, filters]); return ( @@ -223,12 +173,36 @@ const ProjectIssues: NextPage = () => { } right={
+ { + const key = option.key as keyof typeof filters; + + const valueExists = filters[key]?.includes(option.value); + + if (valueExists) { + setFilters({ + ...filters, + [option.key]: ((filters[key] ?? []) as any[])?.filter( + (val) => val !== option.value + ), + }); + } else { + setFilters({ + ...filters, + [option.key]: [...((filters[key] ?? []) as any[]), option.value], + }); + } + }} + direction="left" + height="rg" + /> { @@ -242,162 +216,188 @@ const ProjectIssues: NextPage = () => {
} > -
- issue.issue === issueId)} - currentIssueIndex={inboxIssues?.findIndex((issue) => issue.issue === issueId) ?? 0} - issueCount={inboxIssues?.length ?? 0} - onAccept={() => { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.issue === issueId)?.id!, - { - status: 1, - } - ) - .then(() => { - mutate(ISSUE_DETAILS(issueId as string), undefined); - inboxIssuesMutate((prevData) => - prevData?.map((item) => - item.issue === issueId ? { ...item, status: 1 } : item - ) - ); - }); - }} - onDecline={() => { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.issue === issueId)?.id!, - { - status: -1, - } - ) - .then(() => { - reset(defaultValues); - mutate(ISSUE_DETAILS(issueId as string), undefined); - inboxIssuesMutate((prevData) => - prevData?.map((item) => - item.issue === issueId ? { ...item, status: -1 } : item - ) - ); - }); - }} - onMarkAsDuplicate={() => { - setSelectDuplicateIssue(true); - }} - onSnooze={(date) => { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.issue === issueId)?.id!, - { - status: 0, - snoozed_till: new Date(date), - } - ) - .then(() => { - reset(defaultValues); - mutate(ISSUE_DETAILS(issueId as string), undefined); - inboxIssuesMutate((prevData) => - prevData?.map((item) => - item.issue === issueId - ? { ...item, status: 0, snoozed_till: new Date(date) } - : item - ) - ); - }); - }} - /> - -
-
- {inboxIssues?.map((issue) => ( - - - - - - ))} + {inboxIssues ? ( + inboxIssues.length === 0 ? ( +
+

+ No inbox issues found in this inbox. +

-
- inboxIssue.issue === issueId)?.status} - onAccept={() => { + ) : ( + <> + setSelectDuplicateIssue(false)} + value={ + inboxIssues?.find((inboxIssue) => inboxIssue.id === issueId)?.issue_inbox + .duplicate_to + } + onSubmit={(dupIssueId: string) => { inboxServices .markInboxStatus( workspaceSlug!.toString(), projectId!.toString(), inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.issue === issueId)?.id!, + inboxIssues?.find((inboxIssue) => inboxIssue.id === issueId)?.id!, { - status: 1, - } + status: 2, + duplicate_to: dupIssueId, + }, + user ) .then(() => { reset(defaultValues); + setSelectDuplicateIssue(false); mutate(ISSUE_DETAILS(issueId as string), undefined); inboxIssuesMutate((prevData) => (prevData ?? [])?.map((item) => ({ ...item, - status: item.issue === issueId ? 1 : item.status, + status: item.id === issueId ? 2 : item.issue_inbox.status, + duplicate_to: dupIssueId, })) ); + }) + .catch(() => { + setSelectDuplicateIssue(false); }); }} /> -
-
-
+
+ issue.id === issueId)} + currentIssueIndex={inboxIssues?.findIndex((issue) => issue.id === issueId) ?? 0} + issueCount={inboxIssues?.length ?? 0} + onAccept={() => { + inboxServices + .markInboxStatus( + workspaceSlug!.toString(), + projectId!.toString(), + inboxId!.toString(), + inboxIssues?.find((inboxIssue) => inboxIssue.id === issueId)?.bridge_id!, + { + status: 1, + }, + user + ) + .then(() => { + mutate(ISSUE_DETAILS(issueId as string), undefined); + inboxIssuesMutate((prevData) => + prevData?.map((item) => + item.id === issueId ? { ...item, status: 1 } : item + ) + ); + }); + }} + onDecline={() => { + inboxServices + .markInboxStatus( + workspaceSlug!.toString(), + projectId!.toString(), + inboxId!.toString(), + inboxIssues?.find((inboxIssue) => inboxIssue.id === issueId)?.id!, + { + status: -1, + }, + user + ) + .then(() => { + reset(defaultValues); + mutate(ISSUE_DETAILS(issueId as string), undefined); + inboxIssuesMutate((prevData) => + prevData?.map((item) => + item.id === issueId ? { ...item, status: -1 } : item + ) + ); + }); + }} + onMarkAsDuplicate={() => { + setSelectDuplicateIssue(true); + }} + onSnooze={(date) => { + inboxServices + .markInboxStatus( + workspaceSlug!.toString(), + projectId!.toString(), + inboxId!.toString(), + inboxIssues?.find((inboxIssue) => inboxIssue.id === issueId)?.id!, + { + status: 0, + snoozed_till: new Date(date), + }, + user + ) + .then(() => { + reset(defaultValues); + mutate(ISSUE_DETAILS(issueId as string), undefined); + inboxIssuesMutate((prevData) => + prevData?.map((item) => + item.id === issueId + ? { ...item, status: 0, snoozed_till: new Date(date) } + : item + ) + ); + }); + }} + /> - setSelectDuplicateIssue(false)} - value={inboxIssues?.find((inboxIssue) => inboxIssue.issue === issueId)?.duplicate_to} - onSubmit={(dupIssueId: string) => { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.issue === issueId)?.id!, - { - status: 2, - duplicate_to: dupIssueId, - } - ) - .then(() => { - reset(defaultValues); - setSelectDuplicateIssue(false); - mutate(ISSUE_DETAILS(issueId as string), undefined); - inboxIssuesMutate((prevData) => - (prevData ?? [])?.map((item) => ({ - ...item, - status: item.issue === issueId ? 2 : item.status, - duplicate_to: dupIssueId, - })) - ); - }) - .catch(() => { - setSelectDuplicateIssue(false); - }); - }} - /> +
+
+ {inboxIssues?.map((issue) => ( + + + + + + ))} +
+
+ inboxIssue.id === issueId)?.issue_inbox + .status + } + onAccept={() => { + inboxServices + .markInboxStatus( + workspaceSlug!.toString(), + projectId!.toString(), + inboxId!.toString(), + inboxIssues?.find((inboxIssue) => inboxIssue.id === issueId)?.id!, + { + status: 1, + }, + user + ) + .then(() => { + reset(defaultValues); + mutate(ISSUE_DETAILS(issueId as string), undefined); + inboxIssuesMutate((prevData) => + (prevData ?? [])?.map((item) => ({ + ...item, + status: item.id === issueId ? 1 : item.issue_inbox.status, + })) + ); + }); + }} + /> +
+
+
+ + ) + ) : ( +
+ +
+ )} ); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx index 96df9045d90..e10ce5aa6a6 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx @@ -59,7 +59,7 @@ const featuresList = [ title: "Inbox", description: "Inbox are enabled for all the projects in this workspace. Access it from the issues views page.", - icon: , + icon: , property: "inbox_view", }, ]; diff --git a/apps/app/services/inbox.service.ts b/apps/app/services/inbox.service.ts index 1247375f0ee..3017add6892 100644 --- a/apps/app/services/inbox.service.ts +++ b/apps/app/services/inbox.service.ts @@ -7,7 +7,13 @@ const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; // types -import type { IInboxIssue, IInbox, TInboxStatus } from "types"; +import type { + IInboxIssue, + IInbox, + TInboxStatus, + IInboxIssueDetail, + ICurrentUserResponse, +} from "types"; class InboxServices extends APIService { constructor() { @@ -23,7 +29,7 @@ class InboxServices extends APIService { } async getInboxById(workspaceSlug: string, projectId: string, inboxId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}`) + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -37,7 +43,7 @@ class InboxServices extends APIService { params?: any ): Promise { return this.get( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues`, + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`, { params } ) .then((response) => response?.data) @@ -51,9 +57,9 @@ class InboxServices extends APIService { projectId: string, inboxId: string, inboxIssueId: string - ): Promise { + ): Promise { return this.get( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}` + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/` ) .then((response) => response?.data) .catch((error) => { @@ -66,7 +72,8 @@ class InboxServices extends APIService { projectId: string, inboxId: string, inboxIssueId: string, - data: TInboxStatus + data: TInboxStatus, + user: ICurrentUserResponse | undefined ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`, @@ -81,7 +88,7 @@ class InboxServices extends APIService { : data.status === 1 ? "INBOX_ISSUE_ACCEPTED" : "INBOX_ISSUE_DUPLICATED"; - if (trackEvent) trackEventServices.trackInboxEvent(response?.data, action); + if (trackEvent) trackEventServices.trackInboxEvent(response?.data, action, user); return response?.data; }) .catch((error) => { @@ -93,14 +100,16 @@ class InboxServices extends APIService { workspaceSlug: string, projectId: string, inboxId: string, - data: any + data: any, + user: ICurrentUserResponse | undefined ): Promise { return this.post( `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`, data ) .then((response) => { - if (trackEvent) trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_CREATE"); + if (trackEvent) + trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_CREATE", user); return response?.data; }) .catch((error) => { diff --git a/apps/app/services/track-event.service.ts b/apps/app/services/track-event.service.ts index 9f82960b89b..c520ff79ec3 100644 --- a/apps/app/services/track-event.service.ts +++ b/apps/app/services/track-event.service.ts @@ -750,7 +750,11 @@ class TrackEventServices extends APIService { } // TODO: add types to the data - async trackInboxEvent(data: any, eventName: InboxEventType): Promise { + async trackInboxEvent( + data: any, + eventName: InboxEventType, + user: ICurrentUserResponse | undefined + ): Promise { let payload: any; if (eventName !== "INBOX_DELETE") payload = { @@ -772,6 +776,7 @@ class TrackEventServices extends APIService { extra: { ...payload, }, + user: user, }, }); } diff --git a/apps/app/types/inbox.d.ts b/apps/app/types/inbox.d.ts index ecd749edda6..dd0ed0dda3f 100644 --- a/apps/app/types/inbox.d.ts +++ b/apps/app/types/inbox.d.ts @@ -1,33 +1,76 @@ +import { IIssueFilterOptions, IIssueLabels } from "./issues"; import type { IProjectLite } from "./projects"; +import { IState } from "./state"; +import { IUserLite } from "./users"; export interface IInboxIssue { - id: string; - issue_detail: IInboxIssue; + assignee_details: IUserLite[]; + assignees: string[]; + bridge_id: string; + completed_at: string | null; created_at: Date; + created_by: string; + description: any; + description_html: string; + description_stripped: string; + estimate_point: number | null; + id: string; + issue_inbox: { + duplicate_to: string | null; + snoozed_till: Date | null; + source: string; + status: -2 | -1 | 0 | 1 | 2; + }; + label_details: IIssueLabels[]; + labels: string[]; + name: string; + parent: string | null; + priority: string | null; + project: string; + project_detail: IProjectLite; + sequence_id: number; + sort_order: number; + start_date: string | null; + state: string; + state_detail: IState; + sub_issues_count: number; + target_date: string | null; updated_at: Date; + updated_by: string; + workspace: string; +} + +export interface IInboxIssueDetail { + id: string; + issue_detail: { + id: string; + name: string; + description: any; + description_html: string; + priority: string | null; + start_date: string | null; + target_date: string | null; + sequence_id: number; + sort_order: number; + }; + project_detail: { + id: string; + identifier: string; + name: string; + }; + created_at: string; + updated_at: string; status: -2 | -1 | 0 | 1 | 2; - snoozed_till: Date | null; + snoozed_till: string | null; + source: string | null; created_by: string; updated_by: string; project: string; - project_detail: IProjectLite; workspace: string; inbox: string; issue: string; duplicate_to: string | null; } - -export interface IInboxIssue { - id: string; - name: string; - description: string; - priority: null; - start_date: null; - target_date: null; - sequence_id: number; - sort_order: number; -} - export interface IInbox { id: string; project_detail: IProjectLite; @@ -40,9 +83,13 @@ export interface IInbox { created_by: string; updated_by: string; project: string; + view_props: IInboxFilterOptions; workspace: string; } +interface StatePending { + readonly status: -2; +} interface StatusReject { status: -1; } @@ -61,8 +108,9 @@ interface StatusDuplicate { duplicate_to: string; } -interface StatePending { - readonly status: -2; -} - type TInboxStatus = StatusReject | StatusSnoozed | StatusAccepted | StatusDuplicate | StatePending; + +export interface IInboxFilterOptions { + priority: string[] | null; + inbox_status: number[] | null; +} From 006eb1b2ecbe8bde0b375f21d39ce8e8de295523 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Tue, 13 Jun 2023 17:46:19 +0530 Subject: [PATCH 28/51] fix: removed unnecessary api calls --- .../components/inbox/inbox-action-headers.tsx | 116 +++--- .../components/inbox/inbox-main-content.tsx | 106 ++--- .../ui/buttons/secondary-button.tsx | 6 +- .../projects/[projectId]/inbox/[inboxId].tsx | 379 +++++++++--------- apps/app/types/inbox.d.ts | 2 +- 5 files changed, 299 insertions(+), 310 deletions(-) diff --git a/apps/app/components/inbox/inbox-action-headers.tsx b/apps/app/components/inbox/inbox-action-headers.tsx index 007130686ef..1e66d32bdc6 100644 --- a/apps/app/components/inbox/inbox-action-headers.tsx +++ b/apps/app/components/inbox/inbox-action-headers.tsx @@ -4,8 +4,12 @@ import { useEffect, useState } from "react"; import DatePicker from "react-datepicker"; // headless ui import { Popover } from "@headlessui/react"; +// contexts +import { useProjectMyMembership } from "contexts/project-member.context"; // components -import { PrimaryButton, SecondaryButton, MultiLevelDropdown } from "components/ui"; +import { FiltersDropdown } from "components/inbox"; +// ui +import { PrimaryButton, SecondaryButton } from "components/ui"; // icons import { InboxIcon, StackedLayersHorizontalIcon } from "components/icons"; import { @@ -16,15 +20,14 @@ import { XCircleIcon, } from "@heroicons/react/24/outline"; // types -import type { IInboxIssue } from "types"; -import { useProjectMyMembership } from "contexts/project-member.context"; +import type { IInboxFilterOptions, IInboxIssue } from "types"; type Props = { issueCount: number; currentIssueIndex: number; - filter: any; - setFilter: (value: any) => void; - inboxIssue?: IInboxIssue; + filters: Partial; + setFilters: React.Dispatch>>; + issue?: IInboxIssue; onAccept: () => void; onDecline: () => void; onMarkAsDuplicate: () => void; @@ -39,9 +42,9 @@ export const InboxActionHeader: React.FC = (props) => { onDecline, onMarkAsDuplicate, onSnooze, - filter, - setFilter, - inboxIssue, + filters, + setFilters, + issue, } = props; const [date, setDate] = useState(new Date()); @@ -49,11 +52,12 @@ export const InboxActionHeader: React.FC = (props) => { const { memberRole } = useProjectMyMembership(); useEffect(() => { - if (!inboxIssue?.issue_inbox.snoozed_till) return; + if (!issue?.issue_inbox[0].snoozed_till) return; - setDate(new Date(inboxIssue.issue_inbox.snoozed_till)); - }, [inboxIssue]); + setDate(new Date(issue.issue_inbox[0].snoozed_till)); + }, [issue]); + const issueStatus = issue?.issue_inbox[0].status; const isAllowed = memberRole.isMember || memberRole.isOwner; return ( @@ -64,50 +68,32 @@ export const InboxActionHeader: React.FC = (props) => {

Inbox

- { - setFilter({ - status: value, - }); + { + const key = option.key as keyof typeof filters; + + const valueExists = filters[key]?.includes(option.value); + + if (valueExists) { + setFilters({ + ...filters, + [option.key]: ((filters[key] ?? []) as any[])?.filter( + (val) => val !== option.value + ), + }); + } else { + setFilters({ + ...filters, + [option.key]: [...((filters[key] ?? []) as any[]), option.value], + }); + } }} - direction="left" - options={[ - { - id: "all", - label: "All", - value: null, - selected: filter === null, - }, - { - id: "snooze", - label: "Snooze", - value: 0, - selected: filter === 0, - }, - { - id: "mark_as_duplicate", - label: "Duplicate", - value: 2, - selected: filter === 2, - }, - { - id: "accepted", - label: "Accepted", - value: 1, - selected: filter === 1, - }, - { - id: "declined", - label: "Declined", - value: -1, - selected: filter === -1, - }, - ]} + direction="right" + height="rg" />
-
{isAllowed && ( -
+
- - + + Snooze @@ -172,15 +163,26 @@ export const InboxActionHeader: React.FC = (props) => { size="sm" className="flex gap-2 items-center" onClick={onMarkAsDuplicate} + disabled={issueStatus !== -2} > Mark as duplicate - + Accept - + Decline diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx index 072b9511f72..beb1486f919 100644 --- a/apps/app/components/inbox/inbox-main-content.tsx +++ b/apps/app/components/inbox/inbox-main-content.tsx @@ -10,21 +10,16 @@ import { Control, UseFormReset, UseFormWatch } from "react-hook-form"; import issuesService from "services/issues.service"; import inboxServices from "services/inbox.service"; // ui -import { Spinner } from "components/ui"; +import { Loader } from "components/ui"; // hooks import useUser from "hooks/use-user"; // fetch-keys -import { - INBOX_ISSUES, - INBOX_ISSUE_DETAILS, - ISSUE_DETAILS, - PROJECT_ISSUES_ACTIVITY, -} from "constants/fetch-keys"; +import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; -import { IssueDescriptionForm, IssueDetailsSidebar } from "components/issues"; +import { IssueDescriptionForm } from "components/issues"; // types -import type { IInboxIssue, IIssue } from "types"; +import type { IInboxIssue, IInboxIssueDetail } from "types"; type Props = { onAccept: () => void; @@ -38,43 +33,39 @@ export const InboxMainContent: React.FC = (props) => { const { onAccept, watch, control, reset, status } = props; const router = useRouter(); - const { workspaceSlug, projectId, inboxId, issueId } = router.query; + const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query; const { user } = useUser(); - const { - data: issueDetails, - mutate: mutateIssueDetails, - error: issueDetailError, - } = useSWR( - workspaceSlug && projectId && inboxId && issueId - ? INBOX_ISSUE_DETAILS(inboxId.toString(), issueId.toString()) + const { data: issueDetails, mutate: mutateIssueDetails } = useSWR( + workspaceSlug && projectId && inboxId && inboxIssueId + ? INBOX_ISSUE_DETAILS(inboxId.toString(), inboxIssueId.toString()) : null, - workspaceSlug && projectId && inboxId && issueId + workspaceSlug && projectId && inboxId && inboxIssueId ? () => inboxServices.getInboxIssueById( workspaceSlug.toString(), projectId.toString(), inboxId.toString(), - issueId.toString() + inboxIssueId.toString() ) : null ); useEffect(() => { - if (!issueDetails || !issueId) return; + if (!issueDetails || !inboxIssueId) return; reset({ ...issueDetails.issue_detail, }); - }, [issueDetails, reset, issueId]); + }, [issueDetails, reset, inboxIssueId]); const submitChanges = useCallback( - async (formData: Partial) => { - if (!workspaceSlug || !projectId || !issueId || !inboxId) return; + async (formData: Partial) => { + if (!workspaceSlug || !projectId || !inboxIssueId || !inboxId) return; - mutate( - INBOX_ISSUE_DETAILS(inboxId.toString(), issueId.toString()), + mutate( + INBOX_ISSUE_DETAILS(inboxId.toString(), inboxIssueId.toString()), (prevData) => { if (!prevData) return prevData; @@ -87,50 +78,44 @@ export const InboxMainContent: React.FC = (props) => { ); const payload = { ...formData }; + await issuesService .patchIssue( workspaceSlug.toString(), projectId.toString(), - issueId.toString(), + inboxIssueId.toString(), payload, user ) .then((res) => { mutateIssueDetails(); mutate(INBOX_ISSUES(inboxId.toString())); - mutate(PROJECT_ISSUES_ACTIVITY(issueId.toString())); }) .catch((e) => { console.error(e); }); }, - [workspaceSlug, issueId, projectId, mutateIssueDetails, inboxId, user] + [workspaceSlug, inboxIssueId, projectId, mutateIssueDetails, inboxId, user] ); - if (!issueDetails && !issueDetailError) - return ( -
- -
- ); - - if (issueDetails) - return ( -
-
-
- + return ( + <> + {issueDetails ? ( +
+
+
+ +
-
- {/*
+ {/*
= (props) => { watch={watch} />
*/} -
- ); +
+ ) : ( + +
+ + + + +
+
+ + + + +
+
+ )} + + ); return null; }; diff --git a/apps/app/components/ui/buttons/secondary-button.tsx b/apps/app/components/ui/buttons/secondary-button.tsx index 6f0ab72b4a9..4b857a72c53 100644 --- a/apps/app/components/ui/buttons/secondary-button.tsx +++ b/apps/app/components/ui/buttons/secondary-button.tsx @@ -19,11 +19,7 @@ export const SecondaryButton: React.FC = ({ : size === "md" ? "rounded-md px-3.5 py-2 text-sm" : "rounded-lg px-4 py-2 text-base" - } ${ - disabled - ? "cursor-not-allowed border-brand-base bg-brand-surface-1 hover:border-brand-base hover:border-opacity-100 hover:bg-brand-surface-1 hover:bg-opacity-100" - : "" - } ${ + } ${disabled ? "cursor-not-allowed border-brand-base bg-brand-surface-1" : ""} ${ outline ? "bg-transparent hover:bg-brand-surface-2" : "bg-brand-surface-2 hover:border-opacity-70 hover:bg-opacity-70" diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index 1d8f1304b2e..2166d76d10e 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -25,7 +25,7 @@ import { // helper import { truncateText } from "helpers/string.helper"; // ui -import { PrimaryButton, Spinner } from "components/ui"; +import { Loader, PrimaryButton, Spinner } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons import { PlusIcon } from "@heroicons/react/24/outline"; @@ -33,7 +33,13 @@ import { PlusIcon } from "@heroicons/react/24/outline"; import type { IInboxFilterOptions, IInboxIssue } from "types"; import type { NextPage } from "next"; // fetch-keys -import { INBOX_DETAILS, INBOX_ISSUES, ISSUE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys"; +import { + INBOX_DETAILS, + INBOX_ISSUES, + INBOX_ISSUE_DETAILS, + ISSUE_DETAILS, + PROJECT_DETAILS, +} from "constants/fetch-keys"; import useUser from "hooks/use-user"; const defaultValues = { @@ -57,7 +63,7 @@ const ProjectIssues: NextPage = () => { const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false); const router = useRouter(); - const { workspaceSlug, projectId, inboxId, issueId } = router.query; + const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query; const { user } = useUser(); @@ -104,16 +110,16 @@ const ProjectIssues: NextPage = () => { const onKeyDown = useCallback( (e: KeyboardEvent) => { - if (!inboxIssues || !issueId) return; + if (!inboxIssues || !inboxIssueId) return; - const currentIssueIndex = inboxIssues.findIndex((issue) => issue.id === issueId); + const currentIssueIndex = inboxIssues.findIndex((issue) => issue.bridge_id === inboxIssueId); switch (e.key) { case "ArrowUp": Router.push({ pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, query: { - issueId: + inboxIssueId: currentIssueIndex === 0 ? inboxIssues[inboxIssues.length - 1].id : inboxIssues[currentIssueIndex - 1].id, @@ -124,7 +130,7 @@ const ProjectIssues: NextPage = () => { Router.push({ pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, query: { - issueId: + inboxIssueId: currentIssueIndex === inboxIssues.length - 1 ? inboxIssues[0].id : inboxIssues[currentIssueIndex + 1].id, @@ -136,7 +142,7 @@ const ProjectIssues: NextPage = () => { break; } }, - [workspaceSlug, projectId, issueId, inboxId, inboxIssues] + [workspaceSlug, projectId, inboxIssueId, inboxId, inboxIssues] ); useEffect(() => { @@ -155,7 +161,7 @@ const ProjectIssues: NextPage = () => { Router.push({ pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, query: { - issueId: inboxIssues[0].id, + inboxIssueId: inboxIssues[0].bridge_id, }, }); }, [inboxIssues, workspaceSlug, projectId, inboxId]); @@ -179,30 +185,6 @@ const ProjectIssues: NextPage = () => { } right={
- { - const key = option.key as keyof typeof filters; - - const valueExists = filters[key]?.includes(option.value); - - if (valueExists) { - setFilters({ - ...filters, - [option.key]: ((filters[key] ?? []) as any[])?.filter( - (val) => val !== option.value - ), - }); - } else { - setFilters({ - ...filters, - [option.key]: [...((filters[key] ?? []) as any[]), option.value], - }); - } - }} - direction="left" - height="rg" - /> { @@ -216,188 +198,195 @@ const ProjectIssues: NextPage = () => {
} > - {inboxIssues ? ( - inboxIssues.length === 0 ? ( -
-

- No inbox issues found in this inbox. -

-
- ) : ( - <> - setSelectDuplicateIssue(false)} - value={ - inboxIssues?.find((inboxIssue) => inboxIssue.id === issueId)?.issue_inbox - .duplicate_to - } - onSubmit={(dupIssueId: string) => { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.id === issueId)?.id!, - { - status: 2, - duplicate_to: dupIssueId, - }, - user - ) - .then(() => { - reset(defaultValues); - setSelectDuplicateIssue(false); - mutate(ISSUE_DETAILS(issueId as string), undefined); - inboxIssuesMutate((prevData) => - (prevData ?? [])?.map((item) => ({ - ...item, - status: item.id === issueId ? 2 : item.issue_inbox.status, - duplicate_to: dupIssueId, - })) - ); - }) - .catch(() => { - setSelectDuplicateIssue(false); - }); - }} - /> -
- issue.id === issueId)} - currentIssueIndex={inboxIssues?.findIndex((issue) => issue.id === issueId) ?? 0} - issueCount={inboxIssues?.length ?? 0} - onAccept={() => { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.id === issueId)?.bridge_id!, - { - status: 1, - }, - user + <> + setSelectDuplicateIssue(false)} + value={ + inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId) + ?.issue_inbox[0].duplicate_to + } + onSubmit={(dupIssueId: string) => { + inboxServices + .markInboxStatus( + workspaceSlug!.toString(), + projectId!.toString(), + inboxId!.toString(), + inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId)?.id!, + { + status: 2, + duplicate_to: dupIssueId, + }, + user + ) + .then(() => { + reset(defaultValues); + setSelectDuplicateIssue(false); + mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string)); + inboxIssuesMutate((prevData) => + (prevData ?? [])?.map((item) => ({ + ...item, + status: item.bridge_id === inboxIssueId ? 2 : item.issue_inbox[0].status, + duplicate_to: dupIssueId, + })) + ); + }) + .catch(() => { + setSelectDuplicateIssue(false); + }); + }} + /> +
+ issue.bridge_id === inboxIssueId)} + currentIssueIndex={ + inboxIssues?.findIndex((issue) => issue.bridge_id === inboxIssueId) ?? 0 + } + issueCount={inboxIssues?.length ?? 0} + onAccept={() => { + inboxServices + .markInboxStatus( + workspaceSlug!.toString(), + projectId!.toString(), + inboxId!.toString(), + inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId) + ?.bridge_id!, + { + status: 1, + }, + user + ) + .then(() => { + mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string)); + inboxIssuesMutate((prevData) => + prevData?.map((item) => + item.bridge_id === inboxIssueId ? { ...item, status: 1 } : item ) - .then(() => { - mutate(ISSUE_DETAILS(issueId as string), undefined); - inboxIssuesMutate((prevData) => - prevData?.map((item) => - item.id === issueId ? { ...item, status: 1 } : item - ) - ); - }); - }} - onDecline={() => { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.id === issueId)?.id!, - { - status: -1, - }, - user + ); + }); + }} + onDecline={() => { + inboxServices + .markInboxStatus( + workspaceSlug!.toString(), + projectId!.toString(), + inboxId!.toString(), + inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId) + ?.bridge_id!, + { + status: -1, + }, + user + ) + .then(() => { + reset(defaultValues); + mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string)); + inboxIssuesMutate((prevData) => + prevData?.map((item) => + item.bridge_id === inboxIssueId ? { ...item, status: -1 } : item ) - .then(() => { - reset(defaultValues); - mutate(ISSUE_DETAILS(issueId as string), undefined); - inboxIssuesMutate((prevData) => - prevData?.map((item) => - item.id === issueId ? { ...item, status: -1 } : item - ) - ); - }); - }} - onMarkAsDuplicate={() => { - setSelectDuplicateIssue(true); - }} - onSnooze={(date) => { + ); + }); + }} + onMarkAsDuplicate={() => { + setSelectDuplicateIssue(true); + }} + onSnooze={(date) => { + inboxServices + .markInboxStatus( + workspaceSlug!.toString(), + projectId!.toString(), + inboxId!.toString(), + inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId) + ?.bridge_id!, + { + status: 0, + snoozed_till: new Date(date), + }, + user + ) + .then(() => { + reset(defaultValues); + mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string)); + inboxIssuesMutate((prevData) => + prevData?.map((item) => + item.bridge_id === inboxIssueId + ? { ...item, status: 0, snoozed_till: new Date(date) } + : item + ) + ); + }); + }} + /> +
+ {inboxIssues ? ( + inboxIssues.length > 0 ? ( +
+ {inboxIssues.map((issue) => ( + + + + + + ))} +
+ ) : ( +
+ No issues found for the selected filters. Try changing the filters. +
+ ) + ) : ( + + + + + + + )} +
+ inboxIssue.bridge_id === inboxIssueId) + ?.issue_inbox[0].status + } + onAccept={() => { inboxServices .markInboxStatus( workspaceSlug!.toString(), projectId!.toString(), inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.id === issueId)?.id!, + inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId) + ?.id!, { - status: 0, - snoozed_till: new Date(date), + status: 1, }, user ) .then(() => { reset(defaultValues); - mutate(ISSUE_DETAILS(issueId as string), undefined); + mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string)); inboxIssuesMutate((prevData) => - prevData?.map((item) => - item.id === issueId - ? { ...item, status: 0, snoozed_till: new Date(date) } - : item - ) + (prevData ?? [])?.map((item) => ({ + ...item, + status: + item.bridge_id === inboxIssueId ? 1 : item.issue_inbox[0].status, + })) ); }); }} /> - -
-
- {inboxIssues?.map((issue) => ( - - - - - - ))} -
-
- inboxIssue.id === issueId)?.issue_inbox - .status - } - onAccept={() => { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.id === issueId)?.id!, - { - status: 1, - }, - user - ) - .then(() => { - reset(defaultValues); - mutate(ISSUE_DETAILS(issueId as string), undefined); - inboxIssuesMutate((prevData) => - (prevData ?? [])?.map((item) => ({ - ...item, - status: item.id === issueId ? 1 : item.issue_inbox.status, - })) - ); - }); - }} - /> -
-
- - ) - ) : ( -
- +
- )} + ); diff --git a/apps/app/types/inbox.d.ts b/apps/app/types/inbox.d.ts index dd0ed0dda3f..c35e3d7bf96 100644 --- a/apps/app/types/inbox.d.ts +++ b/apps/app/types/inbox.d.ts @@ -20,7 +20,7 @@ export interface IInboxIssue { snoozed_till: Date | null; source: string; status: -2 | -1 | 0 | 1 | 2; - }; + }[]; label_details: IIssueLabels[]; labels: string[]; name: string; From fabe8e60be2221570e5bc565539eea4a3fb58370 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Wed, 14 Jun 2023 12:27:33 +0530 Subject: [PATCH 29/51] style: viewed issues --- apps/app/components/inbox/inbox-action-headers.tsx | 2 +- apps/app/components/inbox/inbox-issue-card.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/app/components/inbox/inbox-action-headers.tsx b/apps/app/components/inbox/inbox-action-headers.tsx index 1e66d32bdc6..9f0db91ae8e 100644 --- a/apps/app/components/inbox/inbox-action-headers.tsx +++ b/apps/app/components/inbox/inbox-action-headers.tsx @@ -121,7 +121,7 @@ export const InboxActionHeader: React.FC = (props) => {
{isAllowed && ( -
+
= (props) => { id={issue.id} className={`relative h-20 cursor-pointer select-none space-y-3 py-2 px-4 border-b border-brand-base hover:bg-brand-accent hover:bg-opacity-10 ${ active ? "bg-brand-accent bg-opacity-5" : " " - }`} + } ${issue.issue_inbox[0].status !== -2 ? "opacity-60" : ""}`} >

@@ -45,11 +45,11 @@ export const InboxIssueCard: React.FC = (props) => { "text-sm" )}

- {issue.issue_inbox.snoozed_till && ( + {issue.issue_inbox[0].snoozed_till && (

- {new Date(issue.issue_inbox.snoozed_till).toLocaleDateString("en-US", { + {new Date(issue.issue_inbox[0].snoozed_till).toLocaleDateString("en-US", { month: "short", day: "numeric", })} From b9bb1440b0269b1b6c7acf0d885eaaf894d5c22a Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 14 Jun 2023 15:19:52 +0530 Subject: [PATCH 30/51] fix: priority validation --- apiserver/plane/api/views/inbox.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index 882246c440b..e745706280d 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -178,6 +178,7 @@ def create(self, request, slug, project_id, inbox_id): "medium", "high", "urgent", + None ]: return Response( {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST From 2b760fdfe54c578a17dae6f8f835bce0563ea7f7 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 14 Jun 2023 15:20:11 +0530 Subject: [PATCH 31/51] dev: remove print logs --- apiserver/plane/api/views/inbox.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index e745706280d..5d5078c5e13 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -193,8 +193,6 @@ def create(self, request, slug, project_id, inbox_id): color="#ff7700", ) - print(state) - # create an issue issue = Issue.objects.create( name=request.data.get("issue", {}).get("name"), From db5b21314d9b7680e45090cbbdd4881bccb3e2f1 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 14 Jun 2023 17:13:55 +0530 Subject: [PATCH 32/51] dev: update issue inbox update workflow --- apiserver/plane/api/serializers/inbox.py | 2 +- apiserver/plane/api/views/inbox.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/api/serializers/inbox.py b/apiserver/plane/api/serializers/inbox.py index b793a0f01da..ae17b749bfa 100644 --- a/apiserver/plane/api/serializers/inbox.py +++ b/apiserver/plane/api/serializers/inbox.py @@ -40,7 +40,7 @@ class Meta: class InboxIssueLiteSerializer(BaseSerializer): class Meta: model = InboxIssue - fields = ["status", "duplicate_to", "snoozed_till", "source"] + fields = ["id", "status", "duplicate_to", "snoozed_till", "source"] read_only_fields = fields diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index 5d5078c5e13..c77e2d2321c 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -229,6 +229,18 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): inbox_issue = InboxIssue.objects.get( pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id ) + + issue_data = request.data.pop("issue", False) + + if bool(issue_data): + issue = Issue.objects.get(pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id) + issue_serializer = IssueCreateSerializer(issue, data=issue_data, partial=True) + + if issue_serializer.is_valid(): + issue_serializer.save() + else: + return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST) + serializer = InboxIssueSerializer( inbox_issue, data=request.data, partial=True ) From 70be9b5254b1289372c11374d7b86ee3899fc260 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 15 Jun 2023 00:56:06 +0530 Subject: [PATCH 33/51] chore: added inbox view context --- .../components/inbox/decline-issue-modal.tsx | 162 ++++ .../components/inbox/delete-issue-modal.tsx | 145 ++++ .../components/inbox/inbox-action-headers.tsx | 213 ++--- .../app/components/inbox/inbox-issue-card.tsx | 111 ++- .../components/inbox/inbox-main-content.tsx | 132 ++- apps/app/components/inbox/index.ts | 2 + apps/app/components/issues/activity.tsx | 30 +- apps/app/components/issues/modal.tsx | 13 +- apps/app/components/issues/sidebar.tsx | 804 +++++++++--------- apps/app/constants/fetch-keys.ts | 2 + apps/app/contexts/inbox-view-context.tsx | 163 ++++ apps/app/contexts/issue-view.context.tsx | 1 + apps/app/hooks/use-inbox-view.tsx | 51 ++ .../projects/[projectId]/inbox/[inboxId].tsx | 216 ++--- .../projects/[projectId]/issues/[issueId].tsx | 2 +- apps/app/services/inbox.service.ts | 62 +- apps/app/services/track-event.service.ts | 2 + apps/app/types/inbox.d.ts | 56 +- 18 files changed, 1387 insertions(+), 780 deletions(-) create mode 100644 apps/app/components/inbox/decline-issue-modal.tsx create mode 100644 apps/app/components/inbox/delete-issue-modal.tsx create mode 100644 apps/app/contexts/inbox-view-context.tsx create mode 100644 apps/app/hooks/use-inbox-view.tsx diff --git a/apps/app/components/inbox/decline-issue-modal.tsx b/apps/app/components/inbox/decline-issue-modal.tsx new file mode 100644 index 00000000000..68581450ebc --- /dev/null +++ b/apps/app/components/inbox/decline-issue-modal.tsx @@ -0,0 +1,162 @@ +import React, { useEffect, useState } from "react"; + +import { useRouter } from "next/router"; + +import { mutate } from "swr"; + +// headless ui +import { Dialog, Transition } from "@headlessui/react"; +// services +import inboxServices from "services/inbox.service"; +// hooks +import useToast from "hooks/use-toast"; +import useInboxView from "hooks/use-inbox-view"; +import useUser from "hooks/use-user"; +// icons +import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; +// ui +import { SecondaryButton, DangerButton } from "components/ui"; +// types +import type { IInboxIssue, ICurrentUserResponse, IInboxIssueDetail } from "types"; +// fetch-keys +import { INBOX_ISSUES, INBOX_ISSUE_DETAILS } from "constants/fetch-keys"; + +type Props = { + isOpen: boolean; + handleClose: () => void; + data: IInboxIssue | undefined; +}; + +export const DeclineIssueModal: React.FC = ({ isOpen, handleClose, data }) => { + const [isDeclining, setIsDeclining] = useState(false); + + const router = useRouter(); + const { workspaceSlug, projectId, inboxId } = router.query; + + const { user } = useUser(); + const { setToastAlert } = useToast(); + const { params } = useInboxView(); + + const onClose = () => { + setIsDeclining(false); + handleClose(); + }; + + const handleDecline = () => { + if (!workspaceSlug || !projectId || !inboxId || !data) return; + + setIsDeclining(true); + + inboxServices + .markInboxStatus( + workspaceSlug.toString(), + projectId.toString(), + inboxId.toString(), + data.bridge_id, + { + status: -1, + }, + user + ) + .then(() => { + mutate( + INBOX_ISSUE_DETAILS(inboxId.toString(), data.bridge_id), + (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + status: -1, + }; + }, + false + ); + mutate( + INBOX_ISSUES(inboxId.toString(), params), + (prevData) => + prevData?.map((item) => + item.bridge_id === data.bridge_id ? { ...item, status: -1 } : item + ), + false + ); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue declined successfully.", + }); + onClose(); + }) + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be declined. Please try again.", + }) + ) + .finally(() => setIsDeclining(false)); + }; + + return ( + +

+ +
+ + +
+
+ + +
+
+ + + +

Decline Issue

+
+
+ +

+ Are you sure you want to decline issue{" "} + + {data?.project_detail?.identifier}-{data?.sequence_id} + + {""}? This action cannot be undone. +

+
+
+ Cancel + + {isDeclining ? "Declining..." : "Decline Issue"} + +
+
+
+
+
+
+
+ + ); +}; diff --git a/apps/app/components/inbox/delete-issue-modal.tsx b/apps/app/components/inbox/delete-issue-modal.tsx new file mode 100644 index 00000000000..3261cd8a868 --- /dev/null +++ b/apps/app/components/inbox/delete-issue-modal.tsx @@ -0,0 +1,145 @@ +import React, { useEffect, useState } from "react"; + +import { useRouter } from "next/router"; + +import { mutate } from "swr"; + +// headless ui +import { Dialog, Transition } from "@headlessui/react"; +// services +import inboxServices from "services/inbox.service"; +// hooks +import useToast from "hooks/use-toast"; +import useInboxView from "hooks/use-inbox-view"; +import useUser from "hooks/use-user"; +// icons +import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; +// ui +import { SecondaryButton, DangerButton } from "components/ui"; +// types +import type { IInboxIssue, IInboxIssueDetail } from "types"; +// fetch-keys +import { INBOX_ISSUES, INBOX_ISSUE_DETAILS } from "constants/fetch-keys"; + +type Props = { + isOpen: boolean; + handleClose: () => void; + data: IInboxIssue | undefined; +}; + +export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data }) => { + const [isDeleting, setIsDeleting] = useState(false); + + const router = useRouter(); + const { workspaceSlug, projectId, inboxId } = router.query; + + const { user } = useUser(); + const { setToastAlert } = useToast(); + const { params } = useInboxView(); + + const onClose = () => { + setIsDeleting(false); + handleClose(); + }; + + const handleDelete = () => { + if (!workspaceSlug || !projectId || !inboxId || !data) return; + + setIsDeleting(true); + + inboxServices + .deleteInboxIssue( + workspaceSlug.toString(), + projectId.toString(), + inboxId.toString(), + data.bridge_id.toString(), + user + ) + .then(() => { + mutate( + INBOX_ISSUES(inboxId.toString(), params), + (prevData) => (prevData ?? []).filter((i) => i.id !== data.id), + false + ); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue deleted successfully.", + }); + + onClose(); + }) + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be deleted. Please try again.", + }) + ) + .finally(() => setIsDeleting(false)); + }; + + return ( + + + +
+ + +
+
+ + +
+
+ + + +

Delete Issue

+
+
+ +

+ Are you sure you want to delete issue{" "} + + {data?.project_detail.identifier}-{data?.sequence_id} + + {""}? This action cannot be undone. +

+
+
+ Cancel + + {isDeleting ? "Deleting..." : "Delete Issue"} + +
+
+
+
+
+
+
+
+ ); +}; diff --git a/apps/app/components/inbox/inbox-action-headers.tsx b/apps/app/components/inbox/inbox-action-headers.tsx index 9f0db91ae8e..619ca3ebd58 100644 --- a/apps/app/components/inbox/inbox-action-headers.tsx +++ b/apps/app/components/inbox/inbox-action-headers.tsx @@ -6,6 +6,8 @@ import DatePicker from "react-datepicker"; import { Popover } from "@headlessui/react"; // contexts import { useProjectMyMembership } from "contexts/project-member.context"; +// hooks +import useInboxView from "hooks/use-inbox-view"; // components import { FiltersDropdown } from "components/inbox"; // ui @@ -18,20 +20,21 @@ import { CheckCircleIcon, ClockIcon, XCircleIcon, + TrashIcon, } from "@heroicons/react/24/outline"; // types -import type { IInboxFilterOptions, IInboxIssue } from "types"; +import type { IInboxIssue } from "types"; +import { useRouter } from "next/router"; type Props = { issueCount: number; currentIssueIndex: number; - filters: Partial; - setFilters: React.Dispatch>>; issue?: IInboxIssue; onAccept: () => void; onDecline: () => void; onMarkAsDuplicate: () => void; onSnooze: (date: Date | string) => void; + onDelete: () => void; }; export const InboxActionHeader: React.FC = (props) => { @@ -42,14 +45,17 @@ export const InboxActionHeader: React.FC = (props) => { onDecline, onMarkAsDuplicate, onSnooze, - filters, - setFilters, + onDelete, issue, } = props; const [date, setDate] = useState(new Date()); + const router = useRouter(); + const { inboxIssueId } = router.query; + const { memberRole } = useProjectMyMembership(); + const { filters, setFilters } = useInboxView(); useEffect(() => { if (!issue?.issue_inbox[0].snoozed_till) return; @@ -77,14 +83,12 @@ export const InboxActionHeader: React.FC = (props) => { if (valueExists) { setFilters({ - ...filters, [option.key]: ((filters[key] ?? []) as any[])?.filter( (val) => val !== option.value ), }); } else { setFilters({ - ...filters, [option.key]: [...((filters[key] ?? []) as any[]), option.value], }); } @@ -94,101 +98,116 @@ export const InboxActionHeader: React.FC = (props) => { />
-
-
- - -
- {currentIssueIndex + 1}/{issueCount} + {inboxIssueId && ( +
+
+ + +
+ {currentIssueIndex + 1}/{issueCount} +
-
- {isAllowed && ( -
- - + {isAllowed && ( +
+
+ + + + + Snooze + + + + {({ close }) => ( +
+ { + if (!val) return; + setDate(val); + }} + dateFormat="dd-MM-yyyy" + inline + /> + { + close(); + onSnooze(date); + }} + > + Snooze + +
+ )} +
+
- - Snooze + + Mark as duplicate - - - {({ close }) => ( -
- { - if (!val) return; - setDate(val); - }} - dateFormat="dd-MM-yyyy" - inline - /> - { - close(); - onSnooze(date); - }} - > - Snooze - -
- )} -
- - - - Mark as duplicate - - - - Accept - - - - Decline - -
- )} -
+ + + Accept + + + + Decline + +
+
+ + + Delete + +
+
+ )} +
+ )}
); }; diff --git a/apps/app/components/inbox/inbox-issue-card.tsx b/apps/app/components/inbox/inbox-issue-card.tsx index 2f2d8bd4bb1..6da7b260722 100644 --- a/apps/app/components/inbox/inbox-issue-card.tsx +++ b/apps/app/components/inbox/inbox-issue-card.tsx @@ -1,7 +1,10 @@ +// ui +import { Tooltip } from "components/ui"; // icons import { getPriorityIcon } from "components/icons"; -import { CalendarIcon } from "@heroicons/react/24/outline"; - +import { CalendarDaysIcon, ClockIcon } from "@heroicons/react/24/outline"; +// helpers +import { renderShortNumericDateFormat } from "helpers/date-time.helper"; // types import type { IInboxIssue } from "types"; @@ -13,50 +16,74 @@ type Props = { export const InboxIssueCard: React.FC = (props) => { const { issue, active } = props; + const issueStatus = issue.issue_inbox[0].status; + return ( -
-
-

- {issue.project_detail.identifier}-{issue.sequence_id} -

-
{issue.name}
-
-
-
- {getPriorityIcon( - issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None", - "text-sm" +
+
+

+ {issue.project_detail?.identifier}-{issue.sequence_id} +

+
{issue.name}
+
+
+ +
+ {getPriorityIcon( + issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None", + "text-sm" + )} +
+
+ +
+ + {renderShortNumericDateFormat(issue.created_at ?? "")} +
+
+ {issue.issue_inbox[0].snoozed_till && ( +
+ + + Snoozed till {renderShortNumericDateFormat(issue.issue_inbox[0].snoozed_till)} + +
)}
- {issue.issue_inbox[0].snoozed_till && ( -
- -

- {new Date(issue.issue_inbox[0].snoozed_till).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - })} -

-
- )}
-
+ ); }; diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx index beb1486f919..54e802571cb 100644 --- a/apps/app/components/inbox/inbox-main-content.tsx +++ b/apps/app/components/inbox/inbox-main-content.tsx @@ -5,7 +5,7 @@ import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; // react hook form -import { Control, UseFormReset, UseFormWatch } from "react-hook-form"; +import { useForm } from "react-hook-form"; // services import issuesService from "services/issues.service"; import inboxServices from "services/inbox.service"; @@ -14,28 +14,40 @@ import { Loader } from "components/ui"; // hooks import useUser from "hooks/use-user"; // fetch-keys -import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; +import { INBOX_ISSUES, INBOX_ISSUE_DETAILS } from "constants/fetch-keys"; -import { IssueDescriptionForm } from "components/issues"; +import { + AddComment, + IssueActivitySection, + IssueDescriptionForm, + IssueDetailsSidebar, +} from "components/issues"; // types import type { IInboxIssue, IInboxIssueDetail } from "types"; - -type Props = { - onAccept: () => void; - control: Control; - watch: UseFormWatch; - reset: UseFormReset; - status?: -2 | -1 | 0 | 1 | 2; +import useInboxView from "hooks/use-inbox-view"; + +const defaultValues = { + name: "", + description: "", + description_html: "", + estimate_point: null, + assignees_list: [], + priority: "low", + target_date: new Date().toString(), + labels_list: [], }; -export const InboxMainContent: React.FC = (props) => { - const { onAccept, watch, control, reset, status } = props; - +export const InboxMainContent: React.FC = () => { const router = useRouter(); const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query; const { user } = useUser(); + const { params } = useInboxView(); + + const { reset, control, watch } = useForm({ + defaultValues, + }); const { data: issueDetails, mutate: mutateIssueDetails } = useSWR( workspaceSlug && projectId && inboxId && inboxIssueId @@ -61,41 +73,62 @@ export const InboxMainContent: React.FC = (props) => { }, [issueDetails, reset, inboxIssueId]); const submitChanges = useCallback( - async (formData: Partial) => { - if (!workspaceSlug || !projectId || !inboxIssueId || !inboxId) return; + async (formData: Partial) => { + if (!workspaceSlug || !projectId || !inboxIssueId || !inboxId || !issueDetails) return; - mutate( - INBOX_ISSUE_DETAILS(inboxId.toString(), inboxIssueId.toString()), - (prevData) => { - if (!prevData) return prevData; + mutateIssueDetails((prevData) => { + if (!prevData) return prevData; - return { - ...prevData, + return { + ...prevData, + issue_detail: { + ...prevData.issue_detail, ...formData, - }; - }, + }, + }; + }, false); + mutate( + INBOX_ISSUES(inboxId.toString(), params), + (prevData) => + (prevData ?? []).map((i) => { + if (i.bridge_id === inboxIssueId) { + return { + ...i, + ...formData, + }; + } + + return i; + }), false ); - const payload = { ...formData }; + const payload = { issue: { ...formData } }; - await issuesService - .patchIssue( + await inboxServices + .patchInboxIssue( workspaceSlug.toString(), projectId.toString(), - inboxIssueId.toString(), + inboxId.toString(), + issueDetails.id, payload, user ) - .then((res) => { + .then(() => { mutateIssueDetails(); - mutate(INBOX_ISSUES(inboxId.toString())); - }) - .catch((e) => { - console.error(e); + mutate(INBOX_ISSUES(inboxId.toString(), params)); }); }, - [workspaceSlug, inboxIssueId, projectId, mutateIssueDetails, inboxId, user] + [ + workspaceSlug, + inboxIssueId, + projectId, + mutateIssueDetails, + inboxId, + user, + issueDetails, + params, + ] ); return ( @@ -106,23 +139,38 @@ export const InboxMainContent: React.FC = (props) => {
+
+

Comments/Activity

+ + +
- {/*
- -
*/} +
+ +
) : ( diff --git a/apps/app/components/inbox/index.ts b/apps/app/components/inbox/index.ts index 833e06f8c93..e2194768870 100644 --- a/apps/app/components/inbox/index.ts +++ b/apps/app/components/inbox/index.ts @@ -1,3 +1,5 @@ +export * from "./decline-issue-modal"; +export * from "./delete-issue-modal"; export * from "./filters-dropdown"; export * from "./select-duplicate"; export * from "./inbox-issue-card"; diff --git a/apps/app/components/issues/activity.tsx b/apps/app/components/issues/activity.tsx index 3b9ecc8533e..8d708b85612 100644 --- a/apps/app/components/issues/activity.tsx +++ b/apps/app/components/issues/activity.tsx @@ -4,6 +4,14 @@ import { useRouter } from "next/router"; import useSWR from "swr"; +// services +import issuesService from "services/issues.service"; +// hooks +import useEstimateOption from "hooks/use-estimate-option"; +// components +import { CommentCard } from "components/issues/comment"; +// ui +import { Loader } from "components/ui"; // icons import { CalendarDaysIcon, @@ -17,20 +25,13 @@ import { UserIcon, } from "@heroicons/react/24/outline"; import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons"; -// services -import issuesService from "services/issues.service"; -// components -import { CommentCard } from "components/issues/comment"; -// ui -import { Loader } from "components/ui"; - // helpers import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper"; // types import { ICurrentUserResponse, IIssueComment, IIssueLabels } from "types"; +// fetch-keys import { PROJECT_ISSUES_ACTIVITY, PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; -import useEstimateOption from "hooks/use-estimate-option"; const activityDetails: { [key: string]: { @@ -60,7 +61,7 @@ const activityDetails: { }, estimate_point: { message: "set the estimate point to", - icon:
- ( - submitChanges({ state: val })} - userAuth={memberRole} - /> - )} - /> - ( - submitChanges({ assignees_list: val })} - userAuth={memberRole} - /> - )} - /> - ( - submitChanges({ priority: val })} - userAuth={memberRole} - /> - )} - /> - ( - submitChanges({ estimate_point: val })} - userAuth={memberRole} - /> - )} - /> + {(fieldsToShow === "all" || fieldsToShow.state) && ( + ( + submitChanges({ state: val })} + userAuth={memberRole} + /> + )} + /> + )} + {(fieldsToShow === "all" || fieldsToShow.assignee) && ( + ( + submitChanges({ assignees_list: val })} + userAuth={memberRole} + /> + )} + /> + )} + {(fieldsToShow === "all" || fieldsToShow.priority) && ( + ( + submitChanges({ priority: val })} + userAuth={memberRole} + /> + )} + /> + )} + {(fieldsToShow === "all" || fieldsToShow.estimate) && ( + ( + submitChanges({ estimate_point: val })} + userAuth={memberRole} + /> + )} + /> + )}
- - i.id !== issueDetail?.id && - i.id !== issueDetail?.parent && - i.parent !== issueDetail?.id - ) ?? [] - } - customDisplay={ - issueDetail?.parent_detail ? ( - - ) : ( -
- No parent selected -
- ) - } - watch={watchIssue} - userAuth={memberRole} - /> - i.id !== issueDetail?.id) ?? []} - watch={watchIssue} - userAuth={memberRole} - /> - i.id !== issueDetail?.id) ?? []} - watch={watchIssue} - userAuth={memberRole} - /> -
-
- -

Due date

-
-
- ( - - submitChanges({ - target_date: val, - }) - } - className="bg-brand-surface-1" - disabled={isNotAllowed} - /> - )} - /> + {(fieldsToShow === "all" || fieldsToShow.parent) && ( + + i.id !== issueDetail?.id && + i.id !== issueDetail?.parent && + i.parent !== issueDetail?.id + ) ?? [] + } + customDisplay={ + issueDetail?.parent_detail ? ( + + ) : ( +
+ No parent selected +
+ ) + } + watch={watchIssue} + userAuth={memberRole} + /> + )} + {(fieldsToShow === "all" || fieldsToShow.blocker) && ( + i.id !== issueDetail?.id) ?? []} + watch={watchIssue} + userAuth={memberRole} + /> + )} + {(fieldsToShow === "all" || fieldsToShow.blocked) && ( + i.id !== issueDetail?.id) ?? []} + watch={watchIssue} + userAuth={memberRole} + /> + )} + {(fieldsToShow === "all" || fieldsToShow.dueDate) && ( +
+
+ +

Due date

+
+
+ ( + + submitChanges({ + target_date: val, + }) + } + className="bg-brand-surface-1" + disabled={isNotAllowed} + /> + )} + /> +
-
+ )}
- - + {(fieldsToShow === "all" || fieldsToShow.cycle) && ( + + )} + {(fieldsToShow === "all" || fieldsToShow.module) && ( + + )}
-
-
-
- -

Label

-
-
-
- {watchIssue("labels_list")?.map((labelId) => { - const label = issueLabels?.find((l) => l.id === labelId); - - if (label) - return ( - { - const updatedLabels = watchIssue("labels_list")?.filter( - (l) => l !== labelId - ); - submitChanges({ - labels_list: updatedLabels, - }); - }} - > + {(fieldsToShow === "all" || fieldsToShow.label) && ( +
+
+
+ +

Label

+
+
+
+ {watchIssue("labels_list")?.map((labelId) => { + const label = issueLabels?.find((l) => l.id === labelId); + + if (label) + return ( { + const updatedLabels = watchIssue("labels_list")?.filter( + (l) => l !== labelId + ); + submitChanges({ + labels_list: updatedLabels, + }); }} - /> - {label.name} - - - ); - })} - ( - submitChanges({ labels_list: val })} - className="flex-shrink-0" - multiple - disabled={isNotAllowed} - > - {({ open }) => ( -
- - Select Label - - - - -
- {issueLabels ? ( - issueLabels.length > 0 ? ( - issueLabels.map((label: IIssueLabels) => { - const children = issueLabels?.filter( - (l) => l.parent === label.id - ); - - if (children.length === 0) { - if (!label.parent) + > + + {label.name} + + + ); + })} + ( + submitChanges({ labels_list: val })} + className="flex-shrink-0" + multiple + disabled={isNotAllowed} + > + {({ open }) => ( +
+ + Select Label + + + + +
+ {issueLabels ? ( + issueLabels.length > 0 ? ( + issueLabels.map((label: IIssueLabels) => { + const children = issueLabels?.filter( + (l) => l.parent === label.id + ); + + if (children.length === 0) { + if (!label.parent) + return ( + + `${ + active || selected ? "bg-brand-surface-1" : "" + } ${ + selected ? "" : "text-brand-secondary" + } flex cursor-pointer select-none items-center gap-2 truncate p-2` + } + value={label.id} + > + + {label.name} + + ); + } else return ( - - `${ - active || selected ? "bg-brand-surface-1" : "" - } ${ - selected ? "" : "text-brand-secondary" - } flex cursor-pointer select-none items-center gap-2 truncate p-2` - } - value={label.id} - > - - {label.name} - - ); - } else - return ( -
-
- - {label.name} +
+
+ + {label.name} +
+
+ {children.map((child) => ( + + `${ + active || selected ? "bg-brand-base" : "" + } ${ + selected ? "" : "text-brand-secondary" + } flex cursor-pointer select-none items-center gap-2 truncate p-2` + } + value={child.id} + > + + {child.name} + + ))} +
-
- {children.map((child) => ( - - `${active || selected ? "bg-brand-base" : ""} ${ - selected ? "" : "text-brand-secondary" - } flex cursor-pointer select-none items-center gap-2 truncate p-2` - } - value={child.id} - > - - {child.name} - - ))} -
-
- ); - }) + ); + }) + ) : ( +
No labels found
+ ) ) : ( -
No labels found
- ) - ) : ( - - )} -
- - -
+ + )} +
+ + +
+ )} + + )} + /> + {!isNotAllowed && ( + )} - /> - {!isNotAllowed && ( -
+
+
+ {createLabelForm && ( +
+
+ + {({ open }) => ( <> - New + + {watch("color") && watch("color") !== "" && ( + + )} + + + + + + ( + onChange(value.hex)} + /> + )} + /> + + )} - - )} -
-
-
- {createLabelForm && ( - -
- - {({ open }) => ( - <> - - {watch("color") && watch("color") !== "" && ( - - )} - - - - - - ( - onChange(value.hex)} - /> - )} - /> - - - - )} - -
- - - - - )} -
-
-
-

Links

- {!isNotAllowed && ( - + +
+ + + + )}
-
- {issueDetail?.issue_link && issueDetail.issue_link.length > 0 ? ( - - ) : null} + )} + {(fieldsToShow === "all" || fieldsToShow.link) && ( +
+
+

Links

+ {!isNotAllowed && ( + + )} +
+
+ {issueDetail?.issue_link && issueDetail.issue_link.length > 0 ? ( + + ) : null} +
-
+ )}
); diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 5625b710db7..93b533bf024 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -145,6 +145,8 @@ export const INBOX_ISSUES = (inboxId: string, params?: any) => { const paramsKey = inboxParamsToKey(params); + console.log("triggered 2"); + return `INBOX_ISSUES_${inboxId.toUpperCase()}_${paramsKey.toUpperCase()}`; }; export const INBOX_ISSUE_DETAILS = (inboxId: string, issueId: string) => diff --git a/apps/app/contexts/inbox-view-context.tsx b/apps/app/contexts/inbox-view-context.tsx new file mode 100644 index 00000000000..ff3bebc726b --- /dev/null +++ b/apps/app/contexts/inbox-view-context.tsx @@ -0,0 +1,163 @@ +import { createContext, useCallback, useEffect, useReducer } from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// components +import ToastAlert from "components/toast-alert"; +// services +import inboxServices from "services/inbox.service"; +// types +import { IInboxFilterOptions } from "types"; +// fetch-keys +import { INBOX_DETAILS } from "constants/fetch-keys"; + +export const inboxViewContext = createContext({} as ContextType); + +type InboxViewProps = { + filters: IInboxFilterOptions; +}; + +type ReducerActionType = { + type: "REHYDRATE_THEME" | "SET_FILTERS"; + payload?: Partial; +}; + +type ContextType = InboxViewProps & { + setFilters: (filters: Partial) => void; +}; + +type StateType = { + filters: IInboxFilterOptions; +}; +type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType; + +export const initialState: StateType = { + filters: { + priority: null, + inbox_status: null, + }, +}; + +export const reducer: ReducerFunctionType = (state, action) => { + const { type, payload } = action; + + switch (type) { + case "REHYDRATE_THEME": { + return { ...initialState, ...payload }; + } + + case "SET_FILTERS": { + const newState = { + ...state, + filters: { + ...state.filters, + ...payload, + }, + }; + + return { + ...state, + ...newState, + }; + } + } +}; + +const saveDataToServer = async ( + workspaceSlug: string, + projectId: string, + inboxId: string, + state: any +) => { + await inboxServices.patchInbox(workspaceSlug, projectId, inboxId, { + view_props: state, + }); +}; + +export const InboxViewContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [state, dispatch] = useReducer(reducer, initialState); + + const router = useRouter(); + const { workspaceSlug, projectId, inboxId } = router.query; + + const { data: inboxDetails, mutate: mutateInboxDetails } = useSWR( + workspaceSlug && projectId && inboxId ? INBOX_DETAILS(inboxId.toString()) : null, + workspaceSlug && projectId && inboxId + ? () => + inboxServices.getInboxById( + workspaceSlug.toString(), + projectId.toString(), + inboxId.toString() + ) + : null + ); + + const setFilters = useCallback( + (property: Partial) => { + Object.keys(property).forEach((key) => { + if (property[key as keyof typeof property]?.length === 0) + property[key as keyof typeof property] = null; + }); + + dispatch({ + type: "SET_FILTERS", + payload: { + filters: { + ...state.filters, + ...property, + }, + }, + }); + + if (!workspaceSlug || !projectId || !inboxId) return; + + const newViewProps = { + ...state, + filters: { + ...state.filters, + ...property, + }, + }; + + mutateInboxDetails((prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + view_props: newViewProps, + }; + }, false); + + saveDataToServer( + workspaceSlug.toString(), + projectId.toString(), + inboxId.toString(), + newViewProps + ); + }, + [workspaceSlug, projectId, inboxId, mutateInboxDetails, state] + ); + + useEffect(() => { + dispatch({ + type: "REHYDRATE_THEME", + payload: { + ...inboxDetails?.view_props, + }, + }); + }, [inboxDetails]); + + return ( + + + {children} + + ); +}; diff --git a/apps/app/contexts/issue-view.context.tsx b/apps/app/contexts/issue-view.context.tsx index 61902786689..6053b8b1572 100644 --- a/apps/app/contexts/issue-view.context.tsx +++ b/apps/app/contexts/issue-view.context.tsx @@ -633,6 +633,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> = mutateModuleDetails, viewId, mutateViewDetails, + user, ] ); diff --git a/apps/app/hooks/use-inbox-view.tsx b/apps/app/hooks/use-inbox-view.tsx new file mode 100644 index 00000000000..e4d5e253461 --- /dev/null +++ b/apps/app/hooks/use-inbox-view.tsx @@ -0,0 +1,51 @@ +import { useContext, useMemo } from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// contexts +import { inboxViewContext } from "contexts/inbox-view-context"; +// services +import inboxServices from "services/inbox.service"; +// types +import { IInboxQueryParams } from "types"; +// fetch-keys +import { INBOX_ISSUES } from "constants/fetch-keys"; + +const useInboxView = () => { + const { filters, setFilters } = useContext(inboxViewContext); + + const router = useRouter(); + const { workspaceSlug, projectId, inboxId } = router.query; + + const params: IInboxQueryParams = { + priority: filters?.priority ? filters?.priority.join(",") : null, + inbox_status: filters?.inbox_status ? filters?.inbox_status.join(",") : null, + }; + + const { data: inboxIssues, mutate: mutateInboxIssues } = useSWR( + workspaceSlug && projectId && inboxId && params + ? INBOX_ISSUES(inboxId.toString(), params) + : null, + workspaceSlug && projectId && inboxId && params + ? () => + inboxServices.getInboxIssues( + workspaceSlug.toString(), + projectId.toString(), + inboxId.toString(), + params + ) + : null + ); + + return { + filters, + setFilters, + params, + issues: inboxIssues, + mutate: mutateInboxIssues, + } as const; +}; + +export default useInboxView; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index 2166d76d10e..dbbb110c862 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -5,101 +5,50 @@ import Router, { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; -// react hook form -import { useForm } from "react-hook-form"; // services import inboxServices from "services/inbox.service"; import projectService from "services/project.service"; +// hooks +import useInboxView from "hooks/use-inbox-view"; +import useUserAuth from "hooks/use-user-auth"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // contexts -import { IssueViewContextProvider } from "contexts/issue-view.context"; +import { InboxViewContextProvider } from "contexts/inbox-view-context"; // components import { InboxIssueCard, InboxActionHeader, InboxMainContent, SelectDuplicateInboxIssueModal, - FiltersDropdown, + DeclineIssueModal, + DeleteIssueModal, } from "components/inbox"; // helper import { truncateText } from "helpers/string.helper"; // ui -import { Loader, PrimaryButton, Spinner } from "components/ui"; +import { Loader, PrimaryButton } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons import { PlusIcon } from "@heroicons/react/24/outline"; +import { InboxIcon } from "components/icons"; // types -import type { IInboxFilterOptions, IInboxIssue } from "types"; import type { NextPage } from "next"; // fetch-keys -import { - INBOX_DETAILS, - INBOX_ISSUES, - INBOX_ISSUE_DETAILS, - ISSUE_DETAILS, - PROJECT_DETAILS, -} from "constants/fetch-keys"; -import useUser from "hooks/use-user"; - -const defaultValues = { - name: "", - description: "", - description_html: "", - estimate_point: null, - state: "", - assignees_list: [], - priority: "low", - blockers_list: [], - blocked_list: [], - target_date: new Date().toString(), - issue_cycle: null, - issue_module: null, - labels_list: [], -}; +import { INBOX_ISSUE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys"; -const ProjectIssues: NextPage = () => { - const [filters, setFilters] = useState>({}); +const ProjectInbox: NextPage = () => { const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false); + const [declineIssueModal, setDeclineIssueModal] = useState(false); + const [deleteIssueModal, setDeleteIssueModal] = useState(false); const router = useRouter(); const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query; - const { user } = useUser(); - - const { reset, control, watch } = useForm({ - defaultValues, - }); - - const params = { - priority: filters?.priority ? filters?.priority.join(",") : undefined, - inbox_status: filters?.inbox_status ? filters?.inbox_status.join(",") : undefined, - }; + const { user } = useUserAuth(); + const { issues: inboxIssues, mutate: mutateInboxIssues } = useInboxView(); - const { data: inboxDetails } = useSWR( - workspaceSlug && projectId && inboxId ? INBOX_DETAILS(inboxId.toString()) : null, - workspaceSlug && projectId && inboxId - ? () => - inboxServices.getInboxById( - workspaceSlug.toString(), - projectId.toString(), - inboxId.toString() - ) - : null - ); - - const { data: inboxIssues, mutate: inboxIssuesMutate } = useSWR( - workspaceSlug && projectId && inboxId ? INBOX_ISSUES(inboxId.toString(), params) : null, - workspaceSlug && projectId && inboxId - ? () => - inboxServices.getInboxIssues( - workspaceSlug as string, - projectId as string, - inboxId as string, - params - ) - : null - ); + console.log("inboxIssues", inboxIssues); const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, @@ -153,27 +102,22 @@ const ProjectIssues: NextPage = () => { }; }, [onKeyDown]); - useEffect(() => { - if (!inboxIssues || inboxIssues.length === 0) return; - - if (!workspaceSlug || !projectId || !inboxId) return; + // // show the first issue by default in the main content + // useEffect(() => { + // if (!workspaceSlug || !projectId || !inboxId) return; - Router.push({ - pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, - query: { - inboxIssueId: inboxIssues[0].bridge_id, - }, - }); - }, [inboxIssues, workspaceSlug, projectId, inboxId]); + // if (!inboxIssues || inboxIssues.length === 0) return; - useEffect(() => { - if (!inboxDetails || filters) return; - - setFilters(inboxDetails.view_props); - }, [inboxDetails, filters]); + // Router.push({ + // pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, + // query: { + // inboxIssueId: inboxIssues[0].bridge_id, + // }, + // }); + // }, [inboxIssues, workspaceSlug, projectId, inboxId]); return ( - + @@ -220,10 +164,9 @@ const ProjectIssues: NextPage = () => { user ) .then(() => { - reset(defaultValues); setSelectDuplicateIssue(false); mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string)); - inboxIssuesMutate((prevData) => + mutateInboxIssues((prevData) => (prevData ?? [])?.map((item) => ({ ...item, status: item.bridge_id === inboxIssueId ? 2 : item.issue_inbox[0].status, @@ -236,10 +179,18 @@ const ProjectIssues: NextPage = () => { }); }} /> + setDeclineIssueModal(false)} + data={inboxIssues?.find((i) => i.bridge_id === inboxIssueId)} + /> + setDeleteIssueModal(false)} + data={inboxIssues?.find((i) => i.bridge_id === inboxIssueId)} + />
issue.bridge_id === inboxIssueId)} currentIssueIndex={ inboxIssues?.findIndex((issue) => issue.bridge_id === inboxIssueId) ?? 0 @@ -260,36 +211,14 @@ const ProjectIssues: NextPage = () => { ) .then(() => { mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string)); - inboxIssuesMutate((prevData) => + mutateInboxIssues((prevData) => prevData?.map((item) => item.bridge_id === inboxIssueId ? { ...item, status: 1 } : item ) ); }); }} - onDecline={() => { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId) - ?.bridge_id!, - { - status: -1, - }, - user - ) - .then(() => { - reset(defaultValues); - mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string)); - inboxIssuesMutate((prevData) => - prevData?.map((item) => - item.bridge_id === inboxIssueId ? { ...item, status: -1 } : item - ) - ); - }); - }} + onDecline={() => setDeclineIssueModal(true)} onMarkAsDuplicate={() => { setSelectDuplicateIssue(true); }} @@ -308,9 +237,8 @@ const ProjectIssues: NextPage = () => { user ) .then(() => { - reset(defaultValues); mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string)); - inboxIssuesMutate((prevData) => + mutateInboxIssues((prevData) => prevData?.map((item) => item.bridge_id === inboxIssueId ? { ...item, status: 0, snoozed_till: new Date(date) } @@ -319,6 +247,7 @@ const ProjectIssues: NextPage = () => { ); }); }} + onDelete={() => setDeleteIssueModal(true)} />
{inboxIssues ? ( @@ -349,47 +278,36 @@ const ProjectIssues: NextPage = () => { )}
- inboxIssue.bridge_id === inboxIssueId) - ?.issue_inbox[0].status - } - onAccept={() => { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId) - ?.id!, - { - status: 1, - }, - user - ) - .then(() => { - reset(defaultValues); - mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string)); - inboxIssuesMutate((prevData) => - (prevData ?? [])?.map((item) => ({ - ...item, - status: - item.bridge_id === inboxIssueId ? 1 : item.issue_inbox[0].status, - })) - ); - }); - }} - /> + {inboxIssueId ? ( + + ) : ( +
+
+
+ + {inboxIssues && inboxIssues.length > 0 ? ( + + {inboxIssues?.length} issues found. Select an issue from the sidebar to + view its details. + + ) : ( + + No issues found. Use{" "} +
C
{" "} + shortcut to create a new issue +
+ )} +
+
+
+ )}
-
+ ); }; -export default ProjectIssues; +export default ProjectInbox; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 5f6615f5529..555c2644a88 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -208,7 +208,7 @@ const IssueDetailsPage: NextPage = () => {

Comments/Activity

- +
diff --git a/apps/app/services/inbox.service.ts b/apps/app/services/inbox.service.ts index 3017add6892..9ee20923ce5 100644 --- a/apps/app/services/inbox.service.ts +++ b/apps/app/services/inbox.service.ts @@ -13,6 +13,8 @@ import type { TInboxStatus, IInboxIssueDetail, ICurrentUserResponse, + IInboxFilterOptions, + IInboxQueryParams, } from "types"; class InboxServices extends APIService { @@ -36,11 +38,27 @@ class InboxServices extends APIService { }); } + async patchInbox( + workspaceSlug: string, + projectId: string, + inboxId: string, + data: Partial + ): Promise { + return this.patch( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async getInboxIssues( workspaceSlug: string, projectId: string, inboxId: string, - params?: any + params?: IInboxQueryParams ): Promise { return this.get( `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`, @@ -67,6 +85,26 @@ class InboxServices extends APIService { }); } + async deleteInboxIssue( + workspaceSlug: string, + projectId: string, + inboxId: string, + inboxIssueId: string, + user: ICurrentUserResponse | undefined + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/` + ) + .then((response) => { + if (trackEvent) + trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_DELETE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + async markInboxStatus( workspaceSlug: string, projectId: string, @@ -96,6 +134,28 @@ class InboxServices extends APIService { }); } + async patchInboxIssue( + workspaceSlug: string, + projectId: string, + inboxId: string, + inboxIssueId: string, + data: { issue: Partial }, + user: ICurrentUserResponse | undefined + ): Promise { + return this.patch( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`, + data + ) + .then((response) => { + if (trackEvent) + trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_UPDATE", user); + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + async createInboxIssue( workspaceSlug: string, projectId: string, diff --git a/apps/app/services/track-event.service.ts b/apps/app/services/track-event.service.ts index c520ff79ec3..00eb4fc97e6 100644 --- a/apps/app/services/track-event.service.ts +++ b/apps/app/services/track-event.service.ts @@ -83,6 +83,8 @@ type InboxEventType = | "INBOX_UPDATE" | "INBOX_DELETE" | "INBOX_ISSUE_CREATE" + | "INBOX_ISSUE_UPDATE" + | "INBOX_ISSUE_DELETE" | "INBOX_ISSUE_DUPLICATED" | "INBOX_ISSUE_ACCEPTED" | "INBOX_ISSUE_SNOOZED" diff --git a/apps/app/types/inbox.d.ts b/apps/app/types/inbox.d.ts index c35e3d7bf96..bc420272ca9 100644 --- a/apps/app/types/inbox.d.ts +++ b/apps/app/types/inbox.d.ts @@ -1,63 +1,22 @@ -import { IIssueFilterOptions, IIssueLabels } from "./issues"; +import { IIssue, IIssueFilterOptions, IIssueLabels } from "./issues"; import type { IProjectLite } from "./projects"; import { IState } from "./state"; import { IUserLite } from "./users"; -export interface IInboxIssue { - assignee_details: IUserLite[]; - assignees: string[]; +export interface IInboxIssue extends Partial { bridge_id: string; - completed_at: string | null; - created_at: Date; - created_by: string; - description: any; - description_html: string; - description_stripped: string; - estimate_point: number | null; - id: string; issue_inbox: { duplicate_to: string | null; snoozed_till: Date | null; source: string; status: -2 | -1 | 0 | 1 | 2; }[]; - label_details: IIssueLabels[]; - labels: string[]; - name: string; - parent: string | null; - priority: string | null; - project: string; - project_detail: IProjectLite; - sequence_id: number; - sort_order: number; - start_date: string | null; - state: string; - state_detail: IState; - sub_issues_count: number; - target_date: string | null; - updated_at: Date; - updated_by: string; - workspace: string; } export interface IInboxIssueDetail { id: string; - issue_detail: { - id: string; - name: string; - description: any; - description_html: string; - priority: string | null; - start_date: string | null; - target_date: string | null; - sequence_id: number; - sort_order: number; - }; - project_detail: { - id: string; - identifier: string; - name: string; - }; + issue_detail: Partial & { bridge_id: string }; + project_detail: IProjectLite; created_at: string; updated_at: string; status: -2 | -1 | 0 | 1 | 2; @@ -83,7 +42,7 @@ export interface IInbox { created_by: string; updated_by: string; project: string; - view_props: IInboxFilterOptions; + view_props: { filters: IInboxFilterOptions }; workspace: string; } @@ -114,3 +73,8 @@ export interface IInboxFilterOptions { priority: string[] | null; inbox_status: number[] | null; } + +export interface IInboxQueryParams { + priority: string | null; + inbox_status: string | null; +} From c8e0a3e187683b95454ec7ba39b030c96bd1f324 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 15 Jun 2023 01:33:13 +0530 Subject: [PATCH 34/51] fix: type errors --- apps/app/components/inbox/inbox-action-headers.tsx | 2 +- apps/app/components/inbox/inbox-main-content.tsx | 8 ++++---- apps/app/types/inbox.d.ts | 2 +- apps/app/types/issues.d.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/app/components/inbox/inbox-action-headers.tsx b/apps/app/components/inbox/inbox-action-headers.tsx index 619ca3ebd58..d72946fb587 100644 --- a/apps/app/components/inbox/inbox-action-headers.tsx +++ b/apps/app/components/inbox/inbox-action-headers.tsx @@ -79,7 +79,7 @@ export const InboxActionHeader: React.FC = (props) => { onSelect={(option) => { const key = option.key as keyof typeof filters; - const valueExists = filters[key]?.includes(option.value); + const valueExists = (filters[key] as any[])?.includes(option.value); if (valueExists) { setFilters({ diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx index 54e802571cb..ffcbbee266f 100644 --- a/apps/app/components/inbox/inbox-main-content.tsx +++ b/apps/app/components/inbox/inbox-main-content.tsx @@ -7,8 +7,9 @@ import useSWR, { mutate } from "swr"; // react hook form import { useForm } from "react-hook-form"; // services -import issuesService from "services/issues.service"; import inboxServices from "services/inbox.service"; +// hooks +import useInboxView from "hooks/use-inbox-view"; // ui import { Loader } from "components/ui"; // hooks @@ -24,8 +25,7 @@ import { } from "components/issues"; // types -import type { IInboxIssue, IInboxIssueDetail } from "types"; -import useInboxView from "hooks/use-inbox-view"; +import type { IInboxIssue, IIssue } from "types"; const defaultValues = { name: "", @@ -45,7 +45,7 @@ export const InboxMainContent: React.FC = () => { const { user } = useUser(); const { params } = useInboxView(); - const { reset, control, watch } = useForm({ + const { reset, control, watch } = useForm({ defaultValues, }); diff --git a/apps/app/types/inbox.d.ts b/apps/app/types/inbox.d.ts index bc420272ca9..295def57ab3 100644 --- a/apps/app/types/inbox.d.ts +++ b/apps/app/types/inbox.d.ts @@ -15,7 +15,7 @@ export interface IInboxIssue extends Partial { export interface IInboxIssueDetail { id: string; - issue_detail: Partial & { bridge_id: string }; + issue_detail: IIssue & { bridge_id: string }; project_detail: IProjectLite; created_at: string; updated_at: string; diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts index e06d81169cc..aead22b576d 100644 --- a/apps/app/types/issues.d.ts +++ b/apps/app/types/issues.d.ts @@ -100,7 +100,9 @@ export interface IIssue { url: string; }[]; issue_module: IIssueModule | null; + labels: string[]; label_details: any[]; + labels_list: string[]; links_list: IIssueLink[]; link_count: number; module: string | null; @@ -123,8 +125,6 @@ export interface IIssue { updated_by: string; workspace: string; workspace_detail: IWorkspaceLite; - labels: any[]; - labels_list: string[]; } export interface ISubIssuesState { From 0edf1ab6a183a62ecb9baf14dd99f666bfe851ce Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 15 Jun 2023 01:43:49 +0530 Subject: [PATCH 35/51] fix: build errors and warnings --- .../app/components/core/board-view/single-issue.tsx | 13 ++++++++++++- .../components/core/calendar-view/single-issue.tsx | 2 +- apps/app/components/core/issues-view.tsx | 1 + apps/app/components/core/list-view/single-issue.tsx | 13 ++++++++++++- apps/app/components/inbox/delete-issue-modal.tsx | 2 +- apps/app/components/issues/delete-issue-modal.tsx | 10 ++-------- apps/app/components/issues/my-issues-list-item.tsx | 2 +- .../components/pages/create-update-block-inline.tsx | 4 ++-- 8 files changed, 32 insertions(+), 15 deletions(-) diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx index e2d530153f9..072a5113a5e 100644 --- a/apps/app/components/core/board-view/single-issue.tsx +++ b/apps/app/components/core/board-view/single-issue.tsx @@ -186,7 +186,18 @@ export const SingleBoardIssue: React.FC = ({ console.log(error); }); }, - [workspaceSlug, projectId, cycleId, moduleId, groupTitle, index, selectedGroup, orderBy, params] + [ + workspaceSlug, + projectId, + cycleId, + moduleId, + groupTitle, + index, + selectedGroup, + orderBy, + params, + user, + ] ); const getStyle = ( diff --git a/apps/app/components/core/calendar-view/single-issue.tsx b/apps/app/components/core/calendar-view/single-issue.tsx index 4fa9def3b86..12fa60d018d 100644 --- a/apps/app/components/core/calendar-view/single-issue.tsx +++ b/apps/app/components/core/calendar-view/single-issue.tsx @@ -105,7 +105,7 @@ export const SingleCalendarIssue: React.FC = ({ console.log(error); }); }, - [workspaceSlug, projectId, cycleId, moduleId, params] + [workspaceSlug, projectId, cycleId, moduleId, viewId, params, user] ); const handleCopyText = () => { diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx index bfa6b9f4644..2e9e14ae3cc 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/issues-view.tsx @@ -276,6 +276,7 @@ export const IssuesView: React.FC = ({ handleDeleteIssue, params, states, + user, ] ); diff --git a/apps/app/components/core/list-view/single-issue.tsx b/apps/app/components/core/list-view/single-issue.tsx index ea4ebc811ea..f2a5878999d 100644 --- a/apps/app/components/core/list-view/single-issue.tsx +++ b/apps/app/components/core/list-view/single-issue.tsx @@ -154,7 +154,18 @@ export const SingleListIssue: React.FC = ({ } else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params)); }); }, - [workspaceSlug, projectId, cycleId, moduleId, groupTitle, index, selectedGroup, orderBy, params] + [ + workspaceSlug, + projectId, + cycleId, + moduleId, + groupTitle, + index, + selectedGroup, + orderBy, + params, + user, + ] ); const handleCopyText = () => { diff --git a/apps/app/components/inbox/delete-issue-modal.tsx b/apps/app/components/inbox/delete-issue-modal.tsx index 3261cd8a868..68edde77658 100644 --- a/apps/app/components/inbox/delete-issue-modal.tsx +++ b/apps/app/components/inbox/delete-issue-modal.tsx @@ -123,7 +123,7 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data })

Are you sure you want to delete issue{" "} - {data?.project_detail.identifier}-{data?.sequence_id} + {data?.project_detail?.identifier}-{data?.sequence_id} {""}? This action cannot be undone.

diff --git a/apps/app/components/issues/delete-issue-modal.tsx b/apps/app/components/issues/delete-issue-modal.tsx index 18eb76e6643..d0ef4b6e951 100644 --- a/apps/app/components/issues/delete-issue-modal.tsx +++ b/apps/app/components/issues/delete-issue-modal.tsx @@ -17,11 +17,10 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // ui import { SecondaryButton, DangerButton } from "components/ui"; // types -import type { IInboxIssue, IIssue, ICurrentUserResponse } from "types"; +import type { IIssue, ICurrentUserResponse } from "types"; // fetch-keys import { CYCLE_ISSUES_WITH_PARAMS, - INBOX_ISSUES, MODULE_ISSUES_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS, VIEW_ISSUES, @@ -38,7 +37,7 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, u const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId, viewId, inboxId } = router.query; + const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { issueView, params } = useIssuesView(); const { params: calendarParams } = useCalendarIssuesView(); @@ -81,11 +80,6 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, u else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params)); } - if (inboxId) - mutate(INBOX_ISSUES(inboxId.toString()), (prevData) => - prevData?.filter((issue) => issue.issue !== data.id) - ); - handleClose(); setToastAlert({ title: "Success", diff --git a/apps/app/components/issues/my-issues-list-item.tsx b/apps/app/components/issues/my-issues-list-item.tsx index e026b2f8911..5cff38478e2 100644 --- a/apps/app/components/issues/my-issues-list-item.tsx +++ b/apps/app/components/issues/my-issues-list-item.tsx @@ -67,7 +67,7 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId console.log(error); }); }, - [workspaceSlug, projectId] + [workspaceSlug, projectId, user] ); const handleCopyText = () => { diff --git a/apps/app/components/pages/create-update-block-inline.tsx b/apps/app/components/pages/create-update-block-inline.tsx index 27e6bd419c5..91c1108e469 100644 --- a/apps/app/components/pages/create-update-block-inline.tsx +++ b/apps/app/components/pages/create-update-block-inline.tsx @@ -126,7 +126,7 @@ export const CreateUpdateBlockInline: React.FC = ({ }) .finally(() => onClose()); }, - [workspaceSlug, projectId, pageId, onClose, setToastAlert] + [workspaceSlug, projectId, pageId, onClose, setToastAlert, user] ); const updatePageBlock = useCallback( @@ -181,7 +181,7 @@ export const CreateUpdateBlockInline: React.FC = ({ }) .finally(() => onClose()); }, - [workspaceSlug, projectId, pageId, data, onClose, setIsSyncing] + [workspaceSlug, projectId, pageId, data, onClose, setIsSyncing, user] ); const handleAutoGenerateDescription = async () => { From 21476a60bfd74b03d703a025f541b17f1547ccc3 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 15 Jun 2023 12:10:45 +0530 Subject: [PATCH 36/51] dev: update issue inbox workflow and log all the changes --- apiserver/plane/api/views/inbox.py | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index c77e2d2321c..c6121b429fd 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -1,6 +1,10 @@ +# Python imports +import json + # Django import from django.utils import timezone from django.db.models import Q, Count, OuterRef, Func, F, Prefetch +from django.core.serializers.json import DjangoJSONEncoder # Third party imports from rest_framework import status @@ -18,14 +22,17 @@ State, IssueLink, IssueAttachment, + IssueActivity, ) from plane.api.serializers import ( + IssueSerializer, InboxSerializer, InboxIssueSerializer, IssueCreateSerializer, IssueStateInboxSerializer, ) from plane.utils.issue_filters import issue_filters +from plane.bgtasks.issue_activites_task import issue_activity class InboxViewSet(BaseViewSet): @@ -205,6 +212,16 @@ def create(self, request, slug, project_id, inbox_id): state=state, ) + # Create an Issue Activity + # Track the issue + issue_activity.delay( + type="issue.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=None, + ) # create an inbox issue InboxIssue.objects.create( inbox_id=inbox_id, @@ -238,6 +255,20 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): if issue_serializer.is_valid(): issue_serializer.save() + # Log all the updates + requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder) + current_instance = issue + if current_instance is not None: + issue_activity.delay( + type="issue.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=json.dumps( + (current_instance).data, cls=DjangoJSONEncoder + ), + ) else: return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -271,3 +302,16 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + def retrieve(self, request, slug, project_id, inbox_id, pk): + try: + inbox_issue = InboxIssue.objects.get(pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id) + issue = Issue.objects.get(pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id) + serializer = IssueStateInboxSerializer(issue) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) \ No newline at end of file From 426e5abbe685022511b5a77adbe854f4b947fd95 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 15 Jun 2023 14:05:14 +0530 Subject: [PATCH 37/51] fix: filters logic, sidebar fields to show --- .../app/components/inbox/inbox-issue-card.tsx | 108 ++++++++++-------- .../components/inbox/inbox-main-content.tsx | 25 ++-- apps/app/components/inbox/index.ts | 5 +- .../components/inbox/issues-list-sidebar.tsx | 44 +++++++ apps/app/components/issues/sidebar.tsx | 61 +++++----- apps/app/constants/fetch-keys.ts | 2 - apps/app/hooks/use-inbox-view.tsx | 2 +- .../projects/[projectId]/inbox/[inboxId].tsx | 31 +---- apps/app/types/inbox.d.ts | 16 +-- apps/app/types/issues.d.ts | 4 +- 10 files changed, 158 insertions(+), 140 deletions(-) create mode 100644 apps/app/components/inbox/issues-list-sidebar.tsx diff --git a/apps/app/components/inbox/inbox-issue-card.tsx b/apps/app/components/inbox/inbox-issue-card.tsx index 6da7b260722..320af8b068a 100644 --- a/apps/app/components/inbox/inbox-issue-card.tsx +++ b/apps/app/components/inbox/inbox-issue-card.tsx @@ -1,3 +1,5 @@ +import { useRouter } from "next/router"; + // ui import { Tooltip } from "components/ui"; // icons @@ -7,6 +9,7 @@ import { CalendarDaysIcon, ClockIcon } from "@heroicons/react/24/outline"; import { renderShortNumericDateFormat } from "helpers/date-time.helper"; // types import type { IInboxIssue } from "types"; +import Link from "next/link"; type Props = { issue: IInboxIssue; @@ -16,6 +19,9 @@ type Props = { export const InboxIssueCard: React.FC = (props) => { const { issue, active } = props; + const router = useRouter(); + const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query; + const issueStatus = issue.issue_inbox[0].status; return ( @@ -32,58 +38,64 @@ export const InboxIssueCard: React.FC = (props) => { : "Marked as duplicate" } > -
-
-

- {issue.project_detail?.identifier}-{issue.sequence_id} -

-
{issue.name}
-
-
- -
- {getPriorityIcon( - issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None", - "text-sm" - )} -
-
- +
-
- - {renderShortNumericDateFormat(issue.created_at ?? "")} +
+

+ {issue.project_detail?.identifier}-{issue.sequence_id} +

+
{issue.name}
- - {issue.issue_inbox[0].snoozed_till && ( -
- - - Snoozed till {renderShortNumericDateFormat(issue.issue_inbox[0].snoozed_till)} - +
+ +
+ {getPriorityIcon( + issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None", + "text-sm" + )} +
+
+ +
+ + {renderShortNumericDateFormat(issue.created_at ?? "")} +
+
+ {issue.issue_inbox[0].snoozed_till && ( +
+ + + Snoozed till {renderShortNumericDateFormat(issue.issue_inbox[0].snoozed_till)} + +
+ )}
- )} -
-
+
+ +
); }; diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx index ffcbbee266f..114fcecc473 100644 --- a/apps/app/components/inbox/inbox-main-content.tsx +++ b/apps/app/components/inbox/inbox-main-content.tsx @@ -68,7 +68,7 @@ export const InboxMainContent: React.FC = () => { if (!issueDetails || !inboxIssueId) return; reset({ - ...issueDetails.issue_detail, + ...issueDetails, }); }, [issueDetails, reset, inboxIssueId]); @@ -82,7 +82,7 @@ export const InboxMainContent: React.FC = () => { return { ...prevData, issue_detail: { - ...prevData.issue_detail, + ...prevData, ...formData, }, }; @@ -110,7 +110,7 @@ export const InboxMainContent: React.FC = () => { workspaceSlug.toString(), projectId.toString(), inboxId.toString(), - issueDetails.id, + issueDetails.issue_inbox[0].id, payload, user ) @@ -139,9 +139,9 @@ export const InboxMainContent: React.FC = () => {
@@ -156,19 +156,10 @@ export const InboxMainContent: React.FC = () => {
diff --git a/apps/app/components/inbox/index.ts b/apps/app/components/inbox/index.ts index e2194768870..305beadbb99 100644 --- a/apps/app/components/inbox/index.ts +++ b/apps/app/components/inbox/index.ts @@ -1,7 +1,8 @@ export * from "./decline-issue-modal"; export * from "./delete-issue-modal"; export * from "./filters-dropdown"; -export * from "./select-duplicate"; -export * from "./inbox-issue-card"; export * from "./inbox-action-headers"; +export * from "./inbox-issue-card"; export * from "./inbox-main-content"; +export * from "./issues-list-sidebar.tsx"; +export * from "./select-duplicate"; diff --git a/apps/app/components/inbox/issues-list-sidebar.tsx b/apps/app/components/inbox/issues-list-sidebar.tsx new file mode 100644 index 00000000000..02181c02a4e --- /dev/null +++ b/apps/app/components/inbox/issues-list-sidebar.tsx @@ -0,0 +1,44 @@ +import { useRouter } from "next/router"; + +// hooks +import useInboxView from "hooks/use-inbox-view"; +// components +import { InboxIssueCard } from "components/inbox"; +// ui +import { Loader } from "components/ui"; + +export const IssuesListSidebar = () => { + const router = useRouter(); + const { inboxIssueId } = router.query; + + const { issues: inboxIssues } = useInboxView(); + + return ( + <> + {inboxIssues ? ( + inboxIssues.length > 0 ? ( +
+ {inboxIssues.map((issue) => ( + + ))} +
+ ) : ( +
+ No issues found for the selected filters. Try changing the filters. +
+ ) + ) : ( + + + + + + + )} + + ); +}; diff --git a/apps/app/components/issues/sidebar.tsx b/apps/app/components/issues/sidebar.tsx index 1a4964df2b3..3dc3a8e9de9 100644 --- a/apps/app/components/issues/sidebar.tsx +++ b/apps/app/components/issues/sidebar.tsx @@ -57,23 +57,22 @@ type Props = { submitChanges: (formData: any) => void; issueDetail: IIssue | undefined; watch: UseFormWatch; - fieldsToShow?: - | { - state?: boolean; - assignee?: boolean; - priority?: boolean; - estimate?: boolean; - parent?: boolean; - blocker?: boolean; - blocked?: boolean; - dueDate?: boolean; - cycle?: boolean; - module?: boolean; - label?: boolean; - link?: boolean; - delete?: boolean; - } - | "all"; + fieldsToShow?: ( + | "state" + | "assignee" + | "priority" + | "estimate" + | "parent" + | "blocker" + | "blocked" + | "dueDate" + | "cycle" + | "module" + | "label" + | "link" + | "delete" + | "all" + )[]; }; const defaultValues: Partial = { @@ -86,7 +85,7 @@ export const IssueDetailsSidebar: React.FC = ({ submitChanges, issueDetail, watch: watchIssue, - fieldsToShow = "all", + fieldsToShow = ["all"], }) => { const [createLabelForm, setCreateLabelForm] = useState(false); const [deleteIssueModal, setDeleteIssueModal] = useState(false); @@ -276,7 +275,7 @@ export const IssueDetailsSidebar: React.FC = ({ > - {!isNotAllowed && (fieldsToShow === "all" || fieldsToShow.delete) && ( + {!isNotAllowed && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && (
- {(fieldsToShow === "all" || fieldsToShow.state) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && ( = ({ )} /> )} - {(fieldsToShow === "all" || fieldsToShow.assignee) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && ( = ({ )} /> )} - {(fieldsToShow === "all" || fieldsToShow.priority) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && ( = ({ )} /> )} - {(fieldsToShow === "all" || fieldsToShow.estimate) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && ( = ({ )}
- {(fieldsToShow === "all" || fieldsToShow.parent) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( = ({ userAuth={memberRole} /> )} - {(fieldsToShow === "all" || fieldsToShow.blocker) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && ( i.id !== issueDetail?.id) ?? []} @@ -383,7 +382,7 @@ export const IssueDetailsSidebar: React.FC = ({ userAuth={memberRole} /> )} - {(fieldsToShow === "all" || fieldsToShow.blocked) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && ( i.id !== issueDetail?.id) ?? []} @@ -391,7 +390,7 @@ export const IssueDetailsSidebar: React.FC = ({ userAuth={memberRole} /> )} - {(fieldsToShow === "all" || fieldsToShow.dueDate) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
@@ -420,14 +419,14 @@ export const IssueDetailsSidebar: React.FC = ({ )}
- {(fieldsToShow === "all" || fieldsToShow.cycle) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("cycle")) && ( )} - {(fieldsToShow === "all" || fieldsToShow.module) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && ( = ({ )}
- {(fieldsToShow === "all" || fieldsToShow.label) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
@@ -685,7 +684,7 @@ export const IssueDetailsSidebar: React.FC = ({ )}
)} - {(fieldsToShow === "all" || fieldsToShow.link) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (

Links

diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 93b533bf024..5625b710db7 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -145,8 +145,6 @@ export const INBOX_ISSUES = (inboxId: string, params?: any) => { const paramsKey = inboxParamsToKey(params); - console.log("triggered 2"); - return `INBOX_ISSUES_${inboxId.toUpperCase()}_${paramsKey.toUpperCase()}`; }; export const INBOX_ISSUE_DETAILS = (inboxId: string, issueId: string) => diff --git a/apps/app/hooks/use-inbox-view.tsx b/apps/app/hooks/use-inbox-view.tsx index e4d5e253461..67504ce5491 100644 --- a/apps/app/hooks/use-inbox-view.tsx +++ b/apps/app/hooks/use-inbox-view.tsx @@ -1,4 +1,4 @@ -import { useContext, useMemo } from "react"; +import { useContext, useEffect, useMemo, useState } from "react"; import { useRouter } from "next/router"; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index dbbb110c862..d8b26540486 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -23,6 +23,7 @@ import { SelectDuplicateInboxIssueModal, DeclineIssueModal, DeleteIssueModal, + IssuesListSidebar, } from "components/inbox"; // helper import { truncateText } from "helpers/string.helper"; @@ -48,8 +49,6 @@ const ProjectInbox: NextPage = () => { const { user } = useUserAuth(); const { issues: inboxIssues, mutate: mutateInboxIssues } = useInboxView(); - console.log("inboxIssues", inboxIssues); - const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId @@ -250,33 +249,7 @@ const ProjectInbox: NextPage = () => { onDelete={() => setDeleteIssueModal(true)} />
- {inboxIssues ? ( - inboxIssues.length > 0 ? ( -
- {inboxIssues.map((issue) => ( - - - - - - ))} -
- ) : ( -
- No issues found for the selected filters. Try changing the filters. -
- ) - ) : ( - - - - - - - )} +
{inboxIssueId ? ( diff --git a/apps/app/types/inbox.d.ts b/apps/app/types/inbox.d.ts index 295def57ab3..1101d540f04 100644 --- a/apps/app/types/inbox.d.ts +++ b/apps/app/types/inbox.d.ts @@ -13,22 +13,22 @@ export interface IInboxIssue extends Partial { }[]; } -export interface IInboxIssueDetail { +export interface IInboxIssueDetail extends IIssue { id: string; - issue_detail: IIssue & { bridge_id: string }; project_detail: IProjectLite; created_at: string; updated_at: string; - status: -2 | -1 | 0 | 1 | 2; - snoozed_till: string | null; - source: string | null; + issue_inbox: { + duplicate_to: string | null; + id: string; + snoozed_till: Date | null; + source: string; + status: -2 | -1 | 0 | 1 | 2; + }[]; created_by: string; updated_by: string; project: string; workspace: string; - inbox: string; - issue: string; - duplicate_to: string | null; } export interface IInbox { id: string; diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts index aead22b576d..0ac0207be01 100644 --- a/apps/app/types/issues.d.ts +++ b/apps/app/types/issues.d.ts @@ -79,7 +79,7 @@ export interface IIssue { blocks_list: string[]; bridge_id?: string | null; completed_at: Date; - created_at: Date; + created_at: string; created_by: string; cycle: string | null; cycle_id: string | null; @@ -121,7 +121,7 @@ export interface IIssue { state_detail: IState; sub_issues_count: number; target_date: string | null; - updated_at: Date; + updated_at: string; updated_by: string; workspace: string; workspace_detail: IWorkspaceLite; From 8c78c505807d70a4318a346c483a534409728b82 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 15 Jun 2023 16:23:16 +0530 Subject: [PATCH 38/51] dev: update issue filtering status --- apiserver/plane/api/views/analytic.py | 8 ++++---- apiserver/plane/api/views/issue.py | 10 +++++----- apiserver/plane/api/views/people.py | 4 ++-- apiserver/plane/api/views/search.py | 4 ++-- apiserver/plane/api/views/view.py | 2 +- apiserver/plane/api/views/workspace.py | 18 +++++++++--------- .../plane/bgtasks/analytic_plot_export.py | 4 ++-- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/apiserver/plane/api/views/analytic.py b/apiserver/plane/api/views/analytic.py index 56ca12baef2..a096c2700d3 100644 --- a/apiserver/plane/api/views/analytic.py +++ b/apiserver/plane/api/views/analytic.py @@ -40,7 +40,7 @@ def get(self, request, slug): segment = request.GET.get("segment", False) filters = issue_filters(request.GET, "GET") - queryset = Issue.objects.filter(workspace__slug=slug, **filters) + queryset = Issue.issue_objects.filter(workspace__slug=slug, **filters) total_issues = queryset.count() distribution = build_graph_plot( @@ -79,7 +79,7 @@ def get(self, request, slug): assignee_details = {} if x_axis in ["assignees__email"] or segment in ["assignees__email"]: assignee_details = ( - Issue.objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False) + Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False) .order_by("assignees__id") .distinct("assignees__id") .values("assignees__avatar", "assignees__email", "assignees__first_name", "assignees__last_name") @@ -132,7 +132,7 @@ def get(self, request, slug, analytic_id): ) filter = analytic_view.query - queryset = Issue.objects.filter(**filter) + queryset = Issue.issue_objects.filter(**filter) x_axis = analytic_view.query_dict.get("x_axis", False) y_axis = analytic_view.query_dict.get("y_axis", False) @@ -209,7 +209,7 @@ def get(self, request, slug): try: filters = issue_filters(request.GET, "GET") - queryset = Issue.objects.filter(workspace__slug=slug, **filters) + queryset = Issue.issue_objects.filter(workspace__slug=slug, **filters) total_issues = queryset.count() diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 8afa2458c88..794fecf5c1a 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -133,7 +133,7 @@ def perform_destroy(self, instance): def get_queryset(self): return ( Issue.issue_objects.annotate( - sub_issues_count=Issue.objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -263,7 +263,7 @@ def get(self, request, slug): issues = ( Issue.issue_objects.filter(assignees__in=[request.user], workspace__slug=slug) .annotate( - sub_issues_count=Issue.objects.filter(parent=OuterRef("id")) + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -579,7 +579,7 @@ def delete(self, request, slug, project_id): status=status.HTTP_400_BAD_REQUEST, ) - issues = Issue.objects.filter( + issues = Issue.issue_objects.filter( workspace__slug=slug, project_id=project_id, pk__in=issue_ids ) @@ -654,7 +654,7 @@ def get(self, request, slug, project_id, issue_id): # Assign multiple sub issues def post(self, request, slug, project_id, issue_id): try: - parent_issue = Issue.objects.get(pk=issue_id) + parent_issue = Issue.issue_objects.get(pk=issue_id) sub_issue_ids = request.data.get("sub_issue_ids", []) if not len(sub_issue_ids): @@ -670,7 +670,7 @@ def post(self, request, slug, project_id, issue_id): _ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10) - updated_sub_issues = Issue.objects.filter(id__in=sub_issue_ids) + updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids) return Response( IssueFlatSerializer(updated_sub_issues, many=True).data, diff --git a/apiserver/plane/api/views/people.py b/apiserver/plane/api/views/people.py index fcf95ff6474..9dad5380bda 100644 --- a/apiserver/plane/api/views/people.py +++ b/apiserver/plane/api/views/people.py @@ -37,7 +37,7 @@ def retrieve(self, request): workspace_invites = WorkspaceMemberInvite.objects.filter( email=request.user.email ).count() - assigned_issues = Issue.objects.filter(assignees__in=[request.user]).count() + assigned_issues = Issue.issue_objects.filter(assignees__in=[request.user]).count() serialized_data = UserSerializer(request.user).data serialized_data["workspace"] = { @@ -59,7 +59,7 @@ def retrieve(self, request): workspace_invites = WorkspaceMemberInvite.objects.filter( email=request.user.email ).count() - assigned_issues = Issue.objects.filter(assignees__in=[request.user]).count() + assigned_issues = Issue.issue_objects.filter(assignees__in=[request.user]).count() fallback_workspace = Workspace.objects.filter( workspace_member__member=request.user diff --git a/apiserver/plane/api/views/search.py b/apiserver/plane/api/views/search.py index f2d03380fda..078a9a6a5e2 100644 --- a/apiserver/plane/api/views/search.py +++ b/apiserver/plane/api/views/search.py @@ -210,7 +210,7 @@ def get(self, request, slug, project_id): blocker_blocked_by = request.query_params.get("blocker_blocked_by", False) issue_id = request.query_params.get("issue_id", False) - issues = Issue.objects.filter( + issues = Issue.issue_objects.filter( workspace__slug=slug, project_id=project_id, project__project_projectmember__member=self.request.user, @@ -229,7 +229,7 @@ def get(self, request, slug, project_id): ) ) if blocker_blocked_by == "true" and issue_id: - issue = Issue.objects.get(pk=issue_id) + issue = Issue.issue_objects.get(pk=issue_id) issues = issues.filter( ~Q(pk=issue_id), ~Q(blocked_issues__block=issue), diff --git a/apiserver/plane/api/views/view.py b/apiserver/plane/api/views/view.py index 1b6fb42ccb6..874bb94fb16 100644 --- a/apiserver/plane/api/views/view.py +++ b/apiserver/plane/api/views/view.py @@ -67,7 +67,7 @@ def get(self, request, slug, project_id, view_id): filters = issue_filters(request.query_params, "GET") issues = ( - Issue.objects.filter( + Issue.issue_objects.filter( **queries, project_id=project_id, workspace__slug=slug ) .filter(**filters) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 2f3fcb55835..7fb98b60bc3 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -744,7 +744,7 @@ def get(self, request, slug): month = request.GET.get("month", 1) issues = ( - Issue.objects.filter( + Issue.issue_objects.filter( assignees__in=[request.user], workspace__slug=slug, completed_at__month=month, @@ -789,7 +789,7 @@ def get(self, request, slug): month = request.GET.get("month", 1) completed_issues = ( - Issue.objects.filter( + Issue.issue_objects.filter( assignees__in=[request.user], workspace__slug=slug, completed_at__month=month, @@ -802,24 +802,24 @@ def get(self, request, slug): .order_by("week_in_month") ) - assigned_issues = Issue.objects.filter( + assigned_issues = Issue.issue_objects.filter( workspace__slug=slug, assignees__in=[request.user] ).count() - pending_issues_count = Issue.objects.filter( + pending_issues_count = Issue.issue_objects.filter( ~Q(state__group__in=["completed", "cancelled"]), workspace__slug=slug, assignees__in=[request.user], ).count() - completed_issues_count = Issue.objects.filter( + completed_issues_count = Issue.issue_objects.filter( workspace__slug=slug, assignees__in=[request.user], state__group="completed", ).count() issues_due_week = ( - Issue.objects.filter( + Issue.issue_objects.filter( workspace__slug=slug, assignees__in=[request.user], ) @@ -829,14 +829,14 @@ def get(self, request, slug): ) state_distribution = ( - Issue.objects.filter(workspace__slug=slug, assignees__in=[request.user]) + Issue.issue_objects.filter(workspace__slug=slug, assignees__in=[request.user]) .annotate(state_group=F("state__group")) .values("state_group") .annotate(state_count=Count("state_group")) .order_by("state_group") ) - overdue_issues = Issue.objects.filter( + overdue_issues = Issue.issue_objects.filter( ~Q(state__group__in=["completed", "cancelled"]), workspace__slug=slug, assignees__in=[request.user], @@ -844,7 +844,7 @@ def get(self, request, slug): completed_at__isnull=True, ).values("id", "name", "workspace__slug", "project_id", "target_date") - upcoming_issues = Issue.objects.filter( + upcoming_issues = Issue.issue_objects.filter( ~Q(state__group__in=["completed", "cancelled"]), target_date__gte=timezone.now(), workspace__slug=slug, diff --git a/apiserver/plane/bgtasks/analytic_plot_export.py b/apiserver/plane/bgtasks/analytic_plot_export.py index 7f276be82c7..37362416f8e 100644 --- a/apiserver/plane/bgtasks/analytic_plot_export.py +++ b/apiserver/plane/bgtasks/analytic_plot_export.py @@ -36,7 +36,7 @@ def analytic_export_task(email, data, slug): try: filters = issue_filters(data, "POST") - queryset = Issue.objects.filter(**filters, workspace__slug=slug) + queryset = Issue.issue_objects.filter(**filters, workspace__slug=slug) x_axis = data.get("x_axis", False) y_axis = data.get("y_axis", False) @@ -53,7 +53,7 @@ def analytic_export_task(email, data, slug): assignee_details = {} if x_axis in ["assignees__email"] or segment in ["assignees__email"]: assignee_details = ( - Issue.objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False) + Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False) .order_by("assignees__id") .distinct("assignees__id") .values("assignees__avatar", "assignees__email", "assignees__first_name", "assignees__last_name") From 8b6b5234934bef213964318127f18c722aa65b03 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 15 Jun 2023 16:33:08 +0530 Subject: [PATCH 39/51] chore: update create inbox issue modal, fix: mutation issues --- .../command-palette/command-pallette.tsx | 5 +- .../components/inbox/inbox-action-headers.tsx | 19 +- .../app/components/inbox/inbox-issue-card.tsx | 60 ++- .../components/inbox/inbox-main-content.tsx | 10 +- apps/app/components/inbox/index.ts | 2 +- apps/app/components/issues/form.tsx | 456 ++++++++++-------- apps/app/components/issues/modal.tsx | 39 +- apps/app/constants/inbox.ts | 2 + apps/app/hooks/use-inbox-view.tsx | 11 +- .../projects/[projectId]/inbox/[inboxId].tsx | 8 +- .../projects/[projectId]/issues/[issueId].tsx | 2 +- .../projects/[projectId]/issues/index.tsx | 2 +- 12 files changed, 356 insertions(+), 260 deletions(-) diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx index ff889898e9d..ffbe67ca598 100644 --- a/apps/app/components/command-palette/command-pallette.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -102,7 +102,7 @@ export const CommandPalette: React.FC = () => { const page = pages[pages.length - 1]; const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; + const { workspaceSlug, projectId, issueId, inboxId } = router.query; const { user } = useUser(); const { setToastAlert } = useToast(); @@ -145,7 +145,7 @@ export const CommandPalette: React.FC = () => { console.error(e); }); }, - [workspaceSlug, issueId, projectId] + [workspaceSlug, issueId, projectId, user] ); const handleIssueAssignees = (assignee: string) => { @@ -372,6 +372,7 @@ export const CommandPalette: React.FC = () => { setIsIssueModalOpen(false)} + fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]} /> = (props) => { const { inboxIssueId } = router.query; const { memberRole } = useProjectMyMembership(); - const { filters, setFilters } = useInboxView(); + const { filters, setFilters, filtersLength } = useInboxView(); useEffect(() => { if (!issue?.issue_inbox[0].snoozed_till) return; @@ -73,7 +74,7 @@ export const InboxActionHeader: React.FC = (props) => {

Inbox

-
+
{ @@ -96,6 +97,11 @@ export const InboxActionHeader: React.FC = (props) => { direction="right" height="rg" /> + {filtersLength > 0 && ( +
+ {filtersLength} +
+ )}
{inboxIssueId && ( @@ -194,12 +200,7 @@ export const InboxActionHeader: React.FC = (props) => {
- + Delete diff --git a/apps/app/components/inbox/inbox-issue-card.tsx b/apps/app/components/inbox/inbox-issue-card.tsx index 320af8b068a..65efcfb3f8c 100644 --- a/apps/app/components/inbox/inbox-issue-card.tsx +++ b/apps/app/components/inbox/inbox-issue-card.tsx @@ -1,15 +1,16 @@ import { useRouter } from "next/router"; +import Link from "next/link"; // ui import { Tooltip } from "components/ui"; // icons -import { getPriorityIcon } from "components/icons"; +import { getPriorityIcon, getStateGroupIcon } from "components/icons"; import { CalendarDaysIcon, ClockIcon } from "@heroicons/react/24/outline"; // helpers import { renderShortNumericDateFormat } from "helpers/date-time.helper"; +import { addSpaceIfCamelCase } from "helpers/string.helper"; // types import type { IInboxIssue } from "types"; -import Link from "next/link"; type Props = { issue: IInboxIssue; @@ -20,28 +21,29 @@ export const InboxIssueCard: React.FC = (props) => { const { issue, active } = props; const router = useRouter(); - const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query; + const { workspaceSlug, projectId, inboxId } = router.query; const issueStatus = issue.issue_inbox[0].status; return ( - - - + +
= (props) => {
{issue.name}
+ +
+ {getStateGroupIcon( + issue.state_detail?.group ?? "backlog", + "14", + "14", + issue.state_detail?.color + )} + {issue.state_detail?.name ?? "Triage"} +
+
= (props) => { )}
-
- -
+ + + ); }; diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx index 114fcecc473..78f479f3a0a 100644 --- a/apps/app/components/inbox/inbox-main-content.tsx +++ b/apps/app/components/inbox/inbox-main-content.tsx @@ -69,6 +69,9 @@ export const InboxMainContent: React.FC = () => { reset({ ...issueDetails, + assignees_list: + issueDetails.assignees_list ?? (issueDetails.assignee_details ?? []).map((user) => user.id), + labels_list: issueDetails.labels_list ?? issueDetails.labels, }); }, [issueDetails, reset, inboxIssueId]); @@ -76,15 +79,12 @@ export const InboxMainContent: React.FC = () => { async (formData: Partial) => { if (!workspaceSlug || !projectId || !inboxIssueId || !inboxId || !issueDetails) return; - mutateIssueDetails((prevData) => { + mutateIssueDetails((prevData: any) => { if (!prevData) return prevData; return { ...prevData, - issue_detail: { - ...prevData, - ...formData, - }, + ...formData, }; }, false); mutate( diff --git a/apps/app/components/inbox/index.ts b/apps/app/components/inbox/index.ts index 305beadbb99..3f66790bcd7 100644 --- a/apps/app/components/inbox/index.ts +++ b/apps/app/components/inbox/index.ts @@ -4,5 +4,5 @@ export * from "./filters-dropdown"; export * from "./inbox-action-headers"; export * from "./inbox-issue-card"; export * from "./inbox-main-content"; -export * from "./issues-list-sidebar.tsx"; +export * from "./issues-list-sidebar"; export * from "./select-duplicate"; diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx index 894830bd78b..e59e9c2cb7e 100644 --- a/apps/app/components/issues/form.tsx +++ b/apps/app/components/issues/form.tsx @@ -92,6 +92,19 @@ export interface IssueFormProps { handleClose: () => void; status: boolean; user: ICurrentUserResponse | undefined; + fieldsToShow: ( + | "project" + | "name" + | "description" + | "state" + | "priority" + | "assignee" + | "label" + | "dueDate" + | "estimate" + | "parent" + | "all" + )[]; } export const IssueForm: FC = ({ @@ -105,6 +118,7 @@ export const IssueForm: FC = ({ handleClose, status, user, + fieldsToShow, }) => { // states const [mostSimilarIssue, setMostSimilarIssue] = useState(); @@ -252,243 +266,271 @@ export const IssueForm: FC = ({
- ( - - )} - /> + {(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && ( + ( + + )} + /> + )}

{status ? "Update" : "Create"} Issue

- {watch("parent") && watch("parent") !== "" ? ( -
-
- i.id === watch("parent"))?.state_detail - .color, - }} - /> - - {/* {projects?.find((p) => p.id === projectId)?.identifier}- */} - {issues.find((i) => i.id === watch("parent"))?.sequence_id} - - - {issues.find((i) => i.id === watch("parent"))?.name.substring(0, 50)} - - setValue("parent", null)} - /> + {watch("parent") && + watch("parent") !== "" && + (fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( +
+
+ i.id === watch("parent"))?.state_detail + .color, + }} + /> + + {/* {projects?.find((p) => p.id === projectId)?.identifier}- */} + {issues.find((i) => i.id === watch("parent"))?.sequence_id} + + + {issues.find((i) => i.id === watch("parent"))?.name.substring(0, 50)} + + setValue("parent", null)} + /> +
-
- ) : null} + )}
-
- - {mostSimilarIssue && ( - + )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && ( +
+
+ {issueName && issueName !== "" && ( + + )}
- )} -
-
-
- {issueName && issueName !== "" && ( - - )} - + ( + setValue("description", jsonValue)} + onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)} + placeholder="Description" + ref={editorRef} + /> + )} + /> + { + setGptAssistantModal(false); + // this is done so that the title do not reset after gpt popover closed + reset(getValues()); + }} + inset="top-2 left-0" + content="" + htmlContent={watch("description_html")} + onResponse={(response) => { + handleAiAssistance(response); + }} + projectId={projectId} + />
- ( - setValue("description", jsonValue)} - onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)} - placeholder="Description" - ref={editorRef} - /> - )} - /> - { - setGptAssistantModal(false); - // this is done so that the title do not reset after gpt popover closed - reset(getValues()); - }} - inset="top-2 left-0" - content="" - htmlContent={watch("description_html")} - onResponse={(response) => { - handleAiAssistance(response); - }} - projectId={projectId} - /> -
+ )}
- ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> -
+ {(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && ( ( - + )} /> -
-
+ )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && ( ( - + )} /> -
- - - {watch("parent") && watch("parent") !== "" ? ( - <> + )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && ( + ( + + )} + /> + )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && ( + ( + + )} + /> + )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && ( +
+ ( + + )} + /> +
+ )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && ( +
+ ( + + )} + /> +
+ )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( + + )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( + + {watch("parent") && watch("parent") !== "" ? ( + <> + setParentIssueListModalOpen(true)} + > + Change parent issue + + setValue("parent", null)} + > + Remove parent issue + + + ) : ( setParentIssueListModalOpen(true)} > - Change parent issue - - setValue("parent", null)} - > - Remove parent issue + Select Parent Issue - - ) : ( - setParentIssueListModalOpen(true)} - > - Select Parent Issue - - )} - + )} +
+ )}
diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx index cbf3f5e5116..5ef39e13acb 100644 --- a/apps/app/components/issues/modal.tsx +++ b/apps/app/components/issues/modal.tsx @@ -16,6 +16,7 @@ import useUser from "hooks/use-user"; import useIssuesView from "hooks/use-issues-view"; import useCalendarIssuesView from "hooks/use-calendar-issues-view"; import useToast from "hooks/use-toast"; +import useInboxView from "hooks/use-inbox-view"; // components import { IssueForm } from "components/issues"; // types @@ -35,7 +36,8 @@ import { VIEW_ISSUES, INBOX_ISSUES, } from "constants/fetch-keys"; -import useInboxView from "hooks/use-inbox-view"; +// constants +import { INBOX_ISSUE_SOURCE } from "constants/inbox"; export interface IssuesModalProps { isOpen: boolean; @@ -43,6 +45,19 @@ export interface IssuesModalProps { data?: IIssue | null; prePopulateData?: Partial; isUpdatingSingleIssue?: boolean; + fieldsToShow?: ( + | "project" + | "name" + | "description" + | "state" + | "priority" + | "assignee" + | "label" + | "dueDate" + | "estimate" + | "parent" + | "all" + )[]; } export const CreateUpdateIssueModal: React.FC = ({ @@ -51,6 +66,7 @@ export const CreateUpdateIssueModal: React.FC = ({ data, prePopulateData, isUpdatingSingleIssue = false, + fieldsToShow = ["all"], }) => { // states const [createMore, setCreateMore] = useState(false); @@ -144,17 +160,25 @@ export const CreateUpdateIssueModal: React.FC = ({ }); }; - const addIssueToInbox = async (payload: Partial) => { + const addIssueToInbox = async (formData: Partial) => { if (!workspaceSlug || !projectId || !inboxId) return; + const payload = { + issue: { + name: formData.name, + description: formData.description, + description_html: formData.description_html, + priority: formData.priority, + }, + source: INBOX_ISSUE_SOURCE, + }; + await inboxServices .createInboxIssue( workspaceSlug.toString(), projectId.toString(), inboxId.toString(), - { - issue: { ...payload, source: "web" }, - }, + payload, user ) .then((res) => { @@ -264,8 +288,8 @@ export const CreateUpdateIssueModal: React.FC = ({ const payload: Partial = { ...formData, - assignees_list: formData.assignees, - labels_list: formData.labels, + assignees_list: formData.assignees ?? [], + labels_list: formData.labels ?? [], description: formData.description ?? "", description_html: formData.description_html ?? "

", }; @@ -312,6 +336,7 @@ export const CreateUpdateIssueModal: React.FC = ({ setActiveProject={setActiveProject} status={data ? true : false} user={user} + fieldsToShow={fieldsToShow} /> diff --git a/apps/app/constants/inbox.ts b/apps/app/constants/inbox.ts index fa2befeb5f5..33aca2d1dc2 100644 --- a/apps/app/constants/inbox.ts +++ b/apps/app/constants/inbox.ts @@ -5,3 +5,5 @@ export const STATUS: { [key: string]: number } = { Accepted: 1, Duplicate: 2, }; + +export const INBOX_ISSUE_SOURCE = "in-app"; diff --git a/apps/app/hooks/use-inbox-view.tsx b/apps/app/hooks/use-inbox-view.tsx index 67504ce5491..4ce6f485970 100644 --- a/apps/app/hooks/use-inbox-view.tsx +++ b/apps/app/hooks/use-inbox-view.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useMemo, useState } from "react"; +import { useContext } from "react"; import { useRouter } from "next/router"; @@ -39,12 +39,21 @@ const useInboxView = () => { : null ); + let filtersLength = 0; + Object.keys(filters ?? {}).forEach((key) => { + const filterKey = key as keyof typeof filters; + + if (filters[filterKey] && Array.isArray(filters[filterKey])) + filtersLength += (filters[filterKey] ?? []).length; + }); + return { filters, setFilters, params, issues: inboxIssues, mutate: mutateInboxIssues, + filtersLength, } as const; }; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index d8b26540486..bdeffd7c443 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -69,8 +69,8 @@ const ProjectInbox: NextPage = () => { query: { inboxIssueId: currentIssueIndex === 0 - ? inboxIssues[inboxIssues.length - 1].id - : inboxIssues[currentIssueIndex - 1].id, + ? inboxIssues[inboxIssues.length - 1].bridge_id + : inboxIssues[currentIssueIndex - 1].bridge_id, }, }); break; @@ -80,8 +80,8 @@ const ProjectInbox: NextPage = () => { query: { inboxIssueId: currentIssueIndex === inboxIssues.length - 1 - ? inboxIssues[0].id - : inboxIssues[currentIssueIndex + 1].id, + ? inboxIssues[0].bridge_id + : inboxIssues[currentIssueIndex + 1].bridge_id, }, }); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 555c2644a88..148ce745834 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -105,7 +105,7 @@ const IssueDetailsPage: NextPage = () => { console.error(e); }); }, - [workspaceSlug, issueId, projectId, mutateIssueDetails] + [workspaceSlug, issueId, projectId, mutateIssueDetails, user] ); useEffect(() => { diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx index 61323813a15..2270ce7f4f3 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx @@ -77,7 +77,7 @@ const ProjectIssues: NextPage = () => { > Inbox {inboxList && inboxList?.[0]?.pending_issue_count !== 0 && ( - + {inboxList?.[0]?.pending_issue_count} )} From 35588330fe2e28f3c42818006c9d46b7629b7b5d Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 15 Jun 2023 16:35:38 +0530 Subject: [PATCH 40/51] dev: update issue accept workflow --- apiserver/plane/api/views/inbox.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index c6121b429fd..f1dc2b9e1b8 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -257,8 +257,7 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): issue_serializer.save() # Log all the updates requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder) - current_instance = issue - if current_instance is not None: + if issue is not None: issue_activity.delay( type="issue.activity.updated", requested_data=requested_data, @@ -266,7 +265,7 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): issue_id=str(issue.id), project_id=str(project_id), current_instance=json.dumps( - (current_instance).data, cls=DjangoJSONEncoder + issue_serializer.data, cls=DjangoJSONEncoder ), ) else: @@ -289,6 +288,20 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): if state is not None: issue.state = state issue.save() + + # Update the issue state if it is accepted + if serializer.data["status"] in [1]: + issue = Issue.objects.get( + pk=inbox_issue.issue_id, + workspace__slug=slug, + project_id=project_id, + ) + # Move to default state + state = State.objects.filter(workspace__slug=slug, project_id=project_id, default=True).first() + if state is not None: + issue.state = state + issue.save() + return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except InboxIssue.DoesNotExist: @@ -314,4 +327,4 @@ def retrieve(self, request, slug, project_id, inbox_id, pk): return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, - ) \ No newline at end of file + ) From 41666f9bcd7e5545f01022f1816fbddc67eba6ad Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 15 Jun 2023 16:45:26 +0530 Subject: [PATCH 41/51] chore: add comment to inbox issues --- apps/app/components/inbox/delete-issue-modal.tsx | 6 +++--- apps/app/components/inbox/inbox-main-content.tsx | 2 +- apps/app/components/issues/comment/add-comment.tsx | 5 +++-- .../projects/[projectId]/issues/[issueId].tsx | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/app/components/inbox/delete-issue-modal.tsx b/apps/app/components/inbox/delete-issue-modal.tsx index 68edde77658..204e3e3afa4 100644 --- a/apps/app/components/inbox/delete-issue-modal.tsx +++ b/apps/app/components/inbox/delete-issue-modal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { useRouter } from "next/router"; @@ -17,9 +17,9 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // ui import { SecondaryButton, DangerButton } from "components/ui"; // types -import type { IInboxIssue, IInboxIssueDetail } from "types"; +import type { IInboxIssue } from "types"; // fetch-keys -import { INBOX_ISSUES, INBOX_ISSUE_DETAILS } from "constants/fetch-keys"; +import { INBOX_ISSUES } from "constants/fetch-keys"; type Props = { isOpen: boolean; diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx index 78f479f3a0a..697019c97e3 100644 --- a/apps/app/components/inbox/inbox-main-content.tsx +++ b/apps/app/components/inbox/inbox-main-content.tsx @@ -149,7 +149,7 @@ export const InboxMainContent: React.FC = () => {

Comments/Activity

- +
diff --git a/apps/app/components/issues/comment/add-comment.tsx b/apps/app/components/issues/comment/add-comment.tsx index 7a5e626368a..1f4873372eb 100644 --- a/apps/app/components/issues/comment/add-comment.tsx +++ b/apps/app/components/issues/comment/add-comment.tsx @@ -41,10 +41,11 @@ const defaultValues: Partial = { }; type Props = { + issueId: string; user: ICurrentUserResponse | undefined; }; -export const AddComment: React.FC = ({ user }) => { +export const AddComment: React.FC = ({ issueId, user }) => { const { handleSubmit, control, @@ -56,7 +57,7 @@ export const AddComment: React.FC = ({ user }) => { const editorRef = React.useRef(null); const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; + const { workspaceSlug, projectId } = router.query; const { setToastAlert } = useToast(); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 148ce745834..157d716d1b4 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -209,7 +209,7 @@ const IssueDetailsPage: NextPage = () => {

Comments/Activity

- +
From 8398f2f7a0818dda600f57ae67032a6b3df6be44 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 15 Jun 2023 17:18:19 +0530 Subject: [PATCH 42/51] chore: remove inboxIssueId from url after deleting --- apps/app/components/inbox/delete-issue-modal.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/app/components/inbox/delete-issue-modal.tsx b/apps/app/components/inbox/delete-issue-modal.tsx index 204e3e3afa4..46ba1ebdd03 100644 --- a/apps/app/components/inbox/delete-issue-modal.tsx +++ b/apps/app/components/inbox/delete-issue-modal.tsx @@ -68,6 +68,11 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data }) message: "Issue deleted successfully.", }); + // remove inboxIssueId from the url + router.push({ + pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, + }); + onClose(); }) .catch(() => From da6421d5ff9f2ae16949acf8ac1a4b66b32b682e Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 15 Jun 2023 18:30:23 +0530 Subject: [PATCH 43/51] dev: update the issue triage workflow --- apiserver/plane/api/views/inbox.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index f1dc2b9e1b8..bf43232c0bd 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -296,11 +296,14 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): workspace__slug=slug, project_id=project_id, ) - # Move to default state - state = State.objects.filter(workspace__slug=slug, project_id=project_id, default=True).first() - if state is not None: - issue.state = state - issue.save() + + # Update the issue state only if it is in triage state + if issue.state.name == "Triage": + # Move to default state + state = State.objects.filter(workspace__slug=slug, project_id=project_id, default=True).first() + if state is not None: + issue.state = state + issue.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) From 9701849dbe861c74eeb8b27f09d6f866b7e69f3f Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 15 Jun 2023 18:52:17 +0530 Subject: [PATCH 44/51] fix: mutation after issue status change --- .../components/inbox/decline-issue-modal.tsx | 8 +- .../app/components/inbox/inbox-issue-card.tsx | 2 +- .../projects/[projectId]/inbox/[inboxId].tsx | 142 +++++++----------- apps/app/types/inbox.d.ts | 7 +- 4 files changed, 64 insertions(+), 95 deletions(-) diff --git a/apps/app/components/inbox/decline-issue-modal.tsx b/apps/app/components/inbox/decline-issue-modal.tsx index 68581450ebc..64fe1682a4a 100644 --- a/apps/app/components/inbox/decline-issue-modal.tsx +++ b/apps/app/components/inbox/decline-issue-modal.tsx @@ -66,7 +66,7 @@ export const DeclineIssueModal: React.FC = ({ isOpen, handleClose, data } return { ...prevData, - status: -1, + issue_inbox: [{ ...prevData.issue_inbox[0], status: -1 }], }; }, false @@ -74,8 +74,10 @@ export const DeclineIssueModal: React.FC = ({ isOpen, handleClose, data } mutate( INBOX_ISSUES(inboxId.toString(), params), (prevData) => - prevData?.map((item) => - item.bridge_id === data.bridge_id ? { ...item, status: -1 } : item + prevData?.map((i) => + i.bridge_id === data.bridge_id + ? { ...i, issue_inbox: [{ ...i.issue_inbox[0], status: -1 }] } + : i ), false ); diff --git a/apps/app/components/inbox/inbox-issue-card.tsx b/apps/app/components/inbox/inbox-issue-card.tsx index 65efcfb3f8c..682c305a7c4 100644 --- a/apps/app/components/inbox/inbox-issue-card.tsx +++ b/apps/app/components/inbox/inbox-issue-card.tsx @@ -35,7 +35,7 @@ export const InboxIssueCard: React.FC = (props) => { issueStatus === -2 ? "Pending issue" : issueStatus === -1 - ? "Rejected issue" + ? "Declined issue" : issueStatus === 0 ? "Snoozed issue" : issueStatus === 1 diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index bdeffd7c443..e733d8b5cbe 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -1,6 +1,5 @@ import { useState, useEffect, useCallback } from "react"; -import Link from "next/link"; import Router, { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; @@ -17,7 +16,6 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; import { InboxViewContextProvider } from "contexts/inbox-view-context"; // components import { - InboxIssueCard, InboxActionHeader, InboxMainContent, SelectDuplicateInboxIssueModal, @@ -28,12 +26,13 @@ import { // helper import { truncateText } from "helpers/string.helper"; // ui -import { Loader, PrimaryButton } from "components/ui"; +import { PrimaryButton } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons import { PlusIcon } from "@heroicons/react/24/outline"; import { InboxIcon } from "components/icons"; // types +import { IInboxIssueDetail, TInboxStatus } from "types"; import type { NextPage } from "next"; // fetch-keys import { INBOX_ISSUE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys"; @@ -101,19 +100,42 @@ const ProjectInbox: NextPage = () => { }; }, [onKeyDown]); - // // show the first issue by default in the main content - // useEffect(() => { - // if (!workspaceSlug || !projectId || !inboxId) return; - - // if (!inboxIssues || inboxIssues.length === 0) return; - - // Router.push({ - // pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, - // query: { - // inboxIssueId: inboxIssues[0].bridge_id, - // }, - // }); - // }, [inboxIssues, workspaceSlug, projectId, inboxId]); + const markInboxStatus = async (data: TInboxStatus) => { + if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId) return; + + await inboxServices + .markInboxStatus( + workspaceSlug.toString(), + projectId.toString(), + inboxId.toString(), + inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId)?.bridge_id!, + data, + user + ) + .then(() => { + mutate( + INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string), + (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + issue_inbox: [{ ...prevData.issue_inbox[0], ...data }], + }; + }, + false + ); + mutateInboxIssues( + (prevData) => + (prevData ?? []).map((i) => + i.bridge_id === inboxIssueId + ? { ...i, issue_inbox: [{ ...i.issue_inbox[0], ...data }] } + : i + ), + false + ); + }); + }; return ( @@ -150,32 +172,10 @@ const ProjectInbox: NextPage = () => { ?.issue_inbox[0].duplicate_to } onSubmit={(dupIssueId: string) => { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId)?.id!, - { - status: 2, - duplicate_to: dupIssueId, - }, - user - ) - .then(() => { - setSelectDuplicateIssue(false); - mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string)); - mutateInboxIssues((prevData) => - (prevData ?? [])?.map((item) => ({ - ...item, - status: item.bridge_id === inboxIssueId ? 2 : item.issue_inbox[0].status, - duplicate_to: dupIssueId, - })) - ); - }) - .catch(() => { - setSelectDuplicateIssue(false); - }); + markInboxStatus({ + status: 2, + duplicate_to: dupIssueId, + }).finally(() => setSelectDuplicateIssue(false)); }} /> { inboxIssues?.findIndex((issue) => issue.bridge_id === inboxIssueId) ?? 0 } issueCount={inboxIssues?.length ?? 0} - onAccept={() => { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId) - ?.bridge_id!, - { - status: 1, - }, - user - ) - .then(() => { - mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string)); - mutateInboxIssues((prevData) => - prevData?.map((item) => - item.bridge_id === inboxIssueId ? { ...item, status: 1 } : item - ) - ); - }); - }} + onAccept={() => + markInboxStatus({ + status: 1, + }) + } onDecline={() => setDeclineIssueModal(true)} - onMarkAsDuplicate={() => { - setSelectDuplicateIssue(true); - }} + onMarkAsDuplicate={() => setSelectDuplicateIssue(true)} onSnooze={(date) => { - inboxServices - .markInboxStatus( - workspaceSlug!.toString(), - projectId!.toString(), - inboxId!.toString(), - inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId) - ?.bridge_id!, - { - status: 0, - snoozed_till: new Date(date), - }, - user - ) - .then(() => { - mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string)); - mutateInboxIssues((prevData) => - prevData?.map((item) => - item.bridge_id === inboxIssueId - ? { ...item, status: 0, snoozed_till: new Date(date) } - : item - ) - ); - }); + markInboxStatus({ + status: 0, + snoozed_till: new Date(date), + }); }} onDelete={() => setDeleteIssueModal(true)} /> diff --git a/apps/app/types/inbox.d.ts b/apps/app/types/inbox.d.ts index 1101d540f04..5c922e059e2 100644 --- a/apps/app/types/inbox.d.ts +++ b/apps/app/types/inbox.d.ts @@ -67,7 +67,12 @@ interface StatusDuplicate { duplicate_to: string; } -type TInboxStatus = StatusReject | StatusSnoozed | StatusAccepted | StatusDuplicate | StatePending; +export type TInboxStatus = + | StatusReject + | StatusSnoozed + | StatusAccepted + | StatusDuplicate + | StatePending; export interface IInboxFilterOptions { priority: string[] | null; From 9bd611d253c535fcd8405ebea0bad6467b3b447b Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 15 Jun 2023 19:00:16 +0530 Subject: [PATCH 45/51] chore: issue details sidebar divider --- apps/app/components/issues/sidebar.tsx | 317 +++++++++++++------------ 1 file changed, 171 insertions(+), 146 deletions(-) diff --git a/apps/app/components/issues/sidebar.tsx b/apps/app/components/issues/sidebar.tsx index 3dc3a8e9de9..ae0e5ad57ba 100644 --- a/apps/app/components/issues/sidebar.tsx +++ b/apps/app/components/issues/sidebar.tsx @@ -247,6 +247,25 @@ export const IssueDetailsSidebar: React.FC = ({ reset(); }, [createLabelForm, reset]); + const showFirstSection = + fieldsToShow.includes("all") || + fieldsToShow.includes("state") || + fieldsToShow.includes("assignee") || + fieldsToShow.includes("priority") || + fieldsToShow.includes("estimate"); + + const showSecondSection = + fieldsToShow.includes("all") || + fieldsToShow.includes("parent") || + fieldsToShow.includes("blocker") || + fieldsToShow.includes("blocked") || + fieldsToShow.includes("dueDate"); + + const showThirdSection = + fieldsToShow.includes("all") || + fieldsToShow.includes("cycle") || + fieldsToShow.includes("module"); + const isNotAllowed = memberRole.isGuest || memberRole.isViewer; return ( @@ -287,153 +306,159 @@ export const IssueDetailsSidebar: React.FC = ({
-
- {(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && ( - ( - submitChanges({ state: val })} - userAuth={memberRole} - /> - )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && ( - ( - submitChanges({ assignees_list: val })} - userAuth={memberRole} - /> - )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && ( - ( - submitChanges({ priority: val })} - userAuth={memberRole} - /> - )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && ( - ( - submitChanges({ estimate_point: val })} - userAuth={memberRole} - /> - )} - /> - )} -
-
- {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( - - i.id !== issueDetail?.id && - i.id !== issueDetail?.parent && - i.parent !== issueDetail?.id - ) ?? [] - } - customDisplay={ - issueDetail?.parent_detail ? ( - - ) : ( -
- No parent selected -
- ) - } - watch={watchIssue} - userAuth={memberRole} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && ( - i.id !== issueDetail?.id) ?? []} - watch={watchIssue} - userAuth={memberRole} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && ( - i.id !== issueDetail?.id) ?? []} - watch={watchIssue} - userAuth={memberRole} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && ( -
-
- -

Due date

-
-
- ( - - submitChanges({ - target_date: val, - }) - } - className="bg-brand-surface-1" - disabled={isNotAllowed} - /> - )} - /> + {showFirstSection && ( +
+ {(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && ( + ( + submitChanges({ state: val })} + userAuth={memberRole} + /> + )} + /> + )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && ( + ( + submitChanges({ assignees_list: val })} + userAuth={memberRole} + /> + )} + /> + )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && ( + ( + submitChanges({ priority: val })} + userAuth={memberRole} + /> + )} + /> + )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && ( + ( + submitChanges({ estimate_point: val })} + userAuth={memberRole} + /> + )} + /> + )} +
+ )} + {showSecondSection && ( +
+ {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( + + i.id !== issueDetail?.id && + i.id !== issueDetail?.parent && + i.parent !== issueDetail?.id + ) ?? [] + } + customDisplay={ + issueDetail?.parent_detail ? ( + + ) : ( +
+ No parent selected +
+ ) + } + watch={watchIssue} + userAuth={memberRole} + /> + )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && ( + i.id !== issueDetail?.id) ?? []} + watch={watchIssue} + userAuth={memberRole} + /> + )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && ( + i.id !== issueDetail?.id) ?? []} + watch={watchIssue} + userAuth={memberRole} + /> + )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && ( +
+
+ +

Due date

+
+
+ ( + + submitChanges({ + target_date: val, + }) + } + className="bg-brand-surface-1" + disabled={isNotAllowed} + /> + )} + /> +
-
- )} -
-
- {(fieldsToShow.includes("all") || fieldsToShow.includes("cycle")) && ( - - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && ( - - )} -
+ )} +
+ )} + {showThirdSection && ( +
+ {(fieldsToShow.includes("all") || fieldsToShow.includes("cycle")) && ( + + )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && ( + + )} +
+ )}
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
From 8bd79b8e098765fcd06ed505c15925e22a66a001 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 15 Jun 2023 22:06:46 +0530 Subject: [PATCH 46/51] fix: issue activity for inbox issues --- apiserver/plane/api/views/inbox.py | 5 +++-- apiserver/plane/bgtasks/issue_activites_task.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index bf43232c0bd..01d4c4b25f8 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -254,6 +254,7 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): issue_serializer = IssueCreateSerializer(issue, data=issue_data, partial=True) if issue_serializer.is_valid(): + current_instance = issue issue_serializer.save() # Log all the updates requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder) @@ -265,7 +266,7 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): issue_id=str(issue.id), project_id=str(project_id), current_instance=json.dumps( - issue_serializer.data, cls=DjangoJSONEncoder + IssueSerializer(current_instance).data, cls=DjangoJSONEncoder ), ) else: @@ -284,7 +285,7 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): workspace__slug=slug, project_id=project_id, ) - state = State.objects.filter(group="cancelled").first() + state = State.objects.filter(group="cancelled", workspace__slug=slug, project_id=project_id).first() if state is not None: issue.state = state issue.save() diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 417fe2324f7..b1096e30bea 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -44,7 +44,7 @@ def track_name( field="name", project=project, workspace=project.workspace, - comment=f"{actor.email} updated the start date to {requested_data.get('name')}", + comment=f"{actor.email} updated the name to {requested_data.get('name')}", ) ) From 5d176fb6b2580be4c29dc09c4d06766fa5a8df83 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 15 Jun 2023 23:18:02 +0530 Subject: [PATCH 47/51] dev: update inbox perrmissions --- apiserver/plane/api/permissions/__init__.py | 2 +- apiserver/plane/api/permissions/workspace.py | 12 +++++- apiserver/plane/api/views/inbox.py | 41 +++++++++++++------- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/apiserver/plane/api/permissions/__init__.py b/apiserver/plane/api/permissions/__init__.py index 71ec4815de3..7710061ccc2 100644 --- a/apiserver/plane/api/permissions/__init__.py +++ b/apiserver/plane/api/permissions/__init__.py @@ -1,2 +1,2 @@ -from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission +from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission diff --git a/apiserver/plane/api/permissions/workspace.py b/apiserver/plane/api/permissions/workspace.py index 2a2e1d33915..7fccc455e94 100644 --- a/apiserver/plane/api/permissions/workspace.py +++ b/apiserver/plane/api/permissions/workspace.py @@ -5,7 +5,6 @@ from plane.db.models import WorkspaceMember - # Permission Mappings Owner = 20 Admin = 15 @@ -44,7 +43,6 @@ def has_permission(self, request, view): class WorkSpaceAdminPermission(BasePermission): def has_permission(self, request, view): - if request.user.is_anonymous: return False @@ -53,3 +51,13 @@ def has_permission(self, request, view): workspace__slug=view.workspace_slug, role__in=[Owner, Admin], ).exists() + + +class WorkspaceEntityPermission(BasePermission): + def has_permission(self, request, view): + if request.user.is_anonymous: + return False + + return WorkspaceMember.objects.filter( + member=request.user, workspace__slug=view.workspace_slug + ).exists() diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index 01d4c4b25f8..8282c1197a1 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -13,7 +13,7 @@ # Module imports from .base import BaseViewSet -from plane.api.permissions import ProjectBasePermission +from plane.api.permissions import ProjectBasePermission, WorkspaceEntityPermission from plane.db.models import ( Project, Inbox, @@ -87,8 +87,9 @@ def destroy(self, request, slug, project_id, pk): class InboxIssueViewSet(BaseViewSet): permission_classes = [ - ProjectBasePermission, + WorkspaceEntityPermission, ] + serializer_class = InboxIssueSerializer model = InboxIssue @@ -112,7 +113,6 @@ def get_queryset(self): def list(self, request, slug, project_id, inbox_id): try: order_by = request.GET.get("order_by", "created_at") - group_by = request.GET.get("group_by", False) filters = issue_filters(request.query_params, "GET") issues = ( Issue.objects.filter( @@ -185,7 +185,7 @@ def create(self, request, slug, project_id, inbox_id): "medium", "high", "urgent", - None + None, ]: return Response( {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST @@ -213,7 +213,7 @@ def create(self, request, slug, project_id, inbox_id): ) # Create an Issue Activity - # Track the issue + # Track the issue issue_activity.delay( type="issue.activity.created", requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), @@ -250,8 +250,12 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): issue_data = request.data.pop("issue", False) if bool(issue_data): - issue = Issue.objects.get(pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id) - issue_serializer = IssueCreateSerializer(issue, data=issue_data, partial=True) + issue = Issue.objects.get( + pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id + ) + issue_serializer = IssueCreateSerializer( + issue, data=issue_data, partial=True + ) if issue_serializer.is_valid(): current_instance = issue @@ -266,11 +270,14 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): issue_id=str(issue.id), project_id=str(project_id), current_instance=json.dumps( - IssueSerializer(current_instance).data, cls=DjangoJSONEncoder + IssueSerializer(current_instance).data, + cls=DjangoJSONEncoder, ), ) else: - return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response( + issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) serializer = InboxIssueSerializer( inbox_issue, data=request.data, partial=True @@ -285,7 +292,9 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): workspace__slug=slug, project_id=project_id, ) - state = State.objects.filter(group="cancelled", workspace__slug=slug, project_id=project_id).first() + state = State.objects.filter( + group="cancelled", workspace__slug=slug, project_id=project_id + ).first() if state is not None: issue.state = state issue.save() @@ -301,7 +310,9 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): # Update the issue state only if it is in triage state if issue.state.name == "Triage": # Move to default state - state = State.objects.filter(workspace__slug=slug, project_id=project_id, default=True).first() + state = State.objects.filter( + workspace__slug=slug, project_id=project_id, default=True + ).first() if state is not None: issue.state = state issue.save() @@ -322,8 +333,12 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): def retrieve(self, request, slug, project_id, inbox_id, pk): try: - inbox_issue = InboxIssue.objects.get(pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id) - issue = Issue.objects.get(pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id) + inbox_issue = InboxIssue.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + ) + issue = Issue.objects.get( + pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id + ) serializer = IssueStateInboxSerializer(issue) return Response(serializer.data, status=status.HTTP_200_OK) except Exception as e: From a0fbdf66a2d3b97c560112c11893c5d512a5ef5f Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 15 Jun 2023 23:20:42 +0530 Subject: [PATCH 48/51] dev: create new permission layer --- apiserver/plane/api/permissions/__init__.py | 2 +- apiserver/plane/api/permissions/project.py | 13 +++++++++++++ apiserver/plane/api/views/inbox.py | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/apiserver/plane/api/permissions/__init__.py b/apiserver/plane/api/permissions/__init__.py index 7710061ccc2..91b3aea356c 100644 --- a/apiserver/plane/api/permissions/__init__.py +++ b/apiserver/plane/api/permissions/__init__.py @@ -1,2 +1,2 @@ from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission -from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission +from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, ProjectLitePermission diff --git a/apiserver/plane/api/permissions/project.py b/apiserver/plane/api/permissions/project.py index eea5192d5da..e4e3e0f9bc3 100644 --- a/apiserver/plane/api/permissions/project.py +++ b/apiserver/plane/api/permissions/project.py @@ -89,3 +89,16 @@ def has_permission(self, request, view): role__in=[Admin, Member], project_id=view.project_id, ).exists() + + +class ProjectLitePermission(BasePermission): + + def has_permission(self, request, view): + if request.user.is_anonymous: + return False + + return ProjectMember.objects.filter( + workspace__slug=view.workspace_slug, + member=request.user, + project_id=view.project_id, + ).exists() \ No newline at end of file diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index 8282c1197a1..f76638c1708 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -13,7 +13,7 @@ # Module imports from .base import BaseViewSet -from plane.api.permissions import ProjectBasePermission, WorkspaceEntityPermission +from plane.api.permissions import ProjectBasePermission, ProjectLitePermission from plane.db.models import ( Project, Inbox, @@ -87,7 +87,7 @@ def destroy(self, request, slug, project_id, pk): class InboxIssueViewSet(BaseViewSet): permission_classes = [ - WorkspaceEntityPermission, + ProjectLitePermission, ] serializer_class = InboxIssueSerializer From 7aefb69851b79733994faacfa2f716eb7e702fd7 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 15 Jun 2023 23:43:30 +0530 Subject: [PATCH 49/51] chore: auth layer for inbox --- .../components/inbox/inbox-action-headers.tsx | 12 ++++++---- .../components/inbox/inbox-main-content.tsx | 22 +++++++++++-------- .../components/issues/description-form.tsx | 16 +++++++------- .../projects/[projectId]/issues/[issueId].tsx | 13 ++++++++--- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/apps/app/components/inbox/inbox-action-headers.tsx b/apps/app/components/inbox/inbox-action-headers.tsx index 7965699e03b..9e383f8c553 100644 --- a/apps/app/components/inbox/inbox-action-headers.tsx +++ b/apps/app/components/inbox/inbox-action-headers.tsx @@ -10,6 +10,7 @@ import { Popover } from "@headlessui/react"; import { useProjectMyMembership } from "contexts/project-member.context"; // hooks import useInboxView from "hooks/use-inbox-view"; +import useUserAuth from "hooks/use-user-auth"; // components import { FiltersDropdown } from "components/inbox"; // ui @@ -57,6 +58,7 @@ export const InboxActionHeader: React.FC = (props) => { const { memberRole } = useProjectMyMembership(); const { filters, setFilters, filtersLength } = useInboxView(); + const { user } = useUserAuth(); useEffect(() => { if (!issue?.issue_inbox[0].snoozed_till) return; @@ -131,8 +133,8 @@ export const InboxActionHeader: React.FC = (props) => { {currentIssueIndex + 1}/{issueCount}
- {isAllowed && ( -
+
+ {isAllowed && (
@@ -199,14 +201,16 @@ export const InboxActionHeader: React.FC = (props) => { Decline
+ )} + {(isAllowed || user?.id === issue?.created_by) && (
Delete
-
- )} + )} +
)}
diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx index 697019c97e3..76155728cc3 100644 --- a/apps/app/components/inbox/inbox-main-content.tsx +++ b/apps/app/components/inbox/inbox-main-content.tsx @@ -6,26 +6,26 @@ import useSWR, { mutate } from "swr"; // react hook form import { useForm } from "react-hook-form"; +// contexts +import { useProjectMyMembership } from "contexts/project-member.context"; // services import inboxServices from "services/inbox.service"; // hooks import useInboxView from "hooks/use-inbox-view"; -// ui -import { Loader } from "components/ui"; -// hooks -import useUser from "hooks/use-user"; -// fetch-keys -import { INBOX_ISSUES, INBOX_ISSUE_DETAILS } from "constants/fetch-keys"; - +import useUserAuth from "hooks/use-user-auth"; +// components import { AddComment, IssueActivitySection, IssueDescriptionForm, IssueDetailsSidebar, } from "components/issues"; - +// ui +import { Loader } from "components/ui"; // types import type { IInboxIssue, IIssue } from "types"; +// fetch-keys +import { INBOX_ISSUES, INBOX_ISSUE_DETAILS } from "constants/fetch-keys"; const defaultValues = { name: "", @@ -42,7 +42,8 @@ export const InboxMainContent: React.FC = () => { const router = useRouter(); const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query; - const { user } = useUser(); + const { user } = useUserAuth(); + const { memberRole } = useProjectMyMembership(); const { params } = useInboxView(); const { reset, control, watch } = useForm({ @@ -144,6 +145,9 @@ export const InboxMainContent: React.FC = () => { description_html: issueDetails.description_html, }} handleFormSubmit={submitChanges} + isAllowed={ + memberRole.isMember || memberRole.isOwner || user?.id === issueDetails.created_by + } />
diff --git a/apps/app/components/issues/description-form.tsx b/apps/app/components/issues/description-form.tsx index c64e5c02a54..492110a8c8b 100644 --- a/apps/app/components/issues/description-form.tsx +++ b/apps/app/components/issues/description-form.tsx @@ -4,8 +4,6 @@ import dynamic from "next/dynamic"; // react-hook-form import { Controller, useForm } from "react-hook-form"; -// contexts -import { useProjectMyMembership } from "contexts/project-member.context"; // hooks import useReloadConfirmations from "hooks/use-reload-confirmation"; // components @@ -34,14 +32,17 @@ export interface IssueDetailsProps { description_html: string; }; handleFormSubmit: (value: IssueDescriptionFormValues) => Promise; + isAllowed: boolean; } -export const IssueDescriptionForm: FC = ({ issue, handleFormSubmit }) => { +export const IssueDescriptionForm: FC = ({ + issue, + handleFormSubmit, + isAllowed, +}) => { const [isSubmitting, setIsSubmitting] = useState(false); const [characterLimit, setCharacterLimit] = useState(false); - const { memberRole } = useProjectMyMembership(); - const { setShowAlert } = useReloadConfirmations(); const { @@ -82,8 +83,6 @@ export const IssueDescriptionForm: FC = ({ issue, handleFormS }); }, [issue, reset]); - const isNotAllowed = memberRole.isGuest || memberRole.isViewer; - return (
@@ -110,6 +109,7 @@ export const IssueDescriptionForm: FC = ({ issue, handleFormS overflow-hidden rounded border-none bg-transparent px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-theme" role="textbox" + disabled={!isAllowed} /> {characterLimit && (
@@ -160,7 +160,7 @@ export const IssueDescriptionForm: FC = ({ issue, handleFormS }); }} placeholder="Description" - editable={!isNotAllowed} + editable={isAllowed} /> ); }} diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 157d716d1b4..e1c883c0b79 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -7,10 +7,12 @@ import useSWR, { mutate } from "swr"; // react-hook-form import { useForm } from "react-hook-form"; -// hooks -import useUserAuth from "hooks/use-user-auth"; +// contexts +import { useProjectMyMembership } from "contexts/project-member.context"; // services import issuesService from "services/issues.service"; +// hooks +import useUserAuth from "hooks/use-user-auth"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // components @@ -53,6 +55,7 @@ const IssueDetailsPage: NextPage = () => { const { workspaceSlug, projectId, issueId } = router.query; const { user } = useUserAuth(); + const { memberRole } = useProjectMyMembership(); const { data: issueDetails, mutate: mutateIssueDetails } = useSWR( workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, @@ -194,7 +197,11 @@ const IssueDetailsPage: NextPage = () => {
) : null} - +
From fddc28b9293cac822f1f0c116d4dcf76d0db2b67 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 16 Jun 2023 18:21:59 +0530 Subject: [PATCH 50/51] chore: show accepting status --- apps/app/components/inbox/inbox-action-headers.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/app/components/inbox/inbox-action-headers.tsx b/apps/app/components/inbox/inbox-action-headers.tsx index 9e383f8c553..8be550d9844 100644 --- a/apps/app/components/inbox/inbox-action-headers.tsx +++ b/apps/app/components/inbox/inbox-action-headers.tsx @@ -32,7 +32,7 @@ type Props = { issueCount: number; currentIssueIndex: number; issue?: IInboxIssue; - onAccept: () => void; + onAccept: () => Promise; onDecline: () => void; onMarkAsDuplicate: () => void; onSnooze: (date: Date | string) => void; @@ -51,6 +51,7 @@ export const InboxActionHeader: React.FC = (props) => { issue, } = props; + const [isAccepting, setIsAccepting] = useState(false); const [date, setDate] = useState(new Date()); const router = useRouter(); @@ -60,6 +61,12 @@ export const InboxActionHeader: React.FC = (props) => { const { filters, setFilters, filtersLength } = useInboxView(); const { user } = useUserAuth(); + const handleAcceptIssue = () => { + setIsAccepting(true); + + onAccept().finally(() => setIsAccepting(false)); + }; + useEffect(() => { if (!issue?.issue_inbox[0].snoozed_till) return; @@ -185,11 +192,12 @@ export const InboxActionHeader: React.FC = (props) => { - Accept + {isAccepting ? "Accepting..." : "Accept"} Date: Fri, 16 Jun 2023 18:44:46 +0530 Subject: [PATCH 51/51] chore: show issue status at the top of issue details --- .../components/inbox/inbox-main-content.tsx | 73 ++++++++++++++++++- apps/app/constants/inbox.ts | 2 +- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx index 76155728cc3..1bf330e5e60 100644 --- a/apps/app/components/inbox/inbox-main-content.tsx +++ b/apps/app/components/inbox/inbox-main-content.tsx @@ -22,6 +22,17 @@ import { } from "components/issues"; // ui import { Loader } from "components/ui"; +// icons +import { + ArrowTopRightOnSquareIcon, + CheckCircleIcon, + ClockIcon, + DocumentDuplicateIcon, + ExclamationTriangleIcon, + XCircleIcon, +} from "@heroicons/react/24/outline"; +// helpers +import { renderShortNumericDateFormat } from "helpers/date-time.helper"; // types import type { IInboxIssue, IIssue } from "types"; // fetch-keys @@ -132,11 +143,69 @@ export const InboxMainContent: React.FC = () => { ] ); + const issueStatus = issueDetails?.issue_inbox[0].status; + return ( <> {issueDetails ? (
-
+
+
+ {issueStatus === -2 ? ( + <> + +

This issue is still pending.

+ + ) : issueStatus === -1 ? ( + <> + +

This issue has been declined.

+ + ) : issueStatus === 0 ? ( + <> + +

+ This issue has been snoozed till{" "} + {renderShortNumericDateFormat(issueDetails.issue_inbox[0].snoozed_till ?? "")}. +

+ + ) : issueStatus === 1 ? ( + <> + +

This issue has been accepted.

+ + ) : issueStatus === 2 ? ( + <> + +

+ This issue has been marked as a duplicate of + + this issue + + . +

+ + ) : null} +
{ } />
-
+

Comments/Activity

diff --git a/apps/app/constants/inbox.ts b/apps/app/constants/inbox.ts index 33aca2d1dc2..b5731fb89a8 100644 --- a/apps/app/constants/inbox.ts +++ b/apps/app/constants/inbox.ts @@ -1,6 +1,6 @@ export const STATUS: { [key: string]: number } = { Pending: -2, - Rejected: -1, + Declined: -1, Snoozed: 0, Accepted: 1, Duplicate: 2,