mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of gurusainath:makeplane/plane into revamp-estimates-ce
This commit is contained in:
commit
2da91383d4
@ -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,
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -10,7 +10,7 @@ from plane.space.views import (
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
|
||||
"anchor/<str:anchor>/inboxes/<uuid:inbox_id>/inbox-issues/",
|
||||
InboxIssuePublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
@ -20,7 +20,7 @@ urlpatterns = [
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
|
||||
"anchor/<str:anchor>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
|
||||
InboxIssuePublicViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
@ -31,7 +31,7 @@ urlpatterns = [
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/votes/",
|
||||
"anchor/<str:anchor>/issues/<uuid:issue_id>/votes/",
|
||||
IssueVotePublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
|
@ -10,12 +10,12 @@ from plane.space.views import (
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/",
|
||||
"anchor/<str:anchor>/issues/<uuid:issue_id>/",
|
||||
IssueRetrievePublicEndpoint.as_view(),
|
||||
name="workspace-project-boards",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
|
||||
"anchor/<str:anchor>/issues/<uuid:issue_id>/comments/",
|
||||
IssueCommentPublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
@ -25,7 +25,7 @@ urlpatterns = [
|
||||
name="issue-comments-project-board",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
|
||||
"anchor/<str:anchor>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
|
||||
IssueCommentPublicViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
@ -36,7 +36,7 @@ urlpatterns = [
|
||||
name="issue-comments-project-board",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
|
||||
"anchor/<str:anchor>/issues/<uuid:issue_id>/reactions/",
|
||||
IssueReactionPublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
@ -46,7 +46,7 @@ urlpatterns = [
|
||||
name="issue-reactions-project-board",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
|
||||
"anchor/<str:anchor>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
|
||||
IssueReactionPublicViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
@ -55,7 +55,7 @@ urlpatterns = [
|
||||
name="issue-reactions-project-board",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
|
||||
"anchor/<str:anchor>/comments/<uuid:comment_id>/reactions/",
|
||||
CommentReactionPublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
@ -65,7 +65,7 @@ urlpatterns = [
|
||||
name="comment-reactions-project-board",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
|
||||
"anchor/<str:anchor>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
|
||||
CommentReactionPublicViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
|
@ -4,17 +4,23 @@ from django.urls import path
|
||||
from plane.space.views import (
|
||||
ProjectDeployBoardPublicSettingsEndpoint,
|
||||
ProjectIssuesPublicEndpoint,
|
||||
WorkspaceProjectAnchorEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/project-boards/<uuid:project_id>/settings/",
|
||||
"anchor/<str:anchor>/settings/",
|
||||
ProjectDeployBoardPublicSettingsEndpoint.as_view(),
|
||||
name="project-deploy-board-settings",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/",
|
||||
"anchor/<str:anchor>/issues/",
|
||||
ProjectIssuesPublicEndpoint.as_view(),
|
||||
name="project-deploy-board",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/anchor/",
|
||||
WorkspaceProjectAnchorEndpoint.as_view(),
|
||||
name="project-deploy-board",
|
||||
),
|
||||
]
|
||||
|
@ -1,6 +1,7 @@
|
||||
from .project import (
|
||||
ProjectDeployBoardPublicSettingsEndpoint,
|
||||
WorkspaceProjectDeployBoardEndpoint,
|
||||
WorkspaceProjectAnchorEndpoint,
|
||||
)
|
||||
|
||||
from .issue import (
|
||||
|
@ -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", "<p></p>"
|
||||
),
|
||||
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,
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -5,6 +5,7 @@ import { isEqual } from "lodash";
|
||||
import { cn } from "../../helpers";
|
||||
import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
||||
import { DropIndicator } from "../drop-indicator";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
data: any; //@todo make this generic
|
||||
|
42
space/app/[workspaceSlug]/[projectId]/page.ts
Normal file
42
space/app/[workspaceSlug]/[projectId]/page.ts
Normal file
@ -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();
|
||||
}
|
||||
}
|
@ -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 <ProjectDetailsView workspaceSlug={workspace_slug} projectId={project_id} peekId={peekId} />;
|
||||
}
|
@ -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 (
|
||||
<div className="relative h-screen overflow-x-hidden overflow-y-auto container px-5 mx-auto flex justify-center items-center">
|
||||
<div className="w-auto max-w-2xl relative space-y-8 py-10">
|
||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
||||
<Image src={instanceImage} alt="Plane instance failure image" />
|
||||
<h3 className="font-medium text-2xl text-white ">Unable to fetch instance details.</h3>
|
||||
<p className="font-medium text-base text-center">
|
||||
We were unable to fetch the details of the instance. <br />
|
||||
Fret not, it might just be a connectivity issue.
|
||||
<div className="grid h-screen place-items-center p-4">
|
||||
<div className="space-y-8 text-center">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Exception Detected!</h3>
|
||||
<p className="mx-auto w-1/2 text-sm text-custom-text-200">
|
||||
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{" "}
|
||||
<a href="mailto:support@plane.so" className="text-custom-primary">
|
||||
support@plane.so
|
||||
</a>{" "}
|
||||
or on our{" "}
|
||||
<a
|
||||
href="https://discord.com/invite/A92xrEGCge"
|
||||
target="_blank"
|
||||
className="text-custom-primary"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Discord
|
||||
</a>{" "}
|
||||
server for further assistance.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button size="md" onClick={handleRetry}>
|
||||
Retry
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Button variant="primary" size="md" onClick={handleRetry}>
|
||||
Refresh
|
||||
</Button>
|
||||
{/* <Button variant="neutral-primary" size="md" onClick={() => {}}>
|
||||
Sign out
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ErrorPage;
|
||||
|
@ -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 <LogoSpinner />;
|
||||
|
||||
return (
|
||||
<div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden">
|
||||
<div className="relative flex h-[60px] flex-shrink-0 select-none items-center border-b border-custom-border-300 bg-custom-sidebar-background-100">
|
||||
<IssueNavbar workspaceSlug={workspace_slug} projectId={project_id} />
|
||||
<IssuesNavbarRoot publishSettings={publishSettings} />
|
||||
</div>
|
||||
<div className="relative h-full w-full overflow-hidden bg-custom-background-90">{children}</div>
|
||||
<a
|
||||
@ -37,4 +51,6 @@ export default async function ProjectLayout({
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default IssuesLayout;
|
30
space/app/issues/[anchor]/page.tsx
Normal file
30
space/app/issues/[anchor]/page.tsx
Normal file
@ -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 <IssuesLayoutsRoot peekId={peekId} publishSettings={publishSettings} />;
|
||||
});
|
||||
|
||||
export default IssuesPage;
|
@ -4,20 +4,18 @@ import Image from "next/image";
|
||||
// assets
|
||||
import UserLoggedInImage from "public/user-logged-in.svg";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col">
|
||||
<div className="grid h-full w-full place-items-center p-6">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto grid h-52 w-52 place-items-center rounded-full bg-custom-background-80">
|
||||
<div className="h-32 w-32">
|
||||
<Image src={UserLoggedInImage} alt="User already logged in" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="mt-12 text-3xl font-semibold">Not Found</h1>
|
||||
<p className="mt-4">Please enter the appropriate project URL to view the issue board.</p>
|
||||
const NotFound = () => (
|
||||
<div className="h-screen w-screen grid place-items-center">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto size-52 grid place-items-center rounded-full bg-custom-background-80">
|
||||
<div className="size-32">
|
||||
<Image src={UserLoggedInImage} alt="User already logged in" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="mt-12 text-3xl font-semibold">Not Found</h1>
|
||||
<p className="mt-4">Please enter the appropriate project URL to view the issue board.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NotFound;
|
||||
|
@ -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 (
|
||||
<div className="flex h-screen w-screen flex-col">
|
||||
<div className="flex flex-col h-screen w-screen">
|
||||
<div className="relative flex w-full items-center justify-between gap-4 border-b border-custom-border-200 px-6 py-5">
|
||||
<div>
|
||||
<Image src={PlaneLogo} alt="User already logged in" />
|
||||
<div className="h-[30px] w-[133px]">
|
||||
<Image src={logo} alt="Plane logo" />
|
||||
</div>
|
||||
<UserAvatar />
|
||||
</div>
|
||||
|
||||
<div className="grid h-full w-full place-items-center p-6">
|
||||
<div className="size-full grid place-items-center p-6">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto grid h-52 w-52 place-items-center rounded-full bg-custom-background-80">
|
||||
<div className="h-32 w-32">
|
||||
<div className="mx-auto size-52 grid place-items-center rounded-full bg-custom-background-80">
|
||||
<div className="size-32">
|
||||
<Image src={UserLoggedInImage} alt="User already logged in" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="mt-12 text-3xl font-semibold">Logged in Successfully!</h1>
|
||||
<h1 className="mt-12 text-3xl font-semibold">Logged in successfully!</h1>
|
||||
<p className="mt-4">
|
||||
You{"'"}ve successfully logged in. Please enter the appropriate project URL to view the issue board.
|
||||
</p>
|
||||
@ -38,4 +46,4 @@ export const UserLoggedIn = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,3 +1,2 @@
|
||||
export * from "./latest-feature-block";
|
||||
export * from "./project-logo";
|
||||
export * from "./logo-spinner";
|
||||
|
@ -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 (
|
||||
<>
|
||||
<div className="mx-auto mt-16 flex rounded-[3.5px] border border-onboarding-border-200 bg-onboarding-background-100 py-2 sm:w-96">
|
||||
<Lightbulb className="mx-3 mr-2 h-7 w-7" />
|
||||
<p className="text-left text-sm text-onboarding-text-100">
|
||||
Pages gets a facelift! Write anything and use Galileo to help you start.{" "}
|
||||
<Link href="https://plane.so/changelog" target="_blank" rel="noopener noreferrer">
|
||||
<span className="text-sm font-medium underline hover:cursor-pointer">Learn more</span>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={`mx-auto mt-8 overflow-hidden rounded-md border border-onboarding-border-200 object-cover sm:h-52 sm:w-96 ${
|
||||
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
|
||||
}`}
|
||||
>
|
||||
<div className="h-[90%]">
|
||||
<Image
|
||||
src={latestFeatures}
|
||||
alt="Plane Issues"
|
||||
className={`-mt-2 ml-10 h-full rounded-md ${
|
||||
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
|
||||
} `}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,2 +1 @@
|
||||
export * from "./not-ready-view";
|
||||
export * from "./instance-failure-view";
|
||||
|
@ -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 (
|
||||
<div className="relative">
|
||||
<div className="h-screen w-full overflow-hidden overflow-y-auto flex flex-col">
|
||||
<div className="container h-[110px] flex-shrink-0 mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Link href={`${SPACE_BASE_PATH}/`} className="h-[30px] w-[133px]">
|
||||
<Image src={logo} alt="Plane logo" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image src={patternBackground} className="w-screen h-full object-cover" alt="Plane background pattern" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 mb-[110px] flex-grow">
|
||||
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center">
|
||||
<div className="w-auto max-w-2xl relative space-y-8 py-10">
|
||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
||||
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
|
||||
<Image src={PlaneTakeOffImage} alt="Plane Logo" />
|
||||
<p className="font-medium text-base text-onboarding-text-400">
|
||||
Get started by setting up your instance and workspace
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href={GOD_MODE_URL}>
|
||||
<Button size="lg" className="w-full">
|
||||
Get started
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export const IssueBlockDownVotes = ({ number }: { number: number }) => (
|
||||
<div className="flex h-6 items-center rounded border-[0.5px] border-custom-border-300 px-1.5 py-1 pl-1 text-xs text-custom-text-300">
|
||||
<span className="material-symbols-rounded !m-0 rotate-180 !p-0 text-base text-custom-text-300">
|
||||
arrow_upward_alt
|
||||
</span>
|
||||
{number}
|
||||
</div>
|
||||
);
|
@ -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 (
|
||||
<div className="flex items-center gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs text-custom-text-100">
|
||||
<span className={`material-symbols-rounded -my-0.5 text-sm ${iconDetails.className}`}>
|
||||
{iconDetails.iconName}
|
||||
</span>
|
||||
{renderFullDate(due_date)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,19 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export const IssueBlockLabels = ({ labels }: any) => (
|
||||
<div className="relative flex flex-wrap items-center gap-1">
|
||||
{labels &&
|
||||
labels.length > 0 &&
|
||||
labels.map((_label: any) => (
|
||||
<div
|
||||
key={_label?.id}
|
||||
className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: `${_label?.color}` }} />
|
||||
<div className="text-xs">{_label?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
@ -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 (
|
||||
<div className="flex w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs shadow-sm duration-300 focus:outline-none">
|
||||
<div className="flex w-full items-center gap-1.5 text-custom-text-200">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
||||
<div className="text-xs">{state?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,8 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export const IssueBlockUpVotes = ({ number }: { number: number }) => (
|
||||
<div className="flex h-6 items-center rounded border-[0.5px] border-custom-border-300 px-1.5 py-1 pl-1 text-xs text-custom-text-300">
|
||||
<span className="material-symbols-rounded !m-0 !p-0 text-base text-custom-text-300">arrow_upward_alt</span>
|
||||
{number}
|
||||
</div>
|
||||
);
|
@ -1 +0,0 @@
|
||||
export const IssueCalendarView = () => <div> </div>;
|
@ -1 +0,0 @@
|
||||
export const IssueGanttView = () => <div> </div>;
|
@ -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<IssueKanBanBlockProps> = 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 (
|
||||
<div className="flex flex-col gap-1.5 space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs">
|
||||
{/* id */}
|
||||
<div className="break-words text-xs text-custom-text-300">
|
||||
{project?.identifier}-{issue?.sequence_id}
|
||||
</div>
|
||||
|
||||
{/* name */}
|
||||
<h6
|
||||
onClick={handleBlockClick}
|
||||
role="button"
|
||||
className="line-clamp-2 cursor-pointer break-words text-sm font-medium"
|
||||
>
|
||||
{issue.name}
|
||||
</h6>
|
||||
|
||||
<div className="hide-horizontal-scrollbar relative flex w-full flex-grow items-end gap-2 overflow-x-scroll">
|
||||
{/* priority */}
|
||||
{issue?.priority && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockPriority priority={issue?.priority} />
|
||||
</div>
|
||||
)}
|
||||
{/* state */}
|
||||
{issue?.state_detail && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockState state={issue?.state_detail} />
|
||||
</div>
|
||||
)}
|
||||
{/* due date */}
|
||||
{issue?.target_date && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockDueDate due_date={issue?.target_date} group={issue?.state_detail?.group} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -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 (
|
||||
<div className="flex items-center gap-2 px-2 pb-2">
|
||||
<div className="flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="14" width="14" />
|
||||
</div>
|
||||
<div className="mr-1 truncate font-semibold capitalize text-custom-text-200">{state?.name}</div>
|
||||
{/* <span className="flex-shrink-0 rounded-full text-custom-text-300">{getCountOfIssuesByState(state.id)}</span> */}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -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<IssueKanbanViewProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
// store hooks
|
||||
const { states, getFilteredIssuesByState } = useIssue();
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full gap-3 overflow-hidden overflow-x-auto">
|
||||
{states &&
|
||||
states.length > 0 &&
|
||||
states.map((_state: IIssueState) => (
|
||||
<div key={_state.id} className="relative flex h-full w-[340px] flex-shrink-0 flex-col">
|
||||
<div className="flex-shrink-0">
|
||||
<IssueKanBanHeader state={_state} />
|
||||
</div>
|
||||
<div className="hide-vertical-scrollbar h-full w-full overflow-hidden overflow-y-auto">
|
||||
{getFilteredIssuesByState(_state.id) && getFilteredIssuesByState(_state.id).length > 0 ? (
|
||||
<div className="space-y-3 px-2 pb-2">
|
||||
{getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
|
||||
<IssueKanBanBlock
|
||||
key={_issue.id}
|
||||
issue={_issue}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
params={{}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-2 pt-10 text-center text-sm font-medium text-custom-text-200">
|
||||
<Icon iconName="stack" />
|
||||
No issues in this state
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -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<IssueListViewProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
// store hooks
|
||||
const { states, getFilteredIssuesByState } = useIssue();
|
||||
|
||||
return (
|
||||
<>
|
||||
{states &&
|
||||
states.length > 0 &&
|
||||
states.map((_state: IIssueState) => (
|
||||
<div key={_state.id} className="relative w-full">
|
||||
<IssueListHeader state={_state} />
|
||||
{getFilteredIssuesByState(_state.id) && getFilteredIssuesByState(_state.id).length > 0 ? (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
|
||||
<IssueListBlock key={_issue.id} issue={_issue} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-custom-background-100 p-3 text-sm text-custom-text-400">No issues.</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
@ -1 +0,0 @@
|
||||
export const IssueSpreadsheetView = () => <div> </div>;
|
@ -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, " ");
|
||||
|
@ -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<TIssueAppliedFilters> = 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<TIssueAppliedFilters> = 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<TIssueAppliedFilters> = 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 (
|
||||
<div className="border-b border-custom-border-200 p-5 py-3">
|
||||
<div className="border-b border-custom-border-200 bg-custom-background-100 p-4">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters || {}}
|
||||
handleRemoveFilter={handleFilters as any}
|
||||
|
@ -2,13 +2,14 @@
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { X } from "lucide-react";
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
// types
|
||||
import { IIssueState } from "@/types/issue";
|
||||
import { IStateLite } from "@plane/types";
|
||||
// ui
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
states: IIssueState[];
|
||||
states: IStateLite[];
|
||||
values: string[];
|
||||
};
|
||||
|
||||
|
@ -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<IssueFiltersDropdownProps> = 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<IssueFiltersDropdownProps> = 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 (
|
||||
|
@ -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<Props> = observer((props) => {
|
||||
|
@ -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> = (props) => {
|
||||
|
2
space/components/issues/index.ts
Normal file
2
space/components/issues/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./issue-layouts";
|
||||
export * from "./navbar";
|
4
space/components/issues/issue-layouts/index.ts
Normal file
4
space/components/issues/issue-layouts/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./kanban";
|
||||
export * from "./list";
|
||||
export * from "./properties";
|
||||
export * from "./root";
|
78
space/components/issues/issue-layouts/kanban/block.tsx
Normal file
78
space/components/issues/issue-layouts/kanban/block.tsx
Normal file
@ -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<Props> = 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 (
|
||||
<Link
|
||||
href={`/issues/${anchor}?${queryParam}`}
|
||||
onClick={handleBlockClick}
|
||||
className="flex flex-col gap-1.5 space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs select-none"
|
||||
>
|
||||
{/* id */}
|
||||
<div className="break-words text-xs text-custom-text-300">
|
||||
{project_details?.identifier}-{issue?.sequence_id}
|
||||
</div>
|
||||
|
||||
{/* name */}
|
||||
<h6 role="button" className="line-clamp-2 cursor-pointer break-words text-sm">
|
||||
{issue.name}
|
||||
</h6>
|
||||
|
||||
<div className="hide-horizontal-scrollbar relative flex w-full flex-grow items-end gap-2 overflow-x-scroll">
|
||||
{/* priority */}
|
||||
{issue?.priority && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockPriority priority={issue?.priority} />
|
||||
</div>
|
||||
)}
|
||||
{/* state */}
|
||||
{issue?.state_detail && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockState state={issue?.state_detail} />
|
||||
</div>
|
||||
)}
|
||||
{/* due date */}
|
||||
{issue?.target_date && (
|
||||
<div className="flex-shrink-0">
|
||||
<IssueBlockDueDate due_date={issue?.target_date} group={issue?.state_detail?.group} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
});
|
25
space/components/issues/issue-layouts/kanban/header.tsx
Normal file
25
space/components/issues/issue-layouts/kanban/header.tsx
Normal file
@ -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<Props> = observer((props) => {
|
||||
const { state } = props;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-2 pb-2">
|
||||
<div className="flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="14" width="14" />
|
||||
</div>
|
||||
<div className="mr-1 truncate font-medium capitalize text-custom-text-200">{state?.name}</div>
|
||||
{/* <span className="flex-shrink-0 rounded-full text-custom-text-300">{getCountOfIssuesByState(state.id)}</span> */}
|
||||
</div>
|
||||
);
|
||||
});
|
3
space/components/issues/issue-layouts/kanban/index.ts
Normal file
3
space/components/issues/issue-layouts/kanban/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./block";
|
||||
export * from "./header";
|
||||
export * from "./root";
|
50
space/components/issues/issue-layouts/kanban/root.tsx
Normal file
50
space/components/issues/issue-layouts/kanban/root.tsx
Normal file
@ -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<Props> = observer((props) => {
|
||||
const { anchor } = props;
|
||||
// store hooks
|
||||
const { states, getFilteredIssuesByState } = useIssue();
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full gap-3 overflow-hidden overflow-x-auto">
|
||||
{states?.map((state) => {
|
||||
const issues = getFilteredIssuesByState(state.id);
|
||||
|
||||
return (
|
||||
<div key={state.id} className="relative flex h-full w-[340px] flex-shrink-0 flex-col">
|
||||
<div className="flex-shrink-0">
|
||||
<IssueKanBanHeader state={state} />
|
||||
</div>
|
||||
<div className="hide-vertical-scrollbar h-full w-full overflow-hidden overflow-y-auto">
|
||||
{issues && issues.length > 0 ? (
|
||||
<div className="space-y-3 px-2 pb-2">
|
||||
{issues.map((issue) => (
|
||||
<IssueKanBanBlock key={issue.id} anchor={anchor} issue={issue} params={{}} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-2 pt-10 text-center text-sm font-medium text-custom-text-200">
|
||||
<Icon iconName="stack" />
|
||||
No issues in this state
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -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<IssueListBlockProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issue } = props;
|
||||
const searchParams = useSearchParams();
|
||||
export const IssueListLayoutBlock: FC<IssueListBlockProps> = 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 (
|
||||
<div className="relative flex items-center gap-10 bg-custom-background-100 p-3">
|
||||
<Link
|
||||
href={`/issues/${anchor}?${queryParam}`}
|
||||
onClick={handleBlockClick}
|
||||
className="relative flex items-center gap-10 bg-custom-background-100 p-3"
|
||||
>
|
||||
<div className="relative flex w-full flex-grow items-center gap-3 overflow-hidden">
|
||||
{/* id */}
|
||||
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
|
||||
{project?.identifier}-{issue?.sequence_id}
|
||||
{project_details?.identifier}-{issue?.sequence_id}
|
||||
</div>
|
||||
{/* name */}
|
||||
<div onClick={handleBlockClick} className="flex-grow cursor-pointer truncate text-sm font-medium">
|
||||
<div onClick={handleBlockClick} className="flex-grow cursor-pointer truncate text-sm">
|
||||
{issue.name}
|
||||
</div>
|
||||
</div>
|
||||
@ -84,6 +80,6 @@ export const IssueListBlock: FC<IssueListBlockProps> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
});
|
@ -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<Props> = observer((props) => {
|
||||
const { state } = props;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 p-3">
|
3
space/components/issues/issue-layouts/list/index.ts
Normal file
3
space/components/issues/issue-layouts/list/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./block";
|
||||
export * from "./header";
|
||||
export * from "./root";
|
40
space/components/issues/issue-layouts/list/root.tsx
Normal file
40
space/components/issues/issue-layouts/list/root.tsx
Normal file
@ -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<Props> = observer((props) => {
|
||||
const { anchor } = props;
|
||||
// store hooks
|
||||
const { states, getFilteredIssuesByState } = useIssue();
|
||||
|
||||
return (
|
||||
<>
|
||||
{states?.map((state) => {
|
||||
const issues = getFilteredIssuesByState(state.id);
|
||||
|
||||
return (
|
||||
<div key={state.id} className="relative w-full">
|
||||
<IssueListLayoutHeader state={state} />
|
||||
{issues && issues.length > 0 ? (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{issues.map((issue) => (
|
||||
<IssueListLayoutBlock key={issue.id} anchor={anchor} issue={issue} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-custom-background-100 p-3 text-sm text-custom-text-400">No issues.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs text-custom-text-100",
|
||||
{
|
||||
"text-red-500": shouldHighlightIssueDueDate(due_date, group),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<CalendarCheck2 className="size-3 flex-shrink-0" />
|
||||
{renderFormattedDate(due_date)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export * from "./due-date";
|
||||
export * from "./labels";
|
||||
export * from "./priority";
|
||||
export * from "./state";
|
17
space/components/issues/issue-layouts/properties/labels.tsx
Normal file
17
space/components/issues/issue-layouts/properties/labels.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
export const IssueBlockLabels = ({ labels }: any) => (
|
||||
<div className="relative flex flex-wrap items-center gap-1">
|
||||
{labels?.map((_label: any) => (
|
||||
<div
|
||||
key={_label?.id}
|
||||
className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: `${_label?.color}` }} />
|
||||
<div className="text-xs">{_label?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
@ -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 <></>;
|
11
space/components/issues/issue-layouts/properties/state.tsx
Normal file
11
space/components/issues/issue-layouts/properties/state.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
// ui
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
|
||||
export const IssueBlockState = ({ state }: any) => (
|
||||
<div className="flex w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs shadow-sm duration-300 focus:outline-none">
|
||||
<div className="flex w-full items-center gap-1.5">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
||||
<div className="text-xs">{state?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
@ -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<ProjectDetailsViewProps> = observer((props) => {
|
||||
// router
|
||||
const searchParams = useSearchParams();
|
||||
export const IssuesLayoutsRoot: FC<Props> = 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 (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
{workspaceSlug && projectId && peekId && (
|
||||
<IssuePeekOverview workspaceSlug={workspaceSlug} projectId={projectId} peekId={peekId} />
|
||||
)}
|
||||
{peekId && <IssuePeekOverview anchor={anchor} peekId={peekId} />}
|
||||
|
||||
{loader && !issues ? (
|
||||
<div className="py-10 text-center text-sm text-custom-text-100">Loading...</div>
|
||||
@ -90,21 +76,18 @@ export const ProjectDetailsView: FC<ProjectDetailsViewProps> = observer((props)
|
||||
activeLayout && (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
{/* applied filters */}
|
||||
<IssueAppliedFilters workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<IssueAppliedFilters anchor={anchor} />
|
||||
|
||||
{activeLayout === "list" && (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<IssueListView workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<IssuesListLayoutRoot anchor={anchor} />
|
||||
</div>
|
||||
)}
|
||||
{activeLayout === "kanban" && (
|
||||
<div className="relative mx-auto h-full w-full p-5">
|
||||
<IssueKanbanView workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<IssueKanbanLayoutRoot anchor={anchor} />
|
||||
</div>
|
||||
)}
|
||||
{activeLayout === "calendar" && <IssueCalendarView />}
|
||||
{activeLayout === "spreadsheet" && <IssueSpreadsheetView />}
|
||||
{activeLayout === "gantt" && <IssueGanttView />}
|
||||
</div>
|
||||
)
|
||||
)}
|
@ -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<NavbarControlsProps> = 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<NavbarControlsProps> = 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<NavbarControlsProps> = 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 */}
|
||||
<div className="relative flex flex-shrink-0 items-center gap-1 transition-all delay-150 ease-in-out">
|
||||
<NavbarIssueBoardView workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<IssuesLayoutSelection anchor={anchor} />
|
||||
</div>
|
||||
|
||||
{/* issue filters */}
|
||||
<div className="relative flex flex-shrink-0 items-center gap-1 transition-all delay-150 ease-in-out">
|
||||
<IssueFiltersDropdown workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<IssueFiltersDropdown anchor={anchor} />
|
||||
</div>
|
||||
|
||||
{/* theming */}
|
||||
|
5
space/components/issues/navbar/index.ts
Normal file
5
space/components/issues/navbar/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from "./controls";
|
||||
export * from "./layout-selection";
|
||||
export * from "./root";
|
||||
export * from "./theme";
|
||||
export * from "./user-avatar";
|
@ -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<NavbarIssueBoardViewProps> = 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 (
|
||||
<div
|
||||
key={layoutKey}
|
||||
className={`flex h-[28px] w-[28px] cursor-pointer items-center justify-center rounded-sm ${
|
||||
layoutKey === activeLayout
|
||||
? `bg-custom-background-80 text-custom-text-200`
|
||||
: `text-custom-text-300 hover:bg-custom-background-80`
|
||||
}`}
|
||||
onClick={() => handleCurrentBoardView(layoutKey)}
|
||||
title={layoutKey}
|
||||
>
|
||||
<span
|
||||
className={`material-symbols-rounded text-[18px] ${
|
||||
issueLayoutViews[layoutKey]?.className ? issueLayoutViews[layoutKey]?.className : ``
|
||||
}`}
|
||||
>
|
||||
{issueLayoutViews[layoutKey]?.icon}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
67
space/components/issues/navbar/layout-selection.tsx
Normal file
67
space/components/issues/navbar/layout-selection.tsx
Normal file
@ -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<Props> = 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 (
|
||||
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
|
||||
{ISSUE_LAYOUTS.map((layout) => {
|
||||
if (!layoutOptions[layout.key]) return;
|
||||
|
||||
return (
|
||||
<Tooltip key={layout.key} tooltipContent={layout.title}>
|
||||
<button
|
||||
type="button"
|
||||
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
||||
activeLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
||||
}`}
|
||||
onClick={() => handleCurrentBoardView(layout.key)}
|
||||
>
|
||||
<layout.icon
|
||||
strokeWidth={2}
|
||||
className={`size-3.5 ${activeLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"}`}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -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<IssueNavbarProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
export const IssuesNavbarRoot: FC<Props> = observer((props) => {
|
||||
const { publishSettings } = props;
|
||||
// hooks
|
||||
const { project } = useProject();
|
||||
const { project_details } = publishSettings;
|
||||
|
||||
return (
|
||||
<div className="relative flex justify-between w-full gap-4 px-5">
|
||||
{/* project detail */}
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
{project ? (
|
||||
{project_details ? (
|
||||
<span className="h-7 w-7 flex-shrink-0 grid place-items-center">
|
||||
<ProjectLogo logo={project.logo_props} className="text-lg" />
|
||||
<ProjectLogo logo={project_details.logo_props} className="text-lg" />
|
||||
</span>
|
||||
) : (
|
||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
||||
<Briefcase className="h-4 w-4" />
|
||||
</span>
|
||||
)}
|
||||
<div className="line-clamp-1 max-w-[300px] overflow-hidden text-lg font-medium">{project?.name || `...`}</div>
|
||||
<div className="line-clamp-1 max-w-[300px] overflow-hidden text-lg font-medium">
|
||||
{project_details?.name || `...`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<NavbarControls workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<NavbarControls publishSettings={publishSettings} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default IssueNavbar;
|
@ -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<Comment> = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
anchor: string;
|
||||
disabled?: boolean;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const AddComment: React.FC<Props> = observer((props) => {
|
||||
// const { disabled = false } = props;
|
||||
const { workspaceSlug, projectId } = props;
|
||||
const { anchor } = props;
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(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<Props> = observer((props) => {
|
||||
} = useForm<Comment>({ 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<Props> = 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)
|
||||
|
@ -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<Props> = 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<Props> = 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<Props> = observer((props) => {
|
||||
name="comment_html"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<LiteTextEditor
|
||||
workspaceId={workspaceId as string}
|
||||
workspaceSlug={workspaceSlug}
|
||||
workspaceId={workspaceID?.toString() ?? ""}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
onEnterKeyPress={handleSubmit(handleCommentUpdate)}
|
||||
ref={editorRef}
|
||||
initialValue={value}
|
||||
@ -135,7 +133,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
</form>
|
||||
<div className={`${isEditing ? "hidden" : ""}`}>
|
||||
<LiteTextReadOnlyEditor ref={showEditorRef} initialValue={comment.comment_html} />
|
||||
<CommentReactions commentId={comment.id} projectId={comment.project} workspaceSlug={workspaceSlug} />
|
||||
<CommentReactions anchor={anchor} commentId={comment.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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<Props> = observer((props) => {
|
||||
const { anchor, commentId } = props;
|
||||
const router = useRouter();
|
||||
const pathName = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
@ -28,7 +28,6 @@ export const CommentReactions: React.FC<Props> = 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<Props> = 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) => {
|
||||
|
@ -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<Props> = observer((props) => {
|
||||
const { handleClose, issueDetails, workspaceSlug, projectId } = props;
|
||||
const { anchor, handleClose, issueDetails } = props;
|
||||
|
||||
return (
|
||||
<div className="grid h-full w-full grid-cols-10 divide-x divide-custom-border-200 overflow-hidden">
|
||||
@ -30,17 +29,13 @@ export const FullScreenPeekView: React.FC<Props> = observer((props) => {
|
||||
<div className="h-full w-full overflow-y-auto px-6">
|
||||
{/* issue title and description */}
|
||||
<div className="w-full">
|
||||
<PeekOverviewIssueDetails issueDetails={issueDetails} />
|
||||
<PeekOverviewIssueDetails anchor={anchor} issueDetails={issueDetails} />
|
||||
</div>
|
||||
{/* divider */}
|
||||
<div className="my-5 h-[1] w-full border-t border-custom-border-200" />
|
||||
{/* issue activity/comments */}
|
||||
<div className="w-full pb-5">
|
||||
<PeekOverviewIssueActivity
|
||||
issueDetails={issueDetails}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<PeekOverviewIssueActivity anchor={anchor} issueDetails={issueDetails} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
@ -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<Props> = 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 (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
{peekMode === "side" && (
|
||||
<button type="button" onClick={handleClose}>
|
||||
<MoveRight className="h-4 w-4" strokeWidth={2} />
|
||||
<button type="button" onClick={handleClose} className="text-custom-text-300 hover:text-custom-text-200">
|
||||
<MoveRight className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
<Listbox
|
||||
@ -69,8 +70,10 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
onChange={(val) => setPeekMode(val)}
|
||||
className="relative flex-shrink-0 text-left"
|
||||
>
|
||||
<Listbox.Button className={`grid place-items-center ${peekMode === "full" ? "rotate-45" : ""}`}>
|
||||
<Icon iconName={peekModes.find((m) => m.key === peekMode)?.icon ?? ""} className="text-[1rem]" />
|
||||
<Listbox.Button
|
||||
className={`grid place-items-center text-custom-text-300 hover:text-custom-text-200 ${peekMode === "full" ? "rotate-45" : ""}`}
|
||||
>
|
||||
<Icon className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
@ -84,7 +87,7 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
>
|
||||
<Listbox.Options className="absolute left-0 z-10 mt-1 min-w-[8rem] origin-top-left overflow-y-auto whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 text-xs shadow-lg focus:outline-none">
|
||||
<div className="space-y-1 p-2">
|
||||
{peekModes.map((mode) => (
|
||||
{PEEK_MODES.map((mode) => (
|
||||
<Listbox.Option
|
||||
key={mode.key}
|
||||
value={mode.key}
|
||||
@ -117,8 +120,13 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
{isClipboardWriteAllowed && (peekMode === "side" || peekMode === "modal") && (
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<button type="button" onClick={handleCopyLink} className="-rotate-45 focus:outline-none" tabIndex={1}>
|
||||
<Icon iconName="link" className="text-[1rem]" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyLink}
|
||||
className="focus:outline-none text-custom-text-300 hover:text-custom-text-200"
|
||||
tabIndex={1}
|
||||
>
|
||||
<Link2 className="h-4 w-4 -rotate-45" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
@ -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<Props> = 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 (
|
||||
<div className="pb-10">
|
||||
<h4 className="font-medium">Comments</h4>
|
||||
{workspaceSlug && (
|
||||
<div className="mt-4">
|
||||
<div className="space-y-4">
|
||||
{comments.map((comment: any) => (
|
||||
<CommentCard key={comment.id} comment={comment} workspaceSlug={workspaceSlug?.toString()} />
|
||||
))}
|
||||
</div>
|
||||
{!isInIframe &&
|
||||
(currentUser ? (
|
||||
<>
|
||||
{canComment && (
|
||||
<div className="mt-4">
|
||||
<AddComment disabled={!currentUser} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-4 flex items-center justify-between gap-2 rounded border border-custom-border-300 bg-custom-background-80 px-2 py-2.5">
|
||||
<p className="flex gap-2 overflow-hidden break-words text-sm text-custom-text-200">
|
||||
<Icon iconName="lock" className="!text-sm" />
|
||||
Sign in to add your comment
|
||||
</p>
|
||||
<Link href={`/?next_path=${pathname}`}>
|
||||
<Button variant="primary">Sign in</Button>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-4">
|
||||
<div className="space-y-4">
|
||||
{comments.map((comment) => (
|
||||
<CommentCard key={comment.id} anchor={anchor} comment={comment} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!isInIframe &&
|
||||
(currentUser ? (
|
||||
<>
|
||||
{canComment && (
|
||||
<div className="mt-4">
|
||||
<AddComment anchor={anchor} disabled={!currentUser} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-4 flex items-center justify-between gap-2 rounded border border-custom-border-300 bg-custom-background-80 px-2 py-2.5">
|
||||
<p className="flex gap-2 overflow-hidden break-words text-sm text-custom-text-200">
|
||||
<Icon iconName="lock" className="!text-sm" />
|
||||
Sign in to add your comment
|
||||
</p>
|
||||
<Link href={`/?next_path=${pathname}`}>
|
||||
<Button variant="primary">Sign in</Button>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -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<Props> = ({ issueDetails }) => (
|
||||
<div className="space-y-2">
|
||||
<h6 className="font-medium text-custom-text-200">
|
||||
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
|
||||
</h6>
|
||||
<h4 className="break-words text-2xl font-semibold">{issueDetails.name}</h4>
|
||||
{issueDetails.description_html !== "" && issueDetails.description_html !== "<p></p>" && (
|
||||
<RichTextReadOnlyEditor
|
||||
initialValue={
|
||||
!issueDetails.description_html ||
|
||||
issueDetails.description_html === "" ||
|
||||
(typeof issueDetails.description_html === "object" && Object.keys(issueDetails.description_html).length === 0)
|
||||
? "<p></p>"
|
||||
: issueDetails.description_html
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<IssueReactions />
|
||||
</div>
|
||||
);
|
||||
export const PeekOverviewIssueDetails: React.FC<Props> = (props) => {
|
||||
const { anchor, issueDetails } = props;
|
||||
|
||||
const description = issueDetails.description_html;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h6 className="text-base font-medium text-custom-text-400">
|
||||
{issueDetails.project_detail?.identifier}-{issueDetails?.sequence_id}
|
||||
</h6>
|
||||
<h4 className="break-words text-2xl font-medium">{issueDetails.name}</h4>
|
||||
{description !== "" && description !== "<p></p>" && (
|
||||
<RichTextReadOnlyEditor
|
||||
initialValue={
|
||||
!description ||
|
||||
description === "" ||
|
||||
(typeof description === "object" && Object.keys(description).length === 0)
|
||||
? "<p></p>"
|
||||
: description
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<IssueReactions anchor={anchor} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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<IssueEmojiReactionsProps> = observer((props) => {
|
||||
const { anchor } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const pathName = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
@ -25,11 +25,9 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = 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<IssueEmojiReactionsProps> = 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<IssueEmojiReactionsProps> = observer(
|
||||
else handleAddReaction(reactionHex);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user) return;
|
||||
fetchCurrentUser();
|
||||
}, [user, fetchCurrentUser]);
|
||||
|
||||
// derived values
|
||||
const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels });
|
||||
|
||||
|
@ -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<Props> = ({ 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<Props> = ({ issueDetails, mod
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={`space-y-4 ${mode === "full" ? "pt-3" : ""}`}>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex w-1/4 flex-shrink-0 items-center gap-2 font-medium">
|
||||
<Icon iconName="radio_button_checked" className="flex-shrink-0 !text-base" />
|
||||
<span className="flex-grow truncate">State</span>
|
||||
<div className={`space-y-2 ${mode === "full" ? "pt-3" : ""}`}>
|
||||
<div className="flex items-center gap-3 h-8">
|
||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<DoubleCircleIcon className="size-4 flex-shrink-0" />
|
||||
<span>State</span>
|
||||
</div>
|
||||
<div className="w-3/4">
|
||||
{stateGroup && (
|
||||
<div className="inline-flex rounded bg-custom-background-80 px-2.5 py-0.5 text-sm">
|
||||
<div className="flex items-center gap-1.5 text-left text-custom-text-100">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
||||
{addSpaceIfCamelCase(state?.name ?? "")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-3/4 flex items-center gap-1.5 py-0.5 text-sm">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
||||
{addSpaceIfCamelCase(state?.name ?? "")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex w-1/4 flex-shrink-0 items-center gap-2 font-medium">
|
||||
<Icon iconName="signal_cellular_alt" className="flex-shrink-0 !text-base" />
|
||||
<span className="flex-grow truncate">Priority</span>
|
||||
<div className="flex items-center gap-3 h-8">
|
||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<Signal className="size-4 flex-shrink-0" />
|
||||
<span>Priority</span>
|
||||
</div>
|
||||
<div className="w-3/4">
|
||||
<div
|
||||
@ -97,18 +89,24 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex w-1/4 flex-shrink-0 items-center gap-2 font-medium">
|
||||
<Icon iconName="calendar_today" className="flex-shrink-0 !text-base" />
|
||||
<span className="flex-grow truncate">Due date</span>
|
||||
|
||||
<div className="flex items-center gap-3 h-8">
|
||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CalendarCheck2 className="size-4 flex-shrink-0" />
|
||||
<span>Due date</span>
|
||||
</div>
|
||||
<div>
|
||||
{issueDetails.target_date ? (
|
||||
<div className="flex h-6 items-center gap-1 rounded border border-custom-border-100 bg-custom-background-80 px-2.5 py-1 text-xs text-custom-text-100">
|
||||
<span className={`material-symbols-rounded -my-0.5 text-sm ${dueDateIcon.className}`}>
|
||||
{dueDateIcon.iconName}
|
||||
</span>
|
||||
{renderFullDate(issueDetails.target_date)}
|
||||
<div
|
||||
className={cn("flex items-center gap-1.5 rounded py-0.5 text-xs text-custom-text-100", {
|
||||
"text-red-500": shouldHighlightIssueDueDate(
|
||||
issueDetails.target_date,
|
||||
issueDetails.state_detail.group
|
||||
),
|
||||
})}
|
||||
>
|
||||
<CalendarCheck2 className="size-3" />
|
||||
{renderFormattedDate(issueDetails.target_date)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-custom-text-200">Empty</span>
|
||||
|
@ -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<any>();
|
||||
|
||||
const { canVote, canReact } = useProject();
|
||||
export const IssueReactions: React.FC<Props> = observer((props) => {
|
||||
const { anchor } = props;
|
||||
// store hooks
|
||||
const { canVote, canReact } = usePublish(anchor);
|
||||
const isInIframe = useIsInIframe();
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
{canVote && (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<IssueVotes workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
</>
|
||||
<div className="flex items-center gap-2">
|
||||
<IssueVotes anchor={anchor} />
|
||||
</div>
|
||||
)}
|
||||
{!isInIframe && canReact && (
|
||||
<div className="flex items-center gap-2">
|
||||
<IssueEmojiReactions workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<IssueEmojiReactions anchor={anchor} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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<TIssueVotes> = 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<TIssueVotes> = 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<TIssueVotes> = 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
|
||||
|
@ -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<TIssuePeekOverview> = 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<TIssuePeekOverview> = 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<TIssuePeekOverview> = observer((props) => {
|
||||
leaveTo="translate-x-full"
|
||||
>
|
||||
<Dialog.Panel className="fixed right-0 top-0 z-20 h-full w-1/2 bg-custom-background-100 shadow-custom-shadow-sm">
|
||||
<SidePeekView
|
||||
handleClose={handleClose}
|
||||
issueDetails={issueDetails}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<SidePeekView anchor={anchor} handleClose={handleClose} issueDetails={issueDetails} />
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</Dialog>
|
||||
@ -119,20 +115,10 @@ export const IssuePeekOverview: FC<TIssuePeekOverview> = observer((props) => {
|
||||
}`}
|
||||
>
|
||||
{issueDetailStore.peekMode === "modal" && (
|
||||
<SidePeekView
|
||||
handleClose={handleClose}
|
||||
issueDetails={issueDetails}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<SidePeekView anchor={anchor} handleClose={handleClose} issueDetails={issueDetails} />
|
||||
)}
|
||||
{issueDetailStore.peekMode === "full" && (
|
||||
<FullScreenPeekView
|
||||
handleClose={handleClose}
|
||||
issueDetails={issueDetails}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<FullScreenPeekView anchor={anchor} handleClose={handleClose} issueDetails={issueDetails} />
|
||||
)}
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
|
@ -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<Props> = observer((props) => {
|
||||
const { handleClose, issueDetails, workspaceSlug, projectId } = props;
|
||||
|
||||
const { settings } = useProject();
|
||||
const { anchor, handleClose, issueDetails } = props;
|
||||
// store hooks
|
||||
const { canComment } = usePublish(anchor);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
@ -33,7 +32,7 @@ export const SidePeekView: React.FC<Props> = observer((props) => {
|
||||
<div className="h-full w-full overflow-y-auto px-6">
|
||||
{/* issue title and description */}
|
||||
<div className="w-full">
|
||||
<PeekOverviewIssueDetails issueDetails={issueDetails} />
|
||||
<PeekOverviewIssueDetails anchor={anchor} issueDetails={issueDetails} />
|
||||
</div>
|
||||
{/* issue properties */}
|
||||
<div className="mt-6 w-full">
|
||||
@ -42,13 +41,9 @@ export const SidePeekView: React.FC<Props> = observer((props) => {
|
||||
{/* divider */}
|
||||
<div className="my-5 h-[1] w-full border-t border-custom-border-200" />
|
||||
{/* issue activity/comments */}
|
||||
{settings?.comments && (
|
||||
{canComment && (
|
||||
<div className="w-full pb-5">
|
||||
<PeekOverviewIssueActivity
|
||||
issueDetails={issueDetails}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<PeekOverviewIssueActivity anchor={anchor} issueDetails={issueDetails} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -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<DropDownListProps> = (props) => {
|
||||
const { open, items, handleClose } = props;
|
||||
|
||||
const ref = useRef(null);
|
||||
|
||||
useOutSideClick(ref, () => {
|
||||
if (handleClose) handleClose();
|
||||
});
|
||||
|
||||
return (
|
||||
<Popover className="absolute -left-1">
|
||||
<Transition
|
||||
show={open}
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel
|
||||
ref={ref}
|
||||
className="absolute left-1/2 z-10 mt-1 max-w-[9rem] origin-top-right -translate-x-full select-none rounded-md border border-custom-border-300 bg-custom-background-90 text-xs shadow-lg focus:outline-none"
|
||||
>
|
||||
<div className="w-full rounded-md text-sm shadow-lg">
|
||||
{items.map((item, index) => (
|
||||
<DropdownItem key={index} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const DropdownItem: React.FC<DropdownItemProps> = (props) => {
|
||||
const { item } = props;
|
||||
const { display, children, as: itemAs, href, onClick, isSelected } = item;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="group relative flex w-full gap-x-6 rounded-lg p-1">
|
||||
{(!itemAs || itemAs === "button" || itemAs === "div") && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!children) {
|
||||
if (onClick) onClick();
|
||||
return;
|
||||
}
|
||||
setOpen((prev) => !prev);
|
||||
}}
|
||||
className={`flex w-full items-center gap-1 rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80 ${
|
||||
isSelected ? "bg-custom-background-80" : ""
|
||||
}`}
|
||||
>
|
||||
{children && <ChevronLeft className="h-4 w-4 transform transition-transform" strokeWidth={2} />}
|
||||
{!children && <span />}
|
||||
<span className="truncate text-xs">{display}</span>
|
||||
<Check className={`h-3 w-3 opacity-0 ${isSelected ? "opacity-100" : ""}`} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{itemAs === "link" && <Link href={href || "#"}>{display}</Link>}
|
||||
|
||||
{children && <DropdownList open={open} handleClose={() => setOpen(false)} items={children} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Dropdown: React.FC<DropdownProps> = (props) => {
|
||||
const { button, items } = props;
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`group flex items-center justify-between gap-2 rounded-md border border-custom-border-200 px-3 py-1.5 text-xs shadow-sm duration-300 hover:bg-custom-background-90 hover:text-custom-text-100 focus:outline-none ${
|
||||
open ? "bg-custom-background-90 text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
{typeof button === "function" ? button() : button}
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute left-full z-10 mt-1 w-36 origin-top-right -translate-x-full select-none rounded-md border border-custom-border-300 bg-custom-background-90 text-xs shadow-lg focus:outline-none">
|
||||
<div className="w-full">
|
||||
{items.map((item, index) => (
|
||||
<DropdownItem key={index} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export { Dropdown };
|
@ -1,3 +1,2 @@
|
||||
export * from "./dropdown";
|
||||
export * from "./icon";
|
||||
export * from "./reaction-selector";
|
||||
|
@ -1,2 +1 @@
|
||||
export * from "./auth";
|
||||
export * from "./project-details";
|
||||
|
@ -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<TIssueLayoutViews> = {
|
||||
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;
|
||||
};
|
||||
|
37
space/constants/state.ts
Normal file
37
space/constants/state.ts
Normal file
@ -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];
|
@ -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" },
|
||||
];
|
@ -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;
|
||||
};
|
||||
|
@ -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
|
||||
|
30
space/helpers/issue.helper.ts
Normal file
30
space/helpers/issue.helper.ts
Normal file
@ -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;
|
||||
};
|
@ -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);
|
||||
|
@ -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";
|
||||
|
2
space/hooks/store/publish/index.ts
Normal file
2
space/hooks/store/publish/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./use-publish-list";
|
||||
export * from "./use-publish";
|
11
space/hooks/store/publish/use-publish-list.ts
Normal file
11
space/hooks/store/publish/use-publish-list.ts
Normal file
@ -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;
|
||||
};
|
11
space/hooks/store/publish/use-publish.ts
Normal file
11
space/hooks/store/publish/use-publish.ts
Normal file
@ -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] ?? {};
|
||||
};
|
@ -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;
|
||||
};
|
@ -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();
|
||||
|
@ -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}</>;
|
||||
});
|
@ -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",
|
||||
|
@ -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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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();
|
||||
|
@ -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<any> {
|
||||
return this.get(`/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/issues/`, {
|
||||
async fetchPublicIssues(anchor: string, params: any): Promise<TIssuesResponse> {
|
||||
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<any> {
|
||||
return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/`)
|
||||
async getIssueById(anchor: string, issueID: string): Promise<any> {
|
||||
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<any> {
|
||||
return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/votes/`)
|
||||
async getIssueVotes(anchor: string, issueID: string): Promise<any> {
|
||||
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<any> {
|
||||
return this.post(
|
||||
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/votes/`,
|
||||
data
|
||||
)
|
||||
async createIssueVote(anchor: string, issueID: string, data: any): Promise<any> {
|
||||
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<any> {
|
||||
return this.delete(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/votes/`)
|
||||
async deleteIssueVote(anchor: string, issueID: string): Promise<any> {
|
||||
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<any> {
|
||||
return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/reactions/`)
|
||||
async getIssueReactions(anchor: string, issueID: string): Promise<any> {
|
||||
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<any> {
|
||||
return this.post(
|
||||
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/reactions/`,
|
||||
data
|
||||
)
|
||||
async createIssueReaction(anchor: string, issueID: string, data: any): Promise<any> {
|
||||
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<any> {
|
||||
return this.delete(
|
||||
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/reactions/${reactionId}/`
|
||||
)
|
||||
async deleteIssueReaction(anchor: string, issueID: string, reactionId: string): Promise<any> {
|
||||
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<any> {
|
||||
return this.get(`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`)
|
||||
async getIssueComments(anchor: string, issueID: string): Promise<any> {
|
||||
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<any> {
|
||||
return this.post(
|
||||
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`,
|
||||
data
|
||||
)
|
||||
async createIssueComment(anchor: string, issueID: string, data: any): Promise<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
return this.delete(
|
||||
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/${commentId}/`
|
||||
)
|
||||
async deleteIssueComment(anchor: string, issueID: string, commentId: string): Promise<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
return this.delete(
|
||||
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/${reactionHex}/`
|
||||
)
|
||||
async deleteCommentReaction(anchor: string, commentId: string, reactionHex: string): Promise<any> {
|
||||
return this.delete(`/api/public/anchor/${anchor}/comments/${commentId}/reactions/${reactionHex}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
|
@ -9,16 +9,16 @@ export class ProjectMemberService extends APIService {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetchProjectMembers(workspaceSlug: string, projectId: string): Promise<IProjectMembership[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`)
|
||||
async fetchProjectMembers(anchor: string): Promise<IProjectMembership[]> {
|
||||
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<IProjectMember> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`)
|
||||
async getProjectMember(anchor: string, memberID: string): Promise<IProjectMember> {
|
||||
return this.get(`/api/anchor/${anchor}/members/${memberID}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
|
@ -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<any> {
|
||||
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;
|
29
space/services/publish.service.ts
Normal file
29
space/services/publish.service.ts
Normal file
@ -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<TPublishSettings> {
|
||||
return this.get(`/api/public/anchor/${anchor}/settings/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchAnchorFromProjectDetails(workspaceSlug: string, projectID: string): Promise<TPublishSettings> {
|
||||
return this.get(`/api/public/workspaces/${workspaceSlug}/projects/${projectID}/anchor/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default PublishService;
|
@ -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<void>;
|
||||
updateIssueComment: (
|
||||
workspaceId: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
comment_id: string,
|
||||
data: any
|
||||
) => Promise<any>;
|
||||
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<void>;
|
||||
removeIssueVote: (workspaceId: string, projectId: string, issueId: string) => Promise<void>;
|
||||
// issue actions
|
||||
fetchIssueDetails: (anchor: string, issueID: string) => void;
|
||||
// comment actions
|
||||
addIssueComment: (anchor: string, issueID: string, data: any) => Promise<void>;
|
||||
updateIssueComment: (anchor: string, issueID: string, commentID: string, data: any) => Promise<any>;
|
||||
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<void>;
|
||||
removeIssueVote: (anchor: string, issueID: string) => Promise<void>;
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
@ -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: <K extends keyof TIssueFilters>(
|
||||
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 <K extends keyof TIssueFilters>(
|
||||
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;
|
||||
}
|
||||
|
@ -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<void>;
|
||||
getCountOfIssuesByState: (state: string) => number;
|
||||
getFilteredIssuesByState: (state: string) => IIssue[];
|
||||
fetchPublicIssues: (anchor: string, params: any) => Promise<void>;
|
||||
// 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) || []
|
||||
);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user