From 17f83d6458625b236f8b44b4be4d06d7b9f67621 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 5 Jun 2024 20:08:03 +0530 Subject: [PATCH] [WEB-1516] refactor: space app routing and layouts (#4705) * dev: change layout * chore: replace workspace slug and project id with anchor * chore: migration fixes * chore: update filtering logic * chore: endpoint changes * chore: update endpoint * chore: changed url pratterns * chore: use client side for layout and page * chore: issue vote changes * chore: project deploy board response change * refactor: publish project store and components * fix: update layout options after fetching settings * chore: remove unnecessary types * style: peek overview * refactor: components folder structure * fix: redirect from old path * chore: make the whole issue block clickable * chore: removed the migration file * chore: add server side redirection for old routes * chore: is enabled key change * chore: update types * chore: removed the migration file --------- Co-authored-by: NarayanBavisetti --- apiserver/plane/app/urls/project.py | 2 +- apiserver/plane/app/views/project/base.py | 28 +- .../db/migrations/0067_issue_estimate.py | 260 ------------------ apiserver/plane/db/models/__init__.py | 1 + apiserver/plane/space/urls/inbox.py | 6 +- apiserver/plane/space/urls/issue.py | 14 +- apiserver/plane/space/urls/project.py | 10 +- apiserver/plane/space/views/__init__.py | 1 + apiserver/plane/space/views/inbox.py | 54 ++-- apiserver/plane/space/views/issue.py | 151 +++++----- apiserver/plane/space/views/project.py | 26 +- space/app/[workspaceSlug]/[projectId]/page.ts | 42 +++ .../[workspace_slug]/[project_id]/page.tsx | 16 -- space/app/error.tsx | 53 ++-- .../[anchor]}/layout.tsx | 42 ++- space/app/issues/[anchor]/page.tsx | 30 ++ space/app/not-found.tsx | 26 +- space/components/account/user-logged-in.tsx | 30 +- space/components/common/index.ts | 1 - .../common/latest-feature-block.tsx | 40 --- space/components/instance/index.ts | 1 - space/components/instance/not-ready-view.tsx | 62 ----- .../issues/board-views/block-downvotes.tsx | 10 - .../issues/board-views/block-due-date.tsx | 59 ---- .../issues/board-views/block-labels.tsx | 19 -- .../issues/board-views/block-state.tsx | 18 -- .../issues/board-views/block-upvotes.tsx | 8 - .../issues/board-views/calendar/index.tsx | 1 - .../issues/board-views/gantt/index.tsx | 1 - .../issues/board-views/kanban/block.tsx | 82 ------ .../issues/board-views/kanban/header.tsx | 28 -- .../issues/board-views/kanban/index.tsx | 58 ---- .../issues/board-views/list/index.tsx | 42 --- .../issues/board-views/spreadsheet/index.tsx | 1 - .../filters/applied-filters/filters-list.tsx | 6 +- .../issues/filters/applied-filters/root.tsx | 32 +-- .../issues/filters/applied-filters/state.tsx | 7 +- space/components/issues/filters/root.tsx | 21 +- space/components/issues/filters/selection.tsx | 5 +- space/components/issues/filters/state.tsx | 7 +- space/components/issues/index.ts | 2 + .../components/issues/issue-layouts/index.ts | 4 + .../issues/issue-layouts/kanban/block.tsx | 78 ++++++ .../issues/issue-layouts/kanban/header.tsx | 25 ++ .../issues/issue-layouts/kanban/index.ts | 3 + .../issues/issue-layouts/kanban/root.tsx | 50 ++++ .../list/block.tsx | 44 ++- .../list/header.tsx | 20 +- .../issues/issue-layouts/list/index.ts | 3 + .../issues/issue-layouts/list/root.tsx | 40 +++ .../issue-layouts/properties/due-date.tsx | 32 +++ .../issues/issue-layouts/properties/index.ts | 4 + .../issue-layouts/properties/labels.tsx | 17 ++ .../properties/priority.tsx} | 6 +- .../issues/issue-layouts/properties/state.tsx | 11 + .../issue-layouts/root.tsx} | 67 ++--- space/components/issues/navbar/controls.tsx | 48 ++-- space/components/issues/navbar/index.ts | 5 + .../issues/navbar/issue-board-view.tsx | 72 ----- .../issues/navbar/layout-selection.tsx | 67 +++++ .../issues/navbar/{index.tsx => root.tsx} | 29 +- .../peek-overview/comment/add-comment.tsx | 20 +- .../comment/comment-detail-card.tsx | 24 +- .../comment/comment-reactions.tsx | 13 +- .../peek-overview/full-screen-peek-view.tsx | 13 +- .../issues/peek-overview/header.tsx | 46 ++-- .../issues/peek-overview/issue-activity.tsx | 69 +++-- .../issues/peek-overview/issue-details.tsx | 47 ++-- .../peek-overview/issue-emoji-reactions.tsx | 25 +- .../issues/peek-overview/issue-properties.tsx | 70 +++-- .../issues/peek-overview/issue-reaction.tsx | 32 +-- .../peek-overview/issue-vote-reactions.tsx | 33 +-- .../issues/peek-overview/layout.tsx | 38 +-- .../issues/peek-overview/side-peek-view.tsx | 23 +- space/components/ui/dropdown.tsx | 142 ---------- space/components/ui/index.ts | 1 - space/components/views/index.ts | 1 - space/constants/issue.ts | 92 ++----- space/constants/state.ts | 37 +++ space/constants/workspace.ts | 12 - space/helpers/date-time.helper.ts | 54 ++-- space/helpers/emoji.helper.tsx | 20 -- space/helpers/issue.helper.ts | 30 ++ space/helpers/string.helper.ts | 4 +- space/hooks/store/index.ts | 2 +- space/hooks/store/publish/index.ts | 2 + space/hooks/store/publish/use-publish-list.ts | 11 + space/hooks/store/publish/use-publish.ts | 11 + space/hooks/store/use-project.ts | 11 - space/hooks/use-mention.tsx | 4 +- space/lib/user-provider.tsx | 12 - space/package.json | 1 + space/services/file.service.ts | 58 ---- space/services/issue.service.ts | 96 ++----- space/services/project-member.service.ts | 8 +- space/services/project.service.ts | 19 -- space/services/publish.service.ts | 29 ++ space/store/issue-detail.store.ts | 257 ++++++++--------- space/store/issue-filters.store.ts | 77 +++--- space/store/issue.store.ts | 112 ++++---- space/store/project.store.ts | 96 ------- space/store/publish/publish.store.ts | 114 ++++++++ space/store/publish/publish_list.store.ts | 54 ++++ space/store/root.store.ts | 8 +- space/styles/globals.css | 17 ++ space/types/app.d.ts | 14 - space/types/issue.d.ts | 80 +----- space/types/project.d.ts | 24 -- space/types/publish.d.ts | 24 ++ space/types/theme.d.ts | 4 - space/types/user.d.ts | 30 -- .../project/publish-project/modal.tsx | 137 +++++---- .../project/project-publish.service.ts | 28 +- web/store/project/project-publish.store.ts | 162 +++++------ yarn.lock | 43 +-- 115 files changed, 1821 insertions(+), 2498 deletions(-) delete mode 100644 apiserver/plane/db/migrations/0067_issue_estimate.py create mode 100644 space/app/[workspaceSlug]/[projectId]/page.ts delete mode 100644 space/app/[workspace_slug]/[project_id]/page.tsx rename space/app/{[workspace_slug]/[project_id] => issues/[anchor]}/layout.tsx (54%) create mode 100644 space/app/issues/[anchor]/page.tsx delete mode 100644 space/components/common/latest-feature-block.tsx delete mode 100644 space/components/instance/not-ready-view.tsx delete mode 100644 space/components/issues/board-views/block-downvotes.tsx delete mode 100644 space/components/issues/board-views/block-due-date.tsx delete mode 100644 space/components/issues/board-views/block-labels.tsx delete mode 100644 space/components/issues/board-views/block-state.tsx delete mode 100644 space/components/issues/board-views/block-upvotes.tsx delete mode 100644 space/components/issues/board-views/calendar/index.tsx delete mode 100644 space/components/issues/board-views/gantt/index.tsx delete mode 100644 space/components/issues/board-views/kanban/block.tsx delete mode 100644 space/components/issues/board-views/kanban/header.tsx delete mode 100644 space/components/issues/board-views/kanban/index.tsx delete mode 100644 space/components/issues/board-views/list/index.tsx delete mode 100644 space/components/issues/board-views/spreadsheet/index.tsx create mode 100644 space/components/issues/index.ts create mode 100644 space/components/issues/issue-layouts/index.ts create mode 100644 space/components/issues/issue-layouts/kanban/block.tsx create mode 100644 space/components/issues/issue-layouts/kanban/header.tsx create mode 100644 space/components/issues/issue-layouts/kanban/index.ts create mode 100644 space/components/issues/issue-layouts/kanban/root.tsx rename space/components/issues/{board-views => issue-layouts}/list/block.tsx (64%) rename space/components/issues/{board-views => issue-layouts}/list/header.tsx (54%) create mode 100644 space/components/issues/issue-layouts/list/index.ts create mode 100644 space/components/issues/issue-layouts/list/root.tsx create mode 100644 space/components/issues/issue-layouts/properties/due-date.tsx create mode 100644 space/components/issues/issue-layouts/properties/index.ts create mode 100644 space/components/issues/issue-layouts/properties/labels.tsx rename space/components/issues/{board-views/block-priority.tsx => issue-layouts/properties/priority.tsx} (85%) create mode 100644 space/components/issues/issue-layouts/properties/state.tsx rename space/components/{views/project-details.tsx => issues/issue-layouts/root.tsx} (52%) create mode 100644 space/components/issues/navbar/index.ts delete mode 100644 space/components/issues/navbar/issue-board-view.tsx create mode 100644 space/components/issues/navbar/layout-selection.tsx rename space/components/issues/navbar/{index.tsx => root.tsx} (58%) delete mode 100644 space/components/ui/dropdown.tsx create mode 100644 space/constants/state.ts delete mode 100644 space/constants/workspace.ts create mode 100644 space/helpers/issue.helper.ts create mode 100644 space/hooks/store/publish/index.ts create mode 100644 space/hooks/store/publish/use-publish-list.ts create mode 100644 space/hooks/store/publish/use-publish.ts delete mode 100644 space/hooks/store/use-project.ts delete mode 100644 space/lib/user-provider.tsx delete mode 100644 space/services/project.service.ts create mode 100644 space/services/publish.service.ts delete mode 100644 space/store/project.store.ts create mode 100644 space/store/publish/publish.store.ts create mode 100644 space/store/publish/publish_list.store.ts delete mode 100644 space/types/app.d.ts create mode 100644 space/types/publish.d.ts delete mode 100644 space/types/theme.d.ts delete mode 100644 space/types/user.d.ts diff --git a/apiserver/plane/app/urls/project.py b/apiserver/plane/app/urls/project.py index d9c6f0f81..0807c7616 100644 --- a/apiserver/plane/app/urls/project.py +++ b/apiserver/plane/app/urls/project.py @@ -2,6 +2,7 @@ from django.urls import path from plane.app.views import ( ProjectViewSet, + DeployBoardViewSet, ProjectInvitationsViewset, ProjectMemberViewSet, ProjectMemberUserEndpoint, @@ -12,7 +13,6 @@ from plane.app.views import ( ProjectFavoritesViewSet, UserProjectInvitationsViewset, ProjectPublicCoverImagesEndpoint, - DeployBoardViewSet, UserProjectRolesEndpoint, ProjectArchiveUnarchiveEndpoint, ) diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index a62d6d6dd..f6f8e951c 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -646,22 +646,21 @@ class DeployBoardViewSet(BaseViewSet): serializer_class = DeployBoardSerializer model = DeployBoard - def get_queryset(self): - return ( - super() - .get_queryset() - .filter( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - .select_related("project") - ) + def list(self, request, slug, project_id): + project_deploy_board = DeployBoard.objects.filter( + entity_name="project", + entity_identifier=project_id, + workspace__slug=slug, + ).first() + + serializer = DeployBoardSerializer(project_deploy_board) + return Response(serializer.data, status=status.HTTP_200_OK) def create(self, request, slug, project_id): - comments = request.data.get("comments", False) - reactions = request.data.get("reactions", False) + comments = request.data.get("is_comments_enabled", False) + reactions = request.data.get("is_reactions_enabled", False) inbox = request.data.get("inbox", None) - votes = request.data.get("votes", False) + votes = request.data.get("is_votes_enabled", False) views = request.data.get( "views", { @@ -674,7 +673,8 @@ class DeployBoardViewSet(BaseViewSet): ) project_deploy_board, _ = DeployBoard.objects.get_or_create( - anchor=f"{slug}/{project_id}", + entity_name="project", + entity_identifier=project_id, project_id=project_id, ) project_deploy_board.inbox = inbox diff --git a/apiserver/plane/db/migrations/0067_issue_estimate.py b/apiserver/plane/db/migrations/0067_issue_estimate.py deleted file mode 100644 index b341f9864..000000000 --- a/apiserver/plane/db/migrations/0067_issue_estimate.py +++ /dev/null @@ -1,260 +0,0 @@ -# # Generated by Django 4.2.7 on 2024-05-24 09:47 -# Python imports -import uuid -from uuid import uuid4 -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models -import plane.db.models.deploy_board - - -def issue_estimate_point(apps, schema_editor): - Issue = apps.get_model("db", "Issue") - Project = apps.get_model("db", "Project") - EstimatePoint = apps.get_model("db", "EstimatePoint") - IssueActivity = apps.get_model("db", "IssueActivity") - updated_estimate_point = [] - updated_issue_activity = [] - - # loop through all the projects - for project in Project.objects.filter(estimate__isnull=False): - estimate_points = EstimatePoint.objects.filter( - estimate=project.estimate, project=project - ) - - for issue_activity in IssueActivity.objects.filter( - field="estimate_point", project=project - ): - if issue_activity.new_value: - new_identifier = estimate_points.filter( - key=issue_activity.new_value - ).first().id - issue_activity.new_identifier = new_identifier - new_value = estimate_points.filter( - key=issue_activity.new_value - ).first().value - issue_activity.new_value = new_value - - if issue_activity.old_value: - old_identifier = estimate_points.filter( - key=issue_activity.old_value - ).first().id - issue_activity.old_identifier = old_identifier - old_value = estimate_points.filter( - key=issue_activity.old_value - ).first().value - issue_activity.old_value = old_value - updated_issue_activity.append(issue_activity) - - for issue in Issue.objects.filter( - point__isnull=False, project=project - ): - # get the estimate id for the corresponding estimate point in the issue - estimate = estimate_points.filter(key=issue.point).first() - issue.estimate_point = estimate - updated_estimate_point.append(issue) - - Issue.objects.bulk_update( - updated_estimate_point, ["estimate_point"], batch_size=1000 - ) - IssueActivity.objects.bulk_update( - updated_issue_activity, - ["new_value", "old_value", "new_identifier", "old_identifier"], - batch_size=1000, - ) - - -def last_used_estimate(apps, schema_editor): - Project = apps.get_model("db", "Project") - Estimate = apps.get_model("db", "Estimate") - - # Get all estimate ids used in projects - estimate_ids = Project.objects.filter(estimate__isnull=False).values_list( - "estimate", flat=True - ) - - # Update all matching estimates - Estimate.objects.filter(id__in=estimate_ids).update(last_used=True) - - -def populate_deploy_board(apps, schema_editor): - DeployBoard = apps.get_model("db", "DeployBoard") - ProjectDeployBoard = apps.get_model("db", "ProjectDeployBoard") - - DeployBoard.objects.bulk_create( - [ - DeployBoard( - entity_identifier=deploy_board.project_id, - project_id=deploy_board.project_id, - entity_name="project", - anchor=uuid4().hex, - is_comments_enabled=deploy_board.comments, - is_reactions_enabled=deploy_board.reactions, - inbox=deploy_board.inbox, - is_votes_enabled=deploy_board.votes, - view_props=deploy_board.views, - workspace_id=deploy_board.workspace_id, - created_at=deploy_board.created_at, - updated_at=deploy_board.updated_at, - created_by_id=deploy_board.created_by_id, - updated_by_id=deploy_board.updated_by_id, - ) - for deploy_board in ProjectDeployBoard.objects.all() - ], - batch_size=100, - ) - - -class Migration(migrations.Migration): - - dependencies = [ - ("db", "0066_account_id_token_cycle_logo_props_module_logo_props"), - ] - - operations = [ - migrations.CreateModel( - name="DeployBoard", - 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, - ), - ), - ("entity_identifier", models.UUIDField(null=True)), - ( - "entity_name", - models.CharField( - choices=[ - ("project", "Project"), - ("issue", "Issue"), - ("module", "Module"), - ("cycle", "Task"), - ("page", "Page"), - ("view", "View"), - ], - max_length=30, - ), - ), - ( - "anchor", - models.CharField( - db_index=True, - default=plane.db.models.deploy_board.get_anchor, - max_length=255, - unique=True, - ), - ), - ("is_comments_enabled", models.BooleanField(default=False)), - ("is_reactions_enabled", models.BooleanField(default=False)), - ("is_votes_enabled", models.BooleanField(default=False)), - ("view_props", models.JSONField(default=dict)), - ( - "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", - ), - ), - ( - "inbox", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="board_inbox", - to="db.inbox", - ), - ), - ( - "project", - models.ForeignKey( - null=True, - 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", - ), - ), - ( - "workspace", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="workspace_%(class)s", - to="db.workspace", - ), - ), - ], - options={ - "verbose_name": "Deploy Board", - "verbose_name_plural": "Deploy Boards", - "db_table": "deploy_boards", - "ordering": ("-created_at",), - "unique_together": {("entity_name", "entity_identifier")}, - }, - ), - migrations.AddField( - model_name="estimate", - name="last_used", - field=models.BooleanField(default=False), - ), - # Rename the existing field - migrations.RenameField( - model_name="issue", - old_name="estimate_point", - new_name="point", - ), - # Add a new field with the original name as a foreign key - migrations.AddField( - model_name="issue", - name="estimate_point", - field=models.ForeignKey( - on_delete=django.db.models.deletion.SET_NULL, - related_name="issue_estimates", - to="db.EstimatePoint", - blank=True, - null=True, - ), - ), - migrations.AlterField( - model_name="estimate", - name="type", - field=models.CharField(default="categories", max_length=255), - ), - migrations.AlterField( - model_name="estimatepoint", - name="value", - field=models.CharField(max_length=255), - ), - migrations.RunPython(issue_estimate_point), - migrations.RunPython(last_used_estimate), - migrations.RunPython(populate_deploy_board), - ] diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 36718d515..51b0e70e5 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -60,6 +60,7 @@ from .project import ( ProjectMemberInvite, ProjectPublicMember, ) +from .deploy_board import DeployBoard from .session import Session from .social_connection import SocialLoginConnection from .state import State diff --git a/apiserver/plane/space/urls/inbox.py b/apiserver/plane/space/urls/inbox.py index 60de040e2..20ebb3437 100644 --- a/apiserver/plane/space/urls/inbox.py +++ b/apiserver/plane/space/urls/inbox.py @@ -10,7 +10,7 @@ from plane.space.views import ( urlpatterns = [ path( - "workspaces//project-boards//inboxes//inbox-issues/", + "anchor//inboxes//inbox-issues/", InboxIssuePublicViewSet.as_view( { "get": "list", @@ -20,7 +20,7 @@ urlpatterns = [ name="inbox-issue", ), path( - "workspaces//project-boards//inboxes//inbox-issues//", + "anchor//inboxes//inbox-issues//", InboxIssuePublicViewSet.as_view( { "get": "retrieve", @@ -31,7 +31,7 @@ urlpatterns = [ name="inbox-issue", ), path( - "workspaces//project-boards//issues//votes/", + "anchor//issues//votes/", IssueVotePublicViewSet.as_view( { "get": "list", diff --git a/apiserver/plane/space/urls/issue.py b/apiserver/plane/space/urls/issue.py index 099eace5d..61c19ba01 100644 --- a/apiserver/plane/space/urls/issue.py +++ b/apiserver/plane/space/urls/issue.py @@ -10,12 +10,12 @@ from plane.space.views import ( urlpatterns = [ path( - "workspaces//project-boards//issues//", + "anchor//issues//", IssueRetrievePublicEndpoint.as_view(), name="workspace-project-boards", ), path( - "workspaces//project-boards//issues//comments/", + "anchor//issues//comments/", IssueCommentPublicViewSet.as_view( { "get": "list", @@ -25,7 +25,7 @@ urlpatterns = [ name="issue-comments-project-board", ), path( - "workspaces//project-boards//issues//comments//", + "anchor//issues//comments//", IssueCommentPublicViewSet.as_view( { "get": "retrieve", @@ -36,7 +36,7 @@ urlpatterns = [ name="issue-comments-project-board", ), path( - "workspaces//project-boards//issues//reactions/", + "anchor//issues//reactions/", IssueReactionPublicViewSet.as_view( { "get": "list", @@ -46,7 +46,7 @@ urlpatterns = [ name="issue-reactions-project-board", ), path( - "workspaces//project-boards//issues//reactions//", + "anchor//issues//reactions//", IssueReactionPublicViewSet.as_view( { "delete": "destroy", @@ -55,7 +55,7 @@ urlpatterns = [ name="issue-reactions-project-board", ), path( - "workspaces//project-boards//comments//reactions/", + "anchor//comments//reactions/", CommentReactionPublicViewSet.as_view( { "get": "list", @@ -65,7 +65,7 @@ urlpatterns = [ name="comment-reactions-project-board", ), path( - "workspaces//project-boards//comments//reactions//", + "anchor//comments//reactions//", CommentReactionPublicViewSet.as_view( { "delete": "destroy", diff --git a/apiserver/plane/space/urls/project.py b/apiserver/plane/space/urls/project.py index dc97b43a7..3294b01f6 100644 --- a/apiserver/plane/space/urls/project.py +++ b/apiserver/plane/space/urls/project.py @@ -4,17 +4,23 @@ from django.urls import path from plane.space.views import ( ProjectDeployBoardPublicSettingsEndpoint, ProjectIssuesPublicEndpoint, + WorkspaceProjectAnchorEndpoint, ) urlpatterns = [ path( - "workspaces//project-boards//settings/", + "anchor//settings/", ProjectDeployBoardPublicSettingsEndpoint.as_view(), name="project-deploy-board-settings", ), path( - "workspaces//project-boards//issues/", + "anchor//issues/", ProjectIssuesPublicEndpoint.as_view(), name="project-deploy-board", ), + path( + "workspaces//projects//anchor/", + WorkspaceProjectAnchorEndpoint.as_view(), + name="project-deploy-board", + ), ] diff --git a/apiserver/plane/space/views/__init__.py b/apiserver/plane/space/views/__init__.py index 5130e04d5..eced7d1b4 100644 --- a/apiserver/plane/space/views/__init__.py +++ b/apiserver/plane/space/views/__init__.py @@ -1,6 +1,7 @@ from .project import ( ProjectDeployBoardPublicSettingsEndpoint, WorkspaceProjectDeployBoardEndpoint, + WorkspaceProjectAnchorEndpoint, ) from .issue import ( diff --git a/apiserver/plane/space/views/inbox.py b/apiserver/plane/space/views/inbox.py index d15e7aa39..b89c77672 100644 --- a/apiserver/plane/space/views/inbox.py +++ b/apiserver/plane/space/views/inbox.py @@ -58,9 +58,9 @@ class InboxIssuePublicViewSet(BaseViewSet): ) return InboxIssue.objects.none() - def list(self, request, slug, project_id, inbox_id): + def list(self, request, anchor, inbox_id): project_deploy_board = DeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + anchor=anchor, entity_name="project" ) if project_deploy_board.inbox is None: return Response( @@ -72,8 +72,8 @@ class InboxIssuePublicViewSet(BaseViewSet): issues = ( Issue.objects.filter( issue_inbox__inbox_id=inbox_id, - workspace__slug=slug, - project_id=project_id, + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, ) .filter(**filters) .annotate(bridge_id=F("issue_inbox__id")) @@ -117,9 +117,9 @@ class InboxIssuePublicViewSet(BaseViewSet): status=status.HTTP_200_OK, ) - def create(self, request, slug, project_id, inbox_id): + def create(self, request, anchor, inbox_id): project_deploy_board = DeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + anchor=anchor, entity_name="project" ) if project_deploy_board.inbox is None: return Response( @@ -151,7 +151,7 @@ class InboxIssuePublicViewSet(BaseViewSet): name="Triage", group="backlog", description="Default state for managing all Inbox Issues", - project_id=project_id, + project_id=project_deploy_board.project_id, color="#ff7700", ) @@ -163,7 +163,7 @@ class InboxIssuePublicViewSet(BaseViewSet): "description_html", "

" ), priority=request.data.get("issue", {}).get("priority", "low"), - project_id=project_id, + project_id=project_deploy_board.project_id, state=state, ) @@ -173,14 +173,14 @@ class InboxIssuePublicViewSet(BaseViewSet): requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), actor_id=str(request.user.id), issue_id=str(issue.id), - project_id=str(project_id), + project_id=str(project_deploy_board.project_id), current_instance=None, epoch=int(timezone.now().timestamp()), ) # create an inbox issue InboxIssue.objects.create( inbox_id=inbox_id, - project_id=project_id, + project_id=project_deploy_board.project_id, issue=issue, source=request.data.get("source", "in-app"), ) @@ -188,9 +188,9 @@ class InboxIssuePublicViewSet(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, anchor, inbox_id, pk): project_deploy_board = DeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + anchor=anchor, entity_name="project" ) if project_deploy_board.inbox is None: return Response( @@ -200,8 +200,8 @@ class InboxIssuePublicViewSet(BaseViewSet): inbox_issue = InboxIssue.objects.get( pk=pk, - workspace__slug=slug, - project_id=project_id, + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, inbox_id=inbox_id, ) # Get the project member @@ -216,8 +216,8 @@ class InboxIssuePublicViewSet(BaseViewSet): issue = Issue.objects.get( pk=inbox_issue.issue_id, - workspace__slug=slug, - project_id=project_id, + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, ) # viewers and guests since only viewers and guests issue_data = { @@ -242,7 +242,7 @@ class InboxIssuePublicViewSet(BaseViewSet): requested_data=requested_data, actor_id=str(request.user.id), issue_id=str(issue.id), - project_id=str(project_id), + project_id=str(project_deploy_board.project_id), current_instance=json.dumps( IssueSerializer(current_instance).data, cls=DjangoJSONEncoder, @@ -255,9 +255,9 @@ class InboxIssuePublicViewSet(BaseViewSet): issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST ) - def retrieve(self, request, slug, project_id, inbox_id, pk): + def retrieve(self, request, anchor, inbox_id, pk): project_deploy_board = DeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + anchor=anchor, entity_name="project" ) if project_deploy_board.inbox is None: return Response( @@ -267,21 +267,21 @@ class InboxIssuePublicViewSet(BaseViewSet): inbox_issue = InboxIssue.objects.get( pk=pk, - workspace__slug=slug, - project_id=project_id, + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, inbox_id=inbox_id, ) issue = Issue.objects.get( pk=inbox_issue.issue_id, - workspace__slug=slug, - project_id=project_id, + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.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, anchor, inbox_id, pk): project_deploy_board = DeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + anchor=anchor, entity_name="project" ) if project_deploy_board.inbox is None: return Response( @@ -291,8 +291,8 @@ class InboxIssuePublicViewSet(BaseViewSet): inbox_issue = InboxIssue.objects.get( pk=pk, - workspace__slug=slug, - project_id=project_id, + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, inbox_id=inbox_id, ) diff --git a/apiserver/plane/space/views/issue.py b/apiserver/plane/space/views/issue.py index 7ffdf0911..01da67752 100644 --- a/apiserver/plane/space/views/issue.py +++ b/apiserver/plane/space/views/issue.py @@ -77,14 +77,14 @@ class IssueCommentPublicViewSet(BaseViewSet): def get_queryset(self): try: project_deploy_board = DeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), + anchor=self.kwargs.get("anchor"), + entity_name="project", ) if project_deploy_board.comments: return self.filter_queryset( super() .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) + .filter(workspace_id=project_deploy_board.workspace_id) .filter(issue_id=self.kwargs.get("issue_id")) .filter(access="EXTERNAL") .select_related("project") @@ -93,8 +93,8 @@ class IssueCommentPublicViewSet(BaseViewSet): .annotate( is_member=Exists( ProjectMember.objects.filter( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, member_id=self.request.user.id, is_active=True, ) @@ -106,9 +106,9 @@ class IssueCommentPublicViewSet(BaseViewSet): except DeployBoard.DoesNotExist: return IssueComment.objects.none() - def create(self, request, slug, project_id, issue_id): + def create(self, request, anchor, issue_id): project_deploy_board = DeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + anchor=anchor, entity_name="project" ) if not project_deploy_board.comments: @@ -120,7 +120,7 @@ class IssueCommentPublicViewSet(BaseViewSet): serializer = IssueCommentSerializer(data=request.data) if serializer.is_valid(): serializer.save( - project_id=project_id, + project_id=project_deploy_board.project_id, issue_id=issue_id, actor=request.user, access="EXTERNAL", @@ -132,27 +132,27 @@ class IssueCommentPublicViewSet(BaseViewSet): ), actor_id=str(request.user.id), issue_id=str(issue_id), - project_id=str(project_id), + project_id=str(project_deploy_board.project_id), current_instance=None, epoch=int(timezone.now().timestamp()), ) if not ProjectMember.objects.filter( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, is_active=True, ).exists(): # Add the user for workspace tracking _ = ProjectPublicMember.objects.get_or_create( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def partial_update(self, request, slug, project_id, issue_id, pk): + def partial_update(self, request, anchor, issue_id, pk): project_deploy_board = DeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + anchor=anchor, entity_name="project" ) if not project_deploy_board.comments: @@ -160,9 +160,7 @@ class IssueCommentPublicViewSet(BaseViewSet): {"error": "Comments are not enabled for this project"}, status=status.HTTP_400_BAD_REQUEST, ) - comment = IssueComment.objects.get( - workspace__slug=slug, pk=pk, actor=request.user - ) + comment = IssueComment.objects.get(pk=pk, actor=request.user) serializer = IssueCommentSerializer( comment, data=request.data, partial=True ) @@ -173,7 +171,7 @@ class IssueCommentPublicViewSet(BaseViewSet): requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), actor_id=str(request.user.id), issue_id=str(issue_id), - project_id=str(project_id), + project_id=str(project_deploy_board.project_id), current_instance=json.dumps( IssueCommentSerializer(comment).data, cls=DjangoJSONEncoder, @@ -183,9 +181,9 @@ class IssueCommentPublicViewSet(BaseViewSet): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def destroy(self, request, slug, project_id, issue_id, pk): + def destroy(self, request, anchor, issue_id, pk): project_deploy_board = DeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + anchor=anchor, entity_name="project" ) if not project_deploy_board.comments: @@ -194,9 +192,7 @@ class IssueCommentPublicViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) comment = IssueComment.objects.get( - workspace__slug=slug, pk=pk, - project_id=project_id, actor=request.user, ) issue_activity.delay( @@ -204,7 +200,7 @@ class IssueCommentPublicViewSet(BaseViewSet): requested_data=json.dumps({"comment_id": str(pk)}), actor_id=str(request.user.id), issue_id=str(issue_id), - project_id=str(project_id), + project_id=str(project_deploy_board.project_id), current_instance=json.dumps( IssueCommentSerializer(comment).data, cls=DjangoJSONEncoder, @@ -239,9 +235,9 @@ class IssueReactionPublicViewSet(BaseViewSet): except DeployBoard.DoesNotExist: return IssueReaction.objects.none() - def create(self, request, slug, project_id, issue_id): + def create(self, request, anchor, issue_id): project_deploy_board = DeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + anchor=anchor, entity_name="project" ) if not project_deploy_board.reactions: @@ -253,16 +249,18 @@ class IssueReactionPublicViewSet(BaseViewSet): serializer = IssueReactionSerializer(data=request.data) if serializer.is_valid(): serializer.save( - project_id=project_id, issue_id=issue_id, actor=request.user + project_id=project_deploy_board.project_id, + issue_id=issue_id, + actor=request.user, ) if not ProjectMember.objects.filter( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, is_active=True, ).exists(): # Add the user for workspace tracking _ = ProjectPublicMember.objects.get_or_create( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, ) issue_activity.delay( @@ -272,16 +270,16 @@ class IssueReactionPublicViewSet(BaseViewSet): ), actor_id=str(self.request.user.id), issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), + project_id=str(project_deploy_board.project_id), current_instance=None, epoch=int(timezone.now().timestamp()), ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def destroy(self, request, slug, project_id, issue_id, reaction_code): + def destroy(self, request, anchor, issue_id, reaction_code): project_deploy_board = DeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + anchor=anchor, entity_name="project" ) if not project_deploy_board.reactions: @@ -290,7 +288,7 @@ class IssueReactionPublicViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) issue_reaction = IssueReaction.objects.get( - workspace__slug=slug, + workspace_id=project_deploy_board.workspace_id, issue_id=issue_id, reaction=reaction_code, actor=request.user, @@ -300,7 +298,7 @@ class IssueReactionPublicViewSet(BaseViewSet): requested_data=None, actor_id=str(self.request.user.id), issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), + project_id=str(project_deploy_board.project_id), current_instance=json.dumps( { "reaction": str(reaction_code), @@ -320,15 +318,14 @@ class CommentReactionPublicViewSet(BaseViewSet): def get_queryset(self): try: project_deploy_board = DeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), + anchor=self.kwargs.get("anchor"), entity_name="project" ) if project_deploy_board.reactions: return ( super() .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) + .filter(workspace_id=project_deploy_board.workspace_id) + .filter(project_id=project_deploy_board.project_id) .filter(comment_id=self.kwargs.get("comment_id")) .order_by("-created_at") .distinct() @@ -337,9 +334,9 @@ class CommentReactionPublicViewSet(BaseViewSet): except DeployBoard.DoesNotExist: return CommentReaction.objects.none() - def create(self, request, slug, project_id, comment_id): + def create(self, request, anchor, comment_id): project_deploy_board = DeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + anchor=anchor, entity_name="project" ) if not project_deploy_board.reactions: @@ -351,18 +348,18 @@ class CommentReactionPublicViewSet(BaseViewSet): serializer = CommentReactionSerializer(data=request.data) if serializer.is_valid(): serializer.save( - project_id=project_id, + project_id=project_deploy_board.project_id, comment_id=comment_id, actor=request.user, ) if not ProjectMember.objects.filter( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, is_active=True, ).exists(): # Add the user for workspace tracking _ = ProjectPublicMember.objects.get_or_create( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, ) issue_activity.delay( @@ -379,9 +376,9 @@ class CommentReactionPublicViewSet(BaseViewSet): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def destroy(self, request, slug, project_id, comment_id, reaction_code): + def destroy(self, request, anchor, comment_id, reaction_code): project_deploy_board = DeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + anchor=anchor, entity_name="project" ) if not project_deploy_board.reactions: return Response( @@ -390,8 +387,8 @@ class CommentReactionPublicViewSet(BaseViewSet): ) comment_reaction = CommentReaction.objects.get( - project_id=project_id, - workspace__slug=slug, + project_id=project_deploy_board.project_id, + workspace_id=project_deploy_board.workspace_id, comment_id=comment_id, reaction=reaction_code, actor=request.user, @@ -401,7 +398,7 @@ class CommentReactionPublicViewSet(BaseViewSet): requested_data=None, actor_id=str(self.request.user.id), issue_id=None, - project_id=str(self.kwargs.get("project_id", None)), + project_id=str(project_deploy_board.project_id), current_instance=json.dumps( { "reaction": str(reaction_code), @@ -422,35 +419,41 @@ class IssueVotePublicViewSet(BaseViewSet): def get_queryset(self): try: project_deploy_board = DeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), + workspace__slug=self.kwargs.get("anchor"), + entity_name="project", ) if project_deploy_board.votes: return ( super() .get_queryset() .filter(issue_id=self.kwargs.get("issue_id")) - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) + .filter(workspace_id=project_deploy_board.workspace_id) + .filter(project_id=project_deploy_board.project_id) ) return IssueVote.objects.none() except DeployBoard.DoesNotExist: return IssueVote.objects.none() - def create(self, request, slug, project_id, issue_id): + def create(self, request, anchor, issue_id): + print("hite") + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" + ) + print("awer") issue_vote, _ = IssueVote.objects.get_or_create( actor_id=request.user.id, - project_id=project_id, + project_id=project_deploy_board.project_id, issue_id=issue_id, ) + print("AWer") # Add the user for workspace tracking if not ProjectMember.objects.filter( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, is_active=True, ).exists(): _ = ProjectPublicMember.objects.get_or_create( - project_id=project_id, + project_id=project_deploy_board.project_id, member=request.user, ) issue_vote.vote = request.data.get("vote", 1) @@ -462,26 +465,29 @@ class IssueVotePublicViewSet(BaseViewSet): ), actor_id=str(self.request.user.id), issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), + project_id=str(project_deploy_board.project_id), current_instance=None, epoch=int(timezone.now().timestamp()), ) serializer = IssueVoteSerializer(issue_vote) return Response(serializer.data, status=status.HTTP_201_CREATED) - def destroy(self, request, slug, project_id, issue_id): + def destroy(self, request, anchor, issue_id): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" + ) issue_vote = IssueVote.objects.get( - workspace__slug=slug, - project_id=project_id, issue_id=issue_id, actor_id=request.user.id, + project_id=project_deploy_board.project_id, + workspace_id=project_deploy_board.workspace_id, ) issue_activity.delay( type="issue_vote.activity.deleted", requested_data=None, actor_id=str(self.request.user.id), issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), + project_id=str(project_deploy_board.project_id), current_instance=json.dumps( { "vote": str(issue_vote.vote), @@ -499,9 +505,14 @@ class IssueRetrievePublicEndpoint(BaseAPIView): AllowAny, ] - def get(self, request, slug, project_id, issue_id): + def get(self, request, anchor, issue_id): + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" + ) issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=issue_id + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, + pk=issue_id, ) serializer = IssuePublicSerializer(issue) return Response(serializer.data, status=status.HTTP_200_OK) @@ -512,14 +523,17 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): AllowAny, ] - def get(self, request, slug, project_id): + def get(self, request, anchor): if not DeployBoard.objects.filter( - workspace__slug=slug, project_id=project_id + anchor=anchor, entity_name="project" ).exists(): return Response( {"error": "Project is not published"}, status=status.HTTP_404_NOT_FOUND, ) + project_deploy_board = DeployBoard.objects.get( + anchor=anchor, entity_name="project" + ) filters = issue_filters(request.query_params, "GET") @@ -544,8 +558,8 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .filter(project_id=project_id) - .filter(workspace__slug=slug) + .filter(project_id=project_deploy_board.project_id) + .filter(workspace_id=project_deploy_board.workspace_id) .select_related("project", "workspace", "state", "parent") .prefetch_related("assignees", "labels") .prefetch_related( @@ -652,8 +666,8 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): states = ( State.objects.filter( ~Q(name="Triage"), - workspace__slug=slug, - project_id=project_id, + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, ) .annotate( custom_order=Case( @@ -670,7 +684,8 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): ) labels = Label.objects.filter( - workspace__slug=slug, project_id=project_id + workspace_id=project_deploy_board.workspace_id, + project_id=project_deploy_board.project_id, ).values("id", "name", "color", "parent") ## Grouping the results diff --git a/apiserver/plane/space/views/project.py b/apiserver/plane/space/views/project.py index 2cace08da..76f1600ee 100644 --- a/apiserver/plane/space/views/project.py +++ b/apiserver/plane/space/views/project.py @@ -23,9 +23,9 @@ class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView): AllowAny, ] - def get(self, request, slug, project_id): + def get(self, request, anchor): project_deploy_board = DeployBoard.objects.get( - workspace__slug=slug, project_id=project_id + anchor=anchor, entity_name="project" ) serializer = DeployBoardSerializer(project_deploy_board) return Response(serializer.data, status=status.HTTP_200_OK) @@ -36,13 +36,16 @@ class WorkspaceProjectDeployBoardEndpoint(BaseAPIView): AllowAny, ] - def get(self, request, slug): + def get(self, request, anchor): + deploy_board = DeployBoard.objects.filter(anchor=anchor, entity_name="project").values_list projects = ( - Project.objects.filter(workspace__slug=slug) + Project.objects.filter(workspace=deploy_board.workspace) .annotate( is_public=Exists( DeployBoard.objects.filter( - workspace__slug=slug, project_id=OuterRef("pk") + anchor=anchor, + project_id=OuterRef("pk"), + entity_name="project", ) ) ) @@ -58,3 +61,16 @@ class WorkspaceProjectDeployBoardEndpoint(BaseAPIView): ) return Response(projects, status=status.HTTP_200_OK) + + +class WorkspaceProjectAnchorEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request, slug, project_id): + project_deploy_board = DeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + serializer = DeployBoardSerializer(project_deploy_board) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/space/app/[workspaceSlug]/[projectId]/page.ts b/space/app/[workspaceSlug]/[projectId]/page.ts new file mode 100644 index 000000000..8af878397 --- /dev/null +++ b/space/app/[workspaceSlug]/[projectId]/page.ts @@ -0,0 +1,42 @@ +import { notFound, redirect } from "next/navigation"; +// services +import PublishService from "@/services/publish.service"; +// types +import { TPublishSettings } from "@/types/publish"; + +const publishService = new PublishService(); + +type Props = { + params: { + workspaceSlug: string; + projectId: string; + }; + searchParams: any; +}; + +export default async function IssuesPage(props: Props) { + const { params, searchParams } = props; + // query params + const { workspaceSlug, projectId } = params; + const { board, peekId } = searchParams; + + let response: TPublishSettings | undefined = undefined; + try { + response = await publishService.fetchAnchorFromProjectDetails(workspaceSlug, projectId); + } catch (error) { + // redirect to 404 page on error + notFound(); + } + + let url = ""; + if (response.entity_name === "project") { + url = `/issues/${response.anchor}`; + const params = new URLSearchParams(); + if (board) params.append("board", board); + if (peekId) params.append("peekId", peekId); + if (params.toString()) url += `?${params.toString()}`; + redirect(url); + } else { + notFound(); + } +} diff --git a/space/app/[workspace_slug]/[project_id]/page.tsx b/space/app/[workspace_slug]/[project_id]/page.tsx deleted file mode 100644 index 0d08ae7eb..000000000 --- a/space/app/[workspace_slug]/[project_id]/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client"; - -import { useSearchParams } from "next/navigation"; -// components -import { ProjectDetailsView } from "@/components/views"; - -export default function WorkspaceProjectPage({ params }: { params: { workspace_slug: any; project_id: any } }) { - const { workspace_slug, project_id } = params; - - const searchParams = useSearchParams(); - const peekId = searchParams.get("peekId") || undefined; - - if (!workspace_slug || !project_id) return <>; - - return ; -} diff --git a/space/app/error.tsx b/space/app/error.tsx index 2d6f22e90..e47a1af1d 100644 --- a/space/app/error.tsx +++ b/space/app/error.tsx @@ -1,38 +1,47 @@ "use client"; -import Image from "next/image"; -import { useTheme } from "next-themes"; +// ui import { Button } from "@plane/ui"; -// assets -import InstanceFailureDarkImage from "@/public/instance/instance-failure-dark.svg"; -import InstanceFailureImage from "@/public/instance/instance-failure.svg"; - -export default function InstanceError() { - const { resolvedTheme } = useTheme(); - - const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage; +const ErrorPage = () => { const handleRetry = () => { window.location.reload(); }; return ( -
-
-
- Plane instance failure image -

