diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index d8d69f26c..9bdd4baaf 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -69,6 +69,9 @@ from .issue import ( RelatedIssueSerializer, IssuePublicSerializer, IssueDetailSerializer, + IssueReactionLiteSerializer, + IssueAttachmentLiteSerializer, + IssueLinkLiteSerializer, ) from .module import ( diff --git a/apiserver/plane/app/serializers/base.py b/apiserver/plane/app/serializers/base.py index 446fdb6d5..6693ba931 100644 --- a/apiserver/plane/app/serializers/base.py +++ b/apiserver/plane/app/serializers/base.py @@ -58,9 +58,12 @@ class DynamicBaseSerializer(BaseSerializer): IssueSerializer, LabelSerializer, CycleIssueSerializer, - IssueFlatSerializer, + IssueLiteSerializer, IssueRelationSerializer, - InboxIssueLiteSerializer + InboxIssueLiteSerializer, + IssueReactionLiteSerializer, + IssueAttachmentLiteSerializer, + IssueLinkLiteSerializer, ) # Expansion mapper @@ -79,12 +82,34 @@ class DynamicBaseSerializer(BaseSerializer): "assignees": UserLiteSerializer, "labels": LabelSerializer, "issue_cycle": CycleIssueSerializer, - "parent": IssueSerializer, + "parent": IssueLiteSerializer, "issue_relation": IssueRelationSerializer, - "issue_inbox" : InboxIssueLiteSerializer, + "issue_inbox": InboxIssueLiteSerializer, + "issue_reactions": IssueReactionLiteSerializer, + "issue_attachment": IssueAttachmentLiteSerializer, + "issue_link": IssueLinkLiteSerializer, + "sub_issues": IssueLiteSerializer, } - - self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation", "issue_inbox"] else False) + + self.fields[field] = expansion[field]( + many=( + True + if field + in [ + "members", + "assignees", + "labels", + "issue_cycle", + "issue_relation", + "issue_inbox", + "issue_reactions", + "issue_attachment", + "issue_link", + "sub_issues", + ] + else False + ) + ) return self.fields @@ -105,7 +130,11 @@ class DynamicBaseSerializer(BaseSerializer): LabelSerializer, CycleIssueSerializer, IssueRelationSerializer, - InboxIssueLiteSerializer + InboxIssueLiteSerializer, + IssueLiteSerializer, + IssueReactionLiteSerializer, + IssueAttachmentLiteSerializer, + IssueLinkLiteSerializer, ) # Expansion mapper @@ -124,9 +153,13 @@ class DynamicBaseSerializer(BaseSerializer): "assignees": UserLiteSerializer, "labels": LabelSerializer, "issue_cycle": CycleIssueSerializer, - "parent": IssueSerializer, + "parent": IssueLiteSerializer, "issue_relation": IssueRelationSerializer, - "issue_inbox" : InboxIssueLiteSerializer, + "issue_inbox": InboxIssueLiteSerializer, + "issue_reactions": IssueReactionLiteSerializer, + "issue_attachment": IssueAttachmentLiteSerializer, + "issue_link": IssueLinkLiteSerializer, + "sub_issues": IssueLiteSerializer, } # Check if field in expansion then expand the field if expand in expansion: diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index a2cd47e2c..411c5b73f 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -444,6 +444,22 @@ class IssueLinkSerializer(BaseSerializer): return IssueLink.objects.create(**validated_data) +class IssueLinkLiteSerializer(BaseSerializer): + + class Meta: + model = IssueLink + fields = [ + "id", + "issue_id", + "title", + "url", + "metadata", + "created_by_id", + "created_at", + ] + read_only_fields = fields + + class IssueAttachmentSerializer(BaseSerializer): class Meta: model = IssueAttachment @@ -459,6 +475,21 @@ class IssueAttachmentSerializer(BaseSerializer): ] +class IssueAttachmentLiteSerializer(DynamicBaseSerializer): + + class Meta: + model = IssueAttachment + fields = [ + "id", + "asset", + "attributes", + "issue_id", + "updated_at", + "updated_by_id", + ] + read_only_fields = fields + + class IssueReactionSerializer(BaseSerializer): actor_detail = UserLiteSerializer(read_only=True, source="actor") @@ -473,6 +504,18 @@ class IssueReactionSerializer(BaseSerializer): ] +class IssueReactionLiteSerializer(DynamicBaseSerializer): + + class Meta: + model = IssueReaction + fields = [ + "id", + "actor_id", + "issue_id", + "reaction", + ] + + class CommentReactionSerializer(BaseSerializer): class Meta: model = CommentReaction @@ -558,15 +601,15 @@ class IssueSerializer(DynamicBaseSerializer): # ids cycle_id = serializers.PrimaryKeyRelatedField(read_only=True) module_ids = serializers.ListField( - child=serializers.UUIDField(), required=False, allow_null=True + child=serializers.UUIDField(), required=False, ) # Many to many label_ids = serializers.ListField( - child=serializers.UUIDField(), required=False, allow_null=True + child=serializers.UUIDField(), required=False, ) assignee_ids = serializers.ListField( - child=serializers.UUIDField(), required=False, allow_null=True + child=serializers.UUIDField(), required=False, ) # Count items @@ -606,48 +649,39 @@ class IssueSerializer(DynamicBaseSerializer): read_only_fields = fields - class IssueDetailSerializer(IssueSerializer): description_html = serializers.CharField() is_subscribed = serializers.BooleanField(read_only=True) class Meta(IssueSerializer.Meta): - fields = IssueSerializer.Meta.fields + ["description_html", "is_subscribed"] + fields = IssueSerializer.Meta.fields + [ + "description_html", + "is_subscribed", + ] class IssueLiteSerializer(DynamicBaseSerializer): - workspace_detail = WorkspaceLiteSerializer( - read_only=True, source="workspace" - ) - project_detail = ProjectLiteSerializer(read_only=True, source="project") - state_detail = StateLiteSerializer(read_only=True, source="state") - label_details = LabelLiteSerializer( - read_only=True, source="labels", many=True - ) - assignee_details = UserLiteSerializer( - read_only=True, source="assignees", many=True - ) - sub_issues_count = serializers.IntegerField(read_only=True) - cycle_id = serializers.UUIDField(read_only=True) - module_id = serializers.UUIDField(read_only=True) - attachment_count = serializers.IntegerField(read_only=True) - link_count = serializers.IntegerField(read_only=True) - issue_reactions = IssueReactionSerializer(read_only=True, many=True) class Meta: model = Issue - fields = "__all__" - read_only_fields = [ - "start_date", - "target_date", - "completed_at", - "workspace", - "project", - "created_by", - "updated_by", - "created_at", - "updated_at", + fields = [ + "id", + "sequence_id", + "project_id", ] + read_only_fields = fields + + +class IssueDetailSerializer(IssueSerializer): + description_html = serializers.CharField() + is_subscribed = serializers.BooleanField() + + class Meta(IssueSerializer.Meta): + fields = IssueSerializer.Meta.fields + [ + "description_html", + "is_subscribed", + ] + read_only_fields = fields class IssuePublicSerializer(BaseSerializer): diff --git a/apiserver/plane/app/views/inbox.py b/apiserver/plane/app/views/inbox.py index 5707abea0..d70eec4f2 100644 --- a/apiserver/plane/app/views/inbox.py +++ b/apiserver/plane/app/views/inbox.py @@ -3,7 +3,7 @@ import json # Django import from django.utils import timezone -from django.db.models import Q, Count, OuterRef, Func, F, Prefetch +from django.db.models import Q, Count, OuterRef, Func, F, Prefetch, Exists from django.core.serializers.json import DjangoJSONEncoder from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField @@ -25,13 +25,14 @@ from plane.db.models import ( IssueLink, IssueAttachment, ProjectMember, + IssueReaction, + IssueSubscriber, ) from plane.app.serializers import ( + IssueCreateSerializer, IssueSerializer, InboxSerializer, InboxIssueSerializer, - IssueCreateSerializer, - IssueDetailSerializer, ) from plane.utils.issue_filters import issue_filters from plane.bgtasks.issue_activites_task import issue_activity @@ -295,11 +296,7 @@ class InboxIssueViewSet(BaseViewSet): 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 = self.get_queryset().filter(pk=inbox_issue.issue_id).first() # Only allow guests and viewers to edit name and description if project_member.role <= 10: # viewers and guests since only viewers and guests @@ -385,9 +382,7 @@ class InboxIssueViewSet(BaseViewSet): if state is not None: issue.state = state issue.save() - issue = self.get_queryset().filter(pk=issue_id).first() - serializer = IssueSerializer(issue, expand=self.expand) - return Response(serializer.data, status=status.HTTP_200_OK) + return Response(status=status.HTTP_204_NO_CONTENT) return Response( serializer.errors, status=status.HTTP_400_BAD_REQUEST ) @@ -397,11 +392,45 @@ class InboxIssueViewSet(BaseViewSet): return Response(serializer.data, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, inbox_id, issue_id): - issue = self.get_queryset().filter(pk=issue_id).first() - serializer = IssueDetailSerializer( - issue, - expand=self.expand, - ) + issue = ( + self.get_queryset() + .filter(pk=issue_id) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related( + "issue", "actor" + ), + ) + ) + .prefetch_related( + Prefetch( + "issue_attachment", + queryset=IssueAttachment.objects.select_related("issue"), + ) + ) + .prefetch_related( + Prefetch( + "issue_link", + queryset=IssueLink.objects.select_related("created_by"), + ) + ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=OuterRef("pk"), + subscriber=request.user, + ) + ) + ) + ).first() + + if issue is None: + return Response({"error": "Requested object was not found"}, status=status.HTTP_404_NOT_FOUND) + + serializer = IssueSerializer(issue) return Response(serializer.data, status=status.HTTP_200_OK) def destroy(self, request, slug, project_id, inbox_id, issue_id): diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index fa6fc425e..25c42dc5b 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -528,21 +528,62 @@ class IssueViewSet(WebhookMixin, BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, slug, project_id, pk=None): - issue = self.get_queryset().filter(pk=pk).first() - return Response( - IssueDetailSerializer( - issue, fields=self.fields, expand=self.expand - ).data, - status=status.HTTP_200_OK, - ) + issue = ( + self.get_queryset() + .filter(pk=pk) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related( + "issue", "actor" + ), + ) + ) + .prefetch_related( + Prefetch( + "issue_attachment", + queryset=IssueAttachment.objects.select_related("issue"), + ) + ) + .prefetch_related( + Prefetch( + "issue_link", + queryset=IssueLink.objects.select_related("created_by"), + ) + ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=OuterRef("pk"), + subscriber=request.user, + ) + ) + ) + ).first() + if not issue: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + + serializer = IssueDetailSerializer(issue, expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) def partial_update(self, request, slug, project_id, pk=None): - issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) + issue = self.get_queryset().filter(pk=pk).first() + + if not issue: + return Response( + {"error": "Issue not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + current_instance = json.dumps( IssueSerializer(issue).data, cls=DjangoJSONEncoder ) + requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) serializer = IssueCreateSerializer( issue, data=request.data, partial=True @@ -560,39 +601,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet): notification=True, origin=request.META.get("HTTP_ORIGIN"), ) - issue = ( - self.get_queryset() - .filter(pk=pk) - .values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "sequence_id", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "sub_issues_count", - "created_at", - "updated_at", - "created_by", - "updated_by", - "attachment_count", - "link_count", - "is_draft", - "archived_at", - ) - .first() - ) - return Response(issue, status=status.HTTP_200_OK) + issue = self.get_queryset().filter(pk=pk).first() + return Response(status=status.HTTP_204_NO_CONTENT) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def destroy(self, request, slug, project_id, pk=None): @@ -1581,13 +1591,47 @@ class IssueArchiveViewSet(BaseViewSet): return Response(issues, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, pk=None): - issue = self.get_queryset().filter(pk=pk).first() - return Response( - IssueDetailSerializer( - issue, fields=self.fields, expand=self.expand - ).data, - status=status.HTTP_200_OK, - ) + issue = ( + self.get_queryset() + .filter(pk=pk) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related( + "issue", "actor" + ), + ) + ) + .prefetch_related( + Prefetch( + "issue_attachment", + queryset=IssueAttachment.objects.select_related("issue"), + ) + ) + .prefetch_related( + Prefetch( + "issue_link", + queryset=IssueLink.objects.select_related("created_by"), + ) + ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=OuterRef("pk"), + subscriber=request.user, + ) + ) + ) + ).first() + if not issue: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + serializer = IssueDetailSerializer(issue, expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) def unarchive(self, request, slug, project_id, pk=None): issue = Issue.objects.get( @@ -2258,9 +2302,14 @@ class IssueDraftViewSet(BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def partial_update(self, request, slug, project_id, pk): - issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk - ) + issue = self.get_queryset().filter(pk=pk).first() + + if not issue: + return Response( + {"error": "Issue does not exist"}, + status=status.HTTP_404_NOT_FOUND, + ) + serializer = IssueSerializer(issue, data=request.data, partial=True) if serializer.is_valid(): @@ -2286,17 +2335,52 @@ class IssueDraftViewSet(BaseViewSet): notification=True, origin=request.META.get("HTTP_ORIGIN"), ) - return Response(serializer.data, status=status.HTTP_200_OK) + return Response(status=status.HTTP_204_NO_CONTENT) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, slug, project_id, pk=None): - issue = self.get_queryset().filter(pk=pk).first() - return Response( - IssueSerializer( - issue, fields=self.fields, expand=self.expand - ).data, - status=status.HTTP_200_OK, - ) + issue = ( + self.get_queryset() + .filter(pk=pk) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related( + "issue", "actor" + ), + ) + ) + .prefetch_related( + Prefetch( + "issue_attachment", + queryset=IssueAttachment.objects.select_related("issue"), + ) + ) + .prefetch_related( + Prefetch( + "issue_link", + queryset=IssueLink.objects.select_related("created_by"), + ) + ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=OuterRef("pk"), + subscriber=request.user, + ) + ) + ) + ).first() + + if not issue: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + serializer = IssueDetailSerializer(issue, expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) def destroy(self, request, slug, project_id, pk=None): issue = Issue.objects.get( diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index 3c9bc8b60..6677b4c4b 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -560,7 +560,6 @@ class WorkSpaceMemberViewSet(BaseViewSet): .get_queryset() .filter( workspace__slug=self.kwargs.get("slug"), - member__is_bot=False, is_active=True, ) .select_related("workspace", "workspace__owner") @@ -768,7 +767,6 @@ class WorkspaceProjectMemberEndpoint(BaseAPIView): project_ids = ( ProjectMember.objects.filter( member=request.user, - member__is_bot=False, is_active=True, ) .values_list("project_id", flat=True) @@ -778,7 +776,6 @@ class WorkspaceProjectMemberEndpoint(BaseAPIView): # Get all the project members in which the user is involved project_members = ProjectMember.objects.filter( workspace__slug=slug, - member__is_bot=False, project_id__in=project_ids, is_active=True, ).select_related("project", "member", "workspace") diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py index 617bfcfdc..2a98c6b33 100644 --- a/apiserver/plane/bgtasks/email_notification_task.py +++ b/apiserver/plane/bgtasks/email_notification_task.py @@ -10,6 +10,7 @@ from django.utils import timezone from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string from django.utils.html import strip_tags +from django.conf import settings # Module imports from plane.db.models import EmailNotificationLog, User, Issue @@ -301,5 +302,7 @@ def send_email_notification( print("Duplicate task recived. Skipping...") return except (Issue.DoesNotExist, User.DoesNotExist) as e: + if settings.DEBUG: + print(e) release_lock(lock_id=lock_id) return diff --git a/packages/editor/core/src/lib/editor-commands.ts b/packages/editor/core/src/lib/editor-commands.ts index 4a56f07c2..6524d1ff5 100644 --- a/packages/editor/core/src/lib/editor-commands.ts +++ b/packages/editor/core/src/lib/editor-commands.ts @@ -97,8 +97,8 @@ export const insertTableCommand = (editor: Editor, range?: Range) => { } } } - if (range) editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); - else editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); + if (range) editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3 }).run(); + else editor.chain().focus().insertTable({ rows: 3, cols: 3 }).run(); }; export const unsetLinkEditor = (editor: Editor) => { diff --git a/packages/editor/core/src/styles/editor.css b/packages/editor/core/src/styles/editor.css index b0d2a1021..dbbea671e 100644 --- a/packages/editor/core/src/styles/editor.css +++ b/packages/editor/core/src/styles/editor.css @@ -170,68 +170,6 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { } } -#editor-container { - table { - border-collapse: collapse; - table-layout: fixed; - margin: 0.5em 0 0.5em 0; - - border: 1px solid rgb(var(--color-border-200)); - width: 100%; - - td, - th { - min-width: 1em; - border: 1px solid rgb(var(--color-border-200)); - padding: 10px 15px; - vertical-align: top; - box-sizing: border-box; - position: relative; - transition: background-color 0.3s ease; - - > * { - margin-bottom: 0; - } - } - - th { - font-weight: bold; - text-align: left; - background-color: rgb(var(--color-primary-100)); - } - - td:hover { - background-color: rgba(var(--color-primary-300), 0.1); - } - - .selectedCell:after { - z-index: 2; - position: absolute; - content: ""; - left: 0; - right: 0; - top: 0; - bottom: 0; - background-color: rgba(var(--color-primary-300), 0.1); - pointer-events: none; - } - - .column-resize-handle { - position: absolute; - right: -2px; - top: 0; - bottom: -2px; - width: 2px; - background-color: rgb(var(--color-primary-400)); - pointer-events: none; - } - } -} - -.tableWrapper { - overflow-x: auto; -} - .resize-cursor { cursor: ew-resize; cursor: col-resize; diff --git a/packages/editor/core/src/styles/table.css b/packages/editor/core/src/styles/table.css index 8a47a8c59..ca384d34f 100644 --- a/packages/editor/core/src/styles/table.css +++ b/packages/editor/core/src/styles/table.css @@ -9,15 +9,15 @@ border-collapse: collapse; table-layout: fixed; margin: 0; - margin-bottom: 3rem; - border: 1px solid rgba(var(--color-border-200)); + margin-bottom: 1rem; + border: 2px solid rgba(var(--color-border-300)); width: 100%; } .tableWrapper table td, .tableWrapper table th { min-width: 1em; - border: 1px solid rgba(var(--color-border-200)); + border: 1px solid rgba(var(--color-border-300)); padding: 10px 15px; vertical-align: top; box-sizing: border-box; @@ -43,7 +43,8 @@ .tableWrapper table th { font-weight: bold; text-align: left; - background-color: rgba(var(--color-primary-100)); + background-color: #d9e4ff; + color: #171717; } .tableWrapper table th * { @@ -62,6 +63,35 @@ pointer-events: none; } +.colorPicker { + display: grid; + padding: 8px 8px; + grid-template-columns: repeat(6, 1fr); + gap: 5px; +} + +.colorPickerLabel { + font-size: 0.85rem; + color: #6b7280; + padding: 8px 8px; + padding-bottom: 0px; +} + +.colorPickerItem { + margin: 2px 0px; + width: 24px; + height: 24px; + border-radius: 4px; + border: none; + cursor: pointer; +} + +.divider { + background-color: #e5e7eb; + height: 1px; + margin: 3px 0; +} + .tableWrapper table .column-resize-handle { position: absolute; right: -2px; @@ -69,7 +99,7 @@ bottom: -2px; width: 4px; z-index: 99; - background-color: rgba(var(--color-primary-400)); + background-color: #d9e4ff; pointer-events: none; } @@ -112,7 +142,7 @@ } .tableWrapper .tableControls .rowsControlDiv { - background-color: rgba(var(--color-primary-100)); + background-color: #d9e4ff; border: 1px solid rgba(var(--color-border-200)); border-radius: 2px; background-size: 1.25rem; @@ -127,7 +157,7 @@ } .tableWrapper .tableControls .columnsControlDiv { - background-color: rgba(var(--color-primary-100)); + background-color: #d9e4ff; border: 1px solid rgba(var(--color-border-200)); border-radius: 2px; background-size: 1.25rem; @@ -144,10 +174,12 @@ .tableWrapper .tableControls .tableColorPickerToolbox { border: 1px solid rgba(var(--color-border-300)); background-color: rgba(var(--color-background-100)); + border-radius: 5px; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); padding: 0.25rem; display: flex; flex-direction: column; - width: 200px; + width: max-content; gap: 0.25rem; } @@ -158,7 +190,7 @@ align-items: center; gap: 0.5rem; border: none; - padding: 0.1rem; + padding: 0.3rem 0.5rem 0.1rem 0.1rem; border-radius: 4px; cursor: pointer; transition: all 0.2s; @@ -173,9 +205,7 @@ .tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .iconContainer, .tableWrapper .tableControls .tableToolbox .toolboxItem .colorContainer, .tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .colorContainer { - border: 1px solid rgba(var(--color-border-300)); - border-radius: 3px; - padding: 4px; + padding: 4px 0px; display: flex; align-items: center; justify-content: center; @@ -187,8 +217,8 @@ .tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .iconContainer svg, .tableWrapper .tableControls .tableToolbox .toolboxItem .colorContainer svg, .tableWrapper .tableControls .tableColorPickerToolbox .toolboxItem .colorContainer svg { - width: 2rem; - height: 2rem; + width: 1rem; + height: 1rem; } .tableToolbox { diff --git a/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts b/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts index aedb59411..403bd3f02 100644 --- a/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts +++ b/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts @@ -13,7 +13,7 @@ export const TableCell = Node.create({ }; }, - content: "paragraph+", + content: "block+", addAttributes() { return { @@ -33,7 +33,10 @@ export const TableCell = Node.create({ }, }, background: { - default: "none", + default: null, + }, + textColor: { + default: null, }, }; }, @@ -50,7 +53,7 @@ export const TableCell = Node.create({ return [ "td", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { - style: `background-color: ${node.attrs.background}`, + style: `background-color: ${node.attrs.background}; color: ${node.attrs.textColor}`, }), 0, ]; diff --git a/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts b/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts index c0decdbf8..bd994f467 100644 --- a/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts +++ b/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts @@ -33,7 +33,7 @@ export const TableHeader = Node.create({ }, }, background: { - default: "rgb(var(--color-primary-100))", + default: "none", }, }; }, diff --git a/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts b/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts index 28c9a9a48..f961c0582 100644 --- a/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts +++ b/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts @@ -13,6 +13,17 @@ export const TableRow = Node.create({ }; }, + addAttributes() { + return { + background: { + default: null, + }, + textColor: { + default: null, + }, + }; + }, + content: "(tableCell | tableHeader)*", tableRole: "row", @@ -22,6 +33,12 @@ export const TableRow = Node.create({ }, renderHTML({ HTMLAttributes }) { - return ["tr", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; + const style = HTMLAttributes.background + ? `background-color: ${HTMLAttributes.background}; color: ${HTMLAttributes.textColor}` + : ""; + + const attributes = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { style }); + + return ["tr", attributes, 0]; }, }); diff --git a/packages/editor/core/src/ui/extensions/table/table/icons.ts b/packages/editor/core/src/ui/extensions/table/table/icons.ts index c08710ec3..f73c55c09 100644 --- a/packages/editor/core/src/ui/extensions/table/table/icons.ts +++ b/packages/editor/core/src/ui/extensions/table/table/icons.ts @@ -1,7 +1,7 @@ export const icons = { colorPicker: ``, - deleteColumn: ``, - deleteRow: ``, + deleteColumn: ``, + deleteRow: ``, insertLeftTableIcon: ` `, + toggleColumnHeader: ``, + toggleRowHeader: ``, insertBottomTableIcon: ` = { placement: "right", }; -function setCellsBackgroundColor(editor: Editor, backgroundColor: string) { +function setCellsBackgroundColor(editor: Editor, color: { backgroundColor: string; textColor: string }) { return editor .chain() .focus() .updateAttributes("tableCell", { - background: backgroundColor, - }) - .updateAttributes("tableHeader", { - background: backgroundColor, + background: color.backgroundColor, + textColor: color.textColor, }) .run(); } +function setTableRowBackgroundColor(editor: Editor, color: { backgroundColor: string; textColor: string }) { + const { state, dispatch } = editor.view; + const { selection } = state; + if (!(selection instanceof CellSelection)) { + return false; + } + + // Get the position of the hovered cell in the selection to determine the row. + const hoveredCell = selection.$headCell || selection.$anchorCell; + + // Find the depth of the table row node + let rowDepth = hoveredCell.depth; + while (rowDepth > 0 && hoveredCell.node(rowDepth).type.name !== "tableRow") { + rowDepth--; + } + + // If we couldn't find a tableRow node, we can't set the background color + if (hoveredCell.node(rowDepth).type.name !== "tableRow") { + return false; + } + + // Get the position where the table row starts + const rowStartPos = hoveredCell.start(rowDepth); + + // Create a transaction that sets the background color on the tableRow node. + const tr = state.tr.setNodeMarkup(rowStartPos - 1, null, { + ...hoveredCell.node(rowDepth).attrs, + background: color.backgroundColor, + textColor: color.textColor, + }); + + dispatch(tr); + return true; +} + const columnsToolboxItems: ToolboxItem[] = [ { - label: "Add Column Before", + label: "Toggle column header", + icon: icons.toggleColumnHeader, + action: ({ editor }: { editor: Editor }) => editor.chain().focus().toggleHeaderColumn().run(), + }, + { + label: "Add column before", icon: icons.insertLeftTableIcon, action: ({ editor }: { editor: Editor }) => editor.chain().focus().addColumnBefore().run(), }, { - label: "Add Column After", + label: "Add column after", icon: icons.insertRightTableIcon, action: ({ editor }: { editor: Editor }) => editor.chain().focus().addColumnAfter().run(), }, { - label: "Pick Column Color", - icon: icons.colorPicker, - action: ({ - editor, - triggerButton, - controlsContainer, - }: { - editor: Editor; - triggerButton: HTMLElement; - controlsContainer: Element; - }) => { - createColorPickerToolbox({ - triggerButton, - tippyOptions: { - appendTo: controlsContainer, - }, - onSelectColor: (color) => setCellsBackgroundColor(editor, color), - }); - }, + label: "Pick color", + icon: "", // No icon needed for color picker + action: (args: any) => {}, // Placeholder action; actual color picking is handled in `createToolbox` }, { - label: "Delete Column", + label: "Delete column", icon: icons.deleteColumn, action: ({ editor }: { editor: Editor }) => editor.chain().focus().deleteColumn().run(), }, @@ -135,35 +157,24 @@ const columnsToolboxItems: ToolboxItem[] = [ const rowsToolboxItems: ToolboxItem[] = [ { - label: "Add Row Above", + label: "Toggle row header", + icon: icons.toggleRowHeader, + action: ({ editor }: { editor: Editor }) => editor.chain().focus().toggleHeaderRow().run(), + }, + { + label: "Add row above", icon: icons.insertTopTableIcon, action: ({ editor }: { editor: Editor }) => editor.chain().focus().addRowBefore().run(), }, { - label: "Add Row Below", + label: "Add row below", icon: icons.insertBottomTableIcon, action: ({ editor }: { editor: Editor }) => editor.chain().focus().addRowAfter().run(), }, { - label: "Pick Row Color", - icon: icons.colorPicker, - action: ({ - editor, - triggerButton, - controlsContainer, - }: { - editor: Editor; - triggerButton: HTMLButtonElement; - controlsContainer: Element | "parent" | ((ref: Element) => Element) | undefined; - }) => { - createColorPickerToolbox({ - triggerButton, - tippyOptions: { - appendTo: controlsContainer, - }, - onSelectColor: (color) => setCellsBackgroundColor(editor, color), - }); - }, + label: "Pick color", + icon: "", + action: (args: any) => {}, // Placeholder action; actual color picking is handled in `createToolbox` }, { label: "Delete Row", @@ -176,37 +187,57 @@ function createToolbox({ triggerButton, items, tippyOptions, + onSelectColor, onClickItem, + colors, }: { triggerButton: Element | null; items: ToolboxItem[]; tippyOptions: any; onClickItem: (item: ToolboxItem) => void; + onSelectColor: (color: { backgroundColor: string; textColor: string }) => void; + colors: { [key: string]: { backgroundColor: string; textColor: string; icon?: string } }; }): Instance { // @ts-expect-error const toolbox = tippy(triggerButton, { content: h( "div", { className: "tableToolbox" }, - items.map((item) => - h( - "div", - { - className: "toolboxItem", - itemType: "button", - onClick() { - onClickItem(item); + items.map((item, index) => { + if (item.label === "Pick color") { + return h("div", { className: "flex flex-col" }, [ + h("div", { className: "divider" }), + h("div", { className: "colorPickerLabel" }, item.label), + h( + "div", + { className: "colorPicker grid" }, + Object.entries(colors).map(([colorName, colorValue]) => + h("div", { + className: "colorPickerItem", + style: `background-color: ${colorValue.backgroundColor}; + color: ${colorValue.textColor || "inherit"};`, + innerHTML: colorValue?.icon || "", + onClick: () => onSelectColor(colorValue), + }) + ) + ), + h("div", { className: "divider" }), + ]); + } else { + return h( + "div", + { + className: "toolboxItem", + itemType: "div", + onClick: () => onClickItem(item), }, - }, - [ - h("div", { - className: "iconContainer", - innerHTML: item.icon, - }), - h("div", { className: "label" }, item.label), - ] - ) - ) + [ + h("div", { className: "iconContainer", innerHTML: item.icon }), + h("div", { className: "label" }, item.label), + ] + ); + } + }) ), ...tippyOptions, }); @@ -214,71 +245,6 @@ function createToolbox({ return Array.isArray(toolbox) ? toolbox[0] : toolbox; } -function createColorPickerToolbox({ - triggerButton, - tippyOptions, - onSelectColor = () => {}, -}: { - triggerButton: HTMLElement; - tippyOptions: Partial; - onSelectColor?: (color: string) => void; -}) { - const items = { - Default: "rgb(var(--color-primary-100))", - Orange: "#FFE5D1", - Grey: "#F1F1F1", - Yellow: "#FEF3C7", - Green: "#DCFCE7", - Red: "#FFDDDD", - Blue: "#D9E4FF", - Pink: "#FFE8FA", - Purple: "#E8DAFB", - }; - - const colorPicker = tippy(triggerButton, { - ...defaultTippyOptions, - content: h( - "div", - { className: "tableColorPickerToolbox" }, - Object.entries(items).map(([key, value]) => - h( - "div", - { - className: "toolboxItem", - itemType: "button", - onClick: () => { - onSelectColor(value); - colorPicker.hide(); - }, - }, - [ - h("div", { - className: "colorContainer", - style: { - backgroundColor: value, - }, - }), - h( - "div", - { - className: "label", - }, - key - ), - ] - ) - ) - ), - onHidden: (instance) => { - instance.destroy(); - }, - showOnCreate: true, - ...tippyOptions, - }); - - return colorPicker; -} - export class TableView implements NodeView { node: ProseMirrorNode; cellMinWidth: number; @@ -347,10 +313,27 @@ export class TableView implements NodeView { this.rowsControl, this.columnsControl ); + const columnColors = { + Blue: { backgroundColor: "#D9E4FF", textColor: "#171717" }, + Orange: { backgroundColor: "#FFEDD5", textColor: "#171717" }, + Grey: { backgroundColor: "#F1F1F1", textColor: "#171717" }, + Yellow: { backgroundColor: "#FEF3C7", textColor: "#171717" }, + Green: { backgroundColor: "#DCFCE7", textColor: "#171717" }, + Red: { backgroundColor: "#FFDDDD", textColor: "#171717" }, + Pink: { backgroundColor: "#FFE8FA", textColor: "#171717" }, + Purple: { backgroundColor: "#E8DAFB", textColor: "#171717" }, + None: { + backgroundColor: "none", + textColor: "none", + icon: ``, + }, + }; this.columnsToolbox = createToolbox({ triggerButton: this.columnsControl.querySelector(".columnsControlDiv"), items: columnsToolboxItems, + colors: columnColors, + onSelectColor: (color) => setCellsBackgroundColor(this.editor, color), tippyOptions: { ...defaultTippyOptions, appendTo: this.controls, @@ -368,10 +351,12 @@ export class TableView implements NodeView { this.rowsToolbox = createToolbox({ triggerButton: this.rowsControl.firstElementChild, items: rowsToolboxItems, + colors: columnColors, tippyOptions: { ...defaultTippyOptions, appendTo: this.controls, }, + onSelectColor: (color) => setTableRowBackgroundColor(editor, color), onClickItem: (item) => { item.action({ editor: this.editor, @@ -383,8 +368,6 @@ export class TableView implements NodeView { }); } - // Table - this.colgroup = h( "colgroup", null, @@ -437,16 +420,19 @@ export class TableView implements NodeView { } updateControls() { - const { hoveredTable: table, hoveredCell: cell } = Object.values(this.decorations).reduce((acc, curr) => { - if (curr.spec.hoveredCell !== undefined) { - acc["hoveredCell"] = curr.spec.hoveredCell; - } + const { hoveredTable: table, hoveredCell: cell } = Object.values(this.decorations).reduce( + (acc, curr) => { + if (curr.spec.hoveredCell !== undefined) { + acc["hoveredCell"] = curr.spec.hoveredCell; + } - if (curr.spec.hoveredTable !== undefined) { - acc["hoveredTable"] = curr.spec.hoveredTable; - } - return acc; - }, {} as Record) as any; + if (curr.spec.hoveredTable !== undefined) { + acc["hoveredTable"] = curr.spec.hoveredTable; + } + return acc; + }, + {} as Record + ) as any; if (table === undefined || cell === undefined) { return this.root.classList.add("controls--disabled"); @@ -457,12 +443,12 @@ export class TableView implements NodeView { const cellDom = this.editor.view.nodeDOM(cell.pos) as HTMLElement; - if (!this.table) { + if (!this.table || !cellDom) { return; } - const tableRect = this.table.getBoundingClientRect(); - const cellRect = cellDom.getBoundingClientRect(); + const tableRect = this.table?.getBoundingClientRect(); + const cellRect = cellDom?.getBoundingClientRect(); if (this.columnsControl) { this.columnsControl.style.left = `${cellRect.left - tableRect.left - this.table.parentElement!.scrollLeft}px`; diff --git a/packages/editor/core/src/ui/extensions/table/table/table.ts b/packages/editor/core/src/ui/extensions/table/table/table.ts index 5600fd82a..ef595eee2 100644 --- a/packages/editor/core/src/ui/extensions/table/table/table.ts +++ b/packages/editor/core/src/ui/extensions/table/table/table.ts @@ -107,10 +107,9 @@ export const Table = Node.create({ addCommands() { return { insertTable: - ({ rows = 3, cols = 3, withHeaderRow = true } = {}) => + ({ rows = 3, cols = 3, withHeaderRow = false } = {}) => ({ tr, dispatch, editor }) => { const node = createTable(editor.schema, rows, cols, withHeaderRow); - if (dispatch) { const offset = tr.selection.anchor + 1; diff --git a/packages/editor/core/src/ui/props.tsx b/packages/editor/core/src/ui/props.tsx index 2aaeb4264..1846efe47 100644 --- a/packages/editor/core/src/ui/props.tsx +++ b/packages/editor/core/src/ui/props.tsx @@ -42,15 +42,6 @@ export function CoreEditorProps( return false; }, handleDrop: (view, event, _slice, moved) => { - if (typeof window !== "undefined") { - const selection: any = window?.getSelection(); - if (selection.rangeCount !== 0) { - const range = selection.getRangeAt(0); - if (findTableAncestor(range.startContainer)) { - return; - } - } - } if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) { event.preventDefault(); const file = event.dataTransfer.files[0]; diff --git a/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx b/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx index be57a4a91..397e8c576 100644 --- a/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx +++ b/packages/editor/document-editor/src/ui/menu/fixed-menu.tsx @@ -48,34 +48,12 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => { function getComplexItems(): BubbleMenuItem[] { const items: BubbleMenuItem[] = [TableItem(editor)]; - if (shouldShowImageItem()) { - items.push(ImageItem(editor, uploadFile, setIsSubmitting)); - } - + items.push(ImageItem(editor, uploadFile, setIsSubmitting)); return items; } const complexItems: BubbleMenuItem[] = getComplexItems(); - function shouldShowImageItem(): boolean { - if (typeof window !== "undefined") { - const selectionRange: any = window?.getSelection(); - const { selection } = props.editor.state; - - if (selectionRange.rangeCount !== 0) { - const range = selectionRange.getRangeAt(0); - if (findTableAncestor(range.startContainer)) { - return false; - } - if (isCellSelection(selection)) { - return false; - } - } - return true; - } - return false; - } - return (
diff --git a/packages/editor/extensions/src/extensions/drag-drop.tsx b/packages/editor/extensions/src/extensions/drag-drop.tsx index af99fec61..ce4088413 100644 --- a/packages/editor/extensions/src/extensions/drag-drop.tsx +++ b/packages/editor/extensions/src/extensions/drag-drop.tsx @@ -35,7 +35,7 @@ export interface DragHandleOptions { } function absoluteRect(node: Element) { - const data = node.getBoundingClientRect(); + const data = node?.getBoundingClientRect(); return { top: data.top, @@ -65,7 +65,7 @@ function nodeDOMAtCoords(coords: { x: number; y: number }) { } function nodePosAtDOM(node: Element, view: EditorView) { - const boundingRect = node.getBoundingClientRect(); + const boundingRect = node?.getBoundingClientRect(); if (node.nodeName === "IMG") { return view.posAtCoords({ diff --git a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx index 71ad4e0e1..c6786698d 100644 --- a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx @@ -60,34 +60,13 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => { function getComplexItems(): BubbleMenuItem[] { const items: BubbleMenuItem[] = [TableItem(props.editor)]; - if (shouldShowImageItem()) { - items.push(ImageItem(props.editor, props.uploadFile, props.setIsSubmitting)); - } + items.push(ImageItem(props.editor, props.uploadFile, props.setIsSubmitting)); return items; } const complexItems: BubbleMenuItem[] = getComplexItems(); - function shouldShowImageItem(): boolean { - if (typeof window !== "undefined") { - const selectionRange: any = window?.getSelection(); - const { selection } = props.editor.state; - - if (selectionRange.rangeCount !== 0) { - const range = selectionRange.getRangeAt(0); - if (findTableAncestor(range.startContainer)) { - return false; - } - if (isCellSelection(selection)) { - return false; - } - } - return true; - } - return false; - } - const handleAccessChange = (accessKey: string) => { props.commentAccessSpecifier?.onAccessChange(accessKey); }; diff --git a/packages/types/src/cycles.d.ts b/packages/types/src/cycles.d.ts index 0e4890b7f..e7ec66ae2 100644 --- a/packages/types/src/cycles.d.ts +++ b/packages/types/src/cycles.d.ts @@ -30,7 +30,7 @@ export interface ICycle { is_favorite: boolean; issue: string; name: string; - owned_by: string; + owned_by_id: string; progress_snapshot: TProgressSnapshot; project_id: string; status: TCycleGroups; diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts index 527abe630..42c95dc4e 100644 --- a/packages/types/src/issues/issue.d.ts +++ b/packages/types/src/issues/issue.d.ts @@ -1,4 +1,7 @@ import { TIssuePriorities } from "../issues"; +import { TIssueAttachment } from "./issue_attachment"; +import { TIssueLink } from "./issue_link"; +import { TIssueReaction } from "./issue_reaction"; // new issue structure types export type TIssue = { @@ -34,7 +37,12 @@ export type TIssue = { updated_by: string; is_draft: boolean; - is_subscribed: boolean; + is_subscribed?: boolean; + + parent?: partial; + issue_reactions?: TIssueReaction[]; + issue_attachment?: TIssueAttachment[]; + issue_link?: TIssueLink[]; // tempId is used for optimistic updates. It is not a part of the API response. tempId?: string; diff --git a/packages/types/src/issues/issue_attachment.d.ts b/packages/types/src/issues/issue_attachment.d.ts index 90daa08fa..7c3819e00 100644 --- a/packages/types/src/issues/issue_attachment.d.ts +++ b/packages/types/src/issues/issue_attachment.d.ts @@ -1,17 +1,15 @@ export type TIssueAttachment = { id: string; - created_at: string; - updated_at: string; attributes: { name: string; size: number; }; asset: string; - created_by: string; + issue_id: string; + + //need + updated_at: string; updated_by: string; - project: string; - workspace: string; - issue: string; }; export type TIssueAttachmentMap = { diff --git a/packages/types/src/issues/issue_link.d.ts b/packages/types/src/issues/issue_link.d.ts index 2c469e682..10f0d2792 100644 --- a/packages/types/src/issues/issue_link.d.ts +++ b/packages/types/src/issues/issue_link.d.ts @@ -4,11 +4,13 @@ export type TIssueLinkEditableFields = { }; export type TIssueLink = TIssueLinkEditableFields & { - created_at: Date; - created_by: string; - created_by_detail: IUserLite; + created_by_id: string; id: string; metadata: any; + issue_id: string; + + //need + created_at: Date; }; export type TIssueLinkMap = { diff --git a/packages/types/src/issues/issue_reaction.d.ts b/packages/types/src/issues/issue_reaction.d.ts index 88ef27426..a4eaee0a8 100644 --- a/packages/types/src/issues/issue_reaction.d.ts +++ b/packages/types/src/issues/issue_reaction.d.ts @@ -1,15 +1,8 @@ export type TIssueReaction = { - actor: string; - actor_detail: IUserLite; - created_at: Date; - created_by: string; + actor_id: string; id: string; - issue: string; - project: string; + issue_id: string; reaction: string; - updated_at: Date; - updated_by: string; - workspace: string; }; export type TIssueReactionMap = { diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx index c2644abe0..6a7b3c7b9 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx @@ -20,7 +20,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => { const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined; const projectDetails = projectId ? getProjectById(projectId.toString()) : undefined; - const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by) : undefined; + const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by_id) : undefined; const moduleLeadDetails = moduleDetails && moduleDetails.lead_id ? getUserDetails(moduleDetails.lead_id) : undefined; return ( diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index 7e885635f..1fae0412f 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -69,7 +69,7 @@ export const ActiveCycleDetails: React.FC = observer((props ); const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null; - const cycleOwnerDetails = activeCycle ? getUserDetails(activeCycle.owned_by) : undefined; + const cycleOwnerDetails = activeCycle ? getUserDetails(activeCycle.owned_by_id) : undefined; const { data: activeCycleIssues } = useSWR( workspaceSlug && projectId && currentProjectActiveCycleId diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 20605a90c..646736bd2 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -59,7 +59,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const { getUserDetails } = useMember(); // derived values const cycleDetails = getCycleById(cycleId); - const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by) : undefined; + const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by_id) : undefined; // toast alert const { setToastAlert } = useToast(); // form info diff --git a/web/components/cycles/transfer-issues.tsx b/web/components/cycles/transfer-issues.tsx index 7c0d0d475..517df4421 100644 --- a/web/components/cycles/transfer-issues.tsx +++ b/web/components/cycles/transfer-issues.tsx @@ -16,12 +16,13 @@ import { CYCLE_DETAILS } from "constants/fetch-keys"; type Props = { handleClick: () => void; + disabled?: boolean; }; const cycleService = new CycleService(); export const TransferIssues: React.FC = (props) => { - const { handleClick } = props; + const { handleClick, disabled = false } = props; const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; @@ -46,7 +47,12 @@ export const TransferIssues: React.FC = (props) => { {isEmpty(cycleDetails?.progress_snapshot) && transferableIssuesCount > 0 && (
-
diff --git a/web/components/dashboard/widgets/issue-panels/issues-list.tsx b/web/components/dashboard/widgets/issue-panels/issues-list.tsx index cf3f32232..3f1250d4d 100644 --- a/web/components/dashboard/widgets/issue-panels/issues-list.tsx +++ b/web/components/dashboard/widgets/issue-panels/issues-list.tsx @@ -14,7 +14,7 @@ import { IssueListItemProps, } from "components/dashboard/widgets"; // ui -import { getButtonStyling } from "@plane/ui"; +import { Loader, getButtonStyling } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; import { getRedirectionFilters } from "helpers/dashboard.helper"; @@ -63,7 +63,12 @@ export const WidgetIssuesList: React.FC = (props) => { <>
{isLoading ? ( - <> + + + + + + ) : issues.length > 0 ? ( <>
diff --git a/web/components/gantt-chart/blocks/block.tsx b/web/components/gantt-chart/blocks/block.tsx new file mode 100644 index 000000000..1e0882aee --- /dev/null +++ b/web/components/gantt-chart/blocks/block.tsx @@ -0,0 +1,106 @@ +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "../hooks"; +import { useIssueDetail } from "hooks/store"; +// components +import { ChartAddBlock, ChartDraggable } from "../helpers"; +// helpers +import { cn } from "helpers/common.helper"; +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +// types +import { IBlockUpdateData, IGanttBlock } from "../types"; +// constants +import { BLOCK_HEIGHT } from "../constants"; + +type Props = { + block: IGanttBlock; + blockToRender: (data: any) => React.ReactNode; + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + enableBlockLeftResize: boolean; + enableBlockRightResize: boolean; + enableBlockMove: boolean; + enableAddBlock: boolean; + ganttContainerRef: React.RefObject; +}; + +export const GanttChartBlock: React.FC = observer((props) => { + const { + block, + blockToRender, + blockUpdateHandler, + enableBlockLeftResize, + enableBlockRightResize, + enableBlockMove, + enableAddBlock, + ganttContainerRef, + } = props; + // store hooks + const { updateActiveBlockId, isBlockActive } = useGanttChart(); + const { peekIssue } = useIssueDetail(); + + const isBlockVisibleOnChart = block.start_date && block.target_date; + + const handleChartBlockPosition = ( + block: IGanttBlock, + totalBlockShifts: number, + dragDirection: "left" | "right" | "move" + ) => { + if (!block.start_date || !block.target_date) return; + + const originalStartDate = new Date(block.start_date); + const updatedStartDate = new Date(originalStartDate); + + const originalTargetDate = new Date(block.target_date); + const updatedTargetDate = new Date(originalTargetDate); + + // update the start date on left resize + if (dragDirection === "left") updatedStartDate.setDate(originalStartDate.getDate() - totalBlockShifts); + // update the target date on right resize + else if (dragDirection === "right") updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts); + // update both the dates on x-axis move + else if (dragDirection === "move") { + updatedStartDate.setDate(originalStartDate.getDate() + totalBlockShifts); + updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts); + } + + // call the block update handler with the updated dates + blockUpdateHandler(block.data, { + start_date: renderFormattedPayloadDate(updatedStartDate) ?? undefined, + target_date: renderFormattedPayloadDate(updatedTargetDate) ?? undefined, + }); + }; + + return ( +
+
updateActiveBlockId(block.id)} + onMouseLeave={() => updateActiveBlockId(null)} + > + {isBlockVisibleOnChart ? ( + handleChartBlockPosition(block, ...args)} + enableBlockLeftResize={enableBlockLeftResize} + enableBlockRightResize={enableBlockRightResize} + enableBlockMove={enableBlockMove} + ganttContainerRef={ganttContainerRef} + /> + ) : ( + enableAddBlock && + )} +
+
+ ); +}); diff --git a/web/components/gantt-chart/blocks/blocks-list.tsx b/web/components/gantt-chart/blocks/blocks-list.tsx index 15a3e5295..d98524ecc 100644 --- a/web/components/gantt-chart/blocks/blocks-list.tsx +++ b/web/components/gantt-chart/blocks/blocks-list.tsx @@ -1,16 +1,10 @@ -import { observer } from "mobx-react"; import { FC } from "react"; -// hooks -import { useIssueDetail } from "hooks/store"; -import { useChart } from "../hooks"; -// helpers -import { ChartAddBlock, ChartDraggable } from "components/gantt-chart"; -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; -import { cn } from "helpers/common.helper"; +// components +import { GanttChartBlock } from "./block"; // types import { IBlockUpdateData, IGanttBlock } from "../types"; // constants -import { BLOCK_HEIGHT, HEADER_HEIGHT } from "../constants"; +import { HEADER_HEIGHT } from "../constants"; export type GanttChartBlocksProps = { itemsContainerWidth: number; @@ -21,10 +15,11 @@ export type GanttChartBlocksProps = { enableBlockRightResize: boolean; enableBlockMove: boolean; enableAddBlock: boolean; + ganttContainerRef: React.RefObject; showAllBlocks: boolean; }; -export const GanttChartBlocksList: FC = observer((props) => { +export const GanttChartBlocksList: FC = (props) => { const { itemsContainerWidth, blocks, @@ -34,52 +29,9 @@ export const GanttChartBlocksList: FC = observer((props) enableBlockRightResize, enableBlockMove, enableAddBlock, + ganttContainerRef, showAllBlocks, } = props; - // store hooks - const { peekIssue } = useIssueDetail(); - // chart hook - const { activeBlock, dispatch } = useChart(); - - // update the active block on hover - const updateActiveBlock = (block: IGanttBlock | null) => { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - activeBlock: block, - }, - }); - }; - - const handleChartBlockPosition = ( - block: IGanttBlock, - totalBlockShifts: number, - dragDirection: "left" | "right" | "move" - ) => { - if (!block.start_date || !block.target_date) return; - - const originalStartDate = new Date(block.start_date); - const updatedStartDate = new Date(originalStartDate); - - const originalTargetDate = new Date(block.target_date); - const updatedTargetDate = new Date(originalTargetDate); - - // update the start date on left resize - if (dragDirection === "left") updatedStartDate.setDate(originalStartDate.getDate() - totalBlockShifts); - // update the target date on right resize - else if (dragDirection === "right") updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts); - // update both the dates on x-axis move - else if (dragDirection === "move") { - updatedStartDate.setDate(originalStartDate.getDate() + totalBlockShifts); - updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts); - } - - // call the block update handler with the updated dates - blockUpdateHandler(block.data, { - start_date: renderFormattedPayloadDate(updatedStartDate) ?? undefined, - target_date: renderFormattedPayloadDate(updatedTargetDate) ?? undefined, - }); - }; return (
= observer((props) // hide the block if it doesn't have start and target dates and showAllBlocks is false if (!showAllBlocks && !(block.start_date && block.target_date)) return; - const isBlockVisibleOnChart = block.start_date && block.target_date; - return ( -
-
updateActiveBlock(block)} - onMouseLeave={() => updateActiveBlock(null)} - > - {isBlockVisibleOnChart ? ( - handleChartBlockPosition(block, ...args)} - enableBlockLeftResize={enableBlockLeftResize} - enableBlockRightResize={enableBlockRightResize} - enableBlockMove={enableBlockMove} - /> - ) : ( - enableAddBlock && - )} -
-
+ ); })}
); -}); +}; diff --git a/web/components/gantt-chart/chart/header.tsx b/web/components/gantt-chart/chart/header.tsx index 6dcfdc36f..2ebd0360d 100644 --- a/web/components/gantt-chart/chart/header.tsx +++ b/web/components/gantt-chart/chart/header.tsx @@ -1,10 +1,13 @@ import { Expand, Shrink } from "lucide-react"; // hooks -import { useChart } from "../hooks"; // helpers import { cn } from "helpers/common.helper"; // types import { IGanttBlock, TGanttViews } from "../types"; +// constants +import { VIEWS_LIST } from "components/gantt-chart/data"; +import { useGanttChart } from "../hooks/use-gantt-chart"; +import { observer } from "mobx-react"; type Props = { blocks: IGanttBlock[] | null; @@ -16,10 +19,10 @@ type Props = { toggleFullScreenMode: () => void; }; -export const GanttChartHeader: React.FC = (props) => { +export const GanttChartHeader: React.FC = observer((props) => { const { blocks, fullScreenMode, handleChartView, handleToday, loaderTitle, title, toggleFullScreenMode } = props; // chart hook - const { currentView, allViews } = useChart(); + const { currentView } = useGanttChart(); return (
@@ -29,7 +32,7 @@ export const GanttChartHeader: React.FC = (props) => {
- {allViews?.map((chartView: any) => ( + {VIEWS_LIST.map((chartView: any) => (
= (props) => {
); -}; +}); diff --git a/web/components/gantt-chart/chart/main-content.tsx b/web/components/gantt-chart/chart/main-content.tsx index 42670b972..0f7320986 100644 --- a/web/components/gantt-chart/chart/main-content.tsx +++ b/web/components/gantt-chart/chart/main-content.tsx @@ -1,3 +1,7 @@ +import { useRef } from "react"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "../hooks/use-gantt-chart"; // components import { BiWeekChartView, @@ -12,7 +16,6 @@ import { TGanttViews, WeekChartView, YearChartView, - useChart, } from "components/gantt-chart"; // helpers import { cn } from "helpers/common.helper"; @@ -36,7 +39,7 @@ type Props = { quickAdd?: React.JSX.Element | undefined; }; -export const GanttChartMainContent: React.FC = (props) => { +export const GanttChartMainContent: React.FC = observer((props) => { const { blocks, blockToRender, @@ -55,13 +58,15 @@ export const GanttChartMainContent: React.FC = (props) => { updateCurrentViewRenderPayload, quickAdd, } = props; + // refs + const ganttContainerRef = useRef(null); // chart hook - const { currentView, currentViewData, updateScrollLeft } = useChart(); + const { currentView, currentViewData } = useGanttChart(); // handling scroll functionality const onScroll = (e: React.UIEvent) => { const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget; - updateScrollLeft(scrollLeft); + // updateScrollLeft(scrollLeft); const approxRangeLeft = scrollLeft >= clientWidth + 1000 ? 1000 : scrollLeft - clientWidth; const approxRangeRight = scrollWidth - (scrollLeft + clientWidth); @@ -95,6 +100,7 @@ export const GanttChartMainContent: React.FC = (props) => { "mb-8": bottomSpacing, } )} + ref={ganttContainerRef} onScroll={onScroll} > = (props) => { title={title} quickAdd={quickAdd} /> -
+
{currentViewData && ( = (props) => { enableBlockRightResize={enableBlockRightResize} enableBlockMove={enableBlockMove} enableAddBlock={enableAddBlock} + ganttContainerRef={ganttContainerRef} showAllBlocks={showAllBlocks} /> )}
); -}; +}); diff --git a/web/components/gantt-chart/chart/root.tsx b/web/components/gantt-chart/chart/root.tsx index 877c15901..be6229ce3 100644 --- a/web/components/gantt-chart/chart/root.tsx +++ b/web/components/gantt-chart/chart/root.tsx @@ -1,6 +1,9 @@ import { FC, useEffect, useState } from "react"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "../hooks/use-gantt-chart"; // components -import { GanttChartHeader, useChart, GanttChartMainContent } from "components/gantt-chart"; +import { GanttChartHeader, GanttChartMainContent } from "components/gantt-chart"; // views import { generateMonthChart, @@ -34,7 +37,7 @@ type ChartViewRootProps = { quickAdd?: React.JSX.Element | undefined; }; -export const ChartViewRoot: FC = (props) => { +export const ChartViewRoot: FC = observer((props) => { const { border, title, @@ -57,7 +60,8 @@ export const ChartViewRoot: FC = (props) => { const [fullScreenMode, setFullScreenMode] = useState(false); const [chartBlocks, setChartBlocks] = useState(null); // hooks - const { currentView, currentViewData, renderView, dispatch } = useChart(); + const { currentView, currentViewData, renderView, updateCurrentView, updateCurrentViewData, updateRenderView } = + useGanttChart(); // rendering the block structure const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) => @@ -87,36 +91,20 @@ export const ChartViewRoot: FC = (props) => { // updating the prevData, currentData and nextData if (currentRender.payload.length > 0) { + updateCurrentViewData(currentRender.state); + if (side === "left") { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - currentView: selectedCurrentView, - currentViewData: currentRender.state, - renderView: [...currentRender.payload, ...renderView], - }, - }); + updateCurrentView(selectedCurrentView); + updateRenderView([...currentRender.payload, ...renderView]); updatingCurrentLeftScrollPosition(currentRender.scrollWidth); setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth); } else if (side === "right") { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - currentView: view, - currentViewData: currentRender.state, - renderView: [...renderView, ...currentRender.payload], - }, - }); + updateCurrentView(view); + updateRenderView([...renderView, ...currentRender.payload]); setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth); } else { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - currentView: view, - currentViewData: currentRender.state, - renderView: [...currentRender.payload], - }, - }); + updateCurrentView(view); + updateRenderView(currentRender.payload); setItemsContainerWidth(currentRender.scrollWidth); setTimeout(() => { handleScrollToCurrentSelectedDate(currentRender.state, currentRender.state.data.currentDate); @@ -206,4 +194,4 @@ export const ChartViewRoot: FC = (props) => { />
); -}; +}); diff --git a/web/components/gantt-chart/chart/views/bi-week.tsx b/web/components/gantt-chart/chart/views/bi-week.tsx index 6e53d5390..f0ad084e9 100644 --- a/web/components/gantt-chart/chart/views/bi-week.tsx +++ b/web/components/gantt-chart/chart/views/bi-week.tsx @@ -1,10 +1,11 @@ import { FC } from "react"; -// context -import { useChart } from "components/gantt-chart"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart"; -export const BiWeekChartView: FC = () => { +export const BiWeekChartView: FC = observer(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + const { currentView, currentViewData, renderView } = useGanttChart(); return ( <> @@ -50,4 +51,4 @@ export const BiWeekChartView: FC = () => {
); -}; +}); diff --git a/web/components/gantt-chart/chart/views/day.tsx b/web/components/gantt-chart/chart/views/day.tsx index a50b7748a..84b2edac4 100644 --- a/web/components/gantt-chart/chart/views/day.tsx +++ b/web/components/gantt-chart/chart/views/day.tsx @@ -1,10 +1,11 @@ import { FC } from "react"; -// context -import { useChart } from "../../hooks"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart"; -export const DayChartView: FC = () => { +export const DayChartView: FC = observer(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + const { currentView, currentViewData, renderView } = useGanttChart(); return ( <> @@ -50,4 +51,4 @@ export const DayChartView: FC = () => {
); -}; +}); diff --git a/web/components/gantt-chart/chart/views/hours.tsx b/web/components/gantt-chart/chart/views/hours.tsx index e1fd02e3f..bd1a7b6dd 100644 --- a/web/components/gantt-chart/chart/views/hours.tsx +++ b/web/components/gantt-chart/chart/views/hours.tsx @@ -1,10 +1,11 @@ import { FC } from "react"; -// context -import { useChart } from "components/gantt-chart"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart"; -export const HourChartView: FC = () => { +export const HourChartView: FC = observer(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + const { currentView, currentViewData, renderView } = useGanttChart(); return ( <> @@ -50,4 +51,4 @@ export const HourChartView: FC = () => {
); -}; +}); diff --git a/web/components/gantt-chart/chart/views/month.tsx b/web/components/gantt-chart/chart/views/month.tsx index c559e9688..3bfd077fe 100644 --- a/web/components/gantt-chart/chart/views/month.tsx +++ b/web/components/gantt-chart/chart/views/month.tsx @@ -1,6 +1,7 @@ import { FC } from "react"; +import { observer } from "mobx-react"; // hooks -import { useChart } from "components/gantt-chart"; +import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart"; // helpers import { cn } from "helpers/common.helper"; // types @@ -8,9 +9,9 @@ import { IMonthBlock } from "../../views"; // constants import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "components/gantt-chart/constants"; -export const MonthChartView: FC = () => { +export const MonthChartView: FC = observer(() => { // chart hook - const { currentViewData, renderView } = useChart(); + const { currentViewData, renderView } = useGanttChart(); const monthBlocks: IMonthBlock[] = renderView; return ( @@ -71,4 +72,4 @@ export const MonthChartView: FC = () => { ))}
); -}; +}); diff --git a/web/components/gantt-chart/chart/views/quarter.tsx b/web/components/gantt-chart/chart/views/quarter.tsx index ffbc1cbfe..b8adc4b3a 100644 --- a/web/components/gantt-chart/chart/views/quarter.tsx +++ b/web/components/gantt-chart/chart/views/quarter.tsx @@ -1,10 +1,11 @@ import { FC } from "react"; -// context -import { useChart } from "../../hooks"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart"; -export const QuarterChartView: FC = () => { +export const QuarterChartView: FC = observer(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + const { currentView, currentViewData, renderView } = useGanttChart(); return ( <> @@ -46,4 +47,4 @@ export const QuarterChartView: FC = () => { ); -}; +}); diff --git a/web/components/gantt-chart/chart/views/week.tsx b/web/components/gantt-chart/chart/views/week.tsx index 8170affa4..981fc9236 100644 --- a/web/components/gantt-chart/chart/views/week.tsx +++ b/web/components/gantt-chart/chart/views/week.tsx @@ -1,10 +1,11 @@ import { FC } from "react"; -// context -import { useChart } from "../../hooks"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart"; -export const WeekChartView: FC = () => { +export const WeekChartView: FC = observer(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + const { currentView, currentViewData, renderView } = useGanttChart(); return ( <> @@ -50,4 +51,4 @@ export const WeekChartView: FC = () => { ); -}; +}); diff --git a/web/components/gantt-chart/chart/views/year.tsx b/web/components/gantt-chart/chart/views/year.tsx index 9dbeedece..659126ac3 100644 --- a/web/components/gantt-chart/chart/views/year.tsx +++ b/web/components/gantt-chart/chart/views/year.tsx @@ -1,10 +1,11 @@ import { FC } from "react"; -// context -import { useChart } from "../../hooks"; +import { observer } from "mobx-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks/use-gantt-chart"; -export const YearChartView: FC = () => { +export const YearChartView: FC = observer(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + const { currentView, currentViewData, renderView } = useGanttChart(); return ( <> @@ -46,4 +47,4 @@ export const YearChartView: FC = () => { ); -}; +}); diff --git a/web/components/gantt-chart/contexts/index.tsx b/web/components/gantt-chart/contexts/index.tsx index 84e7a19b5..1d8a19f1a 100644 --- a/web/components/gantt-chart/contexts/index.tsx +++ b/web/components/gantt-chart/contexts/index.tsx @@ -1,57 +1,19 @@ -import React, { createContext, useState } from "react"; -// types -import { ChartContextData, ChartContextActionPayload, ChartContextReducer } from "../types"; -// data -import { allViewsWithData, currentViewDataWithView } from "../data"; +import { createContext } from "react"; +// mobx store +import { GanttStore } from "store/issue/issue_gantt_view.store"; -export const ChartContext = createContext(undefined); +let ganttViewStore = new GanttStore(); -const chartReducer = (state: ChartContextData, action: ChartContextActionPayload): ChartContextData => { - switch (action.type) { - case "CURRENT_VIEW": - return { ...state, currentView: action.payload }; - case "CURRENT_VIEW_DATA": - return { ...state, currentViewData: action.payload }; - case "RENDER_VIEW": - return { ...state, currentViewData: action.payload }; - case "PARTIAL_UPDATE": - return { ...state, ...action.payload }; - default: - return state; - } +export const GanttStoreContext = createContext(ganttViewStore); + +const initializeStore = () => { + const _ganttStore = ganttViewStore ?? new GanttStore(); + if (typeof window === "undefined") return _ganttStore; + if (!ganttViewStore) ganttViewStore = _ganttStore; + return _ganttStore; }; -const initialView = "month"; - -export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - // states; - const [state, dispatch] = useState({ - currentView: initialView, - currentViewData: currentViewDataWithView(initialView), - renderView: [], - allViews: allViewsWithData, - activeBlock: null, - }); - const [scrollLeft, setScrollLeft] = useState(0); - - const handleDispatch = (action: ChartContextActionPayload): ChartContextData => { - const newState = chartReducer(state, action); - dispatch(() => newState); - return newState; - }; - - const updateScrollLeft = (scrollLeft: number) => setScrollLeft(scrollLeft); - - return ( - - {children} - - ); +export const GanttStoreProvider = ({ children }: any) => { + const store = initializeStore(); + return {children}; }; diff --git a/web/components/gantt-chart/data/index.ts b/web/components/gantt-chart/data/index.ts index 58ac6e4b2..cc15c5d9e 100644 --- a/web/components/gantt-chart/data/index.ts +++ b/web/components/gantt-chart/data/index.ts @@ -1,5 +1,5 @@ // types -import { WeekMonthDataType, ChartDataType } from "../types"; +import { WeekMonthDataType, ChartDataType, TGanttViews } from "../types"; // constants export const weeks: WeekMonthDataType[] = [ @@ -53,7 +53,7 @@ export const datePreview = (date: Date, includeTime: boolean = false) => { }; // context data -export const allViewsWithData: ChartDataType[] = [ +export const VIEWS_LIST: ChartDataType[] = [ // { // key: "hours", // title: "Hours", @@ -133,7 +133,5 @@ export const allViewsWithData: ChartDataType[] = [ // }, ]; -export const currentViewDataWithView = (view: string = "month") => { - const currentView: ChartDataType | undefined = allViewsWithData.find((_viewData) => _viewData.key === view); - return currentView; -}; +export const currentViewDataWithView = (view: TGanttViews = "month") => + VIEWS_LIST.find((_viewData) => _viewData.key === view); diff --git a/web/components/gantt-chart/helpers/add-block.tsx b/web/components/gantt-chart/helpers/add-block.tsx index bfeddffa2..b7497013f 100644 --- a/web/components/gantt-chart/helpers/add-block.tsx +++ b/web/components/gantt-chart/helpers/add-block.tsx @@ -1,21 +1,21 @@ import { useEffect, useRef, useState } from "react"; import { addDays } from "date-fns"; import { Plus } from "lucide-react"; -// hooks -import { useChart } from "../hooks"; // ui import { Tooltip } from "@plane/ui"; // helpers import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; // types import { IBlockUpdateData, IGanttBlock } from "../types"; +import { useGanttChart } from "../hooks/use-gantt-chart"; +import { observer } from "mobx-react"; type Props = { block: IGanttBlock; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; }; -export const ChartAddBlock: React.FC = (props) => { +export const ChartAddBlock: React.FC = observer((props) => { const { block, blockUpdateHandler } = props; // states const [isButtonVisible, setIsButtonVisible] = useState(false); @@ -24,7 +24,7 @@ export const ChartAddBlock: React.FC = (props) => { // refs const containerRef = useRef(null); // chart hook - const { currentViewData } = useChart(); + const { currentViewData } = useGanttChart(); const handleButtonClick = () => { if (!currentViewData) return; @@ -88,4 +88,4 @@ export const ChartAddBlock: React.FC = (props) => { )} ); -}; +}); diff --git a/web/components/gantt-chart/helpers/block-structure.ts b/web/components/gantt-chart/helpers/block-structure.ts deleted file mode 100644 index 0f18b43cc..000000000 --- a/web/components/gantt-chart/helpers/block-structure.ts +++ /dev/null @@ -1,12 +0,0 @@ -// types -import { TIssue } from "@plane/types"; -import { IGanttBlock } from "components/gantt-chart"; - -export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] => - blocks?.map((block) => ({ - data: block, - id: block.id, - sort_order: block.sort_order, - start_date: block.start_date ? new Date(block.start_date) : null, - target_date: block.target_date ? new Date(block.target_date) : null, - })); diff --git a/web/components/gantt-chart/helpers/draggable.tsx b/web/components/gantt-chart/helpers/draggable.tsx index ac1602346..c2b4dc619 100644 --- a/web/components/gantt-chart/helpers/draggable.tsx +++ b/web/components/gantt-chart/helpers/draggable.tsx @@ -1,11 +1,13 @@ import React, { useEffect, useRef, useState } from "react"; import { ArrowRight } from "lucide-react"; // hooks -import { IGanttBlock, useChart } from "components/gantt-chart"; +import { IGanttBlock } from "components/gantt-chart"; // helpers import { cn } from "helpers/common.helper"; // constants import { SIDEBAR_WIDTH } from "../constants"; +import { useGanttChart } from "../hooks/use-gantt-chart"; +import { observer } from "mobx-react"; type Props = { block: IGanttBlock; @@ -14,19 +16,29 @@ type Props = { enableBlockLeftResize: boolean; enableBlockRightResize: boolean; enableBlockMove: boolean; + ganttContainerRef: React.RefObject; }; -export const ChartDraggable: React.FC = (props) => { - const { block, blockToRender, handleBlock, enableBlockLeftResize, enableBlockRightResize, enableBlockMove } = props; +export const ChartDraggable: React.FC = observer((props) => { + const { + block, + blockToRender, + handleBlock, + enableBlockLeftResize, + enableBlockRightResize, + enableBlockMove, + ganttContainerRef, + } = props; // states const [isLeftResizing, setIsLeftResizing] = useState(false); const [isRightResizing, setIsRightResizing] = useState(false); const [isMoving, setIsMoving] = useState(false); const [isHidden, setIsHidden] = useState(true); + const [scrollLeft, setScrollLeft] = useState(0); // refs const resizableRef = useRef(null); // chart hook - const { currentViewData, scrollLeft } = useChart(); + const { currentViewData } = useGanttChart(); // check if cursor reaches either end while resizing/dragging const checkScrollEnd = (e: MouseEvent): number => { const SCROLL_THRESHOLD = 70; @@ -212,6 +224,17 @@ export const ChartDraggable: React.FC = (props) => { block.position?.width && scrollLeft > block.position.marginLeft + block.position.width; + useEffect(() => { + const ganttContainer = ganttContainerRef.current; + if (!ganttContainer) return; + + const handleScroll = () => setScrollLeft(ganttContainer.scrollLeft); + ganttContainer.addEventListener("scroll", handleScroll); + return () => { + ganttContainer.removeEventListener("scroll", handleScroll); + }; + }, [ganttContainerRef]); + useEffect(() => { const intersectionRoot = document.querySelector("#gantt-container") as HTMLDivElement; const resizableBlock = resizableRef.current; @@ -234,7 +257,7 @@ export const ChartDraggable: React.FC = (props) => { return () => { observer.unobserve(resizableBlock); }; - }, [block.data.name]); + }, []); return ( <> @@ -312,4 +335,4 @@ export const ChartDraggable: React.FC = (props) => { ); -}; +}); diff --git a/web/components/gantt-chart/helpers/index.ts b/web/components/gantt-chart/helpers/index.ts index 1b51dc374..c96d42eec 100644 --- a/web/components/gantt-chart/helpers/index.ts +++ b/web/components/gantt-chart/helpers/index.ts @@ -1,3 +1,2 @@ export * from "./add-block"; -export * from "./block-structure"; export * from "./draggable"; diff --git a/web/components/gantt-chart/hooks/index.ts b/web/components/gantt-chart/hooks/index.ts new file mode 100644 index 000000000..009650675 --- /dev/null +++ b/web/components/gantt-chart/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-gantt-chart"; diff --git a/web/components/gantt-chart/hooks/index.tsx b/web/components/gantt-chart/hooks/index.tsx deleted file mode 100644 index 5fb9bee3f..000000000 --- a/web/components/gantt-chart/hooks/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useContext } from "react"; -// types -import { ChartContextReducer } from "../types"; -// context -import { ChartContext } from "../contexts"; - -export const useChart = (): ChartContextReducer => { - const context = useContext(ChartContext); - - if (!context) throw new Error("useChart must be used within a GanttChart"); - - return context; -}; diff --git a/web/components/gantt-chart/hooks/use-gantt-chart.ts b/web/components/gantt-chart/hooks/use-gantt-chart.ts new file mode 100644 index 000000000..23e025e90 --- /dev/null +++ b/web/components/gantt-chart/hooks/use-gantt-chart.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// mobx store +import { GanttStoreContext } from "components/gantt-chart/contexts"; +// types +import { IGanttStore } from "store/issue/issue_gantt_view.store"; + +export const useGanttChart = (): IGanttStore => { + const context = useContext(GanttStoreContext); + if (context === undefined) throw new Error("useGanttChart must be used within GanttStoreProvider"); + return context; +}; diff --git a/web/components/gantt-chart/index.ts b/web/components/gantt-chart/index.ts index 54a2cc597..78297ffcd 100644 --- a/web/components/gantt-chart/index.ts +++ b/web/components/gantt-chart/index.ts @@ -3,5 +3,5 @@ export * from "./chart"; export * from "./helpers"; export * from "./hooks"; export * from "./root"; -export * from "./types"; export * from "./sidebar"; +export * from "./types"; diff --git a/web/components/gantt-chart/root.tsx b/web/components/gantt-chart/root.tsx index ac132500b..4df5d9931 100644 --- a/web/components/gantt-chart/root.tsx +++ b/web/components/gantt-chart/root.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; // components import { ChartViewRoot, IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; // context -import { ChartContextProvider } from "./contexts"; +import { GanttStoreProvider } from "components/gantt-chart/contexts"; type GanttChartRootProps = { border?: boolean; @@ -42,7 +42,7 @@ export const GanttChartRoot: FC = (props) => { } = props; return ( - + = (props) => { showAllBlocks={showAllBlocks} quickAdd={quickAdd} /> - + ); }; diff --git a/web/components/gantt-chart/sidebar/cycles.tsx b/web/components/gantt-chart/sidebar/cycles.tsx deleted file mode 100644 index 384869a40..000000000 --- a/web/components/gantt-chart/sidebar/cycles.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd"; -import { MoreVertical } from "lucide-react"; -// hooks -import { useChart } from "components/gantt-chart/hooks"; -// ui -import { Loader } from "@plane/ui"; -// components -import { CycleGanttSidebarBlock } from "components/cycles"; -// helpers -import { findTotalDaysInRange } from "helpers/date-time.helper"; -import { cn } from "helpers/common.helper"; -// types -import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; -// constants -import { BLOCK_HEIGHT } from "../constants"; - -type Props = { - title: string; - blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; - blocks: IGanttBlock[] | null; - enableReorder: boolean; -}; - -export const CycleGanttSidebar: React.FC = (props) => { - const { blockUpdateHandler, blocks, enableReorder } = props; - // chart hook - const { activeBlock, dispatch } = useChart(); - - // update the active block on hover - const updateActiveBlock = (block: IGanttBlock | null) => { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - activeBlock: block, - }, - }); - }; - - const handleOrderChange = (result: DropResult) => { - if (!blocks) return; - - const { source, destination } = result; - - // return if dropped outside the list - if (!destination) return; - - // return if dropped on the same index - if (source.index === destination.index) return; - - let updatedSortOrder = blocks[source.index].sort_order; - - // update the sort order to the lowest if dropped at the top - if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; - // update the sort order to the highest if dropped at the bottom - else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; - // update the sort order to the average of the two adjacent blocks if dropped in between - else { - const destinationSortingOrder = blocks[destination.index].sort_order; - const relativeDestinationSortingOrder = - source.index < destination.index - ? blocks[destination.index + 1].sort_order - : blocks[destination.index - 1].sort_order; - - updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; - } - - // extract the element from the source index and insert it at the destination index without updating the entire array - const removedElement = blocks.splice(source.index, 1)[0]; - blocks.splice(destination.index, 0, removedElement); - - // call the block update handler with the updated sort order, new and old index - blockUpdateHandler(removedElement.data, { - sort_order: { - destinationIndex: destination.index, - newSortOrder: updatedSortOrder, - sourceIndex: source.index, - }, - }); - }; - - return ( - - - {(droppableProvided) => ( -
- <> - {blocks ? ( - blocks.map((block, index) => { - const duration = findTotalDaysInRange(block.start_date, block.target_date); - - return ( - - {(provided, snapshot) => ( -
updateActiveBlock(block)} - onMouseLeave={() => updateActiveBlock(null)} - ref={provided.innerRef} - {...provided.draggableProps} - > -
- {enableReorder && ( - - )} -
-
- -
- {duration && ( -
- {duration} day{duration > 1 ? "s" : ""} -
- )} -
-
-
- )} -
- ); - }) - ) : ( - - - - - - - )} - {droppableProvided.placeholder} - -
- )} -
-
- ); -}; diff --git a/web/components/gantt-chart/sidebar/cycles/block.tsx b/web/components/gantt-chart/sidebar/cycles/block.tsx new file mode 100644 index 000000000..f1374c753 --- /dev/null +++ b/web/components/gantt-chart/sidebar/cycles/block.tsx @@ -0,0 +1,72 @@ +import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; +import { observer } from "mobx-react"; +import { MoreVertical } from "lucide-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks"; +// components +import { CycleGanttSidebarBlock } from "components/cycles"; +// helpers +import { cn } from "helpers/common.helper"; +import { findTotalDaysInRange } from "helpers/date-time.helper"; +// types +import { IGanttBlock } from "components/gantt-chart/types"; +// constants +import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; + +type Props = { + block: IGanttBlock; + enableReorder: boolean; + provided: DraggableProvided; + snapshot: DraggableStateSnapshot; +}; + +export const CyclesSidebarBlock: React.FC = observer((props) => { + const { block, enableReorder, provided, snapshot } = props; + // store hooks + const { updateActiveBlockId, isBlockActive } = useGanttChart(); + + const duration = findTotalDaysInRange(block.start_date, block.target_date); + + return ( +
updateActiveBlockId(block.id)} + onMouseLeave={() => updateActiveBlockId(null)} + ref={provided.innerRef} + {...provided.draggableProps} + > +
+ {enableReorder && ( + + )} +
+
+ +
+ {duration && ( +
+ {duration} day{duration > 1 ? "s" : ""} +
+ )} +
+
+
+ ); +}); diff --git a/web/components/gantt-chart/sidebar/cycles/index.ts b/web/components/gantt-chart/sidebar/cycles/index.ts new file mode 100644 index 000000000..01acaeffb --- /dev/null +++ b/web/components/gantt-chart/sidebar/cycles/index.ts @@ -0,0 +1 @@ +export * from "./sidebar"; diff --git a/web/components/gantt-chart/sidebar/cycles/sidebar.tsx b/web/components/gantt-chart/sidebar/cycles/sidebar.tsx new file mode 100644 index 000000000..11f67a099 --- /dev/null +++ b/web/components/gantt-chart/sidebar/cycles/sidebar.tsx @@ -0,0 +1,100 @@ +import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd"; +// ui +import { Loader } from "@plane/ui"; +// components +import { CyclesSidebarBlock } from "./block"; +// types +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; + +type Props = { + title: string; + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + blocks: IGanttBlock[] | null; + enableReorder: boolean; +}; + +export const CycleGanttSidebar: React.FC = (props) => { + const { blockUpdateHandler, blocks, enableReorder } = props; + + const handleOrderChange = (result: DropResult) => { + if (!blocks) return; + + const { source, destination } = result; + + // return if dropped outside the list + if (!destination) return; + + // return if dropped on the same index + if (source.index === destination.index) return; + + let updatedSortOrder = blocks[source.index].sort_order; + + // update the sort order to the lowest if dropped at the top + if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; + // update the sort order to the highest if dropped at the bottom + else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; + // update the sort order to the average of the two adjacent blocks if dropped in between + else { + const destinationSortingOrder = blocks[destination.index].sort_order; + const relativeDestinationSortingOrder = + source.index < destination.index + ? blocks[destination.index + 1].sort_order + : blocks[destination.index - 1].sort_order; + + updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; + } + + // extract the element from the source index and insert it at the destination index without updating the entire array + const removedElement = blocks.splice(source.index, 1)[0]; + blocks.splice(destination.index, 0, removedElement); + + // call the block update handler with the updated sort order, new and old index + blockUpdateHandler(removedElement.data, { + sort_order: { + destinationIndex: destination.index, + newSortOrder: updatedSortOrder, + sourceIndex: source.index, + }, + }); + }; + + return ( + + + {(droppableProvided) => ( +
+ <> + {blocks ? ( + blocks.map((block, index) => ( + + {(provided, snapshot) => ( + + )} + + )) + ) : ( + + + + + + + )} + {droppableProvided.placeholder} + +
+ )} +
+
+ ); +}; diff --git a/web/components/gantt-chart/sidebar/issues.tsx b/web/components/gantt-chart/sidebar/issues.tsx deleted file mode 100644 index 52e30ded5..000000000 --- a/web/components/gantt-chart/sidebar/issues.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { observer } from "mobx-react"; -import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; -import { MoreVertical } from "lucide-react"; -// hooks -import { useChart } from "components/gantt-chart/hooks"; -import { useIssueDetail } from "hooks/store"; -// ui -import { Loader } from "@plane/ui"; -// components -import { IssueGanttSidebarBlock } from "components/issues"; -// helpers -import { findTotalDaysInRange } from "helpers/date-time.helper"; -import { cn } from "helpers/common.helper"; -// types -import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types"; -import { BLOCK_HEIGHT } from "../constants"; - -type Props = { - blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; - blocks: IGanttBlock[] | null; - enableReorder: boolean; - showAllBlocks?: boolean; -}; - -export const IssueGanttSidebar: React.FC = observer((props: Props) => { - const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props; - - const { activeBlock, dispatch } = useChart(); - const { peekIssue } = useIssueDetail(); - - // update the active block on hover - const updateActiveBlock = (block: IGanttBlock | null) => { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - activeBlock: block, - }, - }); - }; - - const handleOrderChange = (result: DropResult) => { - if (!blocks) return; - - const { source, destination } = result; - - // return if dropped outside the list - if (!destination) return; - - // return if dropped on the same index - if (source.index === destination.index) return; - - let updatedSortOrder = blocks[source.index].sort_order; - - // update the sort order to the lowest if dropped at the top - if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; - // update the sort order to the highest if dropped at the bottom - else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; - // update the sort order to the average of the two adjacent blocks if dropped in between - else { - const destinationSortingOrder = blocks[destination.index].sort_order; - const relativeDestinationSortingOrder = - source.index < destination.index - ? blocks[destination.index + 1].sort_order - : blocks[destination.index - 1].sort_order; - - updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; - } - - // extract the element from the source index and insert it at the destination index without updating the entire array - const removedElement = blocks.splice(source.index, 1)[0]; - blocks.splice(destination.index, 0, removedElement); - - // call the block update handler with the updated sort order, new and old index - blockUpdateHandler(removedElement.data, { - sort_order: { - destinationIndex: destination.index, - newSortOrder: updatedSortOrder, - sourceIndex: source.index, - }, - }); - }; - - return ( - <> - - - {(droppableProvided) => ( -
- <> - {blocks ? ( - blocks.map((block, index) => { - const isBlockVisibleOnSidebar = block.start_date && block.target_date; - - // hide the block if it doesn't have start and target dates and showAllBlocks is false - if (!showAllBlocks && !isBlockVisibleOnSidebar) return; - - const duration = - !block.start_date || !block.target_date - ? null - : findTotalDaysInRange(block.start_date, block.target_date); - - return ( - - {(provided, snapshot) => ( -
updateActiveBlock(block)} - onMouseLeave={() => updateActiveBlock(null)} - ref={provided.innerRef} - {...provided.draggableProps} - > -
- {enableReorder && ( - - )} -
-
- -
- {duration && ( -
- - {duration} day{duration > 1 ? "s" : ""} - -
- )} -
-
-
- )} -
- ); - }) - ) : ( - - - - - - - )} - {droppableProvided.placeholder} - -
- )} -
-
- - ); -}); diff --git a/web/components/gantt-chart/sidebar/issues/block.tsx b/web/components/gantt-chart/sidebar/issues/block.tsx new file mode 100644 index 000000000..03a17a65b --- /dev/null +++ b/web/components/gantt-chart/sidebar/issues/block.tsx @@ -0,0 +1,77 @@ +import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; +import { observer } from "mobx-react"; +import { MoreVertical } from "lucide-react"; +// hooks +import { useIssueDetail } from "hooks/store"; +import { useGanttChart } from "components/gantt-chart/hooks"; +// components +import { IssueGanttSidebarBlock } from "components/issues"; +// helpers +import { cn } from "helpers/common.helper"; +import { findTotalDaysInRange } from "helpers/date-time.helper"; +// types +import { IGanttBlock } from "../../types"; +// constants +import { BLOCK_HEIGHT } from "../../constants"; + +type Props = { + block: IGanttBlock; + enableReorder: boolean; + provided: DraggableProvided; + snapshot: DraggableStateSnapshot; +}; + +export const IssuesSidebarBlock: React.FC = observer((props) => { + const { block, enableReorder, provided, snapshot } = props; + // store hooks + const { updateActiveBlockId, isBlockActive } = useGanttChart(); + const { peekIssue } = useIssueDetail(); + + const duration = findTotalDaysInRange(block.start_date, block.target_date); + + return ( +
updateActiveBlockId(block.id)} + onMouseLeave={() => updateActiveBlockId(null)} + ref={provided.innerRef} + {...provided.draggableProps} + > +
+ {enableReorder && ( + + )} +
+
+ +
+ {duration && ( +
+ + {duration} day{duration > 1 ? "s" : ""} + +
+ )} +
+
+
+ ); +}); diff --git a/web/components/gantt-chart/sidebar/issues/index.ts b/web/components/gantt-chart/sidebar/issues/index.ts new file mode 100644 index 000000000..01acaeffb --- /dev/null +++ b/web/components/gantt-chart/sidebar/issues/index.ts @@ -0,0 +1 @@ +export * from "./sidebar"; diff --git a/web/components/gantt-chart/sidebar/issues/sidebar.tsx b/web/components/gantt-chart/sidebar/issues/sidebar.tsx new file mode 100644 index 000000000..323938eec --- /dev/null +++ b/web/components/gantt-chart/sidebar/issues/sidebar.tsx @@ -0,0 +1,107 @@ +import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; +// components +import { IssuesSidebarBlock } from "./block"; +// ui +import { Loader } from "@plane/ui"; +// types +import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types"; + +type Props = { + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + blocks: IGanttBlock[] | null; + enableReorder: boolean; + showAllBlocks?: boolean; +}; + +export const IssueGanttSidebar: React.FC = (props) => { + const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props; + + const handleOrderChange = (result: DropResult) => { + if (!blocks) return; + + const { source, destination } = result; + + // return if dropped outside the list + if (!destination) return; + + // return if dropped on the same index + if (source.index === destination.index) return; + + let updatedSortOrder = blocks[source.index].sort_order; + + // update the sort order to the lowest if dropped at the top + if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; + // update the sort order to the highest if dropped at the bottom + else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; + // update the sort order to the average of the two adjacent blocks if dropped in between + else { + const destinationSortingOrder = blocks[destination.index].sort_order; + const relativeDestinationSortingOrder = + source.index < destination.index + ? blocks[destination.index + 1].sort_order + : blocks[destination.index - 1].sort_order; + + updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; + } + + // extract the element from the source index and insert it at the destination index without updating the entire array + const removedElement = blocks.splice(source.index, 1)[0]; + blocks.splice(destination.index, 0, removedElement); + + // call the block update handler with the updated sort order, new and old index + blockUpdateHandler(removedElement.data, { + sort_order: { + destinationIndex: destination.index, + newSortOrder: updatedSortOrder, + sourceIndex: source.index, + }, + }); + }; + + return ( + + + {(droppableProvided) => ( +
+ <> + {blocks ? ( + blocks.map((block, index) => { + const isBlockVisibleOnSidebar = block.start_date && block.target_date; + + // hide the block if it doesn't have start and target dates and showAllBlocks is false + if (!showAllBlocks && !isBlockVisibleOnSidebar) return; + + return ( + + {(provided, snapshot) => ( + + )} + + ); + }) + ) : ( + + + + + + + )} + {droppableProvided.placeholder} + +
+ )} +
+
+ ); +}; diff --git a/web/components/gantt-chart/sidebar/modules.tsx b/web/components/gantt-chart/sidebar/modules.tsx deleted file mode 100644 index bdf8ca571..000000000 --- a/web/components/gantt-chart/sidebar/modules.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; -import { MoreVertical } from "lucide-react"; -// hooks -import { useChart } from "components/gantt-chart/hooks"; -// ui -import { Loader } from "@plane/ui"; -// components -import { ModuleGanttSidebarBlock } from "components/modules"; -// helpers -import { findTotalDaysInRange } from "helpers/date-time.helper"; -import { cn } from "helpers/common.helper"; -// types -import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; -// constants -import { BLOCK_HEIGHT } from "../constants"; - -type Props = { - title: string; - blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; - blocks: IGanttBlock[] | null; - enableReorder: boolean; -}; - -export const ModuleGanttSidebar: React.FC = (props) => { - const { blockUpdateHandler, blocks, enableReorder } = props; - // chart hook - const { activeBlock, dispatch } = useChart(); - - // update the active block on hover - const updateActiveBlock = (block: IGanttBlock | null) => { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - activeBlock: block, - }, - }); - }; - - const handleOrderChange = (result: DropResult) => { - if (!blocks) return; - - const { source, destination } = result; - - // return if dropped outside the list - if (!destination) return; - - // return if dropped on the same index - if (source.index === destination.index) return; - - let updatedSortOrder = blocks[source.index].sort_order; - - // update the sort order to the lowest if dropped at the top - if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; - // update the sort order to the highest if dropped at the bottom - else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; - // update the sort order to the average of the two adjacent blocks if dropped in between - else { - const destinationSortingOrder = blocks[destination.index].sort_order; - const relativeDestinationSortingOrder = - source.index < destination.index - ? blocks[destination.index + 1].sort_order - : blocks[destination.index - 1].sort_order; - - updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; - } - - // extract the element from the source index and insert it at the destination index without updating the entire array - const removedElement = blocks.splice(source.index, 1)[0]; - blocks.splice(destination.index, 0, removedElement); - - // call the block update handler with the updated sort order, new and old index - blockUpdateHandler(removedElement.data, { - sort_order: { - destinationIndex: destination.index, - newSortOrder: updatedSortOrder, - sourceIndex: source.index, - }, - }); - }; - - return ( - - - {(droppableProvided) => ( -
- <> - {blocks ? ( - blocks.map((block, index) => { - const duration = findTotalDaysInRange(block.start_date, block.target_date); - - return ( - - {(provided, snapshot) => ( -
updateActiveBlock(block)} - onMouseLeave={() => updateActiveBlock(null)} - ref={provided.innerRef} - {...provided.draggableProps} - > -
- {enableReorder && ( - - )} -
-
- -
- {duration !== undefined && ( -
- {duration} day{duration > 1 ? "s" : ""} -
- )} -
-
-
- )} -
- ); - }) - ) : ( - - - - - - - )} - {droppableProvided.placeholder} - -
- )} -
-
- ); -}; diff --git a/web/components/gantt-chart/sidebar/modules/block.tsx b/web/components/gantt-chart/sidebar/modules/block.tsx new file mode 100644 index 000000000..4b2e47226 --- /dev/null +++ b/web/components/gantt-chart/sidebar/modules/block.tsx @@ -0,0 +1,72 @@ +import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd"; +import { observer } from "mobx-react"; +import { MoreVertical } from "lucide-react"; +// hooks +import { useGanttChart } from "components/gantt-chart/hooks"; +// components +import { ModuleGanttSidebarBlock } from "components/modules"; +// helpers +import { cn } from "helpers/common.helper"; +import { findTotalDaysInRange } from "helpers/date-time.helper"; +// types +import { IGanttBlock } from "components/gantt-chart/types"; +// constants +import { BLOCK_HEIGHT } from "components/gantt-chart/constants"; + +type Props = { + block: IGanttBlock; + enableReorder: boolean; + provided: DraggableProvided; + snapshot: DraggableStateSnapshot; +}; + +export const ModulesSidebarBlock: React.FC = observer((props) => { + const { block, enableReorder, provided, snapshot } = props; + // store hooks + const { updateActiveBlockId, isBlockActive } = useGanttChart(); + + const duration = findTotalDaysInRange(block.start_date, block.target_date); + + return ( +
updateActiveBlockId(block.id)} + onMouseLeave={() => updateActiveBlockId(null)} + ref={provided.innerRef} + {...provided.draggableProps} + > +
+ {enableReorder && ( + + )} +
+
+ +
+ {duration !== undefined && ( +
+ {duration} day{duration > 1 ? "s" : ""} +
+ )} +
+
+
+ ); +}); diff --git a/web/components/gantt-chart/sidebar/modules/index.ts b/web/components/gantt-chart/sidebar/modules/index.ts new file mode 100644 index 000000000..01acaeffb --- /dev/null +++ b/web/components/gantt-chart/sidebar/modules/index.ts @@ -0,0 +1 @@ +export * from "./sidebar"; diff --git a/web/components/gantt-chart/sidebar/modules/sidebar.tsx b/web/components/gantt-chart/sidebar/modules/sidebar.tsx new file mode 100644 index 000000000..dee83fa79 --- /dev/null +++ b/web/components/gantt-chart/sidebar/modules/sidebar.tsx @@ -0,0 +1,100 @@ +import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; +// ui +import { Loader } from "@plane/ui"; +// components +import { ModulesSidebarBlock } from "./block"; +// types +import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; + +type Props = { + title: string; + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + blocks: IGanttBlock[] | null; + enableReorder: boolean; +}; + +export const ModuleGanttSidebar: React.FC = (props) => { + const { blockUpdateHandler, blocks, enableReorder } = props; + + const handleOrderChange = (result: DropResult) => { + if (!blocks) return; + + const { source, destination } = result; + + // return if dropped outside the list + if (!destination) return; + + // return if dropped on the same index + if (source.index === destination.index) return; + + let updatedSortOrder = blocks[source.index].sort_order; + + // update the sort order to the lowest if dropped at the top + if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; + // update the sort order to the highest if dropped at the bottom + else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; + // update the sort order to the average of the two adjacent blocks if dropped in between + else { + const destinationSortingOrder = blocks[destination.index].sort_order; + const relativeDestinationSortingOrder = + source.index < destination.index + ? blocks[destination.index + 1].sort_order + : blocks[destination.index - 1].sort_order; + + updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; + } + + // extract the element from the source index and insert it at the destination index without updating the entire array + const removedElement = blocks.splice(source.index, 1)[0]; + blocks.splice(destination.index, 0, removedElement); + + // call the block update handler with the updated sort order, new and old index + blockUpdateHandler(removedElement.data, { + sort_order: { + destinationIndex: destination.index, + newSortOrder: updatedSortOrder, + sourceIndex: source.index, + }, + }); + }; + + return ( + + + {(droppableProvided) => ( +
+ <> + {blocks ? ( + blocks.map((block, index) => ( + + {(provided, snapshot) => ( + + )} + + )) + ) : ( + + + + + + + )} + {droppableProvided.placeholder} + +
+ )} +
+
+ ); +}; diff --git a/web/components/gantt-chart/sidebar/project-views.tsx b/web/components/gantt-chart/sidebar/project-views.tsx index a27c4dded..a7e7c5e35 100644 --- a/web/components/gantt-chart/sidebar/project-views.tsx +++ b/web/components/gantt-chart/sidebar/project-views.tsx @@ -1,17 +1,10 @@ import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; -import { MoreVertical } from "lucide-react"; -// hooks -import { useChart } from "components/gantt-chart/hooks"; // ui import { Loader } from "@plane/ui"; // components -import { IssueGanttSidebarBlock } from "components/issues"; -// helpers -import { findTotalDaysInRange } from "helpers/date-time.helper"; +import { IssuesSidebarBlock } from "./issues/block"; // types import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; -// constants -import { BLOCK_HEIGHT } from "../constants"; type Props = { title: string; @@ -23,18 +16,6 @@ type Props = { export const ProjectViewGanttSidebar: React.FC = (props) => { const { blockUpdateHandler, blocks, enableReorder } = props; - // chart hook - const { activeBlock, dispatch } = useChart(); - - // update the active block on hover - const updateActiveBlock = (block: IGanttBlock | null) => { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - activeBlock: block, - }, - }); - }; const handleOrderChange = (result: DropResult) => { if (!blocks) return; @@ -89,59 +70,23 @@ export const ProjectViewGanttSidebar: React.FC = (props) => { > <> {blocks ? ( - blocks.map((block, index) => { - const duration = findTotalDaysInRange(block.start_date, block.target_date); - - return ( - - {(provided, snapshot) => ( -
updateActiveBlock(block)} - onMouseLeave={() => updateActiveBlock(null)} - ref={provided.innerRef} - {...provided.draggableProps} - > -
- {enableReorder && ( - - )} -
-
- -
- {duration !== undefined && ( -
- {duration} day{duration > 1 ? "s" : ""} -
- )} -
-
-
- )} -
- ); - }) + blocks.map((block, index) => ( + + {(provided, snapshot) => ( + + )} + + )) ) : ( diff --git a/web/components/gantt-chart/types/index.ts b/web/components/gantt-chart/types/index.ts index 1360f9f45..6268e4363 100644 --- a/web/components/gantt-chart/types/index.ts +++ b/web/components/gantt-chart/types/index.ts @@ -1,10 +1,3 @@ -// context types -export type allViewsType = { - key: string; - title: string; - data: Object | null; -}; - export interface IGanttBlock { data: any; id: string; @@ -29,34 +22,6 @@ export interface IBlockUpdateData { export type TGanttViews = "hours" | "day" | "week" | "bi_week" | "month" | "quarter" | "year"; -export interface ChartContextData { - allViews: allViewsType[]; - currentView: TGanttViews; - currentViewData: ChartDataType | undefined; - renderView: any; - activeBlock: IGanttBlock | null; -} - -export type ChartContextActionPayload = - | { - type: "CURRENT_VIEW"; - payload: TGanttViews; - } - | { - type: "CURRENT_VIEW_DATA" | "RENDER_VIEW"; - payload: ChartDataType | undefined; - } - | { - type: "PARTIAL_UPDATE"; - payload: Partial; - }; - -export interface ChartContextReducer extends ChartContextData { - scrollLeft: number; - updateScrollLeft: (scrollLeft: number) => void; - dispatch: (action: ChartContextActionPayload) => void; -} - // chart render types export interface WeekMonthDataType { key: number; diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 05030c500..0a36c133b 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -149,7 +149,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { onClose={() => setAnalyticsModal(false)} cycleDetails={cycleDetails ?? undefined} /> -
+
@@ -175,7 +175,12 @@ export const CycleIssuesHeader: React.FC = observer(() => { } /> - ... + + ... + } /> @@ -282,5 +287,3 @@ export const CycleIssuesHeader: React.FC = observer(() => { ); }); - - diff --git a/web/components/headers/global-issues.tsx b/web/components/headers/global-issues.tsx index cca1a972b..3c40cbbff 100644 --- a/web/components/headers/global-issues.tsx +++ b/web/components/headers/global-issues.tsx @@ -107,7 +107,7 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { return ( <> setCreateViewModal(false)} /> -
+
diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index d51c0f432..f722b506f 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -152,7 +152,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { onClose={() => setAnalyticsModal(false)} moduleDetails={moduleDetails ?? undefined} /> -
+
@@ -178,7 +178,12 @@ export const ModuleIssuesHeader: React.FC = observer(() => { } /> - ... + + ... + } /> @@ -249,7 +254,12 @@ export const ModuleIssuesHeader: React.FC = observer(() => { {canUserCreateIssue && ( <> -
diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 033196758..43030c5c2 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -109,7 +109,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => { onClose={() => setAnalyticsModal(false)} projectDetails={currentProjectDetails ?? undefined} /> -
+
diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index 56cd54f0b..175534a79 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -108,7 +108,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); return ( -
+
diff --git a/web/components/issues/draft-issue-modal.tsx b/web/components/issues/draft-issue-modal.tsx index 0324c1b03..40a79798e 100644 --- a/web/components/issues/draft-issue-modal.tsx +++ b/web/components/issues/draft-issue-modal.tsx @@ -196,9 +196,9 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( const updateDraftIssue = async (payload: Partial) => { await draftIssues .updateIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload) - .then((res) => { + .then(() => { if (isUpdatingSingleIssue) { - mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); + mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...payload } as TIssue), false); } else { if (payload.parent_id) mutate(SUB_ISSUES(payload.parent_id.toString())); } diff --git a/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx b/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx index bf5b15266..f7bce3edc 100644 --- a/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx +++ b/web/components/issues/issue-detail/issue-activity/comments/comment-create.tsx @@ -53,7 +53,8 @@ export const IssueCommentCreate: FC = (props) => { control, formState: { isSubmitting }, reset, - } = useForm>({ defaultValues: { comment_html: "

" } }); + watch, + } = useForm>({ defaultValues: { comment_html: "" } }); const onSubmit = async (formData: Partial) => { await activityOperations.createComment(formData).finally(() => { @@ -88,7 +89,7 @@ export const IssueCommentCreate: FC = (props) => { deleteFile={fileService.getDeleteImageFunction(workspaceId)} restoreFile={fileService.getRestoreImageFunction(workspaceId)} ref={editorRef} - value={!value ? "

" : value} + value={value ?? ""} customClassName="p-2" editorContentCustomClassNames="min-h-[35px]" debouncedUpdatesEnabled={false} @@ -104,7 +105,7 @@ export const IssueCommentCreate: FC = (props) => { } submitButton={
diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx index e0d54e1ea..6252fc03a 100644 --- a/web/components/issues/issue-detail/root.tsx +++ b/web/components/issues/issue-detail/root.tsx @@ -96,7 +96,7 @@ export const IssueDetailRoot: FC = observer((props) => { showToast: boolean = true ) => { try { - const response = await updateIssue(workspaceSlug, projectId, issueId, data); + await updateIssue(workspaceSlug, projectId, issueId, data); if (showToast) { setToastAlert({ title: "Issue updated successfully", @@ -106,7 +106,7 @@ export const IssueDetailRoot: FC = observer((props) => { } captureIssueEvent({ eventName: ISSUE_UPDATED, - payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, + payload: { ...data, issueId, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: Object.keys(data).join(","), change_details: Object.values(data).join(","), diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx index 854550b0b..eb12250e0 100644 --- a/web/components/issues/issue-detail/sidebar.tsx +++ b/web/components/issues/issue-detail/sidebar.tsx @@ -1,7 +1,6 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { differenceInCalendarDays } from "date-fns"; import { LinkIcon, Signal, @@ -15,7 +14,7 @@ import { CalendarDays, } from "lucide-react"; // hooks -import { useEstimate, useIssueDetail, useProject, useUser } from "hooks/store"; +import { useEstimate, useIssueDetail, useProject, useProjectState, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { @@ -41,6 +40,7 @@ import { ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon, UserGroupIcon } import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; import { cn } from "helpers/common.helper"; +import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; // types import type { TIssueOperations } from "./root"; @@ -65,6 +65,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const { issue: { getIssueById }, } = useIssueDetail(); + const { getStateById } = useProjectState(); // states const [deleteIssueModal, setDeleteIssueModal] = useState(false); @@ -83,6 +84,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { }; const projectDetails = issue ? getProjectById(issue.project_id) : null; + const stateDetails = getStateById(issue.state_id); const minDate = issue.start_date ? new Date(issue.start_date) : null; minDate?.setDate(minDate.getDate()); @@ -90,8 +92,6 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const maxDate = issue.target_date ? new Date(issue.target_date) : null; maxDate?.setDate(maxDate.getDate()); - const targetDateDistance = issue.target_date ? differenceInCalendarDays(new Date(issue.target_date), new Date()) : 1; - return ( <> {workspaceSlug && projectId && issue && ( @@ -242,7 +242,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { buttonContainerClassName="w-full text-left" buttonClassName={cn("text-sm", { "text-custom-text-400": !issue.target_date, - "text-red-500": targetDateDistance <= 0, + "text-red-500": shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group), })} hideIcon clearIconClassName="h-3 w-3 hidden group-hover:inline !text-custom-text-100" diff --git a/web/components/issues/issue-detail/subscription.tsx b/web/components/issues/issue-detail/subscription.tsx index 603b3ada7..ab5983960 100644 --- a/web/components/issues/issue-detail/subscription.tsx +++ b/web/components/issues/issue-detail/subscription.tsx @@ -1,11 +1,12 @@ -import { FC, useState } from "react"; import { Bell, BellOff } from "lucide-react"; import { observer } from "mobx-react-lite"; +import { FC, useState } from "react"; // UI import { Button, Loader } from "@plane/ui"; // hooks import { useIssueDetail } from "hooks/store"; import useToast from "hooks/use-toast"; +import isNil from "lodash/isNil"; export type TIssueSubscription = { workspaceSlug: string; @@ -25,17 +26,17 @@ export const IssueSubscription: FC = observer((props) => { // state const [loading, setLoading] = useState(false); - const subscription = getSubscriptionByIssueId(issueId); + const isSubscribed = getSubscriptionByIssueId(issueId); const handleSubscription = async () => { setLoading(true); try { - if (subscription?.subscribed) await removeSubscription(workspaceSlug, projectId, issueId); + if (isSubscribed) await removeSubscription(workspaceSlug, projectId, issueId); else await createSubscription(workspaceSlug, projectId, issueId); setToastAlert({ type: "success", - title: `Issue ${subscription?.subscribed ? `unsubscribed` : `subscribed`} successfully.!`, - message: `Issue ${subscription?.subscribed ? `unsubscribed` : `subscribed`} successfully.!`, + title: `Issue ${isSubscribed ? `unsubscribed` : `subscribed`} successfully.!`, + message: `Issue ${isSubscribed ? `unsubscribed` : `subscribed`} successfully.!`, }); setLoading(false); } catch (error) { @@ -48,42 +49,32 @@ export const IssueSubscription: FC = observer((props) => { } }; - if (!subscription) + if (isNil(isSubscribed)) return ( - + ); return ( - <> - {subscription ? ( -
- -
- ) : ( - <> - - - - - )} - +
+ +
); }); diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 4dcaab335..badb849fb 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -73,42 +73,44 @@ export const CalendarChart: React.FC = observer((props) => { <>
- -
- {layout === "month" && ( -
- {allWeeksOfActiveMonth && - Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => ( - - ))} -
- )} - {layout === "week" && ( - - )} +
+ +
+ {layout === "month" && ( +
+ {allWeeksOfActiveMonth && + Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => ( + + ))} +
+ )} + {layout === "week" && ( + + )} +
diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index 85ab152a7..f92365a58 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -91,7 +91,7 @@ export const CalendarDayTile: React.FC = observer((props) => { snapshot.isDraggingOver || date.date.getDay() === 0 || date.date.getDay() === 6 ? "bg-custom-background-90" : "bg-custom-background-100" - } ${calendarLayout === "month" ? "min-h-[9rem]" : ""}`} + } ${calendarLayout === "month" ? "min-h-[5rem]" : ""}`} {...provided.droppableProps} ref={provided.innerRef} > diff --git a/web/components/issues/issue-layouts/calendar/week-header.tsx b/web/components/issues/issue-layouts/calendar/week-header.tsx index f5ec41e96..a4b714c94 100644 --- a/web/components/issues/issue-layouts/calendar/week-header.tsx +++ b/web/components/issues/issue-layouts/calendar/week-header.tsx @@ -13,7 +13,7 @@ export const CalendarWeekHeader: React.FC = observer((props) => { return (
diff --git a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx index b5f092aba..ec33872eb 100644 --- a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -5,12 +5,9 @@ import { observer } from "mobx-react-lite"; import { useIssues, useUser } from "hooks/store"; // components import { GanttQuickAddIssueForm, IssueGanttBlock } from "components/issues"; -import { - GanttChartRoot, - IBlockUpdateData, - renderIssueBlocksStructure, - IssueGanttSidebar, -} from "components/gantt-chart"; +import { GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "components/gantt-chart"; +// helpers +import { renderIssueBlocksStructure } from "helpers/issue.helper"; // types import { TIssue, TUnGroupedIssues } from "@plane/types"; import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index d2f0d5582..3b31f6b67 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -225,9 +225,15 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas let _kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || []; if (_kanbanFilters.includes(value)) _kanbanFilters = _kanbanFilters.filter((_value) => _value != value); else _kanbanFilters.push(value); - issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.KANBAN_FILTERS, { - [toggle]: _kanbanFilters, - }); + issuesFilter.updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.KANBAN_FILTERS, + { + [toggle]: _kanbanFilters, + }, + viewId + ); } }; diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index f88bb6b92..ec9742baf 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -138,6 +138,7 @@ export const HeaderGroupByCard: FC = observer((props) => { } + placement="bottom-end" > { diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index 840967565..7c8f638ff 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -1,11 +1,10 @@ import { useCallback, useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { differenceInCalendarDays } from "date-fns"; import { Layers, Link, Paperclip } from "lucide-react"; import xor from "lodash/xor"; // hooks -import { useEventTracker, useEstimate, useLabel, useIssues } from "hooks/store"; +import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "hooks/store"; // components import { IssuePropertyLabels } from "../properties/labels"; import { Tooltip } from "@plane/ui"; @@ -21,6 +20,8 @@ import { } from "components/dropdowns"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; +import { cn } from "helpers/common.helper"; // types import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; // constants @@ -47,11 +48,14 @@ export const IssueProperties: React.FC = observer((props) => { const { issues: { addIssueToCycle, removeIssueFromCycle }, } = useIssues(EIssuesStoreType.CYCLE); + const { areEstimatesEnabledForCurrentProject } = useEstimate(); + const { getStateById } = useProjectState(); // router const router = useRouter(); const { workspaceSlug, cycleId, moduleId } = router.query; - const { areEstimatesEnabledForCurrentProject } = useEstimate(); const currentLayout = `${activeLayout} layout`; + // derived values + const stateDetails = getStateById(issue.state_id); const issueOperations = useMemo( () => ({ @@ -231,8 +235,6 @@ export const IssueProperties: React.FC = observer((props) => { const maxDate = issue.target_date ? new Date(issue.target_date) : null; maxDate?.setDate(maxDate.getDate()); - const targetDateDistance = issue.target_date ? differenceInCalendarDays(new Date(issue.target_date), new Date()) : 1; - return (
{/* basic properties */} @@ -300,7 +302,7 @@ export const IssueProperties: React.FC = observer((props) => { minDate={minDate ?? undefined} placeholder="Due date" buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} - buttonClassName={targetDateDistance <= 0 ? "text-red-500" : ""} + buttonClassName={shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-red-500" : ""} clearIconClassName="!text-custom-text-100" disabled={isReadOnly} showTooltip @@ -378,12 +380,17 @@ export const IssueProperties: React.FC = observer((props) => { !!properties.sub_issue_count} + shouldRenderProperty={(properties) => !!properties.sub_issue_count && !!issue.sub_issues_count} >
{}} + className={cn( + "flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1", + { + "hover:bg-custom-background-80 cursor-pointer": issue.sub_issues_count, + } + )} >
{issue.sub_issues_count}
@@ -395,7 +402,7 @@ export const IssueProperties: React.FC = observer((props) => { !!properties.attachment_count} + shouldRenderProperty={(properties) => !!properties.attachment_count && !!issue.attachment_count} >
@@ -409,7 +416,7 @@ export const IssueProperties: React.FC = observer((props) => { !!properties.link} + shouldRenderProperty={(properties) => !!properties.link && !!issue.link_count} >
diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index 4f7088c38..bc9c8c397 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; import size from "lodash/size"; +import isEmpty from "lodash/isEmpty"; // hooks import { useCycle, useIssues } from "hooks/store"; // components @@ -89,7 +90,12 @@ export const CycleLayoutRoot: React.FC = observer(() => { <> setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
- {cycleStatus === "completed" && setTransferIssuesModal(true)} />} + {cycleStatus === "completed" && ( + setTransferIssuesModal(true)} + disabled={!isEmpty(cycleDetails?.progress_snapshot) ?? false} + /> + )} {issues?.groupedIssueIds?.length === 0 ? ( diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx index e07500c03..ebed73b76 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx @@ -1,13 +1,15 @@ import React from "react"; import { observer } from "mobx-react-lite"; -import differenceInCalendarDays from "date-fns/differenceInCalendarDays"; +// hooks +import { useProjectState } from "hooks/store"; // components import { DateDropdown } from "components/dropdowns"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; +import { cn } from "helpers/common.helper"; // types import { TIssue } from "@plane/types"; -import { cn } from "helpers/common.helper"; type Props = { issue: TIssue; @@ -18,8 +20,10 @@ type Props = { export const SpreadsheetDueDateColumn: React.FC = observer((props: Props) => { const { issue, onChange, disabled, onClose } = props; - - const targetDateDistance = issue.target_date ? differenceInCalendarDays(new Date(issue.target_date), new Date()) : 1; + // store hooks + const { getStateById } = useProjectState(); + // derived values + const stateDetails = getStateById(issue.state_id); return (
@@ -42,7 +46,7 @@ export const SpreadsheetDueDateColumn: React.FC = observer((props: Props) buttonVariant="transparent-with-text" buttonContainerClassName="w-full" buttonClassName={cn("rounded-none text-left", { - "text-red-500": targetDateDistance <= 0, + "text-red-500": shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group), })} clearIconClassName="!text-custom-text-100" onClose={onClose} diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx index b7f432385..ac06525df 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx @@ -65,15 +65,16 @@ export const HeaderColumn = (props: Props) => {
} onMenuClose={onClose} - placement="bottom-end" + placement="bottom-start" closeOnSelect > handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
@@ -87,10 +88,11 @@ export const HeaderColumn = (props: Props) => { handleOrderBy(propertyDetails.descendingOrderKey, property)}>
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx index 20864eb96..c635ca85e 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx @@ -5,6 +5,8 @@ import { useRouter } from "next/router"; import { useApplication } from "hooks/store"; // types import { TIssue } from "@plane/types"; +// helpers +import { cn } from "helpers/common.helper"; type Props = { issue: TIssue; @@ -30,8 +32,13 @@ export const SpreadsheetSubIssueColumn: React.FC = observer((props: Props return (
{}} + className={cn( + "flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80", + { + "cursor-pointer": issue?.sub_issues_count, + } + )} > {issue?.sub_issues_count} {issue?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx index 64d1ec0e1..8a4461cb5 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx @@ -19,10 +19,10 @@ export const SpreadsheetHeader = (props: Props) => { const { displayProperties, displayFilters, handleDisplayFilterUpdate, isEstimateEnabled } = props; return ( - + diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 564cc9b08..cfb4b912c 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -64,6 +64,31 @@ export interface IssueFormProps { const aiService = new AIService(); const fileService = new FileService(); +const TAB_INDICES = [ + "name", + "description_html", + "feeling_lucky", + "ai_assistant", + "state_id", + "priority", + "assignee_ids", + "label_ids", + "start_date", + "target_date", + "cycle_id", + "module_ids", + "estimate_point", + "parent_id", + "create_more", + "discard_button", + "draft_button", + "submit_button", + "project_id", + "remove_parent", +]; + +const getTabIndex = (key: string) => TAB_INDICES.findIndex((tabIndex) => tabIndex === key) + 1; + export const IssueFormRoot: FC = observer((props) => { const { data, @@ -271,7 +296,7 @@ export const IssueFormRoot: FC = observer((props) => { }} buttonVariant="border-with-text" // TODO: update tabIndex logic - tabIndex={19} + tabIndex={getTabIndex("project_id")} />
)} @@ -294,15 +319,18 @@ export const IssueFormRoot: FC = observer((props) => { {selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id} {selectedParentIssue.name.substring(0, 50)} - { setValue("parent_id", null); handleFormChange(); setSelectedParentIssue(null); }} - tabIndex={20} - /> + tabIndex={getTabIndex("remove_parent")} + > + +
)} @@ -332,7 +360,7 @@ export const IssueFormRoot: FC = observer((props) => { hasError={Boolean(errors.name)} placeholder="Issue Title" className="resize-none text-xl w-full" - tabIndex={1} + tabIndex={getTabIndex("name")} /> )} /> @@ -346,7 +374,7 @@ export const IssueFormRoot: FC = observer((props) => { }`} onClick={handleAutoGenerateDescription} disabled={iAmFeelingLucky} - tabIndex={3} + tabIndex={getTabIndex("feeling_lucky")} > {iAmFeelingLucky ? ( "Generating response" @@ -375,7 +403,7 @@ export const IssueFormRoot: FC = observer((props) => { type="button" className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90" onClick={() => setGptAssistantModal((prevData) => !prevData)} - tabIndex={4} + tabIndex={getTabIndex("ai_assistant")} > AI @@ -426,7 +454,7 @@ export const IssueFormRoot: FC = observer((props) => { }} projectId={projectId} buttonVariant="border-with-text" - tabIndex={6} + tabIndex={getTabIndex("state_id")} />
)} @@ -443,7 +471,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); }} buttonVariant="border-with-text" - tabIndex={7} + tabIndex={getTabIndex("priority")} />
)} @@ -464,7 +492,7 @@ export const IssueFormRoot: FC = observer((props) => { buttonClassName={value?.length > 0 ? "hover:bg-transparent px-0" : ""} placeholder="Assignees" multiple - tabIndex={8} + tabIndex={getTabIndex("assignee_ids")} />
)} @@ -482,7 +510,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); }} projectId={projectId} - tabIndex={9} + tabIndex={getTabIndex("label_ids")} />
)} @@ -498,6 +526,7 @@ export const IssueFormRoot: FC = observer((props) => { buttonVariant="border-with-text" maxDate={maxDate ?? undefined} placeholder="Start date" + tabIndex={getTabIndex("start_date")} />
)} @@ -513,6 +542,7 @@ export const IssueFormRoot: FC = observer((props) => { buttonVariant="border-with-text" minDate={minDate ?? undefined} placeholder="Due date" + tabIndex={getTabIndex("target_date")} />
)} @@ -531,7 +561,7 @@ export const IssueFormRoot: FC = observer((props) => { }} value={value} buttonVariant="border-with-text" - tabIndex={11} + tabIndex={getTabIndex("cycle_id")} />
)} @@ -551,7 +581,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); }} buttonVariant="border-with-text" - tabIndex={12} + tabIndex={getTabIndex("module_ids")} multiple showCount /> @@ -573,7 +603,7 @@ export const IssueFormRoot: FC = observer((props) => { }} projectId={projectId} buttonVariant="border-with-text" - tabIndex={13} + tabIndex={getTabIndex("estimate_point")} />
)} @@ -603,7 +633,7 @@ export const IssueFormRoot: FC = observer((props) => { } placement="bottom-start" - tabIndex={14} + tabIndex={getTabIndex("parent_id")} > {watch("parent_id") ? ( <> @@ -653,7 +683,7 @@ export const IssueFormRoot: FC = observer((props) => { onKeyDown={(e) => { if (e.key === "Enter") onCreateMoreToggleChange(!isCreateMoreToggleEnabled); }} - tabIndex={15} + tabIndex={getTabIndex("create_more")} >
{}} size="sm" /> @@ -661,7 +691,7 @@ export const IssueFormRoot: FC = observer((props) => { Create more
- @@ -673,7 +703,7 @@ export const IssueFormRoot: FC = observer((props) => { size="sm" loading={isSubmitting} onClick={handleSubmit((data) => handleFormSubmit({ ...data, is_draft: false }))} - tabIndex={17} + tabIndex={getTabIndex("draft_button")} > {isSubmitting ? "Moving" : "Move from draft"} @@ -683,7 +713,7 @@ export const IssueFormRoot: FC = observer((props) => { size="sm" loading={isSubmitting} onClick={handleSubmit((data) => handleFormSubmit(data, true))} - tabIndex={17} + tabIndex={getTabIndex("draft_button")} > {isSubmitting ? "Saving" : "Save as draft"} @@ -691,7 +721,13 @@ export const IssueFormRoot: FC = observer((props) => { )} -
diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx index 97d977ace..b6a3eecc3 100644 --- a/web/components/issues/issue-modal/modal.tsx +++ b/web/components/issues/issue-modal/modal.tsx @@ -183,7 +183,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop if (!workspaceSlug || !payload.project_id || !data?.id) return; try { - const response = await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId); + await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId); setToastAlert({ type: "success", title: "Success!", @@ -191,11 +191,10 @@ export const CreateUpdateIssueModal: React.FC = observer((prop }); captureIssueEvent({ eventName: ISSUE_UPDATED, - payload: { ...response, state: "SUCCESS" }, + payload: { ...payload, issueId: data.id, state: "SUCCESS" }, path: router.asPath, }); handleClose(); - return response; } catch (error) { setToastAlert({ type: "error", diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx index c669c0349..2b428a57b 100644 --- a/web/components/issues/peek-overview/properties.tsx +++ b/web/components/issues/peek-overview/properties.tsx @@ -1,9 +1,8 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { differenceInCalendarDays } from "date-fns"; import { Signal, Tag, Triangle, LayoutPanelTop, CircleDot, CopyPlus, XCircle, CalendarDays } from "lucide-react"; // hooks -import { useIssueDetail, useProject } from "hooks/store"; +import { useIssueDetail, useProject, useProjectState } from "hooks/store"; // ui icons import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon, RelatedIcon } from "@plane/ui"; import { @@ -26,6 +25,7 @@ import { import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // helpers import { cn } from "helpers/common.helper"; +import { shouldHighlightIssueDueDate } from "helpers/issue.helper"; interface IPeekOverviewProperties { workspaceSlug: string; @@ -42,11 +42,13 @@ export const PeekOverviewProperties: FC = observer((pro const { issue: { getIssueById }, } = useIssueDetail(); + const { getStateById } = useProjectState(); // derived values const issue = getIssueById(issueId); if (!issue) return <>; const projectDetails = getProjectById(issue.project_id); const isEstimateEnabled = projectDetails?.estimate; + const stateDetails = getStateById(issue.state_id); const minDate = issue.start_date ? new Date(issue.start_date) : null; minDate?.setDate(minDate.getDate()); @@ -54,8 +56,6 @@ export const PeekOverviewProperties: FC = observer((pro const maxDate = issue.target_date ? new Date(issue.target_date) : null; maxDate?.setDate(maxDate.getDate()); - const targetDateDistance = issue.target_date ? differenceInCalendarDays(new Date(issue.target_date), new Date()) : 1; - return (
Properties
@@ -169,7 +169,7 @@ export const PeekOverviewProperties: FC = observer((pro buttonContainerClassName="w-full text-left" buttonClassName={cn("text-sm", { "text-custom-text-400": !issue.target_date, - "text-red-500": targetDateDistance <= 0, + "text-red-500": shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group), })} hideIcon clearIconClassName="h-3 w-3 hidden group-hover:inline !text-custom-text-100" diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx index 564b5a019..76dec5094 100644 --- a/web/components/issues/peek-overview/root.tsx +++ b/web/components/issues/peek-overview/root.tsx @@ -15,7 +15,6 @@ import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker"; interface IIssuePeekOverview { is_archived?: boolean; - onIssueUpdate?: (issue: Partial) => Promise; } export type TIssuePeekOperations = { @@ -46,7 +45,7 @@ export type TIssuePeekOperations = { }; export const IssuePeekOverview: FC = observer((props) => { - const { is_archived = false, onIssueUpdate } = props; + const { is_archived = false } = props; // hooks const { setToastAlert } = useToast(); // router @@ -87,7 +86,6 @@ export const IssuePeekOverview: FC = observer((props) => { ) => { try { const response = await updateIssue(workspaceSlug, projectId, issueId, data); - if (onIssueUpdate) await onIssueUpdate(response); if (showToast) setToastAlert({ title: "Issue updated successfully", @@ -96,7 +94,7 @@ export const IssuePeekOverview: FC = observer((props) => { }); captureIssueEvent({ eventName: ISSUE_UPDATED, - payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, + payload: { ...data, issueId, state: "SUCCESS", element: "Issue peek-overview" }, updates: { changed_property: Object.keys(data).join(","), change_details: Object.values(data).join(","), @@ -314,7 +312,6 @@ export const IssuePeekOverview: FC = observer((props) => { removeIssueFromModule, removeModulesFromIssue, setToastAlert, - onIssueUpdate, captureIssueEvent, router.asPath, ] diff --git a/web/components/issues/select/label.tsx b/web/components/issues/select/label.tsx index 754edf0bd..d64e6e3f8 100644 --- a/web/components/issues/select/label.tsx +++ b/web/components/issues/select/label.tsx @@ -154,22 +154,22 @@ export const IssueLabelSelect: React.FC = observer((props) => { className={({ active }) => `${ active ? "bg-custom-background-80" : "" - } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200` + } group flex w-full cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200` } value={label.id} > {({ selected }) => (
-
+
- {label.name} + {label.name}
-
+
diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx index 1ecc3974d..3d7468f24 100644 --- a/web/components/modules/module-list-item.tsx +++ b/web/components/modules/module-list-item.tsx @@ -235,7 +235,7 @@ export const ModuleListItem: React.FC = observer((props) => { ))} - + {isEditingAllowed && ( <> diff --git a/web/components/onboarding/user-details.tsx b/web/components/onboarding/user-details.tsx index ac4a3c198..cd129c74c 100644 --- a/web/components/onboarding/user-details.tsx +++ b/web/components/onboarding/user-details.tsx @@ -190,6 +190,7 @@ export const UserDetails: React.FC = observer((props) => { hasError={Boolean(errors.first_name)} placeholder="Enter your full name..." className="w-full border-onboarding-border-100 focus:border-custom-primary-100" + maxLength={24} /> )} /> diff --git a/web/components/project/send-project-invitation-modal.tsx b/web/components/project/send-project-invitation-modal.tsx index 7c02ce8d0..ef7913fb0 100644 --- a/web/components/project/send-project-invitation-modal.tsx +++ b/web/components/project/send-project-invitation-modal.tsx @@ -148,10 +148,14 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { memberDetails?.member.last_name } ${memberDetails?.member.display_name.toLowerCase()}`, content: ( -
- - {memberDetails?.member.display_name} ( - {memberDetails?.member.first_name + " " + memberDetails?.member.last_name}) +
+
+ +
+
+ {memberDetails?.member.display_name} ( + {memberDetails?.member.first_name + " " + memberDetails?.member.last_name}) +
), }; diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx index 249f044f4..289ab6122 100644 --- a/web/components/project/sidebar-list.tsx +++ b/web/components/project/sidebar-list.tsx @@ -11,6 +11,7 @@ import useToast from "hooks/use-toast"; import { CreateProjectModal, ProjectSidebarListItem } from "components/project"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; +import { cn } from "helpers/common.helper"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; @@ -109,9 +110,9 @@ export const ProjectSidebarList: FC = observer(() => { )}
diff --git a/web/components/views/view-list-item.tsx b/web/components/views/view-list-item.tsx index 0bfb57cd6..48cc12ada 100644 --- a/web/components/views/view-list-item.tsx +++ b/web/components/views/view-list-item.tsx @@ -80,7 +80,7 @@ export const ProjectViewListItem: React.FC = observer((props) => { setDeleteViewModal(false)} />
-
+
diff --git a/web/components/workspace/sidebar-dropdown.tsx b/web/components/workspace/sidebar-dropdown.tsx index f625f95b3..aeb0a34c2 100644 --- a/web/components/workspace/sidebar-dropdown.tsx +++ b/web/components/workspace/sidebar-dropdown.tsx @@ -157,7 +157,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
-
+
{currentUser?.email}
{workspacesList ? ( diff --git a/web/components/workspace/views/default-view-list-item.tsx b/web/components/workspace/views/default-view-list-item.tsx index 6262621f4..ad2f487f7 100644 --- a/web/components/workspace/views/default-view-list-item.tsx +++ b/web/components/workspace/views/default-view-list-item.tsx @@ -15,7 +15,7 @@ export const GlobalDefaultViewListItem: React.FC = observer((props) => { return (
-
+
diff --git a/web/components/workspace/views/form.tsx b/web/components/workspace/views/form.tsx index ce04b2f2d..71627c08a 100644 --- a/web/components/workspace/views/form.tsx +++ b/web/components/workspace/views/form.tsx @@ -8,7 +8,7 @@ import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components // ui import { Button, Input, TextArea } from "@plane/ui"; // types -import { IWorkspaceView } from "@plane/types"; +import { IIssueFilterOptions, IWorkspaceView } from "@plane/types"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; @@ -39,7 +39,7 @@ export const WorkspaceViewForm: React.FC = observer((props) => { reset, setValue, watch, - } = useForm({ + } = useForm({ defaultValues, }); @@ -59,7 +59,35 @@ export const WorkspaceViewForm: React.FC = observer((props) => { }); }, [data, preLoadedData, reset]); - const selectedFilters = watch("filters"); + const selectedFilters: IIssueFilterOptions = watch("filters"); + + // filters whose value not null or empty array + let appliedFilters: IIssueFilterOptions | undefined = undefined; + Object.entries(selectedFilters ?? {}).forEach(([key, value]) => { + if (!value) return; + if (Array.isArray(value) && value.length === 0) return; + if (!appliedFilters) appliedFilters = {}; + appliedFilters[key as keyof IIssueFilterOptions] = value; + }); + + const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { + // To clear all filters of any particular filter key. + if (!value) { + setValue("filters", { + ...selectedFilters, + [key]: [], + }); + return; + } + + let newValues = selectedFilters?.[key] ?? []; + newValues = newValues.filter((val) => val !== value); + + setValue("filters", { + ...selectedFilters, + [key]: newValues, + }); + }; const clearAllFilters = () => { if (!selectedFilters) return; @@ -151,11 +179,12 @@ export const WorkspaceViewForm: React.FC = observer((props) => { {selectedFilters && Object.keys(selectedFilters).length > 0 && (
{}} + handleRemoveFilter={handleRemoveFilter} labels={workspaceLabels ?? undefined} states={undefined} + alwaysAllowEditing />
)} diff --git a/web/components/workspace/views/header.tsx b/web/components/workspace/views/header.tsx index faa710131..223fda13c 100644 --- a/web/components/workspace/views/header.tsx +++ b/web/components/workspace/views/header.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useRouter } from "next/router"; import Link from "next/link"; import { observer } from "mobx-react-lite"; @@ -24,9 +24,8 @@ const ViewTab = observer((props: { viewId: string }) => { if (!view) return null; return ( - + { export const GlobalViewsHeader: React.FC = observer(() => { // states const [createViewModal, setCreateViewModal] = useState(false); + const containerRef = useRef(null); // router const router = useRouter(); const { workspaceSlug, globalViewId } = router.query; @@ -54,19 +54,22 @@ export const GlobalViewsHeader: React.FC = observer(() => { // bring the active view to the centre of the header useEffect(() => { - if (!globalViewId) return; - - captureEvent(GLOBAL_VIEW_OPENED, { - view_id: globalViewId, - view_type: ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString()) - ? "Default" - : "Custom", - }); - - const activeTabElement = document.querySelector(`#global-view-${globalViewId.toString()}`); - - if (activeTabElement) activeTabElement.scrollIntoView({ behavior: "smooth", inline: "center" }); - }, [globalViewId]); + if (globalViewId && currentWorkspaceViews) { + captureEvent(GLOBAL_VIEW_OPENED, { + view_id: globalViewId, + view_type: ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString()) + ? "Default" + : "Custom", + }); + const activeTabElement = document.querySelector(`#global-view-${globalViewId.toString()}`); + if (activeTabElement && containerRef.current) { + const containerRect = containerRef.current.getBoundingClientRect(); + const activeTabRect = activeTabElement.getBoundingClientRect(); + const diff = containerRect.right - activeTabRect.right; + activeTabElement.scrollIntoView({ behavior: "smooth", inline: diff > 500 ? "center" : "nearest" }); + } + } + }, [globalViewId, currentWorkspaceViews, containerRef]); const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; @@ -74,9 +77,12 @@ export const GlobalViewsHeader: React.FC = observer(() => { <> setCreateViewModal(false)} />
-
+
{DEFAULT_GLOBAL_VIEWS_LIST.map((tab) => ( - + = observer((props) => { setDeleteViewModal(false)} />
-
+
diff --git a/web/constants/dashboard.ts b/web/constants/dashboard.ts index 3f251ca78..b1cfa51d7 100644 --- a/web/constants/dashboard.ts +++ b/web/constants/dashboard.ts @@ -256,7 +256,7 @@ export const SIDEBAR_MENU_ITEMS: { label: "Analytics", href: `/analytics`, access: EUserWorkspaceRoles.MEMBER, - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/analytics`, + highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/analytics`), Icon: BarChart2, }, { diff --git a/web/constants/issue.ts b/web/constants/issue.ts index 4bb6bcec0..b2a8cd855 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -395,8 +395,8 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { type: [null, "active", "backlog"], }, extra_options: { - access: false, - values: [], + access: true, + values: ["sub_issue"], }, }, gantt_chart: { diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index b0121320e..831cb321e 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -1,10 +1,20 @@ import { v4 as uuidv4 } from "uuid"; +import differenceInCalendarDays from "date-fns/differenceInCalendarDays"; // helpers import { orderArrayBy } from "helpers/array.helper"; // types -import { TIssue, TIssueGroupByOptions, TIssueLayouts, TIssueOrderByOptions, TIssueParams } from "@plane/types"; +import { + TIssue, + TIssueGroupByOptions, + TIssueLayouts, + TIssueOrderByOptions, + TIssueParams, + TStateGroups, +} from "@plane/types"; +import { IGanttBlock } from "components/gantt-chart"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { STATE_GROUPS } from "constants/state"; type THandleIssuesMutation = ( formData: Partial, @@ -123,6 +133,7 @@ export const createIssuePayload: (projectId: string, formData: Partial) const payload: TIssue = { id: uuidv4(), project_id: projectId, + priority: "none", // tempId is used for optimistic updates. It is not a part of the API response. tempId: uuidv4(), // to be overridden by the form data @@ -131,3 +142,32 @@ export const createIssuePayload: (projectId: string, formData: Partial) return payload; }; + +/** + * @description check if the issue due date should be highlighted + * @param date + * @param stateGroup + * @returns boolean + */ +export const shouldHighlightIssueDueDate = ( + date: string | Date | null, + stateGroup: TStateGroups | undefined +): boolean => { + if (!date || !stateGroup) return false; + // if the issue is completed or cancelled, don't highlight the due date + if ([STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateGroup)) return false; + + const parsedDate = new Date(date); + const targetDateDistance = differenceInCalendarDays(parsedDate, new Date()); + + // if the issue is overdue, highlight the due date + return targetDateDistance <= 0; +}; +export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] => + blocks?.map((block) => ({ + data: block, + id: block.id, + sort_order: block.sort_order, + start_date: block.start_date ? new Date(block.start_date) : null, + target_date: block.target_date ? new Date(block.target_date) : null, + })); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx index c24c80a92..97583d16c 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx @@ -30,7 +30,7 @@ const ProjectArchivedIssuesPage: NextPageWithLayout = observer(() => { <>
-
+
-
diff --git a/web/pages/profile/index.tsx b/web/pages/profile/index.tsx index e967df828..bdde41d08 100644 --- a/web/pages/profile/index.tsx +++ b/web/pages/profile/index.tsx @@ -250,6 +250,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { hasError={Boolean(errors.first_name)} placeholder="Enter your first name" className={`w-full rounded-md ${errors.first_name ? "border-red-500" : ""}`} + maxLength={24} /> )} /> @@ -273,6 +274,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { hasError={Boolean(errors.last_name)} placeholder="Enter your last name" className="w-full rounded-md" + maxLength={24} /> )} /> @@ -369,6 +371,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { hasError={Boolean(errors.display_name)} placeholder="Enter your display name" className={`w-full ${errors.display_name ? "border-red-500" : ""}`} + maxLength={24} /> )} /> diff --git a/web/services/issue/issue.service.ts b/web/services/issue/issue.service.ts index 5d3663dd6..316288278 100644 --- a/web/services/issue/issue.service.ts +++ b/web/services/issue/issue.service.ts @@ -1,14 +1,7 @@ // services import { APIService } from "services/api.service"; // type -import type { - TIssue, - IIssueDisplayProperties, - ILinkDetails, - TIssueLink, - TIssueSubIssues, - TIssueActivity, -} from "@plane/types"; +import type { TIssue, IIssueDisplayProperties, TIssueLink, TIssueSubIssues, TIssueActivity } from "@plane/types"; // helper import { API_BASE_URL } from "helpers/common.helper"; @@ -211,7 +204,7 @@ export class IssueService extends APIService { projectId: string, issueId: string, data: Partial - ): Promise { + ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/`, data) .then((response) => response?.data) .catch((error) => { @@ -225,7 +218,7 @@ export class IssueService extends APIService { issueId: string, linkId: string, data: Partial - ): Promise { + ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/${linkId}/`, data diff --git a/web/services/issue/issue_archive.service.ts b/web/services/issue/issue_archive.service.ts index 3337f23ce..065f41d7e 100644 --- a/web/services/issue/issue_archive.service.ts +++ b/web/services/issue/issue_archive.service.ts @@ -1,6 +1,7 @@ import { APIService } from "services/api.service"; // type import { API_BASE_URL } from "helpers/common.helper"; +import { TIssue } from "@plane/types"; export class IssueArchiveService extends APIService { constructor() { @@ -25,8 +26,15 @@ export class IssueArchiveService extends APIService { }); } - async retrieveArchivedIssue(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/${issueId}/`) + async retrieveArchivedIssue( + workspaceSlug: string, + projectId: string, + issueId: string, + queries?: any + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/${issueId}/`, { + params: queries, + }) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/web/store/issue/archived/filter.store.ts b/web/store/issue/archived/filter.store.ts index d92453a30..032928cda 100644 --- a/web/store/issue/archived/filter.store.ts +++ b/web/store/issue/archived/filter.store.ts @@ -89,8 +89,6 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc filteredParams ); - if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; - return filteredRouteParams; } @@ -183,11 +181,6 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc _filters.displayFilters.group_by = "state"; updatedDisplayFilters.group_by = "state"; } - // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true - if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { - _filters.displayFilters.sub_issue = false; - updatedDisplayFilters.sub_issue = false; - } runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { diff --git a/web/store/issue/archived/issue.store.ts b/web/store/issue/archived/issue.store.ts index dca00d702..fa3a06f37 100644 --- a/web/store/issue/archived/issue.store.ts +++ b/web/store/issue/archived/issue.store.ts @@ -17,7 +17,7 @@ export interface IArchivedIssues { groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; // actions fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise; - removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; removeIssueFromArchived: (workspaceSlug: string, projectId: string, issueId: string) => Promise; quickAddIssue: undefined; } @@ -111,15 +111,13 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { try { - const response = await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); + await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); const issueIndex = this.issues[projectId].findIndex((_issueId) => _issueId === issueId); if (issueIndex >= 0) runInAction(() => { this.issues[projectId].splice(issueIndex, 1); }); - - return response; } catch (error) { throw error; } diff --git a/web/store/issue/cycle/filter.store.ts b/web/store/issue/cycle/filter.store.ts index dd81cfc0e..b938a36d4 100644 --- a/web/store/issue/cycle/filter.store.ts +++ b/web/store/issue/cycle/filter.store.ts @@ -90,8 +90,6 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI filteredParams ); - if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; - return filteredRouteParams; } @@ -195,12 +193,6 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI updatedDisplayFilters.group_by = "state"; } - // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true - if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { - _filters.displayFilters.sub_issue = false; - updatedDisplayFilters.sub_issue = false; - } - runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { set( diff --git a/web/store/issue/cycle/issue.store.ts b/web/store/issue/cycle/issue.store.ts index 5a9cae62c..41731e134 100644 --- a/web/store/issue/cycle/issue.store.ts +++ b/web/store/issue/cycle/issue.store.ts @@ -41,13 +41,13 @@ export interface ICycleIssues { issueId: string, data: Partial, cycleId?: string | undefined - ) => Promise; + ) => Promise; removeIssue: ( workspaceSlug: string, projectId: string, issueId: string, cycleId?: string | undefined - ) => Promise; + ) => Promise; quickAddIssue: ( workspaceSlug: string, projectId: string, @@ -207,9 +207,8 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { try { if (!cycleId) throw new Error("Cycle Id is required"); - const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); + await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId); - return response; } catch (error) { this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId); throw error; @@ -225,7 +224,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { try { if (!cycleId) throw new Error("Cycle Id is required"); - const response = await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); + await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId); const issueIndex = this.issues[cycleId].findIndex((_issueId) => _issueId === issueId); @@ -233,8 +232,6 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { runInAction(() => { this.issues[cycleId].splice(issueIndex, 1); }); - - return response; } catch (error) { throw error; } diff --git a/web/store/issue/draft/filter.store.ts b/web/store/issue/draft/filter.store.ts index 8295c263d..cc58a7755 100644 --- a/web/store/issue/draft/filter.store.ts +++ b/web/store/issue/draft/filter.store.ts @@ -89,8 +89,6 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI filteredParams ); - if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; - return filteredRouteParams; } @@ -179,12 +177,6 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI updatedDisplayFilters.group_by = "state"; } - // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true - if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { - _filters.displayFilters.sub_issue = false; - updatedDisplayFilters.sub_issue = false; - } - runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { set( diff --git a/web/store/issue/draft/issue.store.ts b/web/store/issue/draft/issue.store.ts index 5e42e9bab..ee6d785ec 100644 --- a/web/store/issue/draft/issue.store.ts +++ b/web/store/issue/draft/issue.store.ts @@ -22,8 +22,8 @@ export interface IDraftIssues { // actions fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise; createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; - updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; - removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; + removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; quickAddIssue: undefined; } @@ -141,7 +141,7 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues { updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { try { - const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); + await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); if (data.hasOwnProperty("is_draft") && data?.is_draft === false) { runInAction(() => { @@ -151,8 +151,6 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues { }); }); } - - return response; } catch (error) { this.fetchIssues(workspaceSlug, projectId, "mutation"); throw error; @@ -161,7 +159,7 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues { removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { try { - const response = await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); + await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); runInAction(() => { update(this.issues, [projectId], (issueIds = []) => { @@ -169,8 +167,6 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues { return issueIds; }); }); - - return response; } catch (error) { throw error; } diff --git a/web/store/issue/issue-details/attachment.store.ts b/web/store/issue/issue-details/attachment.store.ts index 4550e7fda..5341058c1 100644 --- a/web/store/issue/issue-details/attachment.store.ts +++ b/web/store/issue/issue-details/attachment.store.ts @@ -11,6 +11,7 @@ import { IIssueDetail } from "./root.store"; import { TIssueAttachment, TIssueAttachmentMap, TIssueAttachmentIdMap } from "@plane/types"; export interface IIssueAttachmentStoreActions { + addAttachments: (issueId: string, attachments: TIssueAttachment[]) => void; fetchAttachments: (workspaceSlug: string, projectId: string, issueId: string) => Promise; createAttachment: ( workspaceSlug: string, @@ -54,6 +55,7 @@ export class IssueAttachmentStore implements IIssueAttachmentStore { // computed issueAttachments: computed, // actions + addAttachments: action.bound, fetchAttachments: action, createAttachment: action, removeAttachment: action, @@ -83,17 +85,21 @@ export class IssueAttachmentStore implements IIssueAttachmentStore { }; // actions + addAttachments = (issueId: string, attachments: TIssueAttachment[]) => { + if (attachments && attachments.length > 0) { + const _attachmentIds = attachments.map((attachment) => attachment.id); + runInAction(() => { + update(this.attachments, [issueId], (attachmentIds = []) => uniq(concat(attachmentIds, _attachmentIds))); + attachments.forEach((attachment) => set(this.attachmentMap, attachment.id, attachment)); + }); + } + }; + fetchAttachments = async (workspaceSlug: string, projectId: string, issueId: string) => { try { const response = await this.issueAttachmentService.getIssueAttachment(workspaceSlug, projectId, issueId); - if (response && response.length > 0) { - const _attachmentIds = response.map((attachment) => attachment.id); - runInAction(() => { - update(this.attachments, [issueId], (attachmentIds = []) => uniq(concat(attachmentIds, _attachmentIds))); - response.forEach((attachment) => set(this.attachmentMap, attachment.id, attachment)); - }); - } + this.addAttachments(issueId, response); return response; } catch (error) { diff --git a/web/store/issue/issue-details/issue.store.ts b/web/store/issue/issue-details/issue.store.ts index ccde8c26b..8731bf478 100644 --- a/web/store/issue/issue-details/issue.store.ts +++ b/web/store/issue/issue-details/issue.store.ts @@ -2,15 +2,15 @@ import { makeObservable } from "mobx"; // services import { IssueArchiveService, IssueService } from "services/issue"; // types -import { IIssueDetail } from "./root.store"; import { TIssue } from "@plane/types"; import { computedFn } from "mobx-utils"; +import { IIssueDetail } from "./root.store"; export interface IIssueStoreActions { // actions fetchIssue: (workspaceSlug: string, projectId: string, issueId: string, isArchived?: boolean) => Promise; - updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; - removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; + removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; addModulesToIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise; @@ -54,12 +54,13 @@ export class IssueStore implements IIssueStore { fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => { try { const query = { - expand: "state,assignees,labels,parent", + expand: "issue_reactions,issue_attachment,issue_link,parent", }; - let issue: any; + let issue: TIssue; - if (isArchived) issue = await this.issueArchiveService.retrieveArchivedIssue(workspaceSlug, projectId, issueId); + if (isArchived) + issue = await this.issueArchiveService.retrieveArchivedIssue(workspaceSlug, projectId, issueId, query); else issue = await this.issueService.retrieve(workspaceSlug, projectId, issueId, query); if (!issue) throw new Error("Issue not found"); @@ -75,13 +76,15 @@ export class IssueStore implements IIssueStore { // state // issue reactions - this.rootIssueDetailStore.reaction.fetchReactions(workspaceSlug, projectId, issueId); + if (issue.issue_reactions) this.rootIssueDetailStore.addReactions(issueId, issue.issue_reactions); // fetch issue links - this.rootIssueDetailStore.link.fetchLinks(workspaceSlug, projectId, issueId); + if (issue.issue_link) this.rootIssueDetailStore.addLinks(issueId, issue.issue_link); // fetch issue attachments - this.rootIssueDetailStore.attachment.fetchAttachments(workspaceSlug, projectId, issueId); + if (issue.issue_attachment) this.rootIssueDetailStore.addAttachments(issueId, issue.issue_attachment); + + this.rootIssueDetailStore.addSubscription(issueId, issue.is_subscribed); // fetch issue activity this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); @@ -89,9 +92,6 @@ export class IssueStore implements IIssueStore { // fetch issue comments this.rootIssueDetailStore.comment.fetchComments(workspaceSlug, projectId, issueId); - // fetch issue subscription - this.rootIssueDetailStore.subscription.fetchSubscriptions(workspaceSlug, projectId, issueId); - // fetch sub issues this.rootIssueDetailStore.subIssues.fetchSubIssues(workspaceSlug, projectId, issueId); @@ -109,14 +109,8 @@ export class IssueStore implements IIssueStore { }; updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { - const issue = await this.rootIssueDetailStore.rootIssueStore.projectIssues.updateIssue( - workspaceSlug, - projectId, - issueId, - data - ); + await this.rootIssueDetailStore.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); - return issue; }; removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => diff --git a/web/store/issue/issue-details/link.store.ts b/web/store/issue/issue-details/link.store.ts index 751cdcaa1..81d13438c 100644 --- a/web/store/issue/issue-details/link.store.ts +++ b/web/store/issue/issue-details/link.store.ts @@ -7,16 +7,22 @@ import { IIssueDetail } from "./root.store"; import { TIssueLink, TIssueLinkMap, TIssueLinkIdMap } from "@plane/types"; export interface IIssueLinkStoreActions { + addLinks: (issueId: string, links: TIssueLink[]) => void; fetchLinks: (workspaceSlug: string, projectId: string, issueId: string) => Promise; - createLink: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; + createLink: ( + workspaceSlug: string, + projectId: string, + issueId: string, + data: Partial + ) => Promise; updateLink: ( workspaceSlug: string, projectId: string, issueId: string, linkId: string, data: Partial - ) => Promise; - removeLink: (workspaceSlug: string, projectId: string, issueId: string, linkId: string) => Promise; + ) => Promise; + removeLink: (workspaceSlug: string, projectId: string, issueId: string, linkId: string) => Promise; } export interface IIssueLinkStore extends IIssueLinkStoreActions { @@ -47,6 +53,7 @@ export class IssueLinkStore implements IIssueLinkStore { // computed issueLinks: computed, // actions + addLinks: action.bound, fetchLinks: action, createLink: action, updateLink: action, @@ -77,15 +84,17 @@ export class IssueLinkStore implements IIssueLinkStore { }; // actions + addLinks = (issueId: string, links: TIssueLink[]) => { + runInAction(() => { + this.links[issueId] = links.map((link) => link.id); + links.forEach((link) => set(this.linkMap, link.id, link)); + }); + }; + fetchLinks = async (workspaceSlug: string, projectId: string, issueId: string) => { try { const response = await this.issueService.fetchIssueLinks(workspaceSlug, projectId, issueId); - - runInAction(() => { - this.links[issueId] = response.map((link) => link.id); - response.forEach((link) => set(this.linkMap, link.id, link)); - }); - + this.addLinks(issueId, response); return response; } catch (error) { throw error; @@ -136,7 +145,7 @@ export class IssueLinkStore implements IIssueLinkStore { removeLink = async (workspaceSlug: string, projectId: string, issueId: string, linkId: string) => { try { - const response = await this.issueService.deleteIssueLink(workspaceSlug, projectId, issueId, linkId); + await this.issueService.deleteIssueLink(workspaceSlug, projectId, issueId, linkId); const linkIndex = this.links[issueId].findIndex((_comment) => _comment === linkId); if (linkIndex >= 0) @@ -147,7 +156,6 @@ export class IssueLinkStore implements IIssueLinkStore { // fetching activity this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); - return response; } catch (error) { throw error; } diff --git a/web/store/issue/issue-details/reaction.store.ts b/web/store/issue/issue-details/reaction.store.ts index 6b15f4445..6282ac40e 100644 --- a/web/store/issue/issue-details/reaction.store.ts +++ b/web/store/issue/issue-details/reaction.store.ts @@ -14,6 +14,7 @@ import { groupReactions } from "helpers/emoji.helper"; export interface IIssueReactionStoreActions { // actions + addReactions: (issueId: string, reactions: TIssueReaction[]) => void; fetchReactions: (workspaceSlug: string, projectId: string, issueId: string) => Promise; createReaction: (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => Promise; removeReaction: ( @@ -50,6 +51,7 @@ export class IssueReactionStore implements IIssueReactionStore { reactions: observable, reactionMap: observable, // actions + addReactions: action.bound, fetchReactions: action, createReaction: action, removeReaction: action, @@ -82,30 +84,35 @@ export class IssueReactionStore implements IIssueReactionStore { if (reactions?.[reaction]) reactions?.[reaction].map((reactionId) => { const currentReaction = this.getReactionById(reactionId); - if (currentReaction && currentReaction.actor === userId) _userReactions.push(currentReaction); + if (currentReaction && currentReaction.actor_id === userId) _userReactions.push(currentReaction); }); }); return _userReactions; }; + addReactions = (issueId: string, reactions: TIssueReaction[]) => { + const groupedReactions = groupReactions(reactions || [], "reaction"); + + const issueReactionIdsMap: { [reaction: string]: string[] } = {}; + + Object.keys(groupedReactions).map((reactionId) => { + const reactionIds = (groupedReactions[reactionId] || []).map((reaction) => reaction.id); + issueReactionIdsMap[reactionId] = reactionIds; + }); + + runInAction(() => { + set(this.reactions, issueId, issueReactionIdsMap); + reactions.forEach((reaction) => set(this.reactionMap, reaction.id, reaction)); + }); + }; + // actions fetchReactions = async (workspaceSlug: string, projectId: string, issueId: string) => { try { const response = await this.issueReactionService.listIssueReactions(workspaceSlug, projectId, issueId); - const groupedReactions = groupReactions(response || [], "reaction"); - const issueReactionIdsMap: { [reaction: string]: string[] } = {}; - - Object.keys(groupedReactions).map((reactionId) => { - const reactionIds = (groupedReactions[reactionId] || []).map((reaction) => reaction.id); - issueReactionIdsMap[reactionId] = reactionIds; - }); - - runInAction(() => { - set(this.reactions, issueId, issueReactionIdsMap); - response.forEach((reaction) => set(this.reactionMap, reaction.id, reaction)); - }); + this.addReactions(issueId, response); return response; } catch (error) { @@ -144,7 +151,7 @@ export class IssueReactionStore implements IIssueReactionStore { ) => { try { const userReactions = this.reactionsByUser(issueId, userId); - const currentReaction = find(userReactions, { actor: userId, reaction: reaction }); + const currentReaction = find(userReactions, { actor_id: userId, reaction: reaction }); if (currentReaction && currentReaction.id) { runInAction(() => { diff --git a/web/store/issue/issue-details/root.store.ts b/web/store/issue/issue-details/root.store.ts index d78add446..4c2d6add1 100644 --- a/web/store/issue/issue-details/root.store.ts +++ b/web/store/issue/issue-details/root.store.ts @@ -15,8 +15,15 @@ import { IssueCommentReactionStore, IIssueCommentReactionStoreActions, } from "./comment_reaction.store"; - -import { TIssue, TIssueComment, TIssueCommentReaction, TIssueLink, TIssueRelationTypes } from "@plane/types"; +import { + TIssue, + TIssueAttachment, + TIssueComment, + TIssueCommentReaction, + TIssueLink, + TIssueReaction, + TIssueRelationTypes, +} from "@plane/types"; export type TPeekIssue = { workspaceSlug: string; @@ -151,6 +158,7 @@ export class IssueDetail implements IIssueDetail { this.issue.removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId); // reactions + addReactions = (issueId: string, reactions: TIssueReaction[]) => this.reaction.addReactions(issueId, reactions); fetchReactions = async (workspaceSlug: string, projectId: string, issueId: string) => this.reaction.fetchReactions(workspaceSlug, projectId, issueId); createReaction = async (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => @@ -164,6 +172,8 @@ export class IssueDetail implements IIssueDetail { ) => this.reaction.removeReaction(workspaceSlug, projectId, issueId, reaction, userId); // attachments + addAttachments = (issueId: string, attachments: TIssueAttachment[]) => + this.attachment.addAttachments(issueId, attachments); fetchAttachments = async (workspaceSlug: string, projectId: string, issueId: string) => this.attachment.fetchAttachments(workspaceSlug, projectId, issueId); createAttachment = async (workspaceSlug: string, projectId: string, issueId: string, data: FormData) => @@ -172,6 +182,7 @@ export class IssueDetail implements IIssueDetail { this.attachment.removeAttachment(workspaceSlug, projectId, issueId, attachmentId); // link + addLinks = (issueId: string, links: TIssueLink[]) => this.link.addLinks(issueId, links); fetchLinks = async (workspaceSlug: string, projectId: string, issueId: string) => this.link.fetchLinks(workspaceSlug, projectId, issueId); createLink = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => @@ -206,6 +217,8 @@ export class IssueDetail implements IIssueDetail { this.subIssues.deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId); // subscription + addSubscription = (issueId: string, isSubscribed: boolean | undefined | null) => + this.subscription.addSubscription(issueId, isSubscribed); fetchSubscriptions = async (workspaceSlug: string, projectId: string, issueId: string) => this.subscription.fetchSubscriptions(workspaceSlug, projectId, issueId); createSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => diff --git a/web/store/issue/issue-details/subscription.store.ts b/web/store/issue/issue-details/subscription.store.ts index 02f863cbe..276c952f4 100644 --- a/web/store/issue/issue-details/subscription.store.ts +++ b/web/store/issue/issue-details/subscription.store.ts @@ -6,21 +6,22 @@ import { NotificationService } from "services/notification.service"; import { IIssueDetail } from "./root.store"; export interface IIssueSubscriptionStoreActions { - fetchSubscriptions: (workspaceSlug: string, projectId: string, issueId: string) => Promise; - createSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise; - removeSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + addSubscription: (issueId: string, isSubscribed: boolean | undefined | null) => void; + fetchSubscriptions: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + createSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + removeSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise; } export interface IIssueSubscriptionStore extends IIssueSubscriptionStoreActions { // observables - subscriptionMap: Record>>; // Record defines subscriptionId as key and link as value + subscriptionMap: Record>; // Record defines subscriptionId as key and link as value // helper methods - getSubscriptionByIssueId: (issueId: string) => Record | undefined; + getSubscriptionByIssueId: (issueId: string) => boolean | undefined; } export class IssueSubscriptionStore implements IIssueSubscriptionStore { // observables - subscriptionMap: Record>> = {}; + subscriptionMap: Record> = {}; // root store rootIssueDetail: IIssueDetail; // services @@ -31,6 +32,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore { // observables subscriptionMap: observable, // actions + addSubscription: action.bound, fetchSubscriptions: action, createSubscription: action, removeSubscription: action, @@ -49,22 +51,26 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore { return this.subscriptionMap[issueId]?.[currentUserId] ?? undefined; }; + addSubscription = (issueId: string, isSubscribed: boolean | undefined | null) => { + const currentUserId = this.rootIssueDetail.rootIssueStore.currentUserId; + if (!currentUserId) throw new Error("user id not available"); + + runInAction(() => { + set(this.subscriptionMap, [issueId, currentUserId], isSubscribed ?? false); + }); + }; + fetchSubscriptions = async (workspaceSlug: string, projectId: string, issueId: string) => { try { - const currentUserId = this.rootIssueDetail.rootIssueStore.currentUserId; - if (!currentUserId) throw new Error("user id not available"); - const subscription = await this.notificationService.getIssueNotificationSubscriptionStatus( workspaceSlug, projectId, issueId ); - runInAction(() => { - set(this.subscriptionMap, [issueId, currentUserId], subscription); - }); + this.addSubscription(issueId, subscription?.subscribed); - return subscription; + return subscription?.subscribed; } catch (error) { throw error; } @@ -79,9 +85,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore { set(this.subscriptionMap, [issueId, currentUserId], { subscribed: true }); }); - const response = await this.notificationService.subscribeToIssueNotifications(workspaceSlug, projectId, issueId); - - return response; + await this.notificationService.subscribeToIssueNotifications(workspaceSlug, projectId, issueId); } catch (error) { this.fetchSubscriptions(workspaceSlug, projectId, issueId); throw error; @@ -97,13 +101,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore { set(this.subscriptionMap, [issueId, currentUserId], { subscribed: false }); }); - const response = await this.notificationService.unsubscribeFromIssueNotifications( - workspaceSlug, - projectId, - issueId - ); - - return response; + await this.notificationService.unsubscribeFromIssueNotifications(workspaceSlug, projectId, issueId); } catch (error) { this.fetchSubscriptions(workspaceSlug, projectId, issueId); throw error; diff --git a/web/store/issue/issue_gantt_view.store.ts b/web/store/issue/issue_gantt_view.store.ts new file mode 100644 index 000000000..b087554dd --- /dev/null +++ b/web/store/issue/issue_gantt_view.store.ts @@ -0,0 +1,95 @@ +import { action, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; +// helpers +import { currentViewDataWithView } from "components/gantt-chart/data"; +// types +import { ChartDataType, TGanttViews } from "components/gantt-chart"; + +export interface IGanttStore { + // observables + currentView: TGanttViews; + currentViewData: ChartDataType | undefined; + activeBlockId: string | null; + renderView: any; + // computed functions + isBlockActive: (blockId: string) => boolean; + // actions + updateCurrentView: (view: TGanttViews) => void; + updateCurrentViewData: (data: ChartDataType | undefined) => void; + updateActiveBlockId: (blockId: string | null) => void; + updateRenderView: (data: any[]) => void; +} + +export class GanttStore implements IGanttStore { + // observables + currentView: TGanttViews = "month"; + currentViewData: ChartDataType | undefined = undefined; + activeBlockId: string | null = null; + renderView: any[] = []; + + constructor() { + makeObservable(this, { + // observables + currentView: observable.ref, + currentViewData: observable, + activeBlockId: observable.ref, + renderView: observable, + // actions + updateCurrentView: action.bound, + updateCurrentViewData: action.bound, + updateActiveBlockId: action.bound, + updateRenderView: action.bound, + }); + + this.initGantt(); + } + + /** + * @description check if block is active + * @param {string} blockId + */ + isBlockActive = computedFn((blockId: string): boolean => this.activeBlockId === blockId); + + /** + * @description update current view + * @param {TGanttViews} view + */ + updateCurrentView = (view: TGanttViews) => { + this.currentView = view; + }; + + /** + * @description update current view data + * @param {ChartDataType | undefined} data + */ + updateCurrentViewData = (data: ChartDataType | undefined) => { + this.currentViewData = data; + }; + + /** + * @description update active block + * @param {string | null} block + */ + updateActiveBlockId = (blockId: string | null) => { + this.activeBlockId = blockId; + }; + + /** + * @description update render view + * @param {any[]} data + */ + updateRenderView = (data: any[]) => { + this.renderView = data; + }; + + /** + * @description initialize gantt chart with month view + */ + initGantt = () => { + const newCurrentViewData = currentViewDataWithView(this.currentView); + + runInAction(() => { + this.currentViewData = newCurrentViewData; + }); + }; +} diff --git a/web/store/issue/module/filter.store.ts b/web/store/issue/module/filter.store.ts index e92027235..f10a885a3 100644 --- a/web/store/issue/module/filter.store.ts +++ b/web/store/issue/module/filter.store.ts @@ -90,8 +90,6 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul filteredParams ); - if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; - return filteredRouteParams; } @@ -194,12 +192,6 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul updatedDisplayFilters.group_by = "state"; } - // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true - if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { - _filters.displayFilters.sub_issue = false; - updatedDisplayFilters.sub_issue = false; - } - runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { set( diff --git a/web/store/issue/module/issue.store.ts b/web/store/issue/module/issue.store.ts index b83519cd2..e9b96ac54 100644 --- a/web/store/issue/module/issue.store.ts +++ b/web/store/issue/module/issue.store.ts @@ -39,13 +39,13 @@ export interface IModuleIssues { issueId: string, data: Partial, moduleId?: string | undefined - ) => Promise; + ) => Promise; removeIssue: ( workspaceSlug: string, projectId: string, issueId: string, moduleId?: string | undefined - ) => Promise; + ) => Promise; quickAddIssue: ( workspaceSlug: string, projectId: string, @@ -212,9 +212,8 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { try { if (!moduleId) throw new Error("Module Id is required"); - const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); + await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId); - return response; } catch (error) { this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId); throw error; @@ -230,7 +229,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { try { if (!moduleId) throw new Error("Module Id is required"); - const response = await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); + await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId); const issueIndex = this.issues[moduleId].findIndex((_issueId) => _issueId === issueId); @@ -238,8 +237,6 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { runInAction(() => { this.issues[moduleId].splice(issueIndex, 1); }); - - return response; } catch (error) { throw error; } diff --git a/web/store/issue/profile/filter.store.ts b/web/store/issue/profile/filter.store.ts index 563af5b01..658980082 100644 --- a/web/store/issue/profile/filter.store.ts +++ b/web/store/issue/profile/filter.store.ts @@ -93,8 +93,6 @@ export class ProfileIssuesFilter extends IssueFilterHelperStore implements IProf filteredParams ); - if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; - return filteredRouteParams; } @@ -188,11 +186,6 @@ export class ProfileIssuesFilter extends IssueFilterHelperStore implements IProf _filters.displayFilters.group_by = "priority"; updatedDisplayFilters.group_by = "priority"; } - // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true - if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { - _filters.displayFilters.sub_issue = false; - updatedDisplayFilters.sub_issue = false; - } runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { diff --git a/web/store/issue/profile/issue.store.ts b/web/store/issue/profile/issue.store.ts index 5cde37230..461928c64 100644 --- a/web/store/issue/profile/issue.store.ts +++ b/web/store/issue/profile/issue.store.ts @@ -41,13 +41,13 @@ export interface IProfileIssues { issueId: string, data: Partial, userId?: string | undefined - ) => Promise; + ) => Promise; removeIssue: ( workspaceSlug: string, projectId: string, issueId: string, userId?: string | undefined - ) => Promise; + ) => Promise; quickAddIssue: undefined; } @@ -221,14 +221,7 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues { if (!userId) throw new Error("user id is required"); this.rootStore.issues.updateIssue(issueId, data); - const response = await this.rootIssueStore.projectIssues.updateIssue( - workspaceSlug, - projectId, - data.id as keyof TIssue, - data - ); - - return response; + await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, data.id as keyof TIssue, data); } catch (error) { if (this.currentView) this.fetchIssues(workspaceSlug, undefined, "mutation", userId, this.currentView); throw error; @@ -243,7 +236,7 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues { ) => { if (!userId) return; try { - const response = await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); + await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); const uniqueViewId = `${workspaceSlug}_${this.currentView}`; @@ -252,8 +245,6 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues { runInAction(() => { this.issues[userId][uniqueViewId].splice(issueIndex, 1); }); - - return response; } catch (error) { throw error; } diff --git a/web/store/issue/project-views/filter.store.ts b/web/store/issue/project-views/filter.store.ts index b3df3903b..c7c8988b1 100644 --- a/web/store/issue/project-views/filter.store.ts +++ b/web/store/issue/project-views/filter.store.ts @@ -90,8 +90,6 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I filteredParams ); - if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; - return filteredRouteParams; } @@ -192,11 +190,6 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I _filters.displayFilters.group_by = "state"; updatedDisplayFilters.group_by = "state"; } - // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true - if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { - _filters.displayFilters.sub_issue = false; - updatedDisplayFilters.sub_issue = false; - } runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { diff --git a/web/store/issue/project-views/issue.store.ts b/web/store/issue/project-views/issue.store.ts index d643999c9..8327ffcce 100644 --- a/web/store/issue/project-views/issue.store.ts +++ b/web/store/issue/project-views/issue.store.ts @@ -34,13 +34,13 @@ export interface IProjectViewIssues { issueId: string, data: Partial, viewId?: string | undefined - ) => Promise; + ) => Promise; removeIssue: ( workspaceSlug: string, projectId: string, issueId: string, viewId?: string | undefined - ) => Promise; + ) => Promise; quickAddIssue: ( workspaceSlug: string, projectId: string, @@ -181,8 +181,7 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI try { if (!viewId) throw new Error("View Id is required"); - const response = await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); - return response; + await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); } catch (error) { this.fetchIssues(workspaceSlug, projectId, "mutation"); throw error; @@ -198,15 +197,13 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI try { if (!viewId) throw new Error("View Id is required"); - const response = await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); + await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); const issueIndex = this.issues[viewId].findIndex((_issueId) => _issueId === issueId); if (issueIndex >= 0) runInAction(() => { this.issues[viewId].splice(issueIndex, 1); }); - - return response; } catch (error) { this.fetchIssues(workspaceSlug, projectId, "mutation"); throw error; diff --git a/web/store/issue/project/filter.store.ts b/web/store/issue/project/filter.store.ts index 69393a320..f18654cde 100644 --- a/web/store/issue/project/filter.store.ts +++ b/web/store/issue/project/filter.store.ts @@ -89,8 +89,6 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj filteredParams ); - if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; - return filteredRouteParams; } @@ -191,11 +189,6 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj updatedDisplayFilters.group_by = "state"; } - // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true - if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { - _filters.displayFilters.sub_issue = false; - updatedDisplayFilters.sub_issue = false; - } runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { diff --git a/web/store/issue/project/issue.store.ts b/web/store/issue/project/issue.store.ts index 2000a440a..76bf7bcc2 100644 --- a/web/store/issue/project/issue.store.ts +++ b/web/store/issue/project/issue.store.ts @@ -21,8 +21,8 @@ export interface IProjectIssues { // action fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise; createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; - updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; - removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; + removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; quickAddIssue: (workspaceSlug: string, projectId: string, data: TIssue) => Promise; removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise; } @@ -144,8 +144,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues { try { this.rootStore.issues.updateIssue(issueId, data); - const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data); - return response; + await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data); } catch (error) { this.fetchIssues(workspaceSlug, projectId, "mutation"); throw error; @@ -154,14 +153,13 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues { removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { try { - const response = await this.issueService.deleteIssue(workspaceSlug, projectId, issueId); + await this.issueService.deleteIssue(workspaceSlug, projectId, issueId); runInAction(() => { pull(this.issues[projectId], issueId); }); this.rootStore.issues.removeIssue(issueId); - return response; } catch (error) { throw error; } diff --git a/web/store/issue/workspace/issue.store.ts b/web/store/issue/workspace/issue.store.ts index e168f85c2..e2b8418c7 100644 --- a/web/store/issue/workspace/issue.store.ts +++ b/web/store/issue/workspace/issue.store.ts @@ -30,13 +30,13 @@ export interface IWorkspaceIssues { issueId: string, data: Partial, viewId?: string | undefined - ) => Promise; + ) => Promise; removeIssue: ( workspaceSlug: string, projectId: string, issueId: string, viewId?: string | undefined - ) => Promise; + ) => Promise; } export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssues { @@ -165,8 +165,7 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue if (!viewId) throw new Error("View id is required"); this.rootStore.issues.updateIssue(issueId, data); - const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data); - return response; + await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data); } catch (error) { if (viewId) this.fetchIssues(workspaceSlug, viewId, "mutation"); throw error; @@ -184,7 +183,7 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue const uniqueViewId = `${workspaceSlug}_${viewId}`; - const response = await this.issueService.deleteIssue(workspaceSlug, projectId, issueId); + await this.issueService.deleteIssue(workspaceSlug, projectId, issueId); const issueIndex = this.issues[uniqueViewId].findIndex((_issueId) => _issueId === issueId); if (issueIndex >= 0) @@ -193,8 +192,6 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue }); this.rootStore.issues.removeIssue(issueId); - - return response; } catch (error) { throw error; } diff --git a/web/store/member/workspace-member.store.ts b/web/store/member/workspace-member.store.ts index 1dae25bd4..4a696bfd2 100644 --- a/web/store/member/workspace-member.store.ts +++ b/web/store/member/workspace-member.store.ts @@ -98,7 +98,8 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore { (m) => m.member !== this.userStore.currentUser?.id, (m) => this.memberRoot?.memberMap?.[m.member]?.display_name?.toLowerCase(), ]); - const memberIds = members.map((m) => m.member); + //filter out bots + const memberIds = members.filter((m) => !this.memberRoot?.memberMap?.[m.member]?.is_bot).map((m) => m.member); return memberIds; } diff --git a/web/store/module.store.ts b/web/store/module.store.ts index c27ace487..cd6b7100a 100644 --- a/web/store/module.store.ts +++ b/web/store/module.store.ts @@ -194,7 +194,6 @@ export class ModulesStore implements IModuleStore { runInAction(() => { set(this.moduleMap, [response?.id], response); }); - this.fetchModules(workspaceSlug, projectId); return response; });