Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9975dfa24 | |||
| 380ad340a6 | |||
| f562fcd466 | |||
| 8c8668a3e6 | |||
| 9ce85cdf21 | |||
| 1c6cdb8328 | |||
| 005b42cb8d | |||
| be062ccd34 | |||
| c9d0c5353d | |||
| 2a6eb5fe23 | |||
| d9ccce41bc | |||
| 3db69a3a71 | |||
| faa50b0bbb | |||
| 1991e09035 | |||
| 4fcd081d27 | |||
| 5f1209f1db | |||
| 88e5a05253 | |||
| 981acc81c1 | |||
| cf306ee605 | |||
| 9df0ba6e3a | |||
| b6744dcd29 | |||
| a164dfd532 | |||
| 2b46e5f977 | |||
| 97c3fb40e7 | |||
| 5aad6c71da | |||
| a1ae338c37 | |||
| c16b0daa22 | |||
| 9a29896291 | |||
| a66dcb9419 | |||
| 87a920174e | |||
| 584192faba | |||
| b61adbed4b | |||
| 7434800999 | |||
| 78095e3823 | |||
| f41086fd26 | |||
| 0866dc3494 | |||
| 6ea15ced02 | |||
| 11525f26d0 | |||
| f3bd1691ce | |||
| d83a76a3aa | |||
| 2cd431b4a4 | |||
| d22e4b8212 | |||
| 0e0e09c4fd |
@@ -1,4 +1,4 @@
|
||||
import os, sys
|
||||
import os, sys, random, string
|
||||
import uuid
|
||||
|
||||
sys.path.append("/code")
|
||||
@@ -19,9 +19,9 @@ def populate():
|
||||
user = User.objects.create(email=default_email, username=uuid.uuid4().hex)
|
||||
user.set_password(default_password)
|
||||
user.save()
|
||||
print("User created")
|
||||
|
||||
print("Success")
|
||||
print(f"User created with an email: {default_email}")
|
||||
else:
|
||||
print(f"User already exists with the default email: {default_email}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
from .base import BaseSerializer
|
||||
from .people import (
|
||||
ChangePasswordSerializer,
|
||||
ResetPasswordSerializer,
|
||||
TokenSerializer,
|
||||
)
|
||||
from .user import UserSerializer, UserLiteSerializer
|
||||
from .user import UserSerializer, UserLiteSerializer, ChangePasswordSerializer, ResetPasswordSerializer, UserAdminLiteSerializer
|
||||
from .workspace import (
|
||||
WorkSpaceSerializer,
|
||||
WorkSpaceMemberSerializer,
|
||||
@@ -12,6 +7,7 @@ from .workspace import (
|
||||
WorkSpaceMemberInviteSerializer,
|
||||
WorkspaceLiteSerializer,
|
||||
WorkspaceThemeSerializer,
|
||||
WorkspaceMemberAdminSerializer,
|
||||
)
|
||||
from .project import (
|
||||
ProjectSerializer,
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
from rest_framework.serializers import (
|
||||
ModelSerializer,
|
||||
Serializer,
|
||||
CharField,
|
||||
SerializerMethodField,
|
||||
)
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
|
||||
from plane.db.models import User
|
||||
|
||||
|
||||
class UserSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = "__all__"
|
||||
extra_kwargs = {"password": {"write_only": True}}
|
||||
|
||||
|
||||
class ChangePasswordSerializer(Serializer):
|
||||
model = User
|
||||
|
||||
"""
|
||||
Serializer for password change endpoint.
|
||||
"""
|
||||
old_password = CharField(required=True)
|
||||
new_password = CharField(required=True)
|
||||
|
||||
|
||||
class ResetPasswordSerializer(Serializer):
|
||||
model = User
|
||||
|
||||
"""
|
||||
Serializer for password change endpoint.
|
||||
"""
|
||||
new_password = CharField(required=True)
|
||||
confirm_password = CharField(required=True)
|
||||
|
||||
|
||||
class TokenSerializer(ModelSerializer):
|
||||
|
||||
user = UserSerializer()
|
||||
access_token = SerializerMethodField()
|
||||
refresh_token = SerializerMethodField()
|
||||
|
||||
def get_access_token(self, obj):
|
||||
refresh_token = RefreshToken.for_user(obj.user)
|
||||
return str(refresh_token.access_token)
|
||||
|
||||
def get_refresh_token(self, obj):
|
||||
refresh_token = RefreshToken.for_user(obj.user)
|
||||
return str(refresh_token)
|
||||
|
||||
class Meta:
|
||||
model = Token
|
||||
fields = "__all__"
|
||||
@@ -7,7 +7,7 @@ from rest_framework import serializers
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer
|
||||
from plane.api.serializers.user import UserLiteSerializer
|
||||
from plane.api.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectMember,
|
||||
@@ -110,6 +110,17 @@ class ProjectMemberSerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ProjectMemberAdminSerializer(BaseSerializer):
|
||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||
project = ProjectLiteSerializer(read_only=True)
|
||||
member = UserAdminLiteSerializer(read_only=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
model = ProjectMember
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ProjectMemberInviteSerializer(BaseSerializer):
|
||||
project = ProjectLiteSerializer(read_only=True)
|
||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# Third party imports
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module import
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import User
|
||||
@@ -37,11 +40,50 @@ class UserLiteSerializer(BaseSerializer):
|
||||
"id",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"avatar",
|
||||
"is_bot",
|
||||
"display_name",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"is_bot",
|
||||
]
|
||||
|
||||
|
||||
class UserAdminLiteSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
"id",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"avatar",
|
||||
"is_bot",
|
||||
"display_name",
|
||||
"email",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"is_bot",
|
||||
]
|
||||
|
||||
|
||||
class ChangePasswordSerializer(serializers.Serializer):
|
||||
model = User
|
||||
|
||||
"""
|
||||
Serializer for password change endpoint.
|
||||
"""
|
||||
old_password = serializers.CharField(required=True)
|
||||
new_password = serializers.CharField(required=True)
|
||||
|
||||
|
||||
class ResetPasswordSerializer(serializers.Serializer):
|
||||
model = User
|
||||
|
||||
"""
|
||||
Serializer for password change endpoint.
|
||||
"""
|
||||
new_password = serializers.CharField(required=True)
|
||||
confirm_password = serializers.CharField(required=True)
|
||||
|
||||
@@ -3,7 +3,7 @@ from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from .user import UserLiteSerializer
|
||||
from .user import UserLiteSerializer, UserAdminLiteSerializer
|
||||
|
||||
from plane.db.models import (
|
||||
User,
|
||||
@@ -33,10 +33,30 @@ class WorkSpaceSerializer(BaseSerializer):
|
||||
"owner",
|
||||
]
|
||||
|
||||
class WorkspaceLiteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Workspace
|
||||
fields = [
|
||||
"name",
|
||||
"slug",
|
||||
"id",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
|
||||
class WorkSpaceMemberSerializer(BaseSerializer):
|
||||
member = UserLiteSerializer(read_only=True)
|
||||
workspace = WorkSpaceSerializer(read_only=True)
|
||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = WorkspaceMember
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class WorkspaceMemberAdminSerializer(BaseSerializer):
|
||||
member = UserAdminLiteSerializer(read_only=True)
|
||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = WorkspaceMember
|
||||
@@ -101,17 +121,6 @@ class TeamSerializer(BaseSerializer):
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class WorkspaceLiteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Workspace
|
||||
fields = [
|
||||
"name",
|
||||
"slug",
|
||||
"id",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class WorkspaceThemeSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = WorkspaceTheme
|
||||
|
||||
@@ -32,6 +32,7 @@ from plane.api.views import (
|
||||
InviteWorkspaceEndpoint,
|
||||
JoinWorkspaceEndpoint,
|
||||
WorkSpaceMemberViewSet,
|
||||
WorkspaceMembersEndpoint,
|
||||
WorkspaceInvitationsViewset,
|
||||
UserWorkspaceInvitationsEndpoint,
|
||||
WorkspaceMemberUserEndpoint,
|
||||
@@ -59,6 +60,7 @@ from plane.api.views import (
|
||||
ProjectViewSet,
|
||||
InviteProjectEndpoint,
|
||||
ProjectMemberViewSet,
|
||||
ProjectMemberEndpoint,
|
||||
ProjectMemberInvitationsViewset,
|
||||
ProjectMemberUserEndpoint,
|
||||
AddMemberToProjectEndpoint,
|
||||
@@ -86,6 +88,7 @@ from plane.api.views import (
|
||||
IssueSubscriberViewSet,
|
||||
IssueReactionViewSet,
|
||||
CommentReactionViewSet,
|
||||
ExportIssuesEndpoint,
|
||||
## End Issues
|
||||
# States
|
||||
StateViewSet,
|
||||
@@ -334,6 +337,11 @@ urlpatterns = [
|
||||
),
|
||||
name="workspace",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/workspace-members/",
|
||||
WorkspaceMembersEndpoint.as_view(),
|
||||
name="workspace-members",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/teams/",
|
||||
TeamMemberViewSet.as_view(
|
||||
@@ -467,6 +475,11 @@ urlpatterns = [
|
||||
),
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/",
|
||||
ProjectMemberEndpoint.as_view(),
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/add/",
|
||||
AddMemberToProjectEndpoint.as_view(),
|
||||
@@ -808,6 +821,11 @@ urlpatterns = [
|
||||
IssueAttachmentEndpoint.as_view(),
|
||||
name="project-issue-attachments",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/export-issues/",
|
||||
ExportIssuesEndpoint.as_view(),
|
||||
name="export-issues",
|
||||
),
|
||||
## End Issues
|
||||
## Issue Activity
|
||||
path(
|
||||
|
||||
@@ -12,8 +12,9 @@ from .project import (
|
||||
ProjectUserViewsEndpoint,
|
||||
ProjectMemberUserEndpoint,
|
||||
ProjectFavoritesViewSet,
|
||||
ProjectMemberEndpoint,
|
||||
)
|
||||
from .people import (
|
||||
from .user import (
|
||||
UserEndpoint,
|
||||
UpdateUserOnBoardedEndpoint,
|
||||
UpdateUserTourCompletedEndpoint,
|
||||
@@ -47,6 +48,7 @@ from .workspace import (
|
||||
WorkspaceUserProfileEndpoint,
|
||||
WorkspaceUserProfileIssuesEndpoint,
|
||||
WorkspaceLabelsEndpoint,
|
||||
WorkspaceMembersEndpoint,
|
||||
)
|
||||
from .state import StateViewSet
|
||||
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
|
||||
@@ -75,6 +77,7 @@ from .issue import (
|
||||
IssueSubscriberViewSet,
|
||||
CommentReactionViewSet,
|
||||
IssueReactionViewSet,
|
||||
ExportIssuesEndpoint
|
||||
)
|
||||
|
||||
from .auth_extended import (
|
||||
|
||||
@@ -79,12 +79,12 @@ class AnalyticsEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
assignee_details = {}
|
||||
if x_axis in ["assignees__email"] or segment in ["assignees__email"]:
|
||||
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
|
||||
assignee_details = (
|
||||
Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
|
||||
.order_by("assignees__id")
|
||||
.distinct("assignees__id")
|
||||
.values("assignees__avatar", "assignees__email", "assignees__first_name", "assignees__last_name")
|
||||
.values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name", "assignees__id")
|
||||
)
|
||||
|
||||
|
||||
@@ -243,21 +243,21 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||
)
|
||||
most_issue_created_user = (
|
||||
queryset.exclude(created_by=None)
|
||||
.values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__email")
|
||||
.values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__display_name")
|
||||
.annotate(count=Count("id"))
|
||||
.order_by("-count")
|
||||
)[:5]
|
||||
|
||||
most_issue_closed_user = (
|
||||
queryset.filter(completed_at__isnull=False, assignees__isnull=False)
|
||||
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__email")
|
||||
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name")
|
||||
.annotate(count=Count("id"))
|
||||
.order_by("-count")
|
||||
)[:5]
|
||||
|
||||
pending_issue_user = (
|
||||
queryset.filter(completed_at__isnull=True)
|
||||
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__email")
|
||||
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name")
|
||||
.annotate(count=Count("id"))
|
||||
.order_by("-count")
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ from sentry_sdk import capture_exception
|
||||
|
||||
## Module imports
|
||||
from . import BaseAPIView
|
||||
from plane.api.serializers.people import (
|
||||
from plane.api.serializers import (
|
||||
ChangePasswordSerializer,
|
||||
ResetPasswordSerializer,
|
||||
)
|
||||
|
||||
@@ -332,7 +332,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
||||
# if there is no default state assign any random state
|
||||
if default_state is None:
|
||||
default_state = State.objects.filter(
|
||||
~Q(name="Triage"), sproject_id=project_id
|
||||
~Q(name="Triage"), project_id=project_id
|
||||
).first()
|
||||
|
||||
# Get the maximum sequence_id
|
||||
@@ -458,7 +458,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
||||
actor=request.user,
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
comment=f"{request.user.email} importer the issue from {service}",
|
||||
comment=f"imported the issue from {service}",
|
||||
verb="created",
|
||||
created_by=request.user,
|
||||
)
|
||||
|
||||
@@ -74,6 +74,7 @@ from plane.db.models import (
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.grouper import group_results
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.bgtasks.project_issue_export import issue_export_task
|
||||
|
||||
|
||||
class IssueViewSet(BaseViewSet):
|
||||
@@ -1445,3 +1446,30 @@ class CommentReactionViewSet(BaseViewSet):
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ExportIssuesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
|
||||
def post(self, request, slug):
|
||||
try:
|
||||
|
||||
issue_export_task.delay(
|
||||
email=request.user.email, data=request.data, slug=slug ,exporter_name=request.user.first_name
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"message": f"Once the export is ready it will be emailed to you at {str(request.user.email)}"
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@@ -301,7 +301,7 @@ class CreateIssueFromPageBlockEndpoint(BaseAPIView):
|
||||
issue=issue,
|
||||
actor=request.user,
|
||||
project_id=project_id,
|
||||
comment=f"{request.user.email} created the issue from {page_block.name} block",
|
||||
comment=f"created the issue from {page_block.name} block",
|
||||
verb="created",
|
||||
)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from plane.api.serializers import (
|
||||
ProjectFavoriteSerializer,
|
||||
)
|
||||
|
||||
from plane.api.permissions import ProjectBasePermission
|
||||
from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
|
||||
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
@@ -123,7 +123,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
sort_order_query = ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
project_id=OuterRef("pk"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
).values("sort_order")
|
||||
projects = (
|
||||
self.get_queryset()
|
||||
@@ -458,7 +458,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
"member__email",
|
||||
"member__display_name",
|
||||
"member__first_name",
|
||||
]
|
||||
|
||||
@@ -602,19 +602,29 @@ class AddMemberToProjectEndpoint(BaseAPIView):
|
||||
)
|
||||
bulk_project_members = []
|
||||
|
||||
project_members = ProjectMember.objects.filter(
|
||||
workspace=self.workspace, member_id__in=[member.get("member_id") for member in members]
|
||||
).values("member_id").annotate(sort_order_min=Min("sort_order"))
|
||||
project_members = (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member_id__in=[member.get("member_id") for member in members],
|
||||
)
|
||||
.values("member_id", "sort_order")
|
||||
.order_by("sort_order")
|
||||
)
|
||||
|
||||
for member in members:
|
||||
sort_order = [project_member.get("sort_order") for project_member in project_members]
|
||||
sort_order = [
|
||||
project_member.get("sort_order")
|
||||
for project_member in project_members
|
||||
if str(project_member.get("member_id"))
|
||||
== str(member.get("member_id"))
|
||||
]
|
||||
bulk_project_members.append(
|
||||
ProjectMember(
|
||||
member_id=member.get("member_id"),
|
||||
role=member.get("role", 10),
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
sort_order=sort_order[0] - 10000 if len(sort_order) else 65535
|
||||
sort_order=sort_order[0] - 10000 if len(sort_order) else 65535,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -974,3 +984,23 @@ class ProjectFavoritesViewSet(BaseViewSet):
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class ProjectMemberEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug, project_id):
|
||||
try:
|
||||
project_members = ProjectMember.objects.filter(
|
||||
project_id=project_id, workspace__slug=slug
|
||||
).select_related("project", "member")
|
||||
serializer = ProjectMemberSerializer(project_members, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -47,6 +47,7 @@ from plane.api.serializers import (
|
||||
WorkspaceThemeSerializer,
|
||||
IssueActivitySerializer,
|
||||
IssueLiteSerializer,
|
||||
WorkspaceMemberAdminSerializer
|
||||
)
|
||||
from plane.api.views.base import BaseAPIView
|
||||
from . import BaseViewSet
|
||||
@@ -75,6 +76,7 @@ from plane.db.models import (
|
||||
Label,
|
||||
WorkspaceMember,
|
||||
CycleIssue,
|
||||
IssueReaction,
|
||||
)
|
||||
from plane.api.permissions import (
|
||||
WorkSpaceBasePermission,
|
||||
@@ -536,7 +538,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
|
||||
|
||||
|
||||
class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
serializer_class = WorkSpaceMemberSerializer
|
||||
serializer_class = WorkspaceMemberAdminSerializer
|
||||
model = WorkspaceMember
|
||||
|
||||
permission_classes = [
|
||||
@@ -544,7 +546,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
"member__email",
|
||||
"member__display_name",
|
||||
"member__first_name",
|
||||
]
|
||||
|
||||
@@ -689,7 +691,7 @@ class TeamMemberViewSet(BaseViewSet):
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
"member__email",
|
||||
"member__display_name",
|
||||
"member__first_name",
|
||||
]
|
||||
|
||||
@@ -1047,7 +1049,6 @@ class WorkspaceThemeViewSet(BaseViewSet):
|
||||
|
||||
|
||||
class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug, user_id):
|
||||
try:
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
@@ -1145,14 +1146,18 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
upcoming_cycles = CycleIssue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
cycle__start_date__gt=timezone.now().date(),
|
||||
issue__assignees__in=[user_id,]
|
||||
issue__assignees__in=[
|
||||
user_id,
|
||||
],
|
||||
).values("cycle__name", "cycle__id", "cycle__project_id")
|
||||
|
||||
present_cycle = CycleIssue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
cycle__start_date__lt=timezone.now().date(),
|
||||
cycle__end_date__gt=timezone.now().date(),
|
||||
issue__assignees__in=[user_id,]
|
||||
issue__assignees__in=[
|
||||
user_id,
|
||||
],
|
||||
).values("cycle__name", "cycle__id", "cycle__project_id")
|
||||
|
||||
return Response(
|
||||
@@ -1165,7 +1170,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
"pending_issues": pending_issues_count,
|
||||
"subscribed_issues": subscribed_issues_count,
|
||||
"present_cycles": present_cycle,
|
||||
"upcoming_cycles": upcoming_cycles,
|
||||
"upcoming_cycles": upcoming_cycles,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -1183,7 +1188,6 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug, user_id):
|
||||
try:
|
||||
|
||||
projects = request.query_params.getlist("project", [])
|
||||
|
||||
queryset = IssueActivity.objects.filter(
|
||||
@@ -1211,12 +1215,13 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class WorkspaceUserProfileEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug, user_id):
|
||||
try:
|
||||
user_data = User.objects.get(pk=user_id)
|
||||
|
||||
requesting_workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user)
|
||||
requesting_workspace_member = WorkspaceMember.objects.get(
|
||||
workspace__slug=slug, member=request.user
|
||||
)
|
||||
projects = []
|
||||
if requesting_workspace_member.role >= 10:
|
||||
projects = (
|
||||
@@ -1226,7 +1231,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
|
||||
)
|
||||
.annotate(
|
||||
created_issues=Count(
|
||||
"project_issue", filter=Q(project_issue__created_by_id=user_id)
|
||||
"project_issue",
|
||||
filter=Q(project_issue__created_by_id=user_id),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@@ -1281,6 +1287,7 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
|
||||
"cover_image": user_data.cover_image,
|
||||
"date_joined": user_data.date_joined,
|
||||
"user_timezone": user_data.user_timezone,
|
||||
"display_name": user_data.display_name,
|
||||
},
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
@@ -1321,6 +1328,12 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
)
|
||||
.select_related("project", "workspace", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
@@ -1432,3 +1445,23 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceMembersEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug):
|
||||
try:
|
||||
workspace_members = WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug
|
||||
).select_related("workspace", "member")
|
||||
serialzier = WorkSpaceMemberSerializer(workspace_members, many=True)
|
||||
return Response(serialzier.data, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ row_mapping = {
|
||||
"state__name": "State",
|
||||
"state__group": "State Group",
|
||||
"labels__name": "Label",
|
||||
"assignees__email": "Assignee Name",
|
||||
"assignees__display_name": "Assignee Name",
|
||||
"start_date": "Start Date",
|
||||
"target_date": "Due Date",
|
||||
"completed_at": "Completed At",
|
||||
@@ -51,12 +51,12 @@ def analytic_export_task(email, data, slug):
|
||||
segmented = segment
|
||||
|
||||
assignee_details = {}
|
||||
if x_axis in ["assignees__email"] or segment in ["assignees__email"]:
|
||||
if x_axis in ["assignees__display_name"] or segment in ["assignees__display_name"]:
|
||||
assignee_details = (
|
||||
Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
|
||||
.order_by("assignees__id")
|
||||
.distinct("assignees__id")
|
||||
.values("assignees__avatar", "assignees__email", "assignees__first_name", "assignees__last_name")
|
||||
.values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name")
|
||||
)
|
||||
|
||||
if segment:
|
||||
@@ -93,17 +93,17 @@ def analytic_export_task(email, data, slug):
|
||||
else:
|
||||
generated_row.append("0")
|
||||
# x-axis replacement for names
|
||||
if x_axis in ["assignees__email"]:
|
||||
assignee = [user for user in assignee_details if str(user.get("assignees__email")) == str(item)]
|
||||
if x_axis in ["assignees__display_name"]:
|
||||
assignee = [user for user in assignee_details if str(user.get("assignees__display_name")) == str(item)]
|
||||
if len(assignee):
|
||||
generated_row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
|
||||
rows.append(tuple(generated_row))
|
||||
|
||||
# If segment is ["assignees__email"] then replace segment_zero rows with first and last names
|
||||
if segmented in ["assignees__email"]:
|
||||
# If segment is ["assignees__display_name"] then replace segment_zero rows with first and last names
|
||||
if segmented in ["assignees__display_name"]:
|
||||
for index, segm in enumerate(row_zero[2:]):
|
||||
# find the name of the user
|
||||
assignee = [user for user in assignee_details if str(user.get("assignees__email")) == str(segm)]
|
||||
assignee = [user for user in assignee_details if str(user.get("assignees__display_name")) == str(segm)]
|
||||
if len(assignee):
|
||||
row_zero[index] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
|
||||
|
||||
@@ -141,8 +141,8 @@ def analytic_export_task(email, data, slug):
|
||||
else distribution.get(item)[0].get("estimate "),
|
||||
]
|
||||
# x-axis replacement to names
|
||||
if x_axis in ["assignees__email"]:
|
||||
assignee = [user for user in assignee_details if str(user.get("assignees__email")) == str(item)]
|
||||
if x_axis in ["assignees__display_name"]:
|
||||
assignee = [user for user in assignee_details if str(user.get("assignees__display_name")) == str(item)]
|
||||
if len(assignee):
|
||||
row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ def track_name(
|
||||
field="name",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the name to {requested_data.get('name')}",
|
||||
comment=f"updated the name to {requested_data.get('name')}",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -75,7 +75,7 @@ def track_parent(
|
||||
field="parent",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the parent issue to None",
|
||||
comment=f"updated the parent issue to None",
|
||||
old_identifier=old_parent.id,
|
||||
new_identifier=None,
|
||||
)
|
||||
@@ -95,7 +95,7 @@ def track_parent(
|
||||
field="parent",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the parent issue to {new_parent.name}",
|
||||
comment=f"updated the parent issue to {new_parent.name}",
|
||||
old_identifier=old_parent.id if old_parent is not None else None,
|
||||
new_identifier=new_parent.id,
|
||||
)
|
||||
@@ -123,7 +123,7 @@ def track_priority(
|
||||
field="priority",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the priority to None",
|
||||
comment=f"updated the priority to None",
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -137,7 +137,7 @@ def track_priority(
|
||||
field="priority",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the priority to {requested_data.get('priority')}",
|
||||
comment=f"updated the priority to {requested_data.get('priority')}",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -165,7 +165,7 @@ def track_state(
|
||||
field="state",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the state to {new_state.name}",
|
||||
comment=f"updated the state to {new_state.name}",
|
||||
old_identifier=old_state.id,
|
||||
new_identifier=new_state.id,
|
||||
)
|
||||
@@ -194,7 +194,7 @@ def track_description(
|
||||
field="description",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the description to {requested_data.get('description_html')}",
|
||||
comment=f"updated the description to {requested_data.get('description_html')}",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -220,7 +220,7 @@ def track_target_date(
|
||||
field="target_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the target date to None",
|
||||
comment=f"updated the target date to None",
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -234,7 +234,7 @@ def track_target_date(
|
||||
field="target_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the target date to {requested_data.get('target_date')}",
|
||||
comment=f"updated the target date to {requested_data.get('target_date')}",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -260,7 +260,7 @@ def track_start_date(
|
||||
field="start_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the start date to None",
|
||||
comment=f"updated the start date to None",
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -274,7 +274,7 @@ def track_start_date(
|
||||
field="start_date",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the start date to {requested_data.get('start_date')}",
|
||||
comment=f"updated the start date to {requested_data.get('start_date')}",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -303,7 +303,7 @@ def track_labels(
|
||||
field="labels",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added label {label.name}",
|
||||
comment=f"added label {label.name}",
|
||||
new_identifier=label.id,
|
||||
old_identifier=None,
|
||||
)
|
||||
@@ -324,7 +324,7 @@ def track_labels(
|
||||
field="labels",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed label {label.name}",
|
||||
comment=f"removed label {label.name}",
|
||||
old_identifier=label.id,
|
||||
new_identifier=None,
|
||||
)
|
||||
@@ -353,12 +353,12 @@ def track_assignees(
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value="",
|
||||
new_value=assignee.email,
|
||||
new_value=assignee.display_name,
|
||||
field="assignees",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added assignee {assignee.email}",
|
||||
new_identifier=actor.id,
|
||||
comment=f"added assignee {assignee.display_name}",
|
||||
new_identifier=assignee.id,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -374,13 +374,13 @@ def track_assignees(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=assignee.email,
|
||||
old_value=assignee.display_name,
|
||||
new_value="",
|
||||
field="assignees",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed assignee {assignee.email}",
|
||||
old_identifier=actor.id,
|
||||
comment=f"removed assignee {assignee.display_name}",
|
||||
old_identifier=assignee.id,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -419,7 +419,7 @@ def track_blocks(
|
||||
field="blocks",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added blocking issue {issue.project.identifier}-{issue.sequence_id}",
|
||||
comment=f"added blocking issue {project.identifier}-{issue.sequence_id}",
|
||||
new_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
@@ -441,7 +441,7 @@ def track_blocks(
|
||||
field="blocks",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed blocking issue {issue.project.identifier}-{issue.sequence_id}",
|
||||
comment=f"removed blocking issue {project.identifier}-{issue.sequence_id}",
|
||||
old_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
@@ -481,7 +481,7 @@ def track_blockings(
|
||||
field="blocking",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added blocked by issue {issue.project.identifier}-{issue.sequence_id}",
|
||||
comment=f"added blocked by issue {project.identifier}-{issue.sequence_id}",
|
||||
new_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
@@ -503,7 +503,7 @@ def track_blockings(
|
||||
field="blocking",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed blocked by issue {issue.project.identifier}-{issue.sequence_id}",
|
||||
comment=f"removed blocked by issue {project.identifier}-{issue.sequence_id}",
|
||||
old_identifier=issue.id,
|
||||
)
|
||||
)
|
||||
@@ -517,7 +517,7 @@ def create_issue_activity(
|
||||
issue_id=issue_id,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} created the issue",
|
||||
comment=f"created the issue",
|
||||
verb="created",
|
||||
actor=actor,
|
||||
)
|
||||
@@ -539,7 +539,7 @@ def track_estimate_points(
|
||||
field="estimate_point",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the estimate point to None",
|
||||
comment=f"updated the estimate point to None",
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -553,7 +553,7 @@ def track_estimate_points(
|
||||
field="estimate_point",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated the estimate point to {requested_data.get('estimate_point')}",
|
||||
comment=f"updated the estimate point to {requested_data.get('estimate_point')}",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -567,7 +567,7 @@ def track_archive_at(
|
||||
issue_id=issue_id,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} has restored the issue",
|
||||
comment=f"has restored the issue",
|
||||
verb="updated",
|
||||
actor=actor,
|
||||
field="archived_at",
|
||||
@@ -661,7 +661,7 @@ def delete_issue_activity(
|
||||
IssueActivity(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} deleted the issue",
|
||||
comment=f"deleted the issue",
|
||||
verb="deleted",
|
||||
actor=actor,
|
||||
field="issue",
|
||||
@@ -682,7 +682,7 @@ def create_comment_activity(
|
||||
issue_id=issue_id,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} created a comment",
|
||||
comment=f"created a comment",
|
||||
verb="created",
|
||||
actor=actor,
|
||||
field="comment",
|
||||
@@ -707,7 +707,7 @@ def update_comment_activity(
|
||||
issue_id=issue_id,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated a comment",
|
||||
comment=f"updated a comment",
|
||||
verb="updated",
|
||||
actor=actor,
|
||||
field="comment",
|
||||
@@ -728,7 +728,7 @@ def delete_comment_activity(
|
||||
issue_id=issue_id,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} deleted the comment",
|
||||
comment=f"deleted the comment",
|
||||
verb="deleted",
|
||||
actor=actor,
|
||||
field="comment",
|
||||
@@ -766,7 +766,7 @@ def create_cycle_issue_activity(
|
||||
field="cycles",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated cycle from {old_cycle.name} to {new_cycle.name}",
|
||||
comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}",
|
||||
old_identifier=old_cycle.id,
|
||||
new_identifier=new_cycle.id,
|
||||
)
|
||||
@@ -787,7 +787,7 @@ def create_cycle_issue_activity(
|
||||
field="cycles",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added cycle {cycle.name}",
|
||||
comment=f"added cycle {cycle.name}",
|
||||
new_identifier=cycle.id,
|
||||
)
|
||||
)
|
||||
@@ -816,7 +816,7 @@ def delete_cycle_issue_activity(
|
||||
field="cycles",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed this issue from {cycle.name if cycle is not None else None}",
|
||||
comment=f"removed this issue from {cycle.name if cycle is not None else None}",
|
||||
old_identifier=cycle.id if cycle is not None else None,
|
||||
)
|
||||
)
|
||||
@@ -852,7 +852,7 @@ def create_module_issue_activity(
|
||||
field="modules",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated module from {old_module.name} to {new_module.name}",
|
||||
comment=f"updated module from {old_module.name} to {new_module.name}",
|
||||
old_identifier=old_module.id,
|
||||
new_identifier=new_module.id,
|
||||
)
|
||||
@@ -872,7 +872,7 @@ def create_module_issue_activity(
|
||||
field="modules",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} added module {module.name}",
|
||||
comment=f"added module {module.name}",
|
||||
new_identifier=module.id,
|
||||
)
|
||||
)
|
||||
@@ -901,7 +901,7 @@ def delete_module_issue_activity(
|
||||
field="modules",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} removed this issue from {module.name if module is not None else None}",
|
||||
comment=f"removed this issue from {module.name if module is not None else None}",
|
||||
old_identifier=module.id if module is not None else None,
|
||||
)
|
||||
)
|
||||
@@ -920,7 +920,7 @@ def create_link_activity(
|
||||
issue_id=issue_id,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} created a link",
|
||||
comment=f"created a link",
|
||||
verb="created",
|
||||
actor=actor,
|
||||
field="link",
|
||||
@@ -944,7 +944,7 @@ def update_link_activity(
|
||||
issue_id=issue_id,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} updated a link",
|
||||
comment=f"updated a link",
|
||||
verb="updated",
|
||||
actor=actor,
|
||||
field="link",
|
||||
@@ -969,7 +969,7 @@ def delete_link_activity(
|
||||
issue_id=issue_id,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} deleted the link",
|
||||
comment=f"deleted the link",
|
||||
verb="deleted",
|
||||
actor=actor,
|
||||
field="link",
|
||||
@@ -992,7 +992,7 @@ def create_attachment_activity(
|
||||
issue_id=issue_id,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} created an attachment",
|
||||
comment=f"created an attachment",
|
||||
verb="created",
|
||||
actor=actor,
|
||||
field="attachment",
|
||||
@@ -1010,7 +1010,7 @@ def delete_attachment_activity(
|
||||
issue_id=issue_id,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"{actor.email} deleted the attachment",
|
||||
comment=f"deleted the attachment",
|
||||
verb="deleted",
|
||||
actor=actor,
|
||||
field="attachment",
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# Python imports
|
||||
import csv
|
||||
import io
|
||||
|
||||
# Django imports
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Issue
|
||||
|
||||
@shared_task
|
||||
def issue_export_task(email, data, slug, exporter_name):
|
||||
try:
|
||||
|
||||
project_ids = data.get("project_id", [])
|
||||
issues_filter = {"workspace__slug": slug}
|
||||
|
||||
if project_ids:
|
||||
issues_filter["project_id__in"] = project_ids
|
||||
|
||||
issues = (
|
||||
Issue.objects.filter(**issues_filter)
|
||||
.select_related("project", "workspace", "state", "parent", "created_by")
|
||||
.prefetch_related(
|
||||
"assignees", "labels", "issue_cycle__cycle", "issue_module__module"
|
||||
)
|
||||
.values_list(
|
||||
"project__identifier",
|
||||
"sequence_id",
|
||||
"name",
|
||||
"description_stripped",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"state__name",
|
||||
"project__name",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"completed_at",
|
||||
"archived_at",
|
||||
"issue_cycle__cycle__name",
|
||||
"issue_cycle__cycle__start_date",
|
||||
"issue_cycle__cycle__end_date",
|
||||
"issue_module__module__name",
|
||||
"issue_module__module__start_date",
|
||||
"issue_module__module__target_date",
|
||||
"created_by__first_name",
|
||||
"created_by__last_name",
|
||||
"assignees__first_name",
|
||||
"assignees__last_name",
|
||||
"labels__name",
|
||||
)
|
||||
)
|
||||
|
||||
# CSV header
|
||||
header = [
|
||||
"Issue ID",
|
||||
"Project",
|
||||
"Name",
|
||||
"Description",
|
||||
"State",
|
||||
"Priority",
|
||||
"Created By",
|
||||
"Assignee",
|
||||
"Labels",
|
||||
"Cycle Name",
|
||||
"Cycle Start Date",
|
||||
"Cycle End Date",
|
||||
"Module Name",
|
||||
"Module Start Date",
|
||||
"Module Target Date",
|
||||
"Created At"
|
||||
"Updated At"
|
||||
"Completed At"
|
||||
"Archived At"
|
||||
]
|
||||
|
||||
# Prepare the CSV data
|
||||
rows = [header]
|
||||
|
||||
# Write data for each issue
|
||||
for issue in issues:
|
||||
(
|
||||
project_identifier,
|
||||
sequence_id,
|
||||
name,
|
||||
description,
|
||||
priority,
|
||||
start_date,
|
||||
target_date,
|
||||
state_name,
|
||||
project_name,
|
||||
created_at,
|
||||
updated_at,
|
||||
completed_at,
|
||||
archived_at,
|
||||
cycle_name,
|
||||
cycle_start_date,
|
||||
cycle_end_date,
|
||||
module_name,
|
||||
module_start_date,
|
||||
module_target_date,
|
||||
created_by_first_name,
|
||||
created_by_last_name,
|
||||
assignees_first_names,
|
||||
assignees_last_names,
|
||||
labels_names,
|
||||
) = issue
|
||||
|
||||
created_by_fullname = (
|
||||
f"{created_by_first_name} {created_by_last_name}"
|
||||
if created_by_first_name and created_by_last_name
|
||||
else ""
|
||||
)
|
||||
|
||||
assignees_names = ""
|
||||
if assignees_first_names and assignees_last_names:
|
||||
assignees_names = ", ".join(
|
||||
[
|
||||
f"{assignees_first_name} {assignees_last_name}"
|
||||
for assignees_first_name, assignees_last_name in zip(
|
||||
assignees_first_names, assignees_last_names
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
labels_names = ", ".join(labels_names) if labels_names else ""
|
||||
|
||||
row = [
|
||||
f"{project_identifier}-{sequence_id}",
|
||||
project_name,
|
||||
name,
|
||||
description,
|
||||
state_name,
|
||||
priority,
|
||||
created_by_fullname,
|
||||
assignees_names,
|
||||
labels_names,
|
||||
cycle_name,
|
||||
cycle_start_date,
|
||||
cycle_end_date,
|
||||
module_name,
|
||||
module_start_date,
|
||||
module_target_date,
|
||||
start_date,
|
||||
target_date,
|
||||
created_at,
|
||||
updated_at,
|
||||
completed_at,
|
||||
archived_at,
|
||||
]
|
||||
rows.append(row)
|
||||
|
||||
# Create CSV file in-memory
|
||||
csv_buffer = io.StringIO()
|
||||
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
|
||||
|
||||
# Write CSV data to the buffer
|
||||
for row in rows:
|
||||
writer.writerow(row)
|
||||
|
||||
subject = "Your Issue Export is ready"
|
||||
|
||||
context = {
|
||||
"username": exporter_name,
|
||||
}
|
||||
|
||||
html_content = render_to_string("emails/exports/issues.html", context)
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
csv_buffer.seek(0)
|
||||
msg = EmailMultiAlternatives(
|
||||
subject, text_content, settings.EMAIL_FROM, [email]
|
||||
)
|
||||
msg.attach(f"{slug}-issues-{timezone.now().date()}.csv", csv_buffer.read(), "text/csv")
|
||||
msg.send(fail_silently=False)
|
||||
|
||||
except Exception as e:
|
||||
# Print logs if in DEBUG mode
|
||||
if settings.DEBUG:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
return
|
||||
+10
@@ -67,5 +67,15 @@ class Migration(migrations.Migration):
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('comment', 'actor', 'reaction')},
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='identifier',
|
||||
field=models.CharField(max_length=12, verbose_name='Project Identifier'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='projectidentifier',
|
||||
name='name',
|
||||
field=models.CharField(max_length=12),
|
||||
),
|
||||
]
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
# Generated by Django 4.2.3 on 2023-08-04 09:12
|
||||
import string
|
||||
import random
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def generate_display_name(apps, schema_editor):
|
||||
UserModel = apps.get_model("db", "User")
|
||||
updated_users = []
|
||||
for obj in UserModel.objects.all():
|
||||
obj.display_name = (
|
||||
obj.email.split("@")[0]
|
||||
if len(obj.email.split("@"))
|
||||
else "".join(random.choice(string.ascii_letters) for _ in range(6))
|
||||
)
|
||||
updated_users.append(obj)
|
||||
UserModel.objects.bulk_update(updated_users, ["display_name"], batch_size=100)
|
||||
|
||||
|
||||
def rectify_field_issue_activity(apps, schema_editor):
|
||||
Model = apps.get_model("db", "IssueActivity")
|
||||
updated_activity = []
|
||||
for obj in Model.objects.filter(field="assignee"):
|
||||
obj.field = "assignees"
|
||||
updated_activity.append(obj)
|
||||
|
||||
Model.objects.bulk_update(updated_activity, ["field"], batch_size=100)
|
||||
|
||||
|
||||
def update_assignee_issue_activity(apps, schema_editor):
|
||||
Model = apps.get_model("db", "IssueActivity")
|
||||
updated_activity = []
|
||||
|
||||
# Get all the users
|
||||
User = apps.get_model("db", "User")
|
||||
users = User.objects.values("id", "email", "display_name")
|
||||
|
||||
for obj in Model.objects.filter(field="assignees"):
|
||||
if bool(obj.new_value) and not bool(obj.old_value):
|
||||
# Get user from list
|
||||
assigned_user = [
|
||||
user for user in users if user.get("email") == obj.new_value
|
||||
]
|
||||
if assigned_user:
|
||||
obj.new_value = assigned_user[0].get("display_name")
|
||||
obj.new_identifier = assigned_user[0].get("id")
|
||||
# Update the comment
|
||||
words = obj.comment.split()
|
||||
words[-1] = assigned_user[0].get("display_name")
|
||||
obj.comment = " ".join(words)
|
||||
|
||||
if bool(obj.old_value) and not bool(obj.new_value):
|
||||
# Get user from list
|
||||
assigned_user = [
|
||||
user for user in users if user.get("email") == obj.old_value
|
||||
]
|
||||
if assigned_user:
|
||||
obj.old_value = assigned_user[0].get("display_name")
|
||||
obj.old_identifier = assigned_user[0].get("id")
|
||||
# Update the comment
|
||||
words = obj.comment.split()
|
||||
words[-1] = assigned_user[0].get("display_name")
|
||||
obj.comment = " ".join(words)
|
||||
|
||||
updated_activity.append(obj)
|
||||
|
||||
Model.objects.bulk_update(
|
||||
updated_activity,
|
||||
["old_value", "new_value", "old_identifier", "new_identifier", "comment"],
|
||||
batch_size=200,
|
||||
)
|
||||
|
||||
|
||||
def update_name_activity(apps, schema_editor):
|
||||
Model = apps.get_model("db", "IssueActivity")
|
||||
update_activity = []
|
||||
for obj in Model.objects.filter(field="name"):
|
||||
obj.comment = obj.comment.replace("start date", "name")
|
||||
update_activity.append(obj)
|
||||
|
||||
Model.objects.bulk_update(update_activity, ["comment"], batch_size=1000)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("db", "0040_projectmember_preferences_user_cover_image_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="display_name",
|
||||
field=models.CharField(default="", max_length=255),
|
||||
),
|
||||
migrations.RunPython(generate_display_name),
|
||||
migrations.RunPython(rectify_field_issue_activity),
|
||||
migrations.RunPython(update_assignee_issue_activity),
|
||||
migrations.RunPython(update_name_activity),
|
||||
]
|
||||
@@ -1,6 +1,7 @@
|
||||
# Python imports
|
||||
from enum import unique
|
||||
import uuid
|
||||
import string
|
||||
import random
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
@@ -18,6 +19,7 @@ from sentry_sdk import capture_exception
|
||||
from slack_sdk import WebClient
|
||||
from slack_sdk.errors import SlackApiError
|
||||
|
||||
|
||||
def get_default_onboarding():
|
||||
return {
|
||||
"profile_complete": False,
|
||||
@@ -26,6 +28,7 @@ def get_default_onboarding():
|
||||
"workspace_join": False,
|
||||
}
|
||||
|
||||
|
||||
class User(AbstractBaseUser, PermissionsMixin):
|
||||
id = models.UUIDField(
|
||||
default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True
|
||||
@@ -81,6 +84,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
role = models.CharField(max_length=300, null=True, blank=True)
|
||||
is_bot = models.BooleanField(default=False)
|
||||
theme = models.JSONField(default=dict)
|
||||
display_name = models.CharField(max_length=255, default="")
|
||||
is_tour_completed = models.BooleanField(default=False)
|
||||
onboarding_step = models.JSONField(default=get_default_onboarding)
|
||||
|
||||
@@ -107,6 +111,13 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
self.token = uuid.uuid4().hex + uuid.uuid4().hex
|
||||
self.token_updated_at = timezone.now()
|
||||
|
||||
if not self.display_name:
|
||||
self.display_name = (
|
||||
self.email.split("@")[0]
|
||||
if len(self.email.split("@"))
|
||||
else "".join(random.choice(string.ascii_letters) for _ in range(6))
|
||||
)
|
||||
|
||||
if self.is_superuser:
|
||||
self.is_staff = True
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html>
|
||||
Dear {{username}},<br/>
|
||||
Your requested Issue's data has been successfully exported from Plane. The export includes all relevant information about issues you requested from your selected projects.</br>
|
||||
Please find the attachment and download the CSV file. If you have any questions or need further assistance, please don't hesitate to contact our support team at <a href = "mailto: engineering@plane.com">engineering@plane.so</a>. We're here to help!</br>
|
||||
Thank you for using Plane. We hope this export will aid you in effectively managing your projects.</br>
|
||||
Regards,
|
||||
Team Plane
|
||||
</html>
|
||||
@@ -14,17 +14,17 @@ type Props = {
|
||||
export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) => {
|
||||
let tooltipValue: string | number = "";
|
||||
|
||||
const renderAssigneeName = (assigneeId: string): string => {
|
||||
const assignee = analytics.extras.assignee_details.find((a) => a.assignees__id === assigneeId);
|
||||
|
||||
if (!assignee) return "No assignee";
|
||||
|
||||
return assignee.assignees__display_name || "No assignee";
|
||||
};
|
||||
|
||||
if (params.segment) {
|
||||
if (DATE_KEYS.includes(params.segment)) tooltipValue = renderMonthAndYear(datum.id);
|
||||
else if (params.segment === "assignees__email") {
|
||||
const assignee = analytics.extras.assignee_details.find(
|
||||
(a) => a.assignees__email === datum.id
|
||||
);
|
||||
|
||||
if (assignee)
|
||||
tooltipValue = assignee.assignees__first_name + " " + assignee.assignees__last_name;
|
||||
else tooltipValue = "No assignees";
|
||||
} else tooltipValue = datum.id;
|
||||
else tooltipValue = datum.id;
|
||||
} else {
|
||||
if (DATE_KEYS.includes(params.x_axis)) tooltipValue = datum.indexValue;
|
||||
else tooltipValue = datum.id === "count" ? "Issue count" : "Estimate";
|
||||
@@ -49,7 +49,10 @@ export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) =>
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{tooltipValue}:
|
||||
{params.segment === "assignees__id"
|
||||
? renderAssigneeName(tooltipValue.toString())
|
||||
: tooltipValue}
|
||||
:
|
||||
</span>
|
||||
<span>{datum.value}</span>
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,14 @@ export const AnalyticsGraph: React.FC<Props> = ({
|
||||
yAxisKey,
|
||||
fullScreen,
|
||||
}) => {
|
||||
const renderAssigneeName = (assigneeId: string): string => {
|
||||
const assignee = analytics.extras.assignee_details.find((a) => a.assignees__id === assigneeId);
|
||||
|
||||
if (!assignee) return "?";
|
||||
|
||||
return assignee.assignees__display_name || "?";
|
||||
};
|
||||
|
||||
const generateYAxisTickValues = () => {
|
||||
if (!analytics) return [];
|
||||
|
||||
@@ -70,17 +78,17 @@ export const AnalyticsGraph: React.FC<Props> = ({
|
||||
height={fullScreen ? "400px" : "300px"}
|
||||
margin={{
|
||||
right: 20,
|
||||
bottom: params.x_axis === "assignees__email" ? 50 : longestXAxisLabel.length * 5 + 20,
|
||||
bottom: params.x_axis === "assignees__id" ? 50 : longestXAxisLabel.length * 5 + 20,
|
||||
}}
|
||||
axisBottom={{
|
||||
tickSize: 0,
|
||||
tickPadding: 10,
|
||||
tickRotation: barGraphData.data.length > 7 ? -45 : 0,
|
||||
renderTick:
|
||||
params.x_axis === "assignees__email"
|
||||
params.x_axis === "assignees__id"
|
||||
? (datum) => {
|
||||
const avatar = analytics.extras.assignee_details?.find(
|
||||
(a) => a?.assignees__email === datum?.value
|
||||
(a) => a?.assignees__display_name === datum?.value
|
||||
)?.assignees__avatar;
|
||||
|
||||
if (avatar && avatar !== "")
|
||||
@@ -101,7 +109,11 @@ export const AnalyticsGraph: React.FC<Props> = ({
|
||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||
<circle cy={18} r={8} fill="#374151" />
|
||||
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
|
||||
{datum.value && datum.value !== "None"
|
||||
{params.x_axis === "assignees__id"
|
||||
? datum.value && datum.value !== "None"
|
||||
? renderAssigneeName(datum.value)[0].toUpperCase()
|
||||
: "?"
|
||||
: datum.value && datum.value !== "None"
|
||||
? `${datum.value}`.toUpperCase()[0]
|
||||
: "?"}
|
||||
</text>
|
||||
|
||||
@@ -277,9 +277,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Lead</h6>
|
||||
<span>
|
||||
{cycleDetails.owned_by?.first_name} {cycleDetails.owned_by?.last_name}
|
||||
</span>
|
||||
<span>{cycleDetails.owned_by?.display_name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Start Date</h6>
|
||||
@@ -305,10 +303,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Lead</h6>
|
||||
<span>
|
||||
{moduleDetails.lead_detail?.first_name}{" "}
|
||||
{moduleDetails.lead_detail?.last_name}
|
||||
</span>
|
||||
<span>{moduleDetails.lead_detail?.display_name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Start Date</h6>
|
||||
|
||||
@@ -22,15 +22,12 @@ type Props = {
|
||||
};
|
||||
|
||||
export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, params, yAxisKey }) => {
|
||||
const renderAssigneeName = (email: string): string => {
|
||||
const assignee = analytics.extras.assignee_details.find((a) => a.assignees__email === email);
|
||||
const renderAssigneeName = (assigneeId: string): string => {
|
||||
const assignee = analytics.extras.assignee_details.find((a) => a.assignees__id === assigneeId);
|
||||
|
||||
if (!assignee) return "No assignee";
|
||||
|
||||
if (assignee.assignees__first_name !== "")
|
||||
return assignee.assignees__first_name + " " + assignee.assignees__last_name;
|
||||
|
||||
return email;
|
||||
return assignee.assignees__display_name || "No assignee";
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -65,10 +62,10 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{DATE_KEYS.includes(params.segment ?? "")
|
||||
? renderMonthAndYear(key)
|
||||
: params.segment === "assignees__email"
|
||||
{params.segment === "assignees__id"
|
||||
? renderAssigneeName(key)
|
||||
: DATE_KEYS.includes(params.segment ?? "")
|
||||
? renderMonthAndYear(key)
|
||||
: key}
|
||||
</div>
|
||||
</th>
|
||||
@@ -108,7 +105,7 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{params.x_axis === "assignees__email"
|
||||
{params.x_axis === "assignees__id"
|
||||
? renderAssigneeName(`${item.name}`)
|
||||
: addSpaceIfCamelCase(`${item.name}`)}
|
||||
</td>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
type Props = {
|
||||
users: {
|
||||
avatar: string | null;
|
||||
email: string | null;
|
||||
display_name: string | null;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
count: number;
|
||||
@@ -16,7 +16,7 @@ export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
|
||||
<div className="mt-3 space-y-3">
|
||||
{users.map((user) => (
|
||||
<div
|
||||
key={user.email ?? "None"}
|
||||
key={user.display_name ?? "None"}
|
||||
className="flex items-start justify-between gap-4 text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -25,16 +25,16 @@ export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
|
||||
<img
|
||||
src={user.avatar}
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-full"
|
||||
alt={user.email ?? "None"}
|
||||
alt={user.display_name ?? "None"}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid place-items-center flex-shrink-0 rounded-full bg-gray-700 text-[11px] capitalize text-white h-4 w-4">
|
||||
{user.firstName !== "" ? user.firstName[0] : "?"}
|
||||
{user.display_name !== "" ? user?.display_name?.[0] : "?"}
|
||||
</div>
|
||||
)}
|
||||
<span className="break-words text-custom-text-200">
|
||||
{user.firstName !== "" ? `${user.firstName} ${user.lastName}` : "No assignee"}
|
||||
{user.display_name !== "" ? `${user.display_name}` : "No assignee"}
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex-shrink-0">{user.count}</span>
|
||||
|
||||
@@ -56,9 +56,9 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
||||
<AnalyticsLeaderboard
|
||||
users={defaultAnalytics.most_issue_created_user?.map((user) => ({
|
||||
avatar: user?.created_by__avatar,
|
||||
email: user?.created_by__email,
|
||||
firstName: user?.created_by__first_name,
|
||||
lastName: user?.created_by__last_name,
|
||||
display_name: user?.created_by__display_name,
|
||||
count: user?.count,
|
||||
}))}
|
||||
title="Most issues created"
|
||||
@@ -66,9 +66,9 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
||||
<AnalyticsLeaderboard
|
||||
users={defaultAnalytics.most_issue_closed_user?.map((user) => ({
|
||||
avatar: user?.assignees__avatar,
|
||||
email: user?.assignees__email,
|
||||
firstName: user?.assignees__first_name,
|
||||
lastName: user?.assignees__last_name,
|
||||
display_name: user?.assignees__display_name,
|
||||
count: user?.count,
|
||||
}))}
|
||||
title="Most issues closed"
|
||||
|
||||
@@ -16,23 +16,20 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
{defaultAnalytics.pending_issue_user.length > 0 ? (
|
||||
<BarGraph
|
||||
data={defaultAnalytics.pending_issue_user}
|
||||
indexBy="assignees__email"
|
||||
indexBy="assignees__display_name"
|
||||
keys={["count"]}
|
||||
height="250px"
|
||||
colors={() => `#f97316`}
|
||||
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)}
|
||||
tooltip={(datum) => {
|
||||
const assignee = defaultAnalytics.pending_issue_user.find(
|
||||
(a) => a.assignees__email === `${datum.indexValue}`
|
||||
(a) => a.assignees__display_name === `${datum.indexValue}`
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||
<span className="font-medium text-custom-text-200">
|
||||
{assignee
|
||||
? assignee.assignees__first_name + " " + assignee.assignees__last_name
|
||||
: "No assignee"}
|
||||
:{" "}
|
||||
{assignee ? assignee.assignees__display_name : "No assignee"}:{" "}
|
||||
</span>
|
||||
{datum.value}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,8 @@ import { Command } from "cmdk";
|
||||
import { THEMES_OBJ } from "constants/themes";
|
||||
import { useTheme } from "next-themes";
|
||||
import { SettingIcon } from "components/icons";
|
||||
import userService from "services/user.service";
|
||||
import useUser from "hooks/use-user";
|
||||
|
||||
type Props = {
|
||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
||||
@@ -12,24 +14,50 @@ type Props = {
|
||||
|
||||
export const ChangeInterfaceTheme: React.FC<Props> = ({ setIsPaletteOpen }) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const { user, mutateUser } = useUser();
|
||||
|
||||
const updateUserTheme = (newTheme: string) => {
|
||||
if (!user) return;
|
||||
|
||||
setTheme(newTheme);
|
||||
|
||||
mutateUser((prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
theme: {
|
||||
...prevData.theme,
|
||||
theme: newTheme,
|
||||
},
|
||||
};
|
||||
}, false);
|
||||
|
||||
userService.updateUser({
|
||||
theme: {
|
||||
...user.theme,
|
||||
theme: newTheme,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// useEffect only runs on the client, so now we can safely show the UI
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{THEMES_OBJ.map((theme) => (
|
||||
{THEMES_OBJ.filter((t) => t.value !== "custom").map((theme) => (
|
||||
<Command.Item
|
||||
key={theme.value}
|
||||
onSelect={() => {
|
||||
setTheme(theme.value);
|
||||
updateUserTheme(theme.value);
|
||||
setIsPaletteOpen(false);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
|
||||
@@ -24,8 +24,12 @@ import issuesService from "services/issues.service";
|
||||
import inboxService from "services/inbox.service";
|
||||
// fetch keys
|
||||
import { INBOX_LIST, ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
export const CommandPalette: React.FC = () => {
|
||||
const store: any = useMobxStore();
|
||||
|
||||
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
|
||||
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
|
||||
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
|
||||
@@ -74,37 +78,36 @@ export const CommandPalette: React.FC = () => {
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
const singleShortcutKeys = ["p", "v", "d", "h", "q", "m"];
|
||||
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
|
||||
if (!key) return;
|
||||
const keyPressed = key.toLowerCase();
|
||||
// if on input, textarea or editor, don't do anything
|
||||
if (
|
||||
!(e.target instanceof HTMLTextAreaElement) &&
|
||||
!(e.target instanceof HTMLInputElement) &&
|
||||
!(e.target as Element).classList?.contains("remirror-editor")
|
||||
) {
|
||||
if ((ctrlKey || metaKey) && keyPressed === "k") {
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
(e.target as Element).classList?.contains("remirror-editor")
|
||||
)
|
||||
return;
|
||||
|
||||
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
|
||||
|
||||
if (!key) return;
|
||||
|
||||
const keyPressed = key.toLowerCase();
|
||||
|
||||
const cmdClicked = ctrlKey || metaKey;
|
||||
|
||||
if (cmdClicked) {
|
||||
if (keyPressed === "k") {
|
||||
e.preventDefault();
|
||||
setIsPaletteOpen(true);
|
||||
} else if ((ctrlKey || metaKey) && keyPressed === "c") {
|
||||
if (altKey) {
|
||||
e.preventDefault();
|
||||
copyIssueUrlToClipboard();
|
||||
}
|
||||
} else if (keyPressed === "c") {
|
||||
} else if (keyPressed === "c" && altKey) {
|
||||
e.preventDefault();
|
||||
setIsIssueModalOpen(true);
|
||||
} else if ((ctrlKey || metaKey) && keyPressed === "b") {
|
||||
copyIssueUrlToClipboard();
|
||||
} else if (keyPressed === "b") {
|
||||
e.preventDefault();
|
||||
toggleCollapsed();
|
||||
} else if (key === "Delete") {
|
||||
e.preventDefault();
|
||||
setIsBulkDeleteIssuesModalOpen(true);
|
||||
} else if (
|
||||
singleShortcutKeys.includes(keyPressed) &&
|
||||
(ctrlKey || metaKey || altKey || shiftKey)
|
||||
) {
|
||||
e.preventDefault();
|
||||
}
|
||||
} else {
|
||||
if (keyPressed === "c") {
|
||||
setIsIssueModalOpen(true);
|
||||
} else if (keyPressed === "p") {
|
||||
setIsProjectModalOpen(true);
|
||||
} else if (keyPressed === "v") {
|
||||
@@ -117,10 +120,13 @@ export const CommandPalette: React.FC = () => {
|
||||
setIsCreateCycleModalOpen(true);
|
||||
} else if (keyPressed === "m") {
|
||||
setIsCreateModuleModalOpen(true);
|
||||
} else if (keyPressed === "backspace" || keyPressed === "delete") {
|
||||
e.preventDefault();
|
||||
setIsBulkDeleteIssuesModalOpen(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
[toggleCollapsed, copyIssueUrlToClipboard]
|
||||
[copyIssueUrlToClipboard, toggleCollapsed]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -34,15 +34,12 @@ export const ChangeIssueAssignee: React.FC<Props> = ({ setIsPaletteOpen, issue,
|
||||
const options =
|
||||
members?.map(({ member }) => ({
|
||||
value: member.id,
|
||||
query:
|
||||
(member.first_name && member.first_name !== "" ? member.first_name : member.email) +
|
||||
" " +
|
||||
member.last_name ?? "",
|
||||
query: member.display_name,
|
||||
content: (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member} />
|
||||
{member.first_name && member.first_name !== "" ? member.first_name : member.email}
|
||||
{member.display_name}
|
||||
</div>
|
||||
{issue.assignees.includes(member.id) && (
|
||||
<div>
|
||||
|
||||
@@ -428,6 +428,40 @@ const activityDetails: {
|
||||
),
|
||||
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
start_date: {
|
||||
message: (activity, showIssue) => {
|
||||
if (!activity.new_value)
|
||||
return (
|
||||
<>
|
||||
removed the start date
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
from <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
set the start date to{" "}
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{renderShortDateWithYearFormat(activity.new_value)}
|
||||
</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
for <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
state: {
|
||||
message: (activity, showIssue) => (
|
||||
<>
|
||||
@@ -481,7 +515,7 @@ const activityDetails: {
|
||||
};
|
||||
|
||||
export const ActivityIcon = ({ activity }: { activity: IIssueActivity }) => (
|
||||
<>{activityDetails[activity.field as keyof typeof activityDetails].icon}</>
|
||||
<>{activityDetails[activity.field as keyof typeof activityDetails]?.icon}</>
|
||||
);
|
||||
|
||||
export const ActivityMessage = ({
|
||||
@@ -492,6 +526,6 @@ export const ActivityMessage = ({
|
||||
showIssue?: boolean;
|
||||
}) => (
|
||||
<>
|
||||
{activityDetails[activity.field as keyof typeof activityDetails].message(activity, showIssue)}
|
||||
{activityDetails[activity.field as keyof typeof activityDetails]?.message(activity, showIssue)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
// icons
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
Squares2X2Icon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { BlockedIcon, BlockerIcon } from "components/icons";
|
||||
import { Icon } from "components/ui";
|
||||
// helpers
|
||||
import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper";
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import RemirrorRichTextEditor from "components/rich-text-editor";
|
||||
|
||||
const activityDetails: {
|
||||
[key: string]: {
|
||||
message?: string;
|
||||
icon: JSX.Element;
|
||||
};
|
||||
} = {
|
||||
assignee: {
|
||||
message: "removed the assignee",
|
||||
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
assignees: {
|
||||
message: "added a new assignee",
|
||||
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
blocks: {
|
||||
message: "marked this issue being blocked by",
|
||||
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
blocking: {
|
||||
message: "marked this issue is blocking",
|
||||
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
cycles: {
|
||||
message: "set the cycle to",
|
||||
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
labels: {
|
||||
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
modules: {
|
||||
message: "set the module to",
|
||||
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
state: {
|
||||
message: "set the state to",
|
||||
icon: <Squares2X2Icon className="h-3 w-3 text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
priority: {
|
||||
message: "set the priority to",
|
||||
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
name: {
|
||||
message: "set the name to",
|
||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
description: {
|
||||
message: "updated the description.",
|
||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
estimate_point: {
|
||||
message: "set the estimate point to",
|
||||
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
target_date: {
|
||||
message: "set the due date to",
|
||||
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
parent: {
|
||||
message: "set the parent to",
|
||||
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
issue: {
|
||||
message: "deleted the issue.",
|
||||
icon: <Icon iconName="delete" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
estimate: {
|
||||
message: "updated the estimate",
|
||||
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
link: {
|
||||
message: "updated the link",
|
||||
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
attachment: {
|
||||
message: "updated the attachment",
|
||||
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
archived_at: {
|
||||
message: "archived",
|
||||
icon: <Icon iconName="archive" className="!text-sm text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
};
|
||||
|
||||
export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
<div>
|
||||
<ul role="list" className="-mb-4">
|
||||
{activities.map((activity: any, activityIdx: number) => {
|
||||
// determines what type of action is performed
|
||||
let action = activityDetails[activity.field as keyof typeof activityDetails]?.message;
|
||||
if (activity.field === "labels") {
|
||||
action = activity.new_value !== "" ? "added a new label" : "removed the label";
|
||||
} else if (activity.field === "blocking") {
|
||||
action =
|
||||
activity.new_value !== ""
|
||||
? "marked this issue is blocking"
|
||||
: "removed the issue from blocking";
|
||||
} else if (activity.field === "blocks") {
|
||||
action =
|
||||
activity.new_value !== "" ? "marked this issue being blocked by" : "removed blocker";
|
||||
} else if (activity.field === "target_date") {
|
||||
action =
|
||||
activity.new_value && activity.new_value !== ""
|
||||
? "set the due date to"
|
||||
: "removed the due date";
|
||||
} else if (activity.field === "parent") {
|
||||
action =
|
||||
activity.new_value && activity.new_value !== ""
|
||||
? "set the parent to"
|
||||
: "removed the parent";
|
||||
} else if (activity.field === "priority") {
|
||||
action =
|
||||
activity.new_value && activity.new_value !== ""
|
||||
? "set the priority to"
|
||||
: "removed the priority";
|
||||
} else if (activity.field === "description") {
|
||||
action = "updated the";
|
||||
} else if (activity.field === "attachment") {
|
||||
action = `${activity.verb} the`;
|
||||
} else if (activity.field === "link") {
|
||||
action = `${activity.verb} the`;
|
||||
} else if (activity.field === "archived_at") {
|
||||
action =
|
||||
activity.new_value && activity.new_value === "restore"
|
||||
? "restored the issue"
|
||||
: "archived the issue";
|
||||
}
|
||||
// for values that are after the action clause
|
||||
let value: any = activity.new_value ? activity.new_value : activity.old_value;
|
||||
if (
|
||||
activity.verb === "created" &&
|
||||
activity.field !== "cycles" &&
|
||||
activity.field !== "modules" &&
|
||||
activity.field !== "attachment" &&
|
||||
activity.field !== "link" &&
|
||||
activity.field !== "estimate"
|
||||
) {
|
||||
const { workspace_detail, project, issue } = activity;
|
||||
value = (
|
||||
<span className="text-custom-text-200">
|
||||
created{" "}
|
||||
<Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}>
|
||||
<a className="inline-flex items-center hover:underline">
|
||||
this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
|
||||
</a>
|
||||
</Link>
|
||||
</span>
|
||||
);
|
||||
} else if (activity.field === "state") {
|
||||
value = activity.new_value ? addSpaceIfCamelCase(activity.new_value) : "None";
|
||||
} else if (activity.field === "labels") {
|
||||
let name;
|
||||
let id = "#000000";
|
||||
if (activity.new_value !== "") {
|
||||
name = activity.new_value;
|
||||
id = activity.new_identifier ? activity.new_identifier : id;
|
||||
} else {
|
||||
name = activity.old_value;
|
||||
id = activity.old_identifier ? activity.old_identifier : id;
|
||||
}
|
||||
|
||||
value = name;
|
||||
} else if (activity.field === "assignees") {
|
||||
value = activity.new_value;
|
||||
} else if (activity.field === "target_date") {
|
||||
const date =
|
||||
activity.new_value && activity.new_value !== ""
|
||||
? activity.new_value
|
||||
: activity.old_value;
|
||||
value = renderShortDateWithYearFormat(date as string);
|
||||
} else if (activity.field === "description") {
|
||||
value = "description";
|
||||
} else if (activity.field === "attachment") {
|
||||
value = "attachment";
|
||||
} else if (activity.field === "link") {
|
||||
value = "link";
|
||||
} else if (activity.field === "estimate_point") {
|
||||
value = activity.new_value
|
||||
? activity.new_value + ` Point${parseInt(activity.new_value ?? "", 10) > 1 ? "s" : ""}`
|
||||
: "None";
|
||||
}
|
||||
|
||||
if (activity.field === "comment") {
|
||||
return (
|
||||
<div key={activity.id} className="mt-2">
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{activity.field ? (
|
||||
activity.new_value === "restore" ? (
|
||||
<Icon iconName="history" className="text-sm text-custom-text-200" />
|
||||
) : (
|
||||
activityDetails[activity.field as keyof typeof activityDetails]?.icon
|
||||
)
|
||||
) : activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activity.actor_detail.avatar}
|
||||
alt={activity.actor_detail.first_name}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
|
||||
>
|
||||
{activity.actor_detail.first_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-custom-background-80 px-0.5 py-px">
|
||||
<ChatBubbleLeftEllipsisIcon
|
||||
className="h-3.5 w-3.5 text-custom-text-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="text-xs">
|
||||
{activity.actor_detail.first_name}
|
||||
{activity.actor_detail.is_bot ? "Bot" : " " + activity.actor_detail.last_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||
Commented {timeAgo(activity.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<RemirrorRichTextEditor
|
||||
value={
|
||||
activity.new_value && activity.new_value !== ""
|
||||
? activity.new_value
|
||||
: activity.old_value
|
||||
}
|
||||
editable={false}
|
||||
noBorder
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ("field" in activity && activity.field !== "updated_by") {
|
||||
return (
|
||||
<li key={activity.id}>
|
||||
<div className="relative pb-1">
|
||||
{activities.length > 1 && activityIdx !== activities.length - 1 ? (
|
||||
<span
|
||||
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<div className="relative flex items-start space-x-2">
|
||||
<>
|
||||
<div>
|
||||
<div className="relative px-1.5">
|
||||
<div className="mt-1.5">
|
||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
|
||||
{activity.field ? (
|
||||
activityDetails[activity.field as keyof typeof activityDetails]?.icon
|
||||
) : activity.actor_detail.avatar &&
|
||||
activity.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activity.actor_detail.avatar}
|
||||
alt={activity.actor_detail.first_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
|
||||
>
|
||||
{activity.actor_detail.first_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-3">
|
||||
<div className="text-xs text-custom-text-200">
|
||||
{activity.field === "archived_at" && activity.new_value !== "restore" ? (
|
||||
<span className="text-gray font-medium">Plane</span>
|
||||
) : (
|
||||
<span className="text-gray font-medium">
|
||||
{activity.actor_detail.first_name}
|
||||
{activity.actor_detail.is_bot
|
||||
? " Bot"
|
||||
: " " + activity.actor_detail.last_name}
|
||||
</span>
|
||||
)}
|
||||
<span> {action} </span>
|
||||
{activity.field !== "archived_at" && (
|
||||
<span className="text-xs font-medium text-custom-text-100">
|
||||
{" "}
|
||||
{value}{" "}
|
||||
</span>
|
||||
)}
|
||||
<span className="whitespace-nowrap">{timeAgo(activity.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
@@ -157,10 +157,10 @@ export const FiltersList: React.FC<Props> = ({
|
||||
return (
|
||||
<div
|
||||
key={memberId}
|
||||
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1 capitalize"
|
||||
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
|
||||
>
|
||||
<Avatar user={member} />
|
||||
<span>{member?.first_name}</span>
|
||||
<span>{member?.display_name}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
@@ -184,7 +184,7 @@ export const FiltersList: React.FC<Props> = ({
|
||||
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1 capitalize"
|
||||
>
|
||||
<Avatar user={member} />
|
||||
<span>{member?.first_name}</span>
|
||||
<span>{member?.display_name}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
|
||||
@@ -4,6 +4,5 @@ export * from "./sidebar";
|
||||
export * from "./theme";
|
||||
export * from "./views";
|
||||
export * from "./activity";
|
||||
export * from "./feeds";
|
||||
export * from "./reaction-selector";
|
||||
export * from "./image-picker-popover";
|
||||
|
||||
@@ -62,7 +62,7 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }
|
||||
by{" "}
|
||||
{link.created_by_detail.is_bot
|
||||
? link.created_by_detail.first_name + " Bot"
|
||||
: link.created_by_detail.email}
|
||||
: link.created_by_detail.display_name}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -133,9 +133,10 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
avatar: assignee.avatar ?? "",
|
||||
first_name: assignee.first_name ?? "",
|
||||
last_name: assignee.last_name ?? "",
|
||||
display_name: assignee.display_name ?? "",
|
||||
}}
|
||||
/>
|
||||
<span>{assignee.first_name}</span>
|
||||
<span>{assignee.display_name}</span>
|
||||
</div>
|
||||
}
|
||||
completed={assignee.completed_issues}
|
||||
|
||||
@@ -4,17 +4,15 @@ import { useTheme } from "next-themes";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
// ui
|
||||
import { PrimaryButton } from "components/ui";
|
||||
import { ColorPickerInput } from "components/core";
|
||||
// services
|
||||
import userService from "services/user.service";
|
||||
// helper
|
||||
import { applyTheme } from "helpers/theme.helper";
|
||||
// types
|
||||
import { ICustomTheme } from "types";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
type Props = {
|
||||
preLoadedData?: Partial<ICustomTheme> | null;
|
||||
@@ -28,11 +26,14 @@ const defaultValues: ICustomTheme = {
|
||||
sidebarText: "#c5c5c5",
|
||||
darkPalette: false,
|
||||
palette: "",
|
||||
theme: "custom",
|
||||
};
|
||||
|
||||
export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
|
||||
const [darkPalette, setDarkPalette] = useState(false);
|
||||
export const CustomThemeSelector: React.FC<Props> = observer(({ preLoadedData }) => {
|
||||
const store: any = useMobxStore();
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const [darkPalette, setDarkPalette] = useState(false);
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
@@ -43,11 +44,14 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
|
||||
} = useForm<ICustomTheme>({
|
||||
defaultValues,
|
||||
});
|
||||
useEffect(() => {
|
||||
reset({
|
||||
...defaultValues,
|
||||
...preLoadedData,
|
||||
});
|
||||
}, [preLoadedData, reset]);
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
const { mutateUser } = useUser();
|
||||
|
||||
const handleFormSubmit = async (formData: ICustomTheme) => {
|
||||
const handleUpdateTheme = async (formData: any) => {
|
||||
const payload: ICustomTheme = {
|
||||
background: formData.background,
|
||||
text: formData.text,
|
||||
@@ -56,36 +60,17 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
|
||||
sidebarText: formData.sidebarText,
|
||||
darkPalette: darkPalette,
|
||||
palette: `${formData.background},${formData.text},${formData.primary},${formData.sidebarBackground},${formData.sidebarText}`,
|
||||
theme: "custom",
|
||||
};
|
||||
|
||||
await userService
|
||||
.updateUser({
|
||||
theme: payload,
|
||||
})
|
||||
.then((res) => {
|
||||
mutateUser((prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
setTheme("custom");
|
||||
|
||||
return { ...prevData, ...res };
|
||||
}, false);
|
||||
|
||||
setTheme("custom");
|
||||
applyTheme(payload.palette, darkPalette);
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
return store.user
|
||||
.updateCurrentUserSettings({ theme: payload })
|
||||
.then((response: any) => response)
|
||||
.catch((error: any) => error);
|
||||
};
|
||||
|
||||
const handleUpdateTheme = async (formData: any) => {
|
||||
await handleFormSubmit({ ...formData, darkPalette });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reset({
|
||||
...defaultValues,
|
||||
...preLoadedData,
|
||||
});
|
||||
}, [preLoadedData, reset]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleUpdateTheme)}>
|
||||
<div className="space-y-5">
|
||||
@@ -164,4 +149,4 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,43 +1,44 @@
|
||||
import { useState, useEffect, Dispatch, SetStateAction } from "react";
|
||||
|
||||
// next-themes
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
// constants
|
||||
import { THEMES_OBJ } from "constants/themes";
|
||||
// ui
|
||||
import { CustomSelect } from "components/ui";
|
||||
// types
|
||||
import { ICustomTheme, IUser } from "types";
|
||||
import { ICustomTheme } from "types";
|
||||
import { unsetCustomCssVariables } from "helpers/theme.helper";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
type Props = {
|
||||
user: IUser | undefined;
|
||||
setPreLoadedData: Dispatch<SetStateAction<ICustomTheme | null>>;
|
||||
setPreLoadedData: React.Dispatch<React.SetStateAction<ICustomTheme | null>>;
|
||||
customThemeSelectorOptions: boolean;
|
||||
setCustomThemeSelectorOptions: Dispatch<SetStateAction<boolean>>;
|
||||
setCustomThemeSelectorOptions: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const ThemeSwitch: React.FC<Props> = ({
|
||||
user,
|
||||
setPreLoadedData,
|
||||
customThemeSelectorOptions,
|
||||
setCustomThemeSelectorOptions,
|
||||
}) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { theme, setTheme } = useTheme();
|
||||
export const ThemeSwitch: React.FC<Props> = observer(
|
||||
({ setPreLoadedData, customThemeSelectorOptions, setCustomThemeSelectorOptions }) => {
|
||||
const store: any = useMobxStore();
|
||||
|
||||
// useEffect only runs on the client, so now we can safely show the UI
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
const { user, mutateUser } = useUser();
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
const updateUserTheme = (newTheme: string) => {
|
||||
if (!user) return;
|
||||
setTheme(newTheme);
|
||||
return store.user
|
||||
.updateCurrentUserSettings({ theme: { ...user.theme, theme: newTheme } })
|
||||
.then((response: any) => response)
|
||||
.catch((error: any) => error);
|
||||
};
|
||||
|
||||
const currentThemeObj = THEMES_OBJ.find((t) => t.value === theme);
|
||||
const currentThemeObj = THEMES_OBJ.find((t) => t.value === theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
return (
|
||||
<CustomSelect
|
||||
value={theme}
|
||||
label={
|
||||
@@ -84,26 +85,18 @@ export const ThemeSwitch: React.FC<Props> = ({
|
||||
user.theme.palette !== ",,,,"
|
||||
? user.theme.palette
|
||||
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
|
||||
theme: "custom",
|
||||
});
|
||||
}
|
||||
|
||||
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
|
||||
} else {
|
||||
if (customThemeSelectorOptions) setCustomThemeSelectorOptions(false);
|
||||
|
||||
for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10)) {
|
||||
document.documentElement.style.removeProperty(`--color-background-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-text-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-border-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-primary-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-sidebar-background-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-sidebar-text-${i}`);
|
||||
document.documentElement.style.removeProperty(`--color-sidebar-border-${i}`);
|
||||
}
|
||||
unsetCustomCssVariables();
|
||||
}
|
||||
|
||||
setTheme(value);
|
||||
document.documentElement.style.setProperty("color-scheme", type);
|
||||
updateUserTheme(value);
|
||||
document.documentElement.style.setProperty("--color-scheme", type);
|
||||
}}
|
||||
input
|
||||
width="w-full"
|
||||
@@ -137,6 +130,6 @@ export const ThemeSwitch: React.FC<Props> = ({
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
</>
|
||||
);
|
||||
};
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -81,10 +81,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
break;
|
||||
case "created_by":
|
||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||
title =
|
||||
member?.first_name && member.first_name !== ""
|
||||
? `${member.first_name} ${member.last_name}`
|
||||
: member?.email ?? "";
|
||||
title = member?.display_name ?? "";
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -149,7 +146,9 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
>
|
||||
<span className="flex items-center">{getGroupIcon()}</span>
|
||||
<h2
|
||||
className="text-lg font-semibold capitalize truncate"
|
||||
className={`text-lg font-semibold truncate ${
|
||||
selectedGroup === "created_by" ? "" : "capitalize"
|
||||
}`}
|
||||
style={{
|
||||
writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl",
|
||||
}}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
ViewEstimateSelect,
|
||||
ViewIssueLabel,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
// ui
|
||||
@@ -322,6 +323,14 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
selfPositioned
|
||||
/>
|
||||
)}
|
||||
{properties.start_date && issue.start_date && (
|
||||
<ViewStartDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.due_date && issue.target_date && (
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ViewEstimateSelect,
|
||||
ViewLabelSelect,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
// icons
|
||||
@@ -230,7 +231,14 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
|
||||
{properties.start_date && issue.start_date && (
|
||||
<ViewStartDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.due_date && issue.target_date && (
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ViewEstimateSelect,
|
||||
ViewIssueLabel,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
// ui
|
||||
@@ -244,6 +245,14 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.start_date && issue.start_date && (
|
||||
<ViewStartDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.due_date && issue.target_date && (
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
|
||||
@@ -96,10 +96,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
break;
|
||||
case "created_by":
|
||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||
title =
|
||||
member?.first_name && member.first_name !== ""
|
||||
? `${member.first_name} ${member.last_name}`
|
||||
: member?.email ?? "";
|
||||
title = member?.display_name ?? "";
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -163,7 +160,11 @@ export const SingleList: React.FC<Props> = ({
|
||||
<div className="flex items-center">{getGroupIcon()}</div>
|
||||
)}
|
||||
{selectedGroup !== null ? (
|
||||
<h2 className="text-sm font-semibold capitalize leading-6 text-custom-text-100">
|
||||
<h2
|
||||
className={`text-sm font-semibold leading-6 text-custom-text-100 ${
|
||||
selectedGroup === "created_by" ? "" : "capitalize"
|
||||
}`}
|
||||
>
|
||||
{getGroupTitle()}
|
||||
</h2>
|
||||
) : (
|
||||
|
||||
@@ -361,14 +361,14 @@ export const ActiveCycleDetails: React.FC = () => {
|
||||
height={16}
|
||||
width={16}
|
||||
className="rounded-full"
|
||||
alt={cycle.owned_by.first_name}
|
||||
alt={cycle.owned_by.display_name}
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-background-100 capitalize">
|
||||
{cycle.owned_by.first_name.charAt(0)}
|
||||
{cycle.owned_by.display_name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-custom-text-200">{cycle.owned_by.first_name}</span>
|
||||
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span>
|
||||
</div>
|
||||
|
||||
{cycle.assignees.length > 0 && (
|
||||
|
||||
@@ -88,9 +88,10 @@ export const ActiveCycleProgressStats: React.FC<Props> = ({ cycle }) => {
|
||||
avatar: assignee.avatar ?? "",
|
||||
first_name: assignee.first_name ?? "",
|
||||
last_name: assignee.last_name ?? "",
|
||||
display_name: assignee.display_name ?? "",
|
||||
}}
|
||||
/>
|
||||
<span>{assignee.first_name}</span>
|
||||
<span>{assignee.display_name}</span>
|
||||
</div>
|
||||
}
|
||||
completed={assignee.completed_issues}
|
||||
|
||||
@@ -450,14 +450,14 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
|
||||
height={12}
|
||||
width={12}
|
||||
className="rounded-full"
|
||||
alt={cycle.owned_by.first_name}
|
||||
alt={cycle.owned_by.display_name}
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-800 capitalize text-white">
|
||||
{cycle.owned_by.first_name.charAt(0)}
|
||||
{cycle.owned_by.display_name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-custom-text-200">{cycle.owned_by.first_name}</span>
|
||||
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -250,14 +250,14 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
|
||||
height={16}
|
||||
width={16}
|
||||
className="rounded-full"
|
||||
alt={cycle.owned_by.first_name}
|
||||
alt={cycle.owned_by.display_name}
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-orange-300 capitalize text-white">
|
||||
{cycle.owned_by.first_name.charAt(0)}
|
||||
{cycle.owned_by.display_name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-custom-text-200">{cycle.owned_by.first_name}</span>
|
||||
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-5 items-center gap-2">
|
||||
|
||||
@@ -254,11 +254,11 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
||||
height={16}
|
||||
width={16}
|
||||
className="rounded-full"
|
||||
alt={cycle.owned_by.first_name}
|
||||
alt={cycle.owned_by.display_name}
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-orange-300 capitalize text-white">
|
||||
{cycle.owned_by.first_name.charAt(0)}
|
||||
{cycle.owned_by.display_name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -44,19 +44,12 @@ export const SingleUserSelect: React.FC<Props> = ({ collaborator, index, users,
|
||||
);
|
||||
|
||||
const options = members?.map((member) => ({
|
||||
value: member.member.email,
|
||||
query:
|
||||
(member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email) +
|
||||
" " +
|
||||
member.member.last_name ?? "",
|
||||
value: member.member.display_name,
|
||||
query: member.member.display_name ?? "",
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name + "(" + member.member.email + ")"
|
||||
: member.member.email}
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -3,11 +3,17 @@ import { FC } from "react";
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { useFormContext, useFieldArray, Controller } from "react-hook-form";
|
||||
|
||||
// hooks
|
||||
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||
// fetch keys
|
||||
import { WORKSPACE_MEMBERS_WITH_EMAIL } from "constants/fetch-keys";
|
||||
|
||||
// services
|
||||
import workspaceService from "services/workspace.service";
|
||||
|
||||
// components
|
||||
import { ToggleSwitch, Input, CustomSelect, CustomSearchSelect, Avatar } from "components/ui";
|
||||
@@ -30,22 +36,20 @@ export const JiraImportUsers: FC = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { workspaceMembers: members } = useWorkspaceMembers(workspaceSlug?.toString() ?? "");
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_MEMBERS_WITH_EMAIL(workspaceSlug?.toString() ?? "") : null,
|
||||
workspaceSlug
|
||||
? () => workspaceService.workspaceMembersWithEmail(workspaceSlug?.toString() ?? "")
|
||||
: null
|
||||
);
|
||||
|
||||
const options = members?.map((member) => ({
|
||||
value: member.member.email,
|
||||
query:
|
||||
(member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email) +
|
||||
" " +
|
||||
member.member.last_name ?? "",
|
||||
query: member.member.display_name ?? "",
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name + " (" + member.member.email + ")"
|
||||
: member.member.email}
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -42,12 +42,7 @@ export const SingleImport: React.FC<Props> = ({ service, refreshing, handleDelet
|
||||
</h4>
|
||||
<div className="mt-2 flex items-center gap-2 text-xs text-custom-text-200">
|
||||
<span>{renderShortDateWithYearFormat(service.created_at)}</span>|
|
||||
<span>
|
||||
Imported by{" "}
|
||||
{service.initiated_by_detail.first_name && service.initiated_by_detail.first_name !== ""
|
||||
? service.initiated_by_detail.first_name + " " + service.initiated_by_detail.last_name
|
||||
: service.initiated_by_detail.email}
|
||||
</span>
|
||||
<span>Imported by {service.initiated_by_detail.display_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<CustomMenu ellipsis>
|
||||
|
||||
@@ -122,7 +122,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
||||
activityItem.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activityItem.actor_detail.avatar}
|
||||
alt={activityItem.actor_detail.first_name}
|
||||
alt={activityItem.actor_detail.display_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="rounded-full"
|
||||
@@ -131,7 +131,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
||||
<div
|
||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
|
||||
>
|
||||
{activityItem.actor_detail.first_name.charAt(0)}
|
||||
{activityItem.actor_detail.display_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -150,8 +150,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
||||
) : (
|
||||
<Link href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}>
|
||||
<a className="text-gray font-medium">
|
||||
{activityItem.actor_detail.first_name}{" "}
|
||||
{activityItem.actor_detail.last_name}
|
||||
{activityItem.actor_detail.display_name}
|
||||
</a>
|
||||
</Link>
|
||||
)}{" "}
|
||||
|
||||
@@ -77,7 +77,7 @@ export const IssueAttachments = () => {
|
||||
<Tooltip
|
||||
tooltipContent={`${
|
||||
people?.find((person) => person.member.id === file.updated_by)?.member
|
||||
.first_name ?? ""
|
||||
.display_name ?? ""
|
||||
} uploaded on ${renderLongDateFormat(file.updated_at)}`}
|
||||
>
|
||||
<span>
|
||||
|
||||
@@ -70,7 +70,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
||||
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={comment.actor_detail.avatar}
|
||||
alt={comment.actor_detail.first_name}
|
||||
alt={comment.actor_detail.display_name}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-custom-border-200"
|
||||
@@ -79,7 +79,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
||||
<div
|
||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
|
||||
>
|
||||
{comment.actor_detail.first_name.charAt(0)}
|
||||
{comment.actor_detail.display_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -93,8 +93,9 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="text-xs">
|
||||
{comment.actor_detail.first_name}
|
||||
{comment.actor_detail.is_bot ? "Bot" : " " + comment.actor_detail.last_name}
|
||||
{comment.actor_detail.is_bot
|
||||
? comment.actor_detail.first_name + " Bot"
|
||||
: comment.actor_detail.display_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||
Commented {timeAgo(comment.created_at)}
|
||||
|
||||
@@ -75,6 +75,7 @@ const defaultValues: Partial<IIssue> = {
|
||||
assignees_list: [],
|
||||
labels: [],
|
||||
labels_list: [],
|
||||
start_date: null,
|
||||
target_date: null,
|
||||
};
|
||||
|
||||
@@ -96,6 +97,7 @@ export interface IssueFormProps {
|
||||
| "priority"
|
||||
| "assignee"
|
||||
| "label"
|
||||
| "startDate"
|
||||
| "dueDate"
|
||||
| "estimate"
|
||||
| "parent"
|
||||
@@ -239,6 +241,15 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
});
|
||||
}, [getValues, projectId, reset]);
|
||||
|
||||
const startDate = watch("start_date");
|
||||
const targetDate = watch("target_date");
|
||||
|
||||
const minDate = startDate ? new Date(startDate) : null;
|
||||
minDate?.setDate(minDate.getDate());
|
||||
|
||||
const maxDate = targetDate ? new Date(targetDate) : null;
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectId && (
|
||||
@@ -447,13 +458,34 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="start_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssueDateSelect
|
||||
label="Start date"
|
||||
maxDate={maxDate ?? undefined}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="target_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssueDateSelect value={value} onChange={onChange} />
|
||||
<IssueDateSelect
|
||||
label="Due date"
|
||||
minDate={minDate ?? undefined}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@@ -18,6 +18,7 @@ import useToast from "hooks/use-toast";
|
||||
import useInboxView from "hooks/use-inbox-view";
|
||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
import useProjects from "hooks/use-projects";
|
||||
import useMyIssues from "hooks/my-issues/use-my-issues";
|
||||
// components
|
||||
import { IssueForm } from "components/issues";
|
||||
// types
|
||||
@@ -52,6 +53,7 @@ export interface IssuesModalProps {
|
||||
| "priority"
|
||||
| "assignee"
|
||||
| "label"
|
||||
| "startDate"
|
||||
| "dueDate"
|
||||
| "estimate"
|
||||
| "parent"
|
||||
@@ -85,6 +87,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
const { user } = useUser();
|
||||
const { projects } = useProjects();
|
||||
|
||||
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
|
||||
@@ -95,28 +99,31 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
|
||||
};
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
handleClose();
|
||||
setActiveProject(null);
|
||||
}, [handleClose]);
|
||||
|
||||
useEffect(() => {
|
||||
// if modal is closed, reset active project to null
|
||||
// and return to avoid activeProject being set to some other project
|
||||
if (!isOpen) {
|
||||
setActiveProject(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// if data is present, set active project to the project of the
|
||||
// issue. This has more priority than the project in the url.
|
||||
if (data && data.project) {
|
||||
setActiveProject(data.project);
|
||||
return;
|
||||
}
|
||||
|
||||
// if data is not present, set active project to the project
|
||||
// in the url. This has the least priority.
|
||||
if (projects && projects.length > 0 && !activeProject)
|
||||
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
|
||||
}, [activeProject, data, projectId, projects]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [handleClose]);
|
||||
}, [activeProject, data, projectId, projects, isOpen]);
|
||||
|
||||
const addIssueToCycle = async (issueId: string, cycleId: string) => {
|
||||
if (!workspaceSlug || !activeProject) return;
|
||||
@@ -243,6 +250,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
if (issueView === "calendar") mutate(calendarFetchKey);
|
||||
if (issueView === "gantt_chart") mutate(ganttFetchKey);
|
||||
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||
if (groupedIssues) mutateMyIssues();
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
@@ -263,7 +271,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
});
|
||||
});
|
||||
|
||||
if (!createMore) handleClose();
|
||||
if (!createMore) onClose();
|
||||
};
|
||||
|
||||
const updateIssue = async (payload: Partial<IIssue>) => {
|
||||
@@ -282,7 +290,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
|
||||
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
|
||||
|
||||
if (!createMore) handleClose();
|
||||
if (!createMore) onClose();
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
@@ -320,7 +328,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={() => handleClose()}>
|
||||
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@@ -350,7 +358,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
initialData={data ?? prePopulateData}
|
||||
createMore={createMore}
|
||||
setCreateMore={setCreateMore}
|
||||
handleClose={handleClose}
|
||||
handleClose={onClose}
|
||||
projectId={activeProject ?? ""}
|
||||
setActiveProject={setActiveProject}
|
||||
status={data ? true : false}
|
||||
|
||||
@@ -146,105 +146,108 @@ export const MyIssuesViewOptions: React.FC = () => {
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Group by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
groupBy === "project"
|
||||
? "Project"
|
||||
: GROUP_BY_OPTIONS.find((option) => option.key === groupBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) => {
|
||||
if (issueView === "kanban" && option.key === null) return null;
|
||||
if (option.key === "state" || option.key === "created_by")
|
||||
return null;
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
groupBy === "project"
|
||||
? "Project"
|
||||
: GROUP_BY_OPTIONS.find((option) => option.key === groupBy)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) => {
|
||||
if (issueView === "kanban" && option.key === null) return null;
|
||||
if (option.key === "state" || option.key === "created_by")
|
||||
return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupBy(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupBy(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Order by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) => {
|
||||
if (groupBy === "priority" && option.key === "priority") return null;
|
||||
if (option.key === "sort_order") return null;
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) => {
|
||||
if (groupBy === "priority" && option.key === "priority")
|
||||
return null;
|
||||
if (option.key === "sort_order") return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Issue type</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
|
||||
<div className="w-28">
|
||||
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="relative flex justify-end gap-x-3">
|
||||
<button type="button" onClick={() => resetFilterToDefault()}>
|
||||
Reset to default
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-custom-primary"
|
||||
onClick={() => setNewFilterDefaultView()}
|
||||
>
|
||||
Set as default
|
||||
</button>
|
||||
</div> */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 py-3">
|
||||
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
|
||||
{Object.keys(properties).map((key) => {
|
||||
if (key === "estimate" && !isEstimateActive) return null;
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
[makeIssueCopy, handleEditIssue, handleDeleteIssue]
|
||||
);
|
||||
|
||||
const filtersToDisplay = { ...filters, assignees: null, created_by: null };
|
||||
const filtersToDisplay = { ...filters, assignees: null, created_by: null, subscriber: null };
|
||||
|
||||
const nullFilters = Object.keys(filtersToDisplay).filter(
|
||||
(key) => filtersToDisplay[key as keyof IIssueFilterOptions] === null
|
||||
@@ -264,7 +264,11 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
disableUserActions={disableUserActions}
|
||||
dragDisabled={groupBy !== "priority"}
|
||||
emptyState={{
|
||||
title: "You don't have any issue assigned to you yet",
|
||||
title: filters.assignees
|
||||
? "You don't have any issue assigned to you yet"
|
||||
: filters.created_by
|
||||
? "You have not created any issue yet."
|
||||
: "You have not subscribed to any issue yet.",
|
||||
description: "Keep track of your work in a single place.",
|
||||
primaryButton: {
|
||||
icon: <PlusIcon className="h-4 w-4" />,
|
||||
|
||||
@@ -30,20 +30,11 @@ export const IssueAssigneeSelect: React.FC<Props> = ({ projectId, value = [], on
|
||||
|
||||
const options = members?.map((member) => ({
|
||||
value: member.member.id,
|
||||
query:
|
||||
(member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email) +
|
||||
" " +
|
||||
member.member.last_name ?? "",
|
||||
query: member.member.display_name ?? "",
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{`${
|
||||
member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email
|
||||
} ${member.member.last_name ?? ""}`}
|
||||
{member.member.is_bot ? member.member.first_name : member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -8,11 +8,14 @@ import DatePicker from "react-datepicker";
|
||||
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
value: string | null;
|
||||
label: string;
|
||||
maxDate?: Date;
|
||||
minDate?: Date;
|
||||
onChange: (val: string | null) => void;
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
export const IssueDateSelect: React.FC<Props> = ({ value, onChange }) => (
|
||||
export const IssueDateSelect: React.FC<Props> = ({ label, maxDate, minDate, onChange, value }) => (
|
||||
<Popover className="relative flex items-center justify-center rounded-lg">
|
||||
{({ open }) => (
|
||||
<>
|
||||
@@ -28,7 +31,7 @@ export const IssueDateSelect: React.FC<Props> = ({ value, onChange }) => (
|
||||
) : (
|
||||
<>
|
||||
<CalendarDaysIcon className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span>Due Date</span>
|
||||
<span>{label}</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
@@ -51,6 +54,8 @@ export const IssueDateSelect: React.FC<Props> = ({ value, onChange }) => (
|
||||
else onChange(renderDateFormat(val));
|
||||
}}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
inline
|
||||
/>
|
||||
</Popover.Panel>
|
||||
|
||||
@@ -41,20 +41,11 @@ export const SidebarAssigneeSelect: React.FC<Props> = ({
|
||||
|
||||
const options = members?.map((member) => ({
|
||||
value: member.member.id,
|
||||
query:
|
||||
(member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email) +
|
||||
" " +
|
||||
member.member.last_name ?? "",
|
||||
query: member.member.display_name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{`${
|
||||
member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email
|
||||
} ${member.member.last_name ?? ""}`}
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -54,6 +54,7 @@ type Props = {
|
||||
| "parent"
|
||||
| "blocker"
|
||||
| "blocked"
|
||||
| "startDate"
|
||||
| "dueDate"
|
||||
| "cycle"
|
||||
| "module"
|
||||
@@ -210,6 +211,15 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
fieldsToShow.includes("cycle") ||
|
||||
fieldsToShow.includes("module");
|
||||
|
||||
const startDate = watchIssue("start_date");
|
||||
const targetDate = watchIssue("target_date");
|
||||
|
||||
const minDate = startDate ? new Date(startDate) : null;
|
||||
minDate?.setDate(minDate.getDate());
|
||||
|
||||
const maxDate = targetDate ? new Date(targetDate) : null;
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
|
||||
const isNotAllowed = memberRole.isGuest || memberRole.isViewer;
|
||||
|
||||
return (
|
||||
@@ -367,6 +377,34 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
disabled={uneditable}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
<CalendarDaysIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Start date</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="start_date"
|
||||
render={({ field: { value } }) => (
|
||||
<CustomDatePicker
|
||||
placeholder="Start date"
|
||||
value={value}
|
||||
onChange={(val) =>
|
||||
submitChanges({
|
||||
start_date: val,
|
||||
})
|
||||
}
|
||||
className="bg-custom-background-90"
|
||||
maxDate={maxDate ?? undefined}
|
||||
disabled={isNotAllowed || uneditable}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
@@ -387,6 +425,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
})
|
||||
}
|
||||
className="bg-custom-background-90"
|
||||
minDate={minDate ?? undefined}
|
||||
disabled={isNotAllowed || uneditable}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -47,20 +47,11 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
||||
|
||||
const options = members?.map((member) => ({
|
||||
value: member.member.id,
|
||||
query:
|
||||
(member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email) +
|
||||
" " +
|
||||
member.member.last_name ?? "",
|
||||
query: member.member.display_name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{`${
|
||||
member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email
|
||||
} ${member.member.last_name ?? ""}`}
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -71,11 +62,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
|
||||
tooltipHeading="Assignees"
|
||||
tooltipContent={
|
||||
issue.assignee_details.length > 0
|
||||
? issue.assignee_details
|
||||
.map((assignee) =>
|
||||
assignee?.first_name !== "" ? assignee?.first_name : assignee?.email
|
||||
)
|
||||
.join(", ")
|
||||
? issue.assignee_details.map((assignee) => assignee?.display_name).join(", ")
|
||||
: "No Assignee"
|
||||
}
|
||||
>
|
||||
|
||||
@@ -32,9 +32,12 @@ export const ViewDueDateSelect: React.FC<Props> = ({
|
||||
|
||||
const { issueView } = useIssuesView();
|
||||
|
||||
const minDate = issue.start_date ? new Date(issue.start_date) : null;
|
||||
minDate?.setDate(minDate.getDate());
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading="Due Date"
|
||||
tooltipHeading="Due date"
|
||||
tooltipContent={
|
||||
issue.target_date ? renderShortDateWithYearFormat(issue.target_date) ?? "N/A" : "N/A"
|
||||
}
|
||||
@@ -56,8 +59,6 @@ export const ViewDueDateSelect: React.FC<Props> = ({
|
||||
partialUpdateIssue(
|
||||
{
|
||||
target_date: val,
|
||||
priority: issue.priority,
|
||||
state: issue.state,
|
||||
},
|
||||
issue
|
||||
);
|
||||
@@ -77,6 +78,7 @@ export const ViewDueDateSelect: React.FC<Props> = ({
|
||||
className={`${issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"} ${
|
||||
issueView === "kanban" ? "bg-custom-background-90" : "bg-custom-background-100"
|
||||
}`}
|
||||
minDate={minDate ?? undefined}
|
||||
noBorder={noBorder}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./assignee";
|
||||
export * from "./due-date";
|
||||
export * from "./estimate";
|
||||
export * from "./priority";
|
||||
export * from "./state";
|
||||
export * from "./label";
|
||||
export * from "./priority";
|
||||
export * from "./start-date";
|
||||
export * from "./state";
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// ui
|
||||
import { CustomDatePicker, Tooltip } from "components/ui";
|
||||
// helpers
|
||||
import { findHowManyDaysLeft, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
// services
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue } from "types";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
tooltipPosition?: "top" | "bottom";
|
||||
noBorder?: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const ViewStartDateSelect: React.FC<Props> = ({
|
||||
issue,
|
||||
partialUpdateIssue,
|
||||
tooltipPosition = "top",
|
||||
noBorder = false,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { issueView } = useIssuesView();
|
||||
|
||||
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipHeading="Start date"
|
||||
tooltipContent={
|
||||
issue.start_date ? renderShortDateWithYearFormat(issue.start_date) ?? "N/A" : "N/A"
|
||||
}
|
||||
position={tooltipPosition}
|
||||
>
|
||||
<div className="group flex-shrink-0 relative max-w-[6.5rem]">
|
||||
<CustomDatePicker
|
||||
placeholder="Due date"
|
||||
value={issue?.start_date}
|
||||
onChange={(val) => {
|
||||
partialUpdateIssue(
|
||||
{
|
||||
start_date: val,
|
||||
},
|
||||
issue
|
||||
);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_DUE_DATE",
|
||||
user
|
||||
);
|
||||
}}
|
||||
className={`${issue?.start_date ? "w-[6.5rem]" : "w-[5rem] text-center"} ${
|
||||
issueView === "kanban" ? "bg-custom-background-90" : "bg-custom-background-100"
|
||||
}`}
|
||||
maxDate={maxDate ?? undefined}
|
||||
noBorder={noBorder}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -4,8 +4,13 @@ import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// mobx
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||
// store
|
||||
import labelStore from "store/labels";
|
||||
// hooks
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// react-color
|
||||
@@ -58,20 +63,17 @@ export const CreateUpdateLabelInline = forwardRef<Ref, Props>(function CreateUpd
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const handleLabelCreate: SubmitHandler<IIssueLabels> = async (formData) => {
|
||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||
const { createLabel } = useObserver(() => ({
|
||||
createLabel: labelStore.createLabel,
|
||||
}));
|
||||
|
||||
await issuesService
|
||||
.createIssueLabel(workspaceSlug as string, projectId as string, formData, user)
|
||||
.then((res) => {
|
||||
mutate<IIssueLabels[]>(
|
||||
PROJECT_ISSUE_LABELS(projectId as string),
|
||||
(prevData) => [res, ...(prevData ?? [])],
|
||||
false
|
||||
);
|
||||
reset(defaultValues);
|
||||
setLabelForm(false);
|
||||
});
|
||||
const handleLabelCreate: SubmitHandler<IIssueLabels> = async (formData) => {
|
||||
if (!workspaceSlug || !projectId || isSubmitting || !user) return;
|
||||
|
||||
await createLabel(workspaceSlug.toString(), projectId.toString(), formData, user).then(() => {
|
||||
reset(defaultValues);
|
||||
setLabelForm(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => {
|
||||
|
||||
@@ -32,18 +32,11 @@ export const ModuleLeadSelect: React.FC<Props> = ({ value, onChange }) => {
|
||||
|
||||
const options = members?.map((member) => ({
|
||||
value: member.member.id,
|
||||
query:
|
||||
(member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email) +
|
||||
" " +
|
||||
member.member.last_name ?? "",
|
||||
query: member.member.display_name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email}
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -62,11 +55,7 @@ export const ModuleLeadSelect: React.FC<Props> = ({ value, onChange }) => {
|
||||
<UserCircleIcon className="h-4 w-4 text-custom-text-200" />
|
||||
)}
|
||||
{selectedOption ? (
|
||||
selectedOption?.first_name && selectedOption.first_name !== "" ? (
|
||||
selectedOption?.first_name
|
||||
) : (
|
||||
selectedOption?.email
|
||||
)
|
||||
selectedOption?.display_name
|
||||
) : (
|
||||
<span className="text-custom-text-200">Lead</span>
|
||||
)}
|
||||
|
||||
@@ -30,18 +30,11 @@ export const ModuleMembersSelect: React.FC<Props> = ({ value, onChange }) => {
|
||||
);
|
||||
const options = members?.map((member) => ({
|
||||
value: member.member.id,
|
||||
query:
|
||||
(member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email) +
|
||||
" " +
|
||||
member.member.last_name ?? "",
|
||||
query: member.member.display_name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email}
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -33,18 +33,11 @@ export const SidebarLeadSelect: React.FC<Props> = ({ value, onChange }) => {
|
||||
|
||||
const options = members?.map((member) => ({
|
||||
value: member.member.id,
|
||||
query:
|
||||
(member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email) +
|
||||
" " +
|
||||
member.member.last_name ?? "",
|
||||
query: member.member.display_name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email}
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -64,11 +57,7 @@ export const SidebarLeadSelect: React.FC<Props> = ({ value, onChange }) => {
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedOption && <Avatar user={selectedOption} />}
|
||||
{selectedOption ? (
|
||||
selectedOption?.first_name && selectedOption.first_name !== "" ? (
|
||||
selectedOption?.first_name
|
||||
) : (
|
||||
selectedOption?.email
|
||||
)
|
||||
selectedOption?.display_name
|
||||
) : (
|
||||
<span className="text-custom-text-200">No lead</span>
|
||||
)}
|
||||
|
||||
@@ -31,18 +31,11 @@ export const SidebarMembersSelect: React.FC<Props> = ({ value, onChange }) => {
|
||||
|
||||
const options = members?.map((member) => ({
|
||||
value: member.member.id,
|
||||
query:
|
||||
(member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email) +
|
||||
" " +
|
||||
member.member.last_name ?? "",
|
||||
query: member.member.display_name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email}
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -27,6 +27,7 @@ import { snoozeOptions } from "constants/notification";
|
||||
type NotificationCardProps = {
|
||||
notification: IUserNotification;
|
||||
markNotificationReadStatus: (notificationId: string) => Promise<void>;
|
||||
markNotificationReadStatusToggle: (notificationId: string) => Promise<void>;
|
||||
markNotificationArchivedStatus: (notificationId: string) => Promise<void>;
|
||||
setSelectedNotificationForSnooze: (notificationId: string) => void;
|
||||
markSnoozeNotification: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
|
||||
@@ -36,6 +37,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
const {
|
||||
notification,
|
||||
markNotificationReadStatus,
|
||||
markNotificationReadStatusToggle,
|
||||
markNotificationArchivedStatus,
|
||||
setSelectedNotificationForSnooze,
|
||||
markSnoozeNotification,
|
||||
@@ -76,8 +78,8 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
) : (
|
||||
<div className="w-12 h-12 bg-custom-background-80 rounded-full flex justify-center items-center">
|
||||
<span className="text-custom-text-100 font-medium text-lg">
|
||||
{notification.triggered_by_details.first_name?.[0] ? (
|
||||
notification.triggered_by_details.first_name?.[0]?.toUpperCase()
|
||||
{notification.triggered_by_details.display_name?.[0] ? (
|
||||
notification.triggered_by_details.display_name?.[0]?.toUpperCase()
|
||||
) : (
|
||||
<Icon iconName="person" className="h-6 w-6" />
|
||||
)}
|
||||
@@ -87,10 +89,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
</div>
|
||||
<div className="space-y-2.5 w-full overflow-hidden">
|
||||
<div className="text-sm w-full break-words">
|
||||
<span className="font-semibold">
|
||||
{notification.triggered_by_details.first_name}{" "}
|
||||
{notification.triggered_by_details.last_name}{" "}
|
||||
</span>
|
||||
<span className="font-semibold">{notification.triggered_by_details.display_name} </span>
|
||||
{notification.data.issue_activity.field !== "comment" &&
|
||||
notification.data.issue_activity.verb}{" "}
|
||||
{notification.data.issue_activity.field === "comment"
|
||||
@@ -159,7 +158,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
name: notification.read_at ? "Mark as unread" : "Mark as read",
|
||||
icon: "chat_bubble",
|
||||
onClick: () => {
|
||||
markNotificationReadStatus(notification.id).then(() => {
|
||||
markNotificationReadStatusToggle(notification.id).then(() => {
|
||||
setToastAlert({
|
||||
title: notification.read_at
|
||||
? "Notification marked as unread"
|
||||
|
||||
@@ -21,8 +21,12 @@ import { NotificationsOutlined } from "@mui/icons-material";
|
||||
import emptyNotification from "public/empty-state/notification.svg";
|
||||
// helpers
|
||||
import { getNumberCount } from "helpers/string.helper";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
export const NotificationPopover = () => {
|
||||
const store: any = useMobxStore();
|
||||
|
||||
const {
|
||||
notifications,
|
||||
archived,
|
||||
@@ -38,6 +42,7 @@ export const NotificationPopover = () => {
|
||||
notificationMutate,
|
||||
markNotificationArchivedStatus,
|
||||
markNotificationReadStatus,
|
||||
markNotificationAsRead,
|
||||
markSnoozeNotification,
|
||||
notificationCount,
|
||||
totalNotificationCount,
|
||||
@@ -76,17 +81,17 @@ export const NotificationPopover = () => {
|
||||
tooltipContent="Notifications"
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!sidebarCollapse}
|
||||
disabled={!store?.theme?.sidebarCollapsed}
|
||||
>
|
||||
<Popover.Button
|
||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
||||
isActive
|
||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||
} ${sidebarCollapse ? "justify-center" : ""}`}
|
||||
} ${store?.theme?.sidebarCollapsed ? "justify-center" : ""}`}
|
||||
>
|
||||
<NotificationsOutlined fontSize="small" />
|
||||
{sidebarCollapse ? null : <span>Notifications</span>}
|
||||
{store?.theme?.sidebarCollapsed ? null : <span>Notifications</span>}
|
||||
{totalNotificationCount && totalNotificationCount > 0 ? (
|
||||
<span className="ml-auto bg-custom-primary-300 rounded-full text-xs text-white px-1.5">
|
||||
{getNumberCount(totalNotificationCount)}
|
||||
@@ -128,7 +133,8 @@ export const NotificationPopover = () => {
|
||||
key={notification.id}
|
||||
notification={notification}
|
||||
markNotificationArchivedStatus={markNotificationArchivedStatus}
|
||||
markNotificationReadStatus={markNotificationReadStatus}
|
||||
markNotificationReadStatus={markNotificationAsRead}
|
||||
markNotificationReadStatusToggle={markNotificationReadStatus}
|
||||
setSelectedNotificationForSnooze={setSelectedNotificationForSnooze}
|
||||
markSnoozeNotification={markSnoozeNotification}
|
||||
/>
|
||||
|
||||
@@ -162,7 +162,7 @@ export const SinglePageDetailedItem: React.FC<TSingleStatProps> = ({
|
||||
position="top-right"
|
||||
tooltipContent={`Created by ${
|
||||
people?.find((person) => person.member.id === page.created_by)?.member
|
||||
.first_name ?? ""
|
||||
.display_name ?? ""
|
||||
} on ${renderLongDateFormat(`${page.created_at}`)}`}
|
||||
>
|
||||
<span>
|
||||
|
||||
@@ -161,7 +161,7 @@ export const SinglePageListItem: React.FC<TSingleStatProps> = ({
|
||||
position="top-right"
|
||||
tooltipContent={`Created by ${
|
||||
people?.find((person) => person.member.id === page.created_by)?.member
|
||||
.first_name ?? ""
|
||||
.display_name ?? ""
|
||||
} on ${renderLongDateFormat(`${page.created_at}`)}`}
|
||||
>
|
||||
<span>
|
||||
|
||||
@@ -7,7 +7,9 @@ import userService from "services/user.service";
|
||||
// components
|
||||
import { ActivityMessage } from "components/core";
|
||||
// ui
|
||||
import { Icon, Loader } from "components/ui";
|
||||
import { ProfileEmptyState, Icon, Loader } from "components/ui";
|
||||
// image
|
||||
import recentActivityEmptyState from "public/empty-state/recent_activity.svg";
|
||||
// helpers
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
// fetch-keys
|
||||
@@ -31,51 +33,59 @@ export const ProfileActivity = () => {
|
||||
<h3 className="text-lg font-medium">Recent Activity</h3>
|
||||
<div className="border border-custom-border-100 rounded p-6">
|
||||
{userProfileActivity ? (
|
||||
<div className="space-y-5">
|
||||
{userProfileActivity.results.map((activity) => (
|
||||
<div key={activity.id} className="flex gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activity.actor_detail.avatar}
|
||||
alt={activity.actor_detail.first_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="rounded"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-6 w-6 place-items-center rounded border-2 bg-gray-700 text-xs text-white">
|
||||
{activity.actor_detail.first_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="-mt-1 w-4/5 break-words">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{activity.actor_detail.first_name} {activity.actor_detail.last_name}{" "}
|
||||
</span>
|
||||
{activity.field ? (
|
||||
<ActivityMessage activity={activity} showIssue />
|
||||
userProfileActivity.results.length > 0 ? (
|
||||
<div className="space-y-5">
|
||||
{userProfileActivity.results.map((activity) => (
|
||||
<div key={activity.id} className="flex gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activity.actor_detail.avatar}
|
||||
alt={activity.actor_detail.display_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="rounded"
|
||||
/>
|
||||
) : (
|
||||
<span>
|
||||
created this{" "}
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
Issue
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
</span>
|
||||
<div className="grid h-6 w-6 place-items-center rounded border-2 bg-gray-700 text-xs text-white">
|
||||
{activity.actor_detail.display_name?.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-custom-text-200">{timeAgo(activity.created_at)}</p>
|
||||
</div>
|
||||
<div className="-mt-1 w-4/5 break-words">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{activity.actor_detail.display_name}{" "}
|
||||
</span>
|
||||
{activity.field ? (
|
||||
<ActivityMessage activity={activity} showIssue />
|
||||
) : (
|
||||
<span>
|
||||
created this{" "}
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
Issue
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-custom-text-200">{timeAgo(activity.created_at)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<ProfileEmptyState
|
||||
title="No Data yet"
|
||||
description="We couldn’t find data. Kindly view your inputs"
|
||||
image={recentActivityEmptyState}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// ui
|
||||
import { BarGraph, Loader } from "components/ui";
|
||||
import { BarGraph, ProfileEmptyState, Loader } from "components/ui";
|
||||
// image
|
||||
import priorityGraph from "public/empty-state/priority_graph.svg";
|
||||
// helpers
|
||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||
// types
|
||||
@@ -14,51 +16,61 @@ export const ProfilePriorityDistribution: React.FC<Props> = ({ userProfile }) =>
|
||||
<h3 className="text-lg font-medium">Issues by Priority</h3>
|
||||
{userProfile ? (
|
||||
<div className="border border-custom-border-100 rounded">
|
||||
<BarGraph
|
||||
data={userProfile.priority_distribution.map((priority) => ({
|
||||
priority: capitalizeFirstLetter(priority.priority ?? "None"),
|
||||
value: priority.priority_count,
|
||||
}))}
|
||||
height="300px"
|
||||
indexBy="priority"
|
||||
keys={["value"]}
|
||||
borderRadius={4}
|
||||
padding={0.7}
|
||||
customYAxisTickValues={userProfile.priority_distribution.map((p) => p.priority_count)}
|
||||
tooltip={(datum) => (
|
||||
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||
<span
|
||||
className="h-3 w-3 rounded"
|
||||
style={{
|
||||
backgroundColor: datum.color,
|
||||
}}
|
||||
/>
|
||||
<span className="font-medium text-custom-text-200">{datum.data.priority}:</span>
|
||||
<span>{datum.value}</span>
|
||||
</div>
|
||||
)}
|
||||
colors={(datum) => {
|
||||
if (datum.data.priority === "Urgent") return "#991b1b";
|
||||
else if (datum.data.priority === "High") return "#ef4444";
|
||||
else if (datum.data.priority === "Medium") return "#f59e0b";
|
||||
else if (datum.data.priority === "Low") return "#16a34a";
|
||||
else return "#e5e5e5";
|
||||
}}
|
||||
theme={{
|
||||
axis: {
|
||||
domain: {
|
||||
{userProfile.priority_distribution.length > 0 ? (
|
||||
<BarGraph
|
||||
data={userProfile.priority_distribution.map((priority) => ({
|
||||
priority: capitalizeFirstLetter(priority.priority ?? "None"),
|
||||
value: priority.priority_count,
|
||||
}))}
|
||||
height="300px"
|
||||
indexBy="priority"
|
||||
keys={["value"]}
|
||||
borderRadius={4}
|
||||
padding={0.7}
|
||||
customYAxisTickValues={userProfile.priority_distribution.map((p) => p.priority_count)}
|
||||
tooltip={(datum) => (
|
||||
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||
<span
|
||||
className="h-3 w-3 rounded"
|
||||
style={{
|
||||
backgroundColor: datum.color,
|
||||
}}
|
||||
/>
|
||||
<span className="font-medium text-custom-text-200">{datum.data.priority}:</span>
|
||||
<span>{datum.value}</span>
|
||||
</div>
|
||||
)}
|
||||
colors={(datum) => {
|
||||
if (datum.data.priority === "Urgent") return "#991b1b";
|
||||
else if (datum.data.priority === "High") return "#ef4444";
|
||||
else if (datum.data.priority === "Medium") return "#f59e0b";
|
||||
else if (datum.data.priority === "Low") return "#16a34a";
|
||||
else return "#e5e5e5";
|
||||
}}
|
||||
theme={{
|
||||
axis: {
|
||||
domain: {
|
||||
line: {
|
||||
stroke: "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
line: {
|
||||
stroke: "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
line: {
|
||||
stroke: "transparent",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="p-7">
|
||||
<ProfileEmptyState
|
||||
title="No Data yet"
|
||||
description="Create issues to view the them by priority in the graph for better analysis."
|
||||
image={priorityGraph}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid place-items-center p-7">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// ui
|
||||
import { PieGraph } from "components/ui";
|
||||
import { ProfileEmptyState, PieGraph } from "components/ui";
|
||||
// image
|
||||
import stateGraph from "public/empty-state/state_graph.svg";
|
||||
// types
|
||||
import { IUserProfileData, IUserStateDistribution } from "types";
|
||||
// constants
|
||||
@@ -17,64 +19,72 @@ export const ProfileStateDistribution: React.FC<Props> = ({ stateDistribution, u
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-medium">Issues by State</h3>
|
||||
<div className="border border-custom-border-100 rounded p-7">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<div>
|
||||
<PieGraph
|
||||
data={
|
||||
userProfile.state_distribution.map((group) => ({
|
||||
id: group.state_group,
|
||||
label: group.state_group,
|
||||
value: group.state_count,
|
||||
color: STATE_GROUP_COLORS[group.state_group],
|
||||
})) ?? []
|
||||
}
|
||||
height="250px"
|
||||
innerRadius={0.6}
|
||||
cornerRadius={5}
|
||||
padAngle={2}
|
||||
enableArcLabels
|
||||
arcLabelsTextColor="#000000"
|
||||
enableArcLinkLabels={false}
|
||||
activeInnerRadiusOffset={5}
|
||||
colors={(datum) => datum.data.color}
|
||||
tooltip={(datum) => (
|
||||
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 bg-custom-background-90 p-2 text-xs">
|
||||
<span className="text-custom-text-200 capitalize">
|
||||
{datum.datum.label} issues:
|
||||
</span>{" "}
|
||||
{datum.datum.value}
|
||||
</div>
|
||||
)}
|
||||
margin={{
|
||||
top: 32,
|
||||
right: 0,
|
||||
bottom: 32,
|
||||
left: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="space-y-4 w-full">
|
||||
{stateDistribution.map((group) => (
|
||||
<div
|
||||
key={group.state_group}
|
||||
className="flex items-center justify-between gap-2 text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div
|
||||
className="h-2.5 w-2.5 rounded-sm"
|
||||
style={{
|
||||
backgroundColor: STATE_GROUP_COLORS[group.state_group],
|
||||
}}
|
||||
/>
|
||||
<div className="capitalize whitespace-nowrap">{group.state_group}</div>
|
||||
{userProfile.state_distribution.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<div>
|
||||
<PieGraph
|
||||
data={
|
||||
userProfile.state_distribution.map((group) => ({
|
||||
id: group.state_group,
|
||||
label: group.state_group,
|
||||
value: group.state_count,
|
||||
color: STATE_GROUP_COLORS[group.state_group],
|
||||
})) ?? []
|
||||
}
|
||||
height="250px"
|
||||
innerRadius={0.6}
|
||||
cornerRadius={5}
|
||||
padAngle={2}
|
||||
enableArcLabels
|
||||
arcLabelsTextColor="#000000"
|
||||
enableArcLinkLabels={false}
|
||||
activeInnerRadiusOffset={5}
|
||||
colors={(datum) => datum.data.color}
|
||||
tooltip={(datum) => (
|
||||
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 bg-custom-background-90 p-2 text-xs">
|
||||
<span className="text-custom-text-200 capitalize">
|
||||
{datum.datum.label} issues:
|
||||
</span>{" "}
|
||||
{datum.datum.value}
|
||||
</div>
|
||||
<div>{group.state_count}</div>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
margin={{
|
||||
top: 32,
|
||||
right: 0,
|
||||
bottom: 32,
|
||||
left: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="space-y-4 w-full">
|
||||
{stateDistribution.map((group) => (
|
||||
<div
|
||||
key={group.state_group}
|
||||
className="flex items-center justify-between gap-2 text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div
|
||||
className="h-2.5 w-2.5 rounded-sm"
|
||||
style={{
|
||||
backgroundColor: STATE_GROUP_COLORS[group.state_group],
|
||||
}}
|
||||
/>
|
||||
<div className="capitalize whitespace-nowrap">{group.state_group}</div>
|
||||
</div>
|
||||
<div>{group.state_count}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ProfileEmptyState
|
||||
title="No Data yet"
|
||||
description="Create issues to view the them by states in the graph for better analysis."
|
||||
image={stateGraph}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -172,106 +172,109 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Group by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
groupByProperty === "project"
|
||||
? "Project"
|
||||
: GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) => {
|
||||
if (issueView === "kanban" && option.key === null) return null;
|
||||
if (option.key === "state" || option.key === "created_by")
|
||||
return null;
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
groupByProperty === "project"
|
||||
? "Project"
|
||||
: GROUP_BY_OPTIONS.find(
|
||||
(option) => option.key === groupByProperty
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) => {
|
||||
if (issueView === "kanban" && option.key === null) return null;
|
||||
if (option.key === "state" || option.key === "created_by")
|
||||
return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Order by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) => {
|
||||
if (groupByProperty === "priority" && option.key === "priority")
|
||||
return null;
|
||||
if (option.key === "sort_order") return null;
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) => {
|
||||
if (groupByProperty === "priority" && option.key === "priority")
|
||||
return null;
|
||||
if (option.key === "sort_order") return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Issue type</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
|
||||
<div className="w-28">
|
||||
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="relative flex justify-end gap-x-3">
|
||||
<button type="button" onClick={() => resetFilterToDefault()}>
|
||||
Reset to default
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-custom-primary"
|
||||
onClick={() => setNewFilterDefaultView()}
|
||||
>
|
||||
Set as default
|
||||
</button>
|
||||
</div> */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 py-3">
|
||||
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
|
||||
{Object.keys(properties).map((key) => {
|
||||
if (key === "estimate" && !isEstimateActive) return null;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import useSWR from "swr";
|
||||
import { DropResult } from "react-beautiful-dnd";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import userService from "services/user.service";
|
||||
// hooks
|
||||
import useProfileIssues from "hooks/use-profile-issues";
|
||||
import useUser from "hooks/use-user";
|
||||
@@ -19,7 +20,7 @@ import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssue, IIssueFilterOptions } from "types";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
import { USER_PROFILE_PROJECT_SEGREGATION, WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
|
||||
export const ProfileIssuesView = () => {
|
||||
// create issue modal
|
||||
@@ -60,6 +61,16 @@ export const ProfileIssuesView = () => {
|
||||
params,
|
||||
} = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
|
||||
|
||||
const { data: profileData } = useSWR(
|
||||
workspaceSlug && userId
|
||||
? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString())
|
||||
: null,
|
||||
workspaceSlug && userId
|
||||
? () =>
|
||||
userService.getUserProfileProjectsSegregation(workspaceSlug.toString(), userId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: labels } = useSWR(
|
||||
workspaceSlug && (filters?.labels ?? []).length > 0
|
||||
? WORKSPACE_LABELS(workspaceSlug.toString())
|
||||
@@ -268,10 +279,10 @@ export const ProfileIssuesView = () => {
|
||||
dragDisabled={groupByProperty !== "priority"}
|
||||
emptyState={{
|
||||
title: router.pathname.includes("assigned")
|
||||
? `Issues assigned to ${user?.first_name} ${user?.last_name} will appear here`
|
||||
? `Issues assigned to ${profileData?.user_data.display_name} will appear here`
|
||||
: router.pathname.includes("created")
|
||||
? `Issues created by ${user?.first_name} ${user?.last_name} will appear here`
|
||||
: `Issues subscribed by ${user?.first_name} ${user?.last_name} will appear here`,
|
||||
? `Issues created by ${profileData?.user_data.display_name} will appear here`
|
||||
: `Issues subscribed by ${profileData?.user_data.display_name} will appear here`,
|
||||
}}
|
||||
handleOnDragEnd={handleOnDragEnd}
|
||||
handleIssueAction={handleIssueAction}
|
||||
|
||||
@@ -86,29 +86,29 @@ export const ProfileSidebar = () => {
|
||||
userProjectsData.user_data.cover_image ??
|
||||
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
|
||||
}
|
||||
alt={userProjectsData.user_data.first_name}
|
||||
alt={userProjectsData.user_data.display_name}
|
||||
className="h-32 w-full object-cover"
|
||||
/>
|
||||
<div className="absolute -bottom-[26px] left-5 h-[52px] w-[52px] rounded">
|
||||
{userProjectsData.user_data.avatar && userProjectsData.user_data.avatar !== "" ? (
|
||||
<img
|
||||
src={userProjectsData.user_data.avatar}
|
||||
alt={userProjectsData.user_data.first_name}
|
||||
alt={userProjectsData.user_data.display_name}
|
||||
className="rounded"
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-custom-background-90 flex justify-center items-center w-[52px] h-[52px] rounded text-custom-text-100">
|
||||
{userProjectsData.user_data.first_name[0]}
|
||||
{userProjectsData.user_data.display_name?.[0]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<div className="mt-[38px]">
|
||||
<h4 className="text-lg font-semibold">
|
||||
{userProjectsData.user_data.first_name} {userProjectsData.user_data.last_name}
|
||||
</h4>
|
||||
<h6 className="text-custom-text-200 text-sm">{userProjectsData.user_data.email}</h6>
|
||||
<h4 className="text-lg font-semibold">{userProjectsData.user_data.display_name}</h4>
|
||||
<h6 className="text-custom-text-200 text-sm">
|
||||
{userProjectsData.user_data.display_name}
|
||||
</h6>
|
||||
</div>
|
||||
<div className="mt-6 space-y-5">
|
||||
{userDetails.map((detail) => (
|
||||
|
||||
@@ -67,13 +67,13 @@ const ConfirmProjectMemberRemove: React.FC<Props> = ({ isOpen, onClose, data, ha
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-custom-text-100"
|
||||
>
|
||||
Remove {data?.email}?
|
||||
Remove {data?.display_name}?
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to remove member-{" "}
|
||||
<span className="font-bold">{data?.email}</span>? They will no longer have
|
||||
access to this project. This action cannot be undone.
|
||||
<span className="font-bold">{data?.display_name}</span>? They will no
|
||||
longer have access to this project. This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -163,20 +163,11 @@ export const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen, user })
|
||||
|
||||
const options = workspaceMembers?.map((member) => ({
|
||||
value: member.member.id,
|
||||
query:
|
||||
(member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email) +
|
||||
" " +
|
||||
member.member.last_name ?? "",
|
||||
query: member.member.display_name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{`${
|
||||
member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email
|
||||
} ${member.member.last_name ?? ""}`}
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -376,10 +367,7 @@ export const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen, user })
|
||||
{value ? (
|
||||
<>
|
||||
<Avatar user={selectedMember?.member} />
|
||||
<span>
|
||||
{selectedMember?.member.first_name}{" "}
|
||||
{selectedMember?.member.last_name}
|
||||
</span>
|
||||
<span>{selectedMember?.member.display_name} </span>
|
||||
<span onClick={() => onChange(null)}>
|
||||
<Icon
|
||||
iconName="close"
|
||||
|
||||
@@ -82,7 +82,7 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
|
||||
});
|
||||
|
||||
const uninvitedPeople = people?.filter((person) => {
|
||||
const isInvited = members?.find((member) => member.email === person.member.email);
|
||||
const isInvited = members?.find((member) => member.display_name === person.member.display_name);
|
||||
return !isInvited;
|
||||
});
|
||||
|
||||
@@ -136,11 +136,11 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
|
||||
|
||||
const options = uninvitedPeople?.map((person) => ({
|
||||
value: person.member.id,
|
||||
query: person.member.email,
|
||||
query: person.member.display_name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={person.member} />
|
||||
{person.member.email}
|
||||
{person.member.display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -209,7 +209,10 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
|
||||
people?.find((p) => p.member.id === value)?.member
|
||||
}
|
||||
/>
|
||||
{people?.find((p) => p.member.id === value)?.member.email}
|
||||
{
|
||||
people?.find((p) => p.member.id === value)?.member
|
||||
.display_name
|
||||
}
|
||||
</div>
|
||||
) : (
|
||||
<div>Select co-worker’s email</div>
|
||||
|
||||
@@ -26,8 +26,12 @@ import { orderArrayBy } from "helpers/array.helper";
|
||||
import { IProject } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECTS_LIST } from "constants/fetch-keys";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
export const ProjectSidebarList: FC = () => {
|
||||
const store: any = useMobxStore();
|
||||
|
||||
const [deleteProjectModal, setDeleteProjectModal] = useState(false);
|
||||
const [projectToDelete, setProjectToDelete] = useState<IProject | null>(null);
|
||||
|
||||
@@ -130,7 +134,7 @@ export const ProjectSidebarList: FC = () => {
|
||||
data={projectToDelete}
|
||||
user={user}
|
||||
/>
|
||||
<div className="h-full overflow-y-auto px-5 space-y-3 pt-3 border-t border-custom-sidebar-border-300">
|
||||
<div className="h-full overflow-y-auto px-4 space-y-3 pt-3 border-t border-custom-sidebar-border-300">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="favorite-projects">
|
||||
{(provided) => (
|
||||
@@ -139,7 +143,7 @@ export const ProjectSidebarList: FC = () => {
|
||||
<Disclosure as="div" className="flex flex-col space-y-2" defaultOpen={true}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{!sidebarCollapse && (
|
||||
{!store?.theme?.sidebarCollapsed && (
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
@@ -165,7 +169,7 @@ export const ProjectSidebarList: FC = () => {
|
||||
<SingleSidebarProject
|
||||
key={project.id}
|
||||
project={project}
|
||||
sidebarCollapse={sidebarCollapse}
|
||||
sidebarCollapse={store?.theme?.sidebarCollapsed}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
handleDeleteProject={() => handleDeleteProject(project)}
|
||||
@@ -194,7 +198,7 @@ export const ProjectSidebarList: FC = () => {
|
||||
<Disclosure as="div" className="flex flex-col space-y-2" defaultOpen={true}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{!sidebarCollapse && (
|
||||
{!store?.theme?.sidebarCollapsed && (
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
@@ -215,7 +219,7 @@ export const ProjectSidebarList: FC = () => {
|
||||
<SingleSidebarProject
|
||||
key={project.id}
|
||||
project={project}
|
||||
sidebarCollapse={sidebarCollapse}
|
||||
sidebarCollapse={store?.theme?.sidebarCollapsed}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
handleDeleteProject={() => handleDeleteProject(project)}
|
||||
@@ -243,7 +247,7 @@ export const ProjectSidebarList: FC = () => {
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{!sidebarCollapse && (
|
||||
{!store?.theme?.sidebarCollapsed && (
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
@@ -261,7 +265,7 @@ export const ProjectSidebarList: FC = () => {
|
||||
<SingleSidebarProject
|
||||
key={project.id}
|
||||
project={project}
|
||||
sidebarCollapse={sidebarCollapse}
|
||||
sidebarCollapse={store?.theme?.sidebarCollapsed}
|
||||
handleDeleteProject={() => handleDeleteProject(project)}
|
||||
handleCopyText={() => handleCopyText(project.id)}
|
||||
shortContextMenu
|
||||
@@ -284,7 +288,7 @@ export const ProjectSidebarList: FC = () => {
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
{!sidebarCollapse && "Add Project"}
|
||||
{!store?.theme?.sidebarCollapsed && "Add Project"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,10 +8,9 @@ import { mutate } from "swr";
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useProjectMembers from "hooks/use-project-members";
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { CustomMenu, Loader, Tooltip } from "components/ui";
|
||||
import { CustomMenu, Tooltip } from "components/ui";
|
||||
// icons
|
||||
import {
|
||||
CalendarDaysIcon,
|
||||
@@ -46,11 +45,8 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
// fetching project members information
|
||||
const { members, hasJoined, isOwner, isMember } = useProjectMembers(
|
||||
workspaceSlug as string,
|
||||
project.id
|
||||
);
|
||||
const isOwner = project.member_role === 20;
|
||||
const isMember = project.member_role === 15;
|
||||
|
||||
const handleAddToFavorites = () => {
|
||||
if (!workspaceSlug) return;
|
||||
@@ -125,125 +121,119 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{members ? (
|
||||
<div className="flex flex-col rounded-[10px] bg-custom-background-90 shadow">
|
||||
<div className="flex flex-col rounded-[10px] bg-custom-background-90 shadow">
|
||||
<Link href={`/${workspaceSlug as string}/projects/${project.id}/issues`}>
|
||||
<a>
|
||||
<div className="relative h-32 w-full rounded-t-[10px]">
|
||||
<img
|
||||
src={
|
||||
project.cover_image ??
|
||||
"https://images.unsplash.com/photo-1672243775941-10d763d9adef?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80"
|
||||
}
|
||||
alt={project.name}
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-t-[10px]"
|
||||
/>
|
||||
<div className="absolute bottom-4 left-4 flex items-center gap-3 text-white">
|
||||
{!project.is_member ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setToJoinProject(project.id);
|
||||
}}
|
||||
className="flex cursor-pointer items-center gap-1 rounded bg-green-600 px-2 py-1 text-xs"
|
||||
>
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
<span>Select to Join</span>
|
||||
</button>
|
||||
) : (
|
||||
<span className="cursor-default rounded bg-green-600 px-2 py-1 text-xs">
|
||||
Member
|
||||
</span>
|
||||
)}
|
||||
{project.is_favorite && (
|
||||
<span className="grid h-6 w-9 cursor-default place-items-center rounded bg-orange-400">
|
||||
<StarIcon className="h-3 w-3" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
<div className="flex h-full flex-col rounded-b-[10px] p-4 text-custom-text-200">
|
||||
<Link href={`/${workspaceSlug as string}/projects/${project.id}/issues`}>
|
||||
<a>
|
||||
<div className="relative h-32 w-full rounded-t-[10px]">
|
||||
<img
|
||||
src={
|
||||
project.cover_image ??
|
||||
"https://images.unsplash.com/photo-1672243775941-10d763d9adef?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80"
|
||||
}
|
||||
alt={project.name}
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-t-[10px]"
|
||||
/>
|
||||
<div className="absolute bottom-4 left-4 flex items-center gap-3 text-white">
|
||||
{!hasJoined ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setToJoinProject(project.id);
|
||||
}}
|
||||
className="flex cursor-pointer items-center gap-1 rounded bg-green-600 px-2 py-1 text-xs"
|
||||
>
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
<span>Select to Join</span>
|
||||
</button>
|
||||
) : (
|
||||
<span className="cursor-default rounded bg-green-600 px-2 py-1 text-xs">
|
||||
Member
|
||||
</span>
|
||||
)}
|
||||
{project.is_favorite && (
|
||||
<span className="grid h-6 w-9 cursor-default place-items-center rounded bg-orange-400">
|
||||
<StarIcon className="h-3 w-3" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<h3 className="text-1.5xl font-medium text-custom-text-100">{project.name}</h3>
|
||||
{project.emoji ? (
|
||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
||||
{renderEmoji(project.emoji)}
|
||||
</span>
|
||||
) : project.icon_prop ? (
|
||||
renderEmoji(project.icon_prop)
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-3.5 mb-7 break-words">
|
||||
{truncateText(project.description ?? "", 100)}
|
||||
</p>
|
||||
</a>
|
||||
</Link>
|
||||
<div className="flex h-full flex-col rounded-b-[10px] p-4 text-custom-text-200">
|
||||
<Link href={`/${workspaceSlug as string}/projects/${project.id}/issues`}>
|
||||
<a>
|
||||
<div className="flex items-center gap-1">
|
||||
<h3 className="text-1.5xl font-medium text-custom-text-100">{project.name}</h3>
|
||||
{project.emoji ? (
|
||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
||||
{renderEmoji(project.emoji)}
|
||||
</span>
|
||||
) : project.icon_prop ? (
|
||||
renderEmoji(project.icon_prop)
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-3.5 mb-7 break-words">
|
||||
{truncateText(project.description ?? "", 100)}
|
||||
</p>
|
||||
</a>
|
||||
</Link>
|
||||
<div className="flex h-full items-end justify-between">
|
||||
<Tooltip
|
||||
tooltipContent={`Created at ${renderShortDateWithYearFormat(project.created_at)}`}
|
||||
position="bottom"
|
||||
>
|
||||
<div className="flex cursor-default items-center gap-1.5 text-xs">
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{renderShortDateWithYearFormat(project.created_at)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{hasJoined ? (
|
||||
<div className="flex items-center">
|
||||
{(isOwner || isMember) && (
|
||||
<Link href={`/${workspaceSlug}/projects/${project.id}/settings`}>
|
||||
<a className="grid cursor-pointer place-items-center rounded p-1 duration-300 hover:bg-custom-background-90">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<CustomMenu width="auto" verticalEllipsis>
|
||||
{isOwner && (
|
||||
<CustomMenu.MenuItem onClick={() => setDeleteProject(project.id)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete project</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{project.is_favorite ? (
|
||||
<CustomMenu.MenuItem onClick={handleRemoveFromFavorites}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<StarIcon className="h-4 w-4" />
|
||||
<span>Remove from favorites</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
) : (
|
||||
<CustomMenu.MenuItem onClick={handleAddToFavorites}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<StarIcon className="h-4 w-4" />
|
||||
<span>Add to favorites</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<div className="flex h-full items-end justify-between">
|
||||
<Tooltip
|
||||
tooltipContent={`Created at ${renderShortDateWithYearFormat(project.created_at)}`}
|
||||
position="bottom"
|
||||
>
|
||||
<div className="flex cursor-default items-center gap-1.5 text-xs">
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{renderShortDateWithYearFormat(project.created_at)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{project.is_member ? (
|
||||
<div className="flex items-center">
|
||||
{(isOwner || isMember) && (
|
||||
<Link href={`/${workspaceSlug}/projects/${project.id}/settings`}>
|
||||
<a className="grid cursor-pointer place-items-center rounded p-1 duration-300 hover:bg-custom-background-90">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<CustomMenu width="auto" verticalEllipsis>
|
||||
{isOwner && (
|
||||
<CustomMenu.MenuItem onClick={() => setDeleteProject(project.id)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy project link</span>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete project</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{project.is_favorite ? (
|
||||
<CustomMenu.MenuItem onClick={handleRemoveFromFavorites}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<StarIcon className="h-4 w-4" />
|
||||
<span>Remove from favorites</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
) : (
|
||||
<CustomMenu.MenuItem onClick={handleAddToFavorites}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<StarIcon className="h-4 w-4" />
|
||||
<span>Add to favorites</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy project link</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Loader>
|
||||
<Loader.Item height="144px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -142,16 +142,25 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
||||
}`}
|
||||
>
|
||||
{provided && (
|
||||
<button
|
||||
type="button"
|
||||
className={`absolute top-1/2 -translate-y-1/2 -left-4 hidden rounded p-0.5 ${
|
||||
sidebarCollapse ? "" : "group-hover:!flex"
|
||||
}`}
|
||||
{...provided?.dragHandleProps}
|
||||
<Tooltip
|
||||
tooltipContent={
|
||||
project.sort_order === null
|
||||
? "Join the project to rearrange"
|
||||
: "Drag to rearrange"
|
||||
}
|
||||
position="top-right"
|
||||
>
|
||||
<EllipsisVerticalIcon className="h-4" />
|
||||
<EllipsisVerticalIcon className="-ml-5 h-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`absolute top-1/2 -translate-y-1/2 -left-4 hidden rounded p-0.5 ${
|
||||
sidebarCollapse ? "" : "group-hover:!flex"
|
||||
} ${project.sort_order === null ? "opacity-60 cursor-not-allowed" : ""}`}
|
||||
{...provided?.dragHandleProps}
|
||||
>
|
||||
<EllipsisVerticalIcon className="h-4" />
|
||||
<EllipsisVerticalIcon className="-ml-5 h-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip
|
||||
tooltipContent={`${project.name}`}
|
||||
@@ -161,17 +170,21 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
||||
>
|
||||
<Disclosure.Button
|
||||
as="div"
|
||||
className={`flex items-center w-full cursor-pointer select-none text-left text-sm font-medium ${
|
||||
className={`flex items-center flex-grow truncate cursor-pointer select-none text-left text-sm font-medium ${
|
||||
sidebarCollapse ? "justify-center" : `justify-between`
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div
|
||||
className={`flex items-center flex-grow w-full truncate gap-x-2 ${
|
||||
sidebarCollapse ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
{project.emoji ? (
|
||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
||||
{renderEmoji(project.emoji)}
|
||||
</span>
|
||||
) : project.icon_prop ? (
|
||||
<div className="h-7 w-7 grid place-items-center">
|
||||
<div className="h-7 w-7 flex-shrink-0 grid place-items-center">
|
||||
{renderEmoji(project.icon_prop)}
|
||||
</div>
|
||||
) : (
|
||||
@@ -181,19 +194,15 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
||||
)}
|
||||
|
||||
{!sidebarCollapse && (
|
||||
<p
|
||||
className={`overflow-hidden text-ellipsis ${
|
||||
open ? "" : "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
>
|
||||
{truncateText(project.name, 15)}
|
||||
<p className={`truncate ${open ? "" : "text-custom-sidebar-text-200"}`}>
|
||||
{project.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{!sidebarCollapse && (
|
||||
<ExpandMoreOutlined
|
||||
fontSize="small"
|
||||
className={`${
|
||||
className={`flex-shrink-0 ${
|
||||
open ? "rotate-180" : ""
|
||||
} !hidden group-hover:!block text-custom-sidebar-text-200 duration-300`}
|
||||
/>
|
||||
@@ -202,7 +211,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
||||
</Tooltip>
|
||||
|
||||
{!sidebarCollapse && (
|
||||
<CustomMenu className="hidden group-hover:block" ellipsis>
|
||||
<CustomMenu className="hidden group-hover:block flex-shrink-0" ellipsis>
|
||||
{!shortContextMenu && (
|
||||
<CustomMenu.MenuItem onClick={handleDeleteProject}>
|
||||
<span className="flex items-center justify-start gap-2 ">
|
||||
@@ -257,7 +266,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<Disclosure.Panel className={`space-y-2 ${sidebarCollapse ? "" : "ml-[2.25rem]"}`}>
|
||||
<Disclosure.Panel className={`space-y-2 mt-1 ${sidebarCollapse ? "" : "ml-[2.25rem]"}`}>
|
||||
{navigation(workspaceSlug as string, project?.id).map((item) => {
|
||||
if (
|
||||
(item.name === "Cycles" && !project.cycle_view) ||
|
||||
|
||||
@@ -71,9 +71,7 @@ const SearchListbox: React.FC<Props> = ({
|
||||
} else
|
||||
return (
|
||||
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
|
||||
{user.member.first_name && user.member.first_name !== ""
|
||||
? user.member.first_name.charAt(0)
|
||||
: user.member.email.charAt(0)}
|
||||
{user.member.display_name.charAt(0)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ export const Avatar: React.FC<AvatarProps> = ({
|
||||
<img
|
||||
src={user.avatar}
|
||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-full"
|
||||
alt={user.first_name}
|
||||
alt={user.display_name}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -59,9 +59,7 @@ export const Avatar: React.FC<AvatarProps> = ({
|
||||
fontSize: fontSize,
|
||||
}}
|
||||
>
|
||||
{user?.first_name && user.first_name !== ""
|
||||
? user.first_name.charAt(0)
|
||||
: user?.email?.charAt(0)}
|
||||
{user?.display_name?.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ type Props = {
|
||||
className?: string;
|
||||
isClearable?: boolean;
|
||||
disabled?: boolean;
|
||||
maxDate?: Date;
|
||||
minDate?: Date;
|
||||
};
|
||||
|
||||
@@ -29,6 +30,7 @@ export const CustomDatePicker: React.FC<Props> = ({
|
||||
className = "",
|
||||
isClearable = true,
|
||||
disabled = false,
|
||||
maxDate,
|
||||
minDate,
|
||||
}) => (
|
||||
<DatePicker
|
||||
@@ -54,6 +56,7 @@ export const CustomDatePicker: React.FC<Props> = ({
|
||||
dateFormat="MMM dd, yyyy"
|
||||
isClearable={isClearable}
|
||||
disabled={disabled}
|
||||
maxDate={maxDate}
|
||||
minDate={minDate}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PrimaryButton } from "components/ui";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description: React.ReactNode | string;
|
||||
description?: React.ReactNode;
|
||||
image: any;
|
||||
primaryButton?: {
|
||||
icon?: any;
|
||||
@@ -34,7 +34,7 @@ export const EmptyState: React.FC<Props> = ({
|
||||
<div className="text-center flex flex-col items-center w-full">
|
||||
<Image src={image} className="w-52 sm:w-60" alt={primaryButton?.text} />
|
||||
<h6 className="text-xl font-semibold mt-6 sm:mt-8 mb-3">{title}</h6>
|
||||
<p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>
|
||||
{description && <p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>}
|
||||
<div className="flex items-center gap-4">
|
||||
{primaryButton && (
|
||||
<PrimaryButton className="flex items-center gap-1.5" onClick={primaryButton.onClick}>
|
||||
|
||||
@@ -23,3 +23,4 @@ export * from "./product-updates-modal";
|
||||
export * from "./integration-and-import-export-banner";
|
||||
export * from "./range-datepicker";
|
||||
export * from "./circular-progress";
|
||||
export * from "./profile-empty-state";
|
||||
@@ -66,7 +66,7 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
||||
>
|
||||
<Menu.Items
|
||||
static
|
||||
className="absolute right-0 z-10 mt-1 w-36 origin-top-right select-none rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none"
|
||||
className="absolute right-0 z-10 mt-1 w-36 origin-top-right select-none rounded-md bg-custom-background-90 border border-custom-border-300 text-xs shadow-lg focus:outline-none"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<div className="relative p-1" key={option.id}>
|
||||
@@ -107,7 +107,7 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
||||
</Menu.Item>
|
||||
{option.hasChildren && option.id === openChildFor && (
|
||||
<div
|
||||
className={`absolute top-0 w-36 origin-top-right select-none overflow-y-scroll rounded-md bg-custom-background-90 shadow-lg focus:outline-none ${
|
||||
className={`absolute top-0 min-w-36 whitespace-nowrap origin-top-right select-none overflow-y-scroll rounded-md bg-custom-background-90 border border-custom-border-300 shadow-lg focus:outline-none ${
|
||||
direction === "left"
|
||||
? "right-full -translate-x-1"
|
||||
: "left-full translate-x-1"
|
||||
@@ -125,27 +125,33 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
||||
>
|
||||
{option.children ? (
|
||||
<div className="space-y-1 p-1">
|
||||
{option.children.map((child) => {
|
||||
if (child.element) return child.element;
|
||||
else
|
||||
return (
|
||||
<button
|
||||
key={child.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(child.value)}
|
||||
className={`${
|
||||
child.selected ? "bg-custom-background-80" : ""
|
||||
} flex w-full items-center justify-between break-words rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80`}
|
||||
>
|
||||
{child.label}{" "}
|
||||
<CheckIcon
|
||||
className={`h-3.5 w-3.5 opacity-0 ${
|
||||
child.selected ? "opacity-100" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{option.children.length === 0 ? (
|
||||
<p className="text-custom-text-200 text-center px-1 py-1.5">
|
||||
No {option.label} found
|
||||
</p> //if no children found, show this message.
|
||||
) : (
|
||||
option.children.map((child) => {
|
||||
if (child.element) return child.element;
|
||||
else
|
||||
return (
|
||||
<button
|
||||
key={child.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(child.value)}
|
||||
className={`${
|
||||
child.selected ? "bg-custom-background-80" : ""
|
||||
} flex w-full items-center justify-between break-words rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80`}
|
||||
>
|
||||
{child.label}{" "}
|
||||
<CheckIcon
|
||||
className={`h-3.5 w-3.5 opacity-0 ${
|
||||
child.selected ? "opacity-100" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="p-1 space-y-2">
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description?: React.ReactNode;
|
||||
image: any;
|
||||
};
|
||||
|
||||
export const ProfileEmptyState: React.FC<Props> = ({ title, description, image }) => (
|
||||
<div className={`h-full w-full mx-auto grid place-items-center p-8 `}>
|
||||
<div className="text-center flex flex-col items-center w-full">
|
||||
<div className="flex items-center justify-center h-14 w-14 rounded-full bg-custom-primary-10">
|
||||
<Image src={image} className="w-8" alt={title} />
|
||||
</div>
|
||||
<h6 className="text-base font-semibold mt-3.5 mb-3">{title}</h6>
|
||||
{description && <p className="text-sm text-custom-text-300">{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -127,9 +127,7 @@ export const SelectFilters: React.FC<Props> = ({
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email}
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
@@ -149,9 +147,7 @@ export const SelectFilters: React.FC<Props> = ({
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.first_name && member.member.first_name !== ""
|
||||
? member.member.first_name
|
||||
: member.member.email}
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
|
||||
@@ -67,13 +67,13 @@ const ConfirmWorkspaceMemberRemove: React.FC<Props> = ({ isOpen, onClose, data,
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-custom-text-100"
|
||||
>
|
||||
Remove {data?.email}?
|
||||
Remove {data?.display_name}?
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to remove member-{" "}
|
||||
<span className="font-bold">{data?.email}</span>? They will no longer have
|
||||
access to this workspace. This action cannot be undone.
|
||||
<span className="font-bold">{data?.display_name}</span>? They will no
|
||||
longer have access to this workspace. This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
import { Bolt, HelpOutlineOutlined, WestOutlined } from "@mui/icons-material";
|
||||
import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
|
||||
import { DocumentIcon, DiscordIcon, GithubIcon } from "components/icons";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
const helpOptions = [
|
||||
{
|
||||
@@ -41,6 +43,8 @@ export interface WorkspaceHelpSectionProps {
|
||||
}
|
||||
|
||||
export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setSidebarActive }) => {
|
||||
const store: any = useMobxStore();
|
||||
|
||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||
|
||||
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -53,23 +57,23 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setS
|
||||
<>
|
||||
<div
|
||||
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 py-2 px-4 ${
|
||||
sidebarCollapse ? "flex-col" : ""
|
||||
store?.theme?.sidebarCollapsed ? "flex-col" : ""
|
||||
}`}
|
||||
>
|
||||
{!sidebarCollapse && (
|
||||
{!store?.theme?.sidebarCollapsed && (
|
||||
<div className="w-1/2 text-center cursor-default rounded-md px-2.5 py-1.5 font-medium outline-none text-sm bg-green-500/10 text-green-500">
|
||||
Free Plan
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`flex items-center gap-1 ${
|
||||
sidebarCollapse ? "flex-col justify-center" : "justify-evenly w-1/2"
|
||||
store?.theme?.sidebarCollapsed ? "flex-col justify-center" : "justify-evenly w-1/2"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
||||
sidebarCollapse ? "w-full" : ""
|
||||
store?.theme?.sidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
@@ -83,7 +87,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setS
|
||||
<button
|
||||
type="button"
|
||||
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
||||
sidebarCollapse ? "w-full" : ""
|
||||
store?.theme?.sidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
||||
>
|
||||
@@ -99,13 +103,13 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setS
|
||||
<button
|
||||
type="button"
|
||||
className={`hidden md:grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
||||
sidebarCollapse ? "w-full" : ""
|
||||
store?.theme?.sidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => toggleCollapsed()}
|
||||
onClick={() => store.theme.setSidebarCollapsed(!store?.theme?.sidebarCollapsed)}
|
||||
>
|
||||
<WestOutlined
|
||||
fontSize="small"
|
||||
className={`duration-300 ${sidebarCollapse ? "rotate-180" : ""}`}
|
||||
className={`duration-300 ${store?.theme?.sidebarCollapsed ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@@ -122,7 +126,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setS
|
||||
>
|
||||
<div
|
||||
className={`absolute bottom-2 ${
|
||||
sidebarCollapse ? "left-full" : "left-[-75px]"
|
||||
store?.theme?.sidebarCollapsed ? "left-full" : "left-[-75px]"
|
||||
} space-y-2 rounded-sm bg-custom-background-80 p-1 shadow-md`}
|
||||
ref={helpOptionsRef}
|
||||
>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user