Unable to fetch instance details.

-

- We were unable to fetch the details of the instance.
- Fret not, it might just be a connectivity issue. +

+
+
+

Exception Detected!

+

+ We{"'"}re Sorry! An exception has been detected, and our engineering team has been notified. We apologize + for any inconvenience this may have caused. Please reach out to our engineering team at{" "} + + support@plane.so + {" "} + or on our{" "} + + Discord + {" "} + server for further assistance.

-
- + {/* */}
); -} +}; + +export default ErrorPage; diff --git a/space/app/[workspace_slug]/[project_id]/layout.tsx b/space/app/issues/[anchor]/layout.tsx similarity index 54% rename from space/app/[workspace_slug]/[project_id]/layout.tsx rename to space/app/issues/[anchor]/layout.tsx index b1e134ea6..91291e481 100644 --- a/space/app/[workspace_slug]/[project_id]/layout.tsx +++ b/space/app/issues/[anchor]/layout.tsx @@ -1,25 +1,39 @@ +"use client"; + +import { observer } from "mobx-react-lite"; import Image from "next/image"; -import { notFound } from "next/navigation"; +import useSWR from "swr"; // components -import IssueNavbar from "@/components/issues/navbar"; +import { LogoSpinner } from "@/components/common"; +import { IssuesNavbarRoot } from "@/components/issues"; +// hooks +import { usePublish, usePublishList } from "@/hooks/store"; // assets -import planeLogo from "public/plane-logo.svg"; +import planeLogo from "@/public/plane-logo.svg"; -export default async function ProjectLayout({ - children, - params, -}: { +type Props = { children: React.ReactNode; - params: { workspace_slug: string; project_id: string }; -}) { - const { workspace_slug, project_id } = params; + params: { + anchor: string; + }; +}; - if (!workspace_slug || !project_id) notFound(); +const IssuesLayout = observer((props: Props) => { + const { children, params } = props; + // params + const { anchor } = params; + // store hooks + const { fetchPublishSettings } = usePublishList(); + const publishSettings = usePublish(anchor); + // fetch publish settings + useSWR(anchor ? `PUBLISH_SETTINGS_${anchor}` : null, anchor ? () => fetchPublishSettings(anchor) : null); + + if (!publishSettings) return ; return (
- +
{children}
); -} +}); + +export default IssuesLayout; diff --git a/space/app/issues/[anchor]/page.tsx b/space/app/issues/[anchor]/page.tsx new file mode 100644 index 000000000..b3c9353e6 --- /dev/null +++ b/space/app/issues/[anchor]/page.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { observer } from "mobx-react-lite"; +import { useSearchParams } from "next/navigation"; +// components +import { IssuesLayoutsRoot } from "@/components/issues"; +// hooks +import { usePublish } from "@/hooks/store"; + +type Props = { + params: { + anchor: string; + }; +}; + +const IssuesPage = observer((props: Props) => { + const { params } = props; + const { anchor } = params; + // params + const searchParams = useSearchParams(); + const peekId = searchParams.get("peekId") || undefined; + + const publishSettings = usePublish(anchor); + + if (!publishSettings) return null; + + return ; +}); + +export default IssuesPage; diff --git a/space/app/not-found.tsx b/space/app/not-found.tsx index cae576319..c5320b2dc 100644 --- a/space/app/not-found.tsx +++ b/space/app/not-found.tsx @@ -4,20 +4,18 @@ import Image from "next/image"; // assets import UserLoggedInImage from "public/user-logged-in.svg"; -export default function NotFound() { - return ( -
-
-
-
-
- User already logged in -
-
-

Not Found

-

Please enter the appropriate project URL to view the issue board.

+const NotFound = () => ( +
+
+
+
+ User already logged in
+

Not Found

+

Please enter the appropriate project URL to view the issue board.

- ); -} +
+); + +export default NotFound; diff --git a/space/components/account/user-logged-in.tsx b/space/components/account/user-logged-in.tsx index 33be330fa..5975d73b6 100644 --- a/space/components/account/user-logged-in.tsx +++ b/space/components/account/user-logged-in.tsx @@ -1,36 +1,44 @@ "use client"; +import { observer } from "mobx-react-lite"; import Image from "next/image"; +import { useTheme } from "next-themes"; // components -import { UserAvatar } from "@/components/issues/navbar/user-avatar"; +import { UserAvatar } from "@/components/issues"; // hooks import { useUser } from "@/hooks/store"; // assets -import PlaneLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png"; +import PlaneBlackLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png"; +import PlaneWhiteLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png"; import UserLoggedInImage from "@/public/user-logged-in.svg"; -export const UserLoggedIn = () => { +export const UserLoggedIn = observer(() => { + // store hooks const { data: user } = useUser(); + // next-themes + const { resolvedTheme } = useTheme(); + + const logo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo; if (!user) return null; return ( -
+
-
- User already logged in +
+ Plane logo
-
+
-
-
+
+
User already logged in
-

Logged in Successfully!

+

Logged in successfully!

You{"'"}ve successfully logged in. Please enter the appropriate project URL to view the issue board.

@@ -38,4 +46,4 @@ export const UserLoggedIn = () => {
); -}; +}); diff --git a/space/components/common/index.ts b/space/components/common/index.ts index c4ea97f3c..1949c069b 100644 --- a/space/components/common/index.ts +++ b/space/components/common/index.ts @@ -1,3 +1,2 @@ -export * from "./latest-feature-block"; export * from "./project-logo"; export * from "./logo-spinner"; diff --git a/space/components/common/latest-feature-block.tsx b/space/components/common/latest-feature-block.tsx deleted file mode 100644 index c1b5db954..000000000 --- a/space/components/common/latest-feature-block.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import Image from "next/image"; -import Link from "next/link"; -import { useTheme } from "next-themes"; -// icons -import { Lightbulb } from "lucide-react"; -// images -import latestFeatures from "public/onboarding/onboarding-pages.svg"; - -export const LatestFeatureBlock = () => { - const { resolvedTheme } = useTheme(); - - return ( - <> -
- -

- Pages gets a facelift! Write anything and use Galileo to help you start.{" "} - - Learn more - -

-
-
-
- Plane Issues -
-
- - ); -}; diff --git a/space/components/instance/index.ts b/space/components/instance/index.ts index 6568894f0..be80bc669 100644 --- a/space/components/instance/index.ts +++ b/space/components/instance/index.ts @@ -1,2 +1 @@ -export * from "./not-ready-view"; export * from "./instance-failure-view"; diff --git a/space/components/instance/not-ready-view.tsx b/space/components/instance/not-ready-view.tsx deleted file mode 100644 index be46a9473..000000000 --- a/space/components/instance/not-ready-view.tsx +++ /dev/null @@ -1,62 +0,0 @@ -"use client"; - -import { FC } from "react"; -import Image from "next/image"; -import Link from "next/link"; -import { useTheme } from "next-themes"; -// ui -import { Button } from "@plane/ui"; -// helper -import { GOD_MODE_URL, SPACE_BASE_PATH } from "@/helpers/common.helper"; -// images -import PlaneTakeOffImage from "@/public/instance/plane-takeoff.png"; -import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg"; -import PlaneBackgroundPattern from "public/auth/background-pattern.svg"; -import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.png"; -import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.png"; - -export const InstanceNotReady: FC = () => { - const { resolvedTheme } = useTheme(); - const patternBackground = resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern; - - const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo; - - return ( -
-
-
-
- - Plane logo - -
-
- -
- Plane background pattern -
- -
-
-
-
-

Welcome aboard Plane!

