diff --git a/.github/workflows/create-sync-pr.yml b/.github/workflows/create-sync-pr.yml index 5b5f958d3..0f85e940c 100644 --- a/.github/workflows/create-sync-pr.yml +++ b/.github/workflows/create-sync-pr.yml @@ -3,7 +3,7 @@ name: Create Sync Action on: pull_request: branches: - - preview + - develop # Change this to preview types: - closed env: @@ -33,14 +33,23 @@ jobs: sudo apt update sudo apt install gh -y - - name: Push Changes to Target Repo + - name: Create Pull Request env: GH_TOKEN: ${{ secrets.ACCESS_TOKEN }} run: | TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}" TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}" + TARGET_BASE_BRANCH="${{ secrets.SYNC_TARGET_BASE_BRANCH_NAME }}" SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}" git checkout $SOURCE_BRANCH git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git" - git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH \ No newline at end of file + git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH + + PR_TITLE=${{secrets.SYNC_PR_TITLE}} + + gh pr create \ + --base $TARGET_BASE_BRANCH \ + --head $TARGET_BRANCH \ + --title "$PR_TITLE" \ + --repo $TARGET_REPO diff --git a/apiserver/plane/api/serializers/base.py b/apiserver/plane/api/serializers/base.py index b96422501..4e88597c7 100644 --- a/apiserver/plane/api/serializers/base.py +++ b/apiserver/plane/api/serializers/base.py @@ -97,7 +97,7 @@ class BaseSerializer(serializers.ModelSerializer): exp_serializer = expansion[expand]( getattr(instance, expand) ) - response[expand] = exp_serializer.data + response[expand] = exp_serializer.data else: # You might need to handle this case differently response[expand] = getattr(instance, f"{expand}_id", None) diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index c406453b7..4e0c12fe5 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -17,6 +17,7 @@ from .workspace import ( WorkspaceThemeSerializer, WorkspaceMemberAdminSerializer, WorkspaceMemberMeSerializer, + WorkspaceUserPropertiesSerializer, ) from .project import ( ProjectSerializer, @@ -31,6 +32,7 @@ from .project import ( ProjectDeployBoardSerializer, ProjectMemberAdminSerializer, ProjectPublicMemberSerializer, + ProjectMemberRoleSerializer, ) from .state import StateSerializer, StateLiteSerializer from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer @@ -39,6 +41,7 @@ from .cycle import ( CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer, + CycleUserPropertiesSerializer, ) from .asset import FileAssetSerializer from .issue import ( @@ -61,6 +64,8 @@ from .issue import ( IssueRelationSerializer, RelatedIssueSerializer, IssuePublicSerializer, + IssueRelationLiteSerializer, + ) from .module import ( @@ -69,6 +74,7 @@ from .module import ( ModuleIssueSerializer, ModuleLinkSerializer, ModuleFavoriteSerializer, + ModuleUserPropertiesSerializer, ) from .api import APITokenSerializer, APITokenReadSerializer diff --git a/apiserver/plane/app/serializers/base.py b/apiserver/plane/app/serializers/base.py index 89c9725d9..f67f5cf52 100644 --- a/apiserver/plane/app/serializers/base.py +++ b/apiserver/plane/app/serializers/base.py @@ -9,11 +9,12 @@ class DynamicBaseSerializer(BaseSerializer): def __init__(self, *args, **kwargs): # If 'fields' is provided in the arguments, remove it and store it separately. # This is done so as not to pass this custom argument up to the superclass. - fields = kwargs.pop("fields", None) + fields = kwargs.pop("fields", []) + self.expand = kwargs.pop("expand", []) or [] + fields = self.expand # Call the initialization of the superclass. super().__init__(*args, **kwargs) - # If 'fields' was provided, filter the fields of the serializer accordingly. if fields is not None: self.fields = self._filter_fields(fields) @@ -47,12 +48,91 @@ class DynamicBaseSerializer(BaseSerializer): elif isinstance(item, dict): allowed.append(list(item.keys())[0]) - # Convert the current serializer's fields and the allowed fields to sets. - existing = set(self.fields) - allowed = set(allowed) + for field in allowed: + if field not in self.fields: + from . import ( + WorkspaceLiteSerializer, + ProjectLiteSerializer, + UserLiteSerializer, + StateLiteSerializer, + IssueSerializer, + LabelSerializer, + CycleIssueSerializer, + IssueFlatSerializer, + ) - # Remove fields from the serializer that aren't in the 'allowed' list. - for field_name in (existing - allowed): - self.fields.pop(field_name) + # Expansion mapper + expansion = { + "user": UserLiteSerializer, + "workspace": WorkspaceLiteSerializer, + "project": ProjectLiteSerializer, + "default_assignee": UserLiteSerializer, + "project_lead": UserLiteSerializer, + "state": StateLiteSerializer, + "created_by": UserLiteSerializer, + "issue": IssueSerializer, + "actor": UserLiteSerializer, + "owned_by": UserLiteSerializer, + "members": UserLiteSerializer, + "assignees": UserLiteSerializer, + "labels": LabelSerializer, + "issue_cycle": CycleIssueSerializer, + "parent": IssueFlatSerializer, + } + + self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle"] else False) return self.fields + + + def to_representation(self, instance): + response = super().to_representation(instance) + + # Ensure 'expand' is iterable before processing + if self.expand: + for expand in self.expand: + if expand in self.fields: + # Import all the expandable serializers + from . import ( + WorkspaceLiteSerializer, + ProjectLiteSerializer, + UserLiteSerializer, + StateLiteSerializer, + IssueSerializer, + LabelSerializer, + CycleIssueSerializer, + ) + + # Expansion mapper + expansion = { + "user": UserLiteSerializer, + "workspace": WorkspaceLiteSerializer, + "project": ProjectLiteSerializer, + "default_assignee": UserLiteSerializer, + "project_lead": UserLiteSerializer, + "state": StateLiteSerializer, + "created_by": UserLiteSerializer, + "issue": IssueSerializer, + "actor": UserLiteSerializer, + "owned_by": UserLiteSerializer, + "members": UserLiteSerializer, + "assignees": UserLiteSerializer, + "labels": LabelSerializer, + "issue_cycle": CycleIssueSerializer, + } + # Check if field in expansion then expand the field + if expand in expansion: + if isinstance(response.get(expand), list): + exp_serializer = expansion[expand]( + getattr(instance, expand), many=True + ) + else: + exp_serializer = expansion[expand]( + getattr(instance, expand) + ) + response[expand] = exp_serializer.data + else: + # You might need to handle this case differently + response[expand] = getattr(instance, f"{expand}_id", None) + + return response diff --git a/apiserver/plane/app/serializers/cycle.py b/apiserver/plane/app/serializers/cycle.py index 63abf3a03..f0ee8f9da 100644 --- a/apiserver/plane/app/serializers/cycle.py +++ b/apiserver/plane/app/serializers/cycle.py @@ -7,7 +7,7 @@ from .user import UserLiteSerializer from .issue import IssueStateSerializer from .workspace import WorkspaceLiteSerializer from .project import ProjectLiteSerializer -from plane.db.models import Cycle, CycleIssue, CycleFavorite +from plane.db.models import Cycle, CycleIssue, CycleFavorite, CycleUserProperties class CycleWriteSerializer(BaseSerializer): @@ -106,3 +106,15 @@ class CycleFavoriteSerializer(BaseSerializer): "project", "user", ] + + +class CycleUserPropertiesSerializer(BaseSerializer): + class Meta: + model = CycleUserProperties + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "cycle" + "user", + ] \ No newline at end of file diff --git a/apiserver/plane/app/serializers/inbox.py b/apiserver/plane/app/serializers/inbox.py index f52a90660..cdc2646dd 100644 --- a/apiserver/plane/app/serializers/inbox.py +++ b/apiserver/plane/app/serializers/inbox.py @@ -49,7 +49,6 @@ class IssueStateInboxSerializer(BaseSerializer): 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) - bridge_id = serializers.UUIDField(read_only=True) issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True) class Meta: diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index b13d03e35..6d39f1760 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -278,17 +278,28 @@ class IssueLabelSerializer(BaseSerializer): ] +class IssueRelationLiteSerializer(DynamicBaseSerializer): + project_id = serializers.PrimaryKeyRelatedField(read_only=True) + class Meta: + model = Issue + fields = [ + "id", + "project_id", + "sequence_id", + ] + read_only_fields = [ + "workspace", + "project", + ] + + class IssueRelationSerializer(BaseSerializer): - issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue") + issue_detail = IssueRelationLiteSerializer(read_only=True, source="related_issue") class Meta: model = IssueRelation fields = [ "issue_detail", - "relation_type", - "related_issue", - "issue", - "id" ] read_only_fields = [ "workspace", @@ -296,16 +307,12 @@ class IssueRelationSerializer(BaseSerializer): ] class RelatedIssueSerializer(BaseSerializer): - issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue") + issue_detail = IssueRelationLiteSerializer(read_only=True, source="issue") class Meta: model = IssueRelation fields = [ "issue_detail", - "relation_type", - "related_issue", - "issue", - "id" ] read_only_fields = [ "workspace", @@ -512,7 +519,6 @@ class IssueStateSerializer(DynamicBaseSerializer): project_detail = ProjectLiteSerializer(read_only=True, source="project") assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) sub_issues_count = serializers.IntegerField(read_only=True) - bridge_id = serializers.UUIDField(read_only=True) attachment_count = serializers.IntegerField(read_only=True) link_count = serializers.IntegerField(read_only=True) @@ -521,32 +527,58 @@ class IssueStateSerializer(DynamicBaseSerializer): fields = "__all__" -class IssueSerializer(BaseSerializer): - project_detail = ProjectLiteSerializer(read_only=True, source="project") - state_detail = StateSerializer(read_only=True, source="state") - parent_detail = IssueStateFlatSerializer(read_only=True, source="parent") - label_details = LabelSerializer(read_only=True, source="labels", many=True) - assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) - related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True) - issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True) - issue_cycle = IssueCycleDetailSerializer(read_only=True) - issue_module = IssueModuleDetailSerializer(read_only=True) - issue_link = IssueLinkSerializer(read_only=True, many=True) - issue_attachment = IssueAttachmentSerializer(read_only=True, many=True) +class IssueSerializer(DynamicBaseSerializer): + # ids + project_id = serializers.PrimaryKeyRelatedField(read_only=True) + state_id = serializers.PrimaryKeyRelatedField(read_only=True) + parent_id = serializers.PrimaryKeyRelatedField(read_only=True) + cycle_id = serializers.PrimaryKeyRelatedField(read_only=True) + module_id = serializers.PrimaryKeyRelatedField(read_only=True) + + # Many to many + label_ids = serializers.PrimaryKeyRelatedField(read_only=True, many=True, source="labels") + assignee_ids = serializers.PrimaryKeyRelatedField(read_only=True, many=True, source="assignees") + + # Count items sub_issues_count = serializers.IntegerField(read_only=True) - issue_reactions = IssueReactionSerializer(read_only=True, many=True) + attachment_count = serializers.IntegerField(read_only=True) + link_count = serializers.IntegerField(read_only=True) + + # is + is_subscribed = serializers.BooleanField(read_only=True) class Meta: model = Issue - fields = "__all__" - read_only_fields = [ - "workspace", - "project", - "created_by", - "updated_by", + fields = [ + "id", + "name", + "state_id", + "description_html", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_id", + "label_ids", + "assignee_ids", + "sub_issues_count", "created_at", "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_subscribed", + "is_draft", + "archived_at", ] + read_only_fields = fields class IssueLiteSerializer(DynamicBaseSerializer): diff --git a/apiserver/plane/app/serializers/module.py b/apiserver/plane/app/serializers/module.py index 48f773b0f..b38d05b2c 100644 --- a/apiserver/plane/app/serializers/module.py +++ b/apiserver/plane/app/serializers/module.py @@ -2,7 +2,7 @@ from rest_framework import serializers # Module imports -from .base import BaseSerializer +from .base import BaseSerializer, DynamicBaseSerializer from .user import UserLiteSerializer from .project import ProjectLiteSerializer from .workspace import WorkspaceLiteSerializer @@ -14,6 +14,7 @@ from plane.db.models import ( ModuleIssue, ModuleLink, ModuleFavorite, + ModuleUserProperties, ) @@ -159,7 +160,7 @@ class ModuleLinkSerializer(BaseSerializer): return ModuleLink.objects.create(**validated_data) -class ModuleSerializer(BaseSerializer): +class ModuleSerializer(DynamicBaseSerializer): project_detail = ProjectLiteSerializer(read_only=True, source="project") lead_detail = UserLiteSerializer(read_only=True, source="lead") members_detail = UserLiteSerializer(read_only=True, many=True, source="members") @@ -196,3 +197,14 @@ class ModuleFavoriteSerializer(BaseSerializer): "project", "user", ] + +class ModuleUserPropertiesSerializer(BaseSerializer): + class Meta: + model = ModuleUserProperties + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "module", + "user" + ] \ No newline at end of file diff --git a/apiserver/plane/app/serializers/project.py b/apiserver/plane/app/serializers/project.py index aef715e33..b3122962b 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -159,6 +159,11 @@ class ProjectMemberAdminSerializer(BaseSerializer): model = ProjectMember fields = "__all__" +class ProjectMemberRoleSerializer(DynamicBaseSerializer): + + class Meta: + model = ProjectMember + fields = ("id", "role", "member", "project") class ProjectMemberInviteSerializer(BaseSerializer): project = ProjectLiteSerializer(read_only=True) diff --git a/apiserver/plane/app/serializers/view.py b/apiserver/plane/app/serializers/view.py index e7502609a..db44a2fc0 100644 --- a/apiserver/plane/app/serializers/view.py +++ b/apiserver/plane/app/serializers/view.py @@ -2,7 +2,7 @@ from rest_framework import serializers # Module imports -from .base import BaseSerializer +from .base import BaseSerializer, DynamicBaseSerializer from .workspace import WorkspaceLiteSerializer from .project import ProjectLiteSerializer from plane.db.models import GlobalView, IssueView, IssueViewFavorite @@ -38,7 +38,7 @@ class GlobalViewSerializer(BaseSerializer): return super().update(instance, validated_data) -class IssueViewSerializer(BaseSerializer): +class IssueViewSerializer(DynamicBaseSerializer): is_favorite = serializers.BooleanField(read_only=True) project_detail = ProjectLiteSerializer(source="project", read_only=True) workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) @@ -80,4 +80,4 @@ class IssueViewFavoriteSerializer(BaseSerializer): "workspace", "project", "user", - ] + ] \ No newline at end of file diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index f0ad4b4ab..fe014f364 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -2,7 +2,7 @@ from rest_framework import serializers # Module imports -from .base import BaseSerializer +from .base import BaseSerializer, DynamicBaseSerializer from .user import UserLiteSerializer, UserAdminLiteSerializer from plane.db.models import ( @@ -13,10 +13,11 @@ from plane.db.models import ( TeamMember, WorkspaceMemberInvite, WorkspaceTheme, + WorkspaceUserProperties, ) -class WorkSpaceSerializer(BaseSerializer): +class WorkSpaceSerializer(DynamicBaseSerializer): owner = UserLiteSerializer(read_only=True) total_members = serializers.IntegerField(read_only=True) total_issues = serializers.IntegerField(read_only=True) @@ -62,7 +63,7 @@ class WorkspaceLiteSerializer(BaseSerializer): -class WorkSpaceMemberSerializer(BaseSerializer): +class WorkSpaceMemberSerializer(DynamicBaseSerializer): member = UserLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True) @@ -78,7 +79,7 @@ class WorkspaceMemberMeSerializer(BaseSerializer): fields = "__all__" -class WorkspaceMemberAdminSerializer(BaseSerializer): +class WorkspaceMemberAdminSerializer(DynamicBaseSerializer): member = UserAdminLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True) @@ -161,3 +162,13 @@ class WorkspaceThemeSerializer(BaseSerializer): "workspace", "actor", ] + + +class WorkspaceUserPropertiesSerializer(BaseSerializer): + class Meta: + model = WorkspaceUserProperties + fields = "__all__" + read_only_fields = [ + "workspace", + "user", + ] \ No newline at end of file diff --git a/apiserver/plane/app/urls/cycle.py b/apiserver/plane/app/urls/cycle.py index 46e6a5e84..5fef437c6 100644 --- a/apiserver/plane/app/urls/cycle.py +++ b/apiserver/plane/app/urls/cycle.py @@ -7,6 +7,7 @@ from plane.app.views import ( CycleDateCheckEndpoint, CycleFavoriteViewSet, TransferCycleIssueEndpoint, + CycleUserPropertiesEndpoint, ) @@ -44,7 +45,7 @@ urlpatterns = [ name="project-issue-cycle", ), path( - "workspaces//projects//cycles//cycle-issues//", + "workspaces//projects//cycles//cycle-issues//", CycleIssueViewSet.as_view( { "get": "retrieve", @@ -84,4 +85,9 @@ urlpatterns = [ TransferCycleIssueEndpoint.as_view(), name="transfer-issues", ), + path( + "workspaces//projects//cycles//user-properties/", + CycleUserPropertiesEndpoint.as_view(), + name="cycle-user-filters", + ) ] diff --git a/apiserver/plane/app/urls/inbox.py b/apiserver/plane/app/urls/inbox.py index 16ea40b21..e9ec4e335 100644 --- a/apiserver/plane/app/urls/inbox.py +++ b/apiserver/plane/app/urls/inbox.py @@ -40,7 +40,7 @@ urlpatterns = [ name="inbox-issue", ), path( - "workspaces//projects//inboxes//inbox-issues//", + "workspaces//projects//inboxes//inbox-issues//", InboxIssueViewSet.as_view( { "get": "retrieve", diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index 971fbc395..234c2824d 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -235,7 +235,7 @@ urlpatterns = [ ## End Comment Reactions ## IssueProperty path( - "workspaces//projects//issue-display-properties/", + "workspaces//projects//user-properties/", IssueUserDisplayPropertyEndpoint.as_view(), name="project-issue-display-properties", ), @@ -275,16 +275,17 @@ urlpatterns = [ "workspaces//projects//issues//issue-relation/", IssueRelationViewSet.as_view( { + "get": "list", "post": "create", } ), name="issue-relation", ), path( - "workspaces//projects//issues//issue-relation//", + "workspaces//projects//issues//remove-relation/", IssueRelationViewSet.as_view( { - "delete": "destroy", + "post": "remove_relation", } ), name="issue-relation", diff --git a/apiserver/plane/app/urls/module.py b/apiserver/plane/app/urls/module.py index 5507b3a37..961fff0db 100644 --- a/apiserver/plane/app/urls/module.py +++ b/apiserver/plane/app/urls/module.py @@ -7,6 +7,7 @@ from plane.app.views import ( ModuleLinkViewSet, ModuleFavoriteViewSet, BulkImportModulesEndpoint, + ModuleUserPropertiesEndpoint ) @@ -44,7 +45,7 @@ urlpatterns = [ name="project-module-issues", ), path( - "workspaces//projects//modules//module-issues//", + "workspaces//projects//modules//module-issues//", ModuleIssueViewSet.as_view( { "get": "retrieve", @@ -101,4 +102,9 @@ urlpatterns = [ BulkImportModulesEndpoint.as_view(), name="bulk-modules-create", ), + path( + "workspaces//projects//modules//user-properties/", + ModuleUserPropertiesEndpoint.as_view(), + name="cycle-user-filters", + ) ] diff --git a/apiserver/plane/app/urls/views.py b/apiserver/plane/app/urls/views.py index 3d45b627a..f78f17869 100644 --- a/apiserver/plane/app/urls/views.py +++ b/apiserver/plane/app/urls/views.py @@ -5,7 +5,7 @@ from plane.app.views import ( IssueViewViewSet, GlobalViewViewSet, GlobalViewIssuesViewSet, - IssueViewFavoriteViewSet, + IssueViewFavoriteViewSet, ) diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index 2c3638842..cc78881f9 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -18,6 +18,8 @@ from plane.app.views import ( WorkspaceUserProfileEndpoint, WorkspaceUserProfileIssuesEndpoint, WorkspaceLabelsEndpoint, + WorkspaceProjectMemberEndpoint, + WorkspaceUserPropertiesEndpoint, ) @@ -92,6 +94,11 @@ urlpatterns = [ WorkSpaceMemberViewSet.as_view({"get": "list"}), name="workspace-member", ), + path( + "workspaces//project-members/", + WorkspaceProjectMemberEndpoint.as_view(), + name="workspace-member-roles", + ), path( "workspaces//members//", WorkSpaceMemberViewSet.as_view( @@ -195,4 +202,9 @@ urlpatterns = [ WorkspaceLabelsEndpoint.as_view(), name="workspace-labels", ), + path( + "workspaces//user-properties/", + WorkspaceUserPropertiesEndpoint.as_view(), + name="workspace-user-filters", + ) ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index c122dce9f..520a3fd38 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -45,6 +45,8 @@ from .workspace import ( WorkspaceUserProfileEndpoint, WorkspaceUserProfileIssuesEndpoint, WorkspaceLabelsEndpoint, + WorkspaceProjectMemberEndpoint, + WorkspaceUserPropertiesEndpoint, ) from .state import StateViewSet from .view import ( @@ -59,6 +61,7 @@ from .cycle import ( CycleDateCheckEndpoint, CycleFavoriteViewSet, TransferCycleIssueEndpoint, + CycleUserPropertiesEndpoint, ) from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet from .issue import ( @@ -103,6 +106,7 @@ from .module import ( ModuleIssueViewSet, ModuleLinkViewSet, ModuleFavoriteViewSet, + ModuleUserPropertiesEndpoint, ) from .api import ApiTokenEndpoint diff --git a/apiserver/plane/app/views/base.py b/apiserver/plane/app/views/base.py index 32449597b..5bd79cb96 100644 --- a/apiserver/plane/app/views/base.py +++ b/apiserver/plane/app/views/base.py @@ -159,6 +159,21 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): if resolve(self.request.path_info).url_name == "project": return self.kwargs.get("pk", None) + @property + def fields(self): + fields = [ + field for field in self.request.GET.get("fields", "").split(",") if field + ] + return fields if fields else None + + @property + def expand(self): + expand = [ + expand for expand in self.request.GET.get("expand", "").split(",") if expand + ] + return expand if expand else None + + class BaseAPIView(TimezoneMixin, APIView, BasePaginator): permission_classes = [ @@ -239,3 +254,17 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator): @property def project_id(self): return self.kwargs.get("project_id", None) + + @property + def fields(self): + fields = [ + field for field in self.request.GET.get("fields", "").split(",") if field + ] + return fields if fields else None + + @property + def expand(self): + expand = [ + expand for expand in self.request.GET.get("expand", "").split(",") if expand + ] + return expand if expand else None diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle.py index 02f259de3..73741b983 100644 --- a/apiserver/plane/app/views/cycle.py +++ b/apiserver/plane/app/views/cycle.py @@ -14,7 +14,7 @@ from django.db.models import ( Case, When, Value, - CharField + CharField, ) from django.core import serializers from django.utils import timezone @@ -33,8 +33,9 @@ from plane.app.serializers import ( CycleFavoriteSerializer, IssueStateSerializer, CycleWriteSerializer, + CycleUserPropertiesSerializer, ) -from plane.app.permissions import ProjectEntityPermission +from plane.app.permissions import ProjectEntityPermission, ProjectLitePermission from plane.db.models import ( User, Cycle, @@ -44,6 +45,7 @@ from plane.db.models import ( IssueLink, IssueAttachment, Label, + CycleUserProperties, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results @@ -164,23 +166,18 @@ class CycleViewSet(WebhookMixin, BaseViewSet): .annotate( status=Case( When( - Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()), - then=Value("CURRENT") - ), - When( - start_date__gt=timezone.now(), - then=Value("UPCOMING") - ), - When( - end_date__lt=timezone.now(), - then=Value("COMPLETED") + Q(start_date__lte=timezone.now()) + & Q(end_date__gte=timezone.now()), + then=Value("CURRENT"), ), + When(start_date__gt=timezone.now(), then=Value("UPCOMING")), + When(end_date__lt=timezone.now(), then=Value("COMPLETED")), When( Q(start_date__isnull=True) & Q(end_date__isnull=True), - then=Value("DRAFT") + then=Value("DRAFT"), ), - default=Value("DRAFT"), - output_field=CharField(), + default=Value("DRAFT"), + output_field=CharField(), ) ) .prefetch_related( @@ -202,6 +199,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet): def list(self, request, slug, project_id): queryset = self.get_queryset() cycle_view = request.GET.get("cycle_view", "all") + fields = [field for field in request.GET.get("fields", "").split(",") if field] queryset = queryset.order_by("-is_favorite", "-created_at") @@ -307,44 +305,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet): return Response(data, status=status.HTTP_200_OK) - # Upcoming Cycles - if cycle_view == "upcoming": - queryset = queryset.filter(start_date__gt=timezone.now()) - return Response( - CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) - - # Completed Cycles - if cycle_view == "completed": - queryset = queryset.filter(end_date__lt=timezone.now()) - return Response( - CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) - - # Draft Cycles - if cycle_view == "draft": - queryset = queryset.filter( - end_date=None, - start_date=None, - ) - - return Response( - CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) - - # Incomplete Cycles - if cycle_view == "incomplete": - queryset = queryset.filter( - Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True), - ) - return Response( - CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) - - # If no matching view is found return all cycles - return Response( - CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) + cycles = CycleSerializer(queryset, many=True).data + return Response(cycles, status=status.HTTP_200_OK) def create(self, request, slug, project_id): if ( @@ -576,7 +538,6 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .annotate(bridge_id=F("issue_cycle__id")) .filter(project_id=project_id) .filter(workspace__slug=slug) .select_related("project") @@ -600,12 +561,10 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): .values("count") ) ) - - issues = IssueStateSerializer( + serializer = IssueStateSerializer( issues, many=True, fields=fields if fields else None - ).data - issue_dict = {str(issue["id"]): issue for issue in issues} - return Response(issue_dict, status=status.HTTP_200_OK) + ) + return Response(serializer.data, status=status.HTTP_200_OK) def create(self, request, slug, project_id, cycle_id): issues = request.data.get("issues", []) @@ -698,11 +657,13 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): status=status.HTTP_200_OK, ) - def destroy(self, request, slug, project_id, cycle_id, pk): + def destroy(self, request, slug, project_id, cycle_id, issue_id): cycle_issue = CycleIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id + issue_id=issue_id, + workspace__slug=slug, + project_id=project_id, + cycle_id=cycle_id, ) - issue_id = cycle_issue.issue_id issue_activity.delay( type="cycle.activity.deleted", requested_data=json.dumps( @@ -712,7 +673,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): } ), actor_id=str(self.request.user.id), - issue_id=str(cycle_issue.issue_id), + issue_id=str(issue_id), project_id=str(self.kwargs.get("project_id", None)), current_instance=None, epoch=int(timezone.now().timestamp()), @@ -834,3 +795,39 @@ class TransferCycleIssueEndpoint(BaseAPIView): ) return Response({"message": "Success"}, status=status.HTTP_200_OK) + + +class CycleUserPropertiesEndpoint(BaseAPIView): + permission_classes = [ + ProjectLitePermission, + ] + + def patch(self, request, slug, project_id, cycle_id): + cycle_properties = CycleUserProperties.objects.get( + user=request.user, + cycle_id=cycle_id, + project_id=project_id, + workspace__slug=slug, + ) + + cycle_properties.filters = request.data.get("filters", cycle_properties.filters) + cycle_properties.display_filters = request.data.get( + "display_filters", cycle_properties.display_filters + ) + cycle_properties.display_properties = request.data.get( + "display_properties", cycle_properties.display_properties + ) + cycle_properties.save() + + serializer = CycleUserPropertiesSerializer(cycle_properties) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def get(self, request, slug, project_id, cycle_id): + cycle_properties, _ = CycleUserProperties.objects.get_or_create( + user=request.user, + project_id=project_id, + cycle_id=cycle_id, + workspace__slug=slug, + ) + serializer = CycleUserPropertiesSerializer(cycle_properties) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/inbox.py b/apiserver/plane/app/views/inbox.py index 331ee2175..32f38d97c 100644 --- a/apiserver/plane/app/views/inbox.py +++ b/apiserver/plane/app/views/inbox.py @@ -107,7 +107,6 @@ class InboxIssueViewSet(BaseViewSet): project_id=project_id, ) .filter(**filters) - .annotate(bridge_id=F("issue_inbox__id")) .select_related("workspace", "project", "state", "parent") .prefetch_related("assignees", "labels") .order_by("issue_inbox__snoozed_till", "issue_inbox__status") @@ -204,9 +203,9 @@ class InboxIssueViewSet(BaseViewSet): serializer = IssueStateInboxSerializer(issue) return Response(serializer.data, status=status.HTTP_200_OK) - def partial_update(self, request, slug, project_id, inbox_id, pk): + def partial_update(self, request, slug, project_id, inbox_id, issue_id): inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + issue_id=issue_id, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id ) # Get the project member project_member = ProjectMember.objects.get( @@ -316,19 +315,16 @@ class InboxIssueViewSet(BaseViewSet): InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK ) - def retrieve(self, request, slug, project_id, inbox_id, pk): - inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id - ) + def retrieve(self, request, slug, project_id, inbox_id, issue_id): issue = Issue.objects.get( - pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id + pk=issue_id, workspace__slug=slug, project_id=project_id ) serializer = IssueStateInboxSerializer(issue) return Response(serializer.data, status=status.HTTP_200_OK) - def destroy(self, request, slug, project_id, inbox_id, pk): + def destroy(self, request, slug, project_id, inbox_id, issue_id): inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + issue_id=issue_id, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id ) # Get the project member project_member = ProjectMember.objects.get( @@ -350,7 +346,7 @@ class InboxIssueViewSet(BaseViewSet): if inbox_issue.status in [-2, -1, 0, 2]: # Delete the issue also Issue.objects.filter( - workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id + workspace__slug=slug, project_id=project_id, pk=issue_id ).delete() inbox_issue.delete() diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index d489629ba..6c88ef090 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -52,6 +52,7 @@ from plane.app.serializers import ( IssueRelationSerializer, RelatedIssueSerializer, IssuePublicSerializer, + IssueRelationLiteSerializer, ) from plane.app.permissions import ( ProjectEntityPermission, @@ -129,22 +130,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet): queryset=IssueReaction.objects.select_related("actor"), ) ) - ).distinct() - - @method_decorator(gzip_page) - def list(self, request, slug, project_id): - fields = [field for field in request.GET.get("fields", "").split(",") if field] - filters = issue_filters(request.query_params, "GET") - - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = ( - self.get_queryset() - .filter(**filters) .annotate(cycle_id=F("issue_cycle__cycle_id")) .annotate(module_id=F("issue_module__module_id")) .annotate( @@ -159,7 +144,26 @@ class IssueViewSet(WebhookMixin, BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + subscriber=self.request.user, issue_id=OuterRef("id") + ) + ) + ) + ).distinct() + + @method_decorator(gzip_page) + def list(self, request, slug, project_id): + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = self.get_queryset().filter(**filters) # Priority Ordering if order_by_param == "priority" or order_by_param == "-priority": @@ -217,9 +221,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet): else: issue_queryset = issue_queryset.order_by(order_by_param) - issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data - issue_dict = {str(issue["id"]): issue for issue in issues} - return Response(issue_dict, status=status.HTTP_200_OK) + issues = IssueSerializer( + issue_queryset, many=True, fields=self.fields, expand=self.expand + ).data + return Response(issues, status=status.HTTP_200_OK) def create(self, request, slug, project_id): project = Project.objects.get(pk=project_id) @@ -256,7 +261,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ).get(workspace__slug=slug, project_id=project_id, pk=pk) - return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) + return Response( + IssueSerializer(issue, fields=self.fields, expand=self.expand).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) @@ -590,16 +598,19 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView): ProjectLitePermission, ] - def post(self, request, slug, project_id): - issue_property, created = IssueProperty.objects.get_or_create( + def patch(self, request, slug, project_id): + issue_property = IssueProperty.objects.get( user=request.user, project_id=project_id, ) - if not created: - issue_property.properties = request.data.get("properties", {}) - issue_property.save() - issue_property.properties = request.data.get("properties", {}) + issue_property.filters = request.data.get("filters", issue_property.filters) + issue_property.display_filters = request.data.get( + "display_filters", issue_property.display_filters + ) + issue_property.display_properties = request.data.get( + "display_properties", issue_property.display_properties + ) issue_property.save() serializer = IssuePropertySerializer(issue_property) return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -708,6 +719,13 @@ class SubIssuesEndpoint(BaseAPIView): .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + subscriber=self.request.user, issue_id=OuterRef("id") + ) + ) + ) .prefetch_related( Prefetch( "issue_reactions", @@ -728,7 +746,7 @@ class SubIssuesEndpoint(BaseAPIView): item["state_group"]: item["state_count"] for item in state_distribution } - serializer = IssueLiteSerializer( + serializer = IssueSerializer( sub_issues, many=True, ) @@ -775,7 +793,7 @@ class SubIssuesEndpoint(BaseAPIView): ] return Response( - IssueFlatSerializer(updated_sub_issues, many=True).data, + IssueSerializer(updated_sub_issues, many=True).data, status=status.HTTP_200_OK, ) @@ -1062,9 +1080,10 @@ class IssueArchiveViewSet(BaseViewSet): else issue_queryset.filter(parent__isnull=True) ) - issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data - issue_dict = {str(issue["id"]): issue for issue in issues} - return Response(issue_dict, status=status.HTTP_200_OK) + issues = IssueLiteSerializer( + issue_queryset, many=True, fields=fields if fields else None + ).data + return Response(issues, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, pk=None): issue = Issue.objects.get( @@ -1365,23 +1384,62 @@ class IssueRelationViewSet(BaseViewSet): .distinct() ) + def list(self, request, slug, project_id, issue_id): + issue_relations = ( + IssueRelation.objects.filter(Q(issue_id=issue_id) | Q(related_issue=issue_id)) + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("project") + .select_related("workspace") + .select_related("issue") + .order_by("-created_at") + .distinct() + ) + + blocking_issues = issue_relations.filter(relation_type="blocked_by", related_issue_id=issue_id) + blocked_by_issues = issue_relations.filter(relation_type="blocked_by", issue_id=issue_id) + duplicate_issues = issue_relations.filter(issue_id=issue_id, relation_type="duplicate") + duplicate_issues_related = issue_relations.filter(related_issue_id=issue_id, relation_type="duplicate") + relates_to_issues = issue_relations.filter(issue_id=issue_id, relation_type="relates_to") + relates_to_issues_related = issue_relations.filter(related_issue_id=issue_id, relation_type="relates_to") + + blocked_by_issues_serialized = IssueRelationSerializer(blocked_by_issues, many=True).data + duplicate_issues_serialized = IssueRelationSerializer(duplicate_issues, many=True).data + relates_to_issues_serialized = IssueRelationSerializer(relates_to_issues, many=True).data + + # revere relation for blocked by issues + blocking_issues_serialized = RelatedIssueSerializer(blocking_issues, many=True).data + # reverse relation for duplicate issues + duplicate_issues_related_serialized = RelatedIssueSerializer(duplicate_issues_related, many=True).data + # reverse relation for related issues + relates_to_issues_related_serialized = RelatedIssueSerializer(relates_to_issues_related, many=True).data + + response_data = { + 'blocking': blocking_issues_serialized, + 'blocked_by': blocked_by_issues_serialized, + 'duplicate': duplicate_issues_serialized + duplicate_issues_related_serialized, + 'relates_to': relates_to_issues_serialized + relates_to_issues_related_serialized, + } + + return Response(response_data, status=status.HTTP_200_OK) + + def create(self, request, slug, project_id, issue_id): - related_list = request.data.get("related_list", []) - relation = request.data.get("relation", None) + relation_type = request.data.get("relation_type", None) + issues = request.data.get("issues", []) project = Project.objects.get(pk=project_id) issue_relation = IssueRelation.objects.bulk_create( [ IssueRelation( - issue_id=related_issue["issue"], - related_issue_id=related_issue["related_issue"], - relation_type=related_issue["relation_type"], + issue_id=issue if relation_type == "blocking" else issue_id, + related_issue_id=issue_id if relation_type == "blocking" else issue, + relation_type="blocked_by" if relation_type == "blocking" else relation_type, project_id=project_id, workspace_id=project.workspace_id, created_by=request.user, updated_by=request.user, ) - for related_issue in related_list + for issue in issues ], batch_size=10, ignore_conflicts=True, @@ -1397,7 +1455,7 @@ class IssueRelationViewSet(BaseViewSet): epoch=int(timezone.now().timestamp()), ) - if relation == "blocking": + if relation_type == "blocking": return Response( RelatedIssueSerializer(issue_relation, many=True).data, status=status.HTTP_201_CREATED, @@ -1408,10 +1466,18 @@ class IssueRelationViewSet(BaseViewSet): status=status.HTTP_201_CREATED, ) - def destroy(self, request, slug, project_id, issue_id, pk): - issue_relation = IssueRelation.objects.get( - workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk - ) + def remove_relation(self, request, slug, project_id, issue_id): + relation_type = request.data.get("relation_type", None) + related_issue = request.data.get("related_issue", None) + + if relation_type == "blocking": + issue_relation = IssueRelation.objects.get( + workspace__slug=slug, project_id=project_id, issue_id=related_issue, related_issue_id=issue_id + ) + else: + issue_relation = IssueRelation.objects.get( + workspace__slug=slug, project_id=project_id, issue_id=issue_id, related_issue_id=related_issue + ) current_instance = json.dumps( IssueRelationSerializer(issue_relation).data, cls=DjangoJSONEncoder, @@ -1419,7 +1485,7 @@ class IssueRelationViewSet(BaseViewSet): issue_relation.delete() issue_activity.delay( type="issue_relation.activity.deleted", - requested_data=json.dumps({"related_list": None}), + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), actor_id=str(request.user.id), issue_id=str(issue_id), project_id=str(project_id), @@ -1547,9 +1613,10 @@ class IssueDraftViewSet(BaseViewSet): else: issue_queryset = issue_queryset.order_by(order_by_param) - issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data - issue_dict = {str(issue["id"]): issue for issue in issues} - return Response(issue_dict, status=status.HTTP_200_OK) + issues = IssueLiteSerializer( + issue_queryset, many=True, fields=fields if fields else None + ).data + return Response(issues, status=status.HTTP_200_OK) def create(self, request, slug, project_id): project = Project.objects.get(pk=project_id) @@ -1626,4 +1693,4 @@ class IssueDraftViewSet(BaseViewSet): current_instance=current_instance, epoch=int(timezone.now().timestamp()), ) - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/module.py b/apiserver/plane/app/views/module.py index a8a8655c3..6baf23121 100644 --- a/apiserver/plane/app/views/module.py +++ b/apiserver/plane/app/views/module.py @@ -21,8 +21,9 @@ from plane.app.serializers import ( ModuleLinkSerializer, ModuleFavoriteSerializer, IssueStateSerializer, + ModuleUserPropertiesSerializer, ) -from plane.app.permissions import ProjectEntityPermission +from plane.app.permissions import ProjectEntityPermission, ProjectLitePermission from plane.db.models import ( Module, ModuleIssue, @@ -32,6 +33,7 @@ from plane.db.models import ( ModuleFavorite, IssueLink, IssueAttachment, + ModuleUserProperties, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results @@ -54,7 +56,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): ) def get_queryset(self): - subquery = ModuleFavorite.objects.filter( user=self.request.user, module_id=OuterRef("pk"), @@ -136,7 +137,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): ), ) ) - .order_by("-is_favorite","-created_at") + .order_by("-is_favorite", "-created_at") ) def create(self, request, slug, project_id): @@ -153,6 +154,14 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def list(self, request, slug, project_id): + queryset = self.get_queryset() + fields = [field for field in request.GET.get("fields", "").split(",") if field] + modules = ModuleSerializer( + queryset, many=True, fields=fields if fields else None + ).data + return Response(modules, status=status.HTTP_200_OK) + def retrieve(self, request, slug, project_id, pk): queryset = self.get_queryset().get(pk=pk) @@ -289,7 +298,6 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): webhook_event = "module_issue" bulk = True - filterset_fields = [ "issue__labels__id", "issue__assignees__id", @@ -335,7 +343,6 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .annotate(bridge_id=F("issue_module__id")) .filter(project_id=project_id) .filter(workspace__slug=slug) .select_related("project") @@ -359,9 +366,10 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): .values("count") ) ) - issues = IssueStateSerializer(issues, many=True, fields=fields if fields else None).data - issue_dict = {str(issue["id"]): issue for issue in issues} - return Response(issue_dict, status=status.HTTP_200_OK) + serializer = IssueStateSerializer( + issues, many=True, fields=fields if fields else None + ) + return Response(serializer.data, status=status.HTTP_200_OK) def create(self, request, slug, project_id, module_id): issues = request.data.get("issues", []) @@ -444,20 +452,23 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): status=status.HTTP_200_OK, ) - def destroy(self, request, slug, project_id, module_id, pk): + def destroy(self, request, slug, project_id, module_id, issue_id): module_issue = ModuleIssue.objects.get( - workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk + workspace__slug=slug, + project_id=project_id, + module_id=module_id, + issue_id=issue_id, ) issue_activity.delay( type="module.activity.deleted", requested_data=json.dumps( { "module_id": str(module_id), - "issues": [str(module_issue.issue_id)], + "issues": [str(issue_id)], } ), actor_id=str(request.user.id), - issue_id=str(module_issue.issue_id), + issue_id=str(issue_id), project_id=str(project_id), current_instance=None, epoch=int(timezone.now().timestamp()), @@ -521,4 +532,42 @@ class ModuleFavoriteViewSet(BaseViewSet): module_id=module_id, ) module_favorite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ModuleUserPropertiesEndpoint(BaseAPIView): + permission_classes = [ + ProjectLitePermission, + ] + + def patch(self, request, slug, project_id, module_id): + module_properties = ModuleUserProperties.objects.get( + user=request.user, + module_id=module_id, + project_id=project_id, + workspace__slug=slug, + ) + + module_properties.filters = request.data.get( + "filters", module_properties.filters + ) + module_properties.display_filters = request.data.get( + "display_filters", module_properties.display_filters + ) + module_properties.display_properties = request.data.get( + "display_properties", module_properties.display_properties + ) + module_properties.save() + + serializer = ModuleUserPropertiesSerializer(module_properties) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def get(self, request, slug, project_id, module_id): + module_properties, _ = ModuleUserProperties.objects.get_or_create( + user=request.user, + project_id=project_id, + module_id=module_id, + workspace__slug=slug, + ) + serializer = ModuleUserPropertiesSerializer(module_properties) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/page.py b/apiserver/plane/app/views/page.py index 9bd1f1dd4..482bdfbfe 100644 --- a/apiserver/plane/app/views/page.py +++ b/apiserver/plane/app/views/page.py @@ -157,9 +157,8 @@ class PageViewSet(BaseViewSet): def list(self, request, slug, project_id): queryset = self.get_queryset().filter(archived_at__isnull=True) - return Response( - PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) + pages = PageSerializer(queryset, many=True).data + return Response(pages, status=status.HTTP_200_OK) def archive(self, request, slug, project_id, page_id): page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id) @@ -210,9 +209,9 @@ class PageViewSet(BaseViewSet): workspace__slug=slug, ).filter(archived_at__isnull=False) - return Response( - PageSerializer(pages, many=True).data, status=status.HTTP_200_OK - ) + pages = PageSerializer(pages, many=True).data + return Response(pages, status=status.HTTP_200_OK) + def destroy(self, request, slug, project_id, pk): page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id) diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py index 5b88e3652..c5caac666 100644 --- a/apiserver/plane/app/views/project.py +++ b/apiserver/plane/app/views/project.py @@ -36,6 +36,7 @@ from plane.app.serializers import ( ProjectFavoriteSerializer, ProjectDeployBoardSerializer, ProjectMemberAdminSerializer, + ProjectMemberRoleSerializer, ) from plane.app.permissions import ( @@ -180,12 +181,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): projects, many=True ).data, ) + projects = ProjectListSerializer(projects, many=True, fields=fields if fields else None).data + return Response(projects, status=status.HTTP_200_OK) - return Response( - ProjectListSerializer( - projects, many=True, fields=fields if fields else None - ).data - ) def create(self, request, slug): try: @@ -713,13 +711,7 @@ class ProjectMemberViewSet(BaseViewSet): return Response(serializer.data, status=status.HTTP_201_CREATED) def list(self, request, slug, project_id): - project_member = ProjectMember.objects.get( - member=request.user, - workspace__slug=slug, - project_id=project_id, - is_active=True, - ) - + # Get the list of project members for the project project_members = ProjectMember.objects.filter( project_id=project_id, workspace__slug=slug, @@ -727,10 +719,7 @@ class ProjectMemberViewSet(BaseViewSet): is_active=True, ).select_related("project", "member", "workspace") - if project_member.role > 10: - serializer = ProjectMemberAdminSerializer(project_members, many=True) - else: - serializer = ProjectMemberSerializer(project_members, many=True) + serializer = ProjectMemberRoleSerializer(project_members, fields=("id", "member", "role"), many=True) return Response(serializer.data, status=status.HTTP_200_OK) def partial_update(self, request, slug, project_id, pk): @@ -1010,18 +999,11 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView): def get(self, request): files = [] - s3_client_params = { - "service_name": "s3", - "aws_access_key_id": settings.AWS_ACCESS_KEY_ID, - "aws_secret_access_key": settings.AWS_SECRET_ACCESS_KEY, - } - - # Use AWS_S3_ENDPOINT_URL if it is present in the settings - if hasattr(settings, "AWS_S3_ENDPOINT_URL") and settings.AWS_S3_ENDPOINT_URL: - s3_client_params["endpoint_url"] = settings.AWS_S3_ENDPOINT_URL - - s3 = boto3.client(**s3_client_params) - + s3 = boto3.client( + "s3", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + ) params = { "Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Prefix": "static/project-cover/", @@ -1034,19 +1016,9 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView): if not content["Key"].endswith( "/" ): # This line ensures we're only getting files, not "sub-folders" - if ( - hasattr(settings, "AWS_S3_CUSTOM_DOMAIN") - and settings.AWS_S3_CUSTOM_DOMAIN - and hasattr(settings, "AWS_S3_URL_PROTOCOL") - and settings.AWS_S3_URL_PROTOCOL - ): - files.append( - f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/{content['Key']}" - ) - else: - files.append( - f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" - ) + files.append( + f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" + ) return Response(files, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/view.py b/apiserver/plane/app/views/view.py index eb76407b7..a2f00a819 100644 --- a/apiserver/plane/app/views/view.py +++ b/apiserver/plane/app/views/view.py @@ -27,7 +27,12 @@ from plane.app.serializers import ( IssueLiteSerializer, IssueViewFavoriteSerializer, ) -from plane.app.permissions import WorkspaceEntityPermission, ProjectEntityPermission +from plane.app.permissions import ( + WorkspaceEntityPermission, + ProjectEntityPermission, + WorkspaceViewerPermission, + ProjectLitePermission, +) from plane.db.models import ( Workspace, GlobalView, @@ -43,8 +48,8 @@ from plane.utils.grouper import group_results class GlobalViewViewSet(BaseViewSet): - serializer_class = GlobalViewSerializer - model = GlobalView + serializer_class = IssueViewSerializer + model = IssueView permission_classes = [ WorkspaceEntityPermission, ] @@ -58,6 +63,7 @@ class GlobalViewViewSet(BaseViewSet): super() .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project__isnull=True) .select_related("workspace") .order_by(self.request.GET.get("order_by", "-created_at")) .distinct() @@ -179,12 +185,10 @@ class GlobalViewIssuesViewSet(BaseViewSet): else: issue_queryset = issue_queryset.order_by(order_by_param) - issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data - issue_dict = {str(issue["id"]): issue for issue in issues} - return Response( - issue_dict, - status=status.HTTP_200_OK, + serializer = IssueLiteSerializer( + issue_queryset, many=True, fields=fields if fields else None ) + return Response(serializer.data, status=status.HTTP_200_OK) class IssueViewViewSet(BaseViewSet): @@ -217,6 +221,14 @@ class IssueViewViewSet(BaseViewSet): .distinct() ) + def list(self, request, slug, project_id): + queryset = self.get_queryset() + fields = [field for field in request.GET.get("fields", "").split(",") if field] + views = IssueViewSerializer( + queryset, many=True, fields=fields if fields else None + ).data + return Response(views, status=status.HTTP_200_OK) + class IssueViewFavoriteViewSet(BaseViewSet): serializer_class = IssueViewFavoriteSerializer @@ -246,4 +258,4 @@ class IssueViewFavoriteViewSet(BaseViewSet): view_id=view_id, ) view_favourite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index 11170114a..f51e1ac1e 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -44,6 +44,8 @@ from plane.app.serializers import ( IssueLiteSerializer, WorkspaceMemberAdminSerializer, WorkspaceMemberMeSerializer, + ProjectMemberRoleSerializer, + WorkspaceUserPropertiesSerializer, ) from plane.app.views.base import BaseAPIView from . import BaseViewSet @@ -64,6 +66,7 @@ from plane.db.models import ( WorkspaceMember, CycleIssue, IssueReaction, + WorkspaceUserProperties ) from plane.app.permissions import ( WorkSpaceBasePermission, @@ -71,11 +74,13 @@ from plane.app.permissions import ( WorkspaceEntityPermission, WorkspaceViewerPermission, WorkspaceUserPermission, + ProjectLitePermission, ) from plane.bgtasks.workspace_invitation_task import workspace_invitation from plane.utils.issue_filters import issue_filters from plane.bgtasks.event_tracking_task import workspace_invite_event + class WorkSpaceViewSet(BaseViewSet): model = Workspace serializer_class = WorkSpaceSerializer @@ -173,6 +178,7 @@ class UserWorkSpacesEndpoint(BaseAPIView): ] def get(self, request): + fields = [field for field in request.GET.get("fields", "").split(",") if field] member_count = ( WorkspaceMember.objects.filter( workspace=OuterRef("id"), @@ -208,9 +214,12 @@ class UserWorkSpacesEndpoint(BaseAPIView): ) .distinct() ) - - serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + workspaces = WorkSpaceSerializer( + self.filter_queryset(workspace), + fields=fields if fields else None, + many=True, + ).data + return Response(workspaces, status=status.HTTP_200_OK) class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView): @@ -407,7 +416,7 @@ class WorkspaceJoinEndpoint(BaseAPIView): # Delete the invitation workspace_invite.delete() - + # Send event workspace_invite_event.delay( user=user.id if user is not None else None, @@ -537,10 +546,15 @@ class WorkSpaceMemberViewSet(BaseViewSet): workspace_members = self.get_queryset() if workspace_member.role > 10: - serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True) + serializer = WorkspaceMemberAdminSerializer( + workspace_members, + fields=("id", "member", "role"), + many=True, + ) else: serializer = WorkSpaceMemberSerializer( workspace_members, + fields=("id", "member", "role"), many=True, ) return Response(serializer.data, status=status.HTTP_200_OK) @@ -705,6 +719,43 @@ class WorkSpaceMemberViewSet(BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) +class WorkspaceProjectMemberEndpoint(BaseAPIView): + serializer_class = ProjectMemberRoleSerializer + model = ProjectMember + + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get(self, request, slug): + # Fetch all project IDs where the user is involved + project_ids = ProjectMember.objects.filter( + member=request.user, + member__is_bot=False, + is_active=True, + ).values_list('project_id', flat=True).distinct() + + # 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") + project_members = ProjectMemberRoleSerializer(project_members, many=True).data + + project_members_dict = dict() + + # Construct a dictionary with project_id as key and project_members as value + for project_member in project_members: + project_id = project_member.pop("project") + if str(project_id) not in project_members_dict: + project_members_dict[str(project_id)] = [] + project_members_dict[str(project_id)].append(project_member) + + return Response(project_members_dict, status=status.HTTP_200_OK) + + class TeamMemberViewSet(BaseViewSet): serializer_class = TeamSerializer model = Team @@ -1334,8 +1385,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): issues = IssueLiteSerializer( issue_queryset, many=True, fields=fields if fields else None ).data - issue_dict = {str(issue["id"]): issue for issue in issues} - return Response(issue_dict, status=status.HTTP_200_OK) + return Response(issues, status=status.HTTP_200_OK) class WorkspaceLabelsEndpoint(BaseAPIView): @@ -1349,3 +1399,30 @@ class WorkspaceLabelsEndpoint(BaseAPIView): project__project_projectmember__member=request.user, ).values("parent", "name", "color", "id", "project_id", "workspace__slug") return Response(labels, status=status.HTTP_200_OK) + + +class WorkspaceUserPropertiesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + def patch(self, request, slug): + workspace_properties = WorkspaceUserProperties.objects.get( + user=request.user, + workspace__slug=slug, + ) + + workspace_properties.filters = request.data.get("filters", workspace_properties.filters) + workspace_properties.display_filters = request.data.get("display_filters", workspace_properties.display_filters) + workspace_properties.display_properties = request.data.get("display_properties", workspace_properties.display_properties) + workspace_properties.save() + + serializer = WorkspaceUserPropertiesSerializer(workspace_properties) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def get(self, request, slug): + workspace_properties, _ = WorkspaceUserProperties.objects.get_or_create( + user=request.user, workspace__slug=slug + ) + serializer = WorkspaceUserPropertiesSerializer(workspace_properties) + return Response(serializer.data, status=status.HTTP_200_OK) \ No newline at end of file diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 5d4c0650c..2552ffbc5 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -112,8 +112,16 @@ def track_parent( epoch, ): if current_instance.get("parent") != requested_data.get("parent"): - old_parent = Issue.objects.filter(pk=current_instance.get("parent")).first() if current_instance.get("parent") is not None else None - new_parent = Issue.objects.filter(pk=requested_data.get("parent")).first() if requested_data.get("parent") is not None else None + old_parent = ( + Issue.objects.filter(pk=current_instance.get("parent")).first() + if current_instance.get("parent") is not None + else None + ) + new_parent = ( + Issue.objects.filter(pk=requested_data.get("parent")).first() + if requested_data.get("parent") is not None + else None + ) issue_activities.append( IssueActivity( @@ -714,7 +722,9 @@ def create_cycle_issue_activity( cycle = Cycle.objects.filter( pk=created_record.get("fields").get("cycle") ).first() - issue = Issue.objects.filter(pk=created_record.get("fields").get("issue")).first() + issue = Issue.objects.filter( + pk=created_record.get("fields").get("issue") + ).first() if issue: issue.updated_at = timezone.now() issue.save(update_fields=["updated_at"]) @@ -830,7 +840,9 @@ def create_module_issue_activity( module = Module.objects.filter( pk=created_record.get("fields").get("module") ).first() - issue = Issue.objects.filter(pk=created_record.get("fields").get("issue")).first() + issue = Issue.objects.filter( + pk=created_record.get("fields").get("issue") + ).first() if issue: issue.updated_at = timezone.now() issue.save(update_fields=["updated_at"]) @@ -1276,40 +1288,42 @@ def create_issue_relation_activity( current_instance = ( json.loads(current_instance) if current_instance is not None else None ) - if current_instance is None and requested_data.get("related_list") is not None: - for issue_relation in requested_data.get("related_list"): - if issue_relation.get("relation_type") == "blocked_by": - relation_type = "blocking" - else: - relation_type = issue_relation.get("relation_type") - issue = Issue.objects.get(pk=issue_relation.get("issue")) + if current_instance is None and requested_data.get("issues") is not None: + for related_issue in requested_data.get("issues"): + issue = Issue.objects.get(pk=related_issue) issue_activities.append( IssueActivity( - issue_id=issue_relation.get("related_issue"), + issue_id=issue_id, actor_id=actor_id, verb="created", old_value="", new_value=f"{issue.project.identifier}-{issue.sequence_id}", - field=relation_type, + field=requested_data.get("relation_type"), project_id=project_id, workspace_id=workspace_id, - comment=f"added {relation_type} relation", - old_identifier=issue_relation.get("issue"), + comment=f"added {requested_data.get('relation_type')} relation", + old_identifier=related_issue, ) ) - issue = Issue.objects.get(pk=issue_relation.get("related_issue")) + issue = Issue.objects.get(pk=issue_id) issue_activities.append( IssueActivity( - issue_id=issue_relation.get("issue"), + issue_id=related_issue, actor_id=actor_id, verb="created", old_value="", new_value=f"{issue.project.identifier}-{issue.sequence_id}", - field=f'{issue_relation.get("relation_type")}', + field="blocking" + if requested_data.get("relation_type") == "blocked_by" + else ( + "blocked_by" + if requested_data.get("relation_type") == "blocking" + else requested_data.get("relation_type") + ), project_id=project_id, workspace_id=workspace_id, - comment=f'added {issue_relation.get("relation_type")} relation', - old_identifier=issue_relation.get("related_issue"), + comment=f'added {"blocking" if requested_data.get("relation_type") == "blocked_by" else ("blocked_by" if requested_data.get("relation_type") == "blocking" else requested_data.get("relation_type")),} relation', + old_identifier=issue_id, epoch=epoch, ) ) @@ -1329,44 +1343,44 @@ def delete_issue_relation_activity( current_instance = ( json.loads(current_instance) if current_instance is not None else None ) - if current_instance is not None and requested_data.get("related_list") is None: - if current_instance.get("relation_type") == "blocked_by": - relation_type = "blocking" - else: - relation_type = current_instance.get("relation_type") - issue = Issue.objects.get(pk=current_instance.get("issue")) - issue_activities.append( - IssueActivity( - issue_id=current_instance.get("related_issue"), - actor_id=actor_id, - verb="deleted", - old_value=f"{issue.project.identifier}-{issue.sequence_id}", - new_value="", - field=relation_type, - project_id=project_id, - workspace_id=workspace_id, - comment=f"deleted {relation_type} relation", - old_identifier=current_instance.get("issue"), - epoch=epoch, - ) + issue = Issue.objects.get(pk=requested_data.get("related_issue")) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="deleted", + old_value=f"{issue.project.identifier}-{issue.sequence_id}", + new_value="", + field=requested_data.get("relation_type"), + project_id=project_id, + workspace_id=workspace_id, + comment=f"deleted {requested_data.get('relation_type')} relation", + old_identifier=requested_data.get("related_issue"), + epoch=epoch, ) - issue = Issue.objects.get(pk=current_instance.get("related_issue")) - issue_activities.append( - IssueActivity( - issue_id=current_instance.get("issue"), - actor_id=actor_id, - verb="deleted", - old_value=f"{issue.project.identifier}-{issue.sequence_id}", - new_value="", - field=f'{current_instance.get("relation_type")}', - project_id=project_id, - workspace_id=workspace_id, - comment=f'deleted {current_instance.get("relation_type")} relation', - old_identifier=current_instance.get("related_issue"), - epoch=epoch, - ) + ) + issue = Issue.objects.get(pk=issue_id) + issue_activities.append( + IssueActivity( + issue_id=requested_data.get("related_issue"), + actor_id=actor_id, + verb="deleted", + old_value=f"{issue.project.identifier}-{issue.sequence_id}", + new_value="", + field="blocking" + if requested_data.get("relation_type") == "blocked_by" + else ( + "blocked_by" + if requested_data.get("relation_type") == "blocking" + else requested_data.get("relation_type") + ), + project_id=project_id, + workspace_id=workspace_id, + comment=f'deleted {requested_data.get("relation_type")} relation', + old_identifier=requested_data.get("related_issue"), + epoch=epoch, ) - + ) def create_draft_issue_activity( requested_data, diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py index 4bc27d3ee..d33b883bb 100644 --- a/apiserver/plane/bgtasks/notification_task.py +++ b/apiserver/plane/bgtasks/notification_task.py @@ -291,6 +291,9 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi sender = "in_app:issue_activities:assigned" for issue_activity in issue_activities_created: + # Do not send notification for description update + if issue_activity.get("field") == "description": + continue; issue_comment = issue_activity.get("issue_comment") if issue_comment is not None: issue_comment = IssueComment.objects.get( @@ -341,7 +344,7 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi .order_by("-created_at") .first() ) - + actor = User.objects.get(pk=actor_id) for mention_id in comment_mentions: diff --git a/apiserver/plane/db/migrations/0051_remove_issueproperty_properties_and_more.py b/apiserver/plane/db/migrations/0051_remove_issueproperty_properties_and_more.py new file mode 100644 index 000000000..b61122ef8 --- /dev/null +++ b/apiserver/plane/db/migrations/0051_remove_issueproperty_properties_and_more.py @@ -0,0 +1,136 @@ +# Generated by Django 4.2.7 on 2023-12-20 11:14 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import plane.db.models.cycle +import plane.db.models.issue +import plane.db.models.module +import plane.db.models.view +import plane.db.models.workspace +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0050_user_use_case_alter_workspace_organization_size'), + ] + + operations = [ + migrations.RenameField( + model_name='issueview', + old_name='query_data', + new_name='filters', + ), + migrations.RenameField( + model_name='issueproperty', + old_name='properties', + new_name='display_properties', + ), + migrations.AlterField( + model_name='issueproperty', + name='display_properties', + field=models.JSONField(default=plane.db.models.issue.get_default_display_properties), + ), + migrations.AddField( + model_name='issueproperty', + name='display_filters', + field=models.JSONField(default=plane.db.models.issue.get_default_display_filters), + ), + migrations.AddField( + model_name='issueproperty', + name='filters', + field=models.JSONField(default=plane.db.models.issue.get_default_filters), + ), + migrations.AddField( + model_name='issueview', + name='display_filters', + field=models.JSONField(default=plane.db.models.view.get_default_display_filters), + ), + migrations.AddField( + model_name='issueview', + name='display_properties', + field=models.JSONField(default=plane.db.models.view.get_default_display_properties), + ), + migrations.AddField( + model_name='issueview', + name='sort_order', + field=models.FloatField(default=65535), + ), + migrations.AlterField( + model_name='issueview', + name='project', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.CreateModel( + name='WorkspaceUserProperties', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('filters', models.JSONField(default=plane.db.models.workspace.get_default_filters)), + ('display_filters', models.JSONField(default=plane.db.models.workspace.get_default_display_filters)), + ('display_properties', models.JSONField(default=plane.db.models.workspace.get_default_display_properties)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_properties', to=settings.AUTH_USER_MODEL)), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_properties', to='db.workspace')), + ], + options={ + 'verbose_name': 'Workspace User Property', + 'verbose_name_plural': 'Workspace User Property', + 'db_table': 'Workspace_user_properties', + 'ordering': ('-created_at',), + 'unique_together': {('workspace', 'user')}, + }, + ), + migrations.CreateModel( + name='ModuleUserProperties', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('filters', models.JSONField(default=plane.db.models.module.get_default_filters)), + ('display_filters', models.JSONField(default=plane.db.models.module.get_default_display_filters)), + ('display_properties', models.JSONField(default=plane.db.models.module.get_default_display_properties)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_user_properties', to='db.module')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_user_properties', to=settings.AUTH_USER_MODEL)), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')), + ], + options={ + 'verbose_name': 'Module User Property', + 'verbose_name_plural': 'Module User Property', + 'db_table': 'module_user_properties', + 'ordering': ('-created_at',), + 'unique_together': {('module', 'user')}, + }, + ), + migrations.CreateModel( + name='CycleUserProperties', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('filters', models.JSONField(default=plane.db.models.cycle.get_default_filters)), + ('display_filters', models.JSONField(default=plane.db.models.cycle.get_default_display_filters)), + ('display_properties', models.JSONField(default=plane.db.models.cycle.get_default_display_properties)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('cycle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_user_properties', to='db.cycle')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_user_properties', to=settings.AUTH_USER_MODEL)), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')), + ], + options={ + 'verbose_name': 'Cycle User Property', + 'verbose_name_plural': 'Cycle User Properties', + 'db_table': 'cycle_user_properties', + 'ordering': ('-created_at',), + 'unique_together': {('cycle', 'user')}, + }, + ), + ] diff --git a/apiserver/plane/db/migrations/0052_auto_20231220_1141.py b/apiserver/plane/db/migrations/0052_auto_20231220_1141.py new file mode 100644 index 000000000..b8386bf46 --- /dev/null +++ b/apiserver/plane/db/migrations/0052_auto_20231220_1141.py @@ -0,0 +1,65 @@ +# Generated by Django 4.2.7 on 2023-12-19 19:11 +from plane.db.models import WorkspaceUserProperties, ProjectMember, IssueView +from django.db import migrations + + +def workspace_user_properties(apps, schema_editor): + WorkspaceMember = apps.get_model("db", "WorkspaceMember") + updated_workspace_user_properties = [] + for workspace_members in WorkspaceMember.objects.all(): + updated_workspace_user_properties.append( + WorkspaceUserProperties( + user_id=workspace_members.member_id, + display_filters=workspace_members.view_props.get("display_filters"), + display_properties=workspace_members.view_props.get("display_properties"), + workspace_id=workspace_members.workspace_id, + ) + ) + WorkspaceUserProperties.objects.bulk_create(updated_workspace_user_properties, batch_size=2000) + + +def project_user_properties(apps, schema_editor): + IssueProperty = apps.get_model("db", "IssueProperty") + updated_issue_user_properties = [] + for issue_property in IssueProperty.objects.all(): + project_member = ProjectMember.objects.filter(project_id=issue_property.project_id, member_id=issue_property.user_id).first() + if project_member: + issue_property.filters = project_member.view_props.get("filters") + issue_property.display_filters = project_member.view_props.get("display_filters") + updated_issue_user_properties.append(issue_property) + + IssueProperty.objects.bulk_update(updated_issue_user_properties, ["filters", "display_filters"], batch_size=2000) + + +def issue_view(apps, schema_editor): + GlobalView = apps.get_model("db", "GlobalView") + updated_issue_views = [] + + for global_view in GlobalView.objects.all(): + updated_issue_views.append( + IssueView( + workspace_id=global_view.workspace_id, + name=global_view.name, + description=global_view.description, + query=global_view.query, + access=global_view.access, + filters=global_view.query_data.get("filters", {}), + sort_order=global_view.sort_order, + created_by_id=global_view.created_by_id, + updated_by_id=global_view.updated_by_id, + ) + ) + IssueView.objects.bulk_create(updated_issue_views, batch_size=100) + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0051_remove_issueproperty_properties_and_more'), + ] + + operations = [ + migrations.RunPython(workspace_user_properties), + migrations.RunPython(project_user_properties), + migrations.RunPython(issue_view), + ] \ No newline at end of file diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index c76df6e5b..b88ee8e46 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -9,6 +9,8 @@ from .workspace import ( WorkspaceMemberInvite, TeamMember, WorkspaceTheme, + WorkspaceUserProperties, + WorkspaceBaseModel, ) from .project import ( @@ -48,11 +50,11 @@ from .social_connection import SocialLoginConnection from .state import State -from .cycle import Cycle, CycleIssue, CycleFavorite +from .cycle import Cycle, CycleIssue, CycleFavorite, CycleUserProperties from .view import GlobalView, IssueView, IssueViewFavorite -from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite +from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite, ModuleUserProperties from .api import APIToken, APIActivityLog diff --git a/apiserver/plane/db/models/cycle.py b/apiserver/plane/db/models/cycle.py index e5e2c355b..a441057e1 100644 --- a/apiserver/plane/db/models/cycle.py +++ b/apiserver/plane/db/models/cycle.py @@ -6,6 +6,47 @@ from django.conf import settings from . import ProjectBaseModel +def get_default_filters(): + return { + "priority": None, + "state": None, + "state_group": None, + "assignees": None, + "created_by": None, + "labels": None, + "start_date": None, + "target_date": None, + "subscriber": None, + } + +def get_default_display_filters(): + return { + "group_by": None, + "order_by": "-created_at", + "type": None, + "sub_issue": True, + "show_empty_groups": True, + "layout": "list", + "calendar_date_range": "", + } + +def get_default_display_properties(): + return { + "assignee": True, + "attachment_count": True, + "created_on": True, + "due_date": True, + "estimate": True, + "key": True, + "labels": True, + "link": True, + "priority": True, + "start_date": True, + "state": True, + "sub_issue_count": True, + "updated_on": True, + } + class Cycle(ProjectBaseModel): name = models.CharField(max_length=255, verbose_name="Cycle Name") description = models.TextField(verbose_name="Cycle Description", blank=True) @@ -89,3 +130,28 @@ class CycleFavorite(ProjectBaseModel): def __str__(self): """Return user and the cycle""" return f"{self.user.email} <{self.cycle.name}>" + + +class CycleUserProperties(ProjectBaseModel): + cycle = models.ForeignKey( + "db.Cycle", on_delete=models.CASCADE, related_name="cycle_user_properties" + ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="cycle_user_properties", + ) + filters = models.JSONField(default=get_default_filters) + display_filters = models.JSONField(default=get_default_display_filters) + display_properties = models.JSONField(default=get_default_display_properties) + + + class Meta: + unique_together = ["cycle", "user"] + verbose_name = "Cycle User Property" + verbose_name_plural = "Cycle User Properties" + db_table = "cycle_user_properties" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.cycle.name} {self.user.email}" \ No newline at end of file diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 54acd5c5d..b14376bc5 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -33,6 +33,48 @@ def get_default_properties(): } +def get_default_filters(): + return { + "priority": None, + "state": None, + "state_group": None, + "assignees": None, + "created_by": None, + "labels": None, + "start_date": None, + "target_date": None, + "subscriber": None, + } + +def get_default_display_filters(): + return { + "group_by": None, + "order_by": "-created_at", + "type": None, + "sub_issue": True, + "show_empty_groups": True, + "layout": "list", + "calendar_date_range": "", + } + +def get_default_display_properties(): + return { + "assignee": True, + "attachment_count": True, + "created_on": True, + "due_date": True, + "estimate": True, + "key": True, + "labels": True, + "link": True, + "priority": True, + "start_date": True, + "state": True, + "sub_issue_count": True, + "updated_on": True, + } + + # TODO: Handle identifiers for Bulk Inserts - nk class IssueManager(models.Manager): def get_queryset(self): @@ -394,7 +436,9 @@ class IssueProperty(ProjectBaseModel): on_delete=models.CASCADE, related_name="issue_property_user", ) - properties = models.JSONField(default=get_default_properties) + filters = models.JSONField(default=get_default_filters) + display_filters = models.JSONField(default=get_default_display_filters) + display_properties = models.JSONField(default=get_default_display_properties) class Meta: verbose_name = "Issue Property" diff --git a/apiserver/plane/db/models/module.py b/apiserver/plane/db/models/module.py index e485eea62..cc8185946 100644 --- a/apiserver/plane/db/models/module.py +++ b/apiserver/plane/db/models/module.py @@ -6,6 +6,47 @@ from django.conf import settings from . import ProjectBaseModel +def get_default_filters(): + return { + "priority": None, + "state": None, + "state_group": None, + "assignees": None, + "created_by": None, + "labels": None, + "start_date": None, + "target_date": None, + "subscriber": None, + }, + +def get_default_display_filters(): + return { + "group_by": None, + "order_by": "-created_at", + "type": None, + "sub_issue": True, + "show_empty_groups": True, + "layout": "list", + "calendar_date_range": "", + } + +def get_default_display_properties(): + return { + "assignee": True, + "attachment_count": True, + "created_on": True, + "due_date": True, + "estimate": True, + "key": True, + "labels": True, + "link": True, + "priority": True, + "start_date": True, + "state": True, + "sub_issue_count": True, + "updated_on": True, + } + class Module(ProjectBaseModel): name = models.CharField(max_length=255, verbose_name="Module Name") description = models.TextField(verbose_name="Module Description", blank=True) @@ -141,3 +182,28 @@ class ModuleFavorite(ProjectBaseModel): def __str__(self): """Return user and the module""" return f"{self.user.email} <{self.module.name}>" + + +class ModuleUserProperties(ProjectBaseModel): + module = models.ForeignKey( + "db.Module", on_delete=models.CASCADE, related_name="module_user_properties" + ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="module_user_properties", + ) + filters = models.JSONField(default=get_default_filters) + display_filters = models.JSONField(default=get_default_display_filters) + display_properties = models.JSONField(default=get_default_display_properties) + + + class Meta: + unique_together = ["module", "user"] + verbose_name = "Module User Property" + verbose_name_plural = "Module User Property" + db_table = "module_user_properties" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.module.name} {self.user.email}" \ No newline at end of file diff --git a/apiserver/plane/db/models/view.py b/apiserver/plane/db/models/view.py index 44bc994d0..8a77f0586 100644 --- a/apiserver/plane/db/models/view.py +++ b/apiserver/plane/db/models/view.py @@ -3,9 +3,50 @@ from django.db import models from django.conf import settings # Module import -from . import ProjectBaseModel, BaseModel +from . import ProjectBaseModel, BaseModel, WorkspaceBaseModel +def get_default_filters(): + return { + "priority": None, + "state": None, + "state_group": None, + "assignees": None, + "created_by": None, + "labels": None, + "start_date": None, + "target_date": None, + "subscriber": None, + } + +def get_default_display_filters(): + return { + "group_by": None, + "order_by": "-created_at", + "type": None, + "sub_issue": True, + "show_empty_groups": True, + "layout": "list", + "calendar_date_range": "", + } + +def get_default_display_properties(): + return { + "assignee": True, + "attachment_count": True, + "created_on": True, + "due_date": True, + "estimate": True, + "key": True, + "labels": True, + "link": True, + "priority": True, + "start_date": True, + "state": True, + "sub_issue_count": True, + "updated_on": True, + } + class GlobalView(BaseModel): workspace = models.ForeignKey( "db.Workspace", on_delete=models.CASCADE, related_name="global_views" @@ -40,14 +81,17 @@ class GlobalView(BaseModel): return f"{self.name} <{self.workspace.name}>" -class IssueView(ProjectBaseModel): +class IssueView(WorkspaceBaseModel): name = models.CharField(max_length=255, verbose_name="View Name") description = models.TextField(verbose_name="View Description", blank=True) query = models.JSONField(verbose_name="View Query") + filters = models.JSONField(default=dict) + display_filters = models.JSONField(default=get_default_display_filters) + display_properties = models.JSONField(default=get_default_display_properties) access = models.PositiveSmallIntegerField( default=1, choices=((0, "Private"), (1, "Public")) ) - query_data = models.JSONField(default=dict) + sort_order = models.FloatField(default=65535) class Meta: verbose_name = "Issue View" diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index 505bfbcfa..f0d64ecae 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -54,6 +54,51 @@ def get_default_props(): }, } +def get_default_filters(): + return { + "priority": None, + "state": None, + "state_group": None, + "assignees": None, + "created_by": None, + "labels": None, + "start_date": None, + "target_date": None, + "subscriber": None, + } + +def get_default_display_filters(): + return { + "display_filters": { + "group_by": None, + "order_by": "-created_at", + "type": None, + "sub_issue": True, + "show_empty_groups": True, + "layout": "list", + "calendar_date_range": "", + }, + } + +def get_default_display_properties(): + return { + "display_properties": { + "assignee": True, + "attachment_count": True, + "created_on": True, + "due_date": True, + "estimate": True, + "key": True, + "labels": True, + "link": True, + "priority": True, + "start_date": True, + "state": True, + "sub_issue_count": True, + "updated_on": True, + }, + } + def get_issue_props(): return { @@ -103,6 +148,22 @@ class Workspace(BaseModel): ordering = ("-created_at",) +class WorkspaceBaseModel(BaseModel): + workspace = models.ForeignKey( + "db.Workspace", models.CASCADE, related_name="workspace_%(class)s" + ) + project = models.ForeignKey( + "db.Project", models.CASCADE, related_name="project_%(class)s", null=True + ) + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + if self.project: + self.workspace = self.project.workspace + super(WorkspaceBaseModel, self).save(*args, **kwargs) + class WorkspaceMember(BaseModel): workspace = models.ForeignKey( "db.Workspace", on_delete=models.CASCADE, related_name="workspace_member" @@ -218,3 +279,28 @@ class WorkspaceTheme(BaseModel): verbose_name_plural = "Workspace Themes" db_table = "workspace_themes" ordering = ("-created_at",) + + +class WorkspaceUserProperties(BaseModel): + workspace = models.ForeignKey( + "db.Workspace", on_delete=models.CASCADE, related_name="workspace_user_properties" + ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="workspace_user_properties", + ) + filters = models.JSONField(default=get_default_filters) + display_filters = models.JSONField(default=get_default_display_filters) + display_properties = models.JSONField(default=get_default_display_properties) + + + class Meta: + unique_together = ["workspace", "user"] + verbose_name = "Workspace User Property" + verbose_name_plural = "Workspace User Property" + db_table = "Workspace_user_properties" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.workspace.name} {self.user.email}" \ No newline at end of file diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 0e7a18fa8..6832297e9 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -30,7 +30,7 @@ openpyxl==3.1.2 beautifulsoup4==4.12.2 dj-database-url==2.1.0 posthog==3.0.2 -cryptography==41.0.6 +cryptography==41.0.5 lxml==4.9.3 boto3==1.28.40 diff --git a/deploy/selfhost/install.sh b/deploy/selfhost/install.sh index 15150aa40..645e99cb8 100755 --- a/deploy/selfhost/install.sh +++ b/deploy/selfhost/install.sh @@ -39,7 +39,7 @@ function download(){ echo "" echo "Latest version is now available for you to use" echo "" - echo "In case of Upgrade, your new setting file is available as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file." + echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file." echo "" } diff --git a/package.json b/package.json index b5d997662..aad104784 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "packages/eslint-config-custom", "packages/tailwind-config-custom", "packages/tsconfig", - "packages/ui" + "packages/ui", + "packages/types" ], "scripts": { "build": "turbo run build", diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 000000000..a9dfbb8e0 --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,7 @@ +{ + "name": "@plane/types", + "version": "0.14.0", + "private": true, + "main": "./src/index.d.ts" +} + \ No newline at end of file diff --git a/web/types/ai.d.ts b/packages/types/src/ai.d.ts similarity index 73% rename from web/types/ai.d.ts rename to packages/types/src/ai.d.ts index 6c933a033..ce8bcbadb 100644 --- a/web/types/ai.d.ts +++ b/packages/types/src/ai.d.ts @@ -1,4 +1,4 @@ -import { IProjectLite, IWorkspaceLite } from "types"; +import { IProjectLite, IWorkspaceLite } from "@plane/types"; export interface IGptResponse { response: string; diff --git a/web/types/analytics.d.ts b/packages/types/src/analytics.d.ts similarity index 100% rename from web/types/analytics.d.ts rename to packages/types/src/analytics.d.ts diff --git a/web/types/api_token.d.ts b/packages/types/src/api_token.d.ts similarity index 100% rename from web/types/api_token.d.ts rename to packages/types/src/api_token.d.ts diff --git a/web/types/app.d.ts b/packages/types/src/app.d.ts similarity index 77% rename from web/types/app.d.ts rename to packages/types/src/app.d.ts index 0122cf73a..4d938ce26 100644 --- a/web/types/app.d.ts +++ b/packages/types/src/app.d.ts @@ -1,6 +1,4 @@ -export type NextPageWithLayout

= NextPage & { - getLayout?: (page: ReactElement) => ReactNode; -}; + export interface IAppConfig { email_password_login: boolean; diff --git a/web/types/auth.d.ts b/packages/types/src/auth.d.ts similarity index 100% rename from web/types/auth.d.ts rename to packages/types/src/auth.d.ts diff --git a/web/types/calendar.ts b/packages/types/src/calendar.d.ts similarity index 100% rename from web/types/calendar.ts rename to packages/types/src/calendar.d.ts diff --git a/web/types/cycles.d.ts b/packages/types/src/cycles.d.ts similarity index 91% rename from web/types/cycles.d.ts rename to packages/types/src/cycles.d.ts index 4f243deeb..6723b3946 100644 --- a/web/types/cycles.d.ts +++ b/packages/types/src/cycles.d.ts @@ -1,4 +1,4 @@ -import type { IUser, IIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, IUserLite } from "types"; +import type { IUser, TIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, IUserLite } from "@plane/types"; export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft"; @@ -68,7 +68,7 @@ export type TLabelsDistribution = { export interface CycleIssueResponse { id: string; - issue_detail: IIssue; + issue_detail: TIssue; created_at: Date; updated_at: Date; created_by: string; @@ -82,7 +82,7 @@ export interface CycleIssueResponse { export type SelectCycleType = (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | undefined; -export type SelectIssue = (IIssue & { actionType: "edit" | "delete" | "create" }) | null; +export type SelectIssue = (TIssue & { actionType: "edit" | "delete" | "create" }) | null; export type CycleDateCheckData = { start_date: string; diff --git a/web/types/estimate.d.ts b/packages/types/src/estimate.d.ts similarity index 100% rename from web/types/estimate.d.ts rename to packages/types/src/estimate.d.ts index 32925c793..96b584ce1 100644 --- a/web/types/estimate.d.ts +++ b/packages/types/src/estimate.d.ts @@ -1,24 +1,24 @@ export interface IEstimate { - id: string; created_at: Date; - updated_at: Date; - name: string; - description: string; created_by: string; - updated_by: string; - points: IEstimatePoint[]; + description: string; + id: string; + name: string; project: string; project_detail: IProject; + updated_at: Date; + updated_by: string; + points: IEstimatePoint[]; workspace: string; workspace_detail: IWorkspace; } export interface IEstimatePoint { - id: string; created_at: string; created_by: string; description: string; estimate: string; + id: string; key: number; project: string; updated_at: string; diff --git a/web/types/importer/github-importer.d.ts b/packages/types/src/importer/github-importer.d.ts similarity index 100% rename from web/types/importer/github-importer.d.ts rename to packages/types/src/importer/github-importer.d.ts diff --git a/web/types/importer/index.ts b/packages/types/src/importer/index.d.ts similarity index 92% rename from web/types/importer/index.ts rename to packages/types/src/importer/index.d.ts index 81e1bb22f..877c07196 100644 --- a/web/types/importer/index.ts +++ b/packages/types/src/importer/index.d.ts @@ -1,9 +1,9 @@ export * from "./github-importer"; export * from "./jira-importer"; -import { IProjectLite } from "types/projects"; +import { IProjectLite } from "../projects"; // types -import { IUserLite } from "types/users"; +import { IUserLite } from "../users"; export interface IImporterService { created_at: string; diff --git a/web/types/importer/jira-importer.d.ts b/packages/types/src/importer/jira-importer.d.ts similarity index 100% rename from web/types/importer/jira-importer.d.ts rename to packages/types/src/importer/jira-importer.d.ts diff --git a/web/types/inbox.d.ts b/packages/types/src/inbox.d.ts similarity index 93% rename from web/types/inbox.d.ts rename to packages/types/src/inbox.d.ts index 10fc37b31..1b474c3ab 100644 --- a/web/types/inbox.d.ts +++ b/packages/types/src/inbox.d.ts @@ -1,7 +1,7 @@ -import { IIssue } from "./issues"; +import { TIssue } from "./issues"; import type { IProjectLite } from "./projects"; -export interface IInboxIssue extends IIssue { +export interface IInboxIssue extends TIssue { issue_inbox: { duplicate_to: string | null; id: string; diff --git a/web/types/index.d.ts b/packages/types/src/index.d.ts similarity index 81% rename from web/types/index.d.ts rename to packages/types/src/index.d.ts index 9f27e818c..4bbed28d3 100644 --- a/web/types/index.d.ts +++ b/packages/types/src/index.d.ts @@ -21,6 +21,11 @@ export * from "./reaction"; export * from "./view-props"; export * from "./workspace-views"; export * from "./webhook"; +export * from "./issues/base"; // TODO: Remove this after development and the refactor/mobx-store-issue branch is stable +export * from "./auth"; +export * from "./api_token"; +export * from "./instance"; +export * from "./app"; export type NestedKeyOf = { [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object diff --git a/web/types/instance.d.ts b/packages/types/src/instance.d.ts similarity index 100% rename from web/types/instance.d.ts rename to packages/types/src/instance.d.ts diff --git a/web/types/integration.d.ts b/packages/types/src/integration.d.ts similarity index 100% rename from web/types/integration.d.ts rename to packages/types/src/integration.d.ts diff --git a/web/types/issues.d.ts b/packages/types/src/issues.d.ts similarity index 72% rename from web/types/issues.d.ts rename to packages/types/src/issues.d.ts index 09f21eb3a..c0ad7bc7f 100644 --- a/web/types/issues.d.ts +++ b/packages/types/src/issues.d.ts @@ -1,3 +1,4 @@ +import { ReactElement } from "react"; import { KeyedMutator } from "swr"; import type { IState, @@ -10,7 +11,8 @@ import type { IStateLite, Properties, IIssueDisplayFilterOptions, -} from "types"; + IIssueReaction, +} from "@plane/types"; export interface IIssueCycle { id: string; @@ -83,6 +85,7 @@ export interface IIssue { attachment_count: number; attachments: any[]; issue_relations: IssueRelation[]; + issue_reactions: IIssueReaction[]; related_issues: IssueRelation[]; bridge_id?: string | null; completed_at: Date; @@ -138,7 +141,7 @@ export interface ISubIssuesState { export interface ISubIssueResponse { state_distribution: ISubIssuesState; - sub_issues: IIssue[]; + sub_issues: TIssue[]; } export interface BlockeIssueDetail { @@ -240,13 +243,13 @@ export interface IIssueAttachment { } export interface IIssueViewProps { - groupedIssues: { [key: string]: IIssue[] } | undefined; + groupedIssues: { [key: string]: TIssue[] } | undefined; displayFilters: IIssueDisplayFilterOptions | undefined; isEmpty: boolean; mutateIssues: KeyedMutator< - | IIssue[] + | TIssue[] | { - [key: string]: IIssue[]; + [key: string]: TIssue[]; } >; params: any; @@ -254,3 +257,88 @@ export interface IIssueViewProps { } export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none"; + +export interface ViewFlags { + enableQuickAdd: boolean; + enableIssueCreation: boolean; + enableInlineEditing: boolean; +} + +export type GroupByColumnTypes = + | "project" + | "state" + | "state_detail.group" + | "priority" + | "labels" + | "assignees" + | "created_by"; + +export interface IGroupByColumn { + id: string; + name: string; + Icon: ReactElement | undefined; + payload: Partial; +} + +export interface IIssueMap { + [key: string]: TIssue; +} + +// new issue structure types +export type TIssue = { + id: string; + name: string; + state_id: string; + description_html: string; + sort_order: number; + completed_at: string | null; + estimate_point: number | null; + priority: TIssuePriorities; + start_date: string | null; + target_date: string | null; + sequence_id: number; + project_id: string; + parent_id: string | null; + cycle_id: string | null; + module_id: string | null; + label_ids: string[]; + assignee_ids: string[]; + sub_issues_count: number; + created_at: string; + updated_at: string; + created_by: string; + updated_by: string; + attachment_count: number; + link_count: number; + is_subscribed: boolean; + archived_at: boolean; + is_draft: boolean; + // tempId is used for optimistic updates. It is not a part of the API response. + tempId?: string; + // issue details + related_issues: any; + issue_reactions: any; + issue_relations: any; + issue_cycle: any; + issue_module: any; + parent_detail: any; + issue_link: any; +}; + +export type TIssueMap = { + [issue_id: string]: TIssue; +}; + +export type TLoader = "init-loader" | "mutation" | undefined; + +export type TGroupedIssues = { + [group_id: string]: string[]; +}; + +export type TSubGroupedIssues = { + [sub_grouped_id: string]: { + [group_id: string]: string[]; + }; +}; + +export type TUnGroupedIssues = string[]; diff --git a/packages/types/src/issues/base.d.ts b/packages/types/src/issues/base.d.ts new file mode 100644 index 000000000..08daceb16 --- /dev/null +++ b/packages/types/src/issues/base.d.ts @@ -0,0 +1,23 @@ +// issues +export * from "./issue"; +export * from "./issue_reaction"; +export * from "./issue_link"; +export * from "./issue_attachment"; +export * from "./issue_relation"; +export * from "./issue_activity"; +export * from "./issue_comment_reaction"; +export * from "./issue_sub_issues"; + +export type TLoader = "init-loader" | "mutation" | undefined; + +export type TGroupedIssues = { + [group_id: string]: string[]; +}; + +export type TSubGroupedIssues = { + [sub_grouped_id: string]: { + [group_id: string]: string[]; + }; +}; + +export type TUnGroupedIssues = string[]; diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts new file mode 100644 index 000000000..e9ec14528 --- /dev/null +++ b/packages/types/src/issues/issue.d.ts @@ -0,0 +1,36 @@ +// new issue structure types +export type TIssue = { + id: string; + name: string; + state_id: string; + description_html: string; + sort_order: number; + completed_at: string | null; + estimate_point: number | null; + priority: TIssuePriorities; + start_date: string; + target_date: string; + sequence_id: number; + project_id: string; + parent_id: string | null; + cycle_id: string | null; + module_id: string | null; + label_ids: string[]; + assignee_ids: string[]; + sub_issues_count: number; + created_at: string; + updated_at: string; + created_by: string; + updated_by: string; + attachment_count: number; + link_count: number; + is_subscribed: boolean; + archived_at: boolean; + is_draft: boolean; + // tempId is used for optimistic updates. It is not a part of the API response. + tempId?: string; +}; + +export type TIssueMap = { + [issue_id: string]: TIssue; +}; diff --git a/packages/types/src/issues/issue_activity.d.ts b/packages/types/src/issues/issue_activity.d.ts new file mode 100644 index 000000000..2ce22b361 --- /dev/null +++ b/packages/types/src/issues/issue_activity.d.ts @@ -0,0 +1,41 @@ +export type TIssueActivity = { + access?: "EXTERNAL" | "INTERNAL"; + actor: string; + actor_detail: IUserLite; + attachments: any[]; + comment?: string; + comment_html?: string; + comment_stripped?: string; + created_at: Date; + created_by: string; + field: string | null; + id: string; + issue: string | null; + issue_comment?: string | null; + issue_detail: { + description_html: string; + id: string; + name: string; + priority: string | null; + sequence_id: string; + } | null; + new_identifier: string | null; + new_value: string | null; + old_identifier: string | null; + old_value: string | null; + project: string; + project_detail: IProjectLite; + updated_at: Date; + updated_by: string; + verb: string; + workspace: string; + workspace_detail?: IWorkspaceLite; +}; + +export type TIssueActivityMap = { + [issue_id: string]: TIssueActivity; +}; + +export type TIssueActivityIdMap = { + [issue_id: string]: string[]; +}; diff --git a/packages/types/src/issues/issue_attachment.d.ts b/packages/types/src/issues/issue_attachment.d.ts new file mode 100644 index 000000000..90daa08fa --- /dev/null +++ b/packages/types/src/issues/issue_attachment.d.ts @@ -0,0 +1,23 @@ +export type TIssueAttachment = { + id: string; + created_at: string; + updated_at: string; + attributes: { + name: string; + size: number; + }; + asset: string; + created_by: string; + updated_by: string; + project: string; + workspace: string; + issue: string; +}; + +export type TIssueAttachmentMap = { + [issue_id: string]: TIssueAttachment; +}; + +export type TIssueAttachmentIdMap = { + [issue_id: string]: string[]; +}; diff --git a/packages/types/src/issues/issue_comment_reaction.d.ts b/packages/types/src/issues/issue_comment_reaction.d.ts new file mode 100644 index 000000000..8a3695e85 --- /dev/null +++ b/packages/types/src/issues/issue_comment_reaction.d.ts @@ -0,0 +1,20 @@ +export type TIssueCommentReaction = { + id: string; + created_at: Date; + updated_at: Date; + reaction: string; + created_by: string; + updated_by: string; + project: string; + workspace: string; + actor: string; + comment: string; +}; + +export type TIssueCommentReactionMap = { + [issue_id: string]: TIssueCommentReaction; +}; + +export type TIssueCommentReactionIdMap = { + [issue_id: string]: string[]; +}; diff --git a/packages/types/src/issues/issue_link.d.ts b/packages/types/src/issues/issue_link.d.ts new file mode 100644 index 000000000..2c469e682 --- /dev/null +++ b/packages/types/src/issues/issue_link.d.ts @@ -0,0 +1,20 @@ +export type TIssueLinkEditableFields = { + title: string; + url: string; +}; + +export type TIssueLink = TIssueLinkEditableFields & { + created_at: Date; + created_by: string; + created_by_detail: IUserLite; + id: string; + metadata: any; +}; + +export type TIssueLinkMap = { + [issue_id: string]: TIssueLink; +}; + +export type TIssueLinkIdMap = { + [issue_id: string]: string[]; +}; diff --git a/packages/types/src/issues/issue_reaction.d.ts b/packages/types/src/issues/issue_reaction.d.ts new file mode 100644 index 000000000..2fe646246 --- /dev/null +++ b/packages/types/src/issues/issue_reaction.d.ts @@ -0,0 +1,21 @@ +export type TIssueReaction = { + actor: string; + actor_detail: IUserLite; + created_at: Date; + created_by: string; + id: string; + issue: string; + project: string; + reaction: string; + updated_at: Date; + updated_by: string; + workspace: string; +}; + +export type TIssueReactionMap = { + [issue_id: string]: TIssueReaction; +}; + +export type TIssueReactionIdMap = { + [issue_id: string]: string[]; +}; diff --git a/packages/types/src/issues/issue_relation.d.ts b/packages/types/src/issues/issue_relation.d.ts new file mode 100644 index 000000000..0d959ff6b --- /dev/null +++ b/packages/types/src/issues/issue_relation.d.ts @@ -0,0 +1,20 @@ +import { TIssue } from "./issues"; + +export type TIssueRelationTypes = + | "blocking" + | "blocked_by" + | "duplicate" + | "relates_to"; + +export type TIssueRelationObject = { issue_detail: TIssue }; + +export type TIssueRelation = Record< + TIssueRelationTypes, + TIssueRelationObject[] +>; + +export type TIssueRelationMap = { + [issue_id: string]: Record; +}; + +export type TIssueRelationIdMap = Record; diff --git a/packages/types/src/issues/issue_sub_issues.d.ts b/packages/types/src/issues/issue_sub_issues.d.ts new file mode 100644 index 000000000..76dcf1288 --- /dev/null +++ b/packages/types/src/issues/issue_sub_issues.d.ts @@ -0,0 +1,22 @@ +import { TIssue } from "./issue"; + +export type TSubIssuesStateDistribution = { + backlog: number; + unstarted: number; + started: number; + completed: number; + cancelled: number; +}; + +export type TIssueSubIssues = { + state_distribution: TSubIssuesStateDistribution; + sub_issues: TIssue[]; +}; + +export type TIssueSubIssuesStateDistributionMap = { + [issue_id: string]: TSubIssuesStateDistribution; +}; + +export type TIssueSubIssuesIdMap = { + [issue_id: string]: string[]; +}; diff --git a/packages/types/src/issues/issue_subscription.d.ts b/packages/types/src/issues/issue_subscription.d.ts new file mode 100644 index 000000000..e69de29bb diff --git a/web/types/modules.d.ts b/packages/types/src/modules.d.ts similarity index 93% rename from web/types/modules.d.ts rename to packages/types/src/modules.d.ts index 733b8f7de..0e49da7fe 100644 --- a/web/types/modules.d.ts +++ b/packages/types/src/modules.d.ts @@ -1,14 +1,14 @@ import type { IUser, IUserLite, - IIssue, + TIssue, IProject, IWorkspace, IWorkspaceLite, IProjectLite, IIssueFilterOptions, ILinkDetails, -} from "types"; +} from "@plane/types"; export type TModuleStatus = "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled"; @@ -58,7 +58,7 @@ export interface ModuleIssueResponse { created_by: string; id: string; issue: string; - issue_detail: IIssue; + issue_detail: TIssue; module: string; module_detail: IModule; project: string; @@ -75,4 +75,4 @@ export type ModuleLink = { export type SelectModuleType = (IModule & { actionType: "edit" | "delete" | "create-issue" }) | undefined; -export type SelectIssue = (IIssue & { actionType: "edit" | "delete" | "create" }) | undefined; +export type SelectIssue = (TIssue & { actionType: "edit" | "delete" | "create" }) | undefined; diff --git a/web/types/notifications.d.ts b/packages/types/src/notifications.d.ts similarity index 100% rename from web/types/notifications.d.ts rename to packages/types/src/notifications.d.ts diff --git a/web/types/pages.d.ts b/packages/types/src/pages.d.ts similarity index 79% rename from web/types/pages.d.ts rename to packages/types/src/pages.d.ts index a1c241f6a..29552b94c 100644 --- a/web/types/pages.d.ts +++ b/packages/types/src/pages.d.ts @@ -1,5 +1,5 @@ // types -import { IIssue, IIssueLabel, IWorkspaceLite, IProjectLite } from "types"; +import { TIssue, IIssueLabel, IWorkspaceLite, IProjectLite } from "@plane/types"; export interface IPage { access: number; @@ -27,15 +27,11 @@ export interface IPage { } export interface IRecentPages { - today: IPage[]; - yesterday: IPage[]; - this_week: IPage[]; - older: IPage[]; - [key: string]: IPage[]; -} - -export interface RecentPagesResponse { - [key: string]: IPage[]; + today: string[]; + yesterday: string[]; + this_week: string[]; + older: string[]; + [key: string]: string[]; } export interface IPageBlock { @@ -47,7 +43,7 @@ export interface IPageBlock { description_stripped: any; id: string; issue: string | null; - issue_detail: IIssue | null; + issue_detail: TIssue | null; name: string; page: string; project: string; diff --git a/web/types/projects.d.ts b/packages/types/src/projects.d.ts similarity index 78% rename from web/types/projects.d.ts rename to packages/types/src/projects.d.ts index 129b0bb3b..a412180b8 100644 --- a/web/types/projects.d.ts +++ b/packages/types/src/projects.d.ts @@ -1,6 +1,5 @@ -import type { IUserLite, IWorkspace, IWorkspaceLite, IUserMemberLite, TStateGroups, IProjectViewProps } from "."; - -export type TUserProjectRole = 5 | 10 | 15 | 20; +import { EUserProjectRoles } from "constants/project"; +import type { IUser, IUserLite, IWorkspace, IWorkspaceLite, TStateGroups } from "."; export interface IProject { archive_in: number; @@ -34,13 +33,10 @@ export interface IProject { is_deployed: boolean; is_favorite: boolean; is_member: boolean; - member_role: TUserProjectRole | null; + member_role: EUserProjectRoles | null; members: IProjectMemberLite[]; - issue_views_view: boolean; - module_view: boolean; name: string; network: number; - page_view: boolean; project_lead: IUserLite | string | null; sort_order: number | null; total_cycles: number; @@ -64,6 +60,10 @@ type ProjectPreferences = { }; }; +export interface IProjectMap { + [id: string]: IProject; +} + export interface IProjectMemberLite { id: string; member__avatar: string; @@ -77,7 +77,7 @@ export interface IProjectMember { project: IProjectLite; workspace: IWorkspaceLite; comment: string; - role: TUserProjectRole; + role: EUserProjectRoles; preferences: ProjectPreferences; @@ -90,27 +90,14 @@ export interface IProjectMember { updated_by: string; } -export interface IProjectMemberInvitation { +export interface IProjectMembership { id: string; - - project: IProject; - workspace: IWorkspace; - - email: string; - accepted: boolean; - token: string; - message: string; - responded_at: Date; - role: TUserProjectRole; - - created_at: Date; - updated_at: Date; - created_by: string; - updated_by: string; + member: string; + role: EUserProjectRoles; } export interface IProjectBulkAddFormData { - members: { role: TUserProjectRole; member_id: string }[]; + members: { role: EUserProjectRoles; member_id: string }[]; } export interface IGithubRepository { diff --git a/web/types/reaction.d.ts b/packages/types/src/reaction.d.ts similarity index 100% rename from web/types/reaction.d.ts rename to packages/types/src/reaction.d.ts diff --git a/web/types/state.d.ts b/packages/types/src/state.d.ts similarity index 90% rename from web/types/state.d.ts rename to packages/types/src/state.d.ts index 3fdbaa2d3..822b99f17 100644 --- a/web/types/state.d.ts +++ b/packages/types/src/state.d.ts @@ -1,4 +1,4 @@ -import { IProject, IProjectLite, IWorkspaceLite } from "types"; +import { IProject, IProjectLite, IWorkspaceLite } from "@plane/types"; export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; diff --git a/web/types/users.d.ts b/packages/types/src/users.d.ts similarity index 97% rename from web/types/users.d.ts rename to packages/types/src/users.d.ts index 301c1d7c0..bbca953f6 100644 --- a/web/types/users.d.ts +++ b/packages/types/src/users.d.ts @@ -1,3 +1,4 @@ +import { EUserProjectRoles } from "constants/project"; import { IIssueActivity, IIssueLite, TStateGroups } from "."; export interface IUser { @@ -61,11 +62,10 @@ export interface IUserTheme { export interface IUserLite { avatar: string; - created_at: Date; display_name: string; email?: string; first_name: string; - readonly id: string; + id: string; is_bot: boolean; last_name: string; } @@ -163,7 +163,7 @@ export interface IUserProfileProjectSegregation { } export interface IUserProjectsRole { - [project_id: string]: number; + [projectId: string]: EUserProjectRoles; } // export interface ICurrentUser { diff --git a/web/types/view-props.d.ts b/packages/types/src/view-props.d.ts similarity index 91% rename from web/types/view-props.d.ts rename to packages/types/src/view-props.d.ts index c8c47576b..282fc5a9c 100644 --- a/web/types/view-props.d.ts +++ b/packages/types/src/view-props.d.ts @@ -108,6 +108,18 @@ export interface IIssueDisplayProperties { updated_on?: boolean; } +export interface IIssueFilters { + filters: IIssueFilterOptions | undefined; + displayFilters: IIssueDisplayFilterOptions | undefined; + displayProperties: IIssueDisplayProperties | undefined; +} + +export interface IIssueFiltersResponse { + filters: IIssueFilterOptions; + display_filters: IIssueDisplayFilterOptions; + display_properties: IIssueDisplayProperties; +} + export interface IWorkspaceIssueFilterOptions { assignees?: string[] | null; created_by?: string[] | null; diff --git a/web/types/views.d.ts b/packages/types/src/views.d.ts similarity index 58% rename from web/types/views.d.ts rename to packages/types/src/views.d.ts index 4f55e8c74..db30554a8 100644 --- a/web/types/views.d.ts +++ b/packages/types/src/views.d.ts @@ -1,4 +1,4 @@ -import { IIssueFilterOptions } from "./view-props"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "./view-props"; export interface IProjectView { id: string; @@ -10,6 +10,9 @@ export interface IProjectView { updated_by: string; name: string; description: string; + filters: IIssueFilterOptions; + display_filters: IIssueDisplayFilterOptions; + display_properties: IIssueDisplayProperties; query: IIssueFilterOptions; query_data: IIssueFilterOptions; project: string; diff --git a/web/types/waitlist.d.ts b/packages/types/src/waitlist.d.ts similarity index 100% rename from web/types/waitlist.d.ts rename to packages/types/src/waitlist.d.ts diff --git a/web/types/webhook.d.ts b/packages/types/src/webhook.d.ts similarity index 100% rename from web/types/webhook.d.ts rename to packages/types/src/webhook.d.ts diff --git a/web/types/workspace-views.d.ts b/packages/types/src/workspace-views.d.ts similarity index 64% rename from web/types/workspace-views.d.ts rename to packages/types/src/workspace-views.d.ts index 754e63711..29aa56742 100644 --- a/web/types/workspace-views.d.ts +++ b/packages/types/src/workspace-views.d.ts @@ -1,4 +1,9 @@ -import { IWorkspaceViewProps } from "./view-props"; +import { + IWorkspaceViewProps, + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, +} from "./view-props"; export interface IWorkspaceView { id: string; @@ -10,6 +15,9 @@ export interface IWorkspaceView { updated_by: string; name: string; description: string; + filters: IIssueIIFilterOptions; + display_filters: IIssueDisplayFilterOptions; + display_properties: IIssueDisplayProperties; query: any; query_data: IWorkspaceViewProps; project: string; diff --git a/web/types/workspace.d.ts b/packages/types/src/workspace.d.ts similarity index 88% rename from web/types/workspace.d.ts rename to packages/types/src/workspace.d.ts index fb2aca722..2fc8d6912 100644 --- a/web/types/workspace.d.ts +++ b/packages/types/src/workspace.d.ts @@ -1,6 +1,5 @@ -import type { IProjectMember, IUser, IUserLite, IWorkspaceViewProps } from "types"; - -export type TUserWorkspaceRole = 5 | 10 | 15 | 20; +import { EUserWorkspaceRoles } from "constants/workspace"; +import type { IProjectMember, IUser, IUserLite, IWorkspaceViewProps } from "@plane/types"; export interface IWorkspace { readonly id: string; @@ -27,18 +26,23 @@ export interface IWorkspaceLite { export interface IWorkspaceMemberInvitation { accepted: boolean; - readonly id: string; email: string; - token: string; + id: string; message: string; responded_at: Date; - role: TUserWorkspaceRole; - created_by_detail: IUser; - workspace: IWorkspace; + role: EUserWorkspaceRoles; + token: string; + workspace: string; + workspace_detail: { + id: string; + logo: string; + name: string; + slug: string; + }; } export interface IWorkspaceBulkInviteFormData { - emails: { email: string; role: TUserWorkspaceRole }[]; + emails: { email: string; role: EUserWorkspaceRoles }[]; } export type Properties = { @@ -58,15 +62,9 @@ export type Properties = { }; export interface IWorkspaceMember { - company_role: string | null; - created_at: Date; - created_by: string; id: string; member: IUserLite; - role: TUserWorkspaceRole; - updated_at: Date; - updated_by: string; - workspace: IWorkspaceLite; + role: EUserWorkspaceRoles; } export interface IWorkspaceMemberMe { @@ -76,7 +74,7 @@ export interface IWorkspaceMemberMe { default_props: IWorkspaceViewProps; id: string; member: string; - role: TUserWorkspaceRole; + role: EUserWorkspaceRoles; updated_at: Date; updated_by: string; view_props: IWorkspaceViewProps; diff --git a/packages/ui/src/icons/priority-icon.tsx b/packages/ui/src/icons/priority-icon.tsx index 198391adb..e814233d7 100644 --- a/packages/ui/src/icons/priority-icon.tsx +++ b/packages/ui/src/icons/priority-icon.tsx @@ -1,13 +1,16 @@ import * as React from "react"; - -// icons import { AlertCircle, Ban, SignalHigh, SignalLow, SignalMedium } from "lucide-react"; -// types -import { IPriorityIcon } from "./type"; +type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none"; -export const PriorityIcon: React.FC = ({ priority, className = "", transparentBg = false }) => { - if (!className || className === "") className = "h-4 w-4"; +interface IPriorityIcon { + className?: string; + priority: TIssuePriorities; + size?: number; +} + +export const PriorityIcon: React.FC = (props) => { + const { priority, className = "", size = 14 } = props; // Convert to lowercase for string comparison const lowercasePriority = priority?.toLowerCase(); @@ -16,31 +19,17 @@ export const PriorityIcon: React.FC = ({ priority, className = "" const getPriorityIcon = (): React.ReactNode => { switch (lowercasePriority) { case "urgent": - return ; + return ; case "high": - return ; + return ; case "medium": - return ; + return ; case "low": - return ; + return ; default: - return ; + return ; } }; - return ( - <> - {transparentBg ? ( - getPriorityIcon() - ) : ( -

- {getPriorityIcon()} -
- )} - - ); + return <>{getPriorityIcon()}; }; diff --git a/packages/ui/src/icons/type.d.ts b/packages/ui/src/icons/type.d.ts index 65b188e4c..4a04c948b 100644 --- a/packages/ui/src/icons/type.d.ts +++ b/packages/ui/src/icons/type.d.ts @@ -1,11 +1,3 @@ export interface ISvgIcons extends React.SVGAttributes { className?: string | undefined; } - -export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none"; - -export interface IPriorityIcon { - priority: TIssuePriorities | null; - className?: string; - transparentBg?: boolean | false; -} diff --git a/web/components/account/deactivate-account-modal.tsx b/web/components/account/deactivate-account-modal.tsx index 53ac1df50..307a65ad2 100644 --- a/web/components/account/deactivate-account-modal.tsx +++ b/web/components/account/deactivate-account-modal.tsx @@ -4,8 +4,8 @@ import { useTheme } from "next-themes"; import { Dialog, Transition } from "@headlessui/react"; import { Trash2 } from "lucide-react"; import { mutate } from "swr"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useUser } from "hooks/store"; // ui import { Button } from "@plane/ui"; // hooks @@ -22,9 +22,7 @@ export const DeactivateAccountModal: React.FC = (props) => { // states const [isDeactivating, setIsDeactivating] = useState(false); - const { - user: { deactivateAccount }, - } = useMobxStore(); + const { deactivateAccount } = useUser(); const router = useRouter(); diff --git a/web/components/account/sign-in-forms/email-form.tsx b/web/components/account/sign-in-forms/email-form.tsx index 6b6071475..c1e124eab 100644 --- a/web/components/account/sign-in-forms/email-form.tsx +++ b/web/components/account/sign-in-forms/email-form.tsx @@ -10,7 +10,7 @@ import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types -import { IEmailCheckData } from "types/auth"; +import { IEmailCheckData } from "@plane/types"; // constants import { ESignInSteps } from "components/account"; diff --git a/web/components/account/sign-in-forms/o-auth-options.tsx b/web/components/account/sign-in-forms/o-auth-options.tsx index aec82cfa5..9ed4e7e5f 100644 --- a/web/components/account/sign-in-forms/o-auth-options.tsx +++ b/web/components/account/sign-in-forms/o-auth-options.tsx @@ -1,9 +1,8 @@ import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // services import { AuthService } from "services/auth.service"; // hooks +import { useApplication } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { GitHubSignInButton, GoogleSignInButton } from "components/account"; @@ -21,8 +20,8 @@ export const OAuthOptions: React.FC = observer((props) => { const { setToastAlert } = useToast(); // mobx store const { - appConfig: { envConfig }, - } = useMobxStore(); + config: { envConfig }, + } = useApplication(); const handleGoogleSignIn = async ({ clientId, credential }: any) => { try { diff --git a/web/components/account/sign-in-forms/password.tsx b/web/components/account/sign-in-forms/password.tsx index a75a450e2..ef9edbfbc 100644 --- a/web/components/account/sign-in-forms/password.tsx +++ b/web/components/account/sign-in-forms/password.tsx @@ -11,7 +11,7 @@ import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types -import { IPasswordSignInData } from "types/auth"; +import { IPasswordSignInData } from "@plane/types"; // constants import { ESignInSteps } from "components/account"; diff --git a/web/components/account/sign-in-forms/root.tsx b/web/components/account/sign-in-forms/root.tsx index f7ec6b593..616f4809f 100644 --- a/web/components/account/sign-in-forms/root.tsx +++ b/web/components/account/sign-in-forms/root.tsx @@ -1,8 +1,7 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { useApplication } from "hooks/store"; import useSignInRedirection from "hooks/use-sign-in-redirection"; // components import { LatestFeatureBlock } from "components/common"; @@ -38,8 +37,8 @@ export const SignInRoot = observer(() => { const { handleRedirection } = useSignInRedirection(); // mobx store const { - appConfig: { envConfig }, - } = useMobxStore(); + config: { envConfig }, + } = useApplication(); const isOAuthEnabled = envConfig && (envConfig.google_client_id || envConfig.github_client_id); diff --git a/web/components/account/sign-in-forms/self-hosted-sign-in.tsx b/web/components/account/sign-in-forms/self-hosted-sign-in.tsx index 2335226ce..bcecef20a 100644 --- a/web/components/account/sign-in-forms/self-hosted-sign-in.tsx +++ b/web/components/account/sign-in-forms/self-hosted-sign-in.tsx @@ -11,7 +11,7 @@ import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types -import { IPasswordSignInData } from "types/auth"; +import { IPasswordSignInData } from "@plane/types"; type Props = { email: string; diff --git a/web/components/account/sign-in-forms/set-password-link.tsx b/web/components/account/sign-in-forms/set-password-link.tsx index 17dbd2ad4..788142d80 100644 --- a/web/components/account/sign-in-forms/set-password-link.tsx +++ b/web/components/account/sign-in-forms/set-password-link.tsx @@ -9,7 +9,7 @@ import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types -import { IEmailCheckData } from "types/auth"; +import { IEmailCheckData } from "@plane/types"; type Props = { email: string; diff --git a/web/components/account/sign-in-forms/unique-code.tsx b/web/components/account/sign-in-forms/unique-code.tsx index 1a4fa0e49..433fea00a 100644 --- a/web/components/account/sign-in-forms/unique-code.tsx +++ b/web/components/account/sign-in-forms/unique-code.tsx @@ -13,7 +13,7 @@ import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types -import { IEmailCheckData, IMagicSignInData } from "types/auth"; +import { IEmailCheckData, IMagicSignInData } from "@plane/types"; // constants import { ESignInSteps } from "components/account"; @@ -233,8 +233,8 @@ export const UniqueCodeForm: React.FC = (props) => { {resendTimerCode > 0 ? `Request new code in ${resendTimerCode}s` : isRequestingNewCode - ? "Requesting new code" - : "Request new code"} + ? "Requesting new code" + : "Request new code"} diff --git a/web/components/analytics/custom-analytics/custom-analytics.tsx b/web/components/analytics/custom-analytics/custom-analytics.tsx index 635fbee7f..a3c083b02 100644 --- a/web/components/analytics/custom-analytics/custom-analytics.tsx +++ b/web/components/analytics/custom-analytics/custom-analytics.tsx @@ -7,7 +7,7 @@ import { AnalyticsService } from "services/analytics.service"; // components import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSidebar } from "components/analytics"; // types -import { IAnalyticsParams } from "types"; +import { IAnalyticsParams } from "@plane/types"; // fetch-keys import { ANALYTICS } from "constants/fetch-keys"; diff --git a/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx b/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx index 9917d0f58..ec7c40195 100644 --- a/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx +++ b/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx @@ -3,7 +3,7 @@ import { BarTooltipProps } from "@nivo/bar"; import { DATE_KEYS } from "constants/analytics"; import { renderMonthAndYear } from "helpers/analytics.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse } from "types"; +import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types"; type Props = { datum: BarTooltipProps; @@ -60,8 +60,8 @@ export const CustomTooltip: React.FC = ({ datum, analytics, params }) => ? "capitalize" : "" : params.x_axis === "priority" || params.x_axis === "state__group" - ? "capitalize" - : "" + ? "capitalize" + : "" }`} > {params.segment === "assignees__id" ? renderAssigneeName(tooltipValue.toString()) : tooltipValue}: diff --git a/web/components/analytics/custom-analytics/graph/index.tsx b/web/components/analytics/custom-analytics/graph/index.tsx index 06431ab02..51b4089c4 100644 --- a/web/components/analytics/custom-analytics/graph/index.tsx +++ b/web/components/analytics/custom-analytics/graph/index.tsx @@ -9,7 +9,7 @@ import { BarGraph } from "components/ui"; import { findStringWithMostCharacters } from "helpers/array.helper"; import { generateBarColor, generateDisplayName } from "helpers/analytics.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse } from "types"; +import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types"; type Props = { analytics: IAnalyticsResponse; @@ -101,8 +101,8 @@ export const AnalyticsGraph: React.FC = ({ analytics, barGraphData, param ? generateDisplayName(datum.value, analytics, params, "x_axis")[0].toUpperCase() : "?" : datum.value && datum.value !== "None" - ? `${datum.value}`.toUpperCase()[0] - : "?"} + ? `${datum.value}`.toUpperCase()[0] + : "?"} diff --git a/web/components/analytics/custom-analytics/main-content.tsx b/web/components/analytics/custom-analytics/main-content.tsx index 5cfd15482..3c199f807 100644 --- a/web/components/analytics/custom-analytics/main-content.tsx +++ b/web/components/analytics/custom-analytics/main-content.tsx @@ -8,7 +8,7 @@ import { Button, Loader } from "@plane/ui"; // helpers import { convertResponseToBarGraphData } from "helpers/analytics.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse } from "types"; +import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types"; // fetch-keys import { ANALYTICS } from "constants/fetch-keys"; diff --git a/web/components/analytics/custom-analytics/select-bar.tsx b/web/components/analytics/custom-analytics/select-bar.tsx index f3d7a9993..19f83e40b 100644 --- a/web/components/analytics/custom-analytics/select-bar.tsx +++ b/web/components/analytics/custom-analytics/select-bar.tsx @@ -1,13 +1,11 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Control, Controller, UseFormSetValue } from "react-hook-form"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useProject } from "hooks/store"; // components import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics"; // types -import { IAnalyticsParams } from "types"; +import { IAnalyticsParams } from "@plane/types"; type Props = { control: Control; @@ -20,12 +18,7 @@ type Props = { export const CustomAnalyticsSelectBar: React.FC = observer((props) => { const { control, setValue, params, fullScreen, isProjectLevel } = props; - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { project: projectStore } = useMobxStore(); - - const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; + const { workspaceProjectIds: workspaceProjectIds } = useProject(); return (
= observer((props) => { name="project" control={control} render={({ field: { value, onChange } }) => ( - + )} />
diff --git a/web/components/analytics/custom-analytics/select/project.tsx b/web/components/analytics/custom-analytics/select/project.tsx index 7251c5073..ee3dce6d6 100644 --- a/web/components/analytics/custom-analytics/select/project.tsx +++ b/web/components/analytics/custom-analytics/select/project.tsx @@ -1,25 +1,33 @@ +import { observer } from "mobx-react-lite"; +// hooks +import { useProject } from "hooks/store"; // ui import { CustomSearchSelect } from "@plane/ui"; -// types -import { IProject } from "types"; type Props = { value: string[] | undefined; onChange: (val: string[] | null) => void; - projects: IProject[] | undefined; + projectIds: string[] | undefined; }; -export const SelectProject: React.FC = ({ value, onChange, projects }) => { - const options = projects?.map((project) => ({ - value: project.id, - query: project.name + project.identifier, - content: ( -
- {project.identifier} - {project.name} -
- ), - })); +export const SelectProject: React.FC = observer((props) => { + const { value, onChange, projectIds } = props; + const { getProjectById } = useProject(); + + const options = projectIds?.map((projectId) => { + const projectDetails = getProjectById(projectId); + + return { + value: projectDetails?.id, + query: `${projectDetails?.name} ${projectDetails?.identifier}`, + content: ( +
+ {projectDetails?.identifier} + {projectDetails?.name} +
+ ), + }; + }); return ( = ({ value, onChange, projects }) => options={options} label={ value && value.length > 0 - ? projects - ?.filter((p) => value.includes(p.id)) - .map((p) => p.identifier) + ? projectIds + ?.filter((p) => value.includes(p)) + .map((p) => getProjectById(p)?.name) .join(", ") : "All projects" } @@ -38,4 +46,4 @@ export const SelectProject: React.FC = ({ value, onChange, projects }) => multiple /> ); -}; +}); diff --git a/web/components/analytics/custom-analytics/select/segment.tsx b/web/components/analytics/custom-analytics/select/segment.tsx index 4efc6a211..b45c1fa55 100644 --- a/web/components/analytics/custom-analytics/select/segment.tsx +++ b/web/components/analytics/custom-analytics/select/segment.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; // ui import { CustomSelect } from "@plane/ui"; // types -import { IAnalyticsParams, TXAxisValues } from "types"; +import { IAnalyticsParams, TXAxisValues } from "@plane/types"; // constants import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; diff --git a/web/components/analytics/custom-analytics/select/x-axis.tsx b/web/components/analytics/custom-analytics/select/x-axis.tsx index 66582a1e9..237582ba0 100644 --- a/web/components/analytics/custom-analytics/select/x-axis.tsx +++ b/web/components/analytics/custom-analytics/select/x-axis.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; // ui import { CustomSelect } from "@plane/ui"; // types -import { IAnalyticsParams, TXAxisValues } from "types"; +import { IAnalyticsParams, TXAxisValues } from "@plane/types"; // constants import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; diff --git a/web/components/analytics/custom-analytics/select/y-axis.tsx b/web/components/analytics/custom-analytics/select/y-axis.tsx index 3f7348cce..604457945 100644 --- a/web/components/analytics/custom-analytics/select/y-axis.tsx +++ b/web/components/analytics/custom-analytics/select/y-axis.tsx @@ -1,7 +1,7 @@ // ui import { CustomSelect } from "@plane/ui"; // types -import { TYAxisValues } from "types"; +import { TYAxisValues } from "@plane/types"; // constants import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; diff --git a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx index 41770eec8..d09e8def4 100644 --- a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx +++ b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx @@ -1,65 +1,74 @@ +import { observer } from "mobx-react-lite"; +// hooks +import { useProject } from "hooks/store"; // icons import { Contrast, LayoutGrid, Users } from "lucide-react"; // helpers import { renderEmoji } from "helpers/emoji.helper"; import { truncateText } from "helpers/string.helper"; -// types -import { IProject } from "types"; type Props = { - projects: IProject[]; + projectIds: string[]; }; -export const CustomAnalyticsSidebarProjectsList: React.FC = (props) => { - const { projects } = props; +export const CustomAnalyticsSidebarProjectsList: React.FC = observer((props) => { + const { projectIds } = props; + + const { getProjectById } = useProject(); return (

Selected Projects

- {projects.map((project) => ( -
-
- {project.emoji ? ( - {renderEmoji(project.emoji)} - ) : project.icon_prop ? ( -
{renderEmoji(project.icon_prop)}
- ) : ( - - {project?.name.charAt(0)} - - )} -
-

{truncateText(project.name, 20)}

- ({project.identifier}) -
-
-
-
-
- -
Total members
-
- {project.total_members} + {projectIds.map((projectId) => { + const project = getProjectById(projectId); + + if (!project) return; + + return ( +
+
+ {project.emoji ? ( + {renderEmoji(project.emoji)} + ) : project.icon_prop ? ( +
{renderEmoji(project.icon_prop)}
+ ) : ( + + {project?.name.charAt(0)} + + )} +
+

{truncateText(project.name, 20)}

+ ({project.identifier}) +
-
-
- -
Total cycles
+
+
+
+ +
Total members
+
+ {project.total_members}
- {project.total_cycles} -
-
-
- -
Total modules
+
+
+ +
Total cycles
+
+ {project.total_cycles} +
+
+
+ +
Total modules
+
+ {project.total_modules}
- {project.total_modules}
-
- ))} + ); + })}
); -}; +}); diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx index ac75be686..d46cad191 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx @@ -1,7 +1,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useCycle, useModule, useProject } from "hooks/store"; // helpers import { renderEmoji } from "helpers/emoji.helper"; import { renderFormattedDate } from "helpers/date-time.helper"; @@ -10,16 +10,15 @@ import { NETWORK_CHOICES } from "constants/project"; export const CustomAnalyticsSidebarHeader = observer(() => { const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { projectId, cycleId, moduleId } = router.query; - const { cycle: cycleStore, module: moduleStore, project: projectStore } = useMobxStore(); + const { getProjectById } = useProject(); + const { getCycleById } = useCycle(); + const { getModuleById } = useModule(); - const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined; - const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined; - const projectDetails = - workspaceSlug && projectId - ? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString()) - : undefined; + const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; + const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined; + const projectDetails = projectId ? getProjectById(projectId.toString()) : undefined; return ( <> diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx index 951ed3602..59013a3e3 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx @@ -5,8 +5,8 @@ import { mutate } from "swr"; // services import { AnalyticsService } from "services/analytics.service"; // hooks +import { useCycle, useModule, useProject, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; -import { useMobxStore } from "lib/mobx/store-provider"; // components import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics"; // ui @@ -16,7 +16,7 @@ import { CalendarDays, Download, RefreshCw } from "lucide-react"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "types"; +import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types"; // fetch-keys import { ANALYTICS } from "constants/fetch-keys"; @@ -29,172 +29,167 @@ type Props = { const analyticsService = new AnalyticsService(); -export const CustomAnalyticsSidebar: React.FC = observer( - ({ analytics, params, fullScreen, isProjectLevel = false }) => { - const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; +export const CustomAnalyticsSidebar: React.FC = observer((props) => { + const { analytics, params, fullScreen, isProjectLevel = false } = props; + // router + const router = useRouter(); + const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + // toast alert + const { setToastAlert } = useToast(); + // store hooks + const { currentUser } = useUser(); + const { workspaceProjectIds, getProjectById } = useProject(); + const { fetchCycleDetails, getCycleById } = useCycle(); + const { fetchModuleDetails, getModuleById } = useModule(); - const { setToastAlert } = useToast(); + const projectDetails = projectId ? getProjectById(projectId.toString()) ?? undefined : undefined; - const { user: userStore, project: projectStore, cycle: cycleStore, module: moduleStore } = useMobxStore(); + const trackExportAnalytics = () => { + if (!currentUser) return; - const user = userStore.currentUser; - - const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined; - const projectDetails = - workspaceSlug && projectId - ? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString()) ?? undefined - : undefined; - - const trackExportAnalytics = () => { - if (!user) return; - - const eventPayload: any = { - workspaceSlug: workspaceSlug?.toString(), - params: { - x_axis: params.x_axis, - y_axis: params.y_axis, - group: params.segment, - project: params.project, - }, - }; - - if (projectDetails) { - const workspaceDetails = projectDetails.workspace as IWorkspace; - - eventPayload.workspaceId = workspaceDetails.id; - eventPayload.workspaceName = workspaceDetails.name; - eventPayload.projectId = projectDetails.id; - eventPayload.projectIdentifier = projectDetails.identifier; - eventPayload.projectName = projectDetails.name; - } - - if (cycleDetails || moduleDetails) { - const details = cycleDetails || moduleDetails; - - eventPayload.workspaceId = details?.workspace_detail?.id; - eventPayload.workspaceName = details?.workspace_detail?.name; - eventPayload.projectId = details?.project_detail.id; - eventPayload.projectIdentifier = details?.project_detail.identifier; - eventPayload.projectName = details?.project_detail.name; - } - - if (cycleDetails) { - eventPayload.cycleId = cycleDetails.id; - eventPayload.cycleName = cycleDetails.name; - } - - if (moduleDetails) { - eventPayload.moduleId = moduleDetails.id; - eventPayload.moduleName = moduleDetails.name; - } - }; - - const exportAnalytics = () => { - if (!workspaceSlug) return; - - const data: IExportAnalyticsFormData = { + const eventPayload: any = { + workspaceSlug: workspaceSlug?.toString(), + params: { x_axis: params.x_axis, y_axis: params.y_axis, - }; - - if (params.segment) data.segment = params.segment; - if (params.project) data.project = params.project; - - analyticsService - .exportAnalytics(workspaceSlug.toString(), data) - .then((res) => { - setToastAlert({ - type: "success", - title: "Success!", - message: res.message, - }); - - trackExportAnalytics(); - }) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "There was some error in exporting the analytics. Please try again.", - }) - ); + group: params.segment, + project: params.project, + }, }; - const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined; - const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined; + if (projectDetails) { + const workspaceDetails = projectDetails.workspace as IWorkspace; - // fetch cycle details - useEffect(() => { - if (!workspaceSlug || !projectId || !cycleId || cycleDetails) return; + eventPayload.workspaceId = workspaceDetails.id; + eventPayload.workspaceName = workspaceDetails.name; + eventPayload.projectId = projectDetails.id; + eventPayload.projectIdentifier = projectDetails.identifier; + eventPayload.projectName = projectDetails.name; + } - cycleStore.fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString()); - }, [cycleId, cycleDetails, cycleStore, projectId, workspaceSlug]); + if (cycleDetails || moduleDetails) { + const details = cycleDetails || moduleDetails; - // fetch module details - useEffect(() => { - if (!workspaceSlug || !projectId || !moduleId || moduleDetails) return; + eventPayload.workspaceId = details?.workspace_detail?.id; + eventPayload.workspaceName = details?.workspace_detail?.name; + eventPayload.projectId = details?.project_detail.id; + eventPayload.projectIdentifier = details?.project_detail.identifier; + eventPayload.projectName = details?.project_detail.name; + } - moduleStore.fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString()); - }, [moduleId, moduleDetails, moduleStore, projectId, workspaceSlug]); + if (cycleDetails) { + eventPayload.cycleId = cycleDetails.id; + eventPayload.cycleName = cycleDetails.name; + } - const selectedProjects = params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id); + if (moduleDetails) { + eventPayload.moduleId = moduleDetails.id; + eventPayload.moduleName = moduleDetails.name; + } + }; - return ( -
-
+ const exportAnalytics = () => { + if (!workspaceSlug) return; + + const data: IExportAnalyticsFormData = { + x_axis: params.x_axis, + y_axis: params.y_axis, + }; + + if (params.segment) data.segment = params.segment; + if (params.project) data.project = params.project; + + analyticsService + .exportAnalytics(workspaceSlug.toString(), data) + .then((res) => { + setToastAlert({ + type: "success", + title: "Success!", + message: res.message, + }); + + trackExportAnalytics(); + }) + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "There was some error in exporting the analytics. Please try again.", + }) + ); + }; + + const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; + const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined; + + // fetch cycle details + useEffect(() => { + if (!workspaceSlug || !projectId || !cycleId || cycleDetails) return; + + fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString()); + }, [cycleId, cycleDetails, fetchCycleDetails, projectId, workspaceSlug]); + + // fetch module details + useEffect(() => { + if (!workspaceSlug || !projectId || !moduleId || moduleDetails) return; + + fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString()); + }, [moduleId, moduleDetails, fetchModuleDetails, projectId, workspaceSlug]); + + const selectedProjects = params.project && params.project.length > 0 ? params.project : workspaceProjectIds; + + return ( +
+
+
+ + {analytics ? analytics.total : "..."} Issues +
+ {isProjectLevel && (
- - {analytics ? analytics.total : "..."} Issues + + {renderFormattedDate( + (cycleId + ? cycleDetails?.created_at + : moduleId + ? moduleDetails?.created_at + : projectDetails?.created_at) ?? "" + )}
- {isProjectLevel && ( -
- - {renderFormattedDate( - (cycleId - ? cycleDetails?.created_at - : moduleId - ? moduleDetails?.created_at - : projectDetails?.created_at) ?? "" - )} -
- )} -
-
- {fullScreen ? ( - <> - {!isProjectLevel && selectedProjects && selectedProjects.length > 0 && ( - selectedProjects.includes(p.id)) ?? []} - /> - )} - - - ) : null} -
-
- - -
+ )}
- ); - } -); +
+ {fullScreen ? ( + <> + {!isProjectLevel && selectedProjects && selectedProjects.length > 0 && ( + + )} + + + ) : null} +
+
+ + +
+
+ ); +}); diff --git a/web/components/analytics/custom-analytics/table.tsx b/web/components/analytics/custom-analytics/table.tsx index 2066292c8..c09f26d76 100644 --- a/web/components/analytics/custom-analytics/table.tsx +++ b/web/components/analytics/custom-analytics/table.tsx @@ -5,7 +5,7 @@ import { PriorityIcon } from "@plane/ui"; // helpers import { generateBarColor, generateDisplayName } from "helpers/analytics.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "types"; +import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "@plane/types"; // constants import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; diff --git a/web/components/analytics/project-modal/main-content.tsx b/web/components/analytics/project-modal/main-content.tsx index 55ed1d403..09423e6dd 100644 --- a/web/components/analytics/project-modal/main-content.tsx +++ b/web/components/analytics/project-modal/main-content.tsx @@ -4,7 +4,7 @@ import { Tab } from "@headlessui/react"; // components import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; // types -import { ICycle, IModule, IProject } from "types"; +import { ICycle, IModule, IProject } from "@plane/types"; // constants import { ANALYTICS_TABS } from "constants/analytics"; diff --git a/web/components/analytics/project-modal/modal.tsx b/web/components/analytics/project-modal/modal.tsx index 6dfbfdd6b..a4b82c4b6 100644 --- a/web/components/analytics/project-modal/modal.tsx +++ b/web/components/analytics/project-modal/modal.tsx @@ -5,7 +5,7 @@ import { Dialog, Transition } from "@headlessui/react"; // components import { ProjectAnalyticsModalHeader, ProjectAnalyticsModalMainContent } from "components/analytics"; // types -import { ICycle, IModule, IProject } from "types"; +import { ICycle, IModule, IProject } from "@plane/types"; type Props = { isOpen: boolean; diff --git a/web/components/analytics/scope-and-demand/demand.tsx b/web/components/analytics/scope-and-demand/demand.tsx index df679fbc5..2ff438a39 100644 --- a/web/components/analytics/scope-and-demand/demand.tsx +++ b/web/components/analytics/scope-and-demand/demand.tsx @@ -1,7 +1,7 @@ // icons import { Triangle } from "lucide-react"; // types -import { IDefaultAnalyticsResponse, TStateGroups } from "types"; +import { IDefaultAnalyticsResponse, TStateGroups } from "@plane/types"; // constants import { STATE_GROUP_COLORS } from "constants/state"; diff --git a/web/components/analytics/scope-and-demand/scope.tsx b/web/components/analytics/scope-and-demand/scope.tsx index 4c69a23c5..ea1a51937 100644 --- a/web/components/analytics/scope-and-demand/scope.tsx +++ b/web/components/analytics/scope-and-demand/scope.tsx @@ -3,7 +3,7 @@ import { BarGraph, ProfileEmptyState } from "components/ui"; // image import emptyBarGraph from "public/empty-state/empty_bar_graph.svg"; // types -import { IDefaultAnalyticsResponse } from "types"; +import { IDefaultAnalyticsResponse } from "@plane/types"; type Props = { defaultAnalytics: IDefaultAnalyticsResponse; diff --git a/web/components/analytics/scope-and-demand/year-wise-issues.tsx b/web/components/analytics/scope-and-demand/year-wise-issues.tsx index aec15d9ac..2a62c99d4 100644 --- a/web/components/analytics/scope-and-demand/year-wise-issues.tsx +++ b/web/components/analytics/scope-and-demand/year-wise-issues.tsx @@ -3,7 +3,7 @@ import { LineGraph, ProfileEmptyState } from "components/ui"; // image import emptyGraph from "public/empty-state/empty_graph.svg"; // types -import { IDefaultAnalyticsResponse } from "types"; +import { IDefaultAnalyticsResponse } from "@plane/types"; // constants import { MONTHS_LIST } from "constants/calendar"; diff --git a/web/components/api-token/delete-token-modal.tsx b/web/components/api-token/delete-token-modal.tsx index ed61d3546..993289c10 100644 --- a/web/components/api-token/delete-token-modal.tsx +++ b/web/components/api-token/delete-token-modal.tsx @@ -9,7 +9,7 @@ import useToast from "hooks/use-toast"; // ui import { Button } from "@plane/ui"; // types -import { IApiToken } from "types/api_token"; +import { IApiToken } from "@plane/types"; // fetch-keys import { API_TOKENS_LIST } from "constants/fetch-keys"; diff --git a/web/components/api-token/modal/create-token-modal.tsx b/web/components/api-token/modal/create-token-modal.tsx index 5df1275ba..b3fc3df78 100644 --- a/web/components/api-token/modal/create-token-modal.tsx +++ b/web/components/api-token/modal/create-token-modal.tsx @@ -12,7 +12,7 @@ import { CreateApiTokenForm, GeneratedTokenDetails } from "components/api-token" import { csvDownload } from "helpers/download.helper"; import { renderFormattedDate } from "helpers/date-time.helper"; // types -import { IApiToken } from "types/api_token"; +import { IApiToken } from "@plane/types"; // fetch-keys import { API_TOKENS_LIST } from "constants/fetch-keys"; diff --git a/web/components/api-token/modal/form.tsx b/web/components/api-token/modal/form.tsx index a04968dac..ae7717b39 100644 --- a/web/components/api-token/modal/form.tsx +++ b/web/components/api-token/modal/form.tsx @@ -11,7 +11,7 @@ import { Button, CustomSelect, Input, TextArea, ToggleSwitch } from "@plane/ui"; // helpers import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import { IApiToken } from "types/api_token"; +import { IApiToken } from "@plane/types"; type Props = { handleClose: () => void; @@ -175,8 +175,8 @@ export const CreateApiTokenForm: React.FC = (props) => { {value === "custom" ? "Custom date" : selectedOption - ? selectedOption.label - : "Set expiration date"} + ? selectedOption.label + : "Set expiration date"}
} value={value} @@ -219,8 +219,8 @@ export const CreateApiTokenForm: React.FC = (props) => { ? `Expires ${renderFormattedDate(customDate)}` : null : watch("expired_at") - ? `Expires ${getExpiryDate(watch("expired_at") ?? "")}` - : null} + ? `Expires ${getExpiryDate(watch("expired_at") ?? "")}` + : null} )}
diff --git a/web/components/api-token/modal/generated-token-details.tsx b/web/components/api-token/modal/generated-token-details.tsx index 1ffa69a78..f28ea3481 100644 --- a/web/components/api-token/modal/generated-token-details.tsx +++ b/web/components/api-token/modal/generated-token-details.tsx @@ -7,7 +7,7 @@ import { Button, Tooltip } from "@plane/ui"; import { renderFormattedDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; // types -import { IApiToken } from "types/api_token"; +import { IApiToken } from "@plane/types"; type Props = { handleClose: () => void; diff --git a/web/components/api-token/token-list-item.tsx b/web/components/api-token/token-list-item.tsx index 37bb968d3..2de731222 100644 --- a/web/components/api-token/token-list-item.tsx +++ b/web/components/api-token/token-list-item.tsx @@ -7,7 +7,7 @@ import { Tooltip } from "@plane/ui"; // helpers import { renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper"; // types -import { IApiToken } from "types/api_token"; +import { IApiToken } from "@plane/types"; type Props = { token: IApiToken; diff --git a/web/components/auth-screens/not-authorized-view.tsx b/web/components/auth-screens/not-authorized-view.tsx index f0a3e3d90..8d9d6ecd4 100644 --- a/web/components/auth-screens/not-authorized-view.tsx +++ b/web/components/auth-screens/not-authorized-view.tsx @@ -1,12 +1,12 @@ import React from "react"; -// next import Link from "next/link"; import Image from "next/image"; import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +// hooks +import { useUser } from "hooks/store"; // layouts import DefaultLayout from "layouts/default-layout"; -// hooks -import useUser from "hooks/use-user"; // images import ProjectNotAuthorizedImg from "public/auth/project-not-authorized.svg"; import WorkspaceNotAuthorizedImg from "public/auth/workspace-not-authorized.svg"; @@ -16,8 +16,9 @@ type Props = { type: "project" | "workspace"; }; -export const NotAuthorizedView: React.FC = ({ actionButton, type }) => { - const { user } = useUser(); +export const NotAuthorizedView: React.FC = observer((props) => { + const { actionButton, type } = props; + const { currentUser } = useUser(); const { query } = useRouter(); const { next_path } = query; @@ -35,9 +36,9 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => {

Oops! You are not authorized to view this page

- {user ? ( + {currentUser ? (

- You have signed in as {user.email}.
+ You have signed in as {currentUser.email}.
Sign in {" "} @@ -58,4 +59,4 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => {

); -}; +}); diff --git a/web/components/auth-screens/project/join-project.tsx b/web/components/auth-screens/project/join-project.tsx index 7ee4feacd..35b0b9b49 100644 --- a/web/components/auth-screens/project/join-project.tsx +++ b/web/components/auth-screens/project/join-project.tsx @@ -1,9 +1,8 @@ import { useState } from "react"; import Image from "next/image"; import { useRouter } from "next/router"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; +// hooks +import { useProject, useUser } from "hooks/store"; // ui import { Button } from "@plane/ui"; // icons @@ -12,12 +11,13 @@ import { ClipboardList } from "lucide-react"; import JoinProjectImg from "public/auth/project-not-authorized.svg"; export const JoinProject: React.FC = () => { + // states const [isJoiningProject, setIsJoiningProject] = useState(false); - + // store hooks const { - project: projectStore, - user: { joinProject }, - }: RootStore = useMobxStore(); + membership: { joinProject }, + } = useUser(); + const { fetchProjects } = useProject(); const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -28,12 +28,8 @@ export const JoinProject: React.FC = () => { setIsJoiningProject(true); joinProject(workspaceSlug.toString(), [projectId.toString()]) - .then(() => { - projectStore.fetchProjects(workspaceSlug.toString()); - }) - .finally(() => { - setIsJoiningProject(false); - }); + .then(() => fetchProjects(workspaceSlug.toString())) + .finally(() => setIsJoiningProject(false)); }; return ( diff --git a/web/components/automation/auto-archive-automation.tsx b/web/components/automation/auto-archive-automation.tsx index 6471bc9cf..3d5f6352e 100644 --- a/web/components/automation/auto-archive-automation.tsx +++ b/web/components/automation/auto-archive-automation.tsx @@ -1,17 +1,16 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useProject, useUser } from "hooks/store"; // component import { CustomSelect, Loader, ToggleSwitch } from "@plane/ui"; import { SelectMonthModal } from "components/automation"; // icon import { ArchiveRestore } from "lucide-react"; // constants -import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; +import { EUserProjectRoles, PROJECT_AUTOMATION_MONTHS } from "constants/project"; // types -import { IProject } from "types"; -import { EUserWorkspaceRoles } from "constants/workspace"; +import { IProject } from "@plane/types"; type Props = { handleChange: (formData: Partial) => Promise; @@ -23,13 +22,13 @@ export const AutoArchiveAutomation: React.FC = observer((props) => { const { handleChange } = props; // states const [monthModal, setmonthModal] = useState(false); + // store hooks + const { + membership: { currentProjectRole }, + } = useUser(); + const { currentProjectDetails } = useProject(); - const { user: userStore, project: projectStore } = useMobxStore(); - - const projectDetails = projectStore.currentProjectDetails; - const userRole = userStore.currentProjectRole; - - const isAdmin = userRole === EUserWorkspaceRoles.ADMIN; + const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; return ( <> @@ -54,24 +53,28 @@ export const AutoArchiveAutomation: React.FC = observer((props) => {
- projectDetails?.archive_in === 0 ? handleChange({ archive_in: 1 }) : handleChange({ archive_in: 0 }) + currentProjectDetails?.archive_in === 0 + ? handleChange({ archive_in: 1 }) + : handleChange({ archive_in: 0 }) } size="sm" disabled={!isAdmin} />
- {projectDetails ? ( - projectDetails.archive_in !== 0 && ( + {currentProjectDetails ? ( + currentProjectDetails.archive_in !== 0 && (
Auto-archive issues that are closed for
{ handleChange({ archive_in: val }); }} diff --git a/web/components/automation/auto-close-automation.tsx b/web/components/automation/auto-close-automation.tsx index d21eb8b80..49dd77e10 100644 --- a/web/components/automation/auto-close-automation.tsx +++ b/web/components/automation/auto-close-automation.tsx @@ -1,17 +1,16 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useProject, useProjectState, useUser } from "hooks/store"; // component import { SelectMonthModal } from "components/automation"; import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleCircleIcon, Loader } from "@plane/ui"; // icons import { ArchiveX } from "lucide-react"; // types -import { IProject } from "types"; -// fetch keys -import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; -import { EUserWorkspaceRoles } from "constants/workspace"; +import { IProject } from "@plane/types"; +// constants +import { EUserProjectRoles, PROJECT_AUTOMATION_MONTHS } from "constants/project"; type Props = { handleChange: (formData: Partial) => Promise; @@ -21,15 +20,16 @@ export const AutoCloseAutomation: React.FC = observer((props) => { const { handleChange } = props; // states const [monthModal, setmonthModal] = useState(false); + // store hooks + const { + membership: { currentProjectRole }, + } = useUser(); + const { currentProjectDetails } = useProject(); + const { projectStates } = useProjectState(); - const { user: userStore, project: projectStore, projectState: projectStateStore } = useMobxStore(); - - const userRole = userStore.currentProjectRole; - const projectDetails = projectStore.currentProjectDetails; // const stateGroups = projectStateStore.groupedProjectStates ?? undefined; - const states = projectStateStore.projectStates; - const options = states + const options = projectStates ?.filter((state) => state.group === "cancelled") .map((state) => ({ value: state.id, @@ -44,17 +44,17 @@ export const AutoCloseAutomation: React.FC = observer((props) => { const multipleOptions = (options ?? []).length > 1; - const defaultState = states?.find((s) => s.group === "cancelled")?.id || null; + const defaultState = projectStates?.find((s) => s.group === "cancelled")?.id || null; - const selectedOption = states?.find((s) => s.id === projectDetails?.default_state ?? defaultState); - const currentDefaultState = states?.find((s) => s.id === defaultState); + const selectedOption = projectStates?.find((s) => s.id === currentProjectDetails?.default_state ?? defaultState); + const currentDefaultState = projectStates?.find((s) => s.id === defaultState); const initialValues: Partial = { close_in: 1, default_state: defaultState, }; - const isAdmin = userRole === EUserWorkspaceRoles.ADMIN; + const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; return ( <> @@ -79,9 +79,9 @@ export const AutoCloseAutomation: React.FC = observer((props) => {
- projectDetails?.close_in === 0 + currentProjectDetails?.close_in === 0 ? handleChange({ close_in: 1, default_state: defaultState }) : handleChange({ close_in: 0, default_state: null }) } @@ -90,16 +90,18 @@ export const AutoCloseAutomation: React.FC = observer((props) => { />
- {projectDetails ? ( - projectDetails.close_in !== 0 && ( + {currentProjectDetails ? ( + currentProjectDetails.close_in !== 0 && (
Auto-close issues that are inactive for
{ handleChange({ close_in: val }); }} @@ -118,7 +120,7 @@ export const AutoCloseAutomation: React.FC = observer((props) => { className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80" onClick={() => setmonthModal(true)} > - Customise Time Range + Customize Time Range @@ -129,7 +131,7 @@ export const AutoCloseAutomation: React.FC = observer((props) => {
Auto-close Status
{selectedOption ? ( diff --git a/web/components/automation/select-month-modal.tsx b/web/components/automation/select-month-modal.tsx index eff42bb2d..1d306bb04 100644 --- a/web/components/automation/select-month-modal.tsx +++ b/web/components/automation/select-month-modal.tsx @@ -7,7 +7,7 @@ import { Dialog, Transition } from "@headlessui/react"; // ui import { Button, Input } from "@plane/ui"; // types -import type { IProject } from "types"; +import type { IProject } from "@plane/types"; // types type Props = { diff --git a/web/components/command-palette/actions/help-actions.tsx b/web/components/command-palette/actions/help-actions.tsx index 859a6d23a..4aaaab33a 100644 --- a/web/components/command-palette/actions/help-actions.tsx +++ b/web/components/command-palette/actions/help-actions.tsx @@ -1,7 +1,7 @@ import { Command } from "cmdk"; import { FileText, GithubIcon, MessageSquare, Rocket } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication } from "hooks/store"; // ui import { DiscordIcon } from "@plane/ui"; @@ -14,7 +14,7 @@ export const CommandPaletteHelpActions: React.FC = (props) => { const { commandPalette: { toggleShortcutModal }, - } = useMobxStore(); + } = useApplication(); return ( diff --git a/web/components/command-palette/actions/issue-actions/actions-list.tsx b/web/components/command-palette/actions/issue-actions/actions-list.tsx index 8e188df7b..55f72c85d 100644 --- a/web/components/command-palette/actions/issue-actions/actions-list.tsx +++ b/web/components/command-palette/actions/issue-actions/actions-list.tsx @@ -2,8 +2,8 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Command } from "cmdk"; import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2 } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication, useUser, useIssues } from "hooks/store"; // hooks import useToast from "hooks/use-toast"; // ui @@ -11,11 +11,12 @@ import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; type Props = { closePalette: () => void; - issueDetails: IIssue | undefined; + issueDetails: TIssue | undefined; pages: string[]; setPages: (pages: string[]) => void; setPlaceholder: (placeholder: string) => void; @@ -28,15 +29,17 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { + issues: { updateIssue }, + } = useIssues(EIssuesStoreType.PROJECT); const { commandPalette: { toggleCommandPaletteModal, toggleDeleteIssueModal }, - projectIssues: { updateIssue }, - user: { currentUser }, - } = useMobxStore(); + } = useApplication(); + const { currentUser } = useUser(); const { setToastAlert } = useToast(); - const handleUpdateIssue = async (formData: Partial) => { + const handleUpdateIssue = async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueDetails) return; const payload = { ...formData }; @@ -49,12 +52,12 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { if (!issueDetails || !assignee) return; closePalette(); - const updatedAssignees = issueDetails.assignees ?? []; + const updatedAssignees = issueDetails.assignee_ids ?? []; if (updatedAssignees.includes(assignee)) updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); else updatedAssignees.push(assignee); - handleUpdateIssue({ assignees: updatedAssignees }); + handleUpdateIssue({ assignee_ids: updatedAssignees }); }; const deleteIssue = () => { @@ -130,7 +133,7 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { className="focus:outline-none" >
- {issueDetails?.assignees.includes(currentUser?.id ?? "") ? ( + {issueDetails?.assignee_ids.includes(currentUser?.id ?? "") ? ( <> Un-assign from me diff --git a/web/components/command-palette/actions/issue-actions/change-assignee.tsx b/web/components/command-palette/actions/issue-actions/change-assignee.tsx index 57af2b62a..96fba41f6 100644 --- a/web/components/command-palette/actions/issue-actions/change-assignee.tsx +++ b/web/components/command-palette/actions/issue-actions/change-assignee.tsx @@ -3,15 +3,16 @@ import { observer } from "mobx-react-lite"; import { Command } from "cmdk"; import { Check } from "lucide-react"; // mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { useIssues, useMember } from "hooks/store"; // ui import { Avatar } from "@plane/ui"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; type Props = { closePalette: () => void; - issue: IIssue; + issue: TIssue; }; export const ChangeIssueAssignee: React.FC = observer((props) => { @@ -21,30 +22,40 @@ export const ChangeIssueAssignee: React.FC = observer((props) => { const { workspaceSlug, projectId } = router.query; // store const { - projectIssues: { updateIssue }, - projectMember: { projectMembers }, - } = useMobxStore(); + issues: { updateIssue }, + } = useIssues(EIssuesStoreType.PROJECT); + const { + project: { projectMemberIds, getProjectMemberDetails }, + } = useMember(); const options = - projectMembers?.map(({ member }) => ({ - value: member.id, - query: member.display_name, - content: ( - <> -
- - {member.display_name} -
- {issue.assignees.includes(member.id) && ( -
- -
- )} - - ), - })) ?? []; + projectMemberIds?.map((userId) => { + const memberDetails = getProjectMemberDetails(userId); - const handleUpdateIssue = async (formData: Partial) => { + return { + value: `${memberDetails?.member?.id}`, + query: `${memberDetails?.member?.display_name}`, + content: ( + <> +
+ + {memberDetails?.member?.display_name} +
+ {issue.assignee_ids.includes(memberDetails?.member?.id ?? "") && ( +
+ +
+ )} + + ), + }; + }) ?? []; + + const handleUpdateIssue = async (formData: Partial) => { if (!workspaceSlug || !projectId || !issue) return; const payload = { ...formData }; @@ -54,18 +65,18 @@ export const ChangeIssueAssignee: React.FC = observer((props) => { }; const handleIssueAssignees = (assignee: string) => { - const updatedAssignees = issue.assignees ?? []; + const updatedAssignees = issue.assignee_ids ?? []; if (updatedAssignees.includes(assignee)) updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); else updatedAssignees.push(assignee); - handleUpdateIssue({ assignees: updatedAssignees }); + handleUpdateIssue({ assignee_ids: updatedAssignees }); closePalette(); }; return ( <> - {options.map((option: any) => ( + {options.map((option) => ( handleIssueAssignees(option.value)} diff --git a/web/components/command-palette/actions/issue-actions/change-priority.tsx b/web/components/command-palette/actions/issue-actions/change-priority.tsx index 81b9f7ae9..8d1c48261 100644 --- a/web/components/command-palette/actions/issue-actions/change-priority.tsx +++ b/web/components/command-palette/actions/issue-actions/change-priority.tsx @@ -3,17 +3,17 @@ import { observer } from "mobx-react-lite"; import { Command } from "cmdk"; import { Check } from "lucide-react"; // mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { useIssues } from "hooks/store"; // ui import { PriorityIcon } from "@plane/ui"; // types -import { IIssue, TIssuePriorities } from "types"; +import { TIssue, TIssuePriorities } from "@plane/types"; // constants -import { PRIORITIES } from "constants/project"; +import { EIssuesStoreType, ISSUE_PRIORITIES } from "constants/issue"; type Props = { closePalette: () => void; - issue: IIssue; + issue: TIssue; }; export const ChangeIssuePriority: React.FC = observer((props) => { @@ -23,10 +23,10 @@ export const ChangeIssuePriority: React.FC = observer((props) => { const { workspaceSlug, projectId } = router.query; const { - projectIssues: { updateIssue }, - } = useMobxStore(); + issues: { updateIssue }, + } = useIssues(EIssuesStoreType.PROJECT); - const submitChanges = async (formData: Partial) => { + const submitChanges = async (formData: Partial) => { if (!workspaceSlug || !projectId || !issue) return; const payload = { ...formData }; @@ -42,13 +42,13 @@ export const ChangeIssuePriority: React.FC = observer((props) => { return ( <> - {PRIORITIES.map((priority) => ( - handleIssueState(priority)} className="focus:outline-none"> + {ISSUE_PRIORITIES.map((priority) => ( + handleIssueState(priority.key)} className="focus:outline-none">
- - {priority ?? "None"} + + {priority.title ?? "None"}
-
{priority === issue.priority && }
+
{priority.key === issue.priority && }
))} diff --git a/web/components/command-palette/actions/issue-actions/change-state.tsx b/web/components/command-palette/actions/issue-actions/change-state.tsx index 0ce05bd7b..7841a4a1e 100644 --- a/web/components/command-palette/actions/issue-actions/change-state.tsx +++ b/web/components/command-palette/actions/issue-actions/change-state.tsx @@ -1,33 +1,33 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -// cmdk import { Command } from "cmdk"; +// hooks +import { useProjectState, useIssues } from "hooks/store"; // ui import { Spinner, StateGroupIcon } from "@plane/ui"; // icons import { Check } from "lucide-react"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; type Props = { closePalette: () => void; - issue: IIssue; + issue: TIssue; }; export const ChangeIssueState: React.FC = observer((props) => { const { closePalette, issue } = props; - + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - + // store hooks const { - projectState: { projectStates }, - projectIssues: { updateIssue }, - } = useMobxStore(); + issues: { updateIssue }, + } = useIssues(EIssuesStoreType.PROJECT); + const { projectStates } = useProjectState(); - const submitChanges = async (formData: Partial) => { + const submitChanges = async (formData: Partial) => { if (!workspaceSlug || !projectId || !issue) return; const payload = { ...formData }; @@ -37,7 +37,7 @@ export const ChangeIssueState: React.FC = observer((props) => { }; const handleIssueState = (stateId: string) => { - submitChanges({ state: stateId }); + submitChanges({ state_id: stateId }); closePalette(); }; @@ -51,7 +51,7 @@ export const ChangeIssueState: React.FC = observer((props) => {

{state.name}

-
{state.id === issue.state && }
+
{state.id === issue.state_id && }
)) ) : ( diff --git a/web/components/command-palette/actions/project-actions.tsx b/web/components/command-palette/actions/project-actions.tsx index 1e10b3a46..44b5e6111 100644 --- a/web/components/command-palette/actions/project-actions.tsx +++ b/web/components/command-palette/actions/project-actions.tsx @@ -1,7 +1,7 @@ import { Command } from "cmdk"; import { ContrastIcon, FileText } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication } from "hooks/store"; // ui import { DiceIcon, PhotoFilterIcon } from "@plane/ui"; @@ -14,8 +14,8 @@ export const CommandPaletteProjectActions: React.FC = (props) => { const { commandPalette: { toggleCreateCycleModal, toggleCreateModuleModal, toggleCreatePageModal, toggleCreateViewModal }, - trackEvent: { setTrackElement }, - } = useMobxStore(); + eventTracker: { setTrackElement }, + } = useApplication(); return ( <> diff --git a/web/components/command-palette/actions/search-results.tsx b/web/components/command-palette/actions/search-results.tsx index 791c62656..769a26be7 100644 --- a/web/components/command-palette/actions/search-results.tsx +++ b/web/components/command-palette/actions/search-results.tsx @@ -3,7 +3,7 @@ import { Command } from "cmdk"; // helpers import { commandGroups } from "components/command-palette"; // types -import { IWorkspaceSearchResults } from "types"; +import { IWorkspaceSearchResults } from "@plane/types"; type Props = { closePalette: () => void; diff --git a/web/components/command-palette/actions/theme-actions.tsx b/web/components/command-palette/actions/theme-actions.tsx index f7266a48a..976a63c87 100644 --- a/web/components/command-palette/actions/theme-actions.tsx +++ b/web/components/command-palette/actions/theme-actions.tsx @@ -4,8 +4,8 @@ import { useTheme } from "next-themes"; import { Settings } from "lucide-react"; import { observer } from "mobx-react-lite"; // hooks +import { useUser } from "hooks/store"; import useToast from "hooks/use-toast"; -import { useMobxStore } from "lib/mobx/store-provider"; // constants import { THEME_OPTIONS } from "constants/themes"; @@ -18,9 +18,7 @@ export const CommandPaletteThemeActions: FC = observer((props) => { // states const [mounted, setMounted] = useState(false); // store - const { - user: { updateCurrentUserTheme }, - } = useMobxStore(); + const { updateCurrentUserTheme } = useUser(); // hooks const { setTheme } = useTheme(); const { setToastAlert } = useToast(); diff --git a/web/components/command-palette/command-modal.tsx b/web/components/command-palette/command-modal.tsx index 005e570e7..342827825 100644 --- a/web/components/command-palette/command-modal.tsx +++ b/web/components/command-palette/command-modal.tsx @@ -5,8 +5,8 @@ import { Command } from "cmdk"; import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; import { FolderPlus, Search, Settings } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication, useProject } from "hooks/store"; // services import { WorkspaceService } from "services/workspace.service"; import { IssueService } from "services/issue"; @@ -26,7 +26,7 @@ import { } from "components/command-palette"; import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; // types -import { IWorkspaceSearchResults } from "types"; +import { IWorkspaceSearchResults } from "@plane/types"; // fetch-keys import { ISSUE_DETAILS } from "constants/fetch-keys"; @@ -35,6 +35,8 @@ const workspaceService = new WorkspaceService(); const issueService = new IssueService(); export const CommandModal: React.FC = observer(() => { + // hooks + const { getProjectById } = useProject(); // states const [placeholder, setPlaceholder] = useState("Type a command or search..."); const [resultsCount, setResultsCount] = useState(0); @@ -62,8 +64,8 @@ export const CommandModal: React.FC = observer(() => { toggleCreateIssueModal, toggleCreateProjectModal, }, - trackEvent: { setTrackElement }, - } = useMobxStore(); + eventTracker: { setTrackElement }, + } = useApplication(); // router const router = useRouter(); @@ -135,6 +137,8 @@ export const CommandModal: React.FC = observer(() => { [debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug] // Only call effect if debounced search term changes ); + const projectDetails = getProjectById(issueDetails?.project_id ?? ""); + return ( setSearchTerm("")} as={React.Fragment}> closePalette()}> @@ -188,7 +192,7 @@ export const CommandModal: React.FC = observer(() => { > {issueDetails && (
- {issueDetails.project_detail.identifier}-{issueDetails.sequence_id} {issueDetails.name} + {projectDetails?.identifier}-{issueDetails.sequence_id} {issueDetails.name}
)} {projectId && ( diff --git a/web/components/command-palette/command-pallette.tsx b/web/components/command-palette/command-palette.tsx similarity index 94% rename from web/components/command-palette/command-pallette.tsx rename to web/components/command-palette/command-palette.tsx index 0488455fb..e5f781dd9 100644 --- a/web/components/command-palette/command-pallette.tsx +++ b/web/components/command-palette/command-palette.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import useSWR from "swr"; import { observer } from "mobx-react-lite"; // hooks +import { useApplication, useIssues, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { CommandModal, ShortcutsModal } from "components/command-palette"; @@ -19,8 +20,7 @@ import { copyTextToClipboard } from "helpers/string.helper"; import { IssueService } from "services/issue"; // fetch keys import { ISSUE_DETAILS } from "constants/fetch-keys"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { EIssuesStoreType } from "constants/issue"; // services const issueService = new IssueService(); @@ -28,14 +28,17 @@ const issueService = new IssueService(); export const CommandPalette: FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query; - // store + const { commandPalette, theme: { toggleSidebar }, - user: { currentUser }, - trackEvent: { setTrackElement }, - projectIssues: { removeIssue }, - } = useMobxStore(); + eventTracker: { setTrackElement }, + } = useApplication(); + const { currentUser } = useUser(); + const { + issues: { removeIssue }, + } = useIssues(EIssuesStoreType.PROJECT); + const { toggleCommandPaletteModal, isCreateIssueModalOpen, @@ -214,7 +217,7 @@ export const CommandPalette: FC = observer(() => { isOpen={isCreateIssueModalOpen} handleClose={() => toggleCreateIssueModal(false)} prePopulateData={ - cycleId ? { cycle: cycleId.toString() } : moduleId ? { module: moduleId.toString() } : undefined + cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_id: moduleId.toString() } : undefined } currentStore={createIssueStoreType} /> diff --git a/web/components/command-palette/helpers.tsx b/web/components/command-palette/helpers.tsx index 8bf0c9938..44fc55bbe 100644 --- a/web/components/command-palette/helpers.tsx +++ b/web/components/command-palette/helpers.tsx @@ -6,7 +6,7 @@ import { IWorkspaceIssueSearchResult, IWorkspaceProjectSearchResult, IWorkspaceSearchResult, -} from "types"; +} from "@plane/types"; export const commandGroups: { [key: string]: { diff --git a/web/components/command-palette/index.ts b/web/components/command-palette/index.ts index 192ef8ef9..0d2e042a7 100644 --- a/web/components/command-palette/index.ts +++ b/web/components/command-palette/index.ts @@ -1,5 +1,5 @@ export * from "./actions"; export * from "./shortcuts-modal"; export * from "./command-modal"; -export * from "./command-pallette"; +export * from "./command-palette"; export * from "./helpers"; diff --git a/web/components/common/new-empty-state.tsx b/web/components/common/new-empty-state.tsx index 7bad18734..dbe654e11 100644 --- a/web/components/common/new-empty-state.tsx +++ b/web/components/common/new-empty-state.tsx @@ -19,7 +19,7 @@ type Props = { icon?: any; text: string; onClick: () => void; - } | null; + }; disabled?: boolean; }; diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index 0fd9e90f1..99396dda2 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -1,9 +1,8 @@ import { useRouter } from "next/router"; +import { useEffect } from "react"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -// hook -import useEstimateOption from "hooks/use-estimate-option"; +// store hooks +import { useEstimate, useLabel } from "hooks/store"; // icons import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon } from "@plane/ui"; import { @@ -25,8 +24,7 @@ import { import { renderFormattedDate } from "helpers/date-time.helper"; import { capitalizeFirstLetter } from "helpers/string.helper"; // types -import { IIssueActivity } from "types"; -import { useEffect } from "react"; +import { IIssueActivity } from "@plane/types"; const IssueLink = ({ activity }: { activity: IIssueActivity }) => { const router = useRouter(); @@ -73,11 +71,10 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => { }; const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; workspaceSlug: string }) => { + // store hooks const { - workspace: { labels, fetchWorkspaceLabels }, - } = useMobxStore(); - - const workspaceLabels = labels[workspaceSlug]; + workspace: { workspaceLabels, fetchWorkspaceLabels }, + } = useLabel(); useEffect(() => { if (!workspaceLabels) fetchWorkspaceLabels(workspaceSlug); @@ -94,16 +91,21 @@ const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; works ); }); -const EstimatePoint = ({ point }: { point: string }) => { - const { estimateValue, isEstimateActive } = useEstimateOption(Number(point)); +const EstimatePoint = observer((props: { point: string }) => { + const { point } = props; + const { areEstimatesEnabledForCurrentProject, getEstimatePointValue } = useEstimate(); const currentPoint = Number(point) + 1; + const estimateValue = getEstimatePointValue(Number(point)); + return ( - {isEstimateActive ? estimateValue : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`} + {areEstimatesEnabledForCurrentProject + ? estimateValue + : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`} ); -}; +}); const activityDetails: { [key: string]: { diff --git a/web/components/core/image-picker-popover.tsx b/web/components/core/image-picker-popover.tsx index 41fe05b3f..9f8023833 100644 --- a/web/components/core/image-picker-popover.tsx +++ b/web/components/core/image-picker-popover.tsx @@ -6,8 +6,8 @@ import useSWR from "swr"; import { useDropzone } from "react-dropzone"; import { Tab, Transition, Popover } from "@headlessui/react"; import { Control, Controller } from "react-hook-form"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication, useWorkspace } from "hooks/store"; // services import { FileService } from "services/file.service"; // hooks @@ -45,25 +45,24 @@ const fileService = new FileService(); export const ImagePickerPopover: React.FC = observer((props) => { const { label, value, control, onChange, disabled = false } = props; - + // states const [image, setImage] = useState(null); const [isImageUploading, setIsImageUploading] = useState(false); - const [isOpen, setIsOpen] = useState(false); const [searchParams, setSearchParams] = useState(""); const [formData, setFormData] = useState({ search: "", }); - + // refs const ref = useRef(null); - + // router const router = useRouter(); const { workspaceSlug } = router.query; - + // store hooks const { - workspace: { currentWorkspace }, - appConfig: { envConfig }, - } = useMobxStore(); + config: { envConfig }, + } = useApplication(); + const { currentWorkspace } = useWorkspace(); const { data: unsplashImages, error: unsplashError } = useSWR( `UNSPLASH_IMAGES_${searchParams}`, @@ -119,7 +118,7 @@ export const ImagePickerPopover: React.FC = observer((props) => { if (oldValue && currentWorkspace) fileService.deleteFile(currentWorkspace.id, oldValue); }) .catch((err) => { - console.log(err); + console.error(err); }); }; diff --git a/web/components/core/modals/bulk-delete-issues-modal-item.tsx b/web/components/core/modals/bulk-delete-issues-modal-item.tsx new file mode 100644 index 000000000..8fa8dabda --- /dev/null +++ b/web/components/core/modals/bulk-delete-issues-modal-item.tsx @@ -0,0 +1,38 @@ +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +// hooks +import { useProjectState } from "hooks/store"; + +export const BulkDeleteIssuesModalItem: React.FC = observer((props) => { + const { issue, delete_issue_ids, identifier } = props; + const { getStateById } = useProjectState(); + + const color = getStateById(issue.state_id)?.color; + + return ( + + `flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${ + active ? "bg-custom-background-80 text-custom-text-100" : "" + }` + } + > +
+ + + + {identifier}-{issue.sequence_id} + + {issue.name} +
+
+ ); +}); diff --git a/web/components/core/modals/bulk-delete-issues-modal.tsx b/web/components/core/modals/bulk-delete-issues-modal.tsx index d745e1111..6bb646821 100644 --- a/web/components/core/modals/bulk-delete-issues-modal.tsx +++ b/web/components/core/modals/bulk-delete-issues-modal.tsx @@ -1,22 +1,25 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; +import useSWR from "swr"; import { observer } from "mobx-react-lite"; import { SubmitHandler, useForm } from "react-hook-form"; import { Combobox, Dialog, Transition } from "@headlessui/react"; -import useSWR from "swr"; -// hooks -import { useMobxStore } from "lib/mobx/store-provider"; -import useToast from "hooks/use-toast"; // services import { IssueService } from "services/issue"; +// hooks +import useToast from "hooks/use-toast"; // ui import { Button, LayersIcon } from "@plane/ui"; // icons import { Search } from "lucide-react"; // types -import { IUser, IIssue } from "types"; +import { IUser, TIssue } from "@plane/types"; // fetch keys import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; +// store hooks +import { useProject } from "hooks/store"; +// components +import { BulkDeleteIssuesModalItem } from "./bulk-delete-issues-modal-item"; type FormInput = { delete_issue_ids: string[]; @@ -32,23 +35,17 @@ const issueService = new IssueService(); export const BulkDeleteIssuesModal: React.FC = observer((props) => { const { isOpen, onClose } = props; - // states - const [query, setQuery] = useState(""); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // store hooks - const { - user: { hasPermissionToCurrentProject }, - } = useMobxStore(); + // hooks + const { getProjectById } = useProject(); + // states + const [query, setQuery] = useState(""); // fetching project issues. const { data: issues } = useSWR( - workspaceSlug && projectId && hasPermissionToCurrentProject - ? PROJECT_ISSUES_LIST(workspaceSlug.toString(), projectId.toString()) - : null, - workspaceSlug && projectId && hasPermissionToCurrentProject - ? () => issueService.getIssues(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null, + workspaceSlug && projectId ? () => issueService.getIssues(workspaceSlug as string, projectId as string) : null ); const { setToastAlert } = useToast(); @@ -107,13 +104,15 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { ); }; - const filteredIssues: IIssue[] = + const projectDetails = getProjectById(projectId as string); + + const filteredIssues: TIssue[] = query === "" ? Object.values(issues ?? {}) : Object.values(issues ?? {})?.filter( (issue) => issue.name.toLowerCase().includes(query.toLowerCase()) || - `${issue.project_detail.identifier}-${issue.sequence_id}`.toLowerCase().includes(query.toLowerCase()) + `${projectDetails?.identifier}-${issue.sequence_id}`.toLowerCase().includes(query.toLowerCase()) ) ?? []; return ( @@ -169,34 +168,12 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { )}
    {filteredIssues.map((issue) => ( - - `flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${ - active ? "bg-custom-background-80 text-custom-text-100" : "" - }` - } - > -
    - - - - {issue.project_detail.identifier}-{issue.sequence_id} - - {issue.name} -
    -
    + /> ))}
diff --git a/web/components/core/modals/existing-issues-list-modal.tsx b/web/components/core/modals/existing-issues-list-modal.tsx index 43d8b4f89..ee3144f3b 100644 --- a/web/components/core/modals/existing-issues-list-modal.tsx +++ b/web/components/core/modals/existing-issues-list-modal.tsx @@ -10,7 +10,7 @@ import useDebounce from "hooks/use-debounce"; // ui import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; // types -import { ISearchIssueResponse, TProjectIssuesSearchParams } from "types"; +import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types"; type Props = { isOpen: boolean; diff --git a/web/components/core/modals/link-modal.tsx b/web/components/core/modals/link-modal.tsx index 9f0ec41bc..1c1372e8d 100644 --- a/web/components/core/modals/link-modal.tsx +++ b/web/components/core/modals/link-modal.tsx @@ -7,7 +7,7 @@ import { Dialog, Transition } from "@headlessui/react"; // ui import { Button, Input } from "@plane/ui"; // types -import type { IIssueLink, ILinkDetails, ModuleLink } from "types"; +import type { IIssueLink, ILinkDetails, ModuleLink } from "@plane/types"; type Props = { isOpen: boolean; diff --git a/web/components/core/modals/user-image-upload-modal.tsx b/web/components/core/modals/user-image-upload-modal.tsx index 11bda44ce..6debc2c15 100644 --- a/web/components/core/modals/user-image-upload-modal.tsx +++ b/web/components/core/modals/user-image-upload-modal.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; import { useDropzone } from "react-dropzone"; import { Transition, Dialog } from "@headlessui/react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication } from "hooks/store"; // services import { FileService } from "services/file.service"; // hooks @@ -32,12 +32,12 @@ export const UserImageUploadModal: React.FC = observer((props) => { // states const [image, setImage] = useState(null); const [isImageUploading, setIsImageUploading] = useState(false); - + // toast alert const { setToastAlert } = useToast(); - + // store hooks const { - appConfig: { envConfig }, - } = useMobxStore(); + config: { envConfig }, + } = useApplication(); const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]); diff --git a/web/components/core/modals/workspace-image-upload-modal.tsx b/web/components/core/modals/workspace-image-upload-modal.tsx index 166e911f5..e04ccf820 100644 --- a/web/components/core/modals/workspace-image-upload-modal.tsx +++ b/web/components/core/modals/workspace-image-upload-modal.tsx @@ -3,8 +3,8 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { useDropzone } from "react-dropzone"; import { Transition, Dialog } from "@headlessui/react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication, useWorkspace } from "hooks/store"; // services import { FileService } from "services/file.service"; // hooks @@ -40,9 +40,9 @@ export const WorkspaceImageUploadModal: React.FC = observer((props) => { const { setToastAlert } = useToast(); const { - workspace: { currentWorkspace }, - appConfig: { envConfig }, - } = useMobxStore(); + config: { envConfig }, + } = useApplication(); + const { currentWorkspace } = useWorkspace(); const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]); diff --git a/web/components/core/sidebar/links-list.tsx b/web/components/core/sidebar/links-list.tsx index 3edcb9066..52b1e9de1 100644 --- a/web/components/core/sidebar/links-list.tsx +++ b/web/components/core/sidebar/links-list.tsx @@ -5,7 +5,7 @@ import { Pencil, Trash2, LinkIcon } from "lucide-react"; // helpers import { calculateTimeAgo } from "helpers/date-time.helper"; // types -import { ILinkDetails, UserAuth } from "types"; +import { ILinkDetails, UserAuth } from "@plane/types"; // hooks import useToast from "hooks/use-toast"; @@ -50,8 +50,8 @@ export const LinksList: React.FC = ({ links, handleDeleteLink, handleEdit
-
- {!isNotAllowed && ( + {!isNotAllowed && ( +
- )} - - - - {!isNotAllowed && ( + + + - )} -
+
+ )}

diff --git a/web/components/core/sidebar/progress-chart.tsx b/web/components/core/sidebar/progress-chart.tsx index 3c445760d..9e9a4bac8 100644 --- a/web/components/core/sidebar/progress-chart.tsx +++ b/web/components/core/sidebar/progress-chart.tsx @@ -5,7 +5,7 @@ import { LineGraph } from "components/ui"; // helpers import { renderFormattedDateWithoutYear } from "helpers/date-time.helper"; //types -import { TCompletionChartDistribution } from "types"; +import { TCompletionChartDistribution } from "@plane/types"; type Props = { distribution: TCompletionChartDistribution; diff --git a/web/components/core/sidebar/sidebar-progress-stats.tsx b/web/components/core/sidebar/sidebar-progress-stats.tsx index 8cea3784f..6d89981cd 100644 --- a/web/components/core/sidebar/sidebar-progress-stats.tsx +++ b/web/components/core/sidebar/sidebar-progress-stats.tsx @@ -14,13 +14,12 @@ import { SingleProgressStats } from "components/core"; import { Avatar, StateGroupIcon } from "@plane/ui"; // types import { - IIssueFilterOptions, IModule, TAssigneesDistribution, TCompletionChartDistribution, TLabelsDistribution, TStateGroups, -} from "types"; +} from "@plane/types"; type Props = { distribution: { @@ -36,9 +35,6 @@ type Props = { roundedTab?: boolean; noBackground?: boolean; isPeekView?: boolean; - isCompleted?: boolean; - filters?: IIssueFilterOptions; - handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void; }; export const SidebarProgressStats: React.FC = ({ @@ -48,10 +44,7 @@ export const SidebarProgressStats: React.FC = ({ module, roundedTab, noBackground, - isCompleted = false, isPeekView = false, - filters, - handleFiltersUpdate, }) => { const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees"); @@ -147,11 +140,20 @@ export const SidebarProgressStats: React.FC = ({ } completed={assignee.completed_issues} total={assignee.total_issues} - {...(!isPeekView && - !isCompleted && { - onClick: () => handleFiltersUpdate("assignees", assignee.assignee_id ?? ""), - selected: filters?.assignees?.includes(assignee.assignee_id ?? ""), - })} + {...(!isPeekView && { + onClick: () => { + // TODO: set filters here + // if (filters?.assignees?.includes(assignee.assignee_id ?? "")) + // setFilters({ + // assignees: filters?.assignees?.filter((a) => a !== assignee.assignee_id), + // }); + // else + // setFilters({ + // assignees: [...(filters?.assignees ?? []), assignee.assignee_id ?? ""], + // }); + }, + // selected: filters?.assignees?.includes(assignee.assignee_id ?? ""), + })} /> ); else @@ -198,11 +200,17 @@ export const SidebarProgressStats: React.FC = ({ } completed={label.completed_issues} total={label.total_issues} - {...(!isPeekView && - !isCompleted && { - onClick: () => handleFiltersUpdate("labels", label.label_id ?? ""), - selected: filters?.labels?.includes(label.label_id ?? `no-label-${index}`), - })} + {...(!isPeekView && { + // TODO: set filters here + onClick: () => { + // if (filters.labels?.includes(label.label_id ?? "")) + // setFilters({ + // labels: filters?.labels?.filter((l) => l !== label.label_id), + // }); + // else setFilters({ labels: [...(filters?.labels ?? []), label.label_id ?? ""] }); + }, + // selected: filters?.labels?.includes(label.label_id ?? ""), + })} /> )) ) : ( diff --git a/web/components/core/theme/color-picker-input.tsx b/web/components/core/theme/color-picker-input.tsx index f47c1349f..19cd519cb 100644 --- a/web/components/core/theme/color-picker-input.tsx +++ b/web/components/core/theme/color-picker-input.tsx @@ -18,7 +18,7 @@ import { Input } from "@plane/ui"; // icons import { Palette } from "lucide-react"; // types -import { IUserTheme } from "types"; +import { IUserTheme } from "@plane/types"; type Props = { name: keyof IUserTheme; diff --git a/web/components/core/theme/custom-theme-selector.tsx b/web/components/core/theme/custom-theme-selector.tsx index c55170702..bd6f43569 100644 --- a/web/components/core/theme/custom-theme-selector.tsx +++ b/web/components/core/theme/custom-theme-selector.tsx @@ -1,12 +1,12 @@ import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; import { useTheme } from "next-themes"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useUser } from "hooks/store"; // ui import { Button, InputColorPicker } from "@plane/ui"; // types -import { IUserTheme } from "types"; +import { IUserTheme } from "@plane/types"; const inputRules = { required: "Background color is required", @@ -25,8 +25,8 @@ const inputRules = { }; export const CustomThemeSelector: React.FC = observer(() => { - const { user: userStore } = useMobxStore(); - const userTheme = userStore?.currentUser?.theme; + const { currentUser, updateCurrentUser } = useUser(); + const userTheme = currentUser?.theme; // hooks const { setTheme } = useTheme(); @@ -61,7 +61,7 @@ export const CustomThemeSelector: React.FC = observer(() => { setTheme("custom"); - return userStore.updateCurrentUser({ theme: payload }); + return updateCurrentUser({ theme: payload }); }; const handleValueChange = (val: string | undefined, onChange: any) => { diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index c184f80c9..5ef912572 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -3,9 +3,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { useApplication, useCycle, useIssues, useProjectState } from "hooks/store"; import useToast from "hooks/use-toast"; // ui import { SingleProgressStats } from "components/core"; @@ -31,7 +30,9 @@ import { AlarmClock, AlertTriangle, ArrowRight, CalendarDays, Star, Target } fro import { renderFormattedDate, findHowManyDaysLeft } from "helpers/date-time.helper"; import { truncateText } from "helpers/string.helper"; // types -import { ICycle } from "types"; +import { ICycle } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; +import { ACTIVE_CYCLE_ISSUES } from "store/issue/cycle"; const stateGroups = [ { @@ -67,41 +68,53 @@ interface IActiveCycleDetails { } export const ActiveCycleDetails: React.FC = observer((props) => { + // router const router = useRouter(); - const { workspaceSlug, projectId } = props; - const { cycle: cycleStore, commandPalette: commandPaletteStore } = useMobxStore(); - + const { + issues: { issues }, + issueMap, + } = useIssues(EIssuesStoreType.CYCLE); + // store hooks + const { + commandPalette: { toggleCreateCycleModal }, + } = useApplication(); + const { + fetchActiveCycle, + currentProjectActiveCycleId, + getActiveCycleById, + addCycleToFavorites, + removeCycleFromFavorites, + } = useCycle(); + const { getProjectStates } = useProjectState(); + // toast alert const { setToastAlert } = useToast(); const { isLoading } = useSWR( - workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null, - workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null + workspaceSlug && projectId ? `PROJECT_ACTIVE_CYCLE_${projectId}` : null, + workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null ); - const activeCycle = cycleStore.cycles?.[projectId]?.current || null; - const cycle = activeCycle ? activeCycle[0] : null; - const issues = (cycleStore?.active_cycle_issues as any) || null; + const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null; + const issueIds = issues?.[ACTIVE_CYCLE_ISSUES]; - // const { data: issues } = useSWR( - // workspaceSlug && projectId && cycle?.id ? CYCLE_ISSUES_WITH_PARAMS(cycle?.id, { priority: "urgent,high" }) : null, - // workspaceSlug && projectId && cycle?.id + // useSWR( + // workspaceSlug && projectId && cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId, { priority: "urgent,high" }) : null, + // workspaceSlug && projectId && cycleId // ? () => - // cycleService.getCycleIssuesWithParams(workspaceSlug as string, projectId as string, cycle.id, { - // priority: "urgent,high", - // }) + // fetchActiveCycleIssues(workspaceSlug, projectId, ) // : null - // ) as { data: IIssue[] | undefined }; + // ); - if (!cycle && isLoading) + if (!activeCycle && isLoading) return ( ); - if (!cycle) + if (!activeCycle) return (

@@ -118,7 +131,7 @@ export const ActiveCycleDetails: React.FC = observer((props @@ -126,24 +139,24 @@ export const ActiveCycleDetails: React.FC = observer((props
); - const endDate = new Date(cycle.end_date ?? ""); - const startDate = new Date(cycle.start_date ?? ""); + const endDate = new Date(activeCycle.end_date ?? ""); + const startDate = new Date(activeCycle.start_date ?? ""); const groupedIssues: any = { - backlog: cycle.backlog_issues, - unstarted: cycle.unstarted_issues, - started: cycle.started_issues, - completed: cycle.completed_issues, - cancelled: cycle.cancelled_issues, + backlog: activeCycle.backlog_issues, + unstarted: activeCycle.unstarted_issues, + started: activeCycle.started_issues, + completed: activeCycle.completed_issues, + cancelled: activeCycle.cancelled_issues, }; - const cycleStatus = cycle.status.toLocaleLowerCase(); + const cycleStatus = activeCycle.status.toLocaleLowerCase(); const handleAddToFavorites = (e: MouseEvent) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -156,7 +169,7 @@ export const ActiveCycleDetails: React.FC = observer((props e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -168,7 +181,10 @@ export const ActiveCycleDetails: React.FC = observer((props const progressIndicatorData = stateGroups.map((group, index) => ({ id: index, name: group.title, - value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0, + value: + activeCycle.total_issues > 0 + ? ((activeCycle[group.key as keyof ICycle] as number) / activeCycle.total_issues) * 100 + : 0, color: group.color, })); @@ -196,8 +212,8 @@ export const ActiveCycleDetails: React.FC = observer((props }`} /> - -

{truncateText(cycle.name, 70)}

+ +

{truncateText(activeCycle.name, 70)}

@@ -218,19 +234,19 @@ export const ActiveCycleDetails: React.FC = observer((props {cycleStatus === "current" ? ( - {findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left + {findHowManyDaysLeft(activeCycle.end_date ?? new Date())} Days Left ) : cycleStatus === "upcoming" ? ( - {findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left + {findHowManyDaysLeft(activeCycle.start_date ?? new Date())} Days Left ) : cycleStatus === "completed" ? ( - {cycle.total_issues - cycle.completed_issues > 0 && ( + {activeCycle.total_issues - activeCycle.completed_issues > 0 && ( @@ -244,7 +260,7 @@ export const ActiveCycleDetails: React.FC = observer((props cycleStatus )} - {cycle.is_favorite ? ( + {activeCycle.is_favorite ? (
- +
@@ -363,9 +379,9 @@ export const ActiveCycleDetails: React.FC = observer((props
High Priority Issues
- {issues ? ( - issues.length > 0 ? ( - issues.map((issue: any) => ( + {issueIds ? ( + issueIds.length > 0 ? ( + issueIds.map((issue: any) => (
router.push(`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`)} @@ -428,24 +444,33 @@ export const ActiveCycleDetails: React.FC = observer((props
- {issues && issues.length > 0 && ( + {issueIds && issueIds.length > 0 && (
issue?.state_detail?.group === "completed")?.length / - issues.length) * + (issueIds.filter((issue: any) => issue?.state_detail?.group === "completed")?.length / + issueIds.length) * 100 ?? 0 }%`, }} />
- {issues?.filter((issue: any) => issue?.state_detail?.group === "completed")?.length} of {issues?.length} + of{" "} + { + issueIds?.filter( + (issueId) => + getProjectStates(issueMap[issueId]?.project_id).find( + (issue) => issue.id === issueMap[issueId]?.state_id + )?.group === "completed" + )?.length + }{" "} + of {issueIds?.length}
)} @@ -466,15 +491,18 @@ export const ActiveCycleDetails: React.FC = observer((props - Pending Issues - {cycle.total_issues - (cycle.completed_issues + cycle.cancelled_issues)} + + Pending Issues -{" "} + {activeCycle.total_issues - (activeCycle.completed_issues + activeCycle.cancelled_issues)} +
diff --git a/web/components/cycles/active-cycle-stats.tsx b/web/components/cycles/active-cycle-stats.tsx index 2c9339892..524b02dd0 100644 --- a/web/components/cycles/active-cycle-stats.tsx +++ b/web/components/cycles/active-cycle-stats.tsx @@ -7,7 +7,7 @@ import { SingleProgressStats } from "components/core"; // ui import { Avatar } from "@plane/ui"; // types -import { ICycle } from "types"; +import { ICycle } from "@plane/types"; type Props = { cycle: ICycle; diff --git a/web/components/cycles/cycle-peek-overview.tsx b/web/components/cycles/cycle-peek-overview.tsx index d6806eaf0..b7acff358 100644 --- a/web/components/cycles/cycle-peek-overview.tsx +++ b/web/components/cycles/cycle-peek-overview.tsx @@ -1,10 +1,8 @@ import React, { useEffect } from "react"; - import { useRouter } from "next/router"; - -// mobx import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useCycle } from "hooks/store"; // components import { CycleDetailsSidebar } from "./sidebar"; @@ -14,14 +12,13 @@ type Props = { }; export const CyclePeekOverview: React.FC = observer(({ projectId, workspaceSlug }) => { + // router const router = useRouter(); const { peekCycle } = router.query; - + // refs const ref = React.useRef(null); - - const { cycle: cycleStore } = useMobxStore(); - - const { fetchCycleWithId } = cycleStore; + // store hooks + const { fetchCycleDetails } = useCycle(); const handleClose = () => { delete router.query.peekCycle; @@ -33,8 +30,8 @@ export const CyclePeekOverview: React.FC = observer(({ projectId, workspa useEffect(() => { if (!peekCycle) return; - fetchCycleWithId(workspaceSlug, projectId, peekCycle.toString()); - }, [fetchCycleWithId, peekCycle, projectId, workspaceSlug]); + fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString()); + }, [fetchCycleDetails, peekCycle, projectId, workspaceSlug]); return ( <> diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index ca0d487e8..f6836269c 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -2,6 +2,7 @@ import { FC, MouseEvent, useState } from "react"; import { useRouter } from "next/router"; import Link from "next/link"; // hooks +import { useApplication, useCycle, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; @@ -12,62 +13,65 @@ import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; // helpers import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; -// types -import { ICycle, TCycleGroups } from "types"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; // constants import { CYCLE_STATUS } from "constants/cycle"; import { EUserWorkspaceRoles } from "constants/workspace"; +//.types +import { TCycleGroups } from "@plane/types"; export interface ICyclesBoardCard { workspaceSlug: string; projectId: string; - cycle: ICycle; + cycleId: string; } export const CyclesBoardCard: FC = (props) => { - const { cycle, workspaceSlug, projectId } = props; - // store - const { - cycle: cycleStore, - trackEvent: { setTrackElement }, - user: userStore, - } = useMobxStore(); - // toast - const { setToastAlert } = useToast(); + const { cycleId, workspaceSlug, projectId } = props; // states const [updateModal, setUpdateModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false); - // computed - const cycleStatus = cycle.status.toLocaleLowerCase() as TCycleGroups; - const isCompleted = cycleStatus === "completed"; - const endDate = new Date(cycle.end_date ?? ""); - const startDate = new Date(cycle.start_date ?? ""); - const isDateValid = cycle.start_date || cycle.end_date; - - const { currentProjectRole } = userStore; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; - + // router const router = useRouter(); + // store + const { + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); + const { addCycleToFavorites, removeCycleFromFavorites, getCycleById } = useCycle(); + // toast alert + const { setToastAlert } = useToast(); + // computed + const cycleDetails = getCycleById(cycleId); + + if (!cycleDetails) return null; + + const cycleStatus = cycleDetails.status.toLocaleLowerCase(); + const isCompleted = cycleStatus === "completed"; + const endDate = new Date(cycleDetails.end_date ?? ""); + const startDate = new Date(cycleDetails.start_date ?? ""); + const isDateValid = cycleDetails.start_date || cycleDetails.end_date; + + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); const cycleTotalIssues = - cycle.backlog_issues + - cycle.unstarted_issues + - cycle.started_issues + - cycle.completed_issues + - cycle.cancelled_issues; + cycleDetails.backlog_issues + + cycleDetails.unstarted_issues + + cycleDetails.started_issues + + cycleDetails.completed_issues + + cycleDetails.cancelled_issues; - const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100; + const completionPercentage = (cycleDetails.completed_issues / cycleTotalIssues) * 100; - const issueCount = cycle + const issueCount = cycleDetails ? cycleTotalIssues === 0 ? "0 Issue" - : cycleTotalIssues === cycle.completed_issues + : cycleTotalIssues === cycleDetails.completed_issues ? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}` - : `${cycle.completed_issues}/${cycleTotalIssues} Issues` + : `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues` : "0 Issue"; const handleCopyText = (e: MouseEvent) => { @@ -75,7 +79,7 @@ export const CyclesBoardCard: FC = (props) => { e.stopPropagation(); const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => { + copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => { setToastAlert({ type: "success", title: "Link Copied!", @@ -88,7 +92,7 @@ export const CyclesBoardCard: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -101,7 +105,7 @@ export const CyclesBoardCard: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -130,14 +134,14 @@ export const CyclesBoardCard: FC = (props) => { router.push({ pathname: router.pathname, - query: { ...query, peekCycle: cycle.id }, + query: { ...query, peekCycle: cycleId }, }); }; return (
setUpdateModal(false)} workspaceSlug={workspaceSlug} @@ -145,22 +149,22 @@ export const CyclesBoardCard: FC = (props) => { /> setDeleteModal(false)} workspaceSlug={workspaceSlug} projectId={projectId} /> - +
- + - - {cycle.name} + + {cycleDetails.name}
@@ -173,7 +177,7 @@ export const CyclesBoardCard: FC = (props) => { }} > {currentCycle.value === "current" - ? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}` + ? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}` : `${currentCycle.label}`} )} @@ -189,11 +193,11 @@ export const CyclesBoardCard: FC = (props) => { {issueCount}
- {cycle.assignees.length > 0 && ( - + {cycleDetails.assignees.length > 0 && ( +
- {cycle.assignees.map((assignee) => ( + {cycleDetails.assignees.map((assignee) => ( ))} @@ -233,7 +237,7 @@ export const CyclesBoardCard: FC = (props) => { )}
{isEditingAllowed && - (cycle.is_favorite ? ( + (cycleDetails.is_favorite ? ( diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx index af234b9dc..967e8a395 100644 --- a/web/components/cycles/cycles-board.tsx +++ b/web/components/cycles/cycles-board.tsx @@ -1,14 +1,12 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication } from "hooks/store"; // components import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; -// types -import { ICycle } from "types"; export interface ICyclesBoard { - cycles: ICycle[]; + cycleIds: string[]; filter: string; workspaceSlug: string; projectId: string; @@ -16,13 +14,13 @@ export interface ICyclesBoard { } export const CyclesBoard: FC = observer((props) => { - const { cycles, filter, workspaceSlug, projectId, peekCycle } = props; - - const { commandPalette: commandPaletteStore } = useMobxStore(); + const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props; + // store hooks + const { commandPalette: commandPaletteStore } = useApplication(); return ( <> - {cycles.length > 0 ? ( + {cycleIds?.length > 0 ? (
= observer((props) => { : "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4" } auto-rows-max transition-all `} > - {cycles.map((cycle) => ( - + {cycleIds.map((cycleId) => ( + ))}
void; handleDeleteCycle?: () => void; handleAddToFavorites?: () => void; @@ -31,50 +30,29 @@ type TCyclesListItem = { }; export const CyclesListItem: FC = (props) => { - const { cycle, workspaceSlug, projectId } = props; - // store - const { - cycle: cycleStore, - trackEvent: { setTrackElement }, - user: userStore, - } = useMobxStore(); - // toast - const { setToastAlert } = useToast(); + const { cycleId, workspaceSlug, projectId } = props; // states const [updateModal, setUpdateModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false); - // computed - const cycleStatus = cycle.status.toLocaleLowerCase() as TCycleGroups; - const isCompleted = cycleStatus === "completed"; - const endDate = new Date(cycle.end_date ?? ""); - const startDate = new Date(cycle.start_date ?? ""); - - const { currentProjectRole } = userStore; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; - + // router const router = useRouter(); - - const cycleTotalIssues = - cycle.backlog_issues + - cycle.unstarted_issues + - cycle.started_issues + - cycle.completed_issues + - cycle.cancelled_issues; - - const renderDate = cycle.start_date || cycle.end_date; - - const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100; - - const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage); - - const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); + // store hooks + const { + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); + const { getCycleById, addCycleToFavorites, removeCycleFromFavorites } = useCycle(); + // toast alert + const { setToastAlert } = useToast(); const handleCopyText = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => { + copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => { setToastAlert({ type: "success", title: "Link Copied!", @@ -87,7 +65,7 @@ export const CyclesListItem: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -100,7 +78,7 @@ export const CyclesListItem: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -129,27 +107,56 @@ export const CyclesListItem: FC = (props) => { router.push({ pathname: router.pathname, - query: { ...query, peekCycle: cycle.id }, + query: { ...query, peekCycle: cycleId }, }); }; + const cycleDetails = getCycleById(cycleId); + + if (!cycleDetails) return null; + + // computed + const cycleStatus = cycleDetails.status.toLocaleLowerCase() as TCycleGroups; + const isCompleted = cycleStatus === "completed"; + const endDate = new Date(cycleDetails.end_date ?? ""); + const startDate = new Date(cycleDetails.start_date ?? ""); + + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + + const cycleTotalIssues = + cycleDetails.backlog_issues + + cycleDetails.unstarted_issues + + cycleDetails.started_issues + + cycleDetails.completed_issues + + cycleDetails.cancelled_issues; + + const renderDate = cycleDetails.start_date || cycleDetails.end_date; + + // const areYearsEqual = startDate.getFullYear() === endDate.getFullYear(); + + const completionPercentage = (cycleDetails.completed_issues / cycleTotalIssues) * 100; + + const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage); + + const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); + return ( <> setUpdateModal(false)} workspaceSlug={workspaceSlug} projectId={projectId} /> setDeleteModal(false)} workspaceSlug={workspaceSlug} projectId={projectId} /> - +
@@ -173,8 +180,8 @@ export const CyclesListItem: FC = (props) => { - - {cycle.name} + + {cycleDetails.name}
@@ -194,7 +201,7 @@ export const CyclesListItem: FC = (props) => { }} > {currentCycle.value === "current" - ? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}` + ? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}` : `${currentCycle.label}`} )} @@ -206,11 +213,11 @@ export const CyclesListItem: FC = (props) => { )} - +
- {cycle.assignees.length > 0 ? ( + {cycleDetails.assignees.length > 0 ? ( - {cycle.assignees.map((assignee) => ( + {cycleDetails.assignees.map((assignee) => ( ))} @@ -222,7 +229,7 @@ export const CyclesListItem: FC = (props) => {
{isEditingAllowed && - (cycle.is_favorite ? ( + (cycleDetails.is_favorite ? ( diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 226807b78..686937b71 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -1,39 +1,37 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication } from "hooks/store"; // components import { CyclePeekOverview, CyclesListItem } from "components/cycles"; // ui import { Loader } from "@plane/ui"; -// types -import { ICycle } from "types"; export interface ICyclesList { - cycles: ICycle[]; + cycleIds: string[]; filter: string; workspaceSlug: string; projectId: string; } export const CyclesList: FC = observer((props) => { - const { cycles, filter, workspaceSlug, projectId } = props; - + const { cycleIds, filter, workspaceSlug, projectId } = props; + // store hooks const { commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - } = useMobxStore(); + eventTracker: { setTrackElement }, + } = useApplication(); return ( <> - {cycles ? ( + {cycleIds ? ( <> - {cycles.length > 0 ? ( + {cycleIds.length > 0 ? (
- {cycles.map((cycle) => ( - + {cycleIds.map((cycleId) => ( + ))}
= observer((props) => { const { filter, layout, workspaceSlug, projectId, peekCycle } = props; - - // store - const { cycle: cycleStore } = useMobxStore(); - - // api call to fetch cycles list - useSWR( - workspaceSlug && projectId && filter ? `CYCLES_LIST_${projectId}_${filter}` : null, - workspaceSlug && projectId && filter ? () => cycleStore.fetchCycles(workspaceSlug, projectId, filter) : null - ); + // store hooks + const { + currentProjectCompletedCycleIds, + currentProjectDraftCycleIds, + currentProjectUpcomingCycleIds, + currentProjectCycleIds, + } = useCycle(); const cyclesList = filter === "completed" - ? cycleStore.projectCompletedCycles + ? currentProjectCompletedCycleIds : filter === "draft" - ? cycleStore.projectDraftCycles - : filter === "upcoming" - ? cycleStore.projectUpcomingCycles - : cycleStore.projectCycles; + ? currentProjectDraftCycleIds + : filter === "upcoming" + ? currentProjectUpcomingCycleIds + : currentProjectCycleIds; return ( <> {layout === "list" && ( <> {cyclesList ? ( - + ) : ( @@ -59,7 +56,7 @@ export const CyclesView: FC = observer((props) => { <> {cyclesList ? ( = observer((props) => { {layout === "gantt" && ( <> {cyclesList ? ( - + ) : ( diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx index 33c6254df..44da175b4 100644 --- a/web/components/cycles/delete-modal.tsx +++ b/web/components/cycles/delete-modal.tsx @@ -1,17 +1,15 @@ import { Fragment, useState } from "react"; -// next import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; +// hooks +import { useApplication, useCycle } from "hooks/store"; +import useToast from "hooks/use-toast"; // components import { Button } from "@plane/ui"; -// hooks -import useToast from "hooks/use-toast"; // types -import { ICycle } from "types"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { ICycle } from "@plane/types"; interface ICycleDelete { cycle: ICycle; @@ -23,56 +21,51 @@ interface ICycleDelete { export const CycleDeleteModal: React.FC = observer((props) => { const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props; - // store - const { - cycle: cycleStore, - trackEvent: { postHogEventTracker }, - } = useMobxStore(); - // toast - const { setToastAlert } = useToast(); // states const [loader, setLoader] = useState(false); + // router const router = useRouter(); const { cycleId, peekCycle } = router.query; + // store hooks + const { + eventTracker: { postHogEventTracker }, + } = useApplication(); + const { deleteCycle } = useCycle(); + // toast alert + const { setToastAlert } = useToast(); const formSubmit = async () => { + if (!cycle) return; + setLoader(true); - if (cycle?.id) - try { - await cycleStore - .removeCycle(workspaceSlug, projectId, cycle?.id) - .then(() => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Cycle deleted successfully.", - }); - postHogEventTracker("CYCLE_DELETE", { - state: "SUCCESS", - }); - }) - .catch(() => { - postHogEventTracker("CYCLE_DELETE", { - state: "FAILED", - }); + try { + await deleteCycle(workspaceSlug, projectId, cycle.id) + .then(() => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Cycle deleted successfully.", + }); + postHogEventTracker("CYCLE_DELETE", { + state: "SUCCESS", + }); + }) + .catch(() => { + postHogEventTracker("CYCLE_DELETE", { + state: "FAILED", }); - - if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`); - - handleClose(); - } catch (error) { - setToastAlert({ - type: "error", - title: "Warning!", - message: "Something went wrong please try again later.", }); - } - else + + if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`); + + handleClose(); + } catch (error) { setToastAlert({ type: "error", title: "Warning!", message: "Something went wrong please try again later.", }); + } setLoader(false); }; diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index 2cc087eda..2396d040a 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -1,10 +1,12 @@ import { Controller, useForm } from "react-hook-form"; +// components +import { DateDropdown, ProjectDropdown } from "components/dropdowns"; // ui import { Button, Input, TextArea } from "@plane/ui"; -import { DateSelect } from "components/ui"; -import { IssueProjectSelect } from "components/issues/select"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import { ICycle } from "types"; +import { ICycle } from "@plane/types"; type Props = { handleFormSubmit: (values: Partial) => Promise; @@ -45,19 +47,22 @@ export const CycleForm: React.FC = (props) => {
- ( - { - onChange(val); - setActiveProject(val); - }} - /> - )} - /> + {!status && ( + ( + { + onChange(val); + setActiveProject(val); + }} + buttonVariant="background-with-text" + /> + )} + /> + )}

{status ? "Update" : "New"} Cycle

@@ -112,25 +117,33 @@ export const CycleForm: React.FC = (props) => { control={control} name="start_date" render={({ field: { value, onChange } }) => ( - + onChange(date ? renderFormattedPayloadDate(date) : null)} + buttonVariant="border-with-text" + placeholder="Start date" + maxDate={maxDate ?? undefined} + /> +
+ )} + /> +
+ ( +
+ onChange(val)} - minDate={new Date()} - maxDate={maxDate ?? undefined} + onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)} + buttonVariant="border-with-text" + placeholder="End date" + minDate={minDate} /> - )} - /> -
-
- ( - onChange(val)} minDate={minDate} /> - )} - /> -
+
+ )} + />
diff --git a/web/components/cycles/gantt-chart/blocks.tsx b/web/components/cycles/gantt-chart/blocks.tsx index 8f05c45ab..46bc04039 100644 --- a/web/components/cycles/gantt-chart/blocks.tsx +++ b/web/components/cycles/gantt-chart/blocks.tsx @@ -4,7 +4,7 @@ import { Tooltip, ContrastIcon } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; // types -import { ICycle } from "types"; +import { ICycle } from "@plane/types"; export const CycleGanttBlock = ({ data }: { data: ICycle }) => { const router = useRouter(); diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx index 9671c22af..26d04e103 100644 --- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx +++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx @@ -1,38 +1,41 @@ import { FC } from "react"; - import { useRouter } from "next/router"; - +import { observer } from "mobx-react-lite"; import { KeyedMutator } from "swr"; - +// hooks +import { useCycle, useUser } from "hooks/store"; // services import { CycleService } from "services/cycle.service"; -// hooks -import useUser from "hooks/use-user"; -import useProjectDetails from "hooks/use-project-details"; // components import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart"; import { CycleGanttBlock } from "components/cycles"; // types -import { ICycle } from "types"; +import { ICycle } from "@plane/types"; +// constants +import { EUserProjectRoles } from "constants/project"; type Props = { workspaceSlug: string; - cycles: ICycle[]; + cycleIds: string[]; mutateCycles?: KeyedMutator; }; // services const cycleService = new CycleService(); -export const CyclesListGanttChartView: FC = ({ cycles, mutateCycles }) => { +export const CyclesListGanttChartView: FC = observer((props) => { + const { cycleIds, mutateCycles } = props; + // router const router = useRouter(); const { workspaceSlug } = router.query; - - const { user } = useUser(); - const { projectDetails } = useProjectDetails(); + // store hooks + const { + membership: { currentProjectRole }, + } = useUser(); + const { getCycleById } = useCycle(); const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => { - if (!workspaceSlug || !user) return; + if (!workspaceSlug) return; mutateCycles && mutateCycles((prevData: any) => { if (!prevData) return prevData; @@ -63,27 +66,31 @@ export const CyclesListGanttChartView: FC = ({ cycles, mutateCycles }) => cycleService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload); }; - const blockFormat = (blocks: ICycle[]) => - blocks && blocks.length > 0 - ? blocks - .filter((b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date)) - .map((block) => ({ - data: block, - id: block.id, - sort_order: block.sort_order, - start_date: new Date(block.start_date ?? ""), - target_date: new Date(block.end_date ?? ""), - })) - : []; + const blockFormat = (blocks: (ICycle | null)[]) => { + if (!blocks) return []; - const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15; + const filteredBlocks = blocks.filter((b) => b !== null && b.start_date && b.end_date); + + const structuredBlocks = filteredBlocks.map((block) => ({ + data: block, + id: block?.id ?? "", + sort_order: block?.sort_order ?? 0, + start_date: new Date(block?.start_date ?? ""), + target_date: new Date(block?.end_date ?? ""), + })); + + return structuredBlocks; + }; + + const isAllowed = + currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); return (
getCycleById(c))) : null} blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)} sidebarToRender={(props) => } blockToRender={(data: ICycle) => } @@ -94,4 +101,4 @@ export const CyclesListGanttChartView: FC = ({ cycles, mutateCycles }) => />
); -}; +}); diff --git a/web/components/cycles/modal.tsx b/web/components/cycles/modal.tsx index 665f9865b..bfd30cdf6 100644 --- a/web/components/cycles/modal.tsx +++ b/web/components/cycles/modal.tsx @@ -3,12 +3,12 @@ import { Dialog, Transition } from "@headlessui/react"; // services import { CycleService } from "services/cycle.service"; // hooks +import { useApplication, useCycle } from "hooks/store"; import useToast from "hooks/use-toast"; -import { useMobxStore } from "lib/mobx/store-provider"; // components import { CycleForm } from "components/cycles"; // types -import type { CycleDateCheckData, ICycle } from "types"; +import type { CycleDateCheckData, ICycle } from "@plane/types"; type CycleModalProps = { isOpen: boolean; @@ -23,21 +23,21 @@ const cycleService = new CycleService(); export const CycleCreateUpdateModal: React.FC = (props) => { const { isOpen, handleClose, data, workspaceSlug, projectId } = props; - // store - const { - cycle: cycleStore, - trackEvent: { postHogEventTracker }, - } = useMobxStore(); // states const [activeProject, setActiveProject] = useState(projectId); - // toast + // store hooks + const { + eventTracker: { postHogEventTracker }, + } = useApplication(); + const { createCycle, updateCycleDetails } = useCycle(); + // toast alert const { setToastAlert } = useToast(); - const createCycle = async (payload: Partial) => { + const handleCreateCycle = async (payload: Partial) => { if (!workspaceSlug || !projectId) return; + const selectedProjectId = payload.project ?? projectId.toString(); - await cycleStore - .createCycle(workspaceSlug, selectedProjectId, payload) + await createCycle(workspaceSlug, selectedProjectId, payload) .then((res) => { setToastAlert({ type: "success", @@ -61,11 +61,11 @@ export const CycleCreateUpdateModal: React.FC = (props) => { }); }; - const updateCycle = async (cycleId: string, payload: Partial) => { + const handleUpdateCycle = async (cycleId: string, payload: Partial) => { if (!workspaceSlug || !projectId) return; + const selectedProjectId = payload.project ?? projectId.toString(); - await cycleStore - .patchCycle(workspaceSlug, selectedProjectId, cycleId, payload) + await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload) .then(() => { setToastAlert({ type: "success", @@ -116,8 +116,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { } if (isDateValid) { - if (data) await updateCycle(data.id, payload); - else await createCycle(payload); + if (data) await handleUpdateCycle(data.id, payload); + else await handleCreateCycle(payload); handleClose(); } else setToastAlert({ diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index e73168008..f2f7792f6 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -1,13 +1,12 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { useForm } from "react-hook-form"; import { Disclosure, Popover, Transition } from "@headlessui/react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // services import { CycleService } from "services/cycle.service"; // hooks +import { useApplication, useCycle, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { SidebarProgressStats } from "components/core"; @@ -36,8 +35,7 @@ import { renderFormattedDate, } from "helpers/date-time.helper"; // types -import { ICycle, IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { ICycle } from "@plane/types"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; // fetch-keys @@ -54,20 +52,21 @@ const cycleService = new CycleService(); // TODO: refactor the whole component export const CycleDetailsSidebar: React.FC = observer((props) => { const { cycleId, handleClose } = props; - + // states const [cycleDeleteModal, setCycleDeleteModal] = useState(false); - + // router const router = useRouter(); const { workspaceSlug, projectId, peekCycle } = router.query; - + // store hooks const { - cycle: cycleDetailsStore, - cycleIssuesFilter: { issueFilters, updateFilters }, - trackEvent: { setTrackElement }, - user: { currentProjectRole }, - } = useMobxStore(); + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); + const { getCycleById, updateCycleDetails } = useCycle(); - const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined; + const cycleDetails = getCycleById(cycleId); const { setToastAlert } = useToast(); @@ -83,7 +82,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const submitChanges = (data: Partial) => { if (!workspaceSlug || !projectId || !cycleId) return; - cycleDetailsStore.patchCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data); + updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data); }; const handleCopyText = () => { @@ -254,24 +253,25 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { } }; - const handleFiltersUpdate = useCallback( - (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; - const newValues = issueFilters?.filters?.[key] ?? []; + // TODO: refactor this + // const handleFiltersUpdate = useCallback( + // (key: keyof IIssueFilterOptions, value: string | string[]) => { + // if (!workspaceSlug || !projectId) return; + // const newValues = issueFilters?.filters?.[key] ?? []; - if (Array.isArray(value)) { - value.forEach((val) => { - if (!newValues.includes(val)) newValues.push(val); - }); - } else { - if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); - else newValues.push(value); - } + // if (Array.isArray(value)) { + // value.forEach((val) => { + // if (!newValues.includes(val)) newValues.push(val); + // }); + // } else { + // if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + // else newValues.push(value); + // } - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { [key]: newValues }, cycleId); - }, - [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] - ); + // updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { [key]: newValues }, cycleId); + // }, + // [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] + // ); const cycleStatus = cycleDetails?.status.toLocaleLowerCase(); const isCompleted = cycleStatus === "completed"; @@ -587,9 +587,6 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { }} totalIssues={cycleDetails.total_issues} isPeekView={Boolean(peekCycle)} - isCompleted={isCompleted} - filters={issueFilters?.filters} - handleFiltersUpdate={handleFiltersUpdate} />
)} diff --git a/web/components/cycles/transfer-issues-modal.tsx b/web/components/cycles/transfer-issues-modal.tsx index f47c1ddaa..5956e4a1e 100644 --- a/web/components/cycles/transfer-issues-modal.tsx +++ b/web/components/cycles/transfer-issues-modal.tsx @@ -1,32 +1,31 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import useSWR from "swr"; import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; -// services -import { CycleService } from "services/cycle.service"; // hooks import useToast from "hooks/use-toast"; -import { useMobxStore } from "lib/mobx/store-provider"; +import { useCycle, useIssues } from "hooks/store"; //icons import { ContrastIcon, TransferIcon } from "@plane/ui"; import { AlertCircle, Search, X } from "lucide-react"; -// fetch-key -import { INCOMPLETE_CYCLES_LIST } from "constants/fetch-keys"; -// types -import { ICycle } from "types"; +// constants +import { EIssuesStoreType } from "constants/issue"; type Props = { isOpen: boolean; handleClose: () => void; }; -const cycleService = new CycleService(); - -export const TransferIssuesModal: React.FC = observer(({ isOpen, handleClose }) => { +export const TransferIssuesModal: React.FC = observer((props) => { + const { isOpen, handleClose } = props; + // states const [query, setQuery] = useState(""); - const { cycleIssues: cycleIssueStore } = useMobxStore(); + // store hooks + const { currentProjectIncompleteCycleIds, getCycleById } = useCycle(); + const { + issues: { transferIssuesFromCycle }, + } = useIssues(EIssuesStoreType.CYCLE); const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; @@ -34,12 +33,14 @@ export const TransferIssuesModal: React.FC = observer(({ isOpen, handleCl const { setToastAlert } = useToast(); const transferIssue = async (payload: any) => { - await cycleIssueStore - .transferIssuesFromCycle(workspaceSlug as string, projectId as string, cycleId as string, payload) + if (!workspaceSlug || !projectId || !cycleId) return; + + // TODO: import transferIssuesFromCycle from store + await transferIssuesFromCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), payload) .then(() => { setToastAlert({ type: "success", - title: "Issues transfered successfully", + title: "Issues transferred successfully", message: "Issues have been transferred successfully", }); }) @@ -52,17 +53,11 @@ export const TransferIssuesModal: React.FC = observer(({ isOpen, handleCl }); }; - const { data: incompleteCycles } = useSWR( - workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, "incomplete") - : null - ); + const filteredOptions = currentProjectIncompleteCycleIds?.filter((optionId) => { + const cycleDetails = getCycleById(optionId); - const filteredOptions = - query === "" - ? incompleteCycles - : incompleteCycles?.filter((option) => option.name.toLowerCase().includes(query.toLowerCase())); + return cycleDetails?.name.toLowerCase().includes(query.toLowerCase()); + }); // useEffect(() => { // const handleKeyDown = (e: KeyboardEvent) => { @@ -121,26 +116,32 @@ export const TransferIssuesModal: React.FC = observer(({ isOpen, handleCl
{filteredOptions ? ( filteredOptions.length > 0 ? ( - filteredOptions.map((option: ICycle) => ( - - )) + filteredOptions.map((optionId) => { + const cycleDetails = getCycleById(optionId); + + if (!cycleDetails) return; + + return ( + + ); + }) ) : (
diff --git a/web/components/dropdowns/cycle.tsx b/web/components/dropdowns/cycle.tsx new file mode 100644 index 000000000..4d5c60acd --- /dev/null +++ b/web/components/dropdowns/cycle.tsx @@ -0,0 +1,293 @@ +import { Fragment, ReactNode, useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Placement } from "@popperjs/core"; +import { Check, ChevronDown, Search } from "lucide-react"; +// hooks +import { useApplication, useCycle } from "hooks/store"; +// icons +import { ContrastIcon } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { ICycle } from "@plane/types"; +import { TButtonVariants } from "./types"; + +type Props = { + button?: ReactNode; + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + className?: string; + disabled?: boolean; + dropdownArrow?: boolean; + onChange: (val: string | null) => void; + placement?: Placement; + projectId: string; + value: string | null; +}; + +type ButtonProps = { + className?: string; + cycle: ICycle | null; + hideText?: boolean; + dropdownArrow: boolean; +}; + +type DropdownOptions = + | { + value: string | null; + query: string; + content: JSX.Element; + }[] + | undefined; + +const BorderButton = (props: ButtonProps) => { + const { className, cycle, dropdownArrow, hideText = false } = props; + + return ( +
+ + {!hideText && {cycle?.name ?? "Cycle"}} + {dropdownArrow &&
+ ); +}; + +const BackgroundButton = (props: ButtonProps) => { + const { className, cycle, dropdownArrow, hideText = false } = props; + + return ( +
+ + {!hideText && {cycle?.name ?? "Cycle"}} + {dropdownArrow &&
+ ); +}; + +const TransparentButton = (props: ButtonProps) => { + const { className, cycle, dropdownArrow, hideText = false } = props; + + return ( +
+ + {!hideText && {cycle?.name ?? "Cycle"}} + {dropdownArrow &&
+ ); +}; + +export const CycleDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + onChange, + placement, + projectId, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { getProjectCycleIds, fetchAllCycles, getCycleById } = useCycle(); + const cycleIds = getProjectCycleIds(projectId); + + const options: DropdownOptions = cycleIds?.map((cycleId) => { + const cycleDetails = getCycleById(cycleId); + + return { + value: cycleId, + query: `${cycleDetails?.name}`, + content: ( +
+ + {cycleDetails?.name} +
+ ), + }; + }); + options?.unshift({ + value: null, + query: "No cycle", + content: ( +
+ + No cycle +
+ ), + }); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + // fetch cycles of the project if not already present in the store + useEffect(() => { + if (!workspaceSlug) return; + + if (!cycleIds) fetchAllCycles(workspaceSlug, projectId); + }, [cycleIds, fetchAllCycles, projectId, workspaceSlug]); + + const selectedCycle = value ? getCycleById(value) : null; + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matches found

+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}); diff --git a/web/components/dropdowns/date.tsx b/web/components/dropdowns/date.tsx new file mode 100644 index 000000000..e791413b4 --- /dev/null +++ b/web/components/dropdowns/date.tsx @@ -0,0 +1,243 @@ +import React, { useState } from "react"; +import { Popover } from "@headlessui/react"; +import DatePicker from "react-datepicker"; +import { usePopper } from "react-popper"; +import { CalendarDays, X } from "lucide-react"; +// import "react-datepicker/dist/react-datepicker.css"; +// helpers +import { renderFormattedDate } from "helpers/date-time.helper"; +import { cn } from "helpers/common.helper"; +// types +import { TButtonVariants } from "./types"; +import { Placement } from "@popperjs/core"; + +type Props = { + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + disabled?: boolean; + icon?: React.ReactNode; + isClearable?: boolean; + minDate?: Date; + maxDate?: Date; + onChange: (val: Date | null) => void; + placeholder: string; + placement?: Placement; + value: Date | string | null; + closeOnSelect?: boolean; +}; + +type ButtonProps = { + className?: string; + date: string | Date | null; + icon: React.ReactNode; + isClearable: boolean; + hideText?: boolean; + onClear: () => void; + placeholder: string; +}; + +const BorderButton = (props: ButtonProps) => { + const { className, date, icon, isClearable, hideText = false, onClear, placeholder } = props; + + return ( +
+ {icon} + {!hideText && {date ? renderFormattedDate(date) : placeholder}} + {isClearable && ( + { + e.stopPropagation(); + onClear(); + }} + /> + )} +
+ ); +}; + +const BackgroundButton = (props: ButtonProps) => { + const { className, date, icon, isClearable, hideText = false, onClear, placeholder } = props; + + return ( +
+ {icon} + {!hideText && {date ? renderFormattedDate(date) : placeholder}} + {isClearable && ( + { + e.stopPropagation(); + onClear(); + }} + /> + )} +
+ ); +}; + +const TransparentButton = (props: ButtonProps) => { + const { className, date, icon, isClearable, hideText = false, onClear, placeholder } = props; + + return ( +
+ {icon} + {!hideText && {date ? renderFormattedDate(date) : placeholder}} + {isClearable && ( + { + e.stopPropagation(); + onClear(); + }} + /> + )} +
+ ); +}; + +export const DateDropdown: React.FC = (props) => { + const { + buttonClassName = "", + buttonContainerClassName, + buttonVariant, + disabled = false, + icon = , + isClearable = true, + minDate, + maxDate, + onChange, + placeholder, + placement, + value, + closeOnSelect = true, + } = props; + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + const isDateSelected = value !== null && value !== undefined && value.toString().trim() !== ""; + + return ( + + {({ close }) => ( + <> + + + + +
+ { + onChange(val); + if (closeOnSelect) close(); + }} + dateFormat="dd-MM-yyyy" + minDate={minDate} + maxDate={maxDate} + calendarClassName="shadow-custom-shadow-rg rounded" + inline + /> +
+
+ + )} +
+ ); +}; diff --git a/web/components/dropdowns/estimate.tsx b/web/components/dropdowns/estimate.tsx new file mode 100644 index 000000000..8d538f53f --- /dev/null +++ b/web/components/dropdowns/estimate.tsx @@ -0,0 +1,287 @@ +import { Fragment, ReactNode, useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Placement } from "@popperjs/core"; +import { Check, ChevronDown, Search, Triangle } from "lucide-react"; +import sortBy from "lodash/sortBy"; +// hooks +import { useApplication, useEstimate } from "hooks/store"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TButtonVariants } from "./types"; + +type Props = { + button?: ReactNode; + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + className?: string; + disabled?: boolean; + dropdownArrow?: boolean; + onChange: (val: number | null) => void; + placement?: Placement; + projectId: string; + value: number | null; +}; + +type ButtonProps = { + className?: string; + estimatePoint: string | null; + dropdownArrow: boolean; + hideText?: boolean; +}; + +type DropdownOptions = + | { + value: number | null; + query: string; + content: JSX.Element; + }[] + | undefined; + +const BorderButton = (props: ButtonProps) => { + const { className, estimatePoint, dropdownArrow, hideText = false } = props; + + return ( +
+ + {!hideText && {estimatePoint !== null ? estimatePoint : "Estimate"}} + {dropdownArrow &&
+ ); +}; + +const BackgroundButton = (props: ButtonProps) => { + const { className, estimatePoint, dropdownArrow, hideText = false } = props; + + return ( +
+ + {!hideText && {estimatePoint !== null ? estimatePoint : "Estimate"}} + {dropdownArrow &&
+ ); +}; + +const TransparentButton = (props: ButtonProps) => { + const { className, estimatePoint, dropdownArrow, hideText = false } = props; + + return ( +
+ + {!hideText && {estimatePoint !== null ? estimatePoint : "Estimate"}} + {dropdownArrow &&
+ ); +}; + +export const EstimateDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + onChange, + placement, + projectId, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { fetchProjectEstimates, getProjectActiveEstimateDetails, getEstimatePointValue } = useEstimate(); + const activeEstimate = getProjectActiveEstimateDetails(projectId); + + const options: DropdownOptions = sortBy(activeEstimate?.points ?? [], "key")?.map((point) => ({ + value: point.key, + query: `${point?.value}`, + content: ( +
+ + {point.value} +
+ ), + })); + options?.unshift({ + value: null, + query: "No estimate", + content: ( +
+ + No estimate +
+ ), + }); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + // fetch cycles of the project if not already present in the store + useEffect(() => { + if (!workspaceSlug) return; + + if (!activeEstimate) fetchProjectEstimates(workspaceSlug, projectId); + }, [activeEstimate, fetchProjectEstimates, projectId, workspaceSlug]); + + const selectedEstimate = value !== null ? getEstimatePointValue(value) : null; + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}); diff --git a/web/components/dropdowns/index.ts b/web/components/dropdowns/index.ts new file mode 100644 index 000000000..036ed9f75 --- /dev/null +++ b/web/components/dropdowns/index.ts @@ -0,0 +1,8 @@ +export * from "./member"; +export * from "./cycle"; +export * from "./date"; +export * from "./estimate"; +export * from "./module"; +export * from "./priority"; +export * from "./project"; +export * from "./state"; diff --git a/web/components/dropdowns/member/buttons.tsx b/web/components/dropdowns/member/buttons.tsx new file mode 100644 index 000000000..53003473d --- /dev/null +++ b/web/components/dropdowns/member/buttons.tsx @@ -0,0 +1,113 @@ +import { observer } from "mobx-react-lite"; +import { ChevronDown } from "lucide-react"; +// hooks +import { useMember } from "hooks/store"; +// ui +import { Avatar, AvatarGroup, UserGroupIcon } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; + +type ButtonProps = { + className?: string; + dropdownArrow: boolean; + placeholder: string; + hideText?: boolean; + userIds: string | string[] | null; +}; + +const ButtonAvatars = observer(({ userIds }: { userIds: string | string[] | null }) => { + const { getUserDetails } = useMember(); + + if (Array.isArray(userIds)) { + if (userIds.length > 0) + return ( + + {userIds.map((userId) => { + const userDetails = getUserDetails(userId); + + if (!userDetails) return; + return ; + })} + + ); + } else { + if (userIds) { + const userDetails = getUserDetails(userIds); + return ; + } + } + + return ; +}); + +export const BorderButton = observer((props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, placeholder, userIds } = props; + // store hooks + const { getUserDetails } = useMember(); + + const isMultiple = Array.isArray(userIds); + + return ( +
+ + {!hideText && ( + + {userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder} + + )} + {dropdownArrow &&
+ ); +}); + +export const BackgroundButton = observer((props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, placeholder, userIds } = props; + // store hooks + const { getUserDetails } = useMember(); + + const isMultiple = Array.isArray(userIds); + + return ( +
+ + {!hideText && ( + + {userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder} + + )} + {dropdownArrow &&
+ ); +}); + +export const TransparentButton = observer((props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, placeholder, userIds } = props; + // store hooks + const { getUserDetails } = useMember(); + + const isMultiple = Array.isArray(userIds); + + return ( +
+ + {!hideText && ( + + {userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder} + + )} + {dropdownArrow &&
+ ); +}); diff --git a/web/components/dropdowns/member/index.ts b/web/components/dropdowns/member/index.ts new file mode 100644 index 000000000..bc976b46a --- /dev/null +++ b/web/components/dropdowns/member/index.ts @@ -0,0 +1,3 @@ +export * from "./buttons"; +export * from "./project-member"; +export * from "./workspace-member"; diff --git a/web/components/dropdowns/member/project-member.tsx b/web/components/dropdowns/member/project-member.tsx new file mode 100644 index 000000000..18d317a56 --- /dev/null +++ b/web/components/dropdowns/member/project-member.tsx @@ -0,0 +1,224 @@ +import { Fragment, useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Check, Search } from "lucide-react"; +// hooks +import { useApplication, useMember, useUser } from "hooks/store"; +// components +import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns"; +// icons +import { Avatar } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { MemberDropdownProps } from "./types"; + +type Props = { + projectId: string; +} & MemberDropdownProps; + +export const ProjectMemberDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + multiple, + onChange, + placeholder = "Members", + placement, + projectId, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { currentUser } = useUser(); + const { + getUserDetails, + project: { getProjectMemberIds, fetchProjectMembers }, + } = useMember(); + const projectMemberIds = getProjectMemberIds(projectId); + + const options = projectMemberIds?.map((userId) => { + const userDetails = getUserDetails(userId); + + return { + value: userId, + query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`, + content: ( +
+ + {currentUser?.id === userId ? "You" : userDetails?.display_name} +
+ ), + }; + }); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + const comboboxProps: any = { + value, + onChange, + disabled, + }; + if (multiple) comboboxProps.multiple = true; + + useEffect(() => { + if (!workspaceSlug) return; + + if (!projectMemberIds) fetchProjectMembers(workspaceSlug, projectId); + }, [fetchProjectMembers, projectId, projectMemberIds, workspaceSlug]); + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}); diff --git a/web/components/dropdowns/member/types.d.ts b/web/components/dropdowns/member/types.d.ts new file mode 100644 index 000000000..4c0bff67b --- /dev/null +++ b/web/components/dropdowns/member/types.d.ts @@ -0,0 +1,25 @@ +import { Placement } from "@popperjs/core"; +import { TButtonVariants } from "../types"; + +export type MemberDropdownProps = { + button?: ReactNode; + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + className?: string; + disabled?: boolean; + dropdownArrow?: boolean; + placeholder?: string; + placement?: Placement; +} & ( + | { + multiple: false; + onChange: (val: string | null) => void; + value: string | null; + } + | { + multiple: true; + onChange: (val: string[]) => void; + value: string[]; + } +); diff --git a/web/components/dropdowns/member/workspace-member.tsx b/web/components/dropdowns/member/workspace-member.tsx new file mode 100644 index 000000000..f50da72c8 --- /dev/null +++ b/web/components/dropdowns/member/workspace-member.tsx @@ -0,0 +1,209 @@ +import { Fragment, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Check, Search } from "lucide-react"; +// hooks +import { useMember, useUser } from "hooks/store"; +// components +import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns"; +// icons +import { Avatar } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { MemberDropdownProps } from "./types"; + +export const WorkspaceMemberDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + multiple, + onChange, + placeholder = "Members", + placement, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // store hooks + const { currentUser } = useUser(); + const { + getUserDetails, + workspace: { workspaceMemberIds }, + } = useMember(); + + const options = workspaceMemberIds?.map((userId) => { + const userDetails = getUserDetails(userId); + + return { + value: userId, + query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`, + content: ( +
+ + {currentUser?.id === userId ? "You" : userDetails?.display_name} +
+ ), + }; + }); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + const comboboxProps: any = { + value, + onChange, + disabled, + }; + if (multiple) comboboxProps.multiple = true; + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}); diff --git a/web/components/dropdowns/module.tsx b/web/components/dropdowns/module.tsx new file mode 100644 index 000000000..ff35c26b6 --- /dev/null +++ b/web/components/dropdowns/module.tsx @@ -0,0 +1,293 @@ +import { Fragment, ReactNode, useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Placement } from "@popperjs/core"; +import { Check, ChevronDown, Search } from "lucide-react"; +// hooks +import { useApplication, useModule } from "hooks/store"; +// icons +import { DiceIcon } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { IModule } from "@plane/types"; +import { TButtonVariants } from "./types"; + +type Props = { + button?: ReactNode; + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + className?: string; + disabled?: boolean; + dropdownArrow?: boolean; + onChange: (val: string | null) => void; + placement?: Placement; + projectId: string; + value: string | null; +}; + +type DropdownOptions = + | { + value: string | null; + query: string; + content: JSX.Element; + }[] + | undefined; + +type ButtonProps = { + className?: string; + dropdownArrow: boolean; + hideText?: boolean; + module: IModule | null; +}; + +const BorderButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, module } = props; + + return ( +
+ + {!hideText && {module?.name ?? "Module"}} + {dropdownArrow &&
+ ); +}; + +const BackgroundButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, module } = props; + + return ( +
+ + {!hideText && {module?.name ?? "Module"}} + {dropdownArrow &&
+ ); +}; + +const TransparentButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, module } = props; + + return ( +
+ + {!hideText && {module?.name ?? "Module"}} + {dropdownArrow &&
+ ); +}; + +export const ModuleDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + onChange, + placement, + projectId, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { getProjectModuleIds, fetchModules, getModuleById } = useModule(); + const moduleIds = getProjectModuleIds(projectId); + + const options: DropdownOptions = moduleIds?.map((moduleId) => { + const moduleDetails = getModuleById(moduleId); + + return { + value: moduleId, + query: `${moduleDetails?.name}`, + content: ( +
+ + {moduleDetails?.name} +
+ ), + }; + }); + options?.unshift({ + value: null, + query: "No module", + content: ( +
+ + No module +
+ ), + }); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + // fetch modules of the project if not already present in the store + useEffect(() => { + if (!workspaceSlug) return; + + if (!moduleIds) fetchModules(workspaceSlug, projectId); + }, [moduleIds, fetchModules, projectId, workspaceSlug]); + + const selectedModule = value ? getModuleById(value) : null; + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}); diff --git a/web/components/dropdowns/priority.tsx b/web/components/dropdowns/priority.tsx new file mode 100644 index 000000000..5c467a7b6 --- /dev/null +++ b/web/components/dropdowns/priority.tsx @@ -0,0 +1,398 @@ +import { Fragment, ReactNode, useState } from "react"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Placement } from "@popperjs/core"; +import { Check, ChevronDown, Search } from "lucide-react"; +// icons +import { PriorityIcon } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TIssuePriorities } from "@plane/types"; +import { TButtonVariants } from "./types"; +// constants +import { ISSUE_PRIORITIES } from "constants/issue"; + +type Props = { + button?: ReactNode; + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + className?: string; + disabled?: boolean; + dropdownArrow?: boolean; + highlightUrgent?: boolean; + onChange: (val: TIssuePriorities) => void; + placement?: Placement; + value: TIssuePriorities; +}; + +type ButtonProps = { + className?: string; + dropdownArrow: boolean; + hideText?: boolean; + highlightUrgent: boolean; + priority: TIssuePriorities; +}; + +const BorderButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, highlightUrgent, priority } = props; + + const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority); + + const priorityClasses = { + urgent: "bg-red-500/20 text-red-950 border-red-500", + high: "bg-orange-500/20 text-orange-950 border-orange-500", + medium: "bg-yellow-500/20 text-yellow-950 border-yellow-500", + low: "bg-custom-primary-100/20 text-custom-primary-950 border-custom-primary-100", + none: "bg-custom-background-80 border-custom-border-300", + }; + + return ( + + ); +}; + +const BackgroundButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, highlightUrgent, priority } = props; + + const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority); + + const priorityClasses = { + urgent: "bg-red-500/20 text-red-950", + high: "bg-orange-500/20 text-orange-950", + medium: "bg-yellow-500/20 text-yellow-950", + low: "bg-blue-500/20 text-blue-950", + none: "bg-custom-background-80", + }; + + return ( + + ); +}; + +const TransparentButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, highlightUrgent, priority } = props; + + const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority); + + const priorityClasses = { + urgent: "text-red-950", + high: "text-orange-950", + medium: "text-yellow-950", + low: "text-blue-950", + none: "", + }; + + return ( + + ); +}; + +export const PriorityDropdown: React.FC = (props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + highlightUrgent = true, + onChange, + placement, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + const options = ISSUE_PRIORITIES.map((priority) => { + const priorityClasses = { + urgent: "bg-red-500/20 text-red-950 border-red-500", + high: "bg-orange-500/20 text-orange-950 border-orange-500", + medium: "bg-yellow-500/20 text-yellow-950 border-yellow-500", + low: "bg-custom-primary-100/20 text-custom-primary-950 border-custom-primary-100", + none: "bg-custom-background-80 border-custom-border-300", + }; + + return { + value: priority.key, + query: priority.key, + content: ( +
+
+
+ {priority.title} +
+ ), + }; + }); + + const filteredOptions = + query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ )} +
+
+
+
+ ); +}; diff --git a/web/components/dropdowns/project.tsx b/web/components/dropdowns/project.tsx new file mode 100644 index 000000000..65169dd88 --- /dev/null +++ b/web/components/dropdowns/project.tsx @@ -0,0 +1,273 @@ +import { Fragment, ReactNode, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Placement } from "@popperjs/core"; +import { Check, ChevronDown, Search } from "lucide-react"; +// hooks +import { useProject } from "hooks/store"; +// helpers +import { cn } from "helpers/common.helper"; +import { renderEmoji } from "helpers/emoji.helper"; +// types +import { IProject } from "@plane/types"; +import { TButtonVariants } from "./types"; + +type Props = { + button?: ReactNode; + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + className?: string; + disabled?: boolean; + dropdownArrow?: boolean; + onChange: (val: string) => void; + placement?: Placement; + value: string | null; +}; + +type ButtonProps = { + className?: string; + dropdownArrow: boolean; + hideText?: boolean; + project: IProject | null; +}; + +const BorderButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, project } = props; + + return ( +
+ + {project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null} + + {!hideText && {project?.name ?? "Project"}} + {dropdownArrow &&
+ ); +}; + +const BackgroundButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, project } = props; + + return ( +
+ + {project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null} + + {!hideText && {project?.name ?? "Project"}} + {dropdownArrow &&
+ ); +}; + +const TransparentButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, project } = props; + + return ( +
+ + {project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null} + + {!hideText && {project?.name ?? "Project"}} + {dropdownArrow &&
+ ); +}; + +export const ProjectDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + onChange, + placement, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // store hooks + const { joinedProjectIds, getProjectById } = useProject(); + + const options = joinedProjectIds?.map((projectId) => { + const projectDetails = getProjectById(projectId); + + return { + value: projectId, + query: `${projectDetails?.name}`, + content: ( +
+ + {projectDetails?.emoji + ? renderEmoji(projectDetails?.emoji) + : projectDetails?.icon_prop + ? renderEmoji(projectDetails?.icon_prop) + : null} + + {projectDetails?.name} +
+ ), + }; + }); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + const selectedProject = value ? getProjectById(value) : null; + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}); diff --git a/web/components/dropdowns/state.tsx b/web/components/dropdowns/state.tsx new file mode 100644 index 000000000..c7ba9eced --- /dev/null +++ b/web/components/dropdowns/state.tsx @@ -0,0 +1,271 @@ +import { Fragment, ReactNode, useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Placement } from "@popperjs/core"; +import { Check, ChevronDown, Search } from "lucide-react"; +// hooks +import { useApplication, useProjectState } from "hooks/store"; +// icons +import { StateGroupIcon } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { IState } from "@plane/types"; +import { TButtonVariants } from "./types"; + +type Props = { + button?: ReactNode; + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + className?: string; + disabled?: boolean; + dropdownArrow?: boolean; + onChange: (val: string) => void; + placement?: Placement; + projectId: string; + value: string; +}; + +type ButtonProps = { + className?: string; + dropdownArrow: boolean; + hideText?: boolean; + state: IState | undefined; +}; + +const BorderButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, state } = props; + + return ( +
+ + {!hideText && {state?.name ?? "State"}} + {dropdownArrow &&
+ ); +}; + +const BackgroundButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, state } = props; + + return ( +
+ + {!hideText && {state?.name ?? "State"}} + {dropdownArrow &&
+ ); +}; + +const TransparentButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, state } = props; + + return ( +
+ + {!hideText && {state?.name ?? "State"}} + {dropdownArrow &&
+ ); +}; + +export const StateDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + onChange, + placement, + projectId, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { fetchProjectStates, getProjectStates, getStateById } = useProjectState(); + const statesList = getProjectStates(projectId); + + const options = statesList?.map((state) => ({ + value: state.id, + query: `${state?.name}`, + content: ( +
+ + {state?.name} +
+ ), + })); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + // fetch states of the project if not already present in the store + useEffect(() => { + if (!workspaceSlug) return; + + if (!statesList) fetchProjectStates(workspaceSlug, projectId); + }, [fetchProjectStates, projectId, statesList, workspaceSlug]); + + const selectedState = getStateById(value); + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matches found

+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}); diff --git a/web/components/dropdowns/types.d.ts b/web/components/dropdowns/types.d.ts new file mode 100644 index 000000000..f23914daa --- /dev/null +++ b/web/components/dropdowns/types.d.ts @@ -0,0 +1,7 @@ +export type TButtonVariants = + | "border-with-text" + | "border-without-text" + | "background-with-text" + | "background-without-text" + | "transparent-with-text" + | "transparent-without-text"; diff --git a/web/components/estimates/create-update-estimate-modal.tsx b/web/components/estimates/create-update-estimate-modal.tsx index b24172688..0a607e88d 100644 --- a/web/components/estimates/create-update-estimate-modal.tsx +++ b/web/components/estimates/create-update-estimate-modal.tsx @@ -2,18 +2,16 @@ import React, { useEffect } from "react"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; - -// store import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; -// hooks +// store hooks +import { useEstimate } from "hooks/store"; import useToast from "hooks/use-toast"; // ui import { Button, Input, TextArea } from "@plane/ui"; // helpers import { checkDuplicates } from "helpers/array.helper"; // types -import { IEstimate, IEstimateFormData } from "types"; +import { IEstimate, IEstimateFormData } from "@plane/types"; type Props = { isOpen: boolean; @@ -36,16 +34,14 @@ type FormValues = typeof defaultValues; export const CreateUpdateEstimateModal: React.FC = observer((props) => { const { handleClose, data, isOpen } = props; - // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - - // store - const { - projectEstimates: { createEstimate, updateEstimate }, - } = useMobxStore(); - + // store hooks + const { createEstimate, updateEstimate } = useEstimate(); + // form info + // toast alert + const { setToastAlert } = useToast(); const { formState: { errors, isSubmitting }, handleSubmit, @@ -60,8 +56,6 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { reset(); }; - const { setToastAlert } = useToast(); - const handleCreateEstimate = async (payload: IEstimateFormData) => { if (!workspaceSlug || !projectId) return; diff --git a/web/components/estimates/delete-estimate-modal.tsx b/web/components/estimates/delete-estimate-modal.tsx index c9d34fe8e..8055ddb90 100644 --- a/web/components/estimates/delete-estimate-modal.tsx +++ b/web/components/estimates/delete-estimate-modal.tsx @@ -1,15 +1,13 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -// store import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; -// hooks +import { AlertTriangle } from "lucide-react"; +// store hooks +import { useEstimate } from "hooks/store"; import useToast from "hooks/use-toast"; // types -import { IEstimate } from "types"; -// icons -import { AlertTriangle } from "lucide-react"; +import { IEstimate } from "@plane/types"; // ui import { Button } from "@plane/ui"; @@ -21,18 +19,14 @@ type Props = { export const DeleteEstimateModal: React.FC = observer((props) => { const { isOpen, handleClose, data } = props; - + // states + const [isDeleteLoading, setIsDeleteLoading] = useState(false); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - - // store - const { projectEstimates: projectEstimatesStore } = useMobxStore(); - - // states - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - // hooks + // store hooks + const { deleteEstimate } = useEstimate(); + // toast alert const { setToastAlert } = useToast(); const handleEstimateDelete = () => { @@ -40,8 +34,7 @@ export const DeleteEstimateModal: React.FC = observer((props) => { const estimateId = data?.id!; - projectEstimatesStore - .deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId) + deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId) .then(() => { setIsDeleteLoading(false); handleClose(); diff --git a/web/components/estimates/estimate-list-item.tsx b/web/components/estimates/estimate-list-item.tsx index 65764e5d2..b6effa711 100644 --- a/web/components/estimates/estimate-list-item.tsx +++ b/web/components/estimates/estimate-list-item.tsx @@ -1,11 +1,8 @@ import React from "react"; - import { useRouter } from "next/router"; - -// store import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { useProject } from "hooks/store"; import useToast from "hooks/use-toast"; // ui import { Button, CustomMenu } from "@plane/ui"; @@ -14,7 +11,7 @@ import { Pencil, Trash2 } from "lucide-react"; // helpers import { orderArrayBy } from "helpers/array.helper"; // types -import { IEstimate } from "types"; +import { IEstimate } from "@plane/types"; type Props = { estimate: IEstimate; @@ -27,10 +24,8 @@ export const EstimateListItem: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // store - const { - project: { currentProjectDetails, updateProject }, - } = useMobxStore(); + // store hooks + const { currentProjectDetails, updateProject } = useProject(); // hooks const { setToastAlert } = useToast(); diff --git a/web/components/estimates/estimate-select.tsx b/web/components/estimates/estimate-select.tsx deleted file mode 100644 index e7d656eca..000000000 --- a/web/components/estimates/estimate-select.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React, { useState } from "react"; -import { usePopper } from "react-popper"; -import { Combobox } from "@headlessui/react"; -import { Check, ChevronDown, Search, Triangle } from "lucide-react"; -// types -import { Tooltip } from "@plane/ui"; -import { Placement } from "@popperjs/core"; -// constants -import { IEstimatePoint } from "types"; - -type Props = { - value: number | null; - onChange: (value: number | null) => void; - estimatePoints: IEstimatePoint[] | undefined; - className?: string; - buttonClassName?: string; - optionsClassName?: string; - placement?: Placement; - hideDropdownArrow?: boolean; - disabled?: boolean; -}; - -export const EstimateSelect: React.FC = (props) => { - const { - value, - onChange, - estimatePoints, - className = "", - buttonClassName = "", - optionsClassName = "", - placement, - hideDropdownArrow = false, - disabled = false, - } = props; - - const [query, setQuery] = useState(""); - - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "bottom-start", - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 12, - }, - }, - ], - }); - - const options: { value: number | null; query: string; content: any }[] | undefined = estimatePoints?.map( - (estimate) => ({ - value: estimate.key, - query: estimate.value, - content: ( -
- - {estimate.value} -
- ), - }) - ); - options?.unshift({ - value: null, - query: "none", - content: ( -
- - None -
- ), - }); - - const filteredOptions = - query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); - - const selectedEstimate = estimatePoints?.find((e) => e.key === value); - const label = ( - -
- - {selectedEstimate?.value ?? "None"} -
-
- ); - - return ( - onChange(val as number | null)} - disabled={disabled} - > - - - - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) - ) : ( - -

No matching results

-
- ) - ) : ( -

Loading...

- )} -
-
-
-
- ); -}; diff --git a/web/components/estimates/estimates-list.tsx b/web/components/estimates/estimates-list.tsx index 29debfedb..323cbe888 100644 --- a/web/components/estimates/estimates-list.tsx +++ b/web/components/estimates/estimates-list.tsx @@ -1,40 +1,33 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -// store import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; +import { Plus } from "lucide-react"; +// store hooks +import { useEstimate, useProject } from "hooks/store"; +import useToast from "hooks/use-toast"; // components import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; -//hooks -import useToast from "hooks/use-toast"; // ui import { Button, Loader } from "@plane/ui"; import { EmptyState } from "components/common"; -// icons -import { Plus } from "lucide-react"; // images import emptyEstimate from "public/empty-state/estimate.svg"; // types -import { IEstimate } from "types"; +import { IEstimate } from "@plane/types"; export const EstimatesList: React.FC = observer(() => { - // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - // store - const { - project: { currentProjectDetails, updateProject }, - projectEstimates: { projectEstimates, getProjectEstimateById }, - } = useMobxStore(); // states const [estimateFormOpen, setEstimateFormOpen] = useState(false); const [estimateToDelete, setEstimateToDelete] = useState(null); const [estimateToUpdate, setEstimateToUpdate] = useState(); - // hooks + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + // store hooks + const { updateProject, currentProjectDetails } = useProject(); + const { projectEstimates, getProjectEstimateById } = useEstimate(); + // toast alert const { setToastAlert } = useToast(); - // derived values - const estimatesList = projectEstimates; const editEstimate = (estimate: IEstimate) => { setEstimateFormOpen(true); @@ -96,10 +89,10 @@ export const EstimatesList: React.FC = observer(() => {
- {estimatesList ? ( - estimatesList.length > 0 ? ( + {projectEstimates ? ( + projectEstimates.length > 0 ? (
- {estimatesList.map((estimate) => ( + {projectEstimates.map((estimate) => ( void; data: IImporterService | null; - user: IUser | undefined; + user: IUser | null; provider: string | string[]; mutateServices: () => void; }; @@ -26,28 +26,30 @@ const projectExportService = new ProjectExportService(); export const Exporter: React.FC = observer((props) => { const { isOpen, handleClose, user, provider, mutateServices } = props; - + // states const [exportLoading, setExportLoading] = useState(false); - + // router const router = useRouter(); const { workspaceSlug } = router.query; - - const { project: projectStore } = useMobxStore(); - - const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined; - + // store hooks + const { workspaceProjectIds, getProjectById } = useProject(); + // toast alert const { setToastAlert } = useToast(); - const options = projects?.map((project) => ({ - value: project.id, - query: project.name + project.identifier, - content: ( -
- {project.identifier} - {project.name} -
- ), - })); + const options = workspaceProjectIds?.map((projectId) => { + const projectDetails = getProjectById(projectId); + + return { + value: projectDetails?.id, + query: `${projectDetails?.name} ${projectDetails?.identifier}`, + content: ( +
+ {projectDetails?.identifier} + {projectDetails?.name} +
+ ), + }; + }); const [value, setValue] = React.useState([]); const [multiple, setMultiple] = React.useState(false); @@ -131,10 +133,12 @@ export const Exporter: React.FC = observer((props) => { input label={ value && value.length > 0 - ? projects && - projects - .filter((p) => value.includes(p.id)) - .map((p) => p.identifier) + ? value + .map((projectId) => { + const projectDetails = getProjectById(projectId); + + return projectDetails?.identifier; + }) .join(", ") : "All projects" } diff --git a/web/components/exporter/guide.tsx b/web/components/exporter/guide.tsx index fbbf92c26..87bf0604a 100644 --- a/web/components/exporter/guide.tsx +++ b/web/components/exporter/guide.tsx @@ -20,19 +20,24 @@ import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; import { EXPORT_SERVICES_LIST } from "constants/fetch-keys"; // constants import { EXPORTERS_LIST } from "constants/workspace"; +import { observer } from "mobx-react-lite"; +import { useUser } from "hooks/store"; // services const integrationService = new IntegrationService(); -const IntegrationGuide = () => { +const IntegrationGuide = observer(() => { + // states const [refreshing, setRefreshing] = useState(false); const per_page = 10; const [cursor, setCursor] = useState(`10:0:0`); - + // router const router = useRouter(); const { workspaceSlug, provider } = router.query; - - const { user } = useUserAuth(); + // store hooks + const { currentUser, currentUserLoader } = useUser(); + // custom hooks + const {} = useUserAuth({ user: currentUser, isLoading: currentUserLoader }); const { data: exporterServices } = useSWR( workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null, @@ -153,7 +158,7 @@ const IntegrationGuide = () => { isOpen handleClose={() => handleCsvClose()} data={null} - user={user} + user={currentUser} provider={provider} mutateServices={() => mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`))} /> @@ -161,6 +166,6 @@ const IntegrationGuide = () => {
); -}; +}); export default IntegrationGuide; diff --git a/web/components/exporter/single-export.tsx b/web/components/exporter/single-export.tsx index d2502cefb..34e41fc35 100644 --- a/web/components/exporter/single-export.tsx +++ b/web/components/exporter/single-export.tsx @@ -4,7 +4,7 @@ import { Button } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; // types -import { IExportData } from "types"; +import { IExportData } from "@plane/types"; type Props = { service: IExportData; @@ -38,12 +38,12 @@ export const SingleExport: FC = ({ service, refreshing }) => { service.status === "completed" ? "bg-green-500/20 text-green-500" : service.status === "processing" - ? "bg-yellow-500/20 text-yellow-500" - : service.status === "failed" - ? "bg-red-500/20 text-red-500" - : service.status === "expired" - ? "bg-orange-500/20 text-orange-500" - : "" + ? "bg-yellow-500/20 text-yellow-500" + : service.status === "failed" + ? "bg-red-500/20 text-red-500" + : service.status === "expired" + ? "bg-orange-500/20 text-orange-500" + : "" }`} > {refreshing ? "Refreshing..." : service.status} diff --git a/web/components/gantt-chart/helpers/block-structure.tsx b/web/components/gantt-chart/helpers/block-structure.tsx index ea51c3b12..bc59624a5 100644 --- a/web/components/gantt-chart/helpers/block-structure.tsx +++ b/web/components/gantt-chart/helpers/block-structure.tsx @@ -1,8 +1,8 @@ // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { IGanttBlock } from "components/gantt-chart"; -export const renderIssueBlocksStructure = (blocks: IIssue[]): IGanttBlock[] => +export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] => blocks && blocks.length > 0 ? blocks .filter((b) => new Date(b?.start_date ?? "") <= new Date(b?.target_date ?? "")) diff --git a/web/components/gantt-chart/sidebar/sidebar.tsx b/web/components/gantt-chart/sidebar/sidebar.tsx index 0d4309cf8..c366bcfed 100644 --- a/web/components/gantt-chart/sidebar/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/sidebar.tsx @@ -12,7 +12,7 @@ import { GanttInlineCreateIssueForm, IssueGanttSidebarBlock } from "components/i import { findTotalDaysInRange } from "helpers/date-time.helper"; // types import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types"; -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; type Props = { blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; @@ -22,9 +22,9 @@ type Props = { quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; viewId?: string; disableIssueCreation?: boolean; }; diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 2526199b5..7873ea691 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -1,9 +1,17 @@ import { useCallback, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { + useApplication, + useCycle, + useLabel, + useMember, + useProject, + useProjectState, + useUser, + useIssues, +} from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; @@ -16,37 +24,67 @@ import { ArrowRight, Plus } from "lucide-react"; import { truncateText } from "helpers/string.helper"; import { renderEmoji } from "helpers/emoji.helper"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EFilterType } from "store/issues/types"; -import { EProjectStore } from "store/command-palette.store"; -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; + +const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => { + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + // store hooks + const { getCycleById } = useCycle(); + // derived values + const cycle = getCycleById(cycleId); + + if (!cycle) return null; + + return ( + router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)} + > +
+ + {truncateText(cycle.name, 40)} +
+
+ ); +}; export const CycleIssuesHeader: React.FC = observer(() => { + // states const [analyticsModal, setAnalyticsModal] = useState(false); - + // router const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query as { workspaceSlug: string; projectId: string; cycleId: string; }; - + // store hooks const { - cycle: cycleStore, - projectIssuesFilter: projectIssueFiltersStore, - project: { currentProjectDetails }, - projectMember: { projectMembers }, - projectLabel: { projectLabels }, - projectState: projectStateStore, - commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - cycleIssuesFilter: { issueFilters, updateFilters }, - user: { currentProjectRole }, - } = useMobxStore(); + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.CYCLE); + const { currentProjectCycleIds, getCycleById } = useCycle(); + const { + commandPalette: { toggleCreateIssueModal }, + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); + const { currentProjectDetails } = useProject(); + const { projectStates } = useProjectState(); + const { + project: { projectLabels }, + } = useLabel(); + const { + project: { projectMemberIds }, + } = useMember(); - const activeLayout = projectIssueFiltersStore.issueFilters?.displayFilters?.layout; + const activeLayout = issueFilters?.displayFilters?.layout; const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); @@ -58,7 +96,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId); }, [workspaceSlug, projectId, cycleId, updateFilters] ); @@ -77,7 +115,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { else newValues.push(value); } - updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { [key]: newValues }, cycleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, cycleId); }, [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] ); @@ -85,7 +123,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId); }, [workspaceSlug, projectId, cycleId, updateFilters] ); @@ -93,16 +131,15 @@ export const CycleIssuesHeader: React.FC = observer(() => { const handleDisplayProperties = useCallback( (property: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_PROPERTIES, property, cycleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, cycleId); }, [workspaceSlug, projectId, cycleId, updateFilters] ); - const cyclesList = cycleStore.projectCycles; - const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined; - + // derived values + const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; const canUserCreateIssue = - currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole); + currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); return ( <> @@ -150,16 +187,8 @@ export const CycleIssuesHeader: React.FC = observer(() => { width="auto" placement="bottom-start" > - {cyclesList?.map((cycle) => ( - router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)} - > -
- - {truncateText(cycle.name, 40)} -
-
+ {currentProjectCycleIds?.map((cycleId) => ( + ))} } @@ -179,9 +208,9 @@ export const CycleIssuesHeader: React.FC = observer(() => { layoutDisplayFiltersOptions={ activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined } - labels={projectLabels ?? undefined} - members={projectMembers?.map((m) => m.member)} - states={projectStateStore.states?.[projectId ?? ""] ?? undefined} + labels={projectLabels} + memberIds={projectMemberIds ?? undefined} + states={projectStates} /> @@ -204,7 +233,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { diff --git a/web/components/headers/projects.tsx b/web/components/headers/projects.tsx index 370dfe6d4..825af560d 100644 --- a/web/components/headers/projects.tsx +++ b/web/components/headers/projects.tsx @@ -1,26 +1,22 @@ -import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; import { Search, Plus, Briefcase } from "lucide-react"; +// hooks +import { useApplication, useProject, useUser } from "hooks/store"; // ui import { Breadcrumbs, Button } from "@plane/ui"; -// hooks -import { useMobxStore } from "lib/mobx/store-provider"; -import { observer } from "mobx-react-lite"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; export const ProjectsHeader = observer(() => { - const router = useRouter(); - const { workspaceSlug } = router.query; - - // store + // store hooks const { - project: projectStore, commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - user: { currentWorkspaceRole }, - } = useMobxStore(); - - const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : []; + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentWorkspaceRole }, + } = useUser(); + const { workspaceProjectIds, searchQuery, setSearchQuery } = useProject(); const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; @@ -38,13 +34,13 @@ export const ProjectsHeader = observer(() => {
- {projectsList?.length > 0 && ( + {workspaceProjectIds && workspaceProjectIds?.length > 0 && (
projectStore.setSearchQuery(e.target.value)} + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search" />
diff --git a/web/components/icons/module/module-status-icon.tsx b/web/components/icons/module/module-status-icon.tsx index 303d0f765..a8e87e55c 100644 --- a/web/components/icons/module/module-status-icon.tsx +++ b/web/components/icons/module/module-status-icon.tsx @@ -8,7 +8,7 @@ import { ModulePlannedIcon, } from "components/icons"; // types -import { TModuleStatus } from "types"; +import { TModuleStatus } from "@plane/types"; type Props = { status: TModuleStatus; diff --git a/web/components/icons/priority-icon.tsx b/web/components/icons/priority-icon.tsx index 44248a438..b23f56eab 100644 --- a/web/components/icons/priority-icon.tsx +++ b/web/components/icons/priority-icon.tsx @@ -1,5 +1,5 @@ // types -import { TIssuePriorities } from "types"; +import { TIssuePriorities } from "@plane/types"; type Props = { priority: TIssuePriorities | null; @@ -14,12 +14,12 @@ export const PriorityIcon: React.FC = ({ priority, className = "" }) => { {priority === "urgent" ? "error" : priority === "high" - ? "signal_cellular_alt" - : priority === "medium" - ? "signal_cellular_alt_2_bar" - : priority === "low" - ? "signal_cellular_alt_1_bar" - : "block"} + ? "signal_cellular_alt" + : priority === "medium" + ? "signal_cellular_alt_2_bar" + : priority === "low" + ? "signal_cellular_alt_1_bar" + : "block"} ); }; diff --git a/web/components/icons/state/state-group-icon.tsx b/web/components/icons/state/state-group-icon.tsx index df3b57dd8..c408333f1 100644 --- a/web/components/icons/state/state-group-icon.tsx +++ b/web/components/icons/state/state-group-icon.tsx @@ -7,7 +7,7 @@ import { StateGroupUnstartedIcon, } from "components/icons"; // types -import { TStateGroups } from "types"; +import { TStateGroups } from "@plane/types"; // constants import { STATE_GROUP_COLORS } from "constants/state"; diff --git a/web/components/inbox/actions-header.tsx b/web/components/inbox/actions-header.tsx index 47f0317f4..cab4be600 100644 --- a/web/components/inbox/actions-header.tsx +++ b/web/components/inbox/actions-header.tsx @@ -3,10 +3,8 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import DatePicker from "react-datepicker"; import { Popover } from "@headlessui/react"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { useUser, useInboxIssues } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { @@ -19,51 +17,51 @@ import { // ui import { Button } from "@plane/ui"; // icons -import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Inbox, Trash2, XCircle } from "lucide-react"; +import { CheckCircle2, Clock, FileStack, Inbox, Trash2, XCircle } from "lucide-react"; // types -import type { TInboxStatus } from "types"; -import { EUserWorkspaceRoles } from "constants/workspace"; +import type { TInboxStatus } from "@plane/types"; +import { EUserProjectRoles } from "constants/project"; export const InboxActionsHeader = observer(() => { + // states const [date, setDate] = useState(new Date()); const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false); const [acceptIssueModal, setAcceptIssueModal] = useState(false); const [declineIssueModal, setDeclineIssueModal] = useState(false); const [deleteIssueModal, setDeleteIssueModal] = useState(false); - + // router const router = useRouter(); const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query; - - const { inboxIssues: inboxIssuesStore, inboxIssueDetails: inboxIssueDetailsStore, user: userStore } = useMobxStore(); - - const user = userStore?.currentUser; - const userRole = userStore.currentProjectRole; - const issuesList = inboxId ? inboxIssuesStore.inboxIssues[inboxId.toString()] : null; - + // store hooks + const { updateIssueStatus, getIssueById } = useInboxIssues(); + const { + currentUser, + membership: { currentProjectRole }, + } = useUser(); + // toast const { setToastAlert } = useToast(); + // derived values + const issue = getIssueById(inboxId as string, inboxIssueId as string); const markInboxStatus = async (data: TInboxStatus) => { - if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId || !issuesList) return; + if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId || !issue) return; - await inboxIssueDetailsStore - .updateIssueStatus( - workspaceSlug.toString(), - projectId.toString(), - inboxId.toString(), - issuesList.find((inboxIssue: any) => inboxIssue.issue_inbox[0].id === inboxIssueId)?.issue_inbox[0].id!, - data - ) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Something went wrong while updating inbox status. Please try again.", - }) - ); + await updateIssueStatus( + workspaceSlug.toString(), + projectId.toString(), + inboxId.toString(), + issue.issue_inbox[0].id!, + data + ).catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong while updating inbox status. Please try again.", + }) + ); }; - const issue = issuesList?.find((issue) => issue.issue_inbox[0].id === inboxIssueId); - const currentIssueIndex = issuesList?.findIndex((issue) => issue.issue_inbox[0].id === inboxIssueId) ?? 0; + // const currentIssueIndex = issuesList?.findIndex((issue) => issue.issue_inbox[0].id === inboxIssueId) ?? 0; useEffect(() => { if (!issue?.issue_inbox[0].snoozed_till) return; @@ -72,7 +70,7 @@ export const InboxActionsHeader = observer(() => { }, [issue]); const issueStatus = issue?.issue_inbox[0].status; - const isAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER; + const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const today = new Date(); const tomorrow = new Date(today); @@ -127,7 +125,7 @@ export const InboxActionsHeader = observer(() => {
{inboxIssueId && (
-
+ {/*
+
*/}
{isAllowed && (issueStatus === 0 || issueStatus === -2) && (
@@ -228,7 +226,7 @@ export const InboxActionsHeader = observer(() => {
)} - {(isAllowed || user?.id === issue?.created_by) && ( + {(isAllowed || currentUser?.id === issue?.created_by) && (
+
+ + ); +}; diff --git a/web/components/issues/attachment/attachment-upload.tsx b/web/components/issues/attachment/attachment-upload.tsx index c1b323e74..c53574cb4 100644 --- a/web/components/issues/attachment/attachment-upload.tsx +++ b/web/components/issues/attachment/attachment-upload.tsx @@ -1,40 +1,29 @@ import { useCallback, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { mutate } from "swr"; import { useDropzone } from "react-dropzone"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -// services -import { IssueAttachmentService } from "services/issue"; // hooks -import useToast from "hooks/use-toast"; -// types -import { IIssueAttachment } from "types"; -// fetch-keys -import { ISSUE_ATTACHMENTS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; +import { useApplication } from "hooks/store"; // constants import { MAX_FILE_SIZE } from "constants/common"; +// types +import { TAttachmentOperations } from "./root"; + +type TAttachmentOperationsModal = Exclude; type Props = { disabled?: boolean; + handleAttachmentOperations: TAttachmentOperationsModal; }; -const issueAttachmentService = new IssueAttachmentService(); - export const IssueAttachmentUpload: React.FC = observer((props) => { - const { disabled = false } = props; + const { disabled = false, handleAttachmentOperations } = props; + // store hooks + const { + router: { workspaceSlug }, + config: { envConfig }, + } = useApplication(); // states const [isLoading, setIsLoading] = useState(false); - // router - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; - - const { setToastAlert } = useToast(); - - const { - appConfig: { envConfig }, - } = useMobxStore(); const onDrop = useCallback((acceptedFiles: File[]) => { if (!acceptedFiles[0] || !workspaceSlug) return; @@ -49,31 +38,7 @@ export const IssueAttachmentUpload: React.FC = observer((props) => { }) ); setIsLoading(true); - - issueAttachmentService - .uploadIssueAttachment(workspaceSlug as string, projectId as string, issueId as string, formData) - .then((res) => { - mutate( - ISSUE_ATTACHMENTS(issueId as string), - (prevData) => [res, ...(prevData ?? [])], - false - ); - mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); - setToastAlert({ - type: "success", - title: "Success!", - message: "File added successfully.", - }); - setIsLoading(false); - }) - .catch(() => { - setIsLoading(false); - setToastAlert({ - type: "error", - title: "error!", - message: "Something went wrong. please check file type & size (max 5 MB)", - }); - }); + handleAttachmentOperations.create(formData).finally(() => setIsLoading(false)); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/web/components/issues/attachment/attachments-list.tsx b/web/components/issues/attachment/attachments-list.tsx new file mode 100644 index 000000000..6644d7e8c --- /dev/null +++ b/web/components/issues/attachment/attachments-list.tsx @@ -0,0 +1,32 @@ +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +// hooks +import { useIssueDetail } from "hooks/store"; +// components +import { IssueAttachmentsDetail } from "./attachment-detail"; +// types +import { TAttachmentOperations } from "./root"; + +export type TAttachmentOperationsRemoveModal = Exclude; + +export type TIssueAttachmentsList = { + handleAttachmentOperations: TAttachmentOperationsRemoveModal; +}; + +export const IssueAttachmentsList: FC = observer((props) => { + const { handleAttachmentOperations } = props; + // store hooks + const { + attachment: { issueAttachments }, + } = useIssueDetail(); + + return ( + <> + {issueAttachments && + issueAttachments.length > 0 && + issueAttachments.map((attachmentId) => ( + + ))} + + ); +}); diff --git a/web/components/issues/attachment/attachments.tsx b/web/components/issues/attachment/attachments.tsx deleted file mode 100644 index da751cbe3..000000000 --- a/web/components/issues/attachment/attachments.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { useState } from "react"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import useSWR from "swr"; -// ui -import { Tooltip } from "@plane/ui"; -import { DeleteAttachmentModal } from "./delete-attachment-modal"; -// icons -import { getFileIcon } from "components/icons"; -import { AlertCircle, X } from "lucide-react"; -// services -import { IssueAttachmentService } from "services/issue"; -import { ProjectMemberService } from "services/project"; -// fetch-key -import { ISSUE_ATTACHMENTS, PROJECT_MEMBERS } from "constants/fetch-keys"; -// helper -import { truncateText } from "helpers/string.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; -import { convertBytesToSize, getFileExtension, getFileName } from "helpers/attachment.helper"; -// type -import { IIssueAttachment } from "types"; - -// services -const issueAttachmentService = new IssueAttachmentService(); -const projectMemberService = new ProjectMemberService(); - -type Props = { - editable: boolean; -}; - -export const IssueAttachments: React.FC = (props) => { - const { editable } = props; - - // states - const [deleteAttachment, setDeleteAttachment] = useState(null); - const [attachmentDeleteModal, setAttachmentDeleteModal] = useState(false); - - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; - - const { data: attachments } = useSWR( - workspaceSlug && projectId && issueId ? ISSUE_ATTACHMENTS(issueId as string) : null, - workspaceSlug && projectId && issueId - ? () => issueAttachmentService.getIssueAttachment(workspaceSlug as string, projectId as string, issueId as string) - : null - ); - - const { data: people } = useSWR( - workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, - workspaceSlug && projectId - ? () => projectMemberService.fetchProjectMembers(workspaceSlug as string, projectId as string) - : null - ); - - return ( - <> - - {attachments && - attachments.length > 0 && - attachments.map((file) => ( -
- -
-
{getFileIcon(getFileExtension(file.asset))}
-
-
- - {truncateText(`${getFileName(file.attributes.name)}`, 10)} - - person.member.id === file.updated_by)?.member.display_name ?? "" - } uploaded on ${renderFormattedDate(file.updated_at)}`} - > - - - - -
- -
- {getFileExtension(file.asset).toUpperCase()} - {convertBytesToSize(file.attributes.size)} -
-
-
- - - {editable && ( - - )} -
- ))} - - ); -}; diff --git a/web/components/issues/attachment/delete-attachment-modal.tsx b/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx similarity index 69% rename from web/components/issues/attachment/delete-attachment-modal.tsx rename to web/components/issues/attachment/delete-attachment-confirmation-modal.tsx index d4f391459..6c26bf850 100644 --- a/web/components/issues/attachment/delete-attachment-modal.tsx +++ b/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx @@ -1,72 +1,42 @@ -import React from "react"; - -import { useRouter } from "next/router"; - -import { mutate } from "swr"; - +import { FC, Fragment, Dispatch, SetStateAction, useState } from "react"; +import { AlertTriangle } from "lucide-react"; // headless ui import { Dialog, Transition } from "@headlessui/react"; -// services -import { IssueAttachmentService } from "services/issue"; -// hooks -import useToast from "hooks/use-toast"; // ui import { Button } from "@plane/ui"; -// icons -import { AlertTriangle } from "lucide-react"; // helper import { getFileName } from "helpers/attachment.helper"; // types -import type { IIssueAttachment } from "types"; -// fetch-keys -import { ISSUE_ATTACHMENTS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; +import type { TIssueAttachment } from "@plane/types"; +import { TIssueAttachmentsList } from "./attachments-list"; -type Props = { +type Props = TIssueAttachmentsList & { isOpen: boolean; - setIsOpen: React.Dispatch>; - data: IIssueAttachment | null; + setIsOpen: Dispatch>; + data: TIssueAttachment; }; -// services -const issueAttachmentService = new IssueAttachmentService(); - -export const DeleteAttachmentModal: React.FC = ({ isOpen, setIsOpen, data }) => { - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; - - const { setToastAlert } = useToast(); +export const IssueAttachmentDeleteModal: FC = (props) => { + const { isOpen, setIsOpen, data, handleAttachmentOperations } = props; + // state + const [loader, setLoader] = useState(false); const handleClose = () => { setIsOpen(false); + setLoader(false); }; const handleDeletion = async (assetId: string) => { - if (!workspaceSlug || !projectId || !data) return; - - mutate( - ISSUE_ATTACHMENTS(issueId as string), - (prevData) => (prevData ?? [])?.filter((p) => p.id !== assetId), - false - ); - - await issueAttachmentService - .deleteIssueAttachment(workspaceSlug as string, projectId as string, issueId as string, assetId as string) - .then(() => mutate(PROJECT_ISSUES_ACTIVITY(issueId as string))) - .catch(() => { - setToastAlert({ - type: "error", - title: "error!", - message: "Something went wrong please try again.", - }); - }); + setLoader(true); + handleAttachmentOperations.remove(assetId).finally(() => handleClose()); }; return ( data && ( - + = ({ isOpen, setIsOpen, data
= ({ isOpen, setIsOpen, data tabIndex={1} onClick={() => { handleDeletion(data.id); - handleClose(); }} + disabled={loader} > - Delete + {loader ? "Deleting..." : "Delete"}
diff --git a/web/components/issues/attachment/index.ts b/web/components/issues/attachment/index.ts index 9546de31e..d4385e7da 100644 --- a/web/components/issues/attachment/index.ts +++ b/web/components/issues/attachment/index.ts @@ -1,3 +1,7 @@ +export * from "./root"; + export * from "./attachment-upload"; -export * from "./attachments"; -export * from "./delete-attachment-modal"; +export * from "./delete-attachment-confirmation-modal"; + +export * from "./attachments-list"; +export * from "./attachment-detail"; diff --git a/web/components/issues/attachment/root.tsx b/web/components/issues/attachment/root.tsx new file mode 100644 index 000000000..9d8a31b05 --- /dev/null +++ b/web/components/issues/attachment/root.tsx @@ -0,0 +1,77 @@ +import { FC, useMemo } from "react"; +// hooks +import { useApplication, useIssueDetail } from "hooks/store"; +import useToast from "hooks/use-toast"; +// components +import { IssueAttachmentUpload } from "./attachment-upload"; +import { IssueAttachmentsList } from "./attachments-list"; + +export type TIssueAttachmentRoot = { + isEditable: boolean; +}; + +export type TAttachmentOperations = { + create: (data: FormData) => Promise; + remove: (linkId: string) => Promise; +}; + +export const IssueAttachmentRoot: FC = (props) => { + // props + const { isEditable } = props; + // hooks + const { + router: { workspaceSlug, projectId }, + } = useApplication(); + const { issueId, createAttachment, removeAttachment } = useIssueDetail(); + const { setToastAlert } = useToast(); + + const handleAttachmentOperations: TAttachmentOperations = useMemo( + () => ({ + create: async (data: FormData) => { + try { + if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); + await createAttachment(workspaceSlug, projectId, issueId, data); + setToastAlert({ + message: "The attachment has been successfully uploaded", + type: "success", + title: "Attachment uploaded", + }); + } catch (error) { + setToastAlert({ + message: "The attachment could not be uploaded", + type: "error", + title: "Attachment not uploaded", + }); + } + }, + remove: async (attachmentId: string) => { + try { + if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); + await removeAttachment(workspaceSlug, projectId, issueId, attachmentId); + setToastAlert({ + message: "The attachment has been successfully removed", + type: "success", + title: "Attachment removed", + }); + } catch (error) { + setToastAlert({ + message: "The Attachment could not be removed", + type: "error", + title: "Attachment not removed", + }); + } + }, + }), + [workspaceSlug, projectId, issueId, createAttachment, removeAttachment, setToastAlert] + ); + + return ( +
+

Attachments

+
+ + +
+
+ ); +}; diff --git a/web/components/issues/comment/add-comment.tsx b/web/components/issues/comment/add-comment.tsx index 658e825bf..1bd2c83d6 100644 --- a/web/components/issues/comment/add-comment.tsx +++ b/web/components/issues/comment/add-comment.tsx @@ -1,7 +1,8 @@ import React from "react"; import { useRouter } from "next/router"; import { useForm, Controller } from "react-hook-form"; - +// hooks +import { useMention } from "hooks/store"; // services import { FileService } from "services/file.service"; // components @@ -9,10 +10,8 @@ import { LiteTextEditorWithRef } from "@plane/lite-text-editor"; // ui import { Button } from "@plane/ui"; import { Globe2, Lock } from "lucide-react"; - // types -import type { IIssueActivity } from "types"; -import useEditorSuggestions from "hooks/use-editor-suggestions"; +import type { IIssueActivity } from "@plane/types"; const defaultValues: Partial = { access: "INTERNAL", @@ -47,13 +46,14 @@ const commentAccess: commentAccessType[] = [ const fileService = new FileService(); export const AddComment: React.FC = ({ disabled = false, onSubmit, showAccessSpecifier = false }) => { + // refs const editorRef = React.useRef(null); - + // router const router = useRouter(); const { workspaceSlug } = router.query; - - const editorSuggestions = useEditorSuggestions(); - + // store hooks + const { mentionHighlights, mentionSuggestions } = useMention(); + // form info const { control, formState: { isSubmitting }, @@ -99,8 +99,8 @@ export const AddComment: React.FC = ({ disabled = false, onSubmit, showAc ? { accessValue: accessValue ?? "INTERNAL", onAccessChange, showAccessSpecifier, commentAccess } : undefined } - mentionSuggestions={editorSuggestions.mentionSuggestions} - mentionHighlights={editorSuggestions.mentionHighlights} + mentionSuggestions={mentionSuggestions} + mentionHighlights={mentionHighlights} submitButton={
@@ -145,13 +140,13 @@ export const CommentCard: React.FC = ({ ref={showEditorRef} value={comment.comment_html ?? ""} customClassName="text-xs border border-custom-border-200 bg-custom-background-100" - mentionHighlights={editorSuggestions.mentionHighlights} + mentionHighlights={mentionHighlights} />
- {user?.id === comment.actor && ( + {currentUser?.id === comment.actor && ( setIsEditing(true)} className="flex items-center gap-1"> @@ -191,4 +186,4 @@ export const CommentCard: React.FC = ({ )}
); -}; +}); diff --git a/web/components/issues/comment/comment-reaction.tsx b/web/components/issues/comment/comment-reaction.tsx index c920caeba..eb80b0323 100644 --- a/web/components/issues/comment/comment-reaction.tsx +++ b/web/components/issues/comment/comment-reaction.tsx @@ -1,13 +1,15 @@ import { FC } from "react"; import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; // hooks -import useUser from "hooks/use-user"; +import { useUser } from "hooks/store"; import useCommentReaction from "hooks/use-comment-reaction"; // ui import { ReactionSelector } from "components/core"; // helper import { renderEmoji } from "helpers/emoji.helper"; -import { IssueCommentReaction } from "types"; +// types +import { IssueCommentReaction } from "@plane/types"; type Props = { projectId?: string | string[]; @@ -15,13 +17,13 @@ type Props = { readonly?: boolean; }; -export const CommentReaction: FC = (props) => { +export const CommentReaction: FC = observer((props) => { const { projectId, commentId, readonly = false } = props; - + // router const router = useRouter(); const { workspaceSlug } = router.query; - - const { user } = useUser(); + // store hooks + const { currentUser } = useUser(); const { commentReactions, groupedReactions, handleReactionCreate, handleReactionDelete } = useCommentReaction( workspaceSlug, @@ -33,7 +35,7 @@ export const CommentReaction: FC = (props) => { if (!workspaceSlug || !projectId || !commentId) return; const isSelected = commentReactions?.some( - (r: IssueCommentReaction) => r.actor === user?.id && r.reaction === reaction + (r: IssueCommentReaction) => r.actor === currentUser?.id && r.reaction === reaction ); if (isSelected) { @@ -51,7 +53,7 @@ export const CommentReaction: FC = (props) => { position="top" value={ commentReactions - ?.filter((reaction: IssueCommentReaction) => reaction.actor === user?.id) + ?.filter((reaction: IssueCommentReaction) => reaction.actor === currentUser?.id) .map((r: IssueCommentReaction) => r.reaction) || [] } onSelect={handleReactionClick} @@ -70,7 +72,9 @@ export const CommentReaction: FC = (props) => { }} key={reaction} className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${ - commentReactions?.some((r: IssueCommentReaction) => r.actor === user?.id && r.reaction === reaction) + commentReactions?.some( + (r: IssueCommentReaction) => r.actor === currentUser?.id && r.reaction === reaction + ) ? "bg-custom-primary-100/10" : "bg-custom-background-80" }`} @@ -78,7 +82,9 @@ export const CommentReaction: FC = (props) => { {renderEmoji(reaction)} r.actor === user?.id && r.reaction === reaction) + commentReactions?.some( + (r: IssueCommentReaction) => r.actor === currentUser?.id && r.reaction === reaction + ) ? "text-custom-primary-100" : "" } @@ -90,4 +96,4 @@ export const CommentReaction: FC = (props) => { )}
); -}; +}); diff --git a/web/components/issues/delete-archived-issue-modal.tsx b/web/components/issues/delete-archived-issue-modal.tsx index 14ecd7edd..49d9e19dd 100644 --- a/web/components/issues/delete-archived-issue-modal.tsx +++ b/web/components/issues/delete-archived-issue-modal.tsx @@ -3,19 +3,19 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; +import { useIssues, useProject } from "hooks/store"; // ui import { Button } from "@plane/ui"; // types -import type { IIssue } from "types"; +import type { TIssue } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; type Props = { isOpen: boolean; handleClose: () => void; - data: IIssue; + data: TIssue; onSubmit?: () => Promise; }; @@ -26,8 +26,11 @@ export const DeleteArchivedIssueModal: React.FC = observer((props) => { const { workspaceSlug } = router.query; const { setToastAlert } = useToast(); + const { getProjectById } = useProject(); - const { archivedIssueDetail: archivedIssueDetailStore } = useMobxStore(); + const { + issues: { removeIssue }, + } = useIssues(EIssuesStoreType.ARCHIVED); const [isDeleteLoading, setIsDeleteLoading] = useState(false); @@ -45,8 +48,7 @@ export const DeleteArchivedIssueModal: React.FC = observer((props) => { setIsDeleteLoading(true); - await archivedIssueDetailStore - .deleteArchivedIssue(workspaceSlug.toString(), data.project, data.id) + await removeIssue(workspaceSlug.toString(), data.project_id, data.id) .then(() => { if (onSubmit) onSubmit(); }) @@ -106,7 +108,7 @@ export const DeleteArchivedIssueModal: React.FC = observer((props) => {

Are you sure you want to delete issue{" "} - {data?.project_detail.identifier}-{data?.sequence_id} + {getProjectById(data?.project_id)?.identifier}-{data?.sequence_id} {""}? All of the data related to the archived issue will be permanently removed. This action cannot be undone. diff --git a/web/components/issues/delete-draft-issue-modal.tsx b/web/components/issues/delete-draft-issue-modal.tsx index 955d8ac78..6a2caba18 100644 --- a/web/components/issues/delete-draft-issue-modal.tsx +++ b/web/components/issues/delete-draft-issue-modal.tsx @@ -1,9 +1,6 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // services import { IssueDraftService } from "services/issue"; // hooks @@ -13,29 +10,29 @@ import { AlertTriangle } from "lucide-react"; // ui import { Button } from "@plane/ui"; // types -import type { IIssue } from "types"; +import type { TIssue } from "@plane/types"; +import { useProject } from "hooks/store"; type Props = { isOpen: boolean; handleClose: () => void; - data: IIssue | null; + data: TIssue | null; onSubmit?: () => Promise | void; }; const issueDraftService = new IssueDraftService(); -export const DeleteDraftIssueModal: React.FC = observer((props) => { +export const DeleteDraftIssueModal: React.FC = (props) => { const { isOpen, handleClose, data, onSubmit } = props; - + // states const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - const { user: userStore } = useMobxStore(); - const user = userStore.currentUser; - + // router const router = useRouter(); const { workspaceSlug } = router.query; - + // toast alert const { setToastAlert } = useToast(); + // hooks + const { getProjectById } = useProject(); useEffect(() => { setIsDeleteLoading(false); @@ -47,12 +44,12 @@ export const DeleteDraftIssueModal: React.FC = observer((props) => { }; const handleDeletion = async () => { - if (!workspaceSlug || !data || !user) return; + if (!workspaceSlug || !data) return; setIsDeleteLoading(true); await issueDraftService - .deleteDraftIssue(workspaceSlug as string, data.project, data.id) + .deleteDraftIssue(workspaceSlug.toString(), data.project_id, data.id) .then(() => { setIsDeleteLoading(false); handleClose(); @@ -64,7 +61,7 @@ export const DeleteDraftIssueModal: React.FC = observer((props) => { }); }) .catch((error) => { - console.log(error); + console.error(error); handleClose(); setToastAlert({ title: "Error", @@ -116,7 +113,7 @@ export const DeleteDraftIssueModal: React.FC = observer((props) => {

Are you sure you want to delete issue{" "} - {data?.project_detail.identifier}-{data?.sequence_id} + {data && getProjectById(data?.project_id)?.identifier}-{data?.sequence_id} {""}? All of the data related to the draft issue will be permanently removed. This action cannot be undone. @@ -138,4 +135,4 @@ export const DeleteDraftIssueModal: React.FC = observer((props) => { ); -}); +}; diff --git a/web/components/issues/delete-issue-modal.tsx b/web/components/issues/delete-issue-modal.tsx index 2f53a825f..e2d4a4a00 100644 --- a/web/components/issues/delete-issue-modal.tsx +++ b/web/components/issues/delete-issue-modal.tsx @@ -6,26 +6,37 @@ import { Button } from "@plane/ui"; // hooks import useToast from "hooks/use-toast"; // types -import type { IIssue } from "types"; +import { useIssues } from "hooks/store/use-issues"; +import { TIssue } from "@plane/types"; +import { useProject } from "hooks/store"; type Props = { isOpen: boolean; handleClose: () => void; - data: IIssue; + dataId?: string | null | undefined; + data?: TIssue; onSubmit?: () => Promise; }; export const DeleteIssueModal: React.FC = (props) => { - const { data, isOpen, handleClose, onSubmit } = props; + const { dataId, data, isOpen, handleClose, onSubmit } = props; + + const { issueMap } = useIssues(); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const { setToastAlert } = useToast(); + // hooks + const { getProjectById } = useProject(); useEffect(() => { setIsDeleteLoading(false); }, [isOpen]); + if (!dataId && !data) return null; + + const issue = data ? data : issueMap[dataId!]; + const onClose = () => { setIsDeleteLoading(false); handleClose(); @@ -93,7 +104,7 @@ export const DeleteIssueModal: React.FC = (props) => {

Are you sure you want to delete issue{" "} - {data?.project_detail?.identifier}-{data?.sequence_id} + {getProjectById(issue?.project_id)?.identifier}-{issue?.sequence_id} {""}? All of the data related to the issue will be permanently removed. This action cannot be undone. diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index 677ab5e22..3f463496e 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -7,10 +7,10 @@ import debounce from "lodash/debounce"; import { TextArea } from "@plane/ui"; import { RichTextEditor } from "@plane/rich-text-editor"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; // services import { FileService } from "services/file.service"; -import useEditorSuggestions from "hooks/use-editor-suggestions"; +import { useMention } from "hooks/store"; export interface IssueDescriptionFormValues { name: string; @@ -39,16 +39,16 @@ export const IssueDescriptionForm: FC = (props) => { const [characterLimit, setCharacterLimit] = useState(false); const { setShowAlert } = useReloadConfirmations(); - - const editorSuggestion = useEditorSuggestions(); - + // store hooks + const { mentionHighlights, mentionSuggestions } = useMention(); + // form info const { handleSubmit, watch, reset, control, formState: { errors }, - } = useForm({ + } = useForm({ defaultValues: { name: "", description_html: "", @@ -72,7 +72,7 @@ export const IssueDescriptionForm: FC = (props) => { }, [issue.id]); // TODO: verify the exhaustive-deps warning const handleDescriptionFormSubmit = useCallback( - async (formData: Partial) => { + async (formData: Partial) => { if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return; await handleFormSubmit({ @@ -135,10 +135,8 @@ export const IssueDescriptionForm: FC = (props) => { debouncedFormSave(); }} required - className={`min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary ${ - !isAllowed ? "hover:cursor-not-allowed" : "" - }`} - hasError={Boolean(errors?.description)} + className="min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary" + hasError={Boolean(errors?.name)} role="textbox" disabled={!isAllowed} /> @@ -172,9 +170,7 @@ export const IssueDescriptionForm: FC = (props) => { setShouldShowAlert={setShowAlert} setIsSubmitting={setIsSubmitting} dragDropEnabled - customClassName={ - isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200 pointer-events-none" - } + customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"} noBorder={!isAllowed} onChange={(description: Object, description_html: string) => { setShowAlert(true); @@ -182,8 +178,8 @@ export const IssueDescriptionForm: FC = (props) => { onChange(description_html); debouncedFormSave(); }} - mentionSuggestions={editorSuggestion.mentionSuggestions} - mentionHighlights={editorSuggestion.mentionHighlights} + mentionSuggestions={mentionSuggestions} + mentionHighlights={mentionHighlights} /> )} /> diff --git a/web/components/issues/draft-issue-form.tsx b/web/components/issues/draft-issue-form.tsx index 2e34fe331..2d79f4ee1 100644 --- a/web/components/issues/draft-issue-form.tsx +++ b/web/components/issues/draft-issue-form.tsx @@ -1,72 +1,62 @@ import React, { FC, useState, useEffect, useRef } from "react"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; +import { observer } from "mobx-react-lite"; +import { Sparkle, X } from "lucide-react"; +// hooks +import { useApplication, useEstimate, useMention } from "hooks/store"; +import useToast from "hooks/use-toast"; +import useLocalStorage from "hooks/use-local-storage"; // services import { AIService } from "services/ai.service"; import { FileService } from "services/file.service"; -// hooks -import useToast from "hooks/use-toast"; -import useLocalStorage from "hooks/use-local-storage"; // components import { GptAssistantPopover } from "components/core"; import { ParentIssuesListModal } from "components/issues"; -import { - IssueAssigneeSelect, - IssueDateSelect, - IssueEstimateSelect, - IssueLabelSelect, - IssuePrioritySelect, - IssueProjectSelect, - IssueStateSelect, -} from "components/issues/select"; +import { IssueLabelSelect } from "components/issues/select"; import { CreateStateModal } from "components/states"; import { CreateLabelModal } from "components/labels"; -// ui -import {} from "components/ui"; -import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui"; -// icons -import { Sparkle, X } from "lucide-react"; -// types -import type { IUser, IIssue, ISearchIssueResponse } from "types"; -// components import { RichTextEditorWithRef } from "@plane/rich-text-editor"; -import useEditorSuggestions from "hooks/use-editor-suggestions"; -import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; +import { + DateDropdown, + EstimateDropdown, + PriorityDropdown, + ProjectDropdown, + ProjectMemberDropdown, + StateDropdown, +} from "components/dropdowns"; +// ui +import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +// types +import type { IUser, TIssue, ISearchIssueResponse } from "@plane/types"; const aiService = new AIService(); const fileService = new FileService(); -const defaultValues: Partial = { - project: "", +const defaultValues: Partial = { + project_id: "", name: "", - description: { - type: "doc", - content: [ - { - type: "paragraph", - }, - ], - }, description_html: "

", estimate_point: null, - state: "", - parent: null, + state_id: "", + parent_id: null, priority: "none", - assignees: [], - labels: [], - start_date: null, - target_date: null, + assignee_ids: [], + label_ids: [], + start_date: undefined, + target_date: undefined, }; interface IssueFormProps { handleFormSubmit: ( - formData: Partial, + formData: Partial, action?: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" ) => Promise; - data?: Partial | null; + data?: Partial | null; isOpen: boolean; - prePopulatedData?: Partial | null; + prePopulatedData?: Partial | null; projectId: string; setActiveProject: React.Dispatch>; createMore: boolean; @@ -112,10 +102,12 @@ export const DraftIssueForm: FC = observer((props) => { const [selectedParentIssue, setSelectedParentIssue] = useState(null); const [gptAssistantModal, setGptAssistantModal] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); + // store hooks + const { areEstimatesActiveForProject } = useEstimate(); + const { mentionHighlights, mentionSuggestions } = useMention(); // hooks const { setValue: setLocalStorageValue } = useLocalStorage("draftedIssue", {}); const { setToastAlert } = useToast(); - const editorSuggestions = useEditorSuggestions(); // refs const editorRef = useRef(null); // router @@ -123,8 +115,8 @@ export const DraftIssueForm: FC = observer((props) => { const { workspaceSlug } = router.query; // store const { - appConfig: { envConfig }, - } = useMobxStore(); + config: { envConfig }, + } = useApplication(); // form info const { formState: { errors, isSubmitting }, @@ -135,27 +127,26 @@ export const DraftIssueForm: FC = observer((props) => { getValues, setValue, setFocus, - } = useForm({ + } = useForm({ defaultValues: prePopulatedData ?? defaultValues, reValidateMode: "onChange", }); const issueName = watch("name"); - const payload: Partial = { + const payload: Partial = { name: watch("name"), - description: watch("description"), description_html: watch("description_html"), - state: watch("state"), + state_id: watch("state_id"), priority: watch("priority"), - assignees: watch("assignees"), - labels: watch("labels"), + assignee_ids: watch("assignee_ids"), + label_ids: watch("label_ids"), start_date: watch("start_date"), target_date: watch("target_date"), - project: watch("project"), - parent: watch("parent"), - cycle: watch("cycle"), - module: watch("module"), + project_id: watch("project_id"), + parent_id: watch("parent_id"), + cycle_id: watch("cycle_id"), + module_id: watch("module_id"), }; useEffect(() => { @@ -189,31 +180,24 @@ export const DraftIssueForm: FC = observer((props) => { // }; const handleCreateUpdateIssue = async ( - formData: Partial, + formData: Partial, action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft" ) => { await handleFormSubmit( { ...(data ?? {}), ...formData, - is_draft: action === "createDraft" || action === "updateDraft", + // is_draft: action === "createDraft" || action === "updateDraft", }, action ); + // TODO: check_with_backend setGptAssistantModal(false); reset({ ...defaultValues, - project: projectId, - description: { - type: "doc", - content: [ - { - type: "paragraph", - }, - ], - }, + project_id: projectId, description_html: "

", }); editorRef?.current?.clearEditor(); @@ -222,7 +206,7 @@ export const DraftIssueForm: FC = observer((props) => { const handleAiAssistance = async (response: string) => { if (!workspaceSlug || !projectId) return; - setValue("description", {}); + // setValue("description", {}); setValue("description_html", `${watch("description_html")}

${response}

`); editorRef.current?.setEditorValue(`${watch("description_html")}`); }; @@ -280,7 +264,7 @@ export const DraftIssueForm: FC = observer((props) => { useEffect(() => { reset({ ...getValues(), - project: projectId, + project_id: projectId, }); }, [getValues, projectId, reset]); @@ -302,7 +286,7 @@ export const DraftIssueForm: FC = observer((props) => { isOpen={labelModal} handleClose={() => setLabelModal(false)} projectId={projectId} - onSuccess={(response) => setValue("labels", [...watch("labels"), response.id])} + onSuccess={(response) => setValue("label_ids", [...watch("label_ids"), response.id])} /> )} @@ -316,14 +300,15 @@ export const DraftIssueForm: FC = observer((props) => { {(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && ( ( - { + onChange={(val) => { onChange(val); setActiveProject(val); }} + buttonVariant="background-with-text" /> )} /> @@ -332,7 +317,7 @@ export const DraftIssueForm: FC = observer((props) => { {status ? "Update" : "Create"} Issue
- {watch("parent") && + {watch("parent_id") && (fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && selectedParentIssue && (
@@ -350,7 +335,7 @@ export const DraftIssueForm: FC = observer((props) => { { - setValue("parent", null); + setValue("parent_id", null); setSelectedParentIssue(null); }} /> @@ -454,10 +439,9 @@ export const DraftIssueForm: FC = observer((props) => { customClassName="min-h-[150px]" onChange={(description: Object, description_html: string) => { onChange(description_html); - setValue("description", description); }} - mentionHighlights={editorSuggestions.mentionHighlights} - mentionSuggestions={editorSuggestions.mentionSuggestions} + mentionHighlights={mentionHighlights} + mentionSuggestions={mentionSuggestions} /> )} /> @@ -467,14 +451,16 @@ export const DraftIssueForm: FC = observer((props) => { {(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && ( ( - +
+ +
)} /> )} @@ -483,80 +469,100 @@ export const DraftIssueForm: FC = observer((props) => { control={control} name="priority" render={({ field: { value, onChange } }) => ( - +
+ +
)} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && ( ( - +
+ +
)} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && ( ( - +
+ +
)} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && ( -
- ( - ( +
+ onChange(date ? renderFormattedPayloadDate(date) : null)} + buttonVariant="border-with-text" + placeholder="Start date" + maxDate={maxDate ?? undefined} /> - )} - /> -
+
+ )} + /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && ( -
- ( - ( +
+ onChange(date ? renderFormattedPayloadDate(date) : null)} + buttonVariant="border-with-text" + placeholder="Due date" + minDate={minDate ?? undefined} /> - )} - /> -
+
+ )} + /> )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && ( -
+ {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && + areEstimatesActiveForProject(projectId) && ( ( - +
+ +
)} /> -
- )} + )} {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( ( = observer((props) => { )} {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( - {watch("parent") ? ( + {watch("parent_id") ? ( <> setParentIssueListModalOpen(true)}> Change parent issue - setValue("parent", null)}> + setValue("parent_id", null)}> Remove parent issue diff --git a/web/components/issues/draft-issue-modal.tsx b/web/components/issues/draft-issue-modal.tsx index 51ff30d40..4008e6383 100644 --- a/web/components/issues/draft-issue-modal.tsx +++ b/web/components/issues/draft-issue-modal.tsx @@ -3,27 +3,27 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // services import { IssueService } from "services/issue"; import { ModuleService } from "services/module.service"; // hooks import useToast from "hooks/use-toast"; import useLocalStorage from "hooks/use-local-storage"; +import { useIssues, useProject, useUser } from "hooks/store"; // components import { DraftIssueForm } from "components/issues"; // types -import type { IIssue } from "types"; +import type { TIssue } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; // fetch-keys import { PROJECT_ISSUES_DETAILS, USER_ISSUE, SUB_ISSUES } from "constants/fetch-keys"; interface IssuesModalProps { - data?: IIssue | null; + data?: TIssue | null; handleClose: () => void; isOpen: boolean; isUpdatingSingleIssue?: boolean; - prePopulateData?: Partial; + prePopulateData?: Partial; fieldsToShow?: ( | "project" | "name" @@ -38,7 +38,7 @@ interface IssuesModalProps { | "parent" | "all" )[]; - onSubmit?: (data: Partial) => Promise | void; + onSubmit?: (data: Partial) => Promise | void; } // services @@ -59,15 +59,16 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( // states const [createMore, setCreateMore] = useState(false); const [activeProject, setActiveProject] = useState(null); - const [prePopulateData, setPreloadedData] = useState | undefined>(undefined); - + const [prePopulateData, setPreloadedData] = useState | undefined>(undefined); + // router const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; - - const { project: projectStore, user: userStore, projectDraftIssues: draftIssueStore } = useMobxStore(); - - const user = userStore.currentUser; - const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined; + // store + const { issues: draftIssues } = useIssues(EIssuesStoreType.DRAFT); + const { currentUser } = useUser(); + const { workspaceProjectIds: workspaceProjects } = useProject(); + // derived values + const projects = workspaceProjects; const { clearValue: clearDraftIssueLocalStorage } = useLocalStorage("draftedIssue", {}); @@ -86,14 +87,14 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( useEffect(() => { setPreloadedData(prePopulateDataProps ?? {}); - if (cycleId && !prePopulateDataProps?.cycle) { + if (cycleId && !prePopulateDataProps?.cycle_id) { setPreloadedData((prevData) => ({ ...(prevData ?? {}), ...prePopulateDataProps, cycle: cycleId.toString(), })); } - if (moduleId && !prePopulateDataProps?.module) { + if (moduleId && !prePopulateDataProps?.module_id) { setPreloadedData((prevData) => ({ ...(prevData ?? {}), ...prePopulateDataProps, @@ -102,27 +103,27 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( } if ( (router.asPath.includes("my-issues") || router.asPath.includes("assigned")) && - !prePopulateDataProps?.assignees + !prePopulateDataProps?.assignee_ids ) { setPreloadedData((prevData) => ({ ...(prevData ?? {}), ...prePopulateDataProps, - assignees: prePopulateDataProps?.assignees ?? [user?.id ?? ""], + assignees: prePopulateDataProps?.assignee_ids ?? [currentUser?.id ?? ""], })); } - }, [prePopulateDataProps, cycleId, moduleId, router.asPath, user?.id]); + }, [prePopulateDataProps, cycleId, moduleId, router.asPath, currentUser?.id]); useEffect(() => { setPreloadedData(prePopulateDataProps ?? {}); - if (cycleId && !prePopulateDataProps?.cycle) { + if (cycleId && !prePopulateDataProps?.cycle_id) { setPreloadedData((prevData) => ({ ...(prevData ?? {}), ...prePopulateDataProps, cycle: cycleId.toString(), })); } - if (moduleId && !prePopulateDataProps?.module) { + if (moduleId && !prePopulateDataProps?.module_id) { setPreloadedData((prevData) => ({ ...(prevData ?? {}), ...prePopulateDataProps, @@ -131,15 +132,15 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( } if ( (router.asPath.includes("my-issues") || router.asPath.includes("assigned")) && - !prePopulateDataProps?.assignees + !prePopulateDataProps?.assignee_ids ) { setPreloadedData((prevData) => ({ ...(prevData ?? {}), ...prePopulateDataProps, - assignees: prePopulateDataProps?.assignees ?? [user?.id ?? ""], + assignees: prePopulateDataProps?.assignee_ids ?? [currentUser?.id ?? ""], })); } - }, [prePopulateDataProps, cycleId, moduleId, router.asPath, user?.id]); + }, [prePopulateDataProps, cycleId, moduleId, router.asPath, currentUser?.id]); useEffect(() => { // if modal is closed, reset active project to null @@ -151,32 +152,35 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( // if data is present, set active project to the project of the // issue. This has more priority than the project in the url. - if (data && data.project) return setActiveProject(data.project); + if (data && data.project_id) return setActiveProject(data.project_id); - if (prePopulateData && prePopulateData.project && !activeProject) return setActiveProject(prePopulateData.project); + if (prePopulateData && prePopulateData.project_id && !activeProject) + return setActiveProject(prePopulateData.project_id); - if (prePopulateData && prePopulateData.project && !activeProject) return setActiveProject(prePopulateData.project); + if (prePopulateData && prePopulateData.project_id && !activeProject) + return setActiveProject(prePopulateData.project_id); // if data is not present, set active project to the project // in the url. This has the least priority. if (projects && projects.length > 0 && !activeProject) - setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null); + setActiveProject(projects?.find((id) => id === projectId) ?? projects?.[0] ?? null); }, [activeProject, data, projectId, projects, isOpen, prePopulateData]); - const createDraftIssue = async (payload: Partial) => { - if (!workspaceSlug || !activeProject || !user) return; + const createDraftIssue = async (payload: Partial) => { + if (!workspaceSlug || !activeProject || !currentUser) return; - await draftIssueStore + await draftIssues .createIssue(workspaceSlug as string, activeProject ?? "", payload) .then(async () => { - await draftIssueStore.fetchIssues(workspaceSlug as string, activeProject ?? "", "mutation"); + await draftIssues.fetchIssues(workspaceSlug as string, activeProject ?? "", "mutation"); setToastAlert({ type: "success", title: "Success!", message: "Issue created successfully.", }); - if (payload.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug.toString())); + if (payload.assignee_ids?.some((assignee) => assignee === currentUser?.id)) + mutate(USER_ISSUE(workspaceSlug.toString())); }) .catch(() => { setToastAlert({ @@ -189,22 +193,20 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( if (!createMore) onClose(); }; - const updateDraftIssue = async (payload: Partial) => { - if (!user) return; - - await draftIssueStore + const updateDraftIssue = async (payload: Partial) => { + await draftIssues .updateIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload) .then((res) => { if (isUpdatingSingleIssue) { - mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); + mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); } else { - if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString())); + if (payload.parent_id) mutate(SUB_ISSUES(payload.parent_id.toString())); } - if (!payload.is_draft) { - if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle); - if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module); - } + // if (!payload.is_draft) { // TODO: check_with_backend + // if (payload.cycle_id && payload.cycle_id !== "") addIssueToCycle(res.id, payload.cycle_id); + // if (payload.module_id && payload.module_id !== "") addIssueToModule(res.id, payload.module_id); + // } if (!createMore) onClose(); @@ -224,7 +226,7 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( }; const addIssueToCycle = async (issueId: string, cycleId: string) => { - if (!workspaceSlug || !activeProject || !user) return; + if (!workspaceSlug || !activeProject) return; await issueService.addIssueToCycle(workspaceSlug as string, activeProject ?? "", cycleId, { issues: [issueId], @@ -232,21 +234,21 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( }; const addIssueToModule = async (issueId: string, moduleId: string) => { - if (!workspaceSlug || !activeProject || !user) return; + if (!workspaceSlug || !activeProject) return; await moduleService.addIssuesToModule(workspaceSlug as string, activeProject ?? "", moduleId as string, { issues: [issueId], }); }; - const createIssue = async (payload: Partial) => { - if (!workspaceSlug || !activeProject || !user) return; + const createIssue = async (payload: Partial) => { + if (!workspaceSlug || !activeProject) return; await issueService .createIssue(workspaceSlug.toString(), activeProject, payload) .then(async (res) => { - if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle); - if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module); + if (payload.cycle_id && payload.cycle_id !== "") await addIssueToCycle(res.id, payload.cycle_id); + if (payload.module_id && payload.module_id !== "") await addIssueToModule(res.id, payload.module_id); setToastAlert({ type: "success", @@ -256,9 +258,10 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( if (!createMore) onClose(); - if (payload.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string)); + if (payload.assignee_ids?.some((assignee) => assignee === currentUser?.id)) + mutate(USER_ISSUE(workspaceSlug as string)); - if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); + if (payload.parent_id && payload.parent_id !== "") mutate(SUB_ISSUES(payload.parent_id)); }) .catch(() => { setToastAlert({ @@ -270,14 +273,14 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( }; const handleFormSubmit = async ( - formData: Partial, + formData: Partial, action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft" ) => { if (!workspaceSlug || !activeProject) return; - const payload: Partial = { + const payload: Partial = { ...formData, - description: formData.description ?? "", + // description: formData.description ?? "", description_html: formData.description_html ?? "

", }; @@ -332,7 +335,7 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( projectId={activeProject ?? ""} setActiveProject={setActiveProject} status={data ? true : false} - user={user ?? undefined} + user={currentUser ?? undefined} fieldsToShow={fieldsToShow} /> diff --git a/web/components/issues/form.tsx b/web/components/issues/form.tsx index 3e55ca70c..0f0b5cea3 100644 --- a/web/components/issues/form.tsx +++ b/web/components/issues/form.tsx @@ -2,63 +2,61 @@ import React, { FC, useState, useEffect, useRef } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { LayoutPanelTop, Sparkle, X } from "lucide-react"; +// hooks +import { useApplication, useEstimate, useMention, useProject } from "hooks/store"; +import useToast from "hooks/use-toast"; // services import { AIService } from "services/ai.service"; import { FileService } from "services/file.service"; -// hooks -import useToast from "hooks/use-toast"; // components import { GptAssistantPopover } from "components/core"; import { ParentIssuesListModal } from "components/issues"; -import { - IssueAssigneeSelect, - IssueDateSelect, - IssueEstimateSelect, - IssueLabelSelect, - IssuePrioritySelect, - IssueProjectSelect, - IssueStateSelect, - IssueModuleSelect, - IssueCycleSelect, -} from "components/issues/select"; +import { IssueLabelSelect } from "components/issues/select"; import { CreateStateModal } from "components/states"; import { CreateLabelModal } from "components/labels"; +import { RichTextEditorWithRef } from "@plane/rich-text-editor"; +import { + CycleDropdown, + DateDropdown, + EstimateDropdown, + ModuleDropdown, + PriorityDropdown, + ProjectDropdown, + ProjectMemberDropdown, + StateDropdown, +} from "components/dropdowns"; // ui import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui"; -// icons -import { LayoutPanelTop, Sparkle, X } from "lucide-react"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import type { IIssue, ISearchIssueResponse } from "types"; -// components -import { RichTextEditorWithRef } from "@plane/rich-text-editor"; -import useEditorSuggestions from "hooks/use-editor-suggestions"; +import type { TIssue, ISearchIssueResponse } from "@plane/types"; -const defaultValues: Partial = { - project: "", +const defaultValues: Partial = { + project_id: "", name: "", description_html: "

", estimate_point: null, - state: "", - parent: null, + state_id: "", + parent_id: null, priority: "none", - assignees: [], - labels: [], - start_date: null, - target_date: null, + assignee_ids: [], + label_ids: [], + start_date: undefined, + target_date: undefined, }; export interface IssueFormProps { - handleFormSubmit: (values: Partial) => Promise; - initialData?: Partial; + handleFormSubmit: (values: Partial) => Promise; + initialData?: Partial; projectId: string; setActiveProject: React.Dispatch>; createMore: boolean; setCreateMore: React.Dispatch>; handleDiscardClose: () => void; status: boolean; - handleFormDirty: (payload: Partial | null) => void; + handleFormDirty: (payload: Partial | null) => void; fieldsToShow: ( | "project" | "name" @@ -106,14 +104,14 @@ export const IssueForm: FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // store + // store hooks const { - user: userStore, - appConfig: { envConfig }, - } = useMobxStore(); - const user = userStore.currentUser; - // hooks - const editorSuggestion = useEditorSuggestions(); + config: { envConfig }, + } = useApplication(); + const { getProjectById } = useProject(); + const { areEstimatesActiveForProject } = useEstimate(); + const { mentionHighlights, mentionSuggestions } = useMention(); + // toast alert const { setToastAlert } = useToast(); // form info const { @@ -125,50 +123,44 @@ export const IssueForm: FC = observer((props) => { getValues, setValue, setFocus, - } = useForm({ + } = useForm({ defaultValues: initialData ?? defaultValues, reValidateMode: "onChange", }); const issueName = watch("name"); - const payload: Partial = { + const payload: Partial = { name: getValues("name"), - description: getValues("description"), - state: getValues("state"), + state_id: getValues("state_id"), priority: getValues("priority"), - assignees: getValues("assignees"), - labels: getValues("labels"), + assignee_ids: getValues("assignee_ids"), + label_ids: getValues("label_ids"), start_date: getValues("start_date"), target_date: getValues("target_date"), - project: getValues("project"), - parent: getValues("parent"), - cycle: getValues("cycle"), - module: getValues("module"), + project_id: getValues("project_id"), + parent_id: getValues("parent_id"), + cycle_id: getValues("cycle_id"), + module_id: getValues("module_id"), }; + // derived values + const projectDetails = getProjectById(projectId); + useEffect(() => { if (isDirty) handleFormDirty(payload); else handleFormDirty(null); // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(payload), isDirty]); - const handleCreateUpdateIssue = async (formData: Partial) => { + const handleCreateUpdateIssue = async (formData: Partial) => { await handleFormSubmit(formData); setGptAssistantModal(false); reset({ ...defaultValues, - project: projectId, - description: { - type: "doc", - content: [ - { - type: "paragraph", - }, - ], - }, + project_id: projectId, description_html: "

", }); editorRef?.current?.clearEditor(); @@ -177,18 +169,17 @@ export const IssueForm: FC = observer((props) => { const handleAiAssistance = async (response: string) => { if (!workspaceSlug || !projectId) return; - setValue("description", {}); setValue("description_html", `${watch("description_html")}

${response}

`); editorRef.current?.setEditorValue(`${watch("description_html")}`); }; const handleAutoGenerateDescription = async () => { - if (!workspaceSlug || !projectId || !user) return; + if (!workspaceSlug || !projectId) return; setIAmFeelingLucky(true); aiService - .createGptTask(workspaceSlug as string, projectId as string, { + .createGptTask(workspaceSlug.toString(), projectId.toString(), { prompt: issueName, task: "Generate a proper description for this issue.", }) @@ -227,7 +218,6 @@ export const IssueForm: FC = observer((props) => { reset({ ...defaultValues, ...initialData, - project: projectId, }); }, [setFocus, initialData, reset]); @@ -235,7 +225,7 @@ export const IssueForm: FC = observer((props) => { useEffect(() => { reset({ ...getValues(), - project: projectId, + project_id: projectId, }); }, [getValues, projectId, reset]); @@ -257,29 +247,31 @@ export const IssueForm: FC = observer((props) => { isOpen={labelModal} handleClose={() => setLabelModal(false)} projectId={projectId} - onSuccess={(response) => setValue("labels", [...watch("labels"), response.id])} + onSuccess={(response) => setValue("label_ids", [...watch("label_ids"), response.id])} /> )}
- {(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && !status && ( ( - { - onChange(val); - setActiveProject(val); - }} - /> + render={({ field: { value, onChange } }) => ( +
+ { + onChange(val); + setActiveProject(val); + }} + buttonVariant="border-with-text" + /> +
)} /> )} @@ -287,7 +279,7 @@ export const IssueForm: FC = observer((props) => { {status ? "Update" : "Create"} Issue
- {watch("parent") && + {watch("parent_id") && (fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && selectedParentIssue && (
@@ -305,7 +297,7 @@ export const IssueForm: FC = observer((props) => { { - setValue("parent", null); + setValue("parent_id", null); setSelectedParentIssue(null); }} /> @@ -408,10 +400,9 @@ export const IssueForm: FC = observer((props) => { customClassName="min-h-[7rem] border-custom-border-100" onChange={(description: Object, description_html: string) => { onChange(description_html); - setValue("description", description); }} - mentionHighlights={editorSuggestion.mentionHighlights} - mentionSuggestions={editorSuggestion.mentionSuggestions} + mentionHighlights={mentionHighlights} + mentionSuggestions={mentionSuggestions} /> )} /> @@ -421,14 +412,16 @@ export const IssueForm: FC = observer((props) => { {(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && ( ( - +
+ +
)} /> )} @@ -437,48 +430,63 @@ export const IssueForm: FC = observer((props) => { control={control} name="priority" render={({ field: { value, onChange } }) => ( - +
+ +
)} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && ( ( - +
+ 0 ? "transparent-without-text" : "border-with-text"} + buttonClassName={value?.length > 0 ? "hover:bg-transparent px-0" : ""} + placeholder="Assignees" + multiple + /> +
)} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && ( ( - +
+ +
)} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && ( -
- ( - ( +
+ onChange(date ? renderFormattedPayloadDate(date) : null)} + buttonVariant="border-with-text" + placeholder="Start date" + maxDate={maxDate ?? undefined} /> - )} - /> -
+
+ )} + /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
@@ -486,70 +494,79 @@ export const IssueForm: FC = observer((props) => { control={control} name="target_date" render={({ field: { value, onChange } }) => ( - +
+ onChange(date ? renderFormattedPayloadDate(date) : null)} + buttonVariant="border-with-text" + placeholder="Due date" + minDate={minDate ?? undefined} + /> +
)} />
)} - {(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("cycle")) && projectDetails?.cycle_view && ( ( - { - onChange(val); - }} - /> +
+ +
)} /> )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("cycle")) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && projectDetails?.module_view && ( ( - { - onChange(val); - }} - /> +
+ +
)} /> )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && ( - <> + {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && + areEstimatesActiveForProject(projectId) && ( ( - +
+ +
)} /> - - )} + )} {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( <> - {watch("parent") ? ( + {watch("parent_id") ? (
- + {selectedParentIssue && `${selectedParentIssue.project__identifier}- @@ -563,26 +580,24 @@ export const IssueForm: FC = observer((props) => { setParentIssueListModalOpen(true)}> Change parent issue - setValue("parent", null)}> + setValue("parent_id", null)}> Remove parent issue ) : ( )} ( ; issueActions: { - [EIssueActions.DELETE]: (issue: IIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: IIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: IIssue) => Promise; + [EIssueActions.DELETE]: (issue: TIssue) => Promise; + [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; + [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; }; viewId?: string; - handleDragDrop: (source: any, destination: any, issues: any, issueWithIds: any) => Promise; } export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { - const { issueStore, issuesFilterStore, QuickActions, issueActions, viewId, handleDragDrop } = props; + const { issueStore, issuesFilterStore, QuickActions, issueActions, viewId } = props; // router const router = useRouter(); - const { workspaceSlug, peekIssueId, peekProjectId } = router.query; + const { workspaceSlug, projectId, peekIssueId, peekProjectId } = router.query; // hooks const { setToastAlert } = useToast(); + const { issueMap } = useIssues(); const displayFilters = issuesFilterStore.issueFilters?.displayFilters; - const issues = issueStore.getIssues; - const groupedIssueIds = (issueStore.getIssuesIds ?? {}) as IGroupedIssues; + const groupedIssueIds = (issueStore.groupedIssueIds ?? {}) as TGroupedIssues; const onDragEnd = async (result: DropResult) => { if (!result) return; @@ -64,7 +54,15 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { if (result.destination.droppableId === result.source.droppableId) return; if (handleDragDrop) { - await handleDragDrop(result.source, result.destination, issues, groupedIssueIds).catch((err) => { + await handleDragDrop( + result.source, + result.destination, + workspaceSlug?.toString(), + projectId?.toString(), + issueStore, + issueMap, + groupedIssueIds + ).catch((err) => { setToastAlert({ title: "Error", type: "error", @@ -75,7 +73,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { }; const handleIssues = useCallback( - async (date: string, issue: IIssue, action: EIssueActions) => { + async (date: string, issue: TIssue, action: EIssueActions) => { if (issueActions[action]) { await issueActions[action]!(issue); } @@ -89,7 +87,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { { workspaceSlug={workspaceSlug.toString()} projectId={peekProjectId.toString()} issueId={peekIssueId.toString()} - handleIssue={async (issueToUpdate, action: EIssueActions) => - await handleIssues(issueToUpdate.target_date ?? "", issueToUpdate as IIssue, action) + handleIssue={async (issueToUpdate) => + await handleIssues(issueToUpdate.target_date ?? "", issueToUpdate as TIssue, EIssueActions.UPDATE) } /> )} diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index a2626b023..1652aa89b 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -1,60 +1,56 @@ import { observer } from "mobx-react-lite"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useUser } from "hooks/store"; // components import { CalendarHeader, CalendarWeekDays, CalendarWeekHeader } from "components/issues"; // ui import { Spinner } from "@plane/ui"; // types import { ICalendarWeek } from "./types"; -import { IIssue } from "types"; -import { IGroupedIssues, IIssueResponse } from "store/issues/types"; -import { - ICycleIssuesFilterStore, - IModuleIssuesFilterStore, - IProjectIssuesFilterStore, - IViewIssuesFilterStore, -} from "store/issues"; +import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EUserProjectRoles } from "constants/project"; +import { useCalendarView } from "hooks/store/use-calendar-view"; +import { EIssuesStoreType } from "constants/issue"; +import { ICycleIssuesFilter } from "store/issue/cycle"; +import { IModuleIssuesFilter } from "store/issue/module"; +import { IProjectIssuesFilter } from "store/issue/project"; +import { IProjectViewIssuesFilter } from "store/issue/project-views"; type Props = { - issuesFilterStore: - | IProjectIssuesFilterStore - | IModuleIssuesFilterStore - | ICycleIssuesFilterStore - | IViewIssuesFilterStore; - issues: IIssueResponse | undefined; - groupedIssueIds: IGroupedIssues; + issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; + issues: TIssueMap | undefined; + groupedIssueIds: TGroupedIssues; layout: "month" | "week" | undefined; showWeekends: boolean; - quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode; + quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; viewId?: string; }; export const CalendarChart: React.FC = observer((props) => { const { issuesFilterStore, issues, groupedIssueIds, layout, showWeekends, quickActions, quickAddCallback, viewId } = props; - + // store hooks const { - calendar: calendarStore, - projectIssues: issueStore, - user: { currentProjectRole }, - } = useMobxStore(); + issues: { viewFlags }, + } = useIssues(EIssuesStoreType.PROJECT); + const issueCalendarView = useCalendarView(); + const { + membership: { currentProjectRole }, + } = useUser(); - const { enableIssueCreation } = issueStore?.viewFlags || {}; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const { enableIssueCreation } = viewFlags || {}; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - const calendarPayload = calendarStore.calendarPayload; + const calendarPayload = issueCalendarView.calendarPayload; - const allWeeksOfActiveMonth = calendarStore.allWeeksOfActiveMonth; + const allWeeksOfActiveMonth = issueCalendarView.allWeeksOfActiveMonth; if (!calendarPayload) return ( @@ -66,7 +62,7 @@ export const CalendarChart: React.FC = observer((props) => { return ( <>
- +
{layout === "month" && ( @@ -91,7 +87,7 @@ export const CalendarChart: React.FC = observer((props) => { {layout === "week" && ( React.ReactNode; + issues: TIssueMap | undefined; + groupedIssueIds: TGroupedIssues; + quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; enableQuickIssueCreate?: boolean; disableIssueCreation?: boolean; quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; viewId?: string; }; diff --git a/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx b/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx index ae2b55a55..2443ae17b 100644 --- a/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx +++ b/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx @@ -2,17 +2,26 @@ import React, { useState } from "react"; import { Popover, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; import { usePopper } from "react-popper"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +//hooks +import { useCalendarView } from "hooks/store"; // icons import { ChevronLeft, ChevronRight } from "lucide-react"; // constants import { MONTHS_LIST } from "constants/calendar"; +import { ICycleIssuesFilter } from "store/issue/cycle"; +import { IModuleIssuesFilter } from "store/issue/module"; +import { IProjectIssuesFilter } from "store/issue/project"; +import { IProjectViewIssuesFilter } from "store/issue/project-views"; -export const CalendarMonthsDropdown: React.FC = observer(() => { - const { calendar: calendarStore, issueFilter: issueFilterStore } = useMobxStore(); +interface Props { + issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; +} +export const CalendarMonthsDropdown: React.FC = observer((props: Props) => { + const { issuesFilterStore } = props; - const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month"; + const issueCalendarView = useCalendarView(); + + const calendarLayout = issuesFilterStore.issueFilters?.displayFilters?.calendar?.layout ?? "month"; const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -29,10 +38,10 @@ export const CalendarMonthsDropdown: React.FC = observer(() => { ], }); - const { activeMonthDate } = calendarStore.calendarFilters; + const { activeMonthDate } = issueCalendarView.calendarFilters; const getWeekLayoutHeader = (): string => { - const allDaysOfActiveWeek = calendarStore.allDaysOfActiveWeek; + const allDaysOfActiveWeek = issueCalendarView.allDaysOfActiveWeek; if (!allDaysOfActiveWeek) return "Week view"; @@ -55,7 +64,7 @@ export const CalendarMonthsDropdown: React.FC = observer(() => { }; const handleDateChange = (date: Date) => { - calendarStore.updateCalendarFilters({ + issueCalendarView.updateCalendarFilters({ activeMonthDate: date, }); }; diff --git a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx index c1778b334..0abe8580d 100644 --- a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx +++ b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx @@ -3,39 +3,34 @@ import { useRouter } from "next/router"; import { Popover, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; import { usePopper } from "react-popper"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useCalendarView } from "hooks/store"; // ui import { ToggleSwitch } from "@plane/ui"; // icons import { Check, ChevronUp } from "lucide-react"; // types -import { TCalendarLayouts } from "types"; +import { TCalendarLayouts } from "@plane/types"; // constants import { CALENDAR_LAYOUTS } from "constants/calendar"; -import { EFilterType } from "store/issues/types"; -import { - ICycleIssuesFilterStore, - IModuleIssuesFilterStore, - IProjectIssuesFilterStore, - IViewIssuesFilterStore, -} from "store/issues"; +import { EIssueFilterType } from "constants/issue"; +import { ICycleIssuesFilter } from "store/issue/cycle"; +import { IModuleIssuesFilter } from "store/issue/module"; +import { IProjectIssuesFilter } from "store/issue/project"; +import { IProjectViewIssuesFilter } from "store/issue/project-views"; interface ICalendarHeader { - issuesFilterStore: - | IProjectIssuesFilterStore - | IModuleIssuesFilterStore - | ICycleIssuesFilterStore - | IViewIssuesFilterStore; + issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; + viewId?: string; } export const CalendarOptionsDropdown: React.FC = observer((props) => { - const { issuesFilterStore } = props; + const { issuesFilterStore, viewId } = props; const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { calendar: calendarStore } = useMobxStore(); + const issueCalendarView = useCalendarView(); const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -58,15 +53,17 @@ export const CalendarOptionsDropdown: React.FC = observer((prop const handleLayoutChange = (layout: TCalendarLayouts) => { if (!workspaceSlug || !projectId) return; - issuesFilterStore.updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.DISPLAY_FILTERS, { + issuesFilterStore.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { calendar: { ...issuesFilterStore.issueFilters?.displayFilters?.calendar, layout, }, }); - calendarStore.updateCalendarPayload( - layout === "month" ? calendarStore.calendarFilters.activeMonthDate : calendarStore.calendarFilters.activeWeekDate + issueCalendarView.updateCalendarPayload( + layout === "month" + ? issueCalendarView.calendarFilters.activeMonthDate + : issueCalendarView.calendarFilters.activeWeekDate ); }; @@ -75,12 +72,18 @@ export const CalendarOptionsDropdown: React.FC = observer((prop if (!workspaceSlug || !projectId) return; - issuesFilterStore.updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.DISPLAY_FILTERS, { - calendar: { - ...issuesFilterStore.issueFilters?.displayFilters?.calendar, - show_weekends: !showWeekends, + issuesFilterStore.updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + { + calendar: { + ...issuesFilterStore.issueFilters?.displayFilters?.calendar, + show_weekends: !showWeekends, + }, }, - }); + viewId + ); }; return ( diff --git a/web/components/issues/issue-layouts/calendar/header.tsx b/web/components/issues/issue-layouts/calendar/header.tsx index 1a2280d05..ebbb510fc 100644 --- a/web/components/issues/issue-layouts/calendar/header.tsx +++ b/web/components/issues/issue-layouts/calendar/header.tsx @@ -1,34 +1,28 @@ import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // components import { CalendarMonthsDropdown, CalendarOptionsDropdown } from "components/issues"; // icons import { ChevronLeft, ChevronRight } from "lucide-react"; -import { - ICycleIssuesFilterStore, - IModuleIssuesFilterStore, - IProjectIssuesFilterStore, - IViewIssuesFilterStore, -} from "store/issues"; +import { useCalendarView } from "hooks/store/use-calendar-view"; +import { ICycleIssuesFilter } from "store/issue/cycle"; +import { IModuleIssuesFilter } from "store/issue/module"; +import { IProjectIssuesFilter } from "store/issue/project"; +import { IProjectViewIssuesFilter } from "store/issue/project-views"; interface ICalendarHeader { - issuesFilterStore: - | IProjectIssuesFilterStore - | IModuleIssuesFilterStore - | ICycleIssuesFilterStore - | IViewIssuesFilterStore; + issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; + viewId?: string; } export const CalendarHeader: React.FC = observer((props) => { - const { issuesFilterStore } = props; + const { issuesFilterStore, viewId } = props; - const { calendar: calendarStore } = useMobxStore(); + const issueCalendarView = useCalendarView(); const calendarLayout = issuesFilterStore.issueFilters?.displayFilters?.calendar?.layout ?? "month"; - const { activeMonthDate, activeWeekDate } = calendarStore.calendarFilters; + const { activeMonthDate, activeWeekDate } = issueCalendarView.calendarFilters; const handlePrevious = () => { if (calendarLayout === "month") { @@ -38,7 +32,7 @@ export const CalendarHeader: React.FC = observer((props) => { const previousMonthFirstDate = new Date(previousMonthYear, previousMonthMonth, 1); - calendarStore.updateCalendarFilters({ + issueCalendarView.updateCalendarFilters({ activeMonthDate: previousMonthFirstDate, }); } else { @@ -48,7 +42,7 @@ export const CalendarHeader: React.FC = observer((props) => { activeWeekDate.getDate() - 7 ); - calendarStore.updateCalendarFilters({ + issueCalendarView.updateCalendarFilters({ activeWeekDate: previousWeekDate, }); } @@ -62,7 +56,7 @@ export const CalendarHeader: React.FC = observer((props) => { const nextMonthFirstDate = new Date(nextMonthYear, nextMonthMonth, 1); - calendarStore.updateCalendarFilters({ + issueCalendarView.updateCalendarFilters({ activeMonthDate: nextMonthFirstDate, }); } else { @@ -72,7 +66,7 @@ export const CalendarHeader: React.FC = observer((props) => { activeWeekDate.getDate() + 7 ); - calendarStore.updateCalendarFilters({ + issueCalendarView.updateCalendarFilters({ activeWeekDate: nextWeekDate, }); } @@ -82,7 +76,7 @@ export const CalendarHeader: React.FC = observer((props) => { const today = new Date(); const firstDayOfCurrentMonth = new Date(today.getFullYear(), today.getMonth(), 1); - calendarStore.updateCalendarFilters({ + issueCalendarView.updateCalendarFilters({ activeMonthDate: firstDayOfCurrentMonth, activeWeekDate: today, }); @@ -97,7 +91,7 @@ export const CalendarHeader: React.FC = observer((props) => { - +
- +
); diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index f8eead33f..be30560fb 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -8,16 +8,13 @@ import { Tooltip } from "@plane/ui"; // hooks import useOutsideClickDetector from "hooks/use-outside-click-detector"; // types -import { IIssue } from "types"; -import { IIssueResponse } from "store/issues/types"; -import { useMobxStore } from "lib/mobx/store-provider"; -// constants -import { EUserWorkspaceRoles } from "constants/workspace"; +import { TIssue, TIssueMap } from "@plane/types"; +import { useProject, useProjectState } from "hooks/store"; type Props = { - issues: IIssueResponse | undefined; + issues: TIssueMap | undefined; issueIdList: string[] | null; - quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode; + quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; showAllIssues?: boolean; }; @@ -25,28 +22,21 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { const { issues, issueIdList, quickActions, showAllIssues = false } = props; // router const router = useRouter(); - + // hooks + const { getProjectById } = useProject(); + const { getProjectStates } = useProjectState(); // states const [isMenuActive, setIsMenuActive] = useState(false); - // mobx store - const { - user: { currentProjectRole }, - } = useMobxStore(); - const menuActionRef = useRef(null); - const handleIssuePeekOverview = (issue: IIssue, event: React.MouseEvent) => { + const handleIssuePeekOverview = (issue: TIssue) => { const { query } = router; - if (event.ctrlKey || event.metaKey) { - const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`; - window.open(issueUrl, "_blank"); // Open link in a new tab - } else { - router.push({ - pathname: router.pathname, - query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project }, - }); - } + + router.push({ + pathname: router.pathname, + query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id }, + }); }; useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); @@ -63,8 +53,6 @@ export const CalendarIssueBlocks: React.FC = observer((props) => {
); - const isEditable = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; - return ( <> {issueIdList?.slice(0, showAllIssues ? issueIdList.length : 4).map((issueId, index) => { @@ -72,14 +60,14 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { const issue = issues?.[issueId]; return ( - + {(provided, snapshot) => (
handleIssuePeekOverview(issue, e)} + onClick={() => handleIssuePeekOverview(issue)} > {issue?.tempId !== undefined && (
@@ -96,11 +84,13 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { state?.id == issue?.state_id + )?.color, }} />
- {issue.project_detail.identifier}-{issue.sequence_id} + {getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id}
{issue.name}
diff --git a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx index 85a74a997..7a3c01417 100644 --- a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx @@ -2,9 +2,8 @@ import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { useProject, useWorkspace } from "hooks/store"; import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; @@ -13,24 +12,24 @@ import { createIssuePayload } from "helpers/issue.helper"; // icons import { PlusIcon } from "lucide-react"; // types -import { IIssue, IProject } from "types"; +import { TIssue } from "@plane/types"; type Props = { - formKey: keyof IIssue; + formKey: keyof TIssue; groupId?: string; subGroupId?: string | null; - prePopulatedData?: Partial; + prePopulatedData?: Partial; quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; viewId?: string; onOpen?: () => void; }; -const defaultValues: Partial = { +const defaultValues: Partial = { name: "", }; @@ -62,22 +61,20 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; - - const { workspace: workspaceStore, project: projectStore } = useMobxStore(); - - // ref + const { workspaceSlug, projectId } = router.query; + // store hooks + const { getProjectById } = useProject(); + const { getWorkspaceBySlug } = useWorkspace(); + // refs const ref = useRef(null); - // states const [isOpen, setIsOpen] = useState(false); - + // toast alert const { setToastAlert } = useToast(); // derived values - const workspaceDetail = (workspaceSlug && workspaceStore.getWorkspaceBySlug(workspaceSlug)) || null; - const projectDetail: IProject | null = - (workspaceSlug && projectId && projectStore.getProjectById(workspaceSlug, projectId)) || null; + const workspaceDetail = (workspaceSlug && getWorkspaceBySlug(workspaceSlug.toString())) || null; + const projectDetail = projectId ? getProjectById(projectId.toString()) : null; const { reset, @@ -85,7 +82,7 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { register, setFocus, formState: { errors, isSubmitting }, - } = useForm({ defaultValues }); + } = useForm({ defaultValues }); const handleClose = () => { setIsOpen(false); @@ -102,7 +99,7 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { if (!errors) return; Object.keys(errors).forEach((key) => { - const error = errors[key as keyof IIssue]; + const error = errors[key as keyof TIssue]; setToastAlert({ type: "error", @@ -112,8 +109,8 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { }); }, [errors, setToastAlert]); - const onSubmitHandler = async (formData: IIssue) => { - if (isSubmitting || !groupId || !workspaceDetail || !projectDetail) return; + const onSubmitHandler = async (formData: TIssue) => { + if (isSubmitting || !groupId || !workspaceDetail || !projectDetail || !workspaceSlug || !projectId) return; reset({ ...defaultValues }); @@ -125,8 +122,8 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { try { quickAddCallback && (await quickAddCallback( - workspaceSlug, - projectId, + workspaceSlug.toString(), + projectId.toString(), { ...payload, }, diff --git a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx index 88025ad68..585b1a5e1 100644 --- a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx @@ -1,74 +1,50 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +//hooks +import { useIssues } from "hooks/store"; // components import { CycleIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; import { BaseCalendarRoot } from "../base-calendar-root"; +import { EIssuesStoreType } from "constants/issue"; +import { useMemo } from "react"; export const CycleCalendarLayout: React.FC = observer(() => { - const { - cycleIssues: cycleIssueStore, - cycleIssuesFilter: cycleIssueFilterStore, - calendarHelpers: { handleDragDrop: handleCalenderDragDrop }, - cycle: { fetchCycleWithId }, - } = useMobxStore(); + const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; - const issueActions = { - [EIssueActions.UPDATE]: async (issue: IIssue) => { - if (!workspaceSlug || !cycleId) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId) return; - await cycleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, cycleId.toString()); - fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: IIssue) => { - if (!workspaceSlug || !cycleId) return; - await cycleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, cycleId.toString()); - fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: IIssue) => { - if (!workspaceSlug || !cycleId || !projectId || !issue.bridge_id) return; - await cycleIssueStore.removeIssueFromCycle( - workspaceSlug.toString(), - issue.project, - cycleId.toString(), - issue.id, - issue.bridge_id - ); - fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString()); - }, - }; - - const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => { - if (workspaceSlug && projectId && cycleId) - await handleCalenderDragDrop( - source, - destination, - workspaceSlug.toString(), - projectId.toString(), - cycleIssueStore, - issues, - issueWithIds, - cycleId.toString() - ); - }; + await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId) return; + await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); + }, + [EIssueActions.REMOVE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId || !projectId) return; + await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); + }, + }), + [issues, workspaceSlug, cycleId, projectId] + ); if (!cycleId) return null; return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx index 4a7cfbd3f..d2b23e176 100644 --- a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx @@ -1,68 +1,50 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hoks +import { useIssues } from "hooks/store"; // components import { ModuleIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; import { BaseCalendarRoot } from "../base-calendar-root"; +import { EIssuesStoreType } from "constants/issue"; +import { useMemo } from "react"; export const ModuleCalendarLayout: React.FC = observer(() => { - const { - moduleIssues: moduleIssueStore, - moduleIssuesFilter: moduleIssueFilterStore, - calendarHelpers: { handleDragDrop: handleCalenderDragDrop }, - module: { fetchModuleDetails }, - } = useMobxStore(); - + const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); const router = useRouter(); - const { workspaceSlug, projectId, moduleId } = router.query as { + const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; projectId: string; moduleId: string; }; - const issueActions = { - [EIssueActions.UPDATE]: async (issue: IIssue) => { - if (!workspaceSlug || !moduleId) return; - await moduleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, moduleId); - fetchModuleDetails(workspaceSlug, issue.project, moduleId); - }, - [EIssueActions.DELETE]: async (issue: IIssue) => { - if (!workspaceSlug || !moduleId) return; - await moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId); - fetchModuleDetails(workspaceSlug, issue.project, moduleId); - }, - [EIssueActions.REMOVE]: async (issue: IIssue) => { - if (!workspaceSlug || !moduleId || !issue.bridge_id) return; - await moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, moduleId, issue.id, issue.bridge_id); - fetchModuleDetails(workspaceSlug, issue.project, moduleId); - }, - }; - - const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => { - await handleCalenderDragDrop( - source, - destination, - workspaceSlug, - projectId, - moduleIssueStore, - issues, - issueWithIds, - moduleId - ); - }; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; + await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, moduleId); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; + await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, moduleId); + }, + [EIssueActions.REMOVE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; + await issues.removeIssueFromModule(workspaceSlug, issue.project_id, moduleId, issue.id); + }, + }), + [issues, workspaceSlug, moduleId] + ); return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx index e71cc7e3b..40f72e7b8 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx @@ -1,56 +1,43 @@ import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { useRouter } from "next/router"; +// hooks +import { useIssues } from "hooks/store"; // components import { ProjectIssueQuickActions } from "components/issues"; import { BaseCalendarRoot } from "../base-calendar-root"; import { EIssueActions } from "../../types"; -import { IIssue } from "types"; -import { useRouter } from "next/router"; +import { TIssue } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; +import { useMemo } from "react"; export const CalendarLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug } = router.query; - const { - projectIssues: issueStore, - projectIssuesFilter: projectIssueFiltersStore, - calendarHelpers: { handleDragDrop: handleCalenderDragDrop }, - } = useMobxStore(); + const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - const issueActions = { - [EIssueActions.UPDATE]: async (issue: IIssue) => { - if (!workspaceSlug) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug) return; - await issueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: IIssue) => { - if (!workspaceSlug) return; + await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug) return; - await issueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id); - }, - }; - - const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => { - if (workspaceSlug && projectId) - await handleCalenderDragDrop( - source, - destination, - workspaceSlug.toString(), - projectId.toString(), - issueStore, - issues, - issueWithIds - ); - }; + await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); + }, + }), + [issues, workspaceSlug] + ); return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx index 95d746eec..43e59dc76 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx @@ -1,57 +1,44 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues } from "hooks/store"; // components import { ProjectIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; import { BaseCalendarRoot } from "../base-calendar-root"; +import { EIssuesStoreType } from "constants/issue"; +import { useMemo } from "react"; export const ProjectViewCalendarLayout: React.FC = observer(() => { - const { - viewIssues: projectViewIssuesStore, - viewIssuesFilter: projectIssueViewFiltersStore, - calendarHelpers: { handleDragDrop: handleCalenderDragDrop }, - } = useMobxStore(); - + const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, viewId } = router.query; - const issueActions = { - [EIssueActions.UPDATE]: async (issue: IIssue) => { - if (!workspaceSlug) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - await projectViewIssuesStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: IIssue) => { - if (!workspaceSlug) return; + await issues.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - await projectViewIssuesStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id); - }, - }; - - const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => { - if (workspaceSlug && projectId) - await handleCalenderDragDrop( - source, - destination, - workspaceSlug.toString(), - projectId.toString(), - projectViewIssuesStore, - issues, - issueWithIds - ); - }; + await issues.removeIssue(workspaceSlug.toString(), projectId.toString(), issue.id); + }, + }), + [issues, workspaceSlug, projectId] + ); return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/utils.ts b/web/components/issues/issue-layouts/calendar/utils.ts new file mode 100644 index 000000000..82d9ce0ce --- /dev/null +++ b/web/components/issues/issue-layouts/calendar/utils.ts @@ -0,0 +1,42 @@ +import { DraggableLocation } from "@hello-pangea/dnd"; +import { ICycleIssues } from "store/issue/cycle"; +import { IModuleIssues } from "store/issue/module"; +import { IProjectIssues } from "store/issue/project"; +import { IProjectViewIssues } from "store/issue/project-views"; +import { TGroupedIssues, IIssueMap } from "@plane/types"; + +export const handleDragDrop = async ( + source: DraggableLocation, + destination: DraggableLocation, + workspaceSlug: string | undefined, + projectId: string | undefined, + store: IProjectIssues | IModuleIssues | ICycleIssues | IProjectViewIssues, + issueMap: IIssueMap, + issueWithIds: TGroupedIssues, + viewId: string | null = null // it can be moduleId, cycleId +) => { + if (!issueMap || !issueWithIds || !workspaceSlug || !projectId) return; + + const sourceColumnId = source?.droppableId || null; + const destinationColumnId = destination?.droppableId || null; + + if (!workspaceSlug || !projectId || !sourceColumnId || !destinationColumnId) return; + + if (sourceColumnId === destinationColumnId) return; + + // horizontal + if (sourceColumnId != destinationColumnId) { + const sourceIssues = issueWithIds[sourceColumnId] || []; + + const [removed] = sourceIssues.splice(source.index, 1); + const removedIssueDetail = issueMap[removed]; + + const updateIssue = { + id: removedIssueDetail?.id, + target_date: destinationColumnId, + }; + + if (viewId) return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue, viewId); + else return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue); + } +}; diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index 3160dcfba..c34aaef97 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -5,33 +5,26 @@ import { CalendarDayTile } from "components/issues"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types import { ICalendarDate, ICalendarWeek } from "./types"; -import { IIssue } from "types"; -import { IGroupedIssues, IIssueResponse } from "store/issues/types"; -import { - ICycleIssuesFilterStore, - IModuleIssuesFilterStore, - IProjectIssuesFilterStore, - IViewIssuesFilterStore, -} from "store/issues"; +import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; +import { ICycleIssuesFilter } from "store/issue/cycle"; +import { IModuleIssuesFilter } from "store/issue/module"; +import { IProjectIssuesFilter } from "store/issue/project"; +import { IProjectViewIssuesFilter } from "store/issue/project-views"; type Props = { - issuesFilterStore: - | IProjectIssuesFilterStore - | IModuleIssuesFilterStore - | ICycleIssuesFilterStore - | IViewIssuesFilterStore; - issues: IIssueResponse | undefined; - groupedIssueIds: IGroupedIssues; + issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; + issues: TIssueMap | undefined; + groupedIssueIds: TGroupedIssues; week: ICalendarWeek | undefined; - quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode; + quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; enableQuickIssueCreate?: boolean; disableIssueCreation?: boolean; quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; viewId?: string; }; diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 52baa2eb8..1c3ba1628 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -1,9 +1,8 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { useApplication, useIssues, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { EmptyState } from "components/common"; @@ -13,10 +12,10 @@ import { Button } from "@plane/ui"; // assets import emptyIssue from "public/empty-state/issue.svg"; // types -import { ISearchIssueResponse } from "types"; -import { EProjectStore } from "store/command-palette.store"; +import { ISearchIssueResponse } from "@plane/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EUserProjectRoles } from "constants/project"; +import { EIssuesStoreType } from "constants/issue"; type Props = { workspaceSlug: string | undefined; @@ -28,13 +27,15 @@ export const CycleEmptyState: React.FC = observer((props) => { const { workspaceSlug, projectId, cycleId } = props; // states const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); - + // store hooks + const { issues } = useIssues(EIssuesStoreType.CYCLE); const { - cycleIssues: cycleIssueStore, - commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - user: { currentProjectRole: userRole }, - } = useMobxStore(); + commandPalette: { toggleCreateIssueModal }, + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole: userRole }, + } = useUser(); const { setToastAlert } = useToast(); @@ -43,7 +44,7 @@ export const CycleEmptyState: React.FC = observer((props) => { const issueIds = data.map((i) => i.id); - await cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), cycleId.toString(), issueIds).catch(() => { + await issues.addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -52,7 +53,7 @@ export const CycleEmptyState: React.FC = observer((props) => { }); }; - const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER; + const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER; return ( <> @@ -72,7 +73,7 @@ export const CycleEmptyState: React.FC = observer((props) => { icon: , onClick: () => { setTrackElement("CYCLE_EMPTY_STATE"); - commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE); + toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); }, }} secondaryButton={ diff --git a/web/components/issues/issue-layouts/empty-states/global-view.tsx b/web/components/issues/issue-layouts/empty-states/global-view.tsx index d4348c4bf..cd4070186 100644 --- a/web/components/issues/issue-layouts/empty-states/global-view.tsx +++ b/web/components/issues/issue-layouts/empty-states/global-view.tsx @@ -1,31 +1,24 @@ -// next -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { Plus, PlusIcon } from "lucide-react"; +// hooks +import { useApplication, useProject } from "hooks/store"; // components import { EmptyState } from "components/common"; // assets import emptyIssue from "public/empty-state/issue.svg"; import emptyProject from "public/empty-state/project.svg"; -// icons -import { Plus, PlusIcon } from "lucide-react"; export const GlobalViewEmptyState: React.FC = observer(() => { - const router = useRouter(); - const { workspaceSlug } = router.query; - + // store hooks const { - commandPalette: commandPaletteStore, - project: projectStore, - trackEvent: { setTrackElement }, - } = useMobxStore(); - - const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; + commandPalette: { toggleCreateIssueModal, toggleCreateProjectModal }, + eventTracker: { setTrackElement }, + } = useApplication(); + const { workspaceProjectIds } = useProject(); return (
- {!projects || projects?.length === 0 ? ( + {!workspaceProjectIds || workspaceProjectIds?.length === 0 ? ( { text: "New Project", onClick: () => { setTrackElement("ALL_ISSUES_EMPTY_STATE"); - commandPaletteStore.toggleCreateProjectModal(true); + toggleCreateProjectModal(true); }, }} /> @@ -49,7 +42,7 @@ export const GlobalViewEmptyState: React.FC = observer(() => { icon: , onClick: () => { setTrackElement("ALL_ISSUES_EMPTY_STATE"); - commandPaletteStore.toggleCreateIssueModal(true); + toggleCreateIssueModal(true); }, }} /> diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index ed7f73358..cb70a5a22 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -1,17 +1,21 @@ +import { useState } from "react"; +import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; +// hooks +import { useApplication, useIssues, useUser } from "hooks/store"; +import useToast from "hooks/use-toast"; // components import { EmptyState } from "components/common"; +import { ExistingIssuesListModal } from "components/core"; +// ui import { Button } from "@plane/ui"; // assets import emptyIssue from "public/empty-state/issue.svg"; -import { ExistingIssuesListModal } from "components/core"; -import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; -import { ISearchIssueResponse } from "types"; -import useToast from "hooks/use-toast"; -import { useState } from "react"; +// types +import { ISearchIssueResponse } from "@plane/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EUserProjectRoles } from "constants/project"; +import { EIssuesStoreType } from "constants/issue"; type Props = { workspaceSlug: string | undefined; @@ -23,14 +27,16 @@ export const ModuleEmptyState: React.FC = observer((props) => { const { workspaceSlug, projectId, moduleId } = props; // states const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); - + // store hooks + const { issues } = useIssues(EIssuesStoreType.MODULE); const { - moduleIssues: moduleIssueStore, - commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - user: { currentProjectRole: userRole }, - } = useMobxStore(); - + commandPalette: { toggleCreateIssueModal }, + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole: userRole }, + } = useUser(); + // toast alert const { setToastAlert } = useToast(); const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { @@ -38,16 +44,18 @@ export const ModuleEmptyState: React.FC = observer((props) => { const issueIds = data.map((i) => i.id); - await moduleIssueStore.addIssueToModule(workspaceSlug.toString(), moduleId.toString(), issueIds).catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Selected issues could not be added to the module. Please try again.", - }) - ); + await issues + .addIssueToModule(workspaceSlug.toString(), projectId?.toString(), moduleId.toString(), issueIds) + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Selected issues could not be added to the module. Please try again.", + }) + ); }; - const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER; + const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER; return ( <> @@ -67,7 +75,7 @@ export const ModuleEmptyState: React.FC = observer((props) => { icon: , onClick: () => { setTrackElement("MODULE_EMPTY_STATE"); - commandPaletteStore.toggleCreateIssueModal(true); + toggleCreateIssueModal(true); }, }} secondaryButton={ diff --git a/web/components/issues/issue-layouts/empty-states/project-view.tsx b/web/components/issues/issue-layouts/empty-states/project-view.tsx index 2fd297a90..919decd51 100644 --- a/web/components/issues/issue-layouts/empty-states/project-view.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-view.tsx @@ -1,18 +1,19 @@ import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication } from "hooks/store"; // components import { EmptyState } from "components/common"; // assets import emptyIssue from "public/empty-state/issue.svg"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export const ProjectViewEmptyState: React.FC = observer(() => { + // store hooks const { commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - } = useMobxStore(); + eventTracker: { setTrackElement }, + } = useApplication(); return (
@@ -25,7 +26,7 @@ export const ProjectViewEmptyState: React.FC = observer(() => { icon: , onClick: () => { setTrackElement("VIEW_EMPTY_STATE"); - commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW); + commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW); }, }} /> diff --git a/web/components/issues/issue-layouts/empty-states/project.tsx b/web/components/issues/issue-layouts/empty-states/project.tsx index 7db04b36a..592264d82 100644 --- a/web/components/issues/issue-layouts/empty-states/project.tsx +++ b/web/components/issues/issue-layouts/empty-states/project.tsx @@ -1,23 +1,26 @@ import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication, useUser } from "hooks/store"; // components import { NewEmptyState } from "components/common/new-empty-state"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EUserProjectRoles } from "constants/project"; // assets import emptyIssue from "public/empty-state/empty_issues.webp"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export const ProjectEmptyState: React.FC = observer(() => { + // store hooks const { commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - user: { currentProjectRole }, - } = useMobxStore(); + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; return (
@@ -31,18 +34,14 @@ export const ProjectEmptyState: React.FC = observer(() => { description: "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", }} - primaryButton={ - isEditingAllowed - ? { - text: "Create your first issue", - icon: , - onClick: () => { - setTrackElement("PROJECT_EMPTY_STATE"); - commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT); - }, - } - : null - } + primaryButton={{ + text: "Create your first issue", + icon: , + onClick: () => { + setTrackElement("PROJECT_EMPTY_STATE"); + commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); + }, + }} disabled={!isEditingAllowed} />
diff --git a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx index 7ff8056b9..18eac8525 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx @@ -1,5 +1,7 @@ import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; +import { X } from "lucide-react"; +// hooks +import { useUser } from "hooks/store"; // components import { AppliedDateFilters, @@ -10,22 +12,18 @@ import { AppliedStateFilters, AppliedStateGroupFilters, } from "components/issues"; -// icons -import { X } from "lucide-react"; // helpers import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // types -import { IIssueFilterOptions, IIssueLabel, IProject, IState, IUserLite } from "types"; +import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EUserProjectRoles } from "constants/project"; type Props = { appliedFilters: IIssueFilterOptions; handleClearAllFilters: () => void; handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void; labels?: IIssueLabel[] | undefined; - members?: IUserLite[] | undefined; - projects?: IProject[] | undefined; states?: IState[] | undefined; }; @@ -33,17 +31,17 @@ const membersFilters = ["assignees", "mentions", "created_by", "subscriber"]; const dateFilters = ["start_date", "target_date"]; export const AppliedFiltersList: React.FC = observer((props) => { - const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, members, projects, states } = props; - + const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, states } = props; + // store hooks const { - user: { currentProjectRole }, - } = useMobxStore(); + membership: { currentProjectRole }, + } = useUser(); if (!appliedFilters) return null; if (Object.keys(appliedFilters).length === 0) return null; - const isEditingAllowed = currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const isEditingAllowed = currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; return (
@@ -63,7 +61,6 @@ export const AppliedFiltersList: React.FC = observer((props) => { handleRemoveFilter(filterKey, val)} - members={members} values={value} /> )} @@ -103,7 +100,6 @@ export const AppliedFiltersList: React.FC = observer((props) => { handleRemoveFilter("project", val)} - projects={projects} values={value} /> )} diff --git a/web/components/issues/issue-layouts/filters/applied-filters/label.tsx b/web/components/issues/issue-layouts/filters/applied-filters/label.tsx index 08e7aee44..799233d01 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/label.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/label.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite"; // icons import { X } from "lucide-react"; // types -import { IIssueLabel } from "types"; +import { IIssueLabel } from "@plane/types"; type Props = { handleRemove: (val: string) => void; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/members.tsx b/web/components/issues/issue-layouts/filters/applied-filters/members.tsx index 1dd61d339..94ea9221e 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/members.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/members.tsx @@ -3,22 +3,25 @@ import { X } from "lucide-react"; // ui import { Avatar } from "@plane/ui"; // types -import { IUserLite } from "types"; +import { useMember } from "hooks/store"; type Props = { handleRemove: (val: string) => void; - members: IUserLite[] | undefined; values: string[]; editable: boolean | undefined; }; export const AppliedMembersFilters: React.FC = observer((props) => { - const { handleRemove, members, values, editable } = props; + const { handleRemove, values, editable } = props; + + const { + project: { getProjectMemberDetails }, + } = useMember(); return ( <> {values.map((memberId) => { - const memberDetails = members?.find((m) => m.id === memberId); + const memberDetails = getProjectMemberDetails(memberId)?.member; if (!memberDetails) return null; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx b/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx index 88b39dc00..be3240b55 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; import { PriorityIcon } from "@plane/ui"; import { X } from "lucide-react"; // types -import { TIssuePriorities } from "types"; +import { TIssuePriorities } from "@plane/types"; type Props = { handleRemove: (val: string) => void; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx index b1e17cfe3..4c5affe8d 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx @@ -1,25 +1,25 @@ import { observer } from "mobx-react-lite"; - -// icons import { X } from "lucide-react"; -// types -import { IProject } from "types"; +// hooks +import { useProject } from "hooks/store"; +// helpers import { renderEmoji } from "helpers/emoji.helper"; type Props = { handleRemove: (val: string) => void; - projects: IProject[] | undefined; values: string[]; editable: boolean | undefined; }; export const AppliedProjectFilters: React.FC = observer((props) => { - const { handleRemove, projects, values, editable } = props; + const { handleRemove, values, editable } = props; + // store hooks + const { projectMap } = useProject(); return ( <> {values.map((projectId) => { - const projectDetails = projects?.find((p) => p.id === projectId); + const projectDetails = projectMap?.[projectId] ?? null; if (!projectDetails) return null; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx index 2b6571d3b..b09bc7628 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx @@ -1,27 +1,28 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useLabel, useProjectState } from "hooks/store"; // components import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { IIssueFilterOptions } from "@plane/types"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + // store hooks const { - projectArchivedIssuesFilter: { issueFilters, updateFilters }, - projectLabel: { projectLabels }, - projectMember: { projectMembers }, - projectState: projectStateStore, - } = useMobxStore(); - + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.ARCHIVED); + const { + project: { projectLabels }, + } = useLabel(); + const { projectStates } = useProjectState(); + // derived values const userFilters = issueFilters?.filters; - // filters whose value not null or empty array const appliedFilters: IIssueFilterOptions = {}; Object.entries(userFilters ?? {}).forEach(([key, value]) => { @@ -37,7 +38,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { // remove all values of the key if value is null if (!value) { - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { [key]: null, }); return; @@ -47,7 +48,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { let newValues = issueFilters?.filters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues, }); }; @@ -60,7 +61,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters, }); }; @@ -75,8 +76,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectLabels ?? []} - members={projectMembers?.map((m) => m.member)} - states={projectStateStore.states?.[projectId?.toString() ?? ""]} + states={projectStates} /> diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx index b7c8b6889..f402c9807 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx @@ -1,31 +1,32 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useLabel, useProjectState } from "hooks/store"; // components import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { IIssueFilterOptions } from "@plane/types"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const CycleAppliedFiltersRoot: React.FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query as { workspaceSlug: string; projectId: string; cycleId: string; }; + // store hooks + const { + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.CYCLE); const { - projectLabel: { projectLabels }, - projectState: projectStateStore, - projectMember: { projectMembers }, - cycleIssuesFilter: { issueFilters, updateFilters }, - } = useMobxStore(); - + project: { projectLabels }, + } = useLabel(); + const { projectStates } = useProjectState(); + // derived values const userFilters = issueFilters?.filters; - // filters whose value not null or empty array const appliedFilters: IIssueFilterOptions = {}; Object.entries(userFilters ?? {}).forEach(([key, value]) => { @@ -35,32 +36,20 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { }); const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !cycleId) return; if (!value) { - updateFilters( - workspaceSlug, - projectId, - EFilterType.FILTERS, - { - [key]: null, - }, - cycleId - ); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { + [key]: null, + }); return; } let newValues = issueFilters?.filters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters( - workspaceSlug, - projectId, - EFilterType.FILTERS, - { - [key]: newValues, - }, - cycleId - ); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { + [key]: newValues, + }); }; const handleClearAllFilters = () => { @@ -69,7 +58,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { ...newFilters }, cycleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { ...newFilters }, cycleId); }; // return if no filters are applied @@ -82,8 +71,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectLabels ?? []} - members={projectMembers?.map((m) => m.member)} - states={projectStateStore.states?.[cycleId ?? ""]} + states={projectStates} /> diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx index d3d56266d..f650c0bd5 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx @@ -1,25 +1,26 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useLabel, useProjectState } from "hooks/store"; // components import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { IIssueFilterOptions } from "@plane/types"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; - + // store hooks const { - projectDraftIssuesFilter: { issueFilters, updateFilters }, - projectLabel: { projectLabels }, - projectMember: { projectMembers }, - projectState: projectStateStore, - } = useMobxStore(); - + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.DRAFT); + const { + project: { projectLabels }, + } = useLabel(); + const { projectStates } = useProjectState(); + // derived values const userFilters = issueFilters?.filters; // filters whose value not null or empty array const appliedFilters: IIssueFilterOptions = {}; @@ -34,7 +35,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => { // remove all values of the key if value is null if (!value) { - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { [key]: null, }); return; @@ -44,7 +45,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => { let newValues = issueFilters?.filters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues, }); }; @@ -57,7 +58,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { ...newFilters }); + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters }); }; // return if no filters are applied @@ -70,8 +71,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectLabels ?? []} - members={projectMembers?.map((m) => m.member)} - states={projectStateStore.states?.[projectId?.toString() ?? ""]} + states={projectStates} /> diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx index 543d18645..87bb719c4 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx @@ -1,25 +1,25 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useLabel } from "hooks/store"; // components import { AppliedFiltersList } from "components/issues"; // types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { IIssueFilterOptions } from "@plane/types"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const GlobalViewsAppliedFiltersRoot = observer(() => { + // router const router = useRouter(); - const { workspaceSlug } = router.query as { workspaceSlug: string; globalViewId: string }; - + const { workspaceSlug, globalViewId } = router.query; + // store hooks + const { + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.GLOBAL); const { - project: { workspaceProjects }, workspace: { workspaceLabels }, - workspaceMember: { workspaceMembers }, - workspaceGlobalIssuesFilter: { issueFilters, updateFilters }, - } = useMobxStore(); - + } = useLabel(); + // derived values const userFilters = issueFilters?.filters; // filters whose value not null or empty array @@ -31,23 +31,43 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => { }); const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { + if (!workspaceSlug || !globalViewId) return; + if (!value) { - updateFilters(workspaceSlug, EFilterType.FILTERS, { [key]: null }); + updateFilters( + workspaceSlug.toString(), + undefined, + EIssueFilterType.FILTERS, + { [key]: null }, + globalViewId.toString() + ); return; } let newValues = userFilters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters(workspaceSlug, EFilterType.FILTERS, { [key]: newValues }); + updateFilters( + workspaceSlug.toString(), + undefined, + EIssueFilterType.FILTERS, + { [key]: newValues }, + globalViewId.toString() + ); }; const handleClearAllFilters = () => { - if (!workspaceSlug) return; + if (!workspaceSlug || !globalViewId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, EFilterType.FILTERS, { ...newFilters }); + updateFilters( + workspaceSlug.toString(), + undefined, + EIssueFilterType.FILTERS, + { ...newFilters }, + globalViewId.toString() + ); }; // const handleUpdateView = () => { @@ -78,8 +98,6 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => {
m.member)} - projects={workspaceProjects ?? undefined} appliedFilters={appliedFilters ?? {}} handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx index 62cd4b3d8..a9a7832c6 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx @@ -1,31 +1,31 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useLabel, useProjectState } from "hooks/store"; // components import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { IIssueFilterOptions } from "@plane/types"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const ModuleAppliedFiltersRoot: React.FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query as { workspaceSlug: string; projectId: string; moduleId: string; }; - + // store hooks const { - projectLabel: { projectLabels }, - projectState: projectStateStore, - projectMember: { projectMembers }, - moduleIssuesFilter: { issueFilters, updateFilters }, - } = useMobxStore(); - + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.MODULE); + const { + project: { projectLabels }, + } = useLabel(); + const { projectStates } = useProjectState(); + // derived values const userFilters = issueFilters?.filters; - // filters whose value not null or empty array const appliedFilters: IIssueFilterOptions = {}; Object.entries(userFilters ?? {}).forEach(([key, value]) => { @@ -37,30 +37,18 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { if (!workspaceSlug || !projectId) return; if (!value) { - updateFilters( - workspaceSlug, - projectId, - EFilterType.FILTERS, - { - [key]: null, - }, - moduleId - ); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { + [key]: null, + }); return; } let newValues = issueFilters?.filters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters( - workspaceSlug, - projectId, - EFilterType.FILTERS, - { - [key]: newValues, - }, - moduleId - ); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { + [key]: newValues, + }); }; const handleClearAllFilters = () => { @@ -69,7 +57,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { ...newFilters }, moduleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { ...newFilters }, moduleId); }; // return if no filters are applied @@ -82,8 +70,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectLabels ?? []} - members={projectMembers?.map((m) => m.member)} - states={projectStateStore.states?.[moduleId ?? ""]} + states={projectStates} /> diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx index 89870d98a..0c45c025e 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx @@ -1,26 +1,26 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useLabel } from "hooks/store"; // components import { AppliedFiltersList } from "components/issues"; // types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { IIssueFilterOptions } from "@plane/types"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => { + // router const router = useRouter(); - const { workspaceSlug } = router.query as { - workspaceSlug: string; - }; + const { workspaceSlug, userId } = router.query; + // store hooks + const { + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.PROFILE); const { workspace: { workspaceLabels }, - workspaceProfileIssuesFilter: { issueFilters, updateFilters }, - projectMember: { projectMembers }, - } = useMobxStore(); - + } = useLabel(); + // derived values const userFilters = issueFilters?.filters; // filters whose value not null or empty array @@ -32,27 +32,33 @@ export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => { }); const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { - if (!workspaceSlug) return; + if (!workspaceSlug || !userId) return; if (!value) { - updateFilters(workspaceSlug, EFilterType.FILTERS, { [key]: null }); + updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.FILTERS, { [key]: null }, userId.toString()); return; } let newValues = issueFilters?.filters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters(workspaceSlug, EFilterType.FILTERS, { - [key]: newValues, - }); + updateFilters( + workspaceSlug.toString(), + undefined, + EIssueFilterType.FILTERS, + { + [key]: newValues, + }, + userId.toString() + ); }; const handleClearAllFilters = () => { - if (!workspaceSlug) return; + if (!workspaceSlug || !userId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, EFilterType.FILTERS, { ...newFilters }); + updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.FILTERS, { ...newFilters }, userId.toString()); }; // return if no filters are applied @@ -65,7 +71,6 @@ export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={workspaceLabels ?? []} - members={projectMembers?.map((m) => m.member)} states={[]} />
diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx index 31317366c..98ecf50b4 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx @@ -1,14 +1,15 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useLabel, useProjectState, useUser } from "hooks/store"; +import { useIssues } from "hooks/store/use-issues"; // components import { AppliedFiltersList, SaveFilterView } from "components/issues"; -// types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +// types +import { IIssueFilterOptions } from "@plane/types"; export const ProjectAppliedFiltersRoot: React.FC = observer(() => { // router @@ -17,18 +18,20 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { workspaceSlug: string; projectId: string; }; - // mobx stores + // store hooks const { - projectLabel: { projectLabels }, - projectState: projectStateStore, - projectMember: { projectMembers }, - projectIssuesFilter: { issueFilters, updateFilters }, - user: { currentProjectRole }, - } = useMobxStore(); + project: { projectLabels }, + } = useLabel(); + const { + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.PROJECT); + const { + membership: { currentProjectRole }, + } = useUser(); + const { projectStates } = useProjectState(); // derived values - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const userFilters = issueFilters?.filters; - // filters whose value not null or empty array const appliedFilters: IIssueFilterOptions = {}; Object.entries(userFilters ?? {}).forEach(([key, value]) => { @@ -40,7 +43,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { if (!workspaceSlug || !projectId) return; if (!value) { - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { [key]: null, }); return; @@ -49,7 +52,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { let newValues = issueFilters?.filters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues, }); }; @@ -60,7 +63,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { ...newFilters }); + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters }); }; // return if no filters are applied @@ -73,8 +76,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectLabels ?? []} - members={projectMembers?.map((m) => m.member)} - states={projectStateStore.states?.[projectId?.toString() ?? ""]} + states={projectStates} /> {isEditingAllowed && ( diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx index 6b037a031..ffbae9ac8 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx @@ -1,7 +1,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useLabel, useProjectState, useProjectView } from "hooks/store"; // components import { AppliedFiltersList } from "components/issues"; // ui @@ -9,27 +9,28 @@ import { Button } from "@plane/ui"; // helpers import { areFiltersDifferent } from "helpers/filter.helper"; // types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { IIssueFilterOptions } from "@plane/types"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, projectId, viewId } = router.query as { workspaceSlug: string; projectId: string; viewId: string; }; - + // store hooks const { - projectLabel: { projectLabels }, - projectState: projectStateStore, - projectMember: { projectMembers }, - projectViews: projectViewsStore, - viewIssuesFilter: { issueFilters, updateFilters }, - } = useMobxStore(); - - const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined; - + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.PROJECT_VIEW); + const { + project: { projectLabels }, + } = useLabel(); + const { projectStates } = useProjectState(); + const { getViewById, updateView } = useProjectView(); + // derived values + const viewDetails = viewId ? getViewById(viewId.toString()) : null; const userFilters = issueFilters?.filters; // filters whose value not null or empty array const appliedFilters: IIssueFilterOptions = {}; @@ -42,30 +43,18 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { if (!workspaceSlug || !projectId) return; if (!value) { - updateFilters( - workspaceSlug, - projectId, - EFilterType.FILTERS, - { - [key]: null, - }, - viewId - ); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { + [key]: null, + }); return; } let newValues = issueFilters?.filters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters( - workspaceSlug, - projectId, - EFilterType.FILTERS, - { - [key]: newValues, - }, - viewId - ); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { + [key]: newValues, + }); }; const handleClearAllFilters = () => { @@ -74,7 +63,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { ...newFilters }, viewId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { ...newFilters }, viewId); }; // return if no filters are applied @@ -83,7 +72,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { const handleUpdateView = () => { if (!workspaceSlug || !projectId || !viewId || !viewDetails) return; - projectViewsStore.updateView(workspaceSlug.toString(), projectId.toString(), viewId.toString(), { + updateView(workspaceSlug.toString(), projectId.toString(), viewId.toString(), { query_data: { ...viewDetails.query_data, ...(appliedFilters ?? {}), @@ -98,8 +87,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectLabels ?? []} - members={projectMembers?.map((m) => m.member)} - states={projectStateStore.states?.[projectId?.toString() ?? ""]} + states={projectStates} /> {appliedFilters && diff --git a/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx b/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx index 64f95983e..620a8f781 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite"; // icons import { StateGroupIcon } from "@plane/ui"; import { X } from "lucide-react"; -import { TStateGroups } from "types"; +import { TStateGroups } from "@plane/types"; type Props = { handleRemove: (val: string) => void; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/state.tsx b/web/components/issues/issue-layouts/filters/applied-filters/state.tsx index 9cff84d9b..59a873162 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/state.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/state.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; import { StateGroupIcon } from "@plane/ui"; import { X } from "lucide-react"; // types -import { IState } from "types"; +import { IState } from "@plane/types"; type Props = { handleRemove: (val: string) => void; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx index 412e54794..3c94b4f3f 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx @@ -11,7 +11,7 @@ import { FilterSubGroupBy, } from "components/issues"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "types"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; import { ILayoutDisplayFiltersOptions } from "constants/issue"; type Props = { diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx index 0abe6442a..3ea1453e8 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader } from "../helpers/filter-header"; // types -import { IIssueDisplayProperties } from "types"; +import { IIssueDisplayProperties } from "@plane/types"; // constants import { ISSUE_DISPLAY_PROPERTIES } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx index cb75b53f4..0feb1d891 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; // components import { FilterOption } from "components/issues"; // types -import { IIssueDisplayFilterOptions, TIssueExtraOptions } from "types"; +import { IIssueDisplayFilterOptions, TIssueExtraOptions } from "@plane/types"; // constants import { ISSUE_EXTRA_OPTIONS } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx index aa057e417..659d86d08 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types -import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "types"; +import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "@plane/types"; // constants import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx index a6fa2bf06..59c83a200 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types -import { TIssueTypeFilters } from "types"; +import { TIssueTypeFilters } from "@plane/types"; // constants import { ISSUE_FILTER_OPTIONS } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx index 004d1b6e9..e417c650e 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types -import { TIssueOrderByOptions } from "types"; +import { TIssueOrderByOptions } from "@plane/types"; // constants import { ISSUE_ORDER_BY_OPTIONS } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx index f66422427..331051161 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types -import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "types"; +import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "@plane/types"; // constants import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx b/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx index 0a1ecf3ea..168e31bc0 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx @@ -1,28 +1,31 @@ -import React, { useState } from "react"; +import { useState } from "react"; +import { observer } from "mobx-react-lite"; +// hooks +import { useMember } from "hooks/store"; // components import { FilterHeader, FilterOption } from "components/issues"; // ui import { Avatar, Loader } from "@plane/ui"; -// types -import { IUserLite } from "types"; type Props = { appliedFilters: string[] | null; handleUpdate: (val: string) => void; - members: IUserLite[] | undefined; + memberIds: string[] | undefined; searchQuery: string; }; -export const FilterAssignees: React.FC = (props) => { - const { appliedFilters, handleUpdate, members, searchQuery } = props; - +export const FilterAssignees: React.FC = observer((props: Props) => { + const { appliedFilters, handleUpdate, memberIds, searchQuery } = props; + // states const [itemsToRender, setItemsToRender] = useState(5); const [previewEnabled, setPreviewEnabled] = useState(true); + // store hooks + const { getUserDetails } = useMember(); const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = members?.filter((member) => - member.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredOptions = memberIds?.filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) ); const handleViewToggle = () => { @@ -44,15 +47,20 @@ export const FilterAssignees: React.FC = (props) => { {filteredOptions ? ( filteredOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((member) => ( - handleUpdate(member.id)} - icon={} - title={member.display_name} - /> - ))} + {filteredOptions.slice(0, itemsToRender).map((memberId) => { + const member = getUserDetails(memberId); + + if (!member) return null; + return ( + handleUpdate(member.id)} + icon={} + title={member.display_name} + /> + ); + })} {filteredOptions.length > 5 && (
@@ -109,7 +108,7 @@ export const FilterSelection: React.FC = observer((props) => { handleFiltersUpdate("mentions", val)} - members={members} + memberIds={memberIds} searchQuery={filtersSearchQuery} />
@@ -121,7 +120,7 @@ export const FilterSelection: React.FC = observer((props) => { handleFiltersUpdate("created_by", val)} - members={members} + memberIds={memberIds} searchQuery={filtersSearchQuery} />
@@ -144,7 +143,6 @@ export const FilterSelection: React.FC = observer((props) => {
handleFiltersUpdate("project", val)} searchQuery={filtersSearchQuery} /> diff --git a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx index de6b73596..3e23ae07b 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx @@ -5,7 +5,7 @@ import { FilterHeader, FilterOption } from "components/issues"; // ui import { Loader } from "@plane/ui"; // types -import { IIssueLabel } from "types"; +import { IIssueLabel } from "@plane/types"; const LabelIcons = ({ color }: { color: string }) => ( diff --git a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx index 8e2f4b402..a6af9833a 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx @@ -1,28 +1,31 @@ -import React, { useState } from "react"; +import { useState } from "react"; +import { observer } from "mobx-react-lite"; +// hooks +import { useMember } from "hooks/store"; // components import { FilterHeader, FilterOption } from "components/issues"; // ui import { Loader, Avatar } from "@plane/ui"; -// types -import { IUserLite } from "types"; type Props = { appliedFilters: string[] | null; handleUpdate: (val: string) => void; - members: IUserLite[] | undefined; + memberIds: string[] | undefined; searchQuery: string; }; -export const FilterMentions: React.FC = (props) => { - const { appliedFilters, handleUpdate, members, searchQuery } = props; - +export const FilterMentions: React.FC = observer((props: Props) => { + const { appliedFilters, handleUpdate, memberIds, searchQuery } = props; + // states const [itemsToRender, setItemsToRender] = useState(5); const [previewEnabled, setPreviewEnabled] = useState(true); + // store hooks + const { getUserDetails } = useMember(); const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = members?.filter((member) => - member.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredOptions = memberIds?.filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) ); const handleViewToggle = () => { @@ -44,15 +47,20 @@ export const FilterMentions: React.FC = (props) => { {filteredOptions ? ( filteredOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((member) => ( - handleUpdate(member.id)} - icon={} - title={member.display_name} - /> - ))} + {filteredOptions.slice(0, itemsToRender).map((memberId) => { + const member = getUserDetails(memberId); + + if (!member) return null; + return ( + handleUpdate(member.id)} + icon={} + title={member.display_name} + /> + ); + })} {filteredOptions.length > 5 && ( +
); -}; +}); diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index 7270ae06d..729cd6c68 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -2,41 +2,39 @@ import { FC } from "react"; // components import { IssueBlock } from "components/issues"; // types -import { IIssue, IIssueDisplayProperties } from "types"; -import { IIssueResponse, IGroupedIssues, TUnGroupedIssues } from "store/issues/types"; +import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types"; import { EIssueActions } from "../types"; interface Props { - columnId: string; - issueIds: IGroupedIssues | TUnGroupedIssues | any; - issues: IIssueResponse; + issueIds: TGroupedIssues | TUnGroupedIssues | any; + issuesMap: TIssueMap; canEditProperties: (projectId: string | undefined) => boolean; - handleIssues: (issue: IIssue, action: EIssueActions) => void; - quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; + handleIssues: (issue: TIssue, action: EIssueActions) => void; + quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; } export const IssueBlocksList: FC = (props) => { - const { columnId, issueIds, issues, handleIssues, quickActions, displayProperties, canEditProperties } = props; + const { issueIds, issuesMap, handleIssues, quickActions, displayProperties, canEditProperties } = props; return (
{issueIds && issueIds.length > 0 ? ( - issueIds.map( - (issueId: string) => - issueId != undefined && - issues[issueId] && ( - - ) - ) + issueIds.map((issueId: string) => { + if (!issueId) return null; + + return ( + + ); + }) ) : (
No issues
)} diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index 24781bb41..9bf7cfc78 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -1,25 +1,29 @@ -import React from "react"; // components -import { ListGroupByHeaderRoot } from "./headers/group-by-root"; import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues"; +// hooks +import { useLabel, useMember, useProject, useProjectState } from "hooks/store"; // types -import { IIssue, IIssueDisplayProperties, IIssueLabel, IProject, IState, IUserLite } from "types"; -import { IIssueResponse, IGroupedIssues, TUnGroupedIssues } from "store/issues/types"; +import { + GroupByColumnTypes, + TGroupedIssues, + TIssue, + IIssueDisplayProperties, + TIssueMap, + TUnGroupedIssues, +} from "@plane/types"; import { EIssueActions } from "../types"; // constants -import { getValueFromObject } from "constants/issue"; -import { EProjectStore } from "store/command-palette.store"; +import { HeaderGroupByCard } from "./headers/group-by-card"; +import { getGroupByColumns } from "../utils"; +import { TCreateModalStoreTypes } from "constants/issue"; export interface IGroupByList { - issueIds: IGroupedIssues | TUnGroupedIssues | any; - issues: any; + issueIds: TGroupedIssues | TUnGroupedIssues | any; + issuesMap: TIssueMap; group_by: string | null; - list: any; - listKey: string; - states: IState[] | null; is_list?: boolean; - handleIssues: (issue: IIssue, action: EIssueActions) => Promise; - quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; + handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; enableIssueQuickAdd: boolean; showEmptyGroup?: boolean; @@ -27,24 +31,21 @@ export interface IGroupByList { quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; + currentStore: TCreateModalStoreTypes; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; } const GroupByList: React.FC = (props) => { const { issueIds, - issues, + issuesMap, group_by, - list, - listKey, is_list = false, - states, handleIssues, quickActions, displayProperties, @@ -57,17 +58,26 @@ const GroupByList: React.FC = (props) => { currentStore, addIssuesToView, } = props; + // store hooks + const member = useMember(); + const project = useProject(); + const projectLabel = useLabel(); + const projectState = useProjectState(); + + const list = getGroupByColumns(group_by as GroupByColumnTypes, project, projectLabel, projectState, member, true); + + if (!list) return null; const prePopulateQuickAddData = (groupByKey: string | null, value: any) => { - const defaultState = states?.find((state) => state.default); - if (groupByKey === null) return { state: defaultState?.id }; + const defaultState = projectState.projectStates?.find((state) => state.default); + if (groupByKey === null) return { state_id: defaultState?.id }; else { if (groupByKey === "state") return { state: groupByKey === "state" ? value : defaultState?.id }; - else return { state: defaultState?.id, [groupByKey]: value }; + else return { state_id: defaultState?.id, [groupByKey]: value }; } }; - const validateEmptyIssueGroups = (issues: IIssue[]) => { + const validateEmptyIssueGroups = (issues: TIssue[]) => { const issuesCount = issues?.length || 0; if (!showEmptyGroup && issuesCount <= 0) return false; return true; @@ -79,29 +89,24 @@ const GroupByList: React.FC = (props) => { list.length > 0 && list.map( (_list: any) => - validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[getValueFromObject(_list, listKey) as string]) && ( -
+ validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[_list.id]) && ( +
-
- {issues && ( + {issueIds && ( = (props) => { {enableIssueQuickAdd && !disableIssueCreation && (
@@ -126,37 +131,31 @@ const GroupByList: React.FC = (props) => { }; export interface IList { - issueIds: IGroupedIssues | TUnGroupedIssues | any; - issues: IIssueResponse | undefined; + issueIds: TGroupedIssues | TUnGroupedIssues | any; + issuesMap: TIssueMap; group_by: string | null; - handleIssues: (issue: IIssue, action: EIssueActions) => Promise; - quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; + handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; showEmptyGroup: boolean; enableIssueQuickAdd: boolean; canEditProperties: (projectId: string | undefined) => boolean; - states: IState[] | null; - labels: IIssueLabel[] | null; - members: IUserLite[] | null; - projects: IProject[] | null; - stateGroups: any; - priorities: any; quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; viewId?: string; disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; + currentStore: TCreateModalStoreTypes; + addIssuesToView?: (issueIds: string[]) => Promise; } export const List: React.FC = (props) => { const { issueIds, - issues, + issuesMap, group_by, handleIssues, quickActions, @@ -167,194 +166,28 @@ export const List: React.FC = (props) => { enableIssueQuickAdd, canEditProperties, disableIssueCreation, - states, - stateGroups, - priorities, - labels, - members, - projects, currentStore, addIssuesToView, } = props; return (
- {group_by === null && ( - - )} - - {group_by && group_by === "project" && projects && ( - - )} - - {group_by && group_by === "state" && states && ( - - )} - - {group_by && group_by === "state_detail.group" && stateGroups && ( - - )} - - {group_by && group_by === "priority" && priorities && ( - - )} - - {group_by && group_by === "labels" && labels && ( - - )} - - {group_by && group_by === "assignees" && members && ( - - )} - - {group_by && group_by === "created_by" && members && ( - - )} +
); }; diff --git a/web/components/issues/issue-layouts/list/headers/assignee.tsx b/web/components/issues/issue-layouts/list/headers/assignee.tsx deleted file mode 100644 index d129774aa..000000000 --- a/web/components/issues/issue-layouts/list/headers/assignee.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -// ui -import { Avatar } from "@plane/ui"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface IAssigneesHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -export const Icon = ({ user }: any) => ; - -export const AssigneesHeader: FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - const assignee = column_value ?? null; - - return ( - <> - {assignee && ( - } - title={assignee?.display_name || ""} - count={issues_count} - issuePayload={{ assignees: [assignee?.member?.id] }} - disableIssueCreation={disableIssueCreation} - currentStore={currentStore} - addIssuesToView={addIssuesToView} - /> - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/created-by.tsx b/web/components/issues/issue-layouts/list/headers/created-by.tsx deleted file mode 100644 index 77306998b..000000000 --- a/web/components/issues/issue-layouts/list/headers/created-by.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -import { Icon } from "./assignee"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface ICreatedByHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -export const CreatedByHeader: FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - const createdBy = column_value ?? null; - - return ( - <> - {createdBy && ( - } - title={createdBy?.display_name || ""} - count={issues_count} - issuePayload={{ created_by: createdBy?.member?.id }} - disableIssueCreation={disableIssueCreation} - currentStore={currentStore} - addIssuesToView={addIssuesToView} - /> - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/empty-group.tsx b/web/components/issues/issue-layouts/list/headers/empty-group.tsx deleted file mode 100644 index c7b16fe26..000000000 --- a/web/components/issues/issue-layouts/list/headers/empty-group.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { observer } from "mobx-react-lite"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface IEmptyHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -export const EmptyHeader: React.FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - return ( - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx index c703ea66b..35c7f77cf 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { useRouter } from "next/router"; // lucide icons import { CircleDashed, Plus } from "lucide-react"; @@ -10,18 +9,19 @@ import { CustomMenu } from "@plane/ui"; // mobx import { observer } from "mobx-react-lite"; // types -import { IIssue, ISearchIssueResponse } from "types"; -import { EProjectStore } from "store/command-palette.store"; +import { TIssue, ISearchIssueResponse } from "@plane/types"; import useToast from "hooks/use-toast"; +import { useState } from "react"; +import { TCreateModalStoreTypes } from "constants/issue"; interface IHeaderGroupByCard { icon?: React.ReactNode; title: string; count: number; - issuePayload: Partial; + issuePayload: Partial; disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; + currentStore: TCreateModalStoreTypes; + addIssuesToView?: (issueIds: string[]) => Promise; } export const HeaderGroupByCard = observer( @@ -29,9 +29,9 @@ export const HeaderGroupByCard = observer( const router = useRouter(); const { workspaceSlug, projectId, moduleId, cycleId } = router.query; - const [isOpen, setIsOpen] = React.useState(false); + const [isOpen, setIsOpen] = useState(false); - const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false); + const [openExistingIssueListModal, setOpenExistingIssueListModal] = useState(false); const isDraftIssue = router.pathname.includes("draft-issue"); @@ -45,14 +45,15 @@ export const HeaderGroupByCard = observer( const issues = data.map((i) => i.id); - addIssuesToView && - addIssuesToView(issues)?.catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Selected issues could not be added to the cycle. Please try again.", - }); + try { + addIssuesToView && addIssuesToView(issues); + } catch (error) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Selected issues could not be added to the cycle. Please try again.", }); + } }; return ( diff --git a/web/components/issues/issue-layouts/list/headers/group-by-root.tsx b/web/components/issues/issue-layouts/list/headers/group-by-root.tsx deleted file mode 100644 index 50ed9ad98..000000000 --- a/web/components/issues/issue-layouts/list/headers/group-by-root.tsx +++ /dev/null @@ -1,114 +0,0 @@ -// components -import { EmptyHeader } from "./empty-group"; -import { ProjectHeader } from "./project"; -import { StateHeader } from "./state"; -import { StateGroupHeader } from "./state-group"; -import { AssigneesHeader } from "./assignee"; -import { PriorityHeader } from "./priority"; -import { LabelHeader } from "./label"; -import { CreatedByHeader } from "./created-by"; -// mobx -import { observer } from "mobx-react-lite"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface IListGroupByHeaderRoot { - column_id: string; - column_value: any; - group_by: string | null; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -export const ListGroupByHeaderRoot: React.FC = observer((props) => { - const { column_id, column_value, group_by, issues_count, disableIssueCreation, currentStore, addIssuesToView } = - props; - - return ( - <> - {!group_by && group_by === null && ( - - )} - {group_by && group_by === "project" && ( - - )} - - {group_by && group_by === "state" && ( - - )} - {group_by && group_by === "state_detail.group" && ( - - )} - {group_by && group_by === "priority" && ( - - )} - {group_by && group_by === "labels" && ( - - )} - {group_by && group_by === "assignees" && ( - - )} - {group_by && group_by === "created_by" && ( - - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/label.tsx b/web/components/issues/issue-layouts/list/headers/label.tsx deleted file mode 100644 index b4d740e37..000000000 --- a/web/components/issues/issue-layouts/list/headers/label.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface ILabelHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -const Icon = ({ color }: any) => ( -
-); - -export const LabelHeader: FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - const label = column_value ?? null; - - return ( - <> - {column_value && ( - } - title={column_value?.name || ""} - count={issues_count} - issuePayload={{ labels: [label.id] }} - disableIssueCreation={disableIssueCreation} - currentStore={currentStore} - addIssuesToView={addIssuesToView} - /> - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/priority.tsx b/web/components/issues/issue-layouts/list/headers/priority.tsx deleted file mode 100644 index 5eb19fbfd..000000000 --- a/web/components/issues/issue-layouts/list/headers/priority.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -import { AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface IPriorityHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -const Icon = ({ priority }: any) => ( -
- {priority === "urgent" ? ( -
- -
- ) : priority === "high" ? ( -
- -
- ) : priority === "medium" ? ( -
- -
- ) : priority === "low" ? ( -
- -
- ) : ( -
- -
- )} -
-); - -export const PriorityHeader: FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - const priority = column_value ?? null; - - return ( - <> - {priority && ( - } - title={priority?.title || ""} - count={issues_count} - issuePayload={{ priority: priority?.key }} - disableIssueCreation={disableIssueCreation} - currentStore={currentStore} - addIssuesToView={addIssuesToView} - /> - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/project.tsx b/web/components/issues/issue-layouts/list/headers/project.tsx deleted file mode 100644 index 7578214b2..000000000 --- a/web/components/issues/issue-layouts/list/headers/project.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -// emoji helper -import { renderEmoji } from "helpers/emoji.helper"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface IProjectHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -const Icon = ({ emoji }: any) =>
{renderEmoji(emoji)}
; - -export const ProjectHeader: FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - const project = column_value ?? null; - - return ( - <> - {project && ( - } - title={project?.name || ""} - count={issues_count} - issuePayload={{ project: project?.id ?? "" }} - disableIssueCreation={disableIssueCreation} - currentStore={currentStore} - addIssuesToView={addIssuesToView} - /> - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/state-group.tsx b/web/components/issues/issue-layouts/list/headers/state-group.tsx deleted file mode 100644 index 421a1da8f..000000000 --- a/web/components/issues/issue-layouts/list/headers/state-group.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -// ui -import { StateGroupIcon } from "@plane/ui"; -// helpers -import { capitalizeFirstLetter } from "helpers/string.helper"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface IStateGroupHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -export const Icon = ({ stateGroup, color }: { stateGroup: any; color?: any }) => ( -
- -
-); - -export const StateGroupHeader: FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - const stateGroup = column_value ?? null; - - return ( - <> - {stateGroup && ( - } - title={capitalizeFirstLetter(stateGroup?.key) || ""} - count={issues_count} - issuePayload={{}} - disableIssueCreation={disableIssueCreation} - currentStore={currentStore} - addIssuesToView={addIssuesToView} - /> - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/state.tsx b/web/components/issues/issue-layouts/list/headers/state.tsx deleted file mode 100644 index 926743464..000000000 --- a/web/components/issues/issue-layouts/list/headers/state.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -import { Icon } from "./state-group"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface IStateHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -export const StateHeader: FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - const state = column_value ?? null; - - return ( - <> - {state && ( - } - title={state?.name || ""} - count={issues_count} - issuePayload={{ state: state?.id }} - disableIssueCreation={disableIssueCreation} - currentStore={currentStore} - addIssuesToView={addIssuesToView} - /> - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/list-view-types.d.ts b/web/components/issues/issue-layouts/list/list-view-types.d.ts index efdd79cfc..674ae92d1 100644 --- a/web/components/issues/issue-layouts/list/list-view-types.d.ts +++ b/web/components/issues/issue-layouts/list/list-view-types.d.ts @@ -1,7 +1,7 @@ export interface IQuickActionProps { - issue: IIssue; + issue: TIssue; handleDelete: () => Promise; - handleUpdate?: (data: IIssue) => Promise; + handleUpdate?: (data: TIssue) => Promise; handleRemoveFromView?: () => Promise; customActionButton?: React.ReactElement; } diff --git a/web/components/issues/issue-layouts/list/properties.tsx b/web/components/issues/issue-layouts/list/properties.tsx deleted file mode 100644 index 07129910f..000000000 --- a/web/components/issues/issue-layouts/list/properties.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -import { Layers, Link, Paperclip } from "lucide-react"; -// components -import { IssuePropertyState } from "../properties/state"; -import { IssuePropertyPriority } from "../properties/priority"; -import { IssuePropertyLabels } from "../properties/labels"; -import { IssuePropertyAssignee } from "../properties/assignee"; -import { IssuePropertyEstimates } from "../properties/estimates"; -import { IssuePropertyDate } from "../properties/date"; -// ui -import { Tooltip } from "@plane/ui"; -// types -import { IIssue, IIssueDisplayProperties, IState, TIssuePriorities } from "types"; - -export interface IListProperties { - columnId: string; - issue: IIssue; - handleIssues: (group_by: string | null, issue: IIssue) => void; - displayProperties: IIssueDisplayProperties | undefined; - isReadonly?: boolean; -} - -export const ListProperties: FC = observer((props) => { - const { columnId: group_id, issue, handleIssues, displayProperties, isReadonly } = props; - - const handleState = (state: IState) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, state: state.id }); - }; - - const handlePriority = (value: TIssuePriorities) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, priority: value }); - }; - - const handleLabel = (ids: string[]) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, labels: ids }); - }; - - const handleAssignee = (ids: string[]) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, assignees: ids }); - }; - - const handleStartDate = (date: string | null) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, start_date: date }); - }; - - const handleTargetDate = (date: string | null) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, target_date: date }); - }; - - const handleEstimate = (value: number | null) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, estimate_point: value }); - }; - - return ( -
- {/* basic properties */} - {/* state */} - {displayProperties && displayProperties?.state && ( - - )} - - {/* priority */} - {displayProperties && displayProperties?.priority && ( - - )} - - {/* label */} - {displayProperties && displayProperties?.labels && ( - - )} - - {/* assignee */} - {displayProperties && displayProperties?.assignee && ( - - )} - - {/* start date */} - {displayProperties && displayProperties?.start_date && ( - handleStartDate(date)} - disabled={isReadonly} - type="start_date" - /> - )} - - {/* target/due date */} - {displayProperties && displayProperties?.due_date && ( - handleTargetDate(date)} - disabled={isReadonly} - type="target_date" - /> - )} - - {/* estimates */} - {displayProperties && displayProperties?.estimate && ( - - )} - - {/* extra render properties */} - {/* sub-issues */} - {displayProperties && displayProperties?.sub_issue_count && !!issue?.sub_issues_count && ( - -
- -
{issue.sub_issues_count}
-
-
- )} - - {/* attachments */} - {displayProperties && displayProperties?.attachment_count && !!issue?.attachment_count && ( - -
- -
{issue.attachment_count}
-
-
- )} - - {/* link */} - {displayProperties && displayProperties?.link && !!issue?.link_count && ( - -
- -
{issue.link_count}
-
-
- )} -
- ); -}); diff --git a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx index 9237d8a1f..b6e39606e 100644 --- a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx @@ -4,13 +4,12 @@ import { useForm } from "react-hook-form"; import { PlusIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; // hooks +import { useProject, useWorkspace } from "hooks/store"; import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; // constants -import { IIssue, IProject } from "types"; +import { TIssue, IProject } from "@plane/types"; // types import { createIssuePayload } from "helpers/issue.helper"; @@ -44,31 +43,28 @@ const Inputs: FC = (props) => { }; interface IListQuickAddIssueForm { - prePopulatedData?: Partial; + prePopulatedData?: Partial; quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; viewId?: string; } -const defaultValues: Partial = { +const defaultValues: Partial = { name: "", }; export const ListQuickAddIssueForm: FC = observer((props) => { const { prePopulatedData, quickAddCallback, viewId } = props; - + // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; - - const { workspace: workspaceStore, project: projectStore } = useMobxStore(); - - const workspaceDetail = (workspaceSlug && workspaceStore.getWorkspaceBySlug(workspaceSlug)) || null; - const projectDetail: IProject | null = - (workspaceSlug && projectId && projectStore.getProjectById(workspaceSlug, projectId)) || null; + const { workspaceSlug, projectId } = router.query; + // store hooks + const { currentWorkspace } = useWorkspace(); + const { currentProjectDetails } = useProject(); const ref = useRef(null); @@ -85,24 +81,25 @@ export const ListQuickAddIssueForm: FC = observer((props setFocus, register, formState: { errors, isSubmitting }, - } = useForm({ defaultValues }); + } = useForm({ defaultValues }); useEffect(() => { if (!isOpen) reset({ ...defaultValues }); }, [isOpen, reset]); - const onSubmitHandler = async (formData: IIssue) => { - if (isSubmitting || !workspaceDetail || !projectDetail) return; + const onSubmitHandler = async (formData: TIssue) => { + if (isSubmitting || !currentWorkspace || !currentProjectDetails || !workspaceSlug || !projectId) return; reset({ ...defaultValues }); - const payload = createIssuePayload(workspaceDetail, projectDetail, { + const payload = createIssuePayload(currentWorkspace, currentProjectDetails, { ...(prePopulatedData ?? {}), ...formData, }); try { - quickAddCallback && (await quickAddCallback(workspaceSlug, projectId, { ...payload }, viewId)); + quickAddCallback && + (await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId)); setToastAlert({ type: "success", title: "Success!", @@ -130,7 +127,12 @@ export const ListQuickAddIssueForm: FC = observer((props onSubmit={handleSubmit(onSubmitHandler)} className="flex w-full items-center gap-x-3 border-[0.5px] border-t-0 border-custom-border-100 bg-custom-background-100 px-3" > - +
{`Press 'Enter' to add another issue`}
diff --git a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx index cf4c74063..388699dc7 100644 --- a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx @@ -1,46 +1,40 @@ -import { FC } from "react"; +import { FC, useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import { useMobxStore } from "lib/mobx/store-provider"; +import { useIssues } from "hooks/store"; // components import { ArchivedIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; // constants import { BaseListRoot } from "../base-list-root"; -import { IProjectStore } from "store/project"; import { EIssueActions } from "../../types"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export const ArchivedIssueListLayout: FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; - const { projectArchivedIssues: archivedIssueStore, projectArchivedIssuesFilter: archivedIssueFiltersStore } = - useMobxStore(); + const { issues, issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); + const issueActions = useMemo( + () => ({ + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - const issueActions = { - [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; - - await archivedIssueStore.removeIssue(workspaceSlug, projectId, issue.id); - }, - }; - - const getProjects = (projectStore: IProjectStore) => { - if (!workspaceSlug) return null; - return projectStore?.projects[workspaceSlug.toString()] || null; - }; + await issues.removeIssue(workspaceSlug, projectId, issue.id); + }, + }), + [issues, workspaceSlug, projectId] + ); return ( ); }); diff --git a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx index de579473b..c1db51411 100644 --- a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -1,65 +1,58 @@ -import React from "react"; +import React, { useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues } from "hooks/store"; // components import { CycleIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; // constants import { BaseListRoot } from "../base-list-root"; -import { IProjectStore } from "store/project"; import { EIssueActions } from "../../types"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export interface ICycleListLayout {} export const CycleListLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string }; + const { workspaceSlug, projectId, cycleId } = router.query; // store - const { - cycleIssues: cycleIssueStore, - cycleIssuesFilter: cycleIssueFilterStore, - cycle: { fetchCycleWithId }, - } = useMobxStore(); + const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); - const issueActions = { - [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !cycleId) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId) return; - await cycleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, cycleId); - fetchCycleWithId(workspaceSlug, issue.project, cycleId); - }, - [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !cycleId) return; + await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId) return; - await cycleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, cycleId); - fetchCycleWithId(workspaceSlug, issue.project, cycleId); - }, - [EIssueActions.REMOVE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !cycleId || !issue.bridge_id) return; + await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); + }, + [EIssueActions.REMOVE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId) return; - await cycleIssueStore.removeIssueFromCycle(workspaceSlug, issue.project, cycleId, issue.id, issue.bridge_id); - fetchCycleWithId(workspaceSlug, issue.project, cycleId); - }, - }; - const getProjects = (projectStore: IProjectStore) => { - if (!workspaceSlug) return null; - return projectStore?.projects[workspaceSlug] || null; - }; + await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); + }, + }), + [issues, workspaceSlug, cycleId] + ); return ( cycleIssueStore.addIssueToCycle(workspaceSlug, cycleId, issues)} + viewId={cycleId?.toString()} + currentStore={EIssuesStoreType.CYCLE} + addIssuesToView={(issueIds: string[]) => { + if (!workspaceSlug || !projectId || !cycleId) throw new Error(); + return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds); + }} /> ); }); diff --git a/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx index 6049ec3bc..ef1edc831 100644 --- a/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx @@ -1,17 +1,16 @@ -import { FC } from "react"; +import { FC, useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import { useMobxStore } from "lib/mobx/store-provider"; +import { useIssues } from "hooks/store"; // components import { ProjectIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { IProjectStore } from "store/project"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export const DraftIssueListLayout: FC = observer(() => { const router = useRouter(); @@ -20,31 +19,31 @@ export const DraftIssueListLayout: FC = observer(() => { if (!workspaceSlug || !projectId) return null; // store - const { projectDraftIssuesFilter: projectIssuesFilterStore, projectDraftIssues: projectIssuesStore } = useMobxStore(); + const { issues, issuesFilter } = useIssues(EIssuesStoreType.DRAFT); - const issueActions = { - [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - await projectIssuesStore.updateIssue(workspaceSlug, projectId, issue.id, issue); - }, - [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; + await issues.updateIssue(workspaceSlug, projectId, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - await projectIssuesStore.removeIssue(workspaceSlug, projectId, issue.id); - }, - }; - - const getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects; + await issues.removeIssue(workspaceSlug, projectId, issue.id); + }, + }), + [issues, workspaceSlug, projectId] + ); return ( ); }); diff --git a/web/components/issues/issue-layouts/list/roots/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx index 5d076a0cc..947cfe55b 100644 --- a/web/components/issues/issue-layouts/list/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -1,66 +1,58 @@ -import React from "react"; +import React, { useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { useIssues } from "hooks/store"; // components import { ModuleIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { IProjectStore } from "store/project"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export interface IModuleListLayout {} export const ModuleListLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string }; + const { workspaceSlug, projectId, moduleId } = router.query; - const { - moduleIssues: moduleIssueStore, - moduleIssuesFilter: moduleIssueFilterStore, - module: { fetchModuleDetails }, - } = useMobxStore(); + const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); - const issueActions = { - [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !moduleId) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; - await moduleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, moduleId); - fetchModuleDetails(workspaceSlug, issue.project, moduleId); - }, - [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !moduleId) return; + await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; - await moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId); - fetchModuleDetails(workspaceSlug, issue.project, moduleId); - }, - [EIssueActions.REMOVE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !moduleId || !issue.bridge_id) return; + await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); + }, + [EIssueActions.REMOVE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; - await moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, moduleId, issue.id, issue.bridge_id); - fetchModuleDetails(workspaceSlug, issue.project, moduleId); - }, - }; - - const getProjects = (projectStore: IProjectStore) => { - if (!workspaceSlug) return null; - return projectStore?.projects[workspaceSlug] || null; - }; + await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id); + }, + }), + [issues, workspaceSlug, moduleId] + ); return ( moduleIssueStore.addIssueToModule(workspaceSlug, moduleId, issues)} + viewId={moduleId?.toString()} + currentStore={EIssuesStoreType.MODULE} + addIssuesToView={(issueIds: string[]) => { + if (!workspaceSlug || !projectId || !moduleId) throw new Error(); + return issues.addIssueToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds); + }} /> ); }); diff --git a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx index eedf7ae81..55db4cd71 100644 --- a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx @@ -1,64 +1,58 @@ -import { FC } from "react"; +import { FC, useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import { useMobxStore } from "lib/mobx/store-provider"; +import { useIssues, useUser } from "hooks/store"; // components import { ProjectIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { IProjectStore } from "store/project"; -import { EProjectStore } from "store/command-palette.store"; -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EUserProjectRoles } from "constants/project"; +import { EIssuesStoreType } from "constants/issue"; export const ProfileIssuesListLayout: FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, userId } = router.query as { workspaceSlug: string; userId: string }; + // store hooks + const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROFILE); - // store const { - workspaceProfileIssuesFilter: profileIssueFiltersStore, - workspaceProfileIssues: profileIssuesStore, - workspaceMember: { currentWorkspaceUserProjectsRole }, - } = useMobxStore(); + membership: { currentWorkspaceAllProjectsRole }, + } = useUser(); - const issueActions = { - [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !userId) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !userId) return; - await profileIssuesStore.updateIssue(workspaceSlug, userId, issue.id, issue); - }, - [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !userId) return; + await issues.updateIssue(workspaceSlug, userId, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !userId) return; - await profileIssuesStore.removeIssue(workspaceSlug, issue.project, issue.id, userId); - }, - }; - - const getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects; + await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, userId); + }, + }), + [issues, workspaceSlug, userId] + ); const canEditPropertiesBasedOnProject = (projectId: string) => { - const currentProjectRole = currentWorkspaceUserProjectsRole && currentWorkspaceUserProjectsRole[projectId]; + const currentProjectRole = currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId]; - console.log( - projectId, - currentWorkspaceUserProjectsRole, - !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER - ); - return !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + return !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; }; return ( ); diff --git a/web/components/issues/issue-layouts/list/roots/project-root.tsx b/web/components/issues/issue-layouts/list/roots/project-root.tsx index 0d23f7656..b99b431c8 100644 --- a/web/components/issues/issue-layouts/list/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-root.tsx @@ -1,17 +1,16 @@ -import { FC } from "react"; +import { FC, useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import { useMobxStore } from "lib/mobx/store-provider"; +import { useIssues } from "hooks/store"; // components import { ProjectIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { IProjectStore } from "store/project"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export const ListLayout: FC = observer(() => { const router = useRouter(); @@ -20,31 +19,32 @@ export const ListLayout: FC = observer(() => { if (!workspaceSlug || !projectId) return null; // store - const { projectIssuesFilter: projectIssuesFilterStore, projectIssues: projectIssuesStore } = useMobxStore(); + const { issuesFilter, issues } = useIssues(EIssuesStoreType.PROJECT); - const issueActions = { - [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - await projectIssuesStore.updateIssue(workspaceSlug, projectId, issue.id, issue); - }, - [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; + await issues.updateIssue(workspaceSlug, projectId, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - await projectIssuesStore.removeIssue(workspaceSlug, projectId, issue.id); - }, - }; - - const getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects; + await issues.removeIssue(workspaceSlug, projectId, issue.id); + }, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [issues] + ); return ( ); }); diff --git a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx index 52fa1a759..8139307e6 100644 --- a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx @@ -1,53 +1,50 @@ -import React from "react"; +import React, { useMemo } from "react"; import { observer } from "mobx-react-lite"; - // store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; +import { useIssues } from "hooks/store"; // constants import { useRouter } from "next/router"; import { EIssueActions } from "../../types"; -import { IProjectStore } from "store/project"; -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; // components import { BaseListRoot } from "../base-list-root"; import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export interface IViewListLayout {} export const ProjectViewListLayout: React.FC = observer(() => { - const { viewIssues: projectViewIssueStore, viewIssuesFilter: projectViewIssueFilterStore }: RootStore = - useMobxStore(); + // store + const { issuesFilter, issues } = useIssues(EIssuesStoreType.PROJECT_VIEW); const router = useRouter(); const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; if (!workspaceSlug || !projectId) return null; - const issueActions = { - [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - await projectViewIssueStore.updateIssue(workspaceSlug, projectId, issue.id, issue); - }, - [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; + await issues.updateIssue(workspaceSlug, projectId, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - await projectViewIssueStore.removeIssue(workspaceSlug, projectId, issue.id); - }, - }; - - const getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects; + await issues.removeIssue(workspaceSlug, projectId, issue.id); + }, + }), + [issues, workspaceSlug, projectId] + ); return ( ); }); diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx new file mode 100644 index 000000000..fe05d834b --- /dev/null +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -0,0 +1,207 @@ +import { observer } from "mobx-react-lite"; +import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react"; +// hooks +import { useLabel } from "hooks/store"; +// components +import { IssuePropertyLabels } from "../properties/labels"; +import { Tooltip } from "@plane/ui"; +import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; +import { + DateDropdown, + EstimateDropdown, + PriorityDropdown, + ProjectMemberDropdown, + StateDropdown, +} from "components/dropdowns"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +// types +import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; + +export interface IIssueProperties { + issue: TIssue; + handleIssues: (issue: TIssue) => void; + displayProperties: IIssueDisplayProperties | undefined; + isReadOnly: boolean; + className: string; +} + +export const IssueProperties: React.FC = observer((props) => { + const { issue, handleIssues, displayProperties, isReadOnly, className } = props; + const { labelMap } = useLabel(); + + const handleState = (stateId: string) => { + handleIssues({ ...issue, state_id: stateId }); + }; + + const handlePriority = (value: TIssuePriorities) => { + handleIssues({ ...issue, priority: value }); + }; + + const handleLabel = (ids: string[]) => { + handleIssues({ ...issue, label_ids: ids }); + }; + + const handleAssignee = (ids: string[]) => { + handleIssues({ ...issue, assignee_ids: ids }); + }; + + const handleStartDate = (date: Date | null) => { + handleIssues({ ...issue, start_date: date ? renderFormattedPayloadDate(date) : null }); + }; + + const handleTargetDate = (date: Date | null) => { + handleIssues({ ...issue, target_date: date ? renderFormattedPayloadDate(date) : null }); + }; + + const handleEstimate = (value: number | null) => { + handleIssues({ ...issue, estimate_point: value }); + }; + + if (!displayProperties) return null; + + const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || []; + + return ( +
+ {/* basic properties */} + {/* state */} + +
+ +
+
+ + {/* priority */} + +
+ +
+
+ + {/* label */} + + + + + + {/* start date */} + +
+ } + placeholder="Start date" + buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"} + disabled={isReadOnly} + /> +
+
+ + {/* target/due date */} + +
+ } + placeholder="Due date" + buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} + disabled={isReadOnly} + /> +
+
+ + {/* assignee */} + +
+ 0 ? "transparent-without-text" : "border-without-text"} + buttonClassName={issue.assignee_ids.length > 0 ? "hover:bg-transparent px-0" : ""} + /> +
+
+ + {/* estimates */} + +
+ +
+
+ + {/* extra render properties */} + {/* sub-issues */} + + +
+ +
{issue.sub_issues_count}
+
+
+
+ + {/* attachments */} + + +
+ +
{issue.attachment_count}
+
+
+
+ + {/* link */} + + +
+ +
{issue.link_count}
+
+
+
+
+ ); +}); diff --git a/web/components/issues/issue-layouts/properties/assignee.tsx b/web/components/issues/issue-layouts/properties/assignee.tsx deleted file mode 100644 index 01dec9b83..000000000 --- a/web/components/issues/issue-layouts/properties/assignee.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import { Fragment, useState } from "react"; -import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; -import { usePopper } from "react-popper"; -import { Combobox } from "@headlessui/react"; -import { Check, ChevronDown, CircleUser, Search } from "lucide-react"; -// ui -import { Avatar, AvatarGroup, Tooltip } from "@plane/ui"; -// types -import { Placement } from "@popperjs/core"; -import { IProjectMember } from "types"; - -export interface IIssuePropertyAssignee { - projectId: string | null; - value: string[] | string; - defaultOptions?: any; - onChange: (data: string[]) => void; - disabled?: boolean; - hideDropdownArrow?: boolean; - className?: string; - buttonClassName?: string; - optionsClassName?: string; - placement?: Placement; - multiple?: true; - noLabelBorder?: boolean; -} - -export const IssuePropertyAssignee: React.FC = observer((props) => { - const { - projectId, - value, - defaultOptions = [], - onChange, - disabled = false, - hideDropdownArrow = false, - className, - buttonClassName, - optionsClassName, - placement, - multiple = false, - } = props; - // store - const { - workspace: workspaceStore, - projectMember: { members: _members, fetchProjectMembers }, - } = useMobxStore(); - const workspaceSlug = workspaceStore?.workspaceSlug; - // states - const [query, setQuery] = useState(""); - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - const getProjectMembers = () => { - setIsLoading(true); - if (workspaceSlug && projectId) fetchProjectMembers(workspaceSlug, projectId).then(() => setIsLoading(false)); - }; - - const updatedDefaultOptions: IProjectMember[] = - defaultOptions.map((member: any) => ({ member: { ...member } })) ?? []; - const projectMembers = projectId && _members[projectId] ? _members[projectId] : updatedDefaultOptions; - - const options = projectMembers?.map((member) => ({ - value: member.member.id, - query: member.member.display_name, - content: ( -
- - {member.member.display_name} -
- ), - })); - - const filteredOptions = - query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); - - const getTooltipContent = (): string => { - if (!value || value.length === 0) return "No Assignee"; - - // if multiple assignees - if (Array.isArray(value)) { - const assignees = projectMembers?.filter((m) => value.includes(m.member.id)); - - if (!assignees || assignees.length === 0) return "No Assignee"; - - // if only one assignee in list - if (assignees.length === 1) { - return "1 assignee"; - } else return `${assignees.length} assignees`; - } - - // if single assignee - const assignee = projectMembers?.find((m) => m.member.id === value)?.member; - - if (!assignee) return "No Assignee"; - - // if assignee not null & not list - return "1 assignee"; - }; - - const label = ( - -
- {value && value.length > 0 && Array.isArray(value) ? ( - - {value.map((assigneeId) => { - const member = projectMembers?.find((m) => m.member.id === assigneeId)?.member; - if (!member) return null; - return ; - })} - - ) : ( - - - - )} -
-
- ); - - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "bottom-start", - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 12, - }, - }, - ], - }); - - const comboboxProps: any = { value, onChange, disabled }; - if (multiple) comboboxProps.multiple = true; - - return ( - - - - - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {isLoading ? ( -

Loading...

- ) : filteredOptions && filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${ - active && !selected ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - onClick={(e) => e.stopPropagation()} - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) - ) : ( - -

No matching results

-
- )} -
-
-
-
- ); -}); diff --git a/web/components/issues/issue-layouts/properties/date.tsx b/web/components/issues/issue-layouts/properties/date.tsx deleted file mode 100644 index b66d2e5b6..000000000 --- a/web/components/issues/issue-layouts/properties/date.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import React from "react"; -// headless ui -import { Popover } from "@headlessui/react"; -// lucide icons -import { CalendarCheck2, CalendarClock, X } from "lucide-react"; -// react date picker -import DatePicker from "react-datepicker"; -// mobx -import { observer } from "mobx-react-lite"; -// components -import { Tooltip } from "@plane/ui"; -// hooks -import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; -// helpers -import { renderFormattedPayloadDate, renderFormattedDate } from "helpers/date-time.helper"; - -export interface IIssuePropertyDate { - value: string | null; - onChange: (date: string | null) => void; - disabled?: boolean; - type: "start_date" | "target_date"; -} - -const DATE_OPTIONS = { - start_date: { - key: "start_date", - placeholder: "Start date", - icon: CalendarClock, - }, - target_date: { - key: "target_date", - placeholder: "Target date", - icon: CalendarCheck2, - }, -}; - -export const IssuePropertyDate: React.FC = observer((props) => { - const { value, onChange, disabled, type } = props; - - const dropdownBtn = React.useRef(null); - const dropdownOptions = React.useRef(null); - - const [isOpen, setIsOpen] = React.useState(false); - - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); - - const dateOptionDetails = DATE_OPTIONS[type]; - - return ( - - {({ open }) => { - if (open) { - if (!isOpen) setIsOpen(true); - } else if (isOpen) setIsOpen(false); - - return ( - <> - e.stopPropagation()} - disabled={disabled} - > - -
-
- - {value && ( - <> -
{value}
-
{ - if (onChange) onChange(null); - }} - > - -
- - )} -
-
-
-
- -
- - {({ close }) => ( - { - e?.stopPropagation(); - if (onChange && val) { - onChange(renderFormattedPayloadDate(val)); - close(); - } - }} - dateFormat="dd-MM-yyyy" - calendarClassName="h-full" - inline - /> - )} - -
- - ); - }} -
- ); -}); diff --git a/web/components/issues/issue-layouts/properties/estimates.tsx b/web/components/issues/issue-layouts/properties/estimates.tsx deleted file mode 100644 index e3f617958..000000000 --- a/web/components/issues/issue-layouts/properties/estimates.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { Fragment, useState } from "react"; -import { usePopper } from "react-popper"; -import { observer } from "mobx-react-lite"; -import { Combobox } from "@headlessui/react"; -import { Check, ChevronDown, Search, Triangle } from "lucide-react"; -// ui -import { Tooltip } from "@plane/ui"; -// types -import { Placement } from "@popperjs/core"; -import { useMobxStore } from "lib/mobx/store-provider"; - -export interface IIssuePropertyEstimates { - view?: "profile" | "workspace" | "project"; - projectId: string | null; - value: number | null; - onChange: (value: number | null) => void; - disabled?: boolean; - hideDropdownArrow?: boolean; - className?: string; - buttonClassName?: string; - optionsClassName?: string; - placement?: Placement; -} - -export const IssuePropertyEstimates: React.FC = observer((props) => { - const { - projectId, - value, - onChange, - disabled, - hideDropdownArrow = false, - className = "", - buttonClassName = "", - optionsClassName = "", - placement, - } = props; - - const [query, setQuery] = useState(""); - - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "bottom-start", - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 12, - }, - }, - ], - }); - - const { - project: { project_details }, - projectEstimates: { projectEstimates }, - } = useMobxStore(); - - const projectDetails = projectId ? project_details[projectId] : null; - const isEstimateEnabled = projectDetails?.estimate !== null; - const estimates = projectEstimates; - const estimatePoints = - projectDetails && isEstimateEnabled ? estimates?.find((e) => e.id === projectDetails.estimate)?.points : null; - - const options: { value: number | null; query: string; content: any }[] | undefined = (estimatePoints ?? []).map( - (estimate) => ({ - value: estimate.key, - query: estimate.value, - content: ( -
- - {estimate.value} -
- ), - }) - ); - options?.unshift({ - value: null, - query: "none", - content: ( -
- - None -
- ), - }); - - const filteredOptions = - query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); - - const selectedEstimate = estimatePoints?.find((e) => e.key === value); - const label = ( - -
- - {selectedEstimate?.value ?? "None"} -
-
- ); - - if (!isEstimateEnabled) return null; - - return ( - onChange(val as number | null)} - disabled={disabled} - > - - - - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - onClick={(e) => e.stopPropagation()} - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) - ) : ( - -

No matching results

-
- ) - ) : ( -

Loading...

- )} -
-
-
-
- ); -}); diff --git a/web/components/issues/issue-layouts/properties/index.ts b/web/components/issues/issue-layouts/properties/index.ts new file mode 100644 index 000000000..95f3ce21f --- /dev/null +++ b/web/components/issues/issue-layouts/properties/index.ts @@ -0,0 +1 @@ +export * from "./labels"; diff --git a/web/components/issues/issue-layouts/properties/index.tsx b/web/components/issues/issue-layouts/properties/index.tsx deleted file mode 100644 index 3e2e2acd6..000000000 --- a/web/components/issues/issue-layouts/properties/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./assignee"; -export * from "./date"; -export * from "./estimates"; -export * from "./labels"; -export * from "./priority"; -export * from "./state"; diff --git a/web/components/issues/issue-layouts/properties/labels.tsx b/web/components/issues/issue-layouts/properties/labels.tsx index d0045c3d4..b22083c07 100644 --- a/web/components/issues/issue-layouts/properties/labels.tsx +++ b/web/components/issues/issue-layouts/properties/labels.tsx @@ -1,16 +1,15 @@ import { Fragment, useState } from "react"; import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; -// hooks import { usePopper } from "react-popper"; +import { Check, ChevronDown, Search, Tags } from "lucide-react"; +// hooks +import { useApplication, useLabel } from "hooks/store"; // components import { Combobox } from "@headlessui/react"; import { Tooltip } from "@plane/ui"; -import { Check, ChevronDown, Search, Tags } from "lucide-react"; // types import { Placement } from "@popperjs/core"; -import { RootStore } from "store/root"; -import { IIssueLabel } from "types"; +import { IIssueLabel } from "@plane/types"; export interface IIssuePropertyLabels { projectId: string | null; @@ -44,18 +43,19 @@ export const IssuePropertyLabels: React.FC = observer((pro noLabelBorder = false, placeholderText, } = props; - - const { - workspace: workspaceStore, - projectLabel: { fetchProjectLabels, labels }, - }: RootStore = useMobxStore(); - const workspaceSlug = workspaceStore?.workspaceSlug; - + // states const [query, setQuery] = useState(""); - + // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); const [isLoading, setIsLoading] = useState(false); + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { + project: { fetchProjectLabels, projectLabels: storeLabels }, + } = useLabel(); const fetchLabels = () => { setIsLoading(true); @@ -65,7 +65,6 @@ export const IssuePropertyLabels: React.FC = observer((pro if (!value) return null; let projectLabels: IIssueLabel[] = defaultOptions; - const storeLabels = projectId && labels ? labels[projectId] : []; if (storeLabels && storeLabels.length > 0) projectLabels = storeLabels; const options = projectLabels.map((label) => ({ @@ -107,7 +106,7 @@ export const IssuePropertyLabels: React.FC = observer((pro {projectLabels ?.filter((l) => value.includes(l.id)) .map((label) => ( - +
= observer((pro ? "cursor-pointer" : "cursor-pointer hover:bg-custom-background-80" } ${buttonClassName}`} - onClick={(e) => { - e.stopPropagation(); - !storeLabels && fetchLabels(); - }} + onClick={() => !storeLabels && fetchLabels()} > {label} {!hideDropdownArrow && !disabled &&