Compare commits

..

33 Commits

Author SHA1 Message Date
Dakshesh Jain b9975dfa24 refactor: typed context and using observer hoc 2023-08-11 14:05:32 +05:30
Dakshesh Jain 380ad340a6 dev: label state management setup 2023-08-10 17:51:47 +05:30
Dakshesh Jain f562fcd466 refactor: both delete or backspace can be used to open modal (#1829)
* refactor: both delete or backspace can be used to open modal

* fix: 'Bulk Issue Delete' modal will open on single click
2023-08-10 16:02:10 +05:30
Dakshesh Jain 8c8668a3e6 refactor: not making calls to member's endpoint to get membership status (#1831)
* refactor: not making call to members endpoint to get membership status

* refactor: type for IProject
2023-08-10 15:39:32 +05:30
Dakshesh Jain 9ce85cdf21 refactor: if/else conditions (#1822)
* refactor: shortcuts if/else

* fix: merge conflict
2023-08-10 13:12:17 +05:30
Dakshesh Jain 1c6cdb8328 refactor: added validation for display_name (#1823) 2023-08-10 13:05:03 +05:30
guru_sainath 005b42cb8d dev: Updating themening worfkflow (#1827) 2023-08-10 13:03:42 +05:30
Dakshesh Jain be062ccd34 fix: import user using endpoint with email (#1819) 2023-08-09 17:49:25 +05:30
Dakshesh Jain c9d0c5353d style: displaying email in members page depending on user role (#1817)
* style: displaying email in members page depending on user role

* refactor: added null safety
2023-08-09 17:23:57 +05:30
Dakshesh Jain 2a6eb5fe23 style: option to edit display name in profile page (#1818) 2023-08-09 16:44:35 +05:30
Aaryan Khandelwal d9ccce41bc chore: profile and my issues page start date option (#1815)
* chore: profile and my issues page start date option

* fix: properties order in my profile view options
2023-08-09 15:46:47 +05:30
Anmol Singh Bhatia 3db69a3a71 style: profile empty state (#1811)
* style: profile empty state

* style: priority empty state padding
2023-08-09 15:45:20 +05:30
Dakshesh Jain faa50b0bbb fix: analytics showing user id instead of display name (#1812)
* fix: replacing first, last name and email with display name

* fix: different endpoint for workspace & project member

* fix: falling back to email if display_name doesn't exist

* fix: analytics showing user id instead of display name
2023-08-09 15:20:29 +05:30
Dakshesh Jain 1991e09035 refactor: removed escape keydown listener (#1814) 2023-08-09 15:18:26 +05:30
Aaryan Khandelwal 4fcd081d27 chore: select start date option for issue (#1813) 2023-08-09 15:17:32 +05:30
Aaryan Khandelwal 5f1209f1db chore: empty state for multi-level dropdown (#1802)
* fix :label filter should show something if there is no label #1779 (#1795)

* style: children empty state

---------

Co-authored-by: Pankaj Chotaliya <34762752+pankajvc@users.noreply.github.com>
2023-08-08 14:13:26 +05:30
Aaryan Khandelwal 88e5a05253 chore: subscribed by me tab on my issues page (#1800)
* chore: add subscribed by me tab in my issues

* chore: update tab titles

* fix: build error
2023-08-08 13:15:25 +05:30
Dakshesh Jain 981acc81c1 fix: replaced first name, last name or email to display name (#1796)
* fix: replacing first, last name and email with display name

* fix: different endpoint for workspace & project member

* fix: falling back to email if display_name doesn't exist
2023-08-08 13:01:43 +05:30
Nikhil cf306ee605 feat: user display name (#1179)
* feat: user display name for the entire system

* feat: update issue activity to remove emails

* dev: update to display name wherever assignees__email and member__email

* dev: update display names on issue activity and the user script

* dev: update display_name function to generate display_name from email

* dev: add email for test purpose

* dev: set default display name for the user

* dev: add migration script and default value

* dev: annotate with assignees_id

* dev: return assignees id

* dev: display name for the profile

* dev: project members endpoint

* dev: url update

* dev: trailing /

* dev: update workspace member serializer

* fix: activity for assignees
2023-08-08 12:59:04 +05:30
guru_sainath 9df0ba6e3a feat: initiated plane space (#1801) 2023-08-08 12:55:42 +05:30
guru_sainath b6744dcd29 Chore: mobx setup and app sidebar and theme management (#1798)
* dev: Mobx integration for app sidebar and custom theme

* dev: Handled edge case and conditional rendering for mobx store
2023-08-08 12:50:27 +05:30
DevMiner a164dfd532 chore(frontend): add sharp (#1451) 2023-08-07 15:52:08 +05:30
Bavisetti Narayan 2b46e5f977 feat: issue export csv (#1781)
* feat: created issue export csv

* fix: optimized the queries

---------

Co-authored-by: NarayanBavisetti <narayan311@gmail.com>
2023-08-07 11:59:04 +05:30
Aaryan Khandelwal 97c3fb40e7 fix: custom theme persisting after signing out (#1780)
* fix: custom theme persistence

* chore: remove console logs

* fix: build error

* fix: change theme from command k
2023-08-03 15:01:31 +05:30
Dakshesh Jain 5aad6c71da fix: notification read status being toggled when click on link (#1769) 2023-08-02 17:23:55 +05:30
Aaryan Khandelwal a1ae338c37 chore: add non existing states for project entities (#1770) 2023-08-02 16:47:36 +05:30
Anmol Singh Bhatia c16b0daa22 style: profile activity (#1771)
* style: profile activity comment log styling

* chore: profile feed activity refactor

* style: sidebar project list
2023-08-02 16:45:34 +05:30
Nikhil 9a29896291 fix: bulk issue import (#1773) 2023-08-02 16:42:47 +05:30
Nikhil a66dcb9419 fix: user profiles n plus 1 (#1765) 2023-08-02 16:42:24 +05:30
Dakshesh Jain 87a920174e fix: comment reaction mutation (#1768) 2023-08-02 14:21:48 +05:30
Anmol Singh Bhatia 584192faba style: sidebar project list improvement (#1767) 2023-08-02 14:21:26 +05:30
Dakshesh Jain b61adbed4b fix: invalid project selection in create issue modal (#1766) 2023-08-02 13:38:45 +05:30
Aaryan Khandelwal 7434800999 chore: show message if dragging unjoined project (#1763) 2023-08-02 12:09:53 +05:30
152 changed files with 5400 additions and 3254 deletions
+4 -4
View File
@@ -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__":
+2 -6
View File
@@ -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,
-57
View File
@@ -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__"
+12 -1
View File
@@ -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)
+43 -1
View File
@@ -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)
+22 -13
View File
@@ -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
+18
View File
@@ -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(
+4 -1
View File
@@ -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 (
+5 -5
View File
@@ -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")
)
+1 -1
View File
@@ -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,
)
+1 -1
View File
@@ -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,
)
+28
View File
@@ -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,
)
+1 -1
View File
@@ -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",
)
+22 -2
View File
@@ -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,
@@ -458,7 +458,7 @@ class ProjectMemberViewSet(BaseViewSet):
]
search_fields = [
"member__email",
"member__display_name",
"member__first_name",
]
@@ -984,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,
)
+37 -11
View File
@@ -47,6 +47,7 @@ from plane.api.serializers import (
WorkspaceThemeSerializer,
IssueActivitySerializer,
IssueLiteSerializer,
WorkspaceMemberAdminSerializer
)
from plane.api.views.base import BaseAPIView
from . import BaseViewSet
@@ -537,7 +538,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
class WorkSpaceMemberViewSet(BaseViewSet):
serializer_class = WorkSpaceMemberSerializer
serializer_class = WorkspaceMemberAdminSerializer
model = WorkspaceMember
permission_classes = [
@@ -545,7 +546,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
]
search_fields = [
"member__email",
"member__display_name",
"member__first_name",
]
@@ -690,7 +691,7 @@ class TeamMemberViewSet(BaseViewSet):
]
search_fields = [
"member__email",
"member__display_name",
"member__first_name",
]
@@ -1048,7 +1049,6 @@ class WorkspaceThemeViewSet(BaseViewSet):
class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
def get(self, request, slug, user_id):
try:
filters = issue_filters(request.query_params, "GET")
@@ -1146,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(
@@ -1166,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:
@@ -1184,7 +1188,6 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
def get(self, request, slug, user_id):
try:
projects = request.query_params.getlist("project", [])
queryset = IssueActivity.objects.filter(
@@ -1212,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 = (
@@ -1227,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(
@@ -1282,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,
@@ -1439,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,
)
+10 -10
View File
@@ -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"))
+42 -42
View File
@@ -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
@@ -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),
]
+12 -1
View File
@@ -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>
@@ -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>
+34
View File
@@ -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) => (
<>
@@ -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={() =>
@@ -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;
@@ -31,9 +29,11 @@ const defaultValues: ICustomTheme = {
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 },
@@ -44,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,
@@ -60,34 +63,14 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
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">
@@ -166,4 +149,4 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
</div>
</form>
);
};
});
+103 -129
View File
@@ -1,9 +1,5 @@
import { useState, useEffect } from "react";
// next-themes
import { useTheme } from "next-themes";
// services
import userService from "services/user.service";
// hooks
import useUser from "hooks/use-user";
// constants
@@ -13,6 +9,10 @@ import { CustomSelect } from "components/ui";
// 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 = {
setPreLoadedData: React.Dispatch<React.SetStateAction<ICustomTheme | null>>;
@@ -20,142 +20,116 @@ type Props = {
setCustomThemeSelectorOptions: React.Dispatch<React.SetStateAction<boolean>>;
};
export const ThemeSwitch: React.FC<Props> = ({
setPreLoadedData,
customThemeSelectorOptions,
setCustomThemeSelectorOptions,
}) => {
const [mounted, setMounted] = useState(false);
export const ThemeSwitch: React.FC<Props> = observer(
({ setPreLoadedData, customThemeSelectorOptions, setCustomThemeSelectorOptions }) => {
const store: any = useMobxStore();
const { theme, setTheme } = useTheme();
const { user, mutateUser } = useUser();
const { theme, setTheme } = useTheme();
const { user, mutateUser } = useUser();
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 updateUserTheme = (newTheme: string) => {
if (!user) return;
const currentThemeObj = THEMES_OBJ.find((t) => t.value === theme);
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;
const currentThemeObj = THEMES_OBJ.find((t) => t.value === theme);
return (
<CustomSelect
value={theme}
label={
currentThemeObj ? (
<div className="flex items-center gap-2">
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
style={{
borderColor: currentThemeObj.icon.border,
}}
>
return (
<CustomSelect
value={theme}
label={
currentThemeObj ? (
<div className="flex items-center gap-2">
<div
className="h-full w-1/2 rounded-l-full"
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
style={{
background: currentThemeObj.icon.color1,
borderColor: currentThemeObj.icon.border,
}}
/>
<div
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderLeftColor: currentThemeObj.icon.border,
background: currentThemeObj.icon.color2,
}}
/>
>
<div
className="h-full w-1/2 rounded-l-full"
style={{
background: currentThemeObj.icon.color1,
}}
/>
<div
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderLeftColor: currentThemeObj.icon.border,
background: currentThemeObj.icon.color2,
}}
/>
</div>
{currentThemeObj.label}
</div>
{currentThemeObj.label}
</div>
) : (
"Select your theme"
)
}
onChange={({ value, type }: { value: string; type: string }) => {
if (value === "custom") {
if (user?.theme.palette) {
setPreLoadedData({
background: user.theme.background !== "" ? user.theme.background : "#0d101b",
text: user.theme.text !== "" ? user.theme.text : "#c5c5c5",
primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff",
sidebarBackground:
user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b",
sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5",
darkPalette: false,
palette:
user.theme.palette !== ",,,,"
? user.theme.palette
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
theme: "custom",
});
) : (
"Select your theme"
)
}
onChange={({ value, type }: { value: string; type: string }) => {
if (value === "custom") {
if (user?.theme.palette) {
setPreLoadedData({
background: user.theme.background !== "" ? user.theme.background : "#0d101b",
text: user.theme.text !== "" ? user.theme.text : "#c5c5c5",
primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff",
sidebarBackground:
user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b",
sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5",
darkPalette: false,
palette:
user.theme.palette !== ",,,,"
? user.theme.palette
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
theme: "custom",
});
}
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
} else {
if (customThemeSelectorOptions) setCustomThemeSelectorOptions(false);
unsetCustomCssVariables();
}
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
} else {
if (customThemeSelectorOptions) setCustomThemeSelectorOptions(false);
unsetCustomCssVariables();
}
updateUserTheme(value);
document.documentElement.style.setProperty("--color-scheme", type);
}}
input
width="w-full"
position="right"
>
{THEMES_OBJ.map(({ value, label, type, icon }) => (
<CustomSelect.Option key={value} value={{ value, type }}>
<div className="flex items-center gap-2">
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
style={{
borderColor: icon.border,
}}
>
updateUserTheme(value);
document.documentElement.style.setProperty("--color-scheme", type);
}}
input
width="w-full"
position="right"
>
{THEMES_OBJ.map(({ value, label, type, icon }) => (
<CustomSelect.Option key={value} value={{ value, type }}>
<div className="flex items-center gap-2">
<div
className="h-full w-1/2 rounded-l-full"
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
style={{
background: icon.color1,
borderColor: icon.border,
}}
/>
<div
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderLeftColor: icon.border,
background: icon.color2,
}}
/>
>
<div
className="h-full w-1/2 rounded-l-full"
style={{
background: icon.color1,
}}
/>
<div
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderLeftColor: icon.border,
background: icon.color2,
}}
/>
</div>
{label}
</div>
{label}
</div>
</CustomSelect.Option>
))}
</CustomSelect>
);
};
</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}
+3 -3
View File
@@ -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>
+3 -4
View File
@@ -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)}
+33 -1
View File
@@ -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 -13
View File
@@ -53,6 +53,7 @@ export interface IssuesModalProps {
| "priority"
| "assignee"
| "label"
| "startDate"
| "dueDate"
| "estimate"
| "parent"
@@ -124,19 +125,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
}, [activeProject, data, projectId, projects, isOpen]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [onClose]);
const addIssueToCycle = async (issueId: string, cycleId: string) => {
if (!workspaceSlug || !activeProject) return;
@@ -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" />,
+2 -11
View File
@@ -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 -3
View File
@@ -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>
),
}));
+39
View File
@@ -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) => {
+3 -14
View File
@@ -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>
),
}));
@@ -78,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" />
)}
@@ -89,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"
@@ -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,
@@ -77,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)}
@@ -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 couldnt 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>
);
@@ -279,10 +279,10 @@ export const ProfileIssuesView = () => {
dragDisabled={groupByProperty !== "priority"}
emptyState={{
title: router.pathname.includes("assigned")
? `Issues assigned to ${profileData?.user_data.first_name} ${profileData?.user_data.last_name} will appear here`
? `Issues assigned to ${profileData?.user_data.display_name} will appear here`
: router.pathname.includes("created")
? `Issues created by ${profileData?.user_data.first_name} ${profileData?.user_data.last_name} will appear here`
: `Issues subscribed by ${profileData?.user_data.first_name} ${profileData?.user_data.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}
+7 -7
View File
@@ -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&rsquo;s email</div>
+11 -7
View File
@@ -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);
@@ -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>
</>
);
};
+1 -3
View File
@@ -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>
);
};
+2 -4
View File
@@ -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>
+3
View File
@@ -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}
/>
);
+1
View File
@@ -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";
+29 -23
View File
@@ -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>
);
+2 -6
View File
@@ -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>
+13 -9
View File
@@ -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}
>
@@ -23,6 +23,8 @@ import { CheckIcon, PlusIcon } from "@heroicons/react/24/outline";
import { truncateText } from "helpers/string.helper";
// types
import { IWorkspace } from "types";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// Static Data
const userLinks = (workspaceSlug: string, userId: string) => [
@@ -54,6 +56,8 @@ const profileLinks = (workspaceSlug: string, userId: string) => [
];
export const WorkspaceSidebarDropdown = () => {
const store: any = useMobxStore();
const router = useRouter();
const { workspaceSlug } = router.query;
@@ -108,7 +112,7 @@ export const WorkspaceSidebarDropdown = () => {
<Menu.Button className="text-custom-sidebar-text-200 flex w-full items-center rounded-sm text-sm font-medium focus:outline-none">
<div
className={`flex w-full items-center gap-x-2 rounded-sm bg-custom-sidebar-background-80 p-1 ${
sidebarCollapse ? "justify-center" : ""
store?.theme?.sidebarCollapsed ? "justify-center" : ""
}`}
>
<div className="relative grid h-6 w-6 place-items-center rounded bg-gray-700 uppercase text-white">
@@ -123,7 +127,7 @@ export const WorkspaceSidebarDropdown = () => {
)}
</div>
{!sidebarCollapse && (
{!store?.theme?.sidebarCollapsed && (
<h4 className="text-custom-text-100">
{activeWorkspace?.name ? truncateText(activeWorkspace.name, 14) : "Loading..."}
</h4>
@@ -145,7 +149,7 @@ export const WorkspaceSidebarDropdown = () => {
border border-custom-sidebar-border-200 bg-custom-sidebar-background-90 shadow-lg outline-none"
>
<div className="flex flex-col items-start justify-start gap-3 p-3">
<div className="text-sm text-custom-sidebar-text-200">{user?.email}</div>
<div className="text-sm text-custom-sidebar-text-200">{user?.display_name}</div>
<span className="text-sm font-semibold text-custom-sidebar-text-200">Workspace</span>
{workspaces ? (
<div className="flex h-full w-full flex-col items-start justify-start gap-3.5">
@@ -243,7 +247,7 @@ export const WorkspaceSidebarDropdown = () => {
</Transition>
</Menu>
{!sidebarCollapse && (
{!store?.theme?.sidebarCollapsed && (
<Menu as="div" className="relative flex-shrink-0">
<Menu.Button className="grid place-items-center outline-none">
<Avatar user={user} height="28px" width="28px" fontSize="14px" />
@@ -16,6 +16,8 @@ import {
TaskAltOutlined,
WorkOutlineOutlined,
} from "@mui/icons-material";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
const workspaceLinks = (workspaceSlug: string) => [
{
@@ -41,6 +43,8 @@ const workspaceLinks = (workspaceSlug: string) => [
];
export const WorkspaceSidebarMenu = () => {
const store: any = useMobxStore();
const router = useRouter();
const { workspaceSlug } = router.query;
@@ -61,17 +65,17 @@ export const WorkspaceSidebarMenu = () => {
tooltipContent={link.name}
position="right"
className="ml-2"
disabled={!sidebarCollapse}
disabled={!store?.theme?.sidebarCollapsed}
>
<div
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 focus:bg-custom-sidebar-background-80"
} ${sidebarCollapse ? "justify-center" : ""}`}
} ${store?.theme?.sidebarCollapsed ? "justify-center" : ""}`}
>
{<link.Icon fontSize="small" />}
{!sidebarCollapse && link.name}
{!store?.theme?.sidebarCollapsed && link.name}
</div>
</Tooltip>
</a>
@@ -41,8 +41,8 @@ const SingleInvitation: React.FC<Props> = ({
<p className="text-sm text-custom-text-200">
Invited by{" "}
{invitation.created_by_detail
? invitation.created_by_detail.first_name
: invitation.workspace.owner.first_name}
? invitation.created_by_detail.display_name
: invitation.workspace.owner.display_name}
</p>
</div>
<div className="flex-shrink-0 self-center">
+1 -1
View File
@@ -19,7 +19,7 @@ export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] =
label: "Label",
},
{
value: "assignees__email",
value: "assignees__id",
label: "Assignee",
},
{
+13 -5
View File
@@ -38,11 +38,12 @@ const inboxParamsToKey = (params: any) => {
};
const myIssuesParamsToKey = (params: any) => {
const { assignees, created_by, labels, priority, state_group, target_date } = params;
const { assignees, created_by, labels, priority, state_group, subscriber, target_date } = params;
let assigneesKey = assignees ? assignees.split(",") : [];
let createdByKey = created_by ? created_by.split(",") : [];
let stateGroupKey = state_group ? state_group.split(",") : [];
let subscriberKey = subscriber ? subscriber.split(",") : [];
let priorityKey = priority ? priority.split(",") : [];
let labelsKey = labels ? labels.split(",") : [];
const targetDateKey = target_date ?? "";
@@ -54,10 +55,11 @@ const myIssuesParamsToKey = (params: any) => {
assigneesKey = assigneesKey.sort().join("_");
createdByKey = createdByKey.sort().join("_");
stateGroupKey = stateGroupKey.sort().join("_");
subscriberKey = subscriberKey.sort().join("_");
priorityKey = priorityKey.sort().join("_");
labelsKey = labelsKey.sort().join("_");
return `${assigneesKey}_${createdByKey}_${stateGroupKey}_${priorityKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}`;
return `${assigneesKey}_${createdByKey}_${stateGroupKey}_${subscriberKey}_${priorityKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}`;
};
export const CURRENT_USER = "CURRENT_USER";
@@ -69,9 +71,13 @@ export const WORKSPACE_DETAILS = (workspaceSlug: string) =>
export const WORKSPACE_MEMBERS = (workspaceSlug: string) =>
`WORKSPACE_MEMBERS_${workspaceSlug.toUpperCase()}`;
export const WORKSPACE_MEMBERS_WITH_EMAIL = (workspaceSlug: string) =>
`WORKSPACE_MEMBERS_WITH_EMAIL_${workspaceSlug.toUpperCase()}`;
export const WORKSPACE_MEMBERS_ME = (workspaceSlug: string) =>
`WORKSPACE_MEMBERS_ME${workspaceSlug.toUpperCase()}`;
export const WORKSPACE_INVITATIONS = "WORKSPACE_INVITATIONS";
export const WORKSPACE_INVITATION_WITH_EMAIL = (workspaceSlug: string) =>
`WORKSPACE_INVITATION_WITH_EMAIL_${workspaceSlug.toUpperCase()}`;
export const WORKSPACE_INVITATION = "WORKSPACE_INVITATION";
export const LAST_ACTIVE_WORKSPACE_AND_PROJECTS = "LAST_ACTIVE_WORKSPACE_AND_PROJECTS";
@@ -90,7 +96,11 @@ export const PROJECTS_LIST = (
export const PROJECT_DETAILS = (projectId: string) => `PROJECT_DETAILS_${projectId.toUpperCase()}`;
export const PROJECT_MEMBERS = (projectId: string) => `PROJECT_MEMBERS_${projectId.toUpperCase()}`;
export const PROJECT_MEMBERS_WITH_EMAIL = (workspaceSlug: string, projectId: string) =>
`PROJECT_MEMBERS_WITH_EMAIL_${workspaceSlug}_${projectId.toUpperCase()}`;
export const PROJECT_INVITATIONS = "PROJECT_INVITATIONS";
export const PROJECT_INVITATIONS_WITH_EMAIL = (workspaceSlug: string, projectId: string) =>
`PROJECT_INVITATIONS_WITH_EMAIL_${workspaceSlug}_${projectId.toUpperCase()}`;
export const PROJECT_ISSUES_LIST = (workspaceSlug: string, projectId: string) =>
`PROJECT_ISSUES_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}`;
@@ -309,9 +319,7 @@ export const USER_PROFILE_PROJECT_SEGREGATION = (workspaceSlug: string, userId:
export const USER_PROFILE_ISSUES = (workspaceSlug: string, userId: string, params: any) => {
const paramsKey = myIssuesParamsToKey(params);
const subscriberKey = params.subscriber ? params.subscriber.toUpperCase() : "NULL";
return `USER_PROFILE_ISSUES_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}_${paramsKey}_${subscriberKey}`;
return `USER_PROFILE_ISSUES_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}_${paramsKey}`;
};
// reactions
+2 -2
View File
@@ -32,8 +32,8 @@ export const SPREADSHEET_COLUMN = [
colName: "Assignees",
colSize: "128px",
icon: UserGroupIcon,
ascendingOrder: "assignees__first_name",
descendingOrder: "-assignees__first_name",
ascendingOrder: "assignees__id",
descendingOrder: "-assignees__id",
},
{
propertyName: "labels",
+1
View File
@@ -92,6 +92,7 @@ export const initialState: StateType = {
labels: null,
state: null,
state_group: null,
subscriber: null,
created_by: null,
target_date: null,
},
+8 -7
View File
@@ -19,7 +19,7 @@ type IssueViewProps = {
orderBy: TIssueOrderByOptions;
showEmptyGroups: boolean;
showSubIssues: boolean;
filters: IIssueFilterOptions & { subscriber: string | null };
filters: IIssueFilterOptions;
properties: Properties;
};
@@ -41,7 +41,7 @@ type ContextType = IssueViewProps & {
setOrderBy: (property: TIssueOrderByOptions) => void;
setShowEmptyGroups: (property: boolean) => void;
setShowSubIssues: (value: boolean) => void;
setFilters: (filters: Partial<IIssueFilterOptions & { subscriber: string | null }>) => void;
setFilters: (filters: Partial<IIssueFilterOptions>) => void;
setProperties: (key: keyof Properties) => void;
setIssueView: (property: TIssueViewOptions) => void;
};
@@ -52,7 +52,7 @@ type StateType = {
orderBy: TIssueOrderByOptions;
showEmptyGroups: boolean;
showSubIssues: boolean;
filters: IIssueFilterOptions & { subscriber: string | null };
filters: IIssueFilterOptions;
properties: Properties;
};
type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType;
@@ -76,16 +76,17 @@ export const initialState: StateType = {
},
properties: {
assignee: true,
attachment_count: true,
created_on: true,
start_date: true,
due_date: true,
estimate: true,
key: true,
labels: true,
link: true,
priority: true,
state: true,
sub_issue_count: true,
attachment_count: true,
link: true,
estimate: true,
created_on: true,
updated_on: true,
},
};

Some files were not shown because too many files have changed in this diff Show More