- Plane Logo -

- Get started by setting up your instance and workspace -

-
- -
-
-
-
-
- ); -}; diff --git a/space/components/issues/board-views/block-downvotes.tsx b/space/components/issues/board-views/block-downvotes.tsx deleted file mode 100644 index 4326a8823..000000000 --- a/space/components/issues/board-views/block-downvotes.tsx +++ /dev/null @@ -1,10 +0,0 @@ -"use client"; - -export const IssueBlockDownVotes = ({ number }: { number: number }) => ( -
- - arrow_upward_alt - - {number} -
-); diff --git a/space/components/issues/board-views/block-due-date.tsx b/space/components/issues/board-views/block-due-date.tsx deleted file mode 100644 index ecf229562..000000000 --- a/space/components/issues/board-views/block-due-date.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client"; - -// helpers -import { renderFullDate } from "@/helpers/date-time.helper"; - -export const dueDateIconDetails = ( - date: string, - stateGroup: string -): { - iconName: string; - className: string; -} => { - let iconName = "calendar_today"; - let className = ""; - - if (!date || ["completed", "cancelled"].includes(stateGroup)) { - iconName = "calendar_today"; - className = ""; - } else { - const today = new Date(); - today.setHours(0, 0, 0, 0); - const targetDate = new Date(date); - targetDate.setHours(0, 0, 0, 0); - - const timeDifference = targetDate.getTime() - today.getTime(); - - if (timeDifference < 0) { - iconName = "event_busy"; - className = "text-red-500"; - } else if (timeDifference === 0) { - iconName = "today"; - className = "text-red-500"; - } else if (timeDifference === 24 * 60 * 60 * 1000) { - iconName = "event"; - className = "text-yellow-500"; - } else { - iconName = "calendar_today"; - className = ""; - } - } - - return { - iconName, - className, - }; -}; - -export const IssueBlockDueDate = ({ due_date, group }: { due_date: string; group: string }) => { - const iconDetails = dueDateIconDetails(due_date, group); - - return ( -
- - {iconDetails.iconName} - - {renderFullDate(due_date)} -
- ); -}; diff --git a/space/components/issues/board-views/block-labels.tsx b/space/components/issues/board-views/block-labels.tsx deleted file mode 100644 index 05f6a039f..000000000 --- a/space/components/issues/board-views/block-labels.tsx +++ /dev/null @@ -1,19 +0,0 @@ -"use client"; - -export const IssueBlockLabels = ({ labels }: any) => ( -
- {labels && - labels.length > 0 && - labels.map((_label: any) => ( -
-
-
-
{_label?.name}
-
-
- ))} -
-); diff --git a/space/components/issues/board-views/block-state.tsx b/space/components/issues/board-views/block-state.tsx deleted file mode 100644 index 39b10ceb0..000000000 --- a/space/components/issues/board-views/block-state.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// ui -import { StateGroupIcon } from "@plane/ui"; -// constants -import { issueGroupFilter } from "@/constants/issue"; - -export const IssueBlockState = ({ state }: any) => { - const stateGroup = issueGroupFilter(state.group); - - if (stateGroup === null) return <>; - return ( -
-
- -
{state?.name}
-
-
- ); -}; diff --git a/space/components/issues/board-views/block-upvotes.tsx b/space/components/issues/board-views/block-upvotes.tsx deleted file mode 100644 index 3927acac4..000000000 --- a/space/components/issues/board-views/block-upvotes.tsx +++ /dev/null @@ -1,8 +0,0 @@ -"use client"; - -export const IssueBlockUpVotes = ({ number }: { number: number }) => ( -
- arrow_upward_alt - {number} -
-); diff --git a/space/components/issues/board-views/calendar/index.tsx b/space/components/issues/board-views/calendar/index.tsx deleted file mode 100644 index 0edeca96c..000000000 --- a/space/components/issues/board-views/calendar/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export const IssueCalendarView = () =>
; diff --git a/space/components/issues/board-views/gantt/index.tsx b/space/components/issues/board-views/gantt/index.tsx deleted file mode 100644 index 5da924b2c..000000000 --- a/space/components/issues/board-views/gantt/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export const IssueGanttView = () =>
; diff --git a/space/components/issues/board-views/kanban/block.tsx b/space/components/issues/board-views/kanban/block.tsx deleted file mode 100644 index e34222dd4..000000000 --- a/space/components/issues/board-views/kanban/block.tsx +++ /dev/null @@ -1,82 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -import { useRouter, useSearchParams } from "next/navigation"; -// components -import { IssueBlockDueDate } from "@/components/issues/board-views/block-due-date"; -import { IssueBlockPriority } from "@/components/issues/board-views/block-priority"; -import { IssueBlockState } from "@/components/issues/board-views/block-state"; -// helpers -import { queryParamGenerator } from "@/helpers/query-param-generator"; -// hooks -import { useIssueDetails, useProject } from "@/hooks/store"; -// interfaces -import { IIssue } from "@/types/issue"; - -type IssueKanBanBlockProps = { - issue: IIssue; - workspaceSlug: string; - projectId: string; - params: any; -}; - -export const IssueKanBanBlock: FC = observer((props) => { - const router = useRouter(); - const searchParams = useSearchParams(); - // query params - const board = searchParams.get("board") || undefined; - const state = searchParams.get("state") || undefined; - const priority = searchParams.get("priority") || undefined; - const labels = searchParams.get("labels") || undefined; - // props - const { workspaceSlug, projectId, issue } = props; - // hooks - const { project } = useProject(); - const { setPeekId } = useIssueDetails(); - - const handleBlockClick = () => { - setPeekId(issue.id); - const { queryParam } = queryParamGenerator({ board, peekId: issue.id, priority, state, labels }); - router.push(`/${workspaceSlug}/${projectId}?${queryParam}`); - }; - - return ( -
- {/* id */} -
- {project?.identifier}-{issue?.sequence_id} -
- - {/* name */} -
- {issue.name} -
- -
- {/* priority */} - {issue?.priority && ( -
- -
- )} - {/* state */} - {issue?.state_detail && ( -
- -
- )} - {/* due date */} - {issue?.target_date && ( -
- -
- )} -
-
- ); -}); diff --git a/space/components/issues/board-views/kanban/header.tsx b/space/components/issues/board-views/kanban/header.tsx deleted file mode 100644 index baf5612b3..000000000 --- a/space/components/issues/board-views/kanban/header.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// ui -import { StateGroupIcon } from "@plane/ui"; -// constants -import { issueGroupFilter } from "@/constants/issue"; -// mobx hook -// import { useIssue } from "@/hooks/store"; -// interfaces -import { IIssueState } from "@/types/issue"; - -export const IssueKanBanHeader = observer(({ state }: { state: IIssueState }) => { - // const { getCountOfIssuesByState } = useIssue(); - const stateGroup = issueGroupFilter(state.group); - - if (stateGroup === null) return <>; - - return ( -
-
- -
-
{state?.name}
- {/* {getCountOfIssuesByState(state.id)} */} -
- ); -}); diff --git a/space/components/issues/board-views/kanban/index.tsx b/space/components/issues/board-views/kanban/index.tsx deleted file mode 100644 index e2e4e9900..000000000 --- a/space/components/issues/board-views/kanban/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { IssueKanBanBlock } from "@/components/issues/board-views/kanban/block"; -import { IssueKanBanHeader } from "@/components/issues/board-views/kanban/header"; -// ui -import { Icon } from "@/components/ui"; -// mobx hook -import { useIssue } from "@/hooks/store"; -// interfaces -import { IIssueState, IIssue } from "@/types/issue"; - -type IssueKanbanViewProps = { - workspaceSlug: string; - projectId: string; -}; - -export const IssueKanbanView: FC = observer((props) => { - const { workspaceSlug, projectId } = props; - // store hooks - const { states, getFilteredIssuesByState } = useIssue(); - - return ( -
- {states && - states.length > 0 && - states.map((_state: IIssueState) => ( -
-
- -
-
- {getFilteredIssuesByState(_state.id) && getFilteredIssuesByState(_state.id).length > 0 ? ( -
- {getFilteredIssuesByState(_state.id).map((_issue: IIssue) => ( - - ))} -
- ) : ( -
- - No issues in this state -
- )} -
-
- ))} -
- ); -}); diff --git a/space/components/issues/board-views/list/index.tsx b/space/components/issues/board-views/list/index.tsx deleted file mode 100644 index 2a2b958be..000000000 --- a/space/components/issues/board-views/list/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { IssueListBlock } from "@/components/issues/board-views/list/block"; -import { IssueListHeader } from "@/components/issues/board-views/list/header"; -// mobx hook -import { useIssue } from "@/hooks/store"; -// types -import { IIssueState, IIssue } from "@/types/issue"; - -type IssueListViewProps = { - workspaceSlug: string; - projectId: string; -}; - -export const IssueListView: FC = observer((props) => { - const { workspaceSlug, projectId } = props; - // store hooks - const { states, getFilteredIssuesByState } = useIssue(); - - return ( - <> - {states && - states.length > 0 && - states.map((_state: IIssueState) => ( -
- - {getFilteredIssuesByState(_state.id) && getFilteredIssuesByState(_state.id).length > 0 ? ( -
- {getFilteredIssuesByState(_state.id).map((_issue: IIssue) => ( - - ))} -
- ) : ( -
No issues.
- )} -
- ))} - - ); -}); diff --git a/space/components/issues/board-views/spreadsheet/index.tsx b/space/components/issues/board-views/spreadsheet/index.tsx deleted file mode 100644 index 45ebf2792..000000000 --- a/space/components/issues/board-views/spreadsheet/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export const IssueSpreadsheetView = () =>
; diff --git a/space/components/issues/filters/applied-filters/filters-list.tsx b/space/components/issues/filters/applied-filters/filters-list.tsx index 83d651f5d..87089c500 100644 --- a/space/components/issues/filters/applied-filters/filters-list.tsx +++ b/space/components/issues/filters/applied-filters/filters-list.tsx @@ -1,10 +1,10 @@ "use client"; -// icons import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; // types -import { IIssueLabel, IIssueState, TFilters } from "@/types/issue"; +import { IStateLite } from "@plane/types"; +import { IIssueLabel, TFilters } from "@/types/issue"; // components import { AppliedPriorityFilters } from "./priority"; import { AppliedStateFilters } from "./state"; @@ -14,7 +14,7 @@ type Props = { handleRemoveAllFilters: () => void; handleRemoveFilter: (key: keyof TFilters, value: string | null) => void; labels?: IIssueLabel[] | undefined; - states?: IIssueState[] | undefined; + states?: IStateLite[] | undefined; }; export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " "); diff --git a/space/components/issues/filters/applied-filters/root.tsx b/space/components/issues/filters/applied-filters/root.tsx index 9dd1eb013..9b6625d75 100644 --- a/space/components/issues/filters/applied-filters/root.tsx +++ b/space/components/issues/filters/applied-filters/root.tsx @@ -12,18 +12,18 @@ import { TIssueQueryFilters } from "@/types/issue"; import { AppliedFiltersList } from "./filters-list"; type TIssueAppliedFilters = { - workspaceSlug: string; - projectId: string; + anchor: string; }; export const IssueAppliedFilters: FC = observer((props) => { + const { anchor } = props; + // router const router = useRouter(); - // props - const { workspaceSlug, projectId } = props; - // hooks - const { issueFilters, initIssueFilters, updateIssueFilters } = useIssueFilter(); + // store hooks + const { getIssueFilters, initIssueFilters, updateIssueFilters } = useIssueFilter(); const { states, labels } = useIssue(); - + // derived values + const issueFilters = getIssueFilters(anchor); const activeLayout = issueFilters?.display_filters?.layout || undefined; const userFilters = issueFilters?.filters || {}; @@ -46,30 +46,26 @@ export const IssueAppliedFilters: FC = observer((props) => if (labels.length > 0) params = { ...params, labels: labels.join(",") }; params = new URLSearchParams(params).toString(); - router.push(`/${workspaceSlug}/${projectId}?${params}`); + router.push(`/issues/${anchor}?${params}`); }, - [workspaceSlug, projectId, activeLayout, issueFilters, router] + [activeLayout, anchor, issueFilters, router] ); const handleFilters = useCallback( (key: keyof TIssueQueryFilters, value: string | null) => { - if (!projectId) return; - let newValues = cloneDeep(issueFilters?.filters?.[key]) ?? []; if (value === null) newValues = []; else if (newValues.includes(value)) newValues.splice(newValues.indexOf(value), 1); - updateIssueFilters(projectId, "filters", key, newValues); + updateIssueFilters(anchor, "filters", key, newValues); updateRouteParams(key, newValues); }, - [projectId, issueFilters, updateIssueFilters, updateRouteParams] + [anchor, issueFilters, updateIssueFilters, updateRouteParams] ); const handleRemoveAllFilters = () => { - if (!projectId) return; - - initIssueFilters(projectId, { + initIssueFilters(anchor, { display_filters: { layout: activeLayout || "list" }, filters: { state: [], @@ -78,13 +74,13 @@ export const IssueAppliedFilters: FC = observer((props) => }, }); - router.push(`/${workspaceSlug}/${projectId}?${`board=${activeLayout || "list"}`}`); + router.push(`/issues/${anchor}?${`board=${activeLayout || "list"}`}`); }; if (Object.keys(appliedFilters).length === 0) return null; return ( -
+
void; - states: IIssueState[]; + states: IStateLite[]; values: string[]; }; diff --git a/space/components/issues/filters/root.tsx b/space/components/issues/filters/root.tsx index de972ea8a..dba13f9fb 100644 --- a/space/components/issues/filters/root.tsx +++ b/space/components/issues/filters/root.tsx @@ -17,17 +17,18 @@ import { useIssue, useIssueFilter } from "@/hooks/store"; import { TIssueQueryFilters } from "@/types/issue"; type IssueFiltersDropdownProps = { - workspaceSlug: string; - projectId: string; + anchor: string; }; export const IssueFiltersDropdown: FC = observer((props) => { + const { anchor } = props; + // router const router = useRouter(); - const { workspaceSlug, projectId } = props; // hooks - const { issueFilters, updateIssueFilters } = useIssueFilter(); + const { getIssueFilters, updateIssueFilters } = useIssueFilter(); const { states, labels } = useIssue(); - + // derived values + const issueFilters = getIssueFilters(anchor); const activeLayout = issueFilters?.display_filters?.layout || undefined; const updateRouteParams = useCallback( @@ -37,24 +38,24 @@ export const IssueFiltersDropdown: FC = observer((pro const labels = key === "labels" ? value : issueFilters?.filters?.labels ?? []; const { queryParam } = queryParamGenerator({ board: activeLayout, priority, state, labels }); - router.push(`/${workspaceSlug}/${projectId}?${queryParam}`); + router.push(`/issues/${anchor}?${queryParam}`); }, - [workspaceSlug, projectId, activeLayout, issueFilters, router] + [anchor, activeLayout, issueFilters, router] ); const handleFilters = useCallback( (key: keyof TIssueQueryFilters, value: string) => { - if (!projectId || !value) return; + if (!value) return; const newValues = cloneDeep(issueFilters?.filters?.[key]) ?? []; if (newValues.includes(value)) newValues.splice(newValues.indexOf(value), 1); else newValues.push(value); - updateIssueFilters(projectId, "filters", key, newValues); + updateIssueFilters(anchor, "filters", key, newValues); updateRouteParams(key, newValues); }, - [projectId, issueFilters, updateIssueFilters, updateRouteParams] + [anchor, issueFilters, updateIssueFilters, updateRouteParams] ); return ( diff --git a/space/components/issues/filters/selection.tsx b/space/components/issues/filters/selection.tsx index a1180b0ee..926fbf5b0 100644 --- a/space/components/issues/filters/selection.tsx +++ b/space/components/issues/filters/selection.tsx @@ -4,7 +4,8 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; import { Search, X } from "lucide-react"; // types -import { IIssueState, IIssueLabel, IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue"; +import { IStateLite } from "@plane/types"; +import { IIssueLabel, IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue"; // components import { FilterPriority, FilterState } from "./"; @@ -13,7 +14,7 @@ type Props = { handleFilters: (key: keyof IIssueFilterOptions, value: string | string[]) => void; layoutDisplayFiltersOptions: TIssueFilterKeys[]; labels?: IIssueLabel[] | undefined; - states?: IIssueState[] | undefined; + states?: IStateLite[] | undefined; }; export const FilterSelection: React.FC = observer((props) => { diff --git a/space/components/issues/filters/state.tsx b/space/components/issues/filters/state.tsx index 24b6bb5c8..f61237eef 100644 --- a/space/components/issues/filters/state.tsx +++ b/space/components/issues/filters/state.tsx @@ -1,17 +1,18 @@ "use client"; import React, { useState } from "react"; +// types +import { IStateLite } from "@plane/types"; +// ui import { Loader, StateGroupIcon } from "@plane/ui"; // components import { FilterHeader, FilterOption } from "@/components/issues/filters/helpers"; -// types -import { IIssueState } from "@/types/issue"; type Props = { appliedFilters: string[] | null; handleUpdate: (val: string) => void; searchQuery: string; - states: IIssueState[] | undefined; + states: IStateLite[] | undefined; }; export const FilterState: React.FC = (props) => { diff --git a/space/components/issues/index.ts b/space/components/issues/index.ts new file mode 100644 index 000000000..6aee62097 --- /dev/null +++ b/space/components/issues/index.ts @@ -0,0 +1,2 @@ +export * from "./issue-layouts"; +export * from "./navbar"; diff --git a/space/components/issues/issue-layouts/index.ts b/space/components/issues/issue-layouts/index.ts new file mode 100644 index 000000000..5ab6813cd --- /dev/null +++ b/space/components/issues/issue-layouts/index.ts @@ -0,0 +1,4 @@ +export * from "./kanban"; +export * from "./list"; +export * from "./properties"; +export * from "./root"; diff --git a/space/components/issues/issue-layouts/kanban/block.tsx b/space/components/issues/issue-layouts/kanban/block.tsx new file mode 100644 index 000000000..ac03823b4 --- /dev/null +++ b/space/components/issues/issue-layouts/kanban/block.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; +// components +import { IssueBlockDueDate, IssueBlockPriority, IssueBlockState } from "@/components/issues"; +// helpers +import { queryParamGenerator } from "@/helpers/query-param-generator"; +// hooks +import { useIssueDetails, usePublish } from "@/hooks/store"; +// interfaces +import { IIssue } from "@/types/issue"; + +type Props = { + anchor: string; + issue: IIssue; + params: any; +}; + +export const IssueKanBanBlock: FC = observer((props) => { + const { anchor, issue } = props; + const searchParams = useSearchParams(); + // query params + const board = searchParams.get("board"); + const state = searchParams.get("state"); + const priority = searchParams.get("priority"); + const labels = searchParams.get("labels"); + // store hooks + const { project_details } = usePublish(anchor); + const { setPeekId } = useIssueDetails(); + + const { queryParam } = queryParamGenerator({ board, peekId: issue.id, priority, state, labels }); + + const handleBlockClick = () => { + setPeekId(issue.id); + }; + + return ( + + {/* id */} +
+ {project_details?.identifier}-{issue?.sequence_id} +
+ + {/* name */} +
+ {issue.name} +
+ +
+ {/* priority */} + {issue?.priority && ( +
+ +
+ )} + {/* state */} + {issue?.state_detail && ( +
+ +
+ )} + {/* due date */} + {issue?.target_date && ( +
+ +
+ )} +
+ + ); +}); diff --git a/space/components/issues/issue-layouts/kanban/header.tsx b/space/components/issues/issue-layouts/kanban/header.tsx new file mode 100644 index 000000000..ee5433d68 --- /dev/null +++ b/space/components/issues/issue-layouts/kanban/header.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { observer } from "mobx-react-lite"; +// types +import { IStateLite } from "@plane/types"; +// ui +import { StateGroupIcon } from "@plane/ui"; + +type Props = { + state: IStateLite; +}; + +export const IssueKanBanHeader: React.FC = observer((props) => { + const { state } = props; + + return ( +
+
+ +
+
{state?.name}
+ {/* {getCountOfIssuesByState(state.id)} */} +
+ ); +}); diff --git a/space/components/issues/issue-layouts/kanban/index.ts b/space/components/issues/issue-layouts/kanban/index.ts new file mode 100644 index 000000000..62874fbda --- /dev/null +++ b/space/components/issues/issue-layouts/kanban/index.ts @@ -0,0 +1,3 @@ +export * from "./block"; +export * from "./header"; +export * from "./root"; diff --git a/space/components/issues/issue-layouts/kanban/root.tsx b/space/components/issues/issue-layouts/kanban/root.tsx new file mode 100644 index 000000000..e0a5593e9 --- /dev/null +++ b/space/components/issues/issue-layouts/kanban/root.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +// components +import { IssueKanBanBlock, IssueKanBanHeader } from "@/components/issues"; +// ui +import { Icon } from "@/components/ui"; +// mobx hook +import { useIssue } from "@/hooks/store"; + +type Props = { + anchor: string; +}; + +export const IssueKanbanLayoutRoot: FC = observer((props) => { + const { anchor } = props; + // store hooks + const { states, getFilteredIssuesByState } = useIssue(); + + return ( +
+ {states?.map((state) => { + const issues = getFilteredIssuesByState(state.id); + + return ( +
+
+ +
+
+ {issues && issues.length > 0 ? ( +
+ {issues.map((issue) => ( + + ))} +
+ ) : ( +
+ + No issues in this state +
+ )} +
+
+ ); + })} +
+ ); +}); diff --git a/space/components/issues/board-views/list/block.tsx b/space/components/issues/issue-layouts/list/block.tsx similarity index 64% rename from space/components/issues/board-views/list/block.tsx rename to space/components/issues/issue-layouts/list/block.tsx index 6b6231fcf..8c241753d 100644 --- a/space/components/issues/board-views/list/block.tsx +++ b/space/components/issues/issue-layouts/list/block.tsx @@ -1,56 +1,52 @@ "use client"; import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useRouter, useSearchParams } from "next/navigation"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; // components -import { IssueBlockDueDate } from "@/components/issues/board-views/block-due-date"; -import { IssueBlockLabels } from "@/components/issues/board-views/block-labels"; -import { IssueBlockPriority } from "@/components/issues/board-views/block-priority"; -import { IssueBlockState } from "@/components/issues/board-views/block-state"; +import { IssueBlockDueDate, IssueBlockLabels, IssueBlockPriority, IssueBlockState } from "@/components/issues"; // helpers import { queryParamGenerator } from "@/helpers/query-param-generator"; // hook -import { useIssueDetails, useProject } from "@/hooks/store"; -// interfaces +import { useIssueDetails, usePublish } from "@/hooks/store"; +// types import { IIssue } from "@/types/issue"; -// store type IssueListBlockProps = { + anchor: string; issue: IIssue; - workspaceSlug: string; - projectId: string; }; -export const IssueListBlock: FC = observer((props) => { - const { workspaceSlug, projectId, issue } = props; - const searchParams = useSearchParams(); +export const IssueListLayoutBlock: FC = observer((props) => { + const { anchor, issue } = props; // query params + const searchParams = useSearchParams(); const board = searchParams.get("board") || undefined; const state = searchParams.get("state") || undefined; const priority = searchParams.get("priority") || undefined; const labels = searchParams.get("labels") || undefined; - // store - const { project } = useProject(); + // store hooks const { setPeekId } = useIssueDetails(); - // router - const router = useRouter(); + const { project_details } = usePublish(anchor); + const { queryParam } = queryParamGenerator({ board, peekId: issue.id, priority, state, labels }); const handleBlockClick = () => { setPeekId(issue.id); - - const { queryParam } = queryParamGenerator({ board, peekId: issue.id, priority, state, labels }); - router.push(`/${workspaceSlug}/${projectId}?${queryParam}`); }; return ( -
+
{/* id */}
- {project?.identifier}-{issue?.sequence_id} + {project_details?.identifier}-{issue?.sequence_id}
{/* name */} -
+
{issue.name}
@@ -84,6 +80,6 @@ export const IssueListBlock: FC = observer((props) => {
)}
-
+ ); }); diff --git a/space/components/issues/board-views/list/header.tsx b/space/components/issues/issue-layouts/list/header.tsx similarity index 54% rename from space/components/issues/board-views/list/header.tsx rename to space/components/issues/issue-layouts/list/header.tsx index 2f8f6c018..a038050a9 100644 --- a/space/components/issues/board-views/list/header.tsx +++ b/space/components/issues/issue-layouts/list/header.tsx @@ -1,20 +1,18 @@ "use client"; + +import React from "react"; import { observer } from "mobx-react-lite"; +// types +import { IStateLite } from "@plane/types"; // ui import { StateGroupIcon } from "@plane/ui"; -// constants -import { issueGroupFilter } from "@/constants/issue"; -// mobx hook -// import { useIssue } from "@/hooks/store"; -// types -import { IIssueState } from "@/types/issue"; -export const IssueListHeader = observer(({ state }: { state: IIssueState }) => { - // const { getCountOfIssuesByState } = useIssue(); - const stateGroup = issueGroupFilter(state.group); - // const count = getCountOfIssuesByState(state.id); +type Props = { + state: IStateLite; +}; - if (stateGroup === null) return <>; +export const IssueListLayoutHeader: React.FC = observer((props) => { + const { state } = props; return (
diff --git a/space/components/issues/issue-layouts/list/index.ts b/space/components/issues/issue-layouts/list/index.ts new file mode 100644 index 000000000..62874fbda --- /dev/null +++ b/space/components/issues/issue-layouts/list/index.ts @@ -0,0 +1,3 @@ +export * from "./block"; +export * from "./header"; +export * from "./root"; diff --git a/space/components/issues/issue-layouts/list/root.tsx b/space/components/issues/issue-layouts/list/root.tsx new file mode 100644 index 000000000..02cd25b40 --- /dev/null +++ b/space/components/issues/issue-layouts/list/root.tsx @@ -0,0 +1,40 @@ +"use client"; +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +// components +import { IssueListLayoutBlock, IssueListLayoutHeader } from "@/components/issues"; +// mobx hook +import { useIssue } from "@/hooks/store"; + +type Props = { + anchor: string; +}; + +export const IssuesListLayoutRoot: FC = observer((props) => { + const { anchor } = props; + // store hooks + const { states, getFilteredIssuesByState } = useIssue(); + + return ( + <> + {states?.map((state) => { + const issues = getFilteredIssuesByState(state.id); + + return ( +
+ + {issues && issues.length > 0 ? ( +
+ {issues.map((issue) => ( + + ))} +
+ ) : ( +
No issues.
+ )} +
+ ); + })} + + ); +}); diff --git a/space/components/issues/issue-layouts/properties/due-date.tsx b/space/components/issues/issue-layouts/properties/due-date.tsx new file mode 100644 index 000000000..3b73973e7 --- /dev/null +++ b/space/components/issues/issue-layouts/properties/due-date.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { CalendarCheck2 } from "lucide-react"; +// types +import { TStateGroups } from "@plane/types"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { renderFormattedDate } from "@/helpers/date-time.helper"; +import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; + +type Props = { + due_date: string; + group: TStateGroups; +}; + +export const IssueBlockDueDate = (props: Props) => { + const { due_date, group } = props; + + return ( +
+ + {renderFormattedDate(due_date)} +
+ ); +}; diff --git a/space/components/issues/issue-layouts/properties/index.ts b/space/components/issues/issue-layouts/properties/index.ts new file mode 100644 index 000000000..de78f9966 --- /dev/null +++ b/space/components/issues/issue-layouts/properties/index.ts @@ -0,0 +1,4 @@ +export * from "./due-date"; +export * from "./labels"; +export * from "./priority"; +export * from "./state"; diff --git a/space/components/issues/issue-layouts/properties/labels.tsx b/space/components/issues/issue-layouts/properties/labels.tsx new file mode 100644 index 000000000..75c32c4a0 --- /dev/null +++ b/space/components/issues/issue-layouts/properties/labels.tsx @@ -0,0 +1,17 @@ +"use client"; + +export const IssueBlockLabels = ({ labels }: any) => ( +
+ {labels?.map((_label: any) => ( +
+
+
+
{_label?.name}
+
+
+ ))} +
+); diff --git a/space/components/issues/board-views/block-priority.tsx b/space/components/issues/issue-layouts/properties/priority.tsx similarity index 85% rename from space/components/issues/board-views/block-priority.tsx rename to space/components/issues/issue-layouts/properties/priority.tsx index 3110930ec..b91d56bb8 100644 --- a/space/components/issues/board-views/block-priority.tsx +++ b/space/components/issues/issue-layouts/properties/priority.tsx @@ -1,11 +1,11 @@ "use client"; // types -import { issuePriorityFilter } from "@/constants/issue"; -import { TIssueFilterPriority } from "@/types/issue"; +import { TIssuePriorities } from "@plane/types"; // constants +import { issuePriorityFilter } from "@/constants/issue"; -export const IssueBlockPriority = ({ priority }: { priority: TIssueFilterPriority | null }) => { +export const IssueBlockPriority = ({ priority }: { priority: TIssuePriorities | null }) => { const priority_detail = priority != null ? issuePriorityFilter(priority) : null; if (priority_detail === null) return <>; diff --git a/space/components/issues/issue-layouts/properties/state.tsx b/space/components/issues/issue-layouts/properties/state.tsx new file mode 100644 index 000000000..b80f1f3df --- /dev/null +++ b/space/components/issues/issue-layouts/properties/state.tsx @@ -0,0 +1,11 @@ +// ui +import { StateGroupIcon } from "@plane/ui"; + +export const IssueBlockState = ({ state }: any) => ( +
+
+ +
{state?.name}
+
+
+); diff --git a/space/components/views/project-details.tsx b/space/components/issues/issue-layouts/root.tsx similarity index 52% rename from space/components/views/project-details.tsx rename to space/components/issues/issue-layouts/root.tsx index 462c656f0..e53986c85 100644 --- a/space/components/views/project-details.tsx +++ b/space/components/issues/issue-layouts/root.tsx @@ -6,69 +6,55 @@ import Image from "next/image"; import { useSearchParams } from "next/navigation"; import useSWR from "swr"; // components -import { IssueCalendarView } from "@/components/issues/board-views/calendar"; -import { IssueGanttView } from "@/components/issues/board-views/gantt"; -import { IssueKanbanView } from "@/components/issues/board-views/kanban"; -import { IssueListView } from "@/components/issues/board-views/list"; -import { IssueSpreadsheetView } from "@/components/issues/board-views/spreadsheet"; +import { IssueKanbanLayoutRoot, IssuesListLayoutRoot } from "@/components/issues"; import { IssueAppliedFilters } from "@/components/issues/filters/applied-filters/root"; import { IssuePeekOverview } from "@/components/issues/peek-overview"; -// mobx store -import { useIssue, useUser, useIssueDetails, useIssueFilter, useProject } from "@/hooks/store"; +// hooks +import { useIssue, useIssueDetails, useIssueFilter } from "@/hooks/store"; +// store +import { PublishStore } from "@/store/publish/publish.store"; // assets import SomethingWentWrongImage from "public/something-went-wrong.svg"; -type ProjectDetailsViewProps = { - workspaceSlug: string; - projectId: string; +type Props = { peekId: string | undefined; + publishSettings: PublishStore; }; -export const ProjectDetailsView: FC = observer((props) => { - // router - const searchParams = useSearchParams(); +export const IssuesLayoutsRoot: FC = observer((props) => { + const { peekId, publishSettings } = props; // query params + const searchParams = useSearchParams(); const states = searchParams.get("states") || undefined; const priority = searchParams.get("priority") || undefined; const labels = searchParams.get("labels") || undefined; - - const { workspaceSlug, projectId, peekId } = props; - // hooks - const { fetchProjectSettings } = useProject(); - const { issueFilters } = useIssueFilter(); + // store hooks + const { getIssueFilters } = useIssueFilter(); const { loader, issues, error, fetchPublicIssues } = useIssue(); const issueDetailStore = useIssueDetails(); - const { data: currentUser, fetchCurrentUser } = useUser(); + // derived values + const { anchor } = publishSettings; + const issueFilters = anchor ? getIssueFilters(anchor) : undefined; useSWR( - workspaceSlug && projectId ? "WORKSPACE_PROJECT_SETTINGS" : null, - workspaceSlug && projectId ? () => fetchProjectSettings(workspaceSlug, projectId) : null - ); - useSWR( - (workspaceSlug && projectId) || states || priority || labels ? "WORKSPACE_PROJECT_PUBLIC_ISSUES" : null, - (workspaceSlug && projectId) || states || priority || labels - ? () => fetchPublicIssues(workspaceSlug, projectId, { states, priority, labels }) - : null - ); - useSWR( - workspaceSlug && projectId && !currentUser ? "WORKSPACE_PROJECT_CURRENT_USER" : null, - workspaceSlug && projectId && !currentUser ? () => fetchCurrentUser() : null + anchor ? `PUBLIC_ISSUES_${anchor}` : null, + anchor ? () => fetchPublicIssues(anchor, { states, priority, labels }) : null ); useEffect(() => { - if (peekId && workspaceSlug && projectId) { + if (peekId) { issueDetailStore.setPeekId(peekId.toString()); } - }, [peekId, issueDetailStore, projectId, workspaceSlug]); + }, [peekId, issueDetailStore]); // derived values const activeLayout = issueFilters?.display_filters?.layout || undefined; + if (!anchor) return null; + return (
- {workspaceSlug && projectId && peekId && ( - - )} + {peekId && } {loader && !issues ? (
Loading...
@@ -90,21 +76,18 @@ export const ProjectDetailsView: FC = observer((props) activeLayout && (
{/* applied filters */} - + {activeLayout === "list" && (
- +
)} {activeLayout === "kanban" && (
- +
)} - {activeLayout === "calendar" && } - {activeLayout === "spreadsheet" && } - {activeLayout === "gantt" && }
) )} diff --git a/space/components/issues/navbar/controls.tsx b/space/components/issues/navbar/controls.tsx index 20c0ca408..25f2edfb0 100644 --- a/space/components/issues/navbar/controls.tsx +++ b/space/components/issues/navbar/controls.tsx @@ -4,26 +4,25 @@ import { useEffect, FC } from "react"; import { observer } from "mobx-react-lite"; import { useRouter, useSearchParams } from "next/navigation"; // components +import { IssuesLayoutSelection, NavbarTheme, UserAvatar } from "@/components/issues"; import { IssueFiltersDropdown } from "@/components/issues/filters"; -import { NavbarIssueBoardView } from "@/components/issues/navbar/issue-board-view"; -import { NavbarTheme } from "@/components/issues/navbar/theme"; -import { UserAvatar } from "@/components/issues/navbar/user-avatar"; // helpers import { queryParamGenerator } from "@/helpers/query-param-generator"; // hooks -import { useProject, useIssueFilter, useIssueDetails } from "@/hooks/store"; +import { useIssueFilter, useIssueDetails } from "@/hooks/store"; import useIsInIframe from "@/hooks/use-is-in-iframe"; +// store +import { PublishStore } from "@/store/publish/publish.store"; // types import { TIssueLayout } from "@/types/issue"; export type NavbarControlsProps = { - workspaceSlug: string; - projectId: string; + publishSettings: PublishStore; }; export const NavbarControls: FC = observer((props) => { // props - const { workspaceSlug, projectId } = props; + const { publishSettings } = props; // router const router = useRouter(); const searchParams = useSearchParams(); @@ -34,24 +33,25 @@ export const NavbarControls: FC = observer((props) => { const priority = searchParams.get("priority") || undefined; const peekId = searchParams.get("peekId") || undefined; // hooks - const { issueFilters, isIssueFiltersUpdated, initIssueFilters } = useIssueFilter(); - const { settings } = useProject(); + const { getIssueFilters, isIssueFiltersUpdated, initIssueFilters } = useIssueFilter(); const { setPeekId } = useIssueDetails(); // derived values + const { anchor, view_props, workspace_detail } = publishSettings; + const issueFilters = anchor ? getIssueFilters(anchor) : undefined; const activeLayout = issueFilters?.display_filters?.layout || undefined; const isInIframe = useIsInIframe(); useEffect(() => { - if (workspaceSlug && projectId && settings) { + if (anchor && workspace_detail) { const viewsAcceptable: string[] = []; let currentBoard: TIssueLayout | null = null; - if (settings?.views?.list) viewsAcceptable.push("list"); - if (settings?.views?.kanban) viewsAcceptable.push("kanban"); - if (settings?.views?.calendar) viewsAcceptable.push("calendar"); - if (settings?.views?.gantt) viewsAcceptable.push("gantt"); - if (settings?.views?.spreadsheet) viewsAcceptable.push("spreadsheet"); + if (view_props?.list) viewsAcceptable.push("list"); + if (view_props?.kanban) viewsAcceptable.push("kanban"); + if (view_props?.calendar) viewsAcceptable.push("calendar"); + if (view_props?.gantt) viewsAcceptable.push("gantt"); + if (view_props?.spreadsheet) viewsAcceptable.push("spreadsheet"); if (board) { if (viewsAcceptable.includes(board.toString())) currentBoard = board.toString() as TIssueLayout; @@ -74,39 +74,41 @@ export const NavbarControls: FC = observer((props) => { }, }; - if (!isIssueFiltersUpdated(params)) { - initIssueFilters(projectId, params); - router.push(`/${workspaceSlug}/${projectId}?${queryParam}`); + if (!isIssueFiltersUpdated(anchor, params)) { + initIssueFilters(anchor, params); + router.push(`/issues/${anchor}?${queryParam}`); } } } } }, [ - workspaceSlug, - projectId, + anchor, board, labels, state, priority, peekId, - settings, activeLayout, router, initIssueFilters, setPeekId, isIssueFiltersUpdated, + view_props, + workspace_detail, ]); + if (!anchor) return null; + return ( <> {/* issue views */}
- +
{/* issue filters */}
- +
{/* theming */} diff --git a/space/components/issues/navbar/index.ts b/space/components/issues/navbar/index.ts new file mode 100644 index 000000000..e1bb02d91 --- /dev/null +++ b/space/components/issues/navbar/index.ts @@ -0,0 +1,5 @@ +export * from "./controls"; +export * from "./layout-selection"; +export * from "./root"; +export * from "./theme"; +export * from "./user-avatar"; diff --git a/space/components/issues/navbar/issue-board-view.tsx b/space/components/issues/navbar/issue-board-view.tsx deleted file mode 100644 index 711229961..000000000 --- a/space/components/issues/navbar/issue-board-view.tsx +++ /dev/null @@ -1,72 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -import { useRouter, useSearchParams } from "next/navigation"; -// constants -import { issueLayoutViews } from "@/constants/issue"; -// helpers -import { queryParamGenerator } from "@/helpers/query-param-generator"; -// hooks -import { useIssueFilter } from "@/hooks/store"; -// mobx -import { TIssueLayout } from "@/types/issue"; - -type NavbarIssueBoardViewProps = { - workspaceSlug: string; - projectId: string; -}; - -export const NavbarIssueBoardView: FC = observer((props) => { - const router = useRouter(); - const searchParams = useSearchParams(); - // query params - const labels = searchParams.get("labels") || undefined; - const state = searchParams.get("state") || undefined; - const priority = searchParams.get("priority") || undefined; - const peekId = searchParams.get("peekId") || undefined; - // props - const { workspaceSlug, projectId } = props; - // hooks - const { layoutOptions, issueFilters, updateIssueFilters } = useIssueFilter(); - - // derived values - const activeLayout = issueFilters?.display_filters?.layout || undefined; - - const handleCurrentBoardView = (boardView: TIssueLayout) => { - updateIssueFilters(projectId, "display_filters", "layout", boardView); - const { queryParam } = queryParamGenerator({ board: boardView, peekId, priority, state, labels }); - router.push(`/${workspaceSlug}/${projectId}?${queryParam}`); - }; - - return ( - <> - {issueLayoutViews && - Object.keys(issueLayoutViews).map((key: string) => { - const layoutKey = key as TIssueLayout; - if (layoutOptions[layoutKey]) { - return ( -
handleCurrentBoardView(layoutKey)} - title={layoutKey} - > - - {issueLayoutViews[layoutKey]?.icon} - -
- ); - } - })} - - ); -}); diff --git a/space/components/issues/navbar/layout-selection.tsx b/space/components/issues/navbar/layout-selection.tsx new file mode 100644 index 000000000..1989710b5 --- /dev/null +++ b/space/components/issues/navbar/layout-selection.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +import { useRouter, useSearchParams } from "next/navigation"; +// ui +import { Tooltip } from "@plane/ui"; +// constants +import { ISSUE_LAYOUTS } from "@/constants/issue"; +// helpers +import { queryParamGenerator } from "@/helpers/query-param-generator"; +// hooks +import { useIssueFilter } from "@/hooks/store"; +// mobx +import { TIssueLayout } from "@/types/issue"; + +type Props = { + anchor: string; +}; + +export const IssuesLayoutSelection: FC = observer((props) => { + const { anchor } = props; + // router + const router = useRouter(); + const searchParams = useSearchParams(); + // query params + const labels = searchParams.get("labels"); + const state = searchParams.get("state"); + const priority = searchParams.get("priority"); + const peekId = searchParams.get("peekId"); + // hooks + const { layoutOptions, getIssueFilters, updateIssueFilters } = useIssueFilter(); + // derived values + const issueFilters = getIssueFilters(anchor); + const activeLayout = issueFilters?.display_filters?.layout || undefined; + + const handleCurrentBoardView = (boardView: TIssueLayout) => { + updateIssueFilters(anchor, "display_filters", "layout", boardView); + const { queryParam } = queryParamGenerator({ board: boardView, peekId, priority, state, labels }); + router.push(`/issues/${anchor}?${queryParam}`); + }; + + return ( +
+ {ISSUE_LAYOUTS.map((layout) => { + if (!layoutOptions[layout.key]) return; + + return ( + + + + ); + })} +
+ ); +}); diff --git a/space/components/issues/navbar/index.tsx b/space/components/issues/navbar/root.tsx similarity index 58% rename from space/components/issues/navbar/index.tsx rename to space/components/issues/navbar/root.tsx index f5d60b8b0..1d1a294d9 100644 --- a/space/components/issues/navbar/index.tsx +++ b/space/components/issues/navbar/root.tsx @@ -4,41 +4,40 @@ import { observer } from "mobx-react-lite"; import { Briefcase } from "lucide-react"; // components import { ProjectLogo } from "@/components/common"; -import { NavbarControls } from "@/components/issues/navbar/controls"; -// hooks -import { useProject } from "@/hooks/store"; +import { NavbarControls } from "@/components/issues"; +// store +import { PublishStore } from "@/store/publish/publish.store"; -type IssueNavbarProps = { - workspaceSlug: string; - projectId: string; +type Props = { + publishSettings: PublishStore; }; -const IssueNavbar: FC = observer((props) => { - const { workspaceSlug, projectId } = props; +export const IssuesNavbarRoot: FC = observer((props) => { + const { publishSettings } = props; // hooks - const { project } = useProject(); + const { project_details } = publishSettings; return (
{/* project detail */}
- {project ? ( + {project_details ? ( - + ) : ( )} -
{project?.name || `...`}
+
+ {project_details?.name || `...`} +
- +
); }); - -export default IssueNavbar; diff --git a/space/components/issues/peek-overview/comment/add-comment.tsx b/space/components/issues/peek-overview/comment/add-comment.tsx index a1647c9c5..57ed0b6f6 100644 --- a/space/components/issues/peek-overview/comment/add-comment.tsx +++ b/space/components/issues/peek-overview/comment/add-comment.tsx @@ -8,7 +8,7 @@ import { TOAST_TYPE, setToast } from "@plane/ui"; // editor components import { LiteTextEditor } from "@/components/editor/lite-text-editor"; // hooks -import { useIssueDetails, useProject, useUser } from "@/hooks/store"; +import { useIssueDetails, usePublish, useUser } from "@/hooks/store"; // types import { Comment } from "@/types/issue"; @@ -17,22 +17,18 @@ const defaultValues: Partial = { }; type Props = { + anchor: string; disabled?: boolean; - workspaceSlug: string; - projectId: string; }; export const AddComment: React.FC = observer((props) => { - // const { disabled = false } = props; - const { workspaceSlug, projectId } = props; + const { anchor } = props; // refs const editorRef = useRef(null); // store hooks - const { workspace } = useProject(); const { peekId: issueId, addIssueComment } = useIssueDetails(); const { data: currentUser } = useUser(); - // derived values - const workspaceId = workspace?.id; + const { workspaceSlug, workspace: workspaceID } = usePublish(anchor); // form info const { handleSubmit, @@ -43,9 +39,9 @@ export const AddComment: React.FC = observer((props) => { } = useForm({ defaultValues }); const onSubmit = async (formData: Comment) => { - if (!workspaceSlug || !projectId || !issueId || isSubmitting || !formData.comment_html) return; + if (!anchor || !issueId || isSubmitting || !formData.comment_html) return; - await addIssueComment(workspaceSlug, projectId, issueId, formData) + await addIssueComment(anchor, issueId, formData) .then(() => { reset(defaultValues); editorRef.current?.clearEditor(); @@ -71,8 +67,8 @@ export const AddComment: React.FC = observer((props) => { onEnterKeyPress={(e) => { if (currentUser) handleSubmit(onSubmit)(e); }} - workspaceId={workspaceId as string} - workspaceSlug={workspaceSlug} + workspaceId={workspaceID?.toString() ?? ""} + workspaceSlug={workspaceSlug?.toString() ?? ""} ref={editorRef} initialValue={ !value || value === "" || (typeof value === "object" && Object.keys(value).length === 0) diff --git a/space/components/issues/peek-overview/comment/comment-detail-card.tsx b/space/components/issues/peek-overview/comment/comment-detail-card.tsx index 3ede0333b..31e5f7324 100644 --- a/space/components/issues/peek-overview/comment/comment-detail-card.tsx +++ b/space/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -10,25 +10,23 @@ import { CommentReactions } from "@/components/issues/peek-overview"; // helpers import { timeAgo } from "@/helpers/date-time.helper"; // hooks -import { useIssueDetails, useProject, useUser } from "@/hooks/store"; +import { useIssueDetails, usePublish, useUser } from "@/hooks/store"; import useIsInIframe from "@/hooks/use-is-in-iframe"; // types import { Comment } from "@/types/issue"; type Props = { - workspaceSlug: string; + anchor: string; comment: Comment; }; export const CommentCard: React.FC = observer((props) => { - const { comment, workspaceSlug } = props; + const { anchor, comment } = props; // store hooks - const { workspace } = useProject(); const { peekId, deleteIssueComment, updateIssueComment } = useIssueDetails(); const { data: currentUser } = useUser(); + const { workspaceSlug, workspace: workspaceID } = usePublish(anchor); const isInIframe = useIsInIframe(); - // derived values - const workspaceId = workspace?.id; // states const [isEditing, setIsEditing] = useState(false); @@ -45,13 +43,13 @@ export const CommentCard: React.FC = observer((props) => { }); const handleDelete = () => { - if (!workspaceSlug || !peekId) return; - deleteIssueComment(workspaceSlug, comment.project, peekId, comment.id); + if (!anchor || !peekId) return; + deleteIssueComment(anchor, peekId, comment.id); }; const handleCommentUpdate = async (formData: Comment) => { - if (!workspaceSlug || !peekId) return; - updateIssueComment(workspaceSlug, comment.project, peekId, comment.id, formData); + if (!anchor || !peekId) return; + updateIssueComment(anchor, peekId, comment.id, formData); setIsEditing(false); editorRef.current?.setEditorValue(formData.comment_html); showEditorRef.current?.setEditorValue(formData.comment_html); @@ -103,8 +101,8 @@ export const CommentCard: React.FC = observer((props) => { name="comment_html" render={({ field: { onChange, value } }) => ( = observer((props) => {
- +
diff --git a/space/components/issues/peek-overview/comment/comment-reactions.tsx b/space/components/issues/peek-overview/comment/comment-reactions.tsx index ed915eff4..1b3977794 100644 --- a/space/components/issues/peek-overview/comment/comment-reactions.tsx +++ b/space/components/issues/peek-overview/comment/comment-reactions.tsx @@ -13,12 +13,12 @@ import { useIssueDetails, useUser } from "@/hooks/store"; import useIsInIframe from "@/hooks/use-is-in-iframe"; type Props = { + anchor: string; commentId: string; - projectId: string; - workspaceSlug: string; }; export const CommentReactions: React.FC = observer((props) => { + const { anchor, commentId } = props; const router = useRouter(); const pathName = usePathname(); const searchParams = useSearchParams(); @@ -28,7 +28,6 @@ export const CommentReactions: React.FC = observer((props) => { const priority = searchParams.get("priority") || undefined; const labels = searchParams.get("labels") || undefined; - const { commentId, projectId, workspaceSlug } = props; // hooks const { addCommentReaction, removeCommentReaction, details, peekId } = useIssueDetails(); const { data: user } = useUser(); @@ -40,13 +39,13 @@ export const CommentReactions: React.FC = observer((props) => { const userReactions = commentReactions?.filter((r) => r?.actor_detail?.id === user?.id); const handleAddReaction = (reactionHex: string) => { - if (!workspaceSlug || !projectId || !peekId) return; - addCommentReaction(workspaceSlug, projectId, peekId, commentId, reactionHex); + if (!anchor || !peekId) return; + addCommentReaction(anchor, peekId, commentId, reactionHex); }; const handleRemoveReaction = (reactionHex: string) => { - if (!workspaceSlug || !projectId || !peekId) return; - removeCommentReaction(workspaceSlug, projectId, peekId, commentId, reactionHex); + if (!anchor || !peekId) return; + removeCommentReaction(anchor, peekId, commentId, reactionHex); }; const handleReactionClick = (reactionHex: string) => { diff --git a/space/components/issues/peek-overview/full-screen-peek-view.tsx b/space/components/issues/peek-overview/full-screen-peek-view.tsx index f5918de43..4e9d5ed8c 100644 --- a/space/components/issues/peek-overview/full-screen-peek-view.tsx +++ b/space/components/issues/peek-overview/full-screen-peek-view.tsx @@ -11,14 +11,13 @@ import { import { IIssue } from "@/types/issue"; type Props = { + anchor: string; handleClose: () => void; issueDetails: IIssue | undefined; - workspaceSlug: string; - projectId: string; }; export const FullScreenPeekView: React.FC = observer((props) => { - const { handleClose, issueDetails, workspaceSlug, projectId } = props; + const { anchor, handleClose, issueDetails } = props; return (
@@ -30,17 +29,13 @@ export const FullScreenPeekView: React.FC = observer((props) => {
{/* issue title and description */}
- +
{/* divider */}
{/* issue activity/comments */}
- +
) : ( diff --git a/space/components/issues/peek-overview/header.tsx b/space/components/issues/peek-overview/header.tsx index 0e9b93ab9..3ad07e06b 100644 --- a/space/components/issues/peek-overview/header.tsx +++ b/space/components/issues/peek-overview/header.tsx @@ -1,10 +1,9 @@ import React from "react"; import { observer } from "mobx-react-lite"; -import { MoveRight } from "lucide-react"; +import { Link2, MoveRight } from "lucide-react"; import { Listbox, Transition } from "@headlessui/react"; // ui -import { setToast, TOAST_TYPE } from "@plane/ui"; -import { Icon } from "@/components/ui"; +import { CenterPanelIcon, FullScreenPanelIcon, setToast, SidePanelIcon, TOAST_TYPE } from "@plane/ui"; // helpers import { copyTextToClipboard } from "@/helpers/string.helper"; // hooks @@ -18,21 +17,21 @@ type Props = { issueDetails: IIssue | undefined; }; -const peekModes: { +const PEEK_MODES: { key: IPeekMode; - icon: string; + icon: any; label: string; }[] = [ - { key: "side", icon: "side_navigation", label: "Side Peek" }, + { key: "side", icon: SidePanelIcon, label: "Side Peek" }, { key: "modal", - icon: "dialogs", - label: "Modal Peek", + icon: CenterPanelIcon, + label: "Modal", }, { key: "full", - icon: "nearby", - label: "Full Screen Peek", + icon: FullScreenPanelIcon, + label: "Full Screen", }, ]; @@ -47,20 +46,22 @@ export const PeekOverviewHeader: React.FC = observer((props) => { copyTextToClipboard(urlToCopy).then(() => { setToast({ - type: TOAST_TYPE.INFO, + type: TOAST_TYPE.SUCCESS, title: "Link copied!", - message: "Issue link copied to clipboard", + message: "Issue link copied to clipboard.", }); }); }; + const Icon = PEEK_MODES.find((m) => m.key === peekMode)?.icon ?? SidePanelIcon; + return ( <>
{peekMode === "side" && ( - )} = observer((props) => { onChange={(val) => setPeekMode(val)} className="relative flex-shrink-0 text-left" > - - m.key === peekMode)?.icon ?? ""} className="text-[1rem]" /> + + = observer((props) => { >
- {peekModes.map((mode) => ( + {PEEK_MODES.map((mode) => ( = observer((props) => {
{isClipboardWriteAllowed && (peekMode === "side" || peekMode === "modal") && (
-
)} diff --git a/space/components/issues/peek-overview/issue-activity.tsx b/space/components/issues/peek-overview/issue-activity.tsx index ec73bda7b..1ccb7fa88 100644 --- a/space/components/issues/peek-overview/issue-activity.tsx +++ b/space/components/issues/peek-overview/issue-activity.tsx @@ -7,61 +7,58 @@ import { Button } from "@plane/ui"; import { CommentCard, AddComment } from "@/components/issues/peek-overview"; import { Icon } from "@/components/ui"; // hooks -import { useIssueDetails, useProject, useUser } from "@/hooks/store"; +import { useIssueDetails, usePublish, useUser } from "@/hooks/store"; import useIsInIframe from "@/hooks/use-is-in-iframe"; // types import { IIssue } from "@/types/issue"; type Props = { + anchor: string; issueDetails: IIssue; - workspaceSlug: string; - projectId: string; }; export const PeekOverviewIssueActivity: React.FC = observer((props) => { - const { workspaceSlug, projectId } = props; + const { anchor } = props; // router const pathname = usePathname(); - // store - const { canComment } = useProject(); + // store hooks const { details, peekId } = useIssueDetails(); const { data: currentUser } = useUser(); - const isInIframe = useIsInIframe(); - + const { canComment } = usePublish(anchor); + // derived values const comments = details[peekId || ""]?.comments || []; + const isInIframe = useIsInIframe(); return (

Comments

- {workspaceSlug && ( -
-
- {comments.map((comment: any) => ( - - ))} -
- {!isInIframe && - (currentUser ? ( - <> - {canComment && ( -
- -
- )} - - ) : ( -
-

- - Sign in to add your comment -

- - - -
- ))} +
+
+ {comments.map((comment) => ( + + ))}
- )} + {!isInIframe && + (currentUser ? ( + <> + {canComment && ( +
+ +
+ )} + + ) : ( +
+

+ + Sign in to add your comment +

+ + + +
+ ))} +
); }); diff --git a/space/components/issues/peek-overview/issue-details.tsx b/space/components/issues/peek-overview/issue-details.tsx index 5fe73f67a..97a659554 100644 --- a/space/components/issues/peek-overview/issue-details.tsx +++ b/space/components/issues/peek-overview/issue-details.tsx @@ -5,26 +5,33 @@ import { IssueReactions } from "@/components/issues/peek-overview"; import { IIssue } from "@/types/issue"; type Props = { + anchor: string; issueDetails: IIssue; }; -export const PeekOverviewIssueDetails: React.FC = ({ issueDetails }) => ( -
-
- {issueDetails.project_detail.identifier}-{issueDetails.sequence_id} -
-

{issueDetails.name}

- {issueDetails.description_html !== "" && issueDetails.description_html !== "

" && ( -

" - : issueDetails.description_html - } - /> - )} - -
-); +export const PeekOverviewIssueDetails: React.FC = (props) => { + const { anchor, issueDetails } = props; + + const description = issueDetails.description_html; + + return ( +
+
+ {issueDetails.project_detail?.identifier}-{issueDetails?.sequence_id} +
+

{issueDetails.name}

+ {description !== "" && description !== "

" && ( +

" + : description + } + /> + )} + +
+ ); +}; diff --git a/space/components/issues/peek-overview/issue-emoji-reactions.tsx b/space/components/issues/peek-overview/issue-emoji-reactions.tsx index 4a0e61554..ae960eab3 100644 --- a/space/components/issues/peek-overview/issue-emoji-reactions.tsx +++ b/space/components/issues/peek-overview/issue-emoji-reactions.tsx @@ -1,4 +1,3 @@ -import { useEffect } from "react"; import { observer } from "mobx-react-lite"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; // lib @@ -11,11 +10,12 @@ import { queryParamGenerator } from "@/helpers/query-param-generator"; import { useIssueDetails, useUser } from "@/hooks/store"; type IssueEmojiReactionsProps = { - workspaceSlug: string; - projectId: string; + anchor: string; }; export const IssueEmojiReactions: React.FC = observer((props) => { + const { anchor } = props; + // router const router = useRouter(); const pathName = usePathname(); const searchParams = useSearchParams(); @@ -25,11 +25,9 @@ export const IssueEmojiReactions: React.FC = observer( const state = searchParams.get("state") || undefined; const priority = searchParams.get("priority") || undefined; const labels = searchParams.get("labels") || undefined; - - const { workspaceSlug, projectId } = props; - // store + // store hooks const issueDetailsStore = useIssueDetails(); - const { data: user, fetchCurrentUser } = useUser(); + const { data: user } = useUser(); const issueId = issueDetailsStore.peekId; const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : []; @@ -38,13 +36,13 @@ export const IssueEmojiReactions: React.FC = observer( const userReactions = reactions?.filter((r) => r.actor_detail.id === user?.id); const handleAddReaction = (reactionHex: string) => { - if (!workspaceSlug || !projectId || !issueId) return; - issueDetailsStore.addIssueReaction(workspaceSlug.toString(), projectId.toString(), issueId, reactionHex); + if (!issueId) return; + issueDetailsStore.addIssueReaction(anchor, issueId, reactionHex); }; const handleRemoveReaction = (reactionHex: string) => { - if (!workspaceSlug || !projectId || !issueId) return; - issueDetailsStore.removeIssueReaction(workspaceSlug.toString(), projectId.toString(), issueId, reactionHex); + if (!issueId) return; + issueDetailsStore.removeIssueReaction(anchor, issueId, reactionHex); }; const handleReactionClick = (reactionHex: string) => { @@ -53,11 +51,6 @@ export const IssueEmojiReactions: React.FC = observer( else handleAddReaction(reactionHex); }; - useEffect(() => { - if (user) return; - fetchCurrentUser(); - }, [user, fetchCurrentUser]); - // derived values const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels }); diff --git a/space/components/issues/peek-overview/issue-properties.tsx b/space/components/issues/peek-overview/issue-properties.tsx index 08d22b312..2bdfe21bb 100644 --- a/space/components/issues/peek-overview/issue-properties.tsx +++ b/space/components/issues/peek-overview/issue-properties.tsx @@ -1,16 +1,17 @@ +import { CalendarCheck2, Signal } from "lucide-react"; // ui -import { StateGroupIcon, TOAST_TYPE, setToast } from "@plane/ui"; -// icons +import { DoubleCircleIcon, StateGroupIcon, TOAST_TYPE, setToast } from "@plane/ui"; +// components import { Icon } from "@/components/ui"; // constants -import { issueGroupFilter, issuePriorityFilter } from "@/constants/issue"; +import { issuePriorityFilter } from "@/constants/issue"; // helpers -import { renderFullDate } from "@/helpers/date-time.helper"; +import { cn } from "@/helpers/common.helper"; +import { renderFormattedDate } from "@/helpers/date-time.helper"; +import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper"; // types import { IIssue, IPeekMode } from "@/types/issue"; -// components -import { dueDateIconDetails } from "../board-views/block-due-date"; type Props = { issueDetails: IIssue; @@ -19,12 +20,9 @@ type Props = { export const PeekOverviewIssueProperties: React.FC = ({ issueDetails, mode }) => { const state = issueDetails.state_detail; - const stateGroup = issueGroupFilter(state.group); const priority = issueDetails.priority ? issuePriorityFilter(issueDetails.priority) : null; - const dueDateIcon = dueDateIconDetails(issueDetails.target_date, state.group); - const handleCopyLink = () => { const urlToCopy = window.location.href; @@ -51,28 +49,22 @@ export const PeekOverviewIssueProperties: React.FC = ({ issueDetails, mod
)} -
-
-
- - State +
+
+
+ + State
-
- {stateGroup && ( -
-
- - {addSpaceIfCamelCase(state?.name ?? "")} -
-
- )} +
+ + {addSpaceIfCamelCase(state?.name ?? "")}
-
-
- - Priority +
+
+ + Priority
= ({ issueDetails, mod
-
-
- - Due date + +
+
+ + Due date
{issueDetails.target_date ? ( -
- - {dueDateIcon.iconName} - - {renderFullDate(issueDetails.target_date)} +
+ + {renderFormattedDate(issueDetails.target_date)}
) : ( Empty diff --git a/space/components/issues/peek-overview/issue-reaction.tsx b/space/components/issues/peek-overview/issue-reaction.tsx index 87210f377..c3b580abc 100644 --- a/space/components/issues/peek-overview/issue-reaction.tsx +++ b/space/components/issues/peek-overview/issue-reaction.tsx @@ -1,33 +1,31 @@ -import { useParams } from "next/navigation"; +import { observer } from "mobx-react-lite"; import { IssueEmojiReactions, IssueVotes } from "@/components/issues/peek-overview"; -import { useProject } from "@/hooks/store"; +// hooks +import { usePublish } from "@/hooks/store"; import useIsInIframe from "@/hooks/use-is-in-iframe"; -// type IssueReactionsProps = { -// workspaceSlug: string; -// projectId: string; -// }; +type Props = { + anchor: string; +}; -export const IssueReactions: React.FC = () => { - const { workspace_slug: workspaceSlug, project_id: projectId } = useParams(); - - const { canVote, canReact } = useProject(); +export const IssueReactions: React.FC = observer((props) => { + const { anchor } = props; + // store hooks + const { canVote, canReact } = usePublish(anchor); const isInIframe = useIsInIframe(); return (
{canVote && ( - <> -
- -
- +
+ +
)} {!isInIframe && canReact && (
- +
)}
); -}; +}); diff --git a/space/components/issues/peek-overview/issue-vote-reactions.tsx b/space/components/issues/peek-overview/issue-vote-reactions.tsx index 1e565e862..6b24e5a9f 100644 --- a/space/components/issues/peek-overview/issue-vote-reactions.tsx +++ b/space/components/issues/peek-overview/issue-vote-reactions.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState } from "react"; import { observer } from "mobx-react-lite"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { Tooltip } from "@plane/ui"; @@ -12,11 +12,14 @@ import { useIssueDetails, useUser } from "@/hooks/store"; import useIsInIframe from "@/hooks/use-is-in-iframe"; type TIssueVotes = { - workspaceSlug: string; - projectId: string; + anchor: string; }; export const IssueVotes: React.FC = observer((props) => { + const { anchor } = props; + // states + const [isSubmitting, setIsSubmitting] = useState(false); + // router const router = useRouter(); const pathName = usePathname(); const searchParams = useSearchParams(); @@ -26,13 +29,9 @@ export const IssueVotes: React.FC = observer((props) => { const state = searchParams.get("state") || undefined; const priority = searchParams.get("priority") || undefined; const labels = searchParams.get("labels") || undefined; - - const { workspaceSlug, projectId } = props; - // states - const [isSubmitting, setIsSubmitting] = useState(false); - + // store hooks const issueDetailsStore = useIssueDetails(); - const { data: user, fetchCurrentUser } = useUser(); + const { data: user } = useUser(); const isInIframe = useIsInIframe(); @@ -47,28 +46,22 @@ export const IssueVotes: React.FC = observer((props) => { const isDownVotedByUser = allDownVotes?.some((vote) => vote.actor === user?.id); const handleVote = async (e: any, voteValue: 1 | -1) => { - if (!workspaceSlug || !projectId || !issueId) return; + if (!issueId) return; setIsSubmitting(true); const actionPerformed = votes?.find((vote) => vote.actor === user?.id && vote.vote === voteValue); - if (actionPerformed) - await issueDetailsStore.removeIssueVote(workspaceSlug.toString(), projectId.toString(), issueId); - else - await issueDetailsStore.addIssueVote(workspaceSlug.toString(), projectId.toString(), issueId, { + if (actionPerformed) await issueDetailsStore.removeIssueVote(anchor, issueId); + else { + await issueDetailsStore.addIssueVote(anchor, issueId, { vote: voteValue, }); + } setIsSubmitting(false); }; - useEffect(() => { - if (user) return; - - fetchCurrentUser(); - }, [user, fetchCurrentUser]); - const VOTES_LIMIT = 1000; // derived values diff --git a/space/components/issues/peek-overview/layout.tsx b/space/components/issues/peek-overview/layout.tsx index 453cc59f3..d1fe6f7aa 100644 --- a/space/components/issues/peek-overview/layout.tsx +++ b/space/components/issues/peek-overview/layout.tsx @@ -10,13 +10,12 @@ import { FullScreenPeekView, SidePeekView } from "@/components/issues/peek-overv import { useIssue, useIssueDetails } from "@/hooks/store"; type TIssuePeekOverview = { - workspaceSlug: string; - projectId: string; + anchor: string; peekId: string; }; export const IssuePeekOverview: FC = observer((props) => { - const { workspaceSlug, projectId, peekId } = props; + const { anchor, peekId } = props; const router = useRouter(); const searchParams = useSearchParams(); // query params @@ -34,21 +33,23 @@ export const IssuePeekOverview: FC = observer((props) => { const issueDetails = issueDetailStore.peekId && peekId ? issueDetailStore.details[peekId.toString()] : undefined; useEffect(() => { - if (workspaceSlug && projectId && peekId && issueStore.issues && issueStore.issues.length > 0) { + if (anchor && peekId && issueStore.issues && issueStore.issues.length > 0) { if (!issueDetails) { - issueDetailStore.fetchIssueDetails(workspaceSlug.toString(), projectId.toString(), peekId.toString()); + issueDetailStore.fetchIssueDetails(anchor, peekId.toString()); } } - }, [workspaceSlug, projectId, issueDetailStore, issueDetails, peekId, issueStore.issues]); + }, [anchor, issueDetailStore, issueDetails, peekId, issueStore.issues]); const handleClose = () => { issueDetailStore.setPeekId(null); - let queryParams: any = { board: board }; + let queryParams: any = { + board, + }; if (priority && priority.length > 0) queryParams = { ...queryParams, priority: priority }; if (state && state.length > 0) queryParams = { ...queryParams, state: state }; if (labels && labels.length > 0) queryParams = { ...queryParams, labels: labels }; queryParams = new URLSearchParams(queryParams).toString(); - router.push(`/${workspaceSlug}/${projectId}?${queryParams}`); + router.push(`/issues/${anchor}?${queryParams}`); }; useEffect(() => { @@ -80,12 +81,7 @@ export const IssuePeekOverview: FC = observer((props) => { leaveTo="translate-x-full" > - + @@ -119,20 +115,10 @@ export const IssuePeekOverview: FC = observer((props) => { }`} > {issueDetailStore.peekMode === "modal" && ( - + )} {issueDetailStore.peekMode === "full" && ( - + )}
diff --git a/space/components/issues/peek-overview/side-peek-view.tsx b/space/components/issues/peek-overview/side-peek-view.tsx index a0b544bdd..894441418 100644 --- a/space/components/issues/peek-overview/side-peek-view.tsx +++ b/space/components/issues/peek-overview/side-peek-view.tsx @@ -7,22 +7,21 @@ import { PeekOverviewIssueDetails, PeekOverviewIssueProperties, } from "@/components/issues/peek-overview"; -// hooks -import { useProject } from "@/hooks/store"; +// store hooks +import { usePublish } from "@/hooks/store"; // types import { IIssue } from "@/types/issue"; type Props = { + anchor: string; handleClose: () => void; issueDetails: IIssue | undefined; - workspaceSlug: string; - projectId: string; }; export const SidePeekView: React.FC = observer((props) => { - const { handleClose, issueDetails, workspaceSlug, projectId } = props; - - const { settings } = useProject(); + const { anchor, handleClose, issueDetails } = props; + // store hooks + const { canComment } = usePublish(anchor); return (
@@ -33,7 +32,7 @@ export const SidePeekView: React.FC = observer((props) => {
{/* issue title and description */}
- +
{/* issue properties */}
@@ -42,13 +41,9 @@ export const SidePeekView: React.FC = observer((props) => { {/* divider */}
{/* issue activity/comments */} - {settings?.comments && ( + {canComment && (
- +
)}
diff --git a/space/components/ui/dropdown.tsx b/space/components/ui/dropdown.tsx deleted file mode 100644 index 788627094..000000000 --- a/space/components/ui/dropdown.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { Fragment, useState, useRef } from "react"; -import Link from "next/link"; -import { Check, ChevronLeft } from "lucide-react"; -import { Popover, Transition } from "@headlessui/react"; -// hooks -import useOutSideClick from "hooks/use-outside-click"; - -type ItemOptionType = { - display: React.ReactNode; - as?: "button" | "link" | "div"; - href?: string; - isSelected?: boolean; - onClick?: () => void; - children?: ItemOptionType[] | null; -}; - -type DropdownItemProps = { - item: ItemOptionType; -}; - -type DropDownListProps = { - open: boolean; - handleClose?: () => void; - items: ItemOptionType[]; -}; - -type DropdownProps = { - button: React.ReactNode | (() => React.ReactNode); - items: ItemOptionType[]; -}; - -const DropdownList: React.FC = (props) => { - const { open, items, handleClose } = props; - - const ref = useRef(null); - - useOutSideClick(ref, () => { - if (handleClose) handleClose(); - }); - - return ( - - - -
- {items.map((item, index) => ( - - ))} -
-
-
-
- ); -}; - -const DropdownItem: React.FC = (props) => { - const { item } = props; - const { display, children, as: itemAs, href, onClick, isSelected } = item; - - const [open, setOpen] = useState(false); - - return ( -
- {(!itemAs || itemAs === "button" || itemAs === "div") && ( - - )} - - {itemAs === "link" && {display}} - - {children && setOpen(false)} items={children} />} -
- ); -}; - -const Dropdown: React.FC = (props) => { - const { button, items } = props; - - return ( - - {({ open }) => ( - <> - - {typeof button === "function" ? button() : button} - - - - -
- {items.map((item, index) => ( - - ))} -
-
-
- - )} -
- ); -}; - -export { Dropdown }; diff --git a/space/components/ui/index.ts b/space/components/ui/index.ts index 1e523d5dd..ccd2303c4 100644 --- a/space/components/ui/index.ts +++ b/space/components/ui/index.ts @@ -1,3 +1,2 @@ -export * from "./dropdown"; export * from "./icon"; export * from "./reaction-selector"; diff --git a/space/components/views/index.ts b/space/components/views/index.ts index 251de14e3..97ccf7649 100644 --- a/space/components/views/index.ts +++ b/space/components/views/index.ts @@ -1,2 +1 @@ export * from "./auth"; -export * from "./project-details"; diff --git a/space/constants/issue.ts b/space/constants/issue.ts index fb9c78fcd..77297946f 100644 --- a/space/constants/issue.ts +++ b/space/constants/issue.ts @@ -1,13 +1,7 @@ -// interfaces -import { - TIssueLayout, - TIssueLayoutViews, - TIssueFilterKeys, - TIssueFilterPriority, - TIssueFilterPriorityObject, - TIssueFilterState, - TIssueFilterStateObject, -} from "types/issue"; +import { Calendar, GanttChartSquare, Kanban, List, Sheet } from "lucide-react"; +// types +import { TIssuePriorities } from "@plane/types"; +import { TIssueLayout, TIssueFilterKeys, TIssueFilterPriorityObject } from "@/types/issue"; // issue filters export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { [key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]> } = { @@ -28,20 +22,18 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { [key in TIssueLayout]: Record<"f }, }; -export const issueLayoutViews: Partial = { - list: { - title: "List View", - icon: "format_list_bulleted", - className: "", - }, - kanban: { - title: "Board View", - icon: "grid_view", - className: "", - }, -}; +export const ISSUE_LAYOUTS: { + key: TIssueLayout; + title: string; + icon: any; +}[] = [ + { key: "list", title: "List", icon: List }, + { key: "kanban", title: "Kanban", icon: Kanban }, + { key: "calendar", title: "Calendar", icon: Calendar }, + { key: "spreadsheet", title: "Spreadsheet", icon: Sheet }, + { key: "gantt", title: "Gantt chart", icon: GanttChartSquare }, +]; -// issue priority filters export const issuePriorityFilters: TIssueFilterPriorityObject[] = [ { key: "urgent", @@ -75,7 +67,7 @@ export const issuePriorityFilters: TIssueFilterPriorityObject[] = [ }, ]; -export const issuePriorityFilter = (priorityKey: TIssueFilterPriority): TIssueFilterPriorityObject | undefined => { +export const issuePriorityFilter = (priorityKey: TIssuePriorities): TIssueFilterPriorityObject | undefined => { const currentIssuePriority: TIssueFilterPriorityObject | undefined = issuePriorityFilters && issuePriorityFilters.length > 0 ? issuePriorityFilters.find((_priority) => _priority.key === priorityKey) @@ -84,55 +76,3 @@ export const issuePriorityFilter = (priorityKey: TIssueFilterPriority): TIssueFi if (currentIssuePriority) return currentIssuePriority; return undefined; }; - -// issue group filters -export const issueGroupColors: { - [key in TIssueFilterState]: string; -} = { - backlog: "#d9d9d9", - unstarted: "#3f76ff", - started: "#f59e0b", - completed: "#16a34a", - cancelled: "#dc2626", -}; - -export const issueGroups: TIssueFilterStateObject[] = [ - { - key: "backlog", - title: "Backlog", - color: "#d9d9d9", - className: `text-[#d9d9d9] bg-[#d9d9d9]/10`, - }, - { - key: "unstarted", - title: "Unstarted", - color: "#3f76ff", - className: `text-[#3f76ff] bg-[#3f76ff]/10`, - }, - { - key: "started", - title: "Started", - color: "#f59e0b", - className: `text-[#f59e0b] bg-[#f59e0b]/10`, - }, - { - key: "completed", - title: "Completed", - color: "#16a34a", - className: `text-[#16a34a] bg-[#16a34a]/10`, - }, - { - key: "cancelled", - title: "Cancelled", - color: "#dc2626", - className: `text-[#dc2626] bg-[#dc2626]/10`, - }, -]; - -export const issueGroupFilter = (issueKey: TIssueFilterState): TIssueFilterStateObject | undefined => { - const currentIssueStateGroup: TIssueFilterStateObject | undefined = - issueGroups && issueGroups.length > 0 ? issueGroups.find((group) => group.key === issueKey) : undefined; - - if (currentIssueStateGroup) return currentIssueStateGroup; - return undefined; -}; diff --git a/space/constants/state.ts b/space/constants/state.ts new file mode 100644 index 000000000..b0fd622be --- /dev/null +++ b/space/constants/state.ts @@ -0,0 +1,37 @@ +import { TStateGroups } from "@plane/types"; + +export const STATE_GROUPS: { + [key in TStateGroups]: { + key: TStateGroups; + label: string; + color: string; + }; +} = { + backlog: { + key: "backlog", + label: "Backlog", + color: "#d9d9d9", + }, + unstarted: { + key: "unstarted", + label: "Unstarted", + color: "#3f76ff", + }, + started: { + key: "started", + label: "Started", + color: "#f59e0b", + }, + completed: { + key: "completed", + label: "Completed", + color: "#16a34a", + }, + cancelled: { + key: "cancelled", + label: "Canceled", + color: "#dc2626", + }, +}; + +export const ARCHIVABLE_STATE_GROUPS = [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key]; diff --git a/space/constants/workspace.ts b/space/constants/workspace.ts deleted file mode 100644 index 5ae5a7cf4..000000000 --- a/space/constants/workspace.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const USER_ROLES = [ - { value: "Product / Project Manager", label: "Product / Project Manager" }, - { value: "Development / Engineering", label: "Development / Engineering" }, - { value: "Founder / Executive", label: "Founder / Executive" }, - { value: "Freelancer / Consultant", label: "Freelancer / Consultant" }, - { value: "Marketing / Growth", label: "Marketing / Growth" }, - { value: "Sales / Business Development", label: "Sales / Business Development" }, - { value: "Support / Operations", label: "Support / Operations" }, - { value: "Student / Professor", label: "Student / Professor" }, - { value: "Human Resources", label: "Human Resources" }, - { value: "Other", label: "Other" }, -]; diff --git a/space/helpers/date-time.helper.ts b/space/helpers/date-time.helper.ts index f19a5358b..3930bcb83 100644 --- a/space/helpers/date-time.helper.ts +++ b/space/helpers/date-time.helper.ts @@ -1,3 +1,6 @@ +import { format, isValid } from "date-fns"; +import isNumber from "lodash/isNumber"; + export const timeAgo = (time: any) => { switch (typeof time) { case "number": @@ -14,24 +17,43 @@ export const timeAgo = (time: any) => { }; /** - * @description Returns date and month, if date is of the current year - * @description Returns date, month adn year, if date is of a different year than current - * @param {string} date - * @example renderFullDate("2023-01-01") // 1 Jan - * @example renderFullDate("2021-01-01") // 1 Jan, 2021 + * This method returns a date from string of type yyyy-mm-dd + * This method is recommended to use instead of new Date() as this does not introduce any timezone offsets + * @param date + * @returns date or undefined */ +export const getDate = (date: string | Date | undefined | null): Date | undefined => { + try { + if (!date || date === "") return; -export const renderFullDate = (date: string): string => { - if (!date) return ""; + if (typeof date !== "string" && !(date instanceof String)) return date; - const months: string[] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + const [yearString, monthString, dayString] = date.substring(0, 10).split("-"); + const year = parseInt(yearString); + const month = parseInt(monthString); + const day = parseInt(dayString); + if (!isNumber(year) || !isNumber(month) || !isNumber(day)) return; - const currentDate: Date = new Date(); - const [year, month, day]: number[] = date.split("-").map(Number); - - const formattedMonth: string = months[month - 1]; - const formattedDay: string = day < 10 ? `0${day}` : day.toString(); - - if (currentDate.getFullYear() === year) return `${formattedDay} ${formattedMonth}`; - else return `${formattedDay} ${formattedMonth}, ${year}`; + return new Date(year, month - 1, day); + } catch (e) { + return undefined; + } +}; + +/** + * @returns {string | null} formatted date in the format of MMM dd, yyyy + * @description Returns date in the formatted format + * @param {Date | string} date + * @example renderFormattedDate("2024-01-01") // Jan 01, 2024 + */ +export const renderFormattedDate = (date: string | Date | undefined | null): string | null => { + // Parse the date to check if it is valid + const parsedDate = getDate(date); + // return if undefined + if (!parsedDate) return null; + // Check if the parsed date is valid before formatting + if (!isValid(parsedDate)) return null; // Return null for invalid dates + // Format the date in format (MMM dd, yyyy) + const formattedDate = format(parsedDate, "MMM dd, yyyy"); + return formattedDate; }; diff --git a/space/helpers/emoji.helper.tsx b/space/helpers/emoji.helper.tsx index 7c9f3cfcb..d5f9d1b5a 100644 --- a/space/helpers/emoji.helper.tsx +++ b/space/helpers/emoji.helper.tsx @@ -1,23 +1,3 @@ -export const getRandomEmoji = () => { - const emojis = [ - "8986", - "9200", - "128204", - "127773", - "127891", - "127947", - "128076", - "128077", - "128187", - "128188", - "128512", - "128522", - "128578", - ]; - - return emojis[Math.floor(Math.random() * emojis.length)]; -}; - export const renderEmoji = ( emoji: | string diff --git a/space/helpers/issue.helper.ts b/space/helpers/issue.helper.ts new file mode 100644 index 000000000..a5159edef --- /dev/null +++ b/space/helpers/issue.helper.ts @@ -0,0 +1,30 @@ +import { differenceInCalendarDays } from "date-fns"; +// types +import { TStateGroups } from "@plane/types"; +// constants +import { STATE_GROUPS } from "@/constants/state"; +// helpers +import { getDate } from "@/helpers/date-time.helper"; + +/** + * @description check if the issue due date should be highlighted + * @param date + * @param stateGroup + * @returns boolean + */ +export const shouldHighlightIssueDueDate = ( + date: string | Date | null, + stateGroup: TStateGroups | undefined +): boolean => { + if (!date || !stateGroup) return false; + // if the issue is completed or cancelled, don't highlight the due date + if ([STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateGroup)) return false; + + const parsedDate = getDate(date); + if (!parsedDate) return false; + + const targetDateDistance = differenceInCalendarDays(parsedDate, new Date()); + + // if the issue is overdue, highlight the due date + return targetDateDistance <= 0; +}; diff --git a/space/helpers/string.helper.ts b/space/helpers/string.helper.ts index 525a9fc99..f6319bc75 100644 --- a/space/helpers/string.helper.ts +++ b/space/helpers/string.helper.ts @@ -3,7 +3,7 @@ import DOMPurify from "dompurify"; export const addSpaceIfCamelCase = (str: string) => str.replace(/([a-z])([A-Z])/g, "$1 $2"); const fallbackCopyTextToClipboard = (text: string) => { - var textArea = document.createElement("textarea"); + const textArea = document.createElement("textarea"); textArea.value = text; // Avoid scrolling to bottom @@ -18,7 +18,7 @@ const fallbackCopyTextToClipboard = (text: string) => { try { // FIXME: Even though we are using this as a fallback, execCommand is deprecated 👎. We should find a better way to do this. // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand - var successful = document.execCommand("copy"); + document.execCommand("copy"); } catch (err) {} document.body.removeChild(textArea); diff --git a/space/hooks/store/index.ts b/space/hooks/store/index.ts index 76b6f9315..3f82613d5 100644 --- a/space/hooks/store/index.ts +++ b/space/hooks/store/index.ts @@ -1,5 +1,5 @@ +export * from "./publish"; export * from "./use-instance"; -export * from "./use-project"; export * from "./use-issue"; export * from "./use-user"; export * from "./use-user-profile"; diff --git a/space/hooks/store/publish/index.ts b/space/hooks/store/publish/index.ts new file mode 100644 index 000000000..a7b42ad5b --- /dev/null +++ b/space/hooks/store/publish/index.ts @@ -0,0 +1,2 @@ +export * from "./use-publish-list"; +export * from "./use-publish"; diff --git a/space/hooks/store/publish/use-publish-list.ts b/space/hooks/store/publish/use-publish-list.ts new file mode 100644 index 000000000..aa50c295a --- /dev/null +++ b/space/hooks/store/publish/use-publish-list.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// lib +import { StoreContext } from "@/lib/store-provider"; +// store +import { IPublishListStore } from "@/store/publish/publish_list.store"; + +export const usePublishList = (): IPublishListStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("usePublishList must be used within StoreProvider"); + return context.publishList; +}; diff --git a/space/hooks/store/publish/use-publish.ts b/space/hooks/store/publish/use-publish.ts new file mode 100644 index 000000000..3d920e8cb --- /dev/null +++ b/space/hooks/store/publish/use-publish.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// lib +import { StoreContext } from "@/lib/store-provider"; +// store +import { PublishStore } from "@/store/publish/publish.store"; + +export const usePublish = (anchor: string): PublishStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("usePublish must be used within StoreProvider"); + return context.publishList.publishMap?.[anchor] ?? {}; +}; diff --git a/space/hooks/store/use-project.ts b/space/hooks/store/use-project.ts deleted file mode 100644 index cd3e28958..000000000 --- a/space/hooks/store/use-project.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from "react"; -// lib -import { StoreContext } from "@/lib/store-provider"; -// store -import { IProjectStore } from "@/store/project.store"; - -export const useProject = (): IProjectStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider"); - return context.project; -}; diff --git a/space/hooks/use-mention.tsx b/space/hooks/use-mention.tsx index 8b2d69720..9e33f7d90 100644 --- a/space/hooks/use-mention.tsx +++ b/space/hooks/use-mention.tsx @@ -1,7 +1,9 @@ import { useRef, useEffect } from "react"; import useSWR from "swr"; +// types import { IUser } from "@plane/types"; -import { UserService } from "services/user.service"; +// services +import { UserService } from "@/services/user.service"; export const useMention = () => { const userService = new UserService(); diff --git a/space/lib/user-provider.tsx b/space/lib/user-provider.tsx deleted file mode 100644 index 1ac1c786c..000000000 --- a/space/lib/user-provider.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { ReactNode } from "react"; -import { observer } from "mobx-react-lite"; -import useSWR from "swr"; -import { useUser } from "@/hooks/store"; - -export const UserProvider = observer(({ children }: { children: ReactNode }) => { - const { fetchCurrentUser } = useUser(); - - useSWR("CURRENT_USER", () => fetchCurrentUser()); - - return <>{children}; -}); diff --git a/space/package.json b/space/package.json index e3dadbff8..0932d7abf 100644 --- a/space/package.json +++ b/space/package.json @@ -26,6 +26,7 @@ "@sentry/nextjs": "^8", "axios": "^1.3.4", "clsx": "^2.0.0", + "date-fns": "^3.6.0", "dompurify": "^3.0.11", "dotenv": "^16.3.1", "js-cookie": "^3.0.1", diff --git a/space/services/file.service.ts b/space/services/file.service.ts index 0e277af1e..9fe06cd36 100644 --- a/space/services/file.service.ts +++ b/space/services/file.service.ts @@ -4,30 +4,6 @@ import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; -interface UnSplashImage { - id: string; - created_at: Date; - updated_at: Date; - promoted_at: Date; - width: number; - height: number; - color: string; - blur_hash: string; - description: null; - alt_description: string; - urls: UnSplashImageUrls; - [key: string]: any; -} - -interface UnSplashImageUrls { - raw: string; - full: string; - regular: string; - small: string; - thumb: string; - small_s3: string; -} - class FileService extends APIService { private cancelSource: any; @@ -123,40 +99,6 @@ class FileService extends APIService { throw error?.response?.data; }); } - - async deleteFile(workspaceId: string, assetUrl: string): Promise { - const lastIndex = assetUrl.lastIndexOf("/"); - const assetId = assetUrl.substring(lastIndex + 1); - - return this.delete(`/api/workspaces/file-assets/${workspaceId}/${assetId}/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async uploadUserFile(file: FormData): Promise { - return this.post(`/api/users/file-assets/`, file, { - headers: { - "Content-Type": "multipart/form-data", - }, - }) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async deleteUserFile(assetUrl: string): Promise { - const lastIndex = assetUrl.lastIndexOf("/"); - const assetId = assetUrl.substring(lastIndex + 1); - - return this.delete(`/api/users/file-assets/${assetId}`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } } const fileService = new FileService(); diff --git a/space/services/issue.service.ts b/space/services/issue.service.ts index 1913b678e..f86481812 100644 --- a/space/services/issue.service.ts +++ b/space/services/issue.service.ts @@ -1,14 +1,16 @@ import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; +// types +import { TIssuesResponse } from "@/types/issue"; class IssueService extends APIService { constructor() { super(API_BASE_URL); } - async getPublicIssues(workspace_slug: string, project_slug: string, params: any): Promise { - return this.get(`/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/issues/`, { + async fetchPublicIssues(anchor: string, params: any): Promise { + return this.get(`/api/public/anchor/${anchor}/issues/`, { params, }) .then((response) => response?.data) @@ -17,115 +19,88 @@ class IssueService extends APIService { }); } - async getIssueById(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/`) + async getIssueById(anchor: string, issueID: string): Promise { + return this.get(`/api/public/anchor/${anchor}/issues/${issueID}/`) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async getIssueVotes(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/votes/`) + async getIssueVotes(anchor: string, issueID: string): Promise { + return this.get(`/api/public/anchor/${anchor}/issues/${issueID}/votes/`) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async createIssueVote(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise { - return this.post( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/votes/`, - data - ) + async createIssueVote(anchor: string, issueID: string, data: any): Promise { + return this.post(`/api/public/anchor/${anchor}/issues/${issueID}/votes/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async deleteIssueVote(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.delete(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/votes/`) + async deleteIssueVote(anchor: string, issueID: string): Promise { + return this.delete(`/api/public/anchor/${anchor}/issues/${issueID}/votes/`) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async getIssueReactions(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/reactions/`) + async getIssueReactions(anchor: string, issueID: string): Promise { + return this.get(`/api/public/anchor/${anchor}/issues/${issueID}/reactions/`) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async createIssueReaction(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise { - return this.post( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/reactions/`, - data - ) + async createIssueReaction(anchor: string, issueID: string, data: any): Promise { + return this.post(`/api/public/anchor/${anchor}/issues/${issueID}/reactions/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async deleteIssueReaction( - workspaceSlug: string, - projectId: string, - issueId: string, - reactionId: string - ): Promise { - return this.delete( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/reactions/${reactionId}/` - ) + async deleteIssueReaction(anchor: string, issueID: string, reactionId: string): Promise { + return this.delete(`/api/public/anchor/${anchor}/issues/${issueID}/reactions/${reactionId}/`) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async getIssueComments(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`) + async getIssueComments(anchor: string, issueID: string): Promise { + return this.get(`/api/public/anchor/${anchor}/issues/${issueID}/comments/`) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async createIssueComment(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise { - return this.post( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`, - data - ) + async createIssueComment(anchor: string, issueID: string, data: any): Promise { + return this.post(`/api/public/anchor/${anchor}/issues/${issueID}/comments/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async updateIssueComment( - workspaceSlug: string, - projectId: string, - issueId: string, - commentId: string, - data: any - ): Promise { - return this.patch( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/${commentId}/`, - data - ) + async updateIssueComment(anchor: string, issueID: string, commentId: string, data: any): Promise { + return this.patch(`/api/public/anchor/${anchor}/issues/${issueID}/comments/${commentId}/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async deleteIssueComment(workspaceSlug: string, projectId: string, issueId: string, commentId: string): Promise { - return this.delete( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/${commentId}/` - ) + async deleteIssueComment(anchor: string, issueID: string, commentId: string): Promise { + return this.delete(`/api/public/anchor/${anchor}/issues/${issueID}/comments/${commentId}/`) .then((response) => response?.data) .catch((error) => { throw error?.response; @@ -133,32 +108,21 @@ class IssueService extends APIService { } async createCommentReaction( - workspaceSlug: string, - projectId: string, + anchor: string, commentId: string, data: { reaction: string; } ): Promise { - return this.post( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/`, - data - ) + return this.post(`/api/public/anchor/${anchor}/comments/${commentId}/reactions/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async deleteCommentReaction( - workspaceSlug: string, - projectId: string, - commentId: string, - reactionHex: string - ): Promise { - return this.delete( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/${reactionHex}/` - ) + async deleteCommentReaction(anchor: string, commentId: string, reactionHex: string): Promise { + return this.delete(`/api/public/anchor/${anchor}/comments/${commentId}/reactions/${reactionHex}/`) .then((response) => response?.data) .catch((error) => { throw error?.response; diff --git a/space/services/project-member.service.ts b/space/services/project-member.service.ts index 264d53386..722380efa 100644 --- a/space/services/project-member.service.ts +++ b/space/services/project-member.service.ts @@ -9,16 +9,16 @@ export class ProjectMemberService extends APIService { super(API_BASE_URL); } - async fetchProjectMembers(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`) + async fetchProjectMembers(anchor: string): Promise { + return this.get(`/api/anchor/${anchor}/members/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; }); } - async getProjectMember(workspaceSlug: string, projectId: string, memberId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`) + async getProjectMember(anchor: string, memberID: string): Promise { + return this.get(`/api/anchor/${anchor}/members/${memberID}/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/space/services/project.service.ts b/space/services/project.service.ts deleted file mode 100644 index 14ed7837b..000000000 --- a/space/services/project.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { API_BASE_URL } from "@/helpers/common.helper"; -// services -import { APIService } from "@/services/api.service"; - -class ProjectService extends APIService { - constructor() { - super(API_BASE_URL); - } - - async getProjectSettings(workspace_slug: string, project_slug: string): Promise { - return this.get(`/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/settings/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response; - }); - } -} - -export default ProjectService; diff --git a/space/services/publish.service.ts b/space/services/publish.service.ts new file mode 100644 index 000000000..c0bd3d8de --- /dev/null +++ b/space/services/publish.service.ts @@ -0,0 +1,29 @@ +import { API_BASE_URL } from "@/helpers/common.helper"; +// services +import { APIService } from "@/services/api.service"; +// types +import { TPublishSettings } from "@/types/publish"; + +class PublishService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async fetchPublishSettings(anchor: string): Promise { + return this.get(`/api/public/anchor/${anchor}/settings/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async fetchAnchorFromProjectDetails(workspaceSlug: string, projectID: string): Promise { + return this.get(`/api/public/workspaces/${workspaceSlug}/projects/${projectID}/anchor/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } +} + +export default PublishService; diff --git a/space/store/issue-detail.store.ts b/space/store/issue-detail.store.ts index 03f611cc0..672fe29ad 100644 --- a/space/store/issue-detail.store.ts +++ b/space/store/issue-detail.store.ts @@ -10,108 +10,102 @@ import { IIssue, IPeekMode, IVote } from "@/types/issue"; export interface IIssueDetailStore { loader: boolean; error: any; - // peek info + // observables peekId: string | null; peekMode: IPeekMode; details: { [key: string]: IIssue; }; - // peek actions - setPeekId: (issueId: string | null) => void; + // actions + setPeekId: (issueID: string | null) => void; setPeekMode: (mode: IPeekMode) => void; - // issue details - fetchIssueDetails: (workspaceId: string, projectId: string, issueId: string) => void; - // issue comments - addIssueComment: (workspaceId: string, projectId: string, issueId: string, data: any) => Promise; - updateIssueComment: ( - workspaceId: string, - projectId: string, - issueId: string, - comment_id: string, - data: any - ) => Promise; - deleteIssueComment: (workspaceId: string, projectId: string, issueId: string, comment_id: string) => void; - addCommentReaction: ( - workspaceId: string, - projectId: string, - issueId: string, - commentId: string, - reactionHex: string - ) => void; - removeCommentReaction: ( - workspaceId: string, - projectId: string, - issueId: string, - commentId: string, - reactionHex: string - ) => void; - // issue reactions - addIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void; - removeIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void; - // issue votes - addIssueVote: (workspaceId: string, projectId: string, issueId: string, data: { vote: 1 | -1 }) => Promise; - removeIssueVote: (workspaceId: string, projectId: string, issueId: string) => Promise; + // issue actions + fetchIssueDetails: (anchor: string, issueID: string) => void; + // comment actions + addIssueComment: (anchor: string, issueID: string, data: any) => Promise; + updateIssueComment: (anchor: string, issueID: string, commentID: string, data: any) => Promise; + deleteIssueComment: (anchor: string, issueID: string, commentID: string) => void; + addCommentReaction: (anchor: string, issueID: string, commentID: string, reactionHex: string) => void; + removeCommentReaction: (anchor: string, issueID: string, commentID: string, reactionHex: string) => void; + // reaction actions + addIssueReaction: (anchor: string, issueID: string, reactionHex: string) => void; + removeIssueReaction: (anchor: string, issueID: string, reactionHex: string) => void; + // vote actions + addIssueVote: (anchor: string, issueID: string, data: { vote: 1 | -1 }) => Promise; + removeIssueVote: (anchor: string, issueID: string) => Promise; } export class IssueDetailStore implements IIssueDetailStore { loader: boolean = false; error: any = null; + // observables peekId: string | null = null; peekMode: IPeekMode = "side"; details: { [key: string]: IIssue; } = {}; - issueService; + // root store rootStore: RootStore; + // services + issueService: IssueService; constructor(_rootStore: RootStore) { makeObservable(this, { loader: observable.ref, error: observable.ref, - // peek + // observables peekId: observable.ref, peekMode: observable.ref, - details: observable.ref, + details: observable, // actions setPeekId: action, setPeekMode: action, + // issue actions fetchIssueDetails: action, + // comment actions addIssueComment: action, updateIssueComment: action, deleteIssueComment: action, addCommentReaction: action, removeCommentReaction: action, + // reaction actions addIssueReaction: action, removeIssueReaction: action, + // vote actions addIssueVote: action, removeIssueVote: action, }); - this.issueService = new IssueService(); this.rootStore = _rootStore; + this.issueService = new IssueService(); } - setPeekId = (issueId: string | null) => { - this.peekId = issueId; + setPeekId = (issueID: string | null) => { + this.peekId = issueID; }; setPeekMode = (mode: IPeekMode) => { this.peekMode = mode; }; - fetchIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => { + /** + * @description fetc + * @param {string} anchor + * @param {string} issueID + */ + fetchIssueDetails = async (anchor: string, issueID: string) => { try { this.loader = true; this.error = null; - const issueDetails = this.rootStore.issue.issues?.find((i) => i.id === issueId); - const commentsResponse = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId); + const issueDetails = this.rootStore.issue.issues?.find((i) => i.id === issueID); + const commentsResponse = await this.issueService.getIssueComments(anchor, issueID); if (issueDetails) { runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...(this.details[issueId] ?? issueDetails), + [issueID]: { + ...(this.details[issueID] ?? issueDetails), comments: commentsResponse, }, }; @@ -123,17 +117,17 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - addIssueComment = async (workspaceSlug: string, projectId: string, issueId: string, data: any) => { + addIssueComment = async (anchor: string, issueID: string, data: any) => { try { - const issueDetails = this.rootStore.issue.issues?.find((i) => i.id === issueId); - const issueCommentResponse = await this.issueService.createIssueComment(workspaceSlug, projectId, issueId, data); + const issueDetails = this.rootStore.issue.issues?.find((i) => i.id === issueID); + const issueCommentResponse = await this.issueService.createIssueComment(anchor, issueID, data); if (issueDetails) { runInAction(() => { this.details = { ...this.details, - [issueId]: { + [issueID]: { ...issueDetails, - comments: [...this.details[issueId].comments, issueCommentResponse], + comments: [...this.details[issueID].comments, issueCommentResponse], }, }; }); @@ -145,36 +139,30 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - updateIssueComment = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - commentId: string, - data: any - ) => { + updateIssueComment = async (anchor: string, issueID: string, commentID: string, data: any) => { try { runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], - comments: this.details[issueId].comments.map((c) => ({ + [issueID]: { + ...this.details[issueID], + comments: this.details[issueID].comments.map((c) => ({ ...c, - ...(c.id === commentId ? data : {}), + ...(c.id === commentID ? data : {}), })), }, }; }); - await this.issueService.updateIssueComment(workspaceSlug, projectId, issueId, commentId, data); + await this.issueService.updateIssueComment(anchor, issueID, commentID, data); } catch (error) { - const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId); + const issueComments = await this.issueService.getIssueComments(anchor, issueID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], comments: issueComments, }, }; @@ -182,15 +170,15 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - deleteIssueComment = async (workspaceSlug: string, projectId: string, issueId: string, comment_id: string) => { + deleteIssueComment = async (anchor: string, issueID: string, commentID: string) => { try { - await this.issueService.deleteIssueComment(workspaceSlug, projectId, issueId, comment_id); - const remainingComments = this.details[issueId].comments.filter((c) => c.id != comment_id); + await this.issueService.deleteIssueComment(anchor, issueID, commentID); + const remainingComments = this.details[issueID].comments.filter((c) => c.id != commentID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], comments: remainingComments, }, }; @@ -200,47 +188,41 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - addCommentReaction = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - commentId: string, - reactionHex: string - ) => { + addCommentReaction = async (anchor: string, issueID: string, commentID: string, reactionHex: string) => { const newReaction = { id: uuidv4(), - comment: commentId, + comment: commentID, reaction: reactionHex, actor_detail: this.rootStore.user.currentActor, }; - const newComments = this.details[issueId].comments.map((comment) => ({ + const newComments = this.details[issueID].comments.map((comment) => ({ ...comment, comment_reactions: - comment.id === commentId ? [...comment.comment_reactions, newReaction] : comment.comment_reactions, + comment.id === commentID ? [...comment.comment_reactions, newReaction] : comment.comment_reactions, })); try { runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], comments: [...newComments], }, }; }); - await this.issueService.createCommentReaction(workspaceSlug, projectId, commentId, { + await this.issueService.createCommentReaction(anchor, commentID, { reaction: reactionHex, }); } catch (error) { - const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId); + const issueComments = await this.issueService.getIssueComments(anchor, issueID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], comments: issueComments, }, }; @@ -248,39 +230,33 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - removeCommentReaction = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - commentId: string, - reactionHex: string - ) => { + removeCommentReaction = async (anchor: string, issueID: string, commentID: string, reactionHex: string) => { try { - const comment = this.details[issueId].comments.find((c) => c.id === commentId); + const comment = this.details[issueID].comments.find((c) => c.id === commentID); const newCommentReactions = comment?.comment_reactions.filter((r) => r.reaction !== reactionHex) ?? []; runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], - comments: this.details[issueId].comments.map((c) => ({ + [issueID]: { + ...this.details[issueID], + comments: this.details[issueID].comments.map((c) => ({ ...c, - comment_reactions: c.id === commentId ? newCommentReactions : c.comment_reactions, + comment_reactions: c.id === commentID ? newCommentReactions : c.comment_reactions, })), }, }; }); - await this.issueService.deleteCommentReaction(workspaceSlug, projectId, commentId, reactionHex); + await this.issueService.deleteCommentReaction(anchor, commentID, reactionHex); } catch (error) { - const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId); + const issueComments = await this.issueService.getIssueComments(anchor, issueID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], comments: issueComments, }, }; @@ -288,18 +264,18 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - addIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => { + addIssueReaction = async (anchor: string, issueID: string, reactionHex: string) => { try { runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], reactions: [ - ...this.details[issueId].reactions, + ...this.details[issueID].reactions, { id: uuidv4(), - issue: issueId, + issue: issueID, reaction: reactionHex, actor_detail: this.rootStore.user.currentActor, }, @@ -308,17 +284,17 @@ export class IssueDetailStore implements IIssueDetailStore { }; }); - await this.issueService.createIssueReaction(workspaceSlug, projectId, issueId, { + await this.issueService.createIssueReaction(anchor, issueID, { reaction: reactionHex, }); } catch (error) { console.log("Failed to add issue vote"); - const issueReactions = await this.issueService.getIssueReactions(workspaceSlug, projectId, issueId); + const issueReactions = await this.issueService.getIssueReactions(anchor, issueID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], reactions: issueReactions, }, }; @@ -326,31 +302,31 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - removeIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => { + removeIssueReaction = async (anchor: string, issueID: string, reactionHex: string) => { try { - const newReactions = this.details[issueId].reactions.filter( + const newReactions = this.details[issueID].reactions.filter( (_r) => !(_r.reaction === reactionHex && _r.actor_detail.id === this.rootStore.user.data?.id) ); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], reactions: newReactions, }, }; }); - await this.issueService.deleteIssueReaction(workspaceSlug, projectId, issueId, reactionHex); + await this.issueService.deleteIssueReaction(anchor, issueID, reactionHex); } catch (error) { console.log("Failed to remove issue reaction"); - const reactions = await this.issueService.getIssueReactions(workspaceSlug, projectId, issueId); + const reactions = await this.issueService.getIssueReactions(anchor, issueID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], reactions: reactions, }, }; @@ -358,39 +334,44 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - addIssueVote = async (workspaceSlug: string, projectId: string, issueId: string, data: { vote: 1 | -1 }) => { + addIssueVote = async (anchor: string, issueID: string, data: { vote: 1 | -1 }) => { + const publishSettings = this.rootStore.publishList?.publishMap?.[anchor]; + const projectID = publishSettings?.project; + const workspaceSlug = publishSettings?.workspace_detail?.slug; + if (!projectID || !workspaceSlug) throw new Error("Publish settings not found"); + const newVote: IVote = { actor: this.rootStore.user.data?.id ?? "", actor_detail: this.rootStore.user.currentActor, - issue: issueId, - project: projectId, + issue: issueID, + project: projectID, workspace: workspaceSlug, vote: data.vote, }; - const filteredVotes = this.details[issueId].votes.filter((v) => v.actor !== this.rootStore.user.data?.id); + const filteredVotes = this.details[issueID].votes.filter((v) => v.actor !== this.rootStore.user.data?.id); try { runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], votes: [...filteredVotes, newVote], }, }; }); - await this.issueService.createIssueVote(workspaceSlug, projectId, issueId, data); + await this.issueService.createIssueVote(anchor, issueID, data); } catch (error) { console.log("Failed to add issue vote"); - const issueVotes = await this.issueService.getIssueVotes(workspaceSlug, projectId, issueId); + const issueVotes = await this.issueService.getIssueVotes(anchor, issueID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], votes: issueVotes, }, }; @@ -398,30 +379,30 @@ export class IssueDetailStore implements IIssueDetailStore { } }; - removeIssueVote = async (workspaceSlug: string, projectId: string, issueId: string) => { - const newVotes = this.details[issueId].votes.filter((v) => v.actor !== this.rootStore.user.data?.id); + removeIssueVote = async (anchor: string, issueID: string) => { + const newVotes = this.details[issueID].votes.filter((v) => v.actor !== this.rootStore.user.data?.id); try { runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], votes: newVotes, }, }; }); - await this.issueService.deleteIssueVote(workspaceSlug, projectId, issueId); + await this.issueService.deleteIssueVote(anchor, issueID); } catch (error) { console.log("Failed to remove issue vote"); - const issueVotes = await this.issueService.getIssueVotes(workspaceSlug, projectId, issueId); + const issueVotes = await this.issueService.getIssueVotes(anchor, issueID); runInAction(() => { this.details = { ...this.details, - [issueId]: { - ...this.details[issueId], + [issueID]: { + ...this.details[issueID], votes: issueVotes, }, }; diff --git a/space/store/issue-filters.store.ts b/space/store/issue-filters.store.ts index b7b311af4..daf797f90 100644 --- a/space/store/issue-filters.store.ts +++ b/space/store/issue-filters.store.ts @@ -1,7 +1,7 @@ import cloneDeep from "lodash/cloneDeep"; import isEqual from "lodash/isEqual"; import set from "lodash/set"; -import { action, makeObservable, observable, runInAction, computed } from "mobx"; +import { action, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; @@ -19,16 +19,17 @@ import { export interface IIssueFilterStore { // observables layoutOptions: TIssueLayoutOptions; - filters: { [projectId: string]: TIssueFilters } | undefined; + filters: { [anchor: string]: TIssueFilters } | undefined; // computed - issueFilters: TIssueFilters | undefined; - appliedFilters: TIssueQueryFiltersParams | undefined; - isIssueFiltersUpdated: (filters: TIssueFilters) => boolean; + isIssueFiltersUpdated: (anchor: string, filters: TIssueFilters) => boolean; + // helpers + getIssueFilters: (anchor: string) => TIssueFilters | undefined; + getAppliedFilters: (anchor: string) => TIssueQueryFiltersParams | undefined; // actions updateLayoutOptions: (layout: TIssueLayoutOptions) => void; - initIssueFilters: (projectId: string, filters: TIssueFilters) => void; + initIssueFilters: (anchor: string, filters: TIssueFilters) => void; updateIssueFilters: ( - projectId: string, + anchor: string, filterKind: K, filterKey: keyof TIssueFilters[K], filters: TIssueFilters[K][typeof filterKey] @@ -44,16 +45,13 @@ export class IssueFilterStore implements IIssueFilterStore { gantt: false, spreadsheet: false, }; - filters: { [projectId: string]: TIssueFilters } | undefined = undefined; + filters: { [anchor: string]: TIssueFilters } | undefined = undefined; constructor(private store: RootStore) { makeObservable(this, { // observables layoutOptions: observable, filters: observable, - // computed - issueFilters: computed, - appliedFilters: computed, // actions updateLayoutOptions: action, initIssueFilters: action, @@ -82,79 +80,70 @@ export class IssueFilterStore implements IIssueFilterStore { }; // computed - get issueFilters() { - const projectId = this.store.project.project?.id; - if (!projectId) return undefined; - - const currentFilters = this.filters?.[projectId]; - if (!currentFilters) return undefined; - + getIssueFilters = computedFn((anchor: string) => { + const currentFilters = this.filters?.[anchor]; return currentFilters; - } + }); - get appliedFilters() { - const currentIssueFilters = this.issueFilters; - if (!currentIssueFilters) return undefined; + getAppliedFilters = computedFn((anchor: string) => { + const issueFilters = this.getIssueFilters(anchor); + if (!issueFilters) return undefined; - const currentLayout = currentIssueFilters?.display_filters?.layout; + const currentLayout = issueFilters?.display_filters?.layout; if (!currentLayout) return undefined; const currentFilters: TIssueQueryFilters = { - priority: currentIssueFilters?.filters?.priority || undefined, - state: currentIssueFilters?.filters?.state || undefined, - labels: currentIssueFilters?.filters?.labels || undefined, + priority: issueFilters?.filters?.priority || undefined, + state: issueFilters?.filters?.state || undefined, + labels: issueFilters?.filters?.labels || undefined, }; const filteredParams = ISSUE_DISPLAY_FILTERS_BY_LAYOUT?.[currentLayout]?.filters || []; const currentFilterQueryParams: TIssueQueryFiltersParams = this.computedFilter(currentFilters, filteredParams); return currentFilterQueryParams; - } + }); - isIssueFiltersUpdated = computedFn((userFilters: TIssueFilters) => { - if (!this.issueFilters) return false; + isIssueFiltersUpdated = computedFn((anchor: string, userFilters: TIssueFilters) => { + const issueFilters = this.getIssueFilters(anchor); + if (!issueFilters) return false; const currentUserFilters = cloneDeep(userFilters?.filters || {}); - const currentIssueFilters = cloneDeep(this.issueFilters?.filters || {}); + const currentIssueFilters = cloneDeep(issueFilters?.filters || {}); return isEqual(currentUserFilters, currentIssueFilters); }); // actions updateLayoutOptions = (options: TIssueLayoutOptions) => set(this, ["layoutOptions"], options); - initIssueFilters = async (projectId: string, initFilters: TIssueFilters) => { + initIssueFilters = async (anchor: string, initFilters: TIssueFilters) => { try { - if (!projectId) return; if (this.filters === undefined) runInAction(() => (this.filters = {})); - if (this.filters && initFilters) set(this.filters, [projectId], initFilters); + if (this.filters && initFilters) set(this.filters, [anchor], initFilters); - const workspaceSlug = this.store.project.workspace?.slug; - const currentAppliedFilters = this.appliedFilters; + const appliedFilters = this.getAppliedFilters(anchor); - if (!workspaceSlug) return; - await this.store.issue.fetchPublicIssues(workspaceSlug, projectId, currentAppliedFilters); + await this.store.issue.fetchPublicIssues(anchor, appliedFilters); } catch (error) { throw error; } }; updateIssueFilters = async ( - projectId: string, + anchor: string, filterKind: K, filterKey: keyof TIssueFilters[K], filterValue: TIssueFilters[K][typeof filterKey] ) => { try { - if (!projectId || !filterKind || !filterKey || !filterValue) return; + if (!filterKind || !filterKey || !filterValue) return; if (this.filters === undefined) runInAction(() => (this.filters = {})); runInAction(() => { - if (this.filters) set(this.filters, [projectId, filterKind, filterKey], filterValue); + if (this.filters) set(this.filters, [anchor, filterKind, filterKey], filterValue); }); - const workspaceSlug = this.store.project.workspace?.slug; - const currentAppliedFilters = this.appliedFilters; + const appliedFilters = this.getAppliedFilters(anchor); - if (!workspaceSlug) return; - await this.store.issue.fetchPublicIssues(workspaceSlug, projectId, currentAppliedFilters); + await this.store.issue.fetchPublicIssues(anchor, appliedFilters); } catch (error) { throw error; } diff --git a/space/store/issue.store.ts b/space/store/issue.store.ts index 7967aafb1..4f2d845b5 100644 --- a/space/store/issue.store.ts +++ b/space/store/issue.store.ts @@ -1,87 +1,87 @@ import { observable, action, makeObservable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; +// types +import { IStateLite } from "@plane/types"; // services import IssueService from "@/services/issue.service"; // types -import { IIssue, IIssueState, IIssueLabel } from "@/types/issue"; +import { IIssue, IIssueLabel } from "@/types/issue"; // store import { RootStore } from "./root.store"; -// import { IssueDetailType, TIssueBoardKeys } from "types/issue"; export interface IIssueStore { loader: boolean; error: any; - // issue options - issues: IIssue[] | null; - states: IIssueState[] | null; - labels: IIssueLabel[] | null; - // filtering + // observables + issues: IIssue[]; + states: IStateLite[]; + labels: IIssueLabel[]; + // filter observables filteredStates: string[]; filteredLabels: string[]; filteredPriorities: string[]; - // service - issueService: any; // actions - fetchPublicIssues: (workspace_slug: string, project_slug: string, params: any) => Promise; - getCountOfIssuesByState: (state: string) => number; - getFilteredIssuesByState: (state: string) => IIssue[]; + fetchPublicIssues: (anchor: string, params: any) => Promise; + // helpers + getCountOfIssuesByState: (stateID: string) => number; + getFilteredIssuesByState: (stateID: string) => IIssue[]; } export class IssueStore implements IIssueStore { loader: boolean = false; error: any | null = null; - - states: IIssueState[] | null = []; - labels: IIssueLabel[] | null = []; - + // observables + states: IStateLite[] = []; + labels: IIssueLabel[] = []; + issues: IIssue[] = []; + // filter observables filteredStates: string[] = []; filteredLabels: string[] = []; filteredPriorities: string[] = []; - - issues: IIssue[] | null = []; - issue_detail: any = {}; - + // root store rootStore: RootStore; - issueService: any; + // services + issueService: IssueService; - constructor(_rootStore: any) { + constructor(_rootStore: RootStore) { makeObservable(this, { - // observable - loader: observable, + loader: observable.ref, error: observable, - // issue options - states: observable.ref, - labels: observable.ref, - // filtering - filteredStates: observable.ref, - filteredLabels: observable.ref, - filteredPriorities: observable.ref, - // issues - issues: observable.ref, - issue_detail: observable.ref, + // observables + states: observable, + labels: observable, + issues: observable, + // filter observables + filteredStates: observable, + filteredLabels: observable, + filteredPriorities: observable, // actions fetchPublicIssues: action, - getFilteredIssuesByState: action, }); this.rootStore = _rootStore; this.issueService = new IssueService(); } - fetchPublicIssues = async (workspaceSlug: string, projectId: string, params: any) => { + /** + * @description fetch issues, states and labels + * @param {string} anchor + * @param params + */ + fetchPublicIssues = async (anchor: string, params: any) => { try { - this.loader = true; - this.error = null; + runInAction(() => { + this.loader = true; + this.error = null; + }); - const response = await this.issueService.getPublicIssues(workspaceSlug, projectId, params); + const response = await this.issueService.fetchPublicIssues(anchor, params); if (response) { - const states: IIssueState[] = [...response?.states]; - const labels: IIssueLabel[] = [...response?.labels]; - const issues: IIssue[] = [...response?.issues]; runInAction(() => { - this.states = states; - this.labels = labels; - this.issues = issues; + this.states = response.states; + this.labels = response.labels; + this.issues = response.issues; this.loader = false; }); } @@ -91,11 +91,21 @@ export class IssueStore implements IIssueStore { } }; - // computed - getCountOfIssuesByState(state_id: string): number { - return this.issues?.filter((issue) => issue.state == state_id).length || 0; - } + /** + * @description get total count of issues under a particular state + * @param {string} stateID + * @returns {number} + */ + getCountOfIssuesByState = computedFn( + (stateID: string) => this.issues?.filter((issue) => issue.state == stateID).length || 0 + ); - getFilteredIssuesByState = (state_id: string): IIssue[] | [] => - this.issues?.filter((issue) => issue.state == state_id) || []; + /** + * @description get array of issues under a particular state + * @param {string} stateID + * @returns {IIssue[]} + */ + getFilteredIssuesByState = computedFn( + (stateID: string) => this.issues?.filter((issue) => issue.state == stateID) || [] + ); } diff --git a/space/store/project.store.ts b/space/store/project.store.ts deleted file mode 100644 index 02f250323..000000000 --- a/space/store/project.store.ts +++ /dev/null @@ -1,96 +0,0 @@ -// mobx -import { observable, action, makeObservable, runInAction, computed } from "mobx"; -// service -import ProjectService from "@/services/project.service"; -// store types -import { RootStore } from "@/store/root.store"; -// types -import { TWorkspaceDetails, TProjectDetails, TProjectSettings } from "@/types/project"; - -export interface IProjectStore { - // observables - loader: boolean; - error: any | undefined; - settings: TProjectSettings | undefined; - workspace: TWorkspaceDetails | undefined; - project: TProjectDetails | undefined; - canReact: boolean; - canComment: boolean; - canVote: boolean; - // actions - fetchProjectSettings: (workspace_slug: string, project_slug: string) => Promise; - hydrate: (projectSettings: any) => void; -} - -export class ProjectStore implements IProjectStore { - // observables - loader: boolean = false; - error: any | undefined = undefined; - settings: TProjectSettings | undefined = undefined; - workspace: TWorkspaceDetails | undefined = undefined; - project: TProjectDetails | undefined = undefined; - // service - projectService; - - constructor(private store: RootStore) { - makeObservable(this, { - // loaders and error observables - loader: observable, - error: observable.ref, - // observable - workspace: observable, - project: observable, - settings: observable, - // computed - canReact: computed, - canComment: computed, - canVote: computed, - // actions - fetchProjectSettings: action, - hydrate: action, - }); - // services - this.projectService = new ProjectService(); - } - - // computed - get canReact() { - return this.settings?.reactions ?? false; - } - get canComment() { - return this.settings?.comments ?? false; - } - get canVote() { - return this.settings?.votes ?? false; - } - - fetchProjectSettings = async (workspace_slug: string, project_slug: string) => { - try { - this.loader = true; - this.error = null; - - const response = await this.projectService.getProjectSettings(workspace_slug, project_slug); - - if (response) { - this.store.issueFilter.updateLayoutOptions(response?.views); - runInAction(() => { - this.project = response?.project_details; - this.workspace = response?.workspace_detail; - this.settings = response; - this.loader = false; - }); - } - return response; - } catch (error) { - this.loader = false; - this.error = error; - return error; - } - }; - - hydrate = (projectSettings: TProjectSettings) => { - const { workspace_detail, project_details } = projectSettings; - this.workspace = workspace_detail; - this.project = project_details; - }; -} diff --git a/space/store/publish/publish.store.ts b/space/store/publish/publish.store.ts new file mode 100644 index 000000000..5cad121db --- /dev/null +++ b/space/store/publish/publish.store.ts @@ -0,0 +1,114 @@ +import { observable, makeObservable, computed } from "mobx"; +// types +import { IWorkspaceLite } from "@plane/types"; +// store types +import { RootStore } from "@/store/root.store"; +// types +import { TProjectDetails, TViewDetails } from "@/types/project"; +import { TPublishEntityType, TPublishSettings } from "@/types/publish"; + +export interface IPublishStore extends TPublishSettings { + // computed + workspaceSlug: string | undefined; + canComment: boolean; + canReact: boolean; + canVote: boolean; +} + +export class PublishStore implements IPublishStore { + // observables + anchor: string | undefined; + is_comments_enabled: boolean; + created_at: string | undefined; + created_by: string | undefined; + entity_identifier: string | undefined; + entity_name: TPublishEntityType | undefined; + id: string | undefined; + inbox: unknown; + project: string | undefined; + project_details: TProjectDetails | undefined; + is_reactions_enabled: boolean; + updated_at: string | undefined; + updated_by: string | undefined; + view_props: TViewDetails | undefined; + is_votes_enabled: boolean; + workspace: string | undefined; + workspace_detail: IWorkspaceLite | undefined; + + constructor( + private store: RootStore, + publishSettings: TPublishSettings + ) { + this.anchor = publishSettings.anchor; + this.is_comments_enabled = publishSettings.is_comments_enabled; + this.created_at = publishSettings.created_at; + this.created_by = publishSettings.created_by; + this.entity_identifier = publishSettings.entity_identifier; + this.entity_name = publishSettings.entity_name; + this.id = publishSettings.id; + this.inbox = publishSettings.inbox; + this.project = publishSettings.project; + this.project_details = publishSettings.project_details; + this.is_reactions_enabled = publishSettings.is_reactions_enabled; + this.updated_at = publishSettings.updated_at; + this.updated_by = publishSettings.updated_by; + this.view_props = publishSettings.view_props; + this.is_votes_enabled = publishSettings.is_votes_enabled; + this.workspace = publishSettings.workspace; + this.workspace_detail = publishSettings.workspace_detail; + + makeObservable(this, { + // observables + anchor: observable.ref, + is_comments_enabled: observable.ref, + created_at: observable.ref, + created_by: observable.ref, + entity_identifier: observable.ref, + entity_name: observable.ref, + id: observable.ref, + inbox: observable, + project: observable.ref, + project_details: observable, + is_reactions_enabled: observable.ref, + updated_at: observable.ref, + updated_by: observable.ref, + view_props: observable, + is_votes_enabled: observable.ref, + workspace: observable.ref, + workspace_detail: observable, + // computed + workspaceSlug: computed, + canComment: computed, + canReact: computed, + canVote: computed, + }); + } + + /** + * @description returns the workspace slug from the workspace details + */ + get workspaceSlug() { + return this?.workspace_detail?.slug ?? undefined; + } + + /** + * @description returns whether commenting is enabled or not + */ + get canComment() { + return !!this.is_comments_enabled; + } + + /** + * @description returns whether reacting is enabled or not + */ + get canReact() { + return !!this.is_reactions_enabled; + } + + /** + * @description returns whether voting is enabled or not + */ + get canVote() { + return !!this.is_votes_enabled; + } +} diff --git a/space/store/publish/publish_list.store.ts b/space/store/publish/publish_list.store.ts new file mode 100644 index 000000000..91db19f7a --- /dev/null +++ b/space/store/publish/publish_list.store.ts @@ -0,0 +1,54 @@ +import set from "lodash/set"; +import { makeObservable, observable, runInAction, action } from "mobx"; +// services +import PublishService from "@/services/publish.service"; +// store +import { PublishStore } from "@/store/publish/publish.store"; +// store +import { TPublishSettings } from "@/types/publish"; +import { RootStore } from "../root.store"; + +export interface IPublishListStore { + // observables + publishMap: Record; // anchor => PublishStore + // actions + fetchPublishSettings: (pageId: string) => Promise; +} + +export class PublishListStore implements IPublishListStore { + // observables + publishMap: Record = {}; // anchor => PublishStore + // service + publishService; + + constructor(private store: RootStore) { + makeObservable(this, { + // observables + publishMap: observable, + // actions + fetchPublishSettings: action, + }); + // services + this.publishService = new PublishService(); + } + + /** + * @description fetch publish settings + * @param {string} anchor + */ + fetchPublishSettings = async (anchor: string) => { + try { + const response = await this.publishService.fetchPublishSettings(anchor); + runInAction(() => { + if (response.anchor && response.view_props) { + this.store.issueFilter.updateLayoutOptions(response?.view_props); + set(this.publishMap, [response.anchor], new PublishStore(this.store, response)); + } + }); + + return response; + } catch (error) { + throw error; + } + }; +} diff --git a/space/store/root.store.ts b/space/store/root.store.ts index 4a31840db..082220f5d 100644 --- a/space/store/root.store.ts +++ b/space/store/root.store.ts @@ -3,30 +3,30 @@ import { enableStaticRendering } from "mobx-react-lite"; import { IInstanceStore, InstanceStore } from "@/store/instance.store"; import { IssueDetailStore, IIssueDetailStore } from "@/store/issue-detail.store"; import { IssueStore, IIssueStore } from "@/store/issue.store"; -import { IProjectStore, ProjectStore } from "@/store/project.store"; import { IUserStore, UserStore } from "@/store/user.store"; import { IssueFilterStore, IIssueFilterStore } from "./issue-filters.store"; import { IMentionsStore, MentionsStore } from "./mentions.store"; +import { IPublishListStore, PublishListStore } from "./publish/publish_list.store"; enableStaticRendering(typeof window === "undefined"); export class RootStore { instance: IInstanceStore; user: IUserStore; - project: IProjectStore; issue: IIssueStore; issueDetail: IIssueDetailStore; mentionStore: IMentionsStore; issueFilter: IIssueFilterStore; + publishList: IPublishListStore; constructor() { this.instance = new InstanceStore(this); this.user = new UserStore(this); - this.project = new ProjectStore(this); this.issue = new IssueStore(this); this.issueDetail = new IssueDetailStore(this); this.mentionStore = new MentionsStore(this); this.issueFilter = new IssueFilterStore(this); + this.publishList = new PublishListStore(this); } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -40,10 +40,10 @@ export class RootStore { localStorage.setItem("theme", "system"); this.instance = new InstanceStore(this); this.user = new UserStore(this); - this.project = new ProjectStore(this); this.issue = new IssueStore(this); this.issueDetail = new IssueDetailStore(this); this.mentionStore = new MentionsStore(this); this.issueFilter = new IssueFilterStore(this); + this.publishList = new PublishListStore(this); }; } diff --git a/space/styles/globals.css b/space/styles/globals.css index 47804b768..0b41d8481 100644 --- a/space/styles/globals.css +++ b/space/styles/globals.css @@ -302,6 +302,23 @@ } } +* { + margin: 0; + padding: 0; + box-sizing: border-box; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + font-variant-ligatures: none; + -webkit-font-variant-ligatures: none; + text-rendering: optimizeLegibility; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; +} + +body { + color: rgba(var(--color-text-100)); +} + ::-webkit-scrollbar { width: 5px; height: 5px; diff --git a/space/types/app.d.ts b/space/types/app.d.ts deleted file mode 100644 index bd4af3b0c..000000000 --- a/space/types/app.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface IAppConfig { - email_password_login: boolean; - file_size_limit: number; - google_client_id: string | null; - github_app_name: string | null; - github_client_id: string | null; - magic_login: boolean; - slack_client_id: string | null; - posthog_api_key: string | null; - posthog_host: string | null; - has_openai_configured: boolean; - has_unsplash_configured: boolean; - is_self_managed: boolean; -} diff --git a/space/types/issue.d.ts b/space/types/issue.d.ts index f2625fb76..5b729d1c0 100644 --- a/space/types/issue.d.ts +++ b/space/types/issue.d.ts @@ -1,27 +1,17 @@ +import { IStateLite, IWorkspaceLite, TIssuePriorities, TStateGroups } from "@plane/types"; + export type TIssueLayout = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt"; export type TIssueLayoutOptions = { [key in TIssueLayout]: boolean; }; -export type TIssueLayoutViews = { - [key in TIssueLayout]: { title: string; icon: string; className: string }; -}; -export type TIssueFilterPriority = "urgent" | "high" | "medium" | "low" | "none"; export type TIssueFilterPriorityObject = { - key: TIssueFilterPriority; + key: TIssuePriorities; title: string; className: string; icon: string; }; -export type TIssueFilterState = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; -export type TIssueFilterStateObject = { - key: TIssueFilterState; - title: string; - color: string; - className: string; -}; - export type TIssueFilterKeys = "priority" | "state" | "labels"; export type TDisplayFilters = { @@ -29,8 +19,8 @@ export type TDisplayFilters = { }; export type TFilters = { - state: TIssueFilterState[]; - priority: TIssueFilterPriority[]; + state: TStateGroups[]; + priority: TIssuePriorities[]; labels: string[]; }; @@ -43,6 +33,12 @@ export type TIssueQueryFilters = Partial; export type TIssueQueryFiltersParams = Partial>; +export type TIssuesResponse = { + states: IStateLite[]; + labels: IIssueLabel[]; + issues: IIssue[]; +}; + export interface IIssue { id: string; comments: Comment[]; @@ -68,17 +64,11 @@ export interface IIssue { export type IPeekMode = "side" | "modal" | "full"; -export interface IIssueState { - id: string; - name: string; - group: TIssueGroupKey; - color: string; -} - export interface IIssueLabel { id: string; name: string; color: string; + parent: string | null; } export interface IVote { @@ -114,7 +104,7 @@ export interface Comment { updated_at: Date; updated_by: string; workspace: string; - workspace_detail: WorkspaceDetail; + workspace_detail: IWorkspaceLite; } export interface IIssueReaction { @@ -175,52 +165,8 @@ export interface ProjectDetail { description: string; } -export interface WorkspaceDetail { - name: string; - slug: string; - id: string; -} - -export interface IssueDetailType { - [issueId: string]: { - issue: IIssue; - comments: Comment[]; - reactions: any[]; - votes: any[]; - }; -} - -export type TIssueGroupByOptions = "state" | "priority" | "labels" | null; - -export type TIssueParams = "priority" | "state" | "labels"; - export interface IIssueFilterOptions { state?: string[] | null; labels?: string[] | null; priority?: string[] | null; } - -// issues -export interface IGroupedIssues { - [group_id: string]: string[]; -} - -export interface ISubGroupedIssues { - [sub_grouped_id: string]: { - [group_id: string]: string[]; - }; -} - -export type TUnGroupedIssues = string[]; - -export interface IIssueResponse { - [issue_id: string]: IIssue; -} - -export type TLoader = "init-loader" | "mutation" | undefined; - -export interface ViewFlags { - enableQuickAdd: boolean; - enableIssueCreation: boolean; - enableInlineEditing: boolean; -} diff --git a/space/types/project.d.ts b/space/types/project.d.ts index 90c89ed80..c0ae02583 100644 --- a/space/types/project.d.ts +++ b/space/types/project.d.ts @@ -1,11 +1,5 @@ import { TLogoProps } from "@plane/types"; -export type TWorkspaceDetails = { - name: string; - slug: string; - id: string; -}; - export type TViewDetails = { list: boolean; gantt: boolean; @@ -22,21 +16,3 @@ export type TProjectDetails = { logo_props: TLogoProps; description: string; }; - -export type TProjectSettings = { - id: string; - anchor: string; - comments: boolean; - reactions: boolean; - votes: boolean; - inbox: unknown; - workspace: string; - workspace_detail: TWorkspaceDetails; - project: string; - project_details: TProjectDetails; - views: TViewDetails; - created_by: string; - updated_by: string; - created_at: string; - updated_at: string; -}; diff --git a/space/types/publish.d.ts b/space/types/publish.d.ts new file mode 100644 index 000000000..482cbafec --- /dev/null +++ b/space/types/publish.d.ts @@ -0,0 +1,24 @@ +import { IWorkspaceLite } from "@plane/types"; +import { TProjectDetails, TViewDetails } from "@/types/project"; + +export type TPublishEntityType = "project"; + +export type TPublishSettings = { + anchor: string | undefined; + is_comments_enabled: boolean; + created_at: string | undefined; + created_by: string | undefined; + entity_identifier: string | undefined; + entity_name: TPublishEntityType | undefined; + id: string | undefined; + inbox: unknown; + project: string | undefined; + project_details: TProjectDetails | undefined; + is_reactions_enabled: boolean; + updated_at: string | undefined; + updated_by: string | undefined; + view_props: TViewDetails | undefined; + is_votes_enabled: boolean; + workspace: string | undefined; + workspace_detail: IWorkspaceLite | undefined; +}; diff --git a/space/types/theme.d.ts b/space/types/theme.d.ts deleted file mode 100644 index ca306be51..000000000 --- a/space/types/theme.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IThemeStore { - theme: string; - setTheme: (theme: "light" | "dark" | string) => void; -} diff --git a/space/types/user.d.ts b/space/types/user.d.ts deleted file mode 100644 index d58827876..000000000 --- a/space/types/user.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -export interface IUser { - avatar: string; - cover_image: string | null; - created_at: Date; - created_location: string; - date_joined: Date; - email: string; - display_name: string; - first_name: string; - id: string; - is_email_verified: boolean; - is_onboarded: boolean; - is_tour_completed: boolean; - last_location: string; - last_login: Date; - last_name: string; - mobile_number: string; - role: string; - is_password_autoset: boolean; - onboarding_step: { - workspace_join?: boolean; - profile_complete?: boolean; - workspace_create?: boolean; - workspace_invite?: boolean; - }; - token: string; - updated_at: Date; - username: string; - user_timezone: string; -} diff --git a/web/components/project/publish-project/modal.tsx b/web/components/project/publish-project/modal.tsx index c9534781e..1c547aa8d 100644 --- a/web/components/project/publish-project/modal.tsx +++ b/web/components/project/publish-project/modal.tsx @@ -25,57 +25,52 @@ type Props = { }; type FormData = { + anchor: string; id: string | null; - comments: boolean; - reactions: boolean; - votes: boolean; + is_comments_enabled: boolean; + is_reactions_enabled: boolean; + is_votes_enabled: boolean; inbox: string | null; views: TProjectPublishViews[]; }; const defaultValues: FormData = { + anchor: "", id: null, - comments: false, - reactions: false, - votes: false, + is_comments_enabled: false, + is_reactions_enabled: false, + is_votes_enabled: false, inbox: null, views: ["list", "kanban"], }; -const viewOptions: { +const VIEW_OPTIONS: { key: TProjectPublishViews; label: string; }[] = [ { key: "list", label: "List" }, { key: "kanban", label: "Kanban" }, - // { key: "calendar", label: "Calendar" }, - // { key: "gantt", label: "Gantt" }, - // { key: "spreadsheet", label: "Spreadsheet" }, ]; export const PublishProjectModal: React.FC = observer((props) => { const { isOpen, project, onClose } = props; - // hooks - // const { instance } = useInstance(); // states const [isUnPublishing, setIsUnPublishing] = useState(false); const [isUpdateRequired, setIsUpdateRequired] = useState(false); - - // const plane_deploy_url = instance?.config?.space_base_url || ""; - const SPACE_URL = (SPACE_BASE_URL === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH; - // router const router = useRouter(); const { workspaceSlug } = router.query; // store hooks const { - projectPublishSettings, - getProjectSettingsAsync, + fetchPublishSettings, + getPublishSettingsByProjectID, publishProject, - updateProjectSettingsAsync, + updatePublishSettings, unPublishProject, fetchSettingsLoader, } = useProjectPublish(); + // derived values + const projectPublishSettings = getPublishSettingsByProjectID(project.id); // form info const { control, @@ -97,44 +92,44 @@ export const PublishProjectModal: React.FC = observer((props) => { // prefill form with the saved settings if the project is already published useEffect(() => { - if (projectPublishSettings && projectPublishSettings !== "not-initialized") { - let userBoards: TProjectPublishViews[] = []; + if (!projectPublishSettings?.anchor) return; - if (projectPublishSettings?.views) { - const savedViews = projectPublishSettings?.views; + let userBoards: TProjectPublishViews[] = []; - if (!savedViews) return; + if (projectPublishSettings?.view_props) { + const savedViews = projectPublishSettings?.view_props; - if (savedViews.list) userBoards.push("list"); - if (savedViews.kanban) userBoards.push("kanban"); - if (savedViews.calendar) userBoards.push("calendar"); - if (savedViews.gantt) userBoards.push("gantt"); - if (savedViews.spreadsheet) userBoards.push("spreadsheet"); + if (!savedViews) return; - userBoards = userBoards && userBoards.length > 0 ? userBoards : ["list"]; - } + if (savedViews.list) userBoards.push("list"); + if (savedViews.kanban) userBoards.push("kanban"); + if (savedViews.calendar) userBoards.push("calendar"); + if (savedViews.gantt) userBoards.push("gantt"); + if (savedViews.spreadsheet) userBoards.push("spreadsheet"); - const updatedData = { - id: projectPublishSettings?.id || null, - comments: projectPublishSettings?.comments || false, - reactions: projectPublishSettings?.reactions || false, - votes: projectPublishSettings?.votes || false, - inbox: projectPublishSettings?.inbox || null, - views: userBoards, - }; - - reset({ ...updatedData }); + userBoards = userBoards && userBoards.length > 0 ? userBoards : ["list"]; } + + const updatedData = { + id: projectPublishSettings?.id || null, + is_comments_enabled: !!projectPublishSettings?.is_comments_enabled, + is_reactions_enabled: !!projectPublishSettings?.is_reactions_enabled, + is_votes_enabled: !!projectPublishSettings?.is_votes_enabled, + inbox: projectPublishSettings?.inbox || null, + views: userBoards, + }; + + reset({ ...updatedData }); }, [reset, projectPublishSettings, isOpen]); // fetch publish settings useEffect(() => { if (!workspaceSlug || !isOpen) return; - if (projectPublishSettings === "not-initialized") { - getProjectSettingsAsync(workspaceSlug.toString(), project.id); + if (!projectPublishSettings) { + fetchPublishSettings(workspaceSlug.toString(), project.id); } - }, [isOpen, workspaceSlug, project, projectPublishSettings, getProjectSettingsAsync]); + }, [fetchPublishSettings, isOpen, project, projectPublishSettings, workspaceSlug]); const handlePublishProject = async (payload: IProjectPublishSettings) => { if (!workspaceSlug) return; @@ -145,7 +140,7 @@ export const PublishProjectModal: React.FC = observer((props) => { const handleUpdatePublishSettings = async (payload: IProjectPublishSettings) => { if (!workspaceSlug) return; - await updateProjectSettingsAsync(workspaceSlug.toString(), project.id, payload.id ?? "", payload) + await updatePublishSettings(workspaceSlug.toString(), project.id, payload.id ?? "", payload) .then((res) => { setToast({ type: TOAST_TYPE.SUCCESS, @@ -172,7 +167,7 @@ export const PublishProjectModal: React.FC = observer((props) => { setToast({ type: TOAST_TYPE.ERROR, title: "Error!", - message: "Something went wrong while un-publishing the project.", + message: "Something went wrong while unpublishing the project.", }) ) .finally(() => setIsUnPublishing(false)); @@ -210,11 +205,11 @@ export const PublishProjectModal: React.FC = observer((props) => { } const payload = { - comments: formData.comments, - reactions: formData.reactions, - votes: formData.votes, + is_comments_enabled: formData.is_comments_enabled, + is_reactions_enabled: formData.is_reactions_enabled, + is_votes_enabled: formData.is_votes_enabled, inbox: formData.inbox, - views: { + view_props: { list: formData.views.includes("list"), kanban: formData.views.includes("kanban"), calendar: formData.views.includes("calendar"), @@ -223,29 +218,34 @@ export const PublishProjectModal: React.FC = observer((props) => { }, }; - if (project.is_deployed) await handleUpdatePublishSettings({ id: watch("id") ?? "", ...payload }); + if (project.is_deployed) + await handleUpdatePublishSettings({ + anchor: watch("anchor") ?? "", + id: watch("id") ?? "", + ...payload, + }); else await handlePublishProject(payload); }; // check if an update is required or not const checkIfUpdateIsRequired = () => { - if (!projectPublishSettings || projectPublishSettings === "not-initialized") return; + if (!projectPublishSettings || !projectPublishSettings) return; const currentSettings = projectPublishSettings; const newSettings = getValues(); if ( - currentSettings.comments !== newSettings.comments || - currentSettings.reactions !== newSettings.reactions || - currentSettings.votes !== newSettings.votes + currentSettings.is_comments_enabled !== newSettings.is_comments_enabled || + currentSettings.is_reactions_enabled !== newSettings.is_reactions_enabled || + currentSettings.is_votes_enabled !== newSettings.is_votes_enabled ) { setIsUpdateRequired(true); return; } let viewCheckFlag = 0; - viewOptions.forEach((option) => { - if (currentSettings.views[option.key] !== newSettings.views.includes(option.key)) viewCheckFlag++; + VIEW_OPTIONS.forEach((option) => { + if (currentSettings.view_props?.[option.key] !== newSettings.views.includes(option.key)) viewCheckFlag++; }); if (viewCheckFlag !== 0) { @@ -256,6 +256,8 @@ export const PublishProjectModal: React.FC = observer((props) => { setIsUpdateRequired(false); }; + const SPACE_URL = (SPACE_BASE_URL === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH; + return ( @@ -293,7 +295,7 @@ export const PublishProjectModal: React.FC = observer((props) => { onClick={() => handleUnPublishProject(watch("id") ?? "")} loading={isUnPublishing} > - {isUnPublishing ? "Un-publishing..." : "Un-publish"} + {isUnPublishing ? "Unpublishing" : "Unpublish"} )}
@@ -308,14 +310,12 @@ export const PublishProjectModal: React.FC = observer((props) => { ) : (
- {project.is_deployed && ( + {project.is_deployed && projectPublishSettings && ( <>
-
- {`${SPACE_URL}/${workspaceSlug}/${project.id}`} -
+
{`${SPACE_URL}/issues/${projectPublishSettings.anchor}`}
- +
@@ -337,8 +337,7 @@ export const PublishProjectModal: React.FC = observer((props) => { 0 - ? viewOptions - .filter((v) => value.includes(v.key)) + ? VIEW_OPTIONS.filter((v) => value.includes(v.key)) .map((v) => v.label) .join(", ") : `` @@ -346,7 +345,7 @@ export const PublishProjectModal: React.FC = observer((props) => { placeholder="Select views" > <> - {viewOptions.map((option) => ( + {VIEW_OPTIONS.map((option) => (
= observer((props) => {
Allow comments
( = observer((props) => {
Allow reactions
( = observer((props) => {
Allow voting
( { - return this.get(`/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/`) + async getProjectSettingsAsync(workspaceSlug: string, projectID: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectID}/project-deploy-boards/`) .then((response) => response?.data) .catch((error) => { throw error?.response; @@ -18,11 +18,11 @@ export class ProjectPublishService extends APIService { } async createProjectSettingsAsync( - workspace_slug: string, - project_slug: string, + workspaceSlug: string, + projectID: string, data: IProjectPublishSettings - ): Promise { - return this.post(`/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/`, data) + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectID}/project-deploy-boards/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response; @@ -30,13 +30,13 @@ export class ProjectPublishService extends APIService { } async updateProjectSettingsAsync( - workspace_slug: string, - project_slug: string, + workspaceSlug: string, + projectID: string, project_publish_id: string, data: IProjectPublishSettings - ): Promise { + ): Promise { return this.patch( - `/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/${project_publish_id}/`, + `/api/workspaces/${workspaceSlug}/projects/${projectID}/project-deploy-boards/${project_publish_id}/`, data ) .then((response) => response?.data) @@ -45,13 +45,9 @@ export class ProjectPublishService extends APIService { }); } - async deleteProjectSettingsAsync( - workspace_slug: string, - project_slug: string, - project_publish_id: string - ): Promise { + async deleteProjectSettingsAsync(workspaceSlug: string, projectID: string, project_publish_id: string): Promise { return this.delete( - `/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/${project_publish_id}/` + `/api/workspaces/${workspaceSlug}/projects/${projectID}/project-deploy-boards/${project_publish_id}/` ) .then((response) => response?.data) .catch((error) => { diff --git a/web/store/project/project-publish.store.ts b/web/store/project/project-publish.store.ts index 230090d98..25c2516f5 100644 --- a/web/store/project/project-publish.store.ts +++ b/web/store/project/project-publish.store.ts @@ -1,4 +1,5 @@ import set from "lodash/set"; +import unset from "lodash/unset"; import { observable, action, makeObservable, runInAction } from "mobx"; // types import { ProjectPublishService } from "@/services/project"; @@ -12,12 +13,13 @@ export type TProjectPublishViewsSettings = { }; export interface IProjectPublishSettings { + anchor?: string; id?: string; project?: string; - comments: boolean; - reactions: boolean; - votes: boolean; - views: TProjectPublishViewsSettings; + is_comments_enabled: boolean; + is_reactions_enabled: boolean; + is_votes_enabled: boolean; + view_props: TProjectPublishViewsSettings; inbox: string | null; } @@ -26,27 +28,31 @@ export interface IProjectPublishStore { generalLoader: boolean; fetchSettingsLoader: boolean; // observables - projectPublishSettings: IProjectPublishSettings | "not-initialized"; - // project settings actions - getProjectSettingsAsync: (workspaceSlug: string, projectId: string) => Promise; - updateProjectSettingsAsync: ( + publishSettingsMap: Record; // projectID => IProjectPublishSettings + // helpers + getPublishSettingsByProjectID: (projectID: string) => IProjectPublishSettings | undefined; + // actions + fetchPublishSettings: (workspaceSlug: string, projectID: string) => Promise; + updatePublishSettings: ( workspaceSlug: string, - projectId: string, + projectID: string, projectPublishId: string, data: IProjectPublishSettings - ) => Promise; - // project publish actions - publishProject: (workspaceSlug: string, projectId: string, data: IProjectPublishSettings) => Promise; - unPublishProject: (workspaceSlug: string, projectId: string, projectPublishId: string) => Promise; + ) => Promise; + publishProject: ( + workspaceSlug: string, + projectID: string, + data: IProjectPublishSettings + ) => Promise; + unPublishProject: (workspaceSlug: string, projectID: string, projectPublishId: string) => Promise; } export class ProjectPublishStore implements IProjectPublishStore { // states generalLoader: boolean = false; fetchSettingsLoader: boolean = false; - // actions - project_id: string | null = null; - projectPublishSettings: IProjectPublishSettings | "not-initialized" = "not-initialized"; + // observables + publishSettingsMap: Record = {}; // root store projectRootStore: ProjectRootStore; // services @@ -58,12 +64,10 @@ export class ProjectPublishStore implements IProjectPublishStore { generalLoader: observable.ref, fetchSettingsLoader: observable.ref, // observables - project_id: observable.ref, - projectPublishSettings: observable.ref, - // project settings actions - getProjectSettingsAsync: action, - updateProjectSettingsAsync: action, - // project publish actions + publishSettingsMap: observable, + // actions + fetchPublishSettings: action, + updatePublishSettings: action, publishProject: action, unPublishProject: action, }); @@ -73,44 +77,31 @@ export class ProjectPublishStore implements IProjectPublishStore { this.projectPublishService = new ProjectPublishService(); } + /** + * @description returns the publish settings of a particular project + * @param {string} projectID + * @returns {IProjectPublishSettings | undefined} + */ + getPublishSettingsByProjectID = (projectID: string): IProjectPublishSettings | undefined => + this.publishSettingsMap?.[projectID] ?? undefined; + /** * Fetches project publish settings * @param workspaceSlug - * @param projectId + * @param projectID * @returns */ - getProjectSettingsAsync = async (workspaceSlug: string, projectId: string) => { + fetchPublishSettings = async (workspaceSlug: string, projectID: string) => { try { runInAction(() => { this.fetchSettingsLoader = true; }); - const response = await this.projectPublishService.getProjectSettingsAsync(workspaceSlug, projectId); - if (response && response.length > 0) { - const _projectPublishSettings: IProjectPublishSettings = { - id: response[0]?.id, - comments: response[0]?.comments, - reactions: response[0]?.reactions, - votes: response[0]?.votes, - views: { - list: response[0]?.views?.list || false, - kanban: response[0]?.views?.kanban || false, - calendar: response[0]?.views?.calendar || false, - gantt: response[0]?.views?.gantt || false, - spreadsheet: response[0]?.views?.spreadsheet || false, - }, - inbox: response[0]?.inbox || null, - project: response[0]?.project || null, - }; - runInAction(() => { - this.projectPublishSettings = _projectPublishSettings; - this.fetchSettingsLoader = false; - }); - } else { - runInAction(() => { - this.projectPublishSettings = "not-initialized"; - this.fetchSettingsLoader = false; - }); - } + const response = await this.projectPublishService.getProjectSettingsAsync(workspaceSlug, projectID); + + runInAction(() => { + set(this.publishSettingsMap, [projectID], response); + this.fetchSettingsLoader = false; + }); return response; } catch (error) { runInAction(() => { @@ -123,34 +114,22 @@ export class ProjectPublishStore implements IProjectPublishStore { /** * Publishes project and updates project publish status in the store * @param workspaceSlug - * @param projectId + * @param projectID * @param data * @returns */ - publishProject = async (workspaceSlug: string, projectId: string, data: IProjectPublishSettings) => { + publishProject = async (workspaceSlug: string, projectID: string, data: IProjectPublishSettings) => { try { runInAction(() => { this.generalLoader = true; }); - const response = await this.projectPublishService.createProjectSettingsAsync(workspaceSlug, projectId, data); - if (response) { - const _projectPublishSettings: IProjectPublishSettings = { - id: response?.id || null, - comments: response?.comments || false, - reactions: response?.reactions || false, - votes: response?.votes || false, - views: { ...response?.views }, - inbox: response?.inbox || null, - project: response?.project || null, - }; - - runInAction(() => { - this.projectPublishSettings = _projectPublishSettings; - set(this.projectRootStore.project.projectMap, [projectId, "is_deployed"], true); - this.generalLoader = false; - }); - return response; - } + const response = await this.projectPublishService.createProjectSettingsAsync(workspaceSlug, projectID, data); + runInAction(() => { + set(this.publishSettingsMap, [projectID], response); + set(this.projectRootStore.project.projectMap, [projectID, "is_deployed"], true); + this.generalLoader = false; + }); + return response; } catch (error) { runInAction(() => { this.generalLoader = false; @@ -162,14 +141,14 @@ export class ProjectPublishStore implements IProjectPublishStore { /** * Updates project publish settings * @param workspaceSlug - * @param projectId + * @param projectID * @param projectPublishId * @param data * @returns */ - updateProjectSettingsAsync = async ( + updatePublishSettings = async ( workspaceSlug: string, - projectId: string, + projectID: string, projectPublishId: string, data: IProjectPublishSettings ) => { @@ -179,26 +158,15 @@ export class ProjectPublishStore implements IProjectPublishStore { }); const response = await this.projectPublishService.updateProjectSettingsAsync( workspaceSlug, - projectId, + projectID, projectPublishId, data ); - if (response) { - const _projectPublishSettings: IProjectPublishSettings = { - id: response?.id || null, - comments: response?.comments || false, - reactions: response?.reactions || false, - votes: response?.votes || false, - views: { ...response?.views }, - inbox: response?.inbox || null, - project: response?.project || null, - }; - runInAction(() => { - this.projectPublishSettings = _projectPublishSettings; - this.generalLoader = false; - }); - return response; - } + runInAction(() => { + set(this.publishSettingsMap, [projectID], response); + this.generalLoader = false; + }); + return response; } catch (error) { runInAction(() => { this.generalLoader = false; @@ -210,23 +178,23 @@ export class ProjectPublishStore implements IProjectPublishStore { /** * Unpublishes project and updates project publish status in the store * @param workspaceSlug - * @param projectId + * @param projectID * @param projectPublishId * @returns */ - unPublishProject = async (workspaceSlug: string, projectId: string, projectPublishId: string) => { + unPublishProject = async (workspaceSlug: string, projectID: string, projectPublishId: string) => { try { runInAction(() => { this.generalLoader = true; }); const response = await this.projectPublishService.deleteProjectSettingsAsync( workspaceSlug, - projectId, + projectID, projectPublishId ); runInAction(() => { - this.projectPublishSettings = "not-initialized"; - set(this.projectRootStore.project.projectMap, [projectId, "is_deployed"], false); + unset(this.publishSettingsMap, [projectID]); + set(this.projectRootStore.project.projectMap, [projectID, "is_deployed"], false); this.generalLoader = false; }); return response; diff --git a/yarn.lock b/yarn.lock index 05aec74bf..a2f58cf6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6281,6 +6281,11 @@ date-fns@^2.30.0: dependencies: "@babel/runtime" "^7.21.0" +date-fns@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" + integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -10681,7 +10686,7 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -"prettier-fallback@npm:prettier@^3": +"prettier-fallback@npm:prettier@^3", prettier@^3.1.1, prettier@^3.2.5, prettier@latest: version "3.3.0" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.0.tgz#d173ea0524a691d4c0b1181752f2b46724328cdf" integrity sha512-J9odKxERhCQ10OC2yb93583f6UnYutOeiV5i0zEDS7UGTdUt0u+y8erxl3lBKvwo/JHyyoEdXjwp4dke9oyZ/g== @@ -10708,11 +10713,6 @@ prettier@^2.8.8: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -prettier@^3.1.1, prettier@^3.2.5, prettier@latest: - version "3.3.0" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.0.tgz#d173ea0524a691d4c0b1181752f2b46724328cdf" - integrity sha512-J9odKxERhCQ10OC2yb93583f6UnYutOeiV5i0zEDS7UGTdUt0u+y8erxl3lBKvwo/JHyyoEdXjwp4dke9oyZ/g== - pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" @@ -12112,16 +12112,7 @@ string-argv@~0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12217,14 +12208,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -13648,16 +13632,7 @@ workbox-window@6.6.1, workbox-window@^6.5.4: "@types/trusted-types" "^2.0.2" workbox-core "6.6.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==