From 8581226e60bc66ff011d544aeabab36890bc6ecc Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:34:38 +0530 Subject: [PATCH 001/137] chore: improve access field for comments for public boards (#1956) Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> --- apiserver/plane/api/serializers/issue.py | 2 +- apiserver/plane/api/views/issue.py | 37 ++++++++++++++++-------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 64ee2b8f7..647b299fd 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -569,7 +569,7 @@ class IssueCommentSerializer(BaseSerializer): project_detail = ProjectLiteSerializer(read_only=True, source="project") workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True) - + is_member = serializers.BooleanField(read_only=True) class Meta: model = IssueComment diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 0b08bb14f..ff8dab801 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -77,6 +77,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.export_task import issue_export_task class IssueViewSet(BaseViewSet): @@ -588,6 +589,15 @@ class IssueCommentViewSet(BaseViewSet): .select_related("project") .select_related("workspace") .select_related("issue") + .annotate( + is_member=Exists( + ProjectMember.objects.filter( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + member_id=self.request.user.id, + ) + ) + ) .distinct() ) @@ -769,7 +779,9 @@ class SubIssuesEndpoint(BaseAPIView): .order_by("state_group") ) - result = {item["state_group"]: item["state_count"] for item in state_distribution} + result = { + item["state_group"]: item["state_count"] for item in state_distribution + } serializer = IssueLiteSerializer( sub_issues, @@ -1482,6 +1494,15 @@ class IssueCommentPublicViewSet(BaseViewSet): .select_related("project") .select_related("workspace") .select_related("issue") + .annotate( + is_member=Exists( + ProjectMember.objects.filter( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + member_id=self.request.user.id, + ) + ) + ) .distinct() ) else: @@ -1499,21 +1520,13 @@ class IssueCommentPublicViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) - access = ( - "INTERNAL" - if ProjectMember.objects.filter( - project_id=project_id, member=request.user - ).exists() - else "EXTERNAL" - ) - serializer = IssueCommentSerializer(data=request.data) if serializer.is_valid(): serializer.save( project_id=project_id, issue_id=issue_id, actor=request.user, - access=access, + access="EXTERNAL", ) issue_activity.delay( type="comment.activity.created", @@ -1567,7 +1580,8 @@ class IssueCommentPublicViewSet(BaseViewSet): except (IssueComment.DoesNotExist, ProjectDeployBoard.DoesNotExist): return Response( {"error": "IssueComent Does not exists"}, - status=status.HTTP_400_BAD_REQUEST,) + status=status.HTTP_400_BAD_REQUEST, + ) def destroy(self, request, slug, project_id, issue_id, pk): try: @@ -1826,4 +1840,3 @@ class IssueVotePublicViewSet(BaseViewSet): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) - From 3a41ec74427bb9bcdad0ebe2eac7f4ec4c880180 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:35:13 +0530 Subject: [PATCH 002/137] chore: update user activity endpoint to return only workspace activities (#1980) --- apiserver/plane/api/urls.py | 2 +- apiserver/plane/api/views/user.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index a6beac693..64fb4b025 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -235,7 +235,7 @@ urlpatterns = [ UpdateUserTourCompletedEndpoint.as_view(), name="user-tour", ), - path("users/activities/", UserActivityEndpoint.as_view(), name="user-activities"), + path("users/workspaces//activities/", UserActivityEndpoint.as_view(), name="user-activities"), # user workspaces path( "users/me/workspaces/", diff --git a/apiserver/plane/api/views/user.py b/apiserver/plane/api/views/user.py index 84ee47e42..68958e504 100644 --- a/apiserver/plane/api/views/user.py +++ b/apiserver/plane/api/views/user.py @@ -137,11 +137,11 @@ class UpdateUserTourCompletedEndpoint(BaseAPIView): class UserActivityEndpoint(BaseAPIView, BasePaginator): - def get(self, request): + def get(self, request, slug): try: - queryset = IssueActivity.objects.filter(actor=request.user).select_related( - "actor", "workspace", "issue", "project" - ) + queryset = IssueActivity.objects.filter( + actor=request.user, workspace__slug=slug + ).select_related("actor", "workspace", "issue", "project") return self.paginate( request=request, From abcdebef8511ccb2fcd444126a08ed2f97c9a00e Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:35:36 +0530 Subject: [PATCH 003/137] fix: n+1 in issue history and issue automation tasks (#1994) --- apiserver/plane/api/views/issue.py | 6 ++++++ apiserver/plane/bgtasks/issue_automation_task.py | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index ff8dab801..e704969fd 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -493,6 +493,12 @@ class IssueActivityEndpoint(BaseAPIView): .filter(project__project_projectmember__member=self.request.user) .order_by("created_at") .select_related("actor", "issue", "project", "workspace") + .prefetch_related( + Prefetch( + "comment_reactions", + queryset=CommentReaction.objects.select_related("actor"), + ) + ) ) issue_activities = IssueActivitySerializer(issue_activities, many=True).data issue_comments = IssueCommentSerializer(issue_comments, many=True).data diff --git a/apiserver/plane/bgtasks/issue_automation_task.py b/apiserver/plane/bgtasks/issue_automation_task.py index 0e3ead65d..a1f4a3e92 100644 --- a/apiserver/plane/bgtasks/issue_automation_task.py +++ b/apiserver/plane/bgtasks/issue_automation_task.py @@ -64,7 +64,7 @@ def archive_old_issues(): issues_to_update.append(issue) # Bulk Update the issues and log the activity - Issue.objects.bulk_update( + updated_issues = Issue.objects.bulk_update( issues_to_update, ["archived_at"], batch_size=100 ) [ @@ -77,7 +77,7 @@ def archive_old_issues(): current_instance=None, subscriber=False, ) - for issue in issues_to_update + for issue in updated_issues ] return except Exception as e: @@ -136,7 +136,7 @@ def close_old_issues(): issues_to_update.append(issue) # Bulk Update the issues and log the activity - Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100) + updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100) [ issue_activity.delay( type="issue.activity.updated", @@ -147,7 +147,7 @@ def close_old_issues(): current_instance=None, subscriber=False, ) - for issue in issues_to_update + for issue in updated_issues ] return except Exception as e: From e1ad38568826698adb5f7261642c735e92f587f5 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:45:04 +0530 Subject: [PATCH 004/137] fix: issue exports in self hosted instances (#1996) * fix: issue exports in self hosted instances * dev: remove print logs * dev: update url creation function * fix: changed the presigned url for self hosted exports --------- Co-authored-by: NarayanBavisetti --- apiserver/plane/bgtasks/export_task.py | 81 +++++++++++++------ .../plane/bgtasks/exporter_expired_task.py | 29 ++++--- 2 files changed, 75 insertions(+), 35 deletions(-) diff --git a/apiserver/plane/bgtasks/export_task.py b/apiserver/plane/bgtasks/export_task.py index 22a9afe51..a45120eb5 100644 --- a/apiserver/plane/bgtasks/export_task.py +++ b/apiserver/plane/bgtasks/export_task.py @@ -4,6 +4,7 @@ import io import json import boto3 import zipfile +from urllib.parse import urlparse, urlunparse # Django imports from django.conf import settings @@ -23,9 +24,11 @@ def dateTimeConverter(time): if time: return time.strftime("%a, %d %b %Y %I:%M:%S %Z%z") + def dateConverter(time): if time: - return time.strftime("%a, %d %b %Y") + return time.strftime("%a, %d %b %Y") + def create_csv_file(data): csv_buffer = io.StringIO() @@ -66,28 +69,53 @@ def create_zip_file(files): def upload_to_s3(zip_file, workspace_id, token_id, slug): - s3 = boto3.client( - "s3", - region_name=settings.AWS_REGION, - aws_access_key_id=settings.AWS_ACCESS_KEY_ID, - aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, - config=Config(signature_version="s3v4"), - ) file_name = f"{workspace_id}/export-{slug}-{token_id[:6]}-{timezone.now()}.zip" - - s3.upload_fileobj( - zip_file, - settings.AWS_S3_BUCKET_NAME, - file_name, - ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"}, - ) - expires_in = 7 * 24 * 60 * 60 - presigned_url = s3.generate_presigned_url( - "get_object", - Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name}, - ExpiresIn=expires_in, - ) + + if settings.DOCKERIZED and settings.USE_MINIO: + s3 = boto3.client( + "s3", + endpoint_url=settings.AWS_S3_ENDPOINT_URL, + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + config=Config(signature_version="s3v4"), + ) + s3.upload_fileobj( + zip_file, + settings.AWS_STORAGE_BUCKET_NAME, + file_name, + ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"}, + ) + presigned_url = s3.generate_presigned_url( + "get_object", + Params={"Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Key": file_name}, + ExpiresIn=expires_in, + ) + # Create the new url with updated domain and protocol + presigned_url = presigned_url.replace( + "http://plane-minio:9000/uploads/", + f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/", + ) + else: + s3 = boto3.client( + "s3", + region_name=settings.AWS_REGION, + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + config=Config(signature_version="s3v4"), + ) + s3.upload_fileobj( + zip_file, + settings.AWS_S3_BUCKET_NAME, + file_name, + ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"}, + ) + + presigned_url = s3.generate_presigned_url( + "get_object", + Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name}, + ExpiresIn=expires_in, + ) exporter_instance = ExporterHistory.objects.get(token=token_id) @@ -98,7 +126,7 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug): else: exporter_instance.status = "failed" - exporter_instance.save(update_fields=["status", "url","key"]) + exporter_instance.save(update_fields=["status", "url", "key"]) def generate_table_row(issue): @@ -145,7 +173,7 @@ def generate_json_row(issue): else "", "Labels": issue["labels__name"], "Cycle Name": issue["issue_cycle__cycle__name"], - "Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]), + "Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]), "Cycle End Date": dateConverter(issue["issue_cycle__cycle__end_date"]), "Module Name": issue["issue_module__module__name"], "Module Start Date": dateConverter(issue["issue_module__module__start_date"]), @@ -242,7 +270,9 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s workspace_issues = ( ( Issue.objects.filter( - workspace__id=workspace_id, project_id__in=project_ids + workspace__id=workspace_id, + project_id__in=project_ids, + project__project_projectmember__member=exporter_instance.initiated_by_id, ) .select_related("project", "workspace", "state", "parent", "created_by") .prefetch_related( @@ -275,7 +305,7 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s "labels__name", ) ) - .order_by("project__identifier","sequence_id") + .order_by("project__identifier", "sequence_id") .distinct() ) # CSV header @@ -338,7 +368,6 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s exporter_instance.status = "failed" exporter_instance.reason = str(e) exporter_instance.save(update_fields=["status", "reason"]) - # Print logs if in DEBUG mode if settings.DEBUG: print(e) diff --git a/apiserver/plane/bgtasks/exporter_expired_task.py b/apiserver/plane/bgtasks/exporter_expired_task.py index 799904347..a77d68b4b 100644 --- a/apiserver/plane/bgtasks/exporter_expired_task.py +++ b/apiserver/plane/bgtasks/exporter_expired_task.py @@ -21,18 +21,29 @@ def delete_old_s3_link(): expired_exporter_history = ExporterHistory.objects.filter( Q(url__isnull=False) & Q(created_at__lte=timezone.now() - timedelta(days=8)) ).values_list("key", "id") - - s3 = boto3.client( - "s3", - region_name="ap-south-1", - aws_access_key_id=settings.AWS_ACCESS_KEY_ID, - aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, - config=Config(signature_version="s3v4"), - ) + if settings.DOCKERIZED and settings.USE_MINIO: + s3 = boto3.client( + "s3", + endpoint_url=settings.AWS_S3_ENDPOINT_URL, + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + config=Config(signature_version="s3v4"), + ) + else: + s3 = boto3.client( + "s3", + region_name="ap-south-1", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + config=Config(signature_version="s3v4"), + ) for file_name, exporter_id in expired_exporter_history: # Delete object from S3 if file_name: - s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name) + if settings.DOCKERIZED and settings.USE_MINIO: + s3.delete_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_name) + else: + s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name) ExporterHistory.objects.filter(id=exporter_id).update(url=None) From 5ad5da4fd7c0c364edc6919d5ce32dae954e4d22 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:45:25 +0530 Subject: [PATCH 005/137] dev: remove gunicorn config (#1999) --- apiserver/Procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/Procfile b/apiserver/Procfile index 694c49df4..63736e8e8 100644 --- a/apiserver/Procfile +++ b/apiserver/Procfile @@ -1,3 +1,3 @@ -web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile - +web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --max-requests 10000 --max-requests-jitter 1000 --access-logfile - worker: celery -A plane worker -l info beat: celery -A plane beat -l INFO \ No newline at end of file From 91c10930a424b97f383b7b6943025e2fd1aeb24d Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Tue, 29 Aug 2023 14:57:27 +0530 Subject: [PATCH 006/137] feat: mark all read notifications (#1963) * feat: mark all read notifications * fix: changed string to boolean * fix: changed snoozed condition --- apiserver/plane/api/urls.py | 10 +++ apiserver/plane/api/views/__init__.py | 2 +- apiserver/plane/api/views/notification.py | 93 ++++++++++++++++++++++- 3 files changed, 101 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 64fb4b025..483bf99a4 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -164,6 +164,7 @@ from plane.api.views import ( # Notification NotificationViewSet, UnreadNotificationEndpoint, + MarkAllReadNotificationViewSet, ## End Notification # Public Boards ProjectDeployBoardViewSet, @@ -1494,6 +1495,15 @@ urlpatterns = [ UnreadNotificationEndpoint.as_view(), name="unread-notifications", ), + path( + "workspaces//users/notifications/mark-all-read/", + MarkAllReadNotificationViewSet.as_view( + { + "post": "create", + } + ), + name="mark-all-read-notifications", + ), ## End Notification # Public Boards path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 9572c552f..47e7c6f85 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -162,7 +162,7 @@ from .analytic import ( DefaultAnalyticsEndpoint, ) -from .notification import NotificationViewSet, UnreadNotificationEndpoint +from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet from .exporter import ( ExportIssuesEndpoint, diff --git a/apiserver/plane/api/views/notification.py b/apiserver/plane/api/views/notification.py index 2abc82631..75b94f034 100644 --- a/apiserver/plane/api/views/notification.py +++ b/apiserver/plane/api/views/notification.py @@ -10,7 +10,13 @@ from plane.utils.paginator import BasePaginator # Module imports from .base import BaseViewSet, BaseAPIView -from plane.db.models import Notification, IssueAssignee, IssueSubscriber, Issue, WorkspaceMember +from plane.db.models import ( + Notification, + IssueAssignee, + IssueSubscriber, + Issue, + WorkspaceMember, +) from plane.api.serializers import NotificationSerializer @@ -83,13 +89,17 @@ class NotificationViewSet(BaseViewSet, BasePaginator): # Created issues if type == "created": - if WorkspaceMember.objects.filter(workspace__slug=slug, member=request.user, role__lt=15).exists(): + if WorkspaceMember.objects.filter( + workspace__slug=slug, member=request.user, role__lt=15 + ).exists(): notifications = Notification.objects.none() else: issue_ids = Issue.objects.filter( workspace__slug=slug, created_by=request.user ).values_list("pk", flat=True) - notifications = notifications.filter(entity_identifier__in=issue_ids) + notifications = notifications.filter( + entity_identifier__in=issue_ids + ) # Pagination if request.GET.get("per_page", False) and request.GET.get("cursor", False): @@ -274,3 +284,80 @@ class UnreadNotificationEndpoint(BaseAPIView): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class MarkAllReadNotificationViewSet(BaseViewSet): + def create(self, request, slug): + try: + snoozed = request.data.get("snoozed", False) + archived = request.data.get("archived", False) + type = request.data.get("type", "all") + + notifications = ( + Notification.objects.filter( + workspace__slug=slug, + receiver_id=request.user.id, + read_at__isnull=True, + ) + .select_related("workspace", "project", "triggered_by", "receiver") + .order_by("snoozed_till", "-created_at") + ) + + # Filter for snoozed notifications + if snoozed: + notifications = notifications.filter( + Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False) + ) + else: + notifications = notifications.filter( + Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), + ) + + # Filter for archived or unarchive + if archived: + notifications = notifications.filter(archived_at__isnull=False) + else: + notifications = notifications.filter(archived_at__isnull=True) + + # Subscribed issues + if type == "watching": + issue_ids = IssueSubscriber.objects.filter( + workspace__slug=slug, subscriber_id=request.user.id + ).values_list("issue_id", flat=True) + notifications = notifications.filter(entity_identifier__in=issue_ids) + + # Assigned Issues + if type == "assigned": + issue_ids = IssueAssignee.objects.filter( + workspace__slug=slug, assignee_id=request.user.id + ).values_list("issue_id", flat=True) + notifications = notifications.filter(entity_identifier__in=issue_ids) + + # Created issues + if type == "created": + if WorkspaceMember.objects.filter( + workspace__slug=slug, member=request.user, role__lt=15 + ).exists(): + notifications = Notification.objects.none() + else: + issue_ids = Issue.objects.filter( + workspace__slug=slug, created_by=request.user + ).values_list("pk", flat=True) + notifications = notifications.filter( + entity_identifier__in=issue_ids + ) + + updated_notifications = [] + for notification in notifications: + notification.read_at = timezone.now() + updated_notifications.append(notification) + Notification.objects.bulk_update( + updated_notifications, ["read_at"], batch_size=100 + ) + return Response({"message": "Successful"}, 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, + ) From 1d30a9a0a8ef8d1d8dc0406f54866c3a712fb22f Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 29 Aug 2023 15:00:26 +0530 Subject: [PATCH 007/137] chore: project public board issue retrieve (#2003) * chore: project public board issue retrieve * dev: project issues list endpoint * fix: issue public retrieve endpoint --- apiserver/plane/api/serializers/__init__.py | 1 + apiserver/plane/api/serializers/issue.py | 32 +++- apiserver/plane/api/urls.py | 10 +- apiserver/plane/api/views/__init__.py | 3 +- apiserver/plane/api/views/issue.py | 174 ++++++++++++++++++++ apiserver/plane/api/views/project.py | 148 ----------------- 6 files changed, 214 insertions(+), 154 deletions(-) diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 5855f0413..93b21a7f2 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -44,6 +44,7 @@ from .issue import ( IssueReactionSerializer, CommentReactionSerializer, IssueVoteSerializer, + IssuePublicSerializer, ) from .module import ( diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 647b299fd..07afc91bc 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -113,7 +113,11 @@ class IssueCreateSerializer(BaseSerializer): ] def validate(self, data): - if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None): + if ( + data.get("start_date", None) is not None + and data.get("target_date", None) is not None + and data.get("start_date", None) > data.get("target_date", None) + ): raise serializers.ValidationError("Start date cannot exceed target date") return data @@ -554,9 +558,7 @@ class CommentReactionSerializer(BaseSerializer): read_only_fields = ["workspace", "project", "comment", "actor"] - class IssueVoteSerializer(BaseSerializer): - class Meta: model = IssueVote fields = ["issue", "vote", "workspace_id", "project_id", "actor"] @@ -676,6 +678,30 @@ class IssueLiteSerializer(BaseSerializer): ] +class IssuePublicSerializer(BaseSerializer): + project_detail = ProjectLiteSerializer(read_only=True, source="project") + state_detail = StateLiteSerializer(read_only=True, source="state") + issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True) + + class Meta: + model = Issue + fields = [ + "id", + "name", + "description_html", + "sequence_id", + "state", + "state_detail", + "project", + "project_detail", + "workspace", + "priority", + "target_date", + "issue_reactions", + ] + read_only_fields = fields + + class IssueSubscriberSerializer(BaseSerializer): class Meta: model = IssueSubscriber diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 483bf99a4..1fb2b8e90 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -168,13 +168,14 @@ from plane.api.views import ( ## End Notification # Public Boards ProjectDeployBoardViewSet, - ProjectDeployBoardIssuesPublicEndpoint, + ProjectIssuesPublicEndpoint, ProjectDeployBoardPublicSettingsEndpoint, IssueReactionPublicViewSet, CommentReactionPublicViewSet, InboxIssuePublicViewSet, IssueVotePublicViewSet, WorkspaceProjectDeployBoardEndpoint, + IssueRetrievePublicEndpoint, ## End Public Boards ## Exporter ExportIssuesEndpoint, @@ -1534,9 +1535,14 @@ urlpatterns = [ ), path( "public/workspaces//project-boards//issues/", - ProjectDeployBoardIssuesPublicEndpoint.as_view(), + ProjectIssuesPublicEndpoint.as_view(), name="project-deploy-board", ), + path( + "public/workspaces//project-boards//issues//", + IssueRetrievePublicEndpoint.as_view(), + name="workspace-project-boards", + ), path( "public/workspaces//project-boards//issues//comments/", IssueCommentPublicViewSet.as_view( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 47e7c6f85..b697741ae 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -12,7 +12,6 @@ from .project import ( ProjectUserViewsEndpoint, ProjectMemberUserEndpoint, ProjectFavoritesViewSet, - ProjectDeployBoardIssuesPublicEndpoint, ProjectDeployBoardViewSet, ProjectDeployBoardPublicSettingsEndpoint, ProjectMemberEndpoint, @@ -85,6 +84,8 @@ from .issue import ( IssueReactionPublicViewSet, CommentReactionPublicViewSet, IssueVotePublicViewSet, + IssueRetrievePublicEndpoint, + ProjectIssuesPublicEndpoint, ) from .auth_extended import ( diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index e704969fd..4ed109a80 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -28,6 +28,7 @@ from django.conf import settings from rest_framework.response import Response from rest_framework import status from rest_framework.parsers import MultiPartParser, FormParser +from rest_framework.permissions import AllowAny from sentry_sdk import capture_exception # Module imports @@ -49,6 +50,7 @@ from plane.api.serializers import ( IssueReactionSerializer, CommentReactionSerializer, IssueVoteSerializer, + IssuePublicSerializer, ) from plane.api.permissions import ( WorkspaceEntityPermission, @@ -1846,3 +1848,175 @@ class IssueVotePublicViewSet(BaseViewSet): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class IssueRetrievePublicEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request, slug, project_id, issue_id): + try: + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=issue_id + ) + serializer = IssuePublicSerializer(issue) + return Response(serializer.data, status=status.HTTP_200_OK) + except Issue.DoesNotExist: + return Response( + {"error": "Issue Does not exist"}, status=status.HTTP_400_BAD_REQUEST + ) + except Exception as e: + print(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class ProjectIssuesPublicEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request, slug, project_id): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", None] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = ( + Issue.issue_objects.annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .filter(project_id=project_id) + .filter(workspace__slug=slug) + .select_related("project", "workspace", "state", "parent") + .prefetch_related("assignees", "labels") + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), + ) + ) + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + ) + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" if order_by_param.startswith("-") else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + issues = IssuePublicSerializer(issue_queryset, many=True).data + + states = State.objects.filter( + workspace__slug=slug, project_id=project_id + ).values("name", "group", "color", "id") + + labels = Label.objects.filter( + workspace__slug=slug, project_id=project_id + ).values("id", "name", "color", "parent") + + ## Grouping the results + group_by = request.GET.get("group_by", False) + if group_by: + issues = group_results(issues, group_by) + + return Response( + { + "issues": issues, + "states": states, + "labels": labels, + }, + status=status.HTTP_200_OK, + ) + except ProjectDeployBoard.DoesNotExist: + return Response( + {"error": "Board does not exists"}, status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) \ No newline at end of file diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 3e5ca1c4b..97b06fce5 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -1143,154 +1143,6 @@ class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView): ) -class ProjectDeployBoardIssuesPublicEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - - def get(self, request, slug, project_id): - try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - - filters = issue_filters(request.query_params, "GET") - - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", None] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = ( - Issue.issue_objects.annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .filter(project_id=project_id) - .filter(workspace__slug=slug) - .select_related("project", "workspace", "state", "parent") - .prefetch_related("assignees", "labels") - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related("actor"), - ) - ) - .filter(**filters) - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate(module_id=F("issue_module__module_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - ) - - # Priority Ordering - if order_by_param == "priority" or order_by_param == "-priority": - priority_order = ( - priority_order - if order_by_param == "priority" - else priority_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - priority_order=Case( - *[ - When(priority=p, then=Value(i)) - for i, p in enumerate(priority_order) - ], - output_field=CharField(), - ) - ).order_by("priority_order") - - # State Ordering - elif order_by_param in [ - "state__name", - "state__group", - "-state__name", - "-state__group", - ]: - state_order = ( - state_order - if order_by_param in ["state__name", "state__group"] - else state_order[::-1] - ) - issue_queryset = issue_queryset.annotate( - state_order=Case( - *[ - When(state__group=state_group, then=Value(i)) - for i, state_group in enumerate(state_order) - ], - default=Value(len(state_order)), - output_field=CharField(), - ) - ).order_by("state_order") - # assignee and label ordering - elif order_by_param in [ - "labels__name", - "-labels__name", - "assignees__first_name", - "-assignees__first_name", - ]: - issue_queryset = issue_queryset.annotate( - max_values=Max( - order_by_param[1::] - if order_by_param.startswith("-") - else order_by_param - ) - ).order_by( - "-max_values" if order_by_param.startswith("-") else "max_values" - ) - else: - issue_queryset = issue_queryset.order_by(order_by_param) - - issues = IssueLiteSerializer(issue_queryset, many=True).data - - states = State.objects.filter( - workspace__slug=slug, project_id=project_id - ).values("name", "group", "color", "id") - - labels = Label.objects.filter( - workspace__slug=slug, project_id=project_id - ).values("id", "name", "color", "parent") - - ## Grouping the results - group_by = request.GET.get("group_by", False) - if group_by: - issues = group_results(issues, group_by) - - return Response( - { - "issues": issues, - "states": states, - "labels": labels, - }, - status=status.HTTP_200_OK, - ) - except ProjectDeployBoard.DoesNotExist: - return Response( - {"error": "Board does not exists"}, status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - class WorkspaceProjectDeployBoardEndpoint(BaseAPIView): permission_classes = [AllowAny,] From 1cf5e8d80a980d2f241b853fbca50d55ced6b705 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Tue, 29 Aug 2023 15:01:18 +0530 Subject: [PATCH 008/137] fix: only external comments will show in deploy boards (#2010) --- apiserver/plane/api/views/issue.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 4ed109a80..616cef0d6 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -1499,6 +1499,7 @@ class IssueCommentPublicViewSet(BaseViewSet): .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) .filter(issue_id=self.kwargs.get("issue_id")) + .filter(access="EXTERNAL") .select_related("project") .select_related("workspace") .select_related("issue") From b2a41d3bf67ce46f712ed0b57007de50be2cf49c Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Tue, 29 Aug 2023 15:02:29 +0530 Subject: [PATCH 009/137] fix: issue votes (#2006) * fix: issue votes * fix: added default as 1 in vote * fix: issue vote migration file --- apiserver/plane/api/views/issue.py | 3 ++- ..._together_alter_issuevote_vote_and_more.py | 26 +++++++++++++++++++ apiserver/plane/db/models/issue.py | 6 +++-- 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 616cef0d6..05434aec5 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -1822,8 +1822,9 @@ class IssueVotePublicViewSet(BaseViewSet): actor_id=request.user.id, project_id=project_id, issue_id=issue_id, - vote=request.data.get("vote", 1), ) + issue_vote.vote = request.data.get("vote", 1) + issue_vote.save() serializer = IssueVoteSerializer(issue_vote) return Response(serializer.data, status=status.HTTP_201_CREATED) except Exception as e: diff --git a/apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py b/apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py new file mode 100644 index 000000000..d8063acc0 --- /dev/null +++ b/apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.3 on 2023-08-29 07:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0041_cycle_sort_order_issuecomment_access_and_more'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='issuevote', + unique_together=set(), + ), + migrations.AlterField( + model_name='issuevote', + name='vote', + field=models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')], default=1), + ), + migrations.AlterUniqueTogether( + name='issuevote', + unique_together={('issue', 'actor', 'vote')}, + ), + ] diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 7af9e6e14..1633cbaf9 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -476,10 +476,12 @@ class IssueVote(ProjectBaseModel): choices=( (-1, "DOWNVOTE"), (1, "UPVOTE"), - ) + ), + default=1, ) + class Meta: - unique_together = ["issue", "actor"] + unique_together = ["issue", "actor", "vote"] verbose_name = "Issue Vote" verbose_name_plural = "Issue Votes" db_table = "issue_votes" From 90cf39cf59dd20d2abbf4752f42d98b1da221e1a Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 29 Aug 2023 16:40:28 +0530 Subject: [PATCH 010/137] fix: access creation in comments (#2013) --- apiserver/plane/api/serializers/issue.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 07afc91bc..2a75b2f48 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -584,7 +584,6 @@ class IssueCommentSerializer(BaseSerializer): "updated_by", "created_at", "updated_at", - "access", ] From 38a5623c43c74b6480f59bf4a3a9b987167f7dc0 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 29 Aug 2023 20:11:06 +0530 Subject: [PATCH 011/137] dev: user timezone select option (#2002) --- apps/app/constants/timezones.ts | 2386 +++++++++++++++++ .../[workspaceSlug]/me/profile/index.tsx | 47 +- apps/app/types/users.d.ts | 1 + 3 files changed, 2433 insertions(+), 1 deletion(-) create mode 100644 apps/app/constants/timezones.ts diff --git a/apps/app/constants/timezones.ts b/apps/app/constants/timezones.ts new file mode 100644 index 000000000..0ba1df4c0 --- /dev/null +++ b/apps/app/constants/timezones.ts @@ -0,0 +1,2386 @@ +export const TIME_ZONES = [ + { + label: "Africa/Abidjan, GMT", + value: "Africa/Abidjan", + }, + { + label: "Africa/Accra, GMT", + value: "Africa/Accra", + }, + { + label: "Africa/Addis_Ababa, GMT+03:00", + value: "Africa/Addis_Ababa", + }, + { + label: "Africa/Algiers, GMT+01:00", + value: "Africa/Algiers", + }, + { + label: "Africa/Asmara, GMT+03:00", + value: "Africa/Asmara", + }, + { + label: "Africa/Asmera, GMT+03:00", + value: "Africa/Asmera", + }, + { + label: "Africa/Bamako, GMT", + value: "Africa/Bamako", + }, + { + label: "Africa/Bangui, GMT+01:00", + value: "Africa/Bangui", + }, + { + label: "Africa/Banjul, GMT", + value: "Africa/Banjul", + }, + { + label: "Africa/Bissau, GMT", + value: "Africa/Bissau", + }, + { + label: "Africa/Blantyre, GMT+02:00", + value: "Africa/Blantyre", + }, + { + label: "Africa/Brazzaville, GMT+01:00", + value: "Africa/Brazzaville", + }, + { + label: "Africa/Bujumbura, GMT+02:00", + value: "Africa/Bujumbura", + }, + { + label: "Africa/Cairo, GMT+03:00", + value: "Africa/Cairo", + }, + { + label: "Africa/Casablanca, GMT+01:00", + value: "Africa/Casablanca", + }, + { + label: "Africa/Ceuta, GMT+02:00", + value: "Africa/Ceuta", + }, + { + label: "Africa/Conakry, GMT", + value: "Africa/Conakry", + }, + { + label: "Africa/Dakar, GMT", + value: "Africa/Dakar", + }, + { + label: "Africa/Dar_es_Salaam, GMT+03:00", + value: "Africa/Dar_es_Salaam", + }, + { + label: "Africa/Djibouti, GMT+03:00", + value: "Africa/Djibouti", + }, + { + label: "Africa/Douala, GMT+01:00", + value: "Africa/Douala", + }, + { + label: "Africa/El_Aaiun, GMT+01:00", + value: "Africa/El_Aaiun", + }, + { + label: "Africa/Freetown, GMT", + value: "Africa/Freetown", + }, + { + label: "Africa/Gaborone, GMT+02:00", + value: "Africa/Gaborone", + }, + { + label: "Africa/Harare, GMT+02:00", + value: "Africa/Harare", + }, + { + label: "Africa/Johannesburg, GMT+02:00", + value: "Africa/Johannesburg", + }, + { + label: "Africa/Juba, GMT+02:00", + value: "Africa/Juba", + }, + { + label: "Africa/Kampala, GMT+03:00", + value: "Africa/Kampala", + }, + { + label: "Africa/Khartoum, GMT+02:00", + value: "Africa/Khartoum", + }, + { + label: "Africa/Kigali, GMT+02:00", + value: "Africa/Kigali", + }, + { + label: "Africa/Kinshasa, GMT+01:00", + value: "Africa/Kinshasa", + }, + { + label: "Africa/Lagos, GMT+01:00", + value: "Africa/Lagos", + }, + { + label: "Africa/Libreville, GMT+01:00", + value: "Africa/Libreville", + }, + { + label: "Africa/Lome, GMT", + value: "Africa/Lome", + }, + { + label: "Africa/Luanda, GMT+01:00", + value: "Africa/Luanda", + }, + { + label: "Africa/Lubumbashi, GMT+02:00", + value: "Africa/Lubumbashi", + }, + { + label: "Africa/Lusaka, GMT+02:00", + value: "Africa/Lusaka", + }, + { + label: "Africa/Malabo, GMT+01:00", + value: "Africa/Malabo", + }, + { + label: "Africa/Maputo, GMT+02:00", + value: "Africa/Maputo", + }, + { + label: "Africa/Maseru, GMT+02:00", + value: "Africa/Maseru", + }, + { + label: "Africa/Mbabane, GMT+02:00", + value: "Africa/Mbabane", + }, + { + label: "Africa/Mogadishu, GMT+03:00", + value: "Africa/Mogadishu", + }, + { + label: "Africa/Monrovia, GMT", + value: "Africa/Monrovia", + }, + { + label: "Africa/Nairobi, GMT+03:00", + value: "Africa/Nairobi", + }, + { + label: "Africa/Ndjamena, GMT+01:00", + value: "Africa/Ndjamena", + }, + { + label: "Africa/Niamey, GMT+01:00", + value: "Africa/Niamey", + }, + { + label: "Africa/Nouakchott, GMT", + value: "Africa/Nouakchott", + }, + { + label: "Africa/Ouagadougou, GMT", + value: "Africa/Ouagadougou", + }, + { + label: "Africa/Porto-Novo, GMT+01:00", + value: "Africa/Porto-Novo", + }, + { + label: "Africa/Sao_Tome, GMT", + value: "Africa/Sao_Tome", + }, + { + label: "Africa/Timbuktu, GMT", + value: "Africa/Timbuktu", + }, + { + label: "Africa/Tripoli, GMT+02:00", + value: "Africa/Tripoli", + }, + { + label: "Africa/Tunis, GMT+01:00", + value: "Africa/Tunis", + }, + { + label: "Africa/Windhoek, GMT+02:00", + value: "Africa/Windhoek", + }, + { + label: "America/Adak, GMT-09:00", + value: "America/Adak", + }, + { + label: "America/Anchorage, GMT-08:00", + value: "America/Anchorage", + }, + { + label: "America/Anguilla, GMT-04:00", + value: "America/Anguilla", + }, + { + label: "America/Antigua, GMT-04:00", + value: "America/Antigua", + }, + { + label: "America/Araguaina, GMT-03:00", + value: "America/Araguaina", + }, + { + label: "America/Argentina/Buenos_Aires, GMT-03:00", + value: "America/Argentina/Buenos_Aires", + }, + { + label: "America/Argentina/Catamarca, GMT-03:00", + value: "America/Argentina/Catamarca", + }, + { + label: "America/Argentina/ComodRivadavia, GMT-03:00", + value: "America/Argentina/ComodRivadavia", + }, + { + label: "America/Argentina/Cordoba, GMT-03:00", + value: "America/Argentina/Cordoba", + }, + { + label: "America/Argentina/Jujuy, GMT-03:00", + value: "America/Argentina/Jujuy", + }, + { + label: "America/Argentina/La_Rioja, GMT-03:00", + value: "America/Argentina/La_Rioja", + }, + { + label: "America/Argentina/Mendoza, GMT-03:00", + value: "America/Argentina/Mendoza", + }, + { + label: "America/Argentina/Rio_Gallegos, GMT-03:00", + value: "America/Argentina/Rio_Gallegos", + }, + { + label: "America/Argentina/Salta, GMT-03:00", + value: "America/Argentina/Salta", + }, + { + label: "America/Argentina/San_Juan, GMT-03:00", + value: "America/Argentina/San_Juan", + }, + { + label: "America/Argentina/San_Luis, GMT-03:00", + value: "America/Argentina/San_Luis", + }, + { + label: "America/Argentina/Tucuman, GMT-03:00", + value: "America/Argentina/Tucuman", + }, + { + label: "America/Argentina/Ushuaia, GMT-03:00", + value: "America/Argentina/Ushuaia", + }, + { + label: "America/Aruba, GMT-04:00", + value: "America/Aruba", + }, + { + label: "America/Asuncion, GMT-04:00", + value: "America/Asuncion", + }, + { + label: "America/Atikokan, GMT-05:00", + value: "America/Atikokan", + }, + { + label: "America/Atka, GMT-09:00", + value: "America/Atka", + }, + { + label: "America/Bahia, GMT-03:00", + value: "America/Bahia", + }, + { + label: "America/Bahia_Banderas, GMT-06:00", + value: "America/Bahia_Banderas", + }, + { + label: "America/Barbados, GMT-04:00", + value: "America/Barbados", + }, + { + label: "America/Belem, GMT-03:00", + value: "America/Belem", + }, + { + label: "America/Belize, GMT-06:00", + value: "America/Belize", + }, + { + label: "America/Blanc-Sablon, GMT-04:00", + value: "America/Blanc-Sablon", + }, + { + label: "America/Boa_Vista, GMT-04:00", + value: "America/Boa_Vista", + }, + { + label: "America/Bogota, GMT-05:00", + value: "America/Bogota", + }, + { + label: "America/Boise, GMT-06:00", + value: "America/Boise", + }, + { + label: "America/Buenos_Aires, GMT-03:00", + value: "America/Buenos_Aires", + }, + { + label: "America/Cambridge_Bay, GMT-06:00", + value: "America/Cambridge_Bay", + }, + { + label: "America/Campo_Grande, GMT-04:00", + value: "America/Campo_Grande", + }, + { + label: "America/Cancun, GMT-05:00", + value: "America/Cancun", + }, + { + label: "America/Caracas, GMT-04:00", + value: "America/Caracas", + }, + { + label: "America/Catamarca, GMT-03:00", + value: "America/Catamarca", + }, + { + label: "America/Cayenne, GMT-03:00", + value: "America/Cayenne", + }, + { + label: "America/Cayman, GMT-05:00", + value: "America/Cayman", + }, + { + label: "America/Chicago, GMT-05:00", + value: "America/Chicago", + }, + { + label: "America/Chihuahua, GMT-06:00", + value: "America/Chihuahua", + }, + { + label: "America/Ciudad_Juarez, GMT-06:00", + value: "America/Ciudad_Juarez", + }, + { + label: "America/Coral_Harbour, GMT-05:00", + value: "America/Coral_Harbour", + }, + { + label: "America/Cordoba, GMT-03:00", + value: "America/Cordoba", + }, + { + label: "America/Costa_Rica, GMT-06:00", + value: "America/Costa_Rica", + }, + { + label: "America/Creston, GMT-07:00", + value: "America/Creston", + }, + { + label: "America/Cuiaba, GMT-04:00", + value: "America/Cuiaba", + }, + { + label: "America/Curacao, GMT-04:00", + value: "America/Curacao", + }, + { + label: "America/Danmarkshavn, GMT", + value: "America/Danmarkshavn", + }, + { + label: "America/Dawson, GMT-07:00", + value: "America/Dawson", + }, + { + label: "America/Dawson_Creek, GMT-07:00", + value: "America/Dawson_Creek", + }, + { + label: "America/Denver, GMT-06:00", + value: "America/Denver", + }, + { + label: "America/Detroit, GMT-04:00", + value: "America/Detroit", + }, + { + label: "America/Dominica, GMT-04:00", + value: "America/Dominica", + }, + { + label: "America/Edmonton, GMT-06:00", + value: "America/Edmonton", + }, + { + label: "America/Eirunepe, GMT-05:00", + value: "America/Eirunepe", + }, + { + label: "America/El_Salvador, GMT-06:00", + value: "America/El_Salvador", + }, + { + label: "America/Ensenada, GMT-07:00", + value: "America/Ensenada", + }, + { + label: "America/Fort_Nelson, GMT-07:00", + value: "America/Fort_Nelson", + }, + { + label: "America/Fort_Wayne, GMT-04:00", + value: "America/Fort_Wayne", + }, + { + label: "America/Fortaleza, GMT-03:00", + value: "America/Fortaleza", + }, + { + label: "America/Glace_Bay, GMT-03:00", + value: "America/Glace_Bay", + }, + { + label: "America/Godthab, GMT-02:00", + value: "America/Godthab", + }, + { + label: "America/Goose_Bay, GMT-03:00", + value: "America/Goose_Bay", + }, + { + label: "America/Grand_Turk, GMT-04:00", + value: "America/Grand_Turk", + }, + { + label: "America/Grenada, GMT-04:00", + value: "America/Grenada", + }, + { + label: "America/Guadeloupe, GMT-04:00", + value: "America/Guadeloupe", + }, + { + label: "America/Guatemala, GMT-06:00", + value: "America/Guatemala", + }, + { + label: "America/Guayaquil, GMT-05:00", + value: "America/Guayaquil", + }, + { + label: "America/Guyana, GMT-04:00", + value: "America/Guyana", + }, + { + label: "America/Halifax, GMT-03:00", + value: "America/Halifax", + }, + { + label: "America/Havana, GMT-04:00", + value: "America/Havana", + }, + { + label: "America/Hermosillo, GMT-07:00", + value: "America/Hermosillo", + }, + { + label: "America/Indiana/Indianapolis, GMT-04:00", + value: "America/Indiana/Indianapolis", + }, + { + label: "America/Indiana/Knox, GMT-05:00", + value: "America/Indiana/Knox", + }, + { + label: "America/Indiana/Marengo, GMT-04:00", + value: "America/Indiana/Marengo", + }, + { + label: "America/Indiana/Petersburg, GMT-04:00", + value: "America/Indiana/Petersburg", + }, + { + label: "America/Indiana/Tell_City, GMT-05:00", + value: "America/Indiana/Tell_City", + }, + { + label: "America/Indiana/Vevay, GMT-04:00", + value: "America/Indiana/Vevay", + }, + { + label: "America/Indiana/Vincennes, GMT-04:00", + value: "America/Indiana/Vincennes", + }, + { + label: "America/Indiana/Winamac, GMT-04:00", + value: "America/Indiana/Winamac", + }, + { + label: "America/Indianapolis, GMT-04:00", + value: "America/Indianapolis", + }, + { + label: "America/Inuvik, GMT-06:00", + value: "America/Inuvik", + }, + { + label: "America/Iqaluit, GMT-04:00", + value: "America/Iqaluit", + }, + { + label: "America/Jamaica, GMT-05:00", + value: "America/Jamaica", + }, + { + label: "America/Jujuy, GMT-03:00", + value: "America/Jujuy", + }, + { + label: "America/Juneau, GMT-08:00", + value: "America/Juneau", + }, + { + label: "America/Kentucky/Louisville, GMT-04:00", + value: "America/Kentucky/Louisville", + }, + { + label: "America/Kentucky/Monticello, GMT-04:00", + value: "America/Kentucky/Monticello", + }, + { + label: "America/Knox_IN, GMT-05:00", + value: "America/Knox_IN", + }, + { + label: "America/Kralendijk, GMT-04:00", + value: "America/Kralendijk", + }, + { + label: "America/La_Paz, GMT-04:00", + value: "America/La_Paz", + }, + { + label: "America/Lima, GMT-05:00", + value: "America/Lima", + }, + { + label: "America/Los_Angeles, GMT-07:00", + value: "America/Los_Angeles", + }, + { + label: "America/Louisville, GMT-04:00", + value: "America/Louisville", + }, + { + label: "America/Lower_Princes, GMT-04:00", + value: "America/Lower_Princes", + }, + { + label: "America/Maceio, GMT-03:00", + value: "America/Maceio", + }, + { + label: "America/Managua, GMT-06:00", + value: "America/Managua", + }, + { + label: "America/Manaus, GMT-04:00", + value: "America/Manaus", + }, + { + label: "America/Marigot, GMT-04:00", + value: "America/Marigot", + }, + { + label: "America/Martinique, GMT-04:00", + value: "America/Martinique", + }, + { + label: "America/Matamoros, GMT-05:00", + value: "America/Matamoros", + }, + { + label: "America/Mazatlan, GMT-07:00", + value: "America/Mazatlan", + }, + { + label: "America/Mendoza, GMT-03:00", + value: "America/Mendoza", + }, + { + label: "America/Menominee, GMT-05:00", + value: "America/Menominee", + }, + { + label: "America/Merida, GMT-06:00", + value: "America/Merida", + }, + { + label: "America/Metlakatla, GMT-08:00", + value: "America/Metlakatla", + }, + { + label: "America/Mexico_City, GMT-06:00", + value: "America/Mexico_City", + }, + { + label: "America/Miquelon, GMT-02:00", + value: "America/Miquelon", + }, + { + label: "America/Moncton, GMT-03:00", + value: "America/Moncton", + }, + { + label: "America/Monterrey, GMT-06:00", + value: "America/Monterrey", + }, + { + label: "America/Montevideo, GMT-03:00", + value: "America/Montevideo", + }, + { + label: "America/Montreal, GMT-04:00", + value: "America/Montreal", + }, + { + label: "America/Montserrat, GMT-04:00", + value: "America/Montserrat", + }, + { + label: "America/Nassau, GMT-04:00", + value: "America/Nassau", + }, + { + label: "America/New_York, GMT-04:00", + value: "America/New_York", + }, + { + label: "America/Nipigon, GMT-04:00", + value: "America/Nipigon", + }, + { + label: "America/Nome, GMT-08:00", + value: "America/Nome", + }, + { + label: "America/Noronha, GMT-02:00", + value: "America/Noronha", + }, + { + label: "America/North_Dakota/Beulah, GMT-05:00", + value: "America/North_Dakota/Beulah", + }, + { + label: "America/North_Dakota/Center, GMT-05:00", + value: "America/North_Dakota/Center", + }, + { + label: "America/North_Dakota/New_Salem, GMT-05:00", + value: "America/North_Dakota/New_Salem", + }, + { + label: "America/Nuuk, GMT-02:00", + value: "America/Nuuk", + }, + { + label: "America/Ojinaga, GMT-05:00", + value: "America/Ojinaga", + }, + { + label: "America/Panama, GMT-05:00", + value: "America/Panama", + }, + { + label: "America/Pangnirtung, GMT-04:00", + value: "America/Pangnirtung", + }, + { + label: "America/Paramaribo, GMT-03:00", + value: "America/Paramaribo", + }, + { + label: "America/Phoenix, GMT-07:00", + value: "America/Phoenix", + }, + { + label: "America/Port-au-Prince, GMT-04:00", + value: "America/Port-au-Prince", + }, + { + label: "America/Port_of_Spain, GMT-04:00", + value: "America/Port_of_Spain", + }, + { + label: "America/Porto_Acre, GMT-05:00", + value: "America/Porto_Acre", + }, + { + label: "America/Porto_Velho, GMT-04:00", + value: "America/Porto_Velho", + }, + { + label: "America/Puerto_Rico, GMT-04:00", + value: "America/Puerto_Rico", + }, + { + label: "America/Punta_Arenas, GMT-03:00", + value: "America/Punta_Arenas", + }, + { + label: "America/Rainy_River, GMT-05:00", + value: "America/Rainy_River", + }, + { + label: "America/Rankin_Inlet, GMT-05:00", + value: "America/Rankin_Inlet", + }, + { + label: "America/Recife, GMT-03:00", + value: "America/Recife", + }, + { + label: "America/Regina, GMT-06:00", + value: "America/Regina", + }, + { + label: "America/Resolute, GMT-05:00", + value: "America/Resolute", + }, + { + label: "America/Rio_Branco, GMT-05:00", + value: "America/Rio_Branco", + }, + { + label: "America/Rosario, GMT-03:00", + value: "America/Rosario", + }, + { + label: "America/Santa_Isabel, GMT-07:00", + value: "America/Santa_Isabel", + }, + { + label: "America/Santarem, GMT-03:00", + value: "America/Santarem", + }, + { + label: "America/Santiago, GMT-04:00", + value: "America/Santiago", + }, + { + label: "America/Santo_Domingo, GMT-04:00", + value: "America/Santo_Domingo", + }, + { + label: "America/Sao_Paulo, GMT-03:00", + value: "America/Sao_Paulo", + }, + { + label: "America/Scoresbysund, GMT", + value: "America/Scoresbysund", + }, + { + label: "America/Shiprock, GMT-06:00", + value: "America/Shiprock", + }, + { + label: "America/Sitka, GMT-08:00", + value: "America/Sitka", + }, + { + label: "America/St_Barthelemy, GMT-04:00", + value: "America/St_Barthelemy", + }, + { + label: "America/St_Johns, GMT-02:30", + value: "America/St_Johns", + }, + { + label: "America/St_Kitts, GMT-04:00", + value: "America/St_Kitts", + }, + { + label: "America/St_Lucia, GMT-04:00", + value: "America/St_Lucia", + }, + { + label: "America/St_Thomas, GMT-04:00", + value: "America/St_Thomas", + }, + { + label: "America/St_Vincent, GMT-04:00", + value: "America/St_Vincent", + }, + { + label: "America/Swift_Current, GMT-06:00", + value: "America/Swift_Current", + }, + { + label: "America/Tegucigalpa, GMT-06:00", + value: "America/Tegucigalpa", + }, + { + label: "America/Thule, GMT-03:00", + value: "America/Thule", + }, + { + label: "America/Thunder_Bay, GMT-04:00", + value: "America/Thunder_Bay", + }, + { + label: "America/Tijuana, GMT-07:00", + value: "America/Tijuana", + }, + { + label: "America/Toronto, GMT-04:00", + value: "America/Toronto", + }, + { + label: "America/Tortola, GMT-04:00", + value: "America/Tortola", + }, + { + label: "America/Vancouver, GMT-07:00", + value: "America/Vancouver", + }, + { + label: "America/Virgin, GMT-04:00", + value: "America/Virgin", + }, + { + label: "America/Whitehorse, GMT-07:00", + value: "America/Whitehorse", + }, + { + label: "America/Winnipeg, GMT-05:00", + value: "America/Winnipeg", + }, + { + label: "America/Yakutat, GMT-08:00", + value: "America/Yakutat", + }, + { + label: "America/Yellowknife, GMT-06:00", + value: "America/Yellowknife", + }, + { + label: "Antarctica/Casey, GMT+11:00", + value: "Antarctica/Casey", + }, + { + label: "Antarctica/Davis, GMT+07:00", + value: "Antarctica/Davis", + }, + { + label: "Antarctica/DumontDUrville, GMT+10:00", + value: "Antarctica/DumontDUrville", + }, + { + label: "Antarctica/Macquarie, GMT+10:00", + value: "Antarctica/Macquarie", + }, + { + label: "Antarctica/Mawson, GMT+05:00", + value: "Antarctica/Mawson", + }, + { + label: "Antarctica/McMurdo, GMT+12:00", + value: "Antarctica/McMurdo", + }, + { + label: "Antarctica/Palmer, GMT-03:00", + value: "Antarctica/Palmer", + }, + { + label: "Antarctica/Rothera, GMT-03:00", + value: "Antarctica/Rothera", + }, + { + label: "Antarctica/South_Pole, GMT+12:00", + value: "Antarctica/South_Pole", + }, + { + label: "Antarctica/Syowa, GMT+03:00", + value: "Antarctica/Syowa", + }, + { + label: "Antarctica/Troll, GMT+02:00", + value: "Antarctica/Troll", + }, + { + label: "Antarctica/Vostok, GMT+06:00", + value: "Antarctica/Vostok", + }, + { + label: "Arctic/Longyearbyen, GMT+02:00", + value: "Arctic/Longyearbyen", + }, + { + label: "Asia/Aden, GMT+03:00", + value: "Asia/Aden", + }, + { + label: "Asia/Almaty, GMT+06:00", + value: "Asia/Almaty", + }, + { + label: "Asia/Amman, GMT+03:00", + value: "Asia/Amman", + }, + { + label: "Asia/Anadyr, GMT+12:00", + value: "Asia/Anadyr", + }, + { + label: "Asia/Aqtau, GMT+05:00", + value: "Asia/Aqtau", + }, + { + label: "Asia/Aqtobe, GMT+05:00", + value: "Asia/Aqtobe", + }, + { + label: "Asia/Ashgabat, GMT+05:00", + value: "Asia/Ashgabat", + }, + { + label: "Asia/Ashkhabad, GMT+05:00", + value: "Asia/Ashkhabad", + }, + { + label: "Asia/Atyrau, GMT+05:00", + value: "Asia/Atyrau", + }, + { + label: "Asia/Baghdad, GMT+03:00", + value: "Asia/Baghdad", + }, + { + label: "Asia/Bahrain, GMT+03:00", + value: "Asia/Bahrain", + }, + { + label: "Asia/Baku, GMT+04:00", + value: "Asia/Baku", + }, + { + label: "Asia/Bangkok, GMT+07:00", + value: "Asia/Bangkok", + }, + { + label: "Asia/Barnaul, GMT+07:00", + value: "Asia/Barnaul", + }, + { + label: "Asia/Beirut, GMT+03:00", + value: "Asia/Beirut", + }, + { + label: "Asia/Bishkek, GMT+06:00", + value: "Asia/Bishkek", + }, + { + label: "Asia/Brunei, GMT+08:00", + value: "Asia/Brunei", + }, + { + label: "Asia/Calcutta, GMT+05:30", + value: "Asia/Calcutta", + }, + { + label: "Asia/Chita, GMT+09:00", + value: "Asia/Chita", + }, + { + label: "Asia/Choibalsan, GMT+08:00", + value: "Asia/Choibalsan", + }, + { + label: "Asia/Chongqing, GMT+08:00", + value: "Asia/Chongqing", + }, + { + label: "Asia/Chungking, GMT+08:00", + value: "Asia/Chungking", + }, + { + label: "Asia/Colombo, GMT+05:30", + value: "Asia/Colombo", + }, + { + label: "Asia/Dacca, GMT+06:00", + value: "Asia/Dacca", + }, + { + label: "Asia/Damascus, GMT+03:00", + value: "Asia/Damascus", + }, + { + label: "Asia/Dhaka, GMT+06:00", + value: "Asia/Dhaka", + }, + { + label: "Asia/Dili, GMT+09:00", + value: "Asia/Dili", + }, + { + label: "Asia/Dubai, GMT+04:00", + value: "Asia/Dubai", + }, + { + label: "Asia/Dushanbe, GMT+05:00", + value: "Asia/Dushanbe", + }, + { + label: "Asia/Famagusta, GMT+03:00", + value: "Asia/Famagusta", + }, + { + label: "Asia/Gaza, GMT+03:00", + value: "Asia/Gaza", + }, + { + label: "Asia/Harbin, GMT+08:00", + value: "Asia/Harbin", + }, + { + label: "Asia/Hebron, GMT+03:00", + value: "Asia/Hebron", + }, + { + label: "Asia/Ho_Chi_Minh, GMT+07:00", + value: "Asia/Ho_Chi_Minh", + }, + { + label: "Asia/Hong_Kong, GMT+08:00", + value: "Asia/Hong_Kong", + }, + { + label: "Asia/Hovd, GMT+07:00", + value: "Asia/Hovd", + }, + { + label: "Asia/Irkutsk, GMT+08:00", + value: "Asia/Irkutsk", + }, + { + label: "Asia/Istanbul, GMT+03:00", + value: "Asia/Istanbul", + }, + { + label: "Asia/Jakarta, GMT+07:00", + value: "Asia/Jakarta", + }, + { + label: "Asia/Jayapura, GMT+09:00", + value: "Asia/Jayapura", + }, + { + label: "Asia/Jerusalem, GMT+03:00", + value: "Asia/Jerusalem", + }, + { + label: "Asia/Kabul, GMT+04:30", + value: "Asia/Kabul", + }, + { + label: "Asia/Kamchatka, GMT+12:00", + value: "Asia/Kamchatka", + }, + { + label: "Asia/Karachi, GMT+05:00", + value: "Asia/Karachi", + }, + { + label: "Asia/Kashgar, GMT+06:00", + value: "Asia/Kashgar", + }, + { + label: "Asia/Kathmandu, GMT+05:45", + value: "Asia/Kathmandu", + }, + { + label: "Asia/Katmandu, GMT+05:45", + value: "Asia/Katmandu", + }, + { + label: "Asia/Khandyga, GMT+09:00", + value: "Asia/Khandyga", + }, + { + label: "Asia/Kolkata, GMT+05:30", + value: "Asia/Kolkata", + }, + { + label: "Asia/Krasnoyarsk, GMT+07:00", + value: "Asia/Krasnoyarsk", + }, + { + label: "Asia/Kuala_Lumpur, GMT+08:00", + value: "Asia/Kuala_Lumpur", + }, + { + label: "Asia/Kuching, GMT+08:00", + value: "Asia/Kuching", + }, + { + label: "Asia/Kuwait, GMT+03:00", + value: "Asia/Kuwait", + }, + { + label: "Asia/Macao, GMT+08:00", + value: "Asia/Macao", + }, + { + label: "Asia/Macau, GMT+08:00", + value: "Asia/Macau", + }, + { + label: "Asia/Magadan, GMT+11:00", + value: "Asia/Magadan", + }, + { + label: "Asia/Makassar, GMT+08:00", + value: "Asia/Makassar", + }, + { + label: "Asia/Manila, GMT+08:00", + value: "Asia/Manila", + }, + { + label: "Asia/Muscat, GMT+04:00", + value: "Asia/Muscat", + }, + { + label: "Asia/Nicosia, GMT+03:00", + value: "Asia/Nicosia", + }, + { + label: "Asia/Novokuznetsk, GMT+07:00", + value: "Asia/Novokuznetsk", + }, + { + label: "Asia/Novosibirsk, GMT+07:00", + value: "Asia/Novosibirsk", + }, + { + label: "Asia/Omsk, GMT+06:00", + value: "Asia/Omsk", + }, + { + label: "Asia/Oral, GMT+05:00", + value: "Asia/Oral", + }, + { + label: "Asia/Phnom_Penh, GMT+07:00", + value: "Asia/Phnom_Penh", + }, + { + label: "Asia/Pontianak, GMT+07:00", + value: "Asia/Pontianak", + }, + { + label: "Asia/Pyongyang, GMT+09:00", + value: "Asia/Pyongyang", + }, + { + label: "Asia/Qatar, GMT+03:00", + value: "Asia/Qatar", + }, + { + label: "Asia/Qostanay, GMT+06:00", + value: "Asia/Qostanay", + }, + { + label: "Asia/Qyzylorda, GMT+05:00", + value: "Asia/Qyzylorda", + }, + { + label: "Asia/Rangoon, GMT+06:30", + value: "Asia/Rangoon", + }, + { + label: "Asia/Riyadh, GMT+03:00", + value: "Asia/Riyadh", + }, + { + label: "Asia/Saigon, GMT+07:00", + value: "Asia/Saigon", + }, + { + label: "Asia/Sakhalin, GMT+11:00", + value: "Asia/Sakhalin", + }, + { + label: "Asia/Samarkand, GMT+05:00", + value: "Asia/Samarkand", + }, + { + label: "Asia/Seoul, GMT+09:00", + value: "Asia/Seoul", + }, + { + label: "Asia/Shanghai, GMT+08:00", + value: "Asia/Shanghai", + }, + { + label: "Asia/Singapore, GMT+08:00", + value: "Asia/Singapore", + }, + { + label: "Asia/Srednekolymsk, GMT+11:00", + value: "Asia/Srednekolymsk", + }, + { + label: "Asia/Taipei, GMT+08:00", + value: "Asia/Taipei", + }, + { + label: "Asia/Tashkent, GMT+05:00", + value: "Asia/Tashkent", + }, + { + label: "Asia/Tbilisi, GMT+04:00", + value: "Asia/Tbilisi", + }, + { + label: "Asia/Tehran, GMT+03:30", + value: "Asia/Tehran", + }, + { + label: "Asia/Tel_Aviv, GMT+03:00", + value: "Asia/Tel_Aviv", + }, + { + label: "Asia/Thimbu, GMT+06:00", + value: "Asia/Thimbu", + }, + { + label: "Asia/Thimphu, GMT+06:00", + value: "Asia/Thimphu", + }, + { + label: "Asia/Tokyo, GMT+09:00", + value: "Asia/Tokyo", + }, + { + label: "Asia/Tomsk, GMT+07:00", + value: "Asia/Tomsk", + }, + { + label: "Asia/Ujung_Pandang, GMT+08:00", + value: "Asia/Ujung_Pandang", + }, + { + label: "Asia/Ulaanbaatar, GMT+08:00", + value: "Asia/Ulaanbaatar", + }, + { + label: "Asia/Ulan_Bator, GMT+08:00", + value: "Asia/Ulan_Bator", + }, + { + label: "Asia/Urumqi, GMT+06:00", + value: "Asia/Urumqi", + }, + { + label: "Asia/Ust-Nera, GMT+10:00", + value: "Asia/Ust-Nera", + }, + { + label: "Asia/Vientiane, GMT+07:00", + value: "Asia/Vientiane", + }, + { + label: "Asia/Vladivostok, GMT+10:00", + value: "Asia/Vladivostok", + }, + { + label: "Asia/Yakutsk, GMT+09:00", + value: "Asia/Yakutsk", + }, + { + label: "Asia/Yangon, GMT+06:30", + value: "Asia/Yangon", + }, + { + label: "Asia/Yekaterinburg, GMT+05:00", + value: "Asia/Yekaterinburg", + }, + { + label: "Asia/Yerevan, GMT+04:00", + value: "Asia/Yerevan", + }, + { + label: "Atlantic/Azores, GMT", + value: "Atlantic/Azores", + }, + { + label: "Atlantic/Bermuda, GMT-03:00", + value: "Atlantic/Bermuda", + }, + { + label: "Atlantic/Canary, GMT+01:00", + value: "Atlantic/Canary", + }, + { + label: "Atlantic/Cape_Verde, GMT-01:00", + value: "Atlantic/Cape_Verde", + }, + { + label: "Atlantic/Faeroe, GMT+01:00", + value: "Atlantic/Faeroe", + }, + { + label: "Atlantic/Faroe, GMT+01:00", + value: "Atlantic/Faroe", + }, + { + label: "Atlantic/Jan_Mayen, GMT+02:00", + value: "Atlantic/Jan_Mayen", + }, + { + label: "Atlantic/Madeira, GMT+01:00", + value: "Atlantic/Madeira", + }, + { + label: "Atlantic/Reykjavik, GMT", + value: "Atlantic/Reykjavik", + }, + { + label: "Atlantic/South_Georgia, GMT-02:00", + value: "Atlantic/South_Georgia", + }, + { + label: "Atlantic/St_Helena, GMT", + value: "Atlantic/St_Helena", + }, + { + label: "Atlantic/Stanley, GMT-03:00", + value: "Atlantic/Stanley", + }, + { + label: "Australia/ACT, GMT+10:00", + value: "Australia/ACT", + }, + { + label: "Australia/Adelaide, GMT+09:30", + value: "Australia/Adelaide", + }, + { + label: "Australia/Brisbane, GMT+10:00", + value: "Australia/Brisbane", + }, + { + label: "Australia/Broken_Hill, GMT+09:30", + value: "Australia/Broken_Hill", + }, + { + label: "Australia/Canberra, GMT+10:00", + value: "Australia/Canberra", + }, + { + label: "Australia/Currie, GMT+10:00", + value: "Australia/Currie", + }, + { + label: "Australia/Darwin, GMT+09:30", + value: "Australia/Darwin", + }, + { + label: "Australia/Eucla, GMT+08:45", + value: "Australia/Eucla", + }, + { + label: "Australia/Hobart, GMT+10:00", + value: "Australia/Hobart", + }, + { + label: "Australia/LHI, GMT+10:30", + value: "Australia/LHI", + }, + { + label: "Australia/Lindeman, GMT+10:00", + value: "Australia/Lindeman", + }, + { + label: "Australia/Lord_Howe, GMT+10:30", + value: "Australia/Lord_Howe", + }, + { + label: "Australia/Melbourne, GMT+10:00", + value: "Australia/Melbourne", + }, + { + label: "Australia/NSW, GMT+10:00", + value: "Australia/NSW", + }, + { + label: "Australia/North, GMT+09:30", + value: "Australia/North", + }, + { + label: "Australia/Perth, GMT+08:00", + value: "Australia/Perth", + }, + { + label: "Australia/Queensland, GMT+10:00", + value: "Australia/Queensland", + }, + { + label: "Australia/South, GMT+09:30", + value: "Australia/South", + }, + { + label: "Australia/Sydney, GMT+10:00", + value: "Australia/Sydney", + }, + { + label: "Australia/Tasmania, GMT+10:00", + value: "Australia/Tasmania", + }, + { + label: "Australia/Victoria, GMT+10:00", + value: "Australia/Victoria", + }, + { + label: "Australia/West, GMT+08:00", + value: "Australia/West", + }, + { + label: "Australia/Yancowinna, GMT+09:30", + value: "Australia/Yancowinna", + }, + { + label: "Brazil/Acre, GMT-05:00", + value: "Brazil/Acre", + }, + { + label: "Brazil/DeNoronha, GMT-02:00", + value: "Brazil/DeNoronha", + }, + { + label: "Brazil/East, GMT-03:00", + value: "Brazil/East", + }, + { + label: "Brazil/West, GMT-04:00", + value: "Brazil/West", + }, + { + label: "CET, GMT+02:00", + value: "CET", + }, + { + label: "CST6CDT, GMT-05:00", + value: "CST6CDT", + }, + { + label: "Canada/Atlantic, GMT-03:00", + value: "Canada/Atlantic", + }, + { + label: "Canada/Central, GMT-05:00", + value: "Canada/Central", + }, + { + label: "Canada/Eastern, GMT-04:00", + value: "Canada/Eastern", + }, + { + label: "Canada/Mountain, GMT-06:00", + value: "Canada/Mountain", + }, + { + label: "Canada/Newfoundland, GMT-02:30", + value: "Canada/Newfoundland", + }, + { + label: "Canada/Pacific, GMT-07:00", + value: "Canada/Pacific", + }, + { + label: "Canada/Saskatchewan, GMT-06:00", + value: "Canada/Saskatchewan", + }, + { + label: "Canada/Yukon, GMT-07:00", + value: "Canada/Yukon", + }, + { + label: "Chile/Continental, GMT-04:00", + value: "Chile/Continental", + }, + { + label: "Chile/EasterIsland, GMT-06:00", + value: "Chile/EasterIsland", + }, + { + label: "Cuba, GMT-04:00", + value: "Cuba", + }, + { + label: "EET, GMT+03:00", + value: "EET", + }, + { + label: "EST, GMT-05:00", + value: "EST", + }, + { + label: "EST5EDT, GMT-04:00", + value: "EST5EDT", + }, + { + label: "Egypt, GMT+03:00", + value: "Egypt", + }, + { + label: "Eire, GMT+01:00", + value: "Eire", + }, + { + label: "Etc/GMT, GMT", + value: "Etc/GMT", + }, + { + label: "Etc/GMT+0, GMT", + value: "Etc/GMT+0", + }, + { + label: "Etc/GMT+1, GMT-01:00", + value: "Etc/GMT+1", + }, + { + label: "Etc/GMT+10, GMT-10:00", + value: "Etc/GMT+10", + }, + { + label: "Etc/GMT+11, GMT-11:00", + value: "Etc/GMT+11", + }, + { + label: "Etc/GMT+12, GMT-12:00", + value: "Etc/GMT+12", + }, + { + label: "Etc/GMT+2, GMT-02:00", + value: "Etc/GMT+2", + }, + { + label: "Etc/GMT+3, GMT-03:00", + value: "Etc/GMT+3", + }, + { + label: "Etc/GMT+4, GMT-04:00", + value: "Etc/GMT+4", + }, + { + label: "Etc/GMT+5, GMT-05:00", + value: "Etc/GMT+5", + }, + { + label: "Etc/GMT+6, GMT-06:00", + value: "Etc/GMT+6", + }, + { + label: "Etc/GMT+7, GMT-07:00", + value: "Etc/GMT+7", + }, + { + label: "Etc/GMT+8, GMT-08:00", + value: "Etc/GMT+8", + }, + { + label: "Etc/GMT+9, GMT-09:00", + value: "Etc/GMT+9", + }, + { + label: "Etc/GMT-0, GMT", + value: "Etc/GMT-0", + }, + { + label: "Etc/GMT-1, GMT+01:00", + value: "Etc/GMT-1", + }, + { + label: "Etc/GMT-10, GMT+10:00", + value: "Etc/GMT-10", + }, + { + label: "Etc/GMT-11, GMT+11:00", + value: "Etc/GMT-11", + }, + { + label: "Etc/GMT-12, GMT+12:00", + value: "Etc/GMT-12", + }, + { + label: "Etc/GMT-13, GMT+13:00", + value: "Etc/GMT-13", + }, + { + label: "Etc/GMT-14, GMT+14:00", + value: "Etc/GMT-14", + }, + { + label: "Etc/GMT-2, GMT+02:00", + value: "Etc/GMT-2", + }, + { + label: "Etc/GMT-3, GMT+03:00", + value: "Etc/GMT-3", + }, + { + label: "Etc/GMT-4, GMT+04:00", + value: "Etc/GMT-4", + }, + { + label: "Etc/GMT-5, GMT+05:00", + value: "Etc/GMT-5", + }, + { + label: "Etc/GMT-6, GMT+06:00", + value: "Etc/GMT-6", + }, + { + label: "Etc/GMT-7, GMT+07:00", + value: "Etc/GMT-7", + }, + { + label: "Etc/GMT-8, GMT+08:00", + value: "Etc/GMT-8", + }, + { + label: "Etc/GMT-9, GMT+09:00", + value: "Etc/GMT-9", + }, + { + label: "Etc/GMT0, GMT", + value: "Etc/GMT0", + }, + { + label: "Etc/Greenwich, GMT", + value: "Etc/Greenwich", + }, + { + label: "Etc/UCT, GMT", + value: "Etc/UCT", + }, + { + label: "Etc/UTC, GMT", + value: "Etc/UTC", + }, + { + label: "Etc/Universal, GMT", + value: "Etc/Universal", + }, + { + label: "Etc/Zulu, GMT", + value: "Etc/Zulu", + }, + { + label: "Europe/Amsterdam, GMT+02:00", + value: "Europe/Amsterdam", + }, + { + label: "Europe/Andorra, GMT+02:00", + value: "Europe/Andorra", + }, + { + label: "Europe/Astrakhan, GMT+04:00", + value: "Europe/Astrakhan", + }, + { + label: "Europe/Athens, GMT+03:00", + value: "Europe/Athens", + }, + { + label: "Europe/Belfast, GMT+01:00", + value: "Europe/Belfast", + }, + { + label: "Europe/Belgrade, GMT+02:00", + value: "Europe/Belgrade", + }, + { + label: "Europe/Berlin, GMT+02:00", + value: "Europe/Berlin", + }, + { + label: "Europe/Bratislava, GMT+02:00", + value: "Europe/Bratislava", + }, + { + label: "Europe/Brussels, GMT+02:00", + value: "Europe/Brussels", + }, + { + label: "Europe/Bucharest, GMT+03:00", + value: "Europe/Bucharest", + }, + { + label: "Europe/Budapest, GMT+02:00", + value: "Europe/Budapest", + }, + { + label: "Europe/Busingen, GMT+02:00", + value: "Europe/Busingen", + }, + { + label: "Europe/Chisinau, GMT+03:00", + value: "Europe/Chisinau", + }, + { + label: "Europe/Copenhagen, GMT+02:00", + value: "Europe/Copenhagen", + }, + { + label: "Europe/Dublin, GMT+01:00", + value: "Europe/Dublin", + }, + { + label: "Europe/Gibraltar, GMT+02:00", + value: "Europe/Gibraltar", + }, + { + label: "Europe/Guernsey, GMT+01:00", + value: "Europe/Guernsey", + }, + { + label: "Europe/Helsinki, GMT+03:00", + value: "Europe/Helsinki", + }, + { + label: "Europe/Isle_of_Man, GMT+01:00", + value: "Europe/Isle_of_Man", + }, + { + label: "Europe/Istanbul, GMT+03:00", + value: "Europe/Istanbul", + }, + { + label: "Europe/Jersey, GMT+01:00", + value: "Europe/Jersey", + }, + { + label: "Europe/Kaliningrad, GMT+02:00", + value: "Europe/Kaliningrad", + }, + { + label: "Europe/Kiev, GMT+03:00", + value: "Europe/Kiev", + }, + { + label: "Europe/Kirov, GMT+03:00", + value: "Europe/Kirov", + }, + { + label: "Europe/Kyiv, GMT+03:00", + value: "Europe/Kyiv", + }, + { + label: "Europe/Lisbon, GMT+01:00", + value: "Europe/Lisbon", + }, + { + label: "Europe/Ljubljana, GMT+02:00", + value: "Europe/Ljubljana", + }, + { + label: "Europe/London, GMT+01:00", + value: "Europe/London", + }, + { + label: "Europe/Luxembourg, GMT+02:00", + value: "Europe/Luxembourg", + }, + { + label: "Europe/Madrid, GMT+02:00", + value: "Europe/Madrid", + }, + { + label: "Europe/Malta, GMT+02:00", + value: "Europe/Malta", + }, + { + label: "Europe/Mariehamn, GMT+03:00", + value: "Europe/Mariehamn", + }, + { + label: "Europe/Minsk, GMT+03:00", + value: "Europe/Minsk", + }, + { + label: "Europe/Monaco, GMT+02:00", + value: "Europe/Monaco", + }, + { + label: "Europe/Moscow, GMT+03:00", + value: "Europe/Moscow", + }, + { + label: "Europe/Nicosia, GMT+03:00", + value: "Europe/Nicosia", + }, + { + label: "Europe/Oslo, GMT+02:00", + value: "Europe/Oslo", + }, + { + label: "Europe/Paris, GMT+02:00", + value: "Europe/Paris", + }, + { + label: "Europe/Podgorica, GMT+02:00", + value: "Europe/Podgorica", + }, + { + label: "Europe/Prague, GMT+02:00", + value: "Europe/Prague", + }, + { + label: "Europe/Riga, GMT+03:00", + value: "Europe/Riga", + }, + { + label: "Europe/Rome, GMT+02:00", + value: "Europe/Rome", + }, + { + label: "Europe/Samara, GMT+04:00", + value: "Europe/Samara", + }, + { + label: "Europe/San_Marino, GMT+02:00", + value: "Europe/San_Marino", + }, + { + label: "Europe/Sarajevo, GMT+02:00", + value: "Europe/Sarajevo", + }, + { + label: "Europe/Saratov, GMT+04:00", + value: "Europe/Saratov", + }, + { + label: "Europe/Simferopol, GMT+03:00", + value: "Europe/Simferopol", + }, + { + label: "Europe/Skopje, GMT+02:00", + value: "Europe/Skopje", + }, + { + label: "Europe/Sofia, GMT+03:00", + value: "Europe/Sofia", + }, + { + label: "Europe/Stockholm, GMT+02:00", + value: "Europe/Stockholm", + }, + { + label: "Europe/Tallinn, GMT+03:00", + value: "Europe/Tallinn", + }, + { + label: "Europe/Tirane, GMT+02:00", + value: "Europe/Tirane", + }, + { + label: "Europe/Tiraspol, GMT+03:00", + value: "Europe/Tiraspol", + }, + { + label: "Europe/Ulyanovsk, GMT+04:00", + value: "Europe/Ulyanovsk", + }, + { + label: "Europe/Uzhgorod, GMT+03:00", + value: "Europe/Uzhgorod", + }, + { + label: "Europe/Vaduz, GMT+02:00", + value: "Europe/Vaduz", + }, + { + label: "Europe/Vatican, GMT+02:00", + value: "Europe/Vatican", + }, + { + label: "Europe/Vienna, GMT+02:00", + value: "Europe/Vienna", + }, + { + label: "Europe/Vilnius, GMT+03:00", + value: "Europe/Vilnius", + }, + { + label: "Europe/Volgograd, GMT+03:00", + value: "Europe/Volgograd", + }, + { + label: "Europe/Warsaw, GMT+02:00", + value: "Europe/Warsaw", + }, + { + label: "Europe/Zagreb, GMT+02:00", + value: "Europe/Zagreb", + }, + { + label: "Europe/Zaporozhye, GMT+03:00", + value: "Europe/Zaporozhye", + }, + { + label: "Europe/Zurich, GMT+02:00", + value: "Europe/Zurich", + }, + { + label: "GB, GMT+01:00", + value: "GB", + }, + { + label: "GB-Eire, GMT+01:00", + value: "GB-Eire", + }, + { + label: "GMT, GMT", + value: "GMT", + }, + { + label: "GMT+0, GMT", + value: "GMT+0", + }, + { + label: "GMT-0, GMT", + value: "GMT-0", + }, + { + label: "GMT0, GMT", + value: "GMT0", + }, + { + label: "Greenwich, GMT", + value: "Greenwich", + }, + { + label: "HST, GMT-10:00", + value: "HST", + }, + { + label: "Hongkong, GMT+08:00", + value: "Hongkong", + }, + { + label: "Iceland, GMT", + value: "Iceland", + }, + { + label: "Indian/Antananarivo, GMT+03:00", + value: "Indian/Antananarivo", + }, + { + label: "Indian/Chagos, GMT+06:00", + value: "Indian/Chagos", + }, + { + label: "Indian/Christmas, GMT+07:00", + value: "Indian/Christmas", + }, + { + label: "Indian/Cocos, GMT+06:30", + value: "Indian/Cocos", + }, + { + label: "Indian/Comoro, GMT+03:00", + value: "Indian/Comoro", + }, + { + label: "Indian/Kerguelen, GMT+05:00", + value: "Indian/Kerguelen", + }, + { + label: "Indian/Mahe, GMT+04:00", + value: "Indian/Mahe", + }, + { + label: "Indian/Maldives, GMT+05:00", + value: "Indian/Maldives", + }, + { + label: "Indian/Mauritius, GMT+04:00", + value: "Indian/Mauritius", + }, + { + label: "Indian/Mayotte, GMT+03:00", + value: "Indian/Mayotte", + }, + { + label: "Indian/Reunion, GMT+04:00", + value: "Indian/Reunion", + }, + { + label: "Iran, GMT+03:30", + value: "Iran", + }, + { + label: "Israel, GMT+03:00", + value: "Israel", + }, + { + label: "Jamaica, GMT-05:00", + value: "Jamaica", + }, + { + label: "Japan, GMT+09:00", + value: "Japan", + }, + { + label: "Kwajalein, GMT+12:00", + value: "Kwajalein", + }, + { + label: "Libya, GMT+02:00", + value: "Libya", + }, + { + label: "MET, GMT+02:00", + value: "MET", + }, + { + label: "MST, GMT-07:00", + value: "MST", + }, + { + label: "MST7MDT, GMT-06:00", + value: "MST7MDT", + }, + { + label: "Mexico/BajaNorte, GMT-07:00", + value: "Mexico/BajaNorte", + }, + { + label: "Mexico/BajaSur, GMT-07:00", + value: "Mexico/BajaSur", + }, + { + label: "Mexico/General, GMT-06:00", + value: "Mexico/General", + }, + { + label: "NZ, GMT+12:00", + value: "NZ", + }, + { + label: "NZ-CHAT, GMT+12:45", + value: "NZ-CHAT", + }, + { + label: "Navajo, GMT-06:00", + value: "Navajo", + }, + { + label: "PRC, GMT+08:00", + value: "PRC", + }, + { + label: "PST8PDT, GMT-07:00", + value: "PST8PDT", + }, + { + label: "Pacific/Apia, GMT+13:00", + value: "Pacific/Apia", + }, + { + label: "Pacific/Auckland, GMT+12:00", + value: "Pacific/Auckland", + }, + { + label: "Pacific/Bougainville, GMT+11:00", + value: "Pacific/Bougainville", + }, + { + label: "Pacific/Chatham, GMT+12:45", + value: "Pacific/Chatham", + }, + { + label: "Pacific/Chuuk, GMT+10:00", + value: "Pacific/Chuuk", + }, + { + label: "Pacific/Easter, GMT-06:00", + value: "Pacific/Easter", + }, + { + label: "Pacific/Efate, GMT+11:00", + value: "Pacific/Efate", + }, + { + label: "Pacific/Enderbury, GMT+13:00", + value: "Pacific/Enderbury", + }, + { + label: "Pacific/Fakaofo, GMT+13:00", + value: "Pacific/Fakaofo", + }, + { + label: "Pacific/Fiji, GMT+12:00", + value: "Pacific/Fiji", + }, + { + label: "Pacific/Funafuti, GMT+12:00", + value: "Pacific/Funafuti", + }, + { + label: "Pacific/Galapagos, GMT-06:00", + value: "Pacific/Galapagos", + }, + { + label: "Pacific/Gambier, GMT-09:00", + value: "Pacific/Gambier", + }, + { + label: "Pacific/Guadalcanal, GMT+11:00", + value: "Pacific/Guadalcanal", + }, + { + label: "Pacific/Guam, GMT+10:00", + value: "Pacific/Guam", + }, + { + label: "Pacific/Honolulu, GMT-10:00", + value: "Pacific/Honolulu", + }, + { + label: "Pacific/Johnston, GMT-10:00", + value: "Pacific/Johnston", + }, + { + label: "Pacific/Kanton, GMT+13:00", + value: "Pacific/Kanton", + }, + { + label: "Pacific/Kiritimati, GMT+14:00", + value: "Pacific/Kiritimati", + }, + { + label: "Pacific/Kosrae, GMT+11:00", + value: "Pacific/Kosrae", + }, + { + label: "Pacific/Kwajalein, GMT+12:00", + value: "Pacific/Kwajalein", + }, + { + label: "Pacific/Majuro, GMT+12:00", + value: "Pacific/Majuro", + }, + { + label: "Pacific/Marquesas, GMT-09:30", + value: "Pacific/Marquesas", + }, + { + label: "Pacific/Midway, GMT-11:00", + value: "Pacific/Midway", + }, + { + label: "Pacific/Nauru, GMT+12:00", + value: "Pacific/Nauru", + }, + { + label: "Pacific/Niue, GMT-11:00", + value: "Pacific/Niue", + }, + { + label: "Pacific/Norfolk, GMT+11:00", + value: "Pacific/Norfolk", + }, + { + label: "Pacific/Noumea, GMT+11:00", + value: "Pacific/Noumea", + }, + { + label: "Pacific/Pago_Pago, GMT-11:00", + value: "Pacific/Pago_Pago", + }, + { + label: "Pacific/Palau, GMT+09:00", + value: "Pacific/Palau", + }, + { + label: "Pacific/Pitcairn, GMT-08:00", + value: "Pacific/Pitcairn", + }, + { + label: "Pacific/Pohnpei, GMT+11:00", + value: "Pacific/Pohnpei", + }, + { + label: "Pacific/Ponape, GMT+11:00", + value: "Pacific/Ponape", + }, + { + label: "Pacific/Port_Moresby, GMT+10:00", + value: "Pacific/Port_Moresby", + }, + { + label: "Pacific/Rarotonga, GMT-10:00", + value: "Pacific/Rarotonga", + }, + { + label: "Pacific/Saipan, GMT+10:00", + value: "Pacific/Saipan", + }, + { + label: "Pacific/Samoa, GMT-11:00", + value: "Pacific/Samoa", + }, + { + label: "Pacific/Tahiti, GMT-10:00", + value: "Pacific/Tahiti", + }, + { + label: "Pacific/Tarawa, GMT+12:00", + value: "Pacific/Tarawa", + }, + { + label: "Pacific/Tongatapu, GMT+13:00", + value: "Pacific/Tongatapu", + }, + { + label: "Pacific/Truk, GMT+10:00", + value: "Pacific/Truk", + }, + { + label: "Pacific/Wake, GMT+12:00", + value: "Pacific/Wake", + }, + { + label: "Pacific/Wallis, GMT+12:00", + value: "Pacific/Wallis", + }, + { + label: "Pacific/Yap, GMT+10:00", + value: "Pacific/Yap", + }, + { + label: "Poland, GMT+02:00", + value: "Poland", + }, + { + label: "Portugal, GMT+01:00", + value: "Portugal", + }, + { + label: "ROC, GMT+08:00", + value: "ROC", + }, + { + label: "ROK, GMT+09:00", + value: "ROK", + }, + { + label: "Singapore, GMT+08:00", + value: "Singapore", + }, + { + label: "Turkey, GMT+03:00", + value: "Turkey", + }, + { + label: "UCT, GMT", + value: "UCT", + }, + { + label: "US/Alaska, GMT-08:00", + value: "US/Alaska", + }, + { + label: "US/Aleutian, GMT-09:00", + value: "US/Aleutian", + }, + { + label: "US/Arizona, GMT-07:00", + value: "US/Arizona", + }, + { + label: "US/Central, GMT-05:00", + value: "US/Central", + }, + { + label: "US/East-Indiana, GMT-04:00", + value: "US/East-Indiana", + }, + { + label: "US/Eastern, GMT-04:00", + value: "US/Eastern", + }, + { + label: "US/Hawaii, GMT-10:00", + value: "US/Hawaii", + }, + { + label: "US/Indiana-Starke, GMT-05:00", + value: "US/Indiana-Starke", + }, + { + label: "US/Michigan, GMT-04:00", + value: "US/Michigan", + }, + { + label: "US/Mountain, GMT-06:00", + value: "US/Mountain", + }, + { + label: "US/Pacific, GMT-07:00", + value: "US/Pacific", + }, + { + label: "US/Samoa, GMT-11:00", + value: "US/Samoa", + }, + { + label: "UTC, GMT", + value: "UTC", + }, + { + label: "Universal, GMT", + value: "Universal", + }, + { + label: "W-SU, GMT+03:00", + value: "W-SU", + }, + { + label: "WET, GMT+01:00", + value: "WET", + }, + { + label: "Zulu, GMT", + value: "Zulu", + }, +]; diff --git a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx index 4ea92d49d..6c0af1bc2 100644 --- a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx +++ b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx @@ -14,7 +14,14 @@ import SettingsNavbar from "layouts/settings-navbar"; // components import { ImagePickerPopover, ImageUploadModal } from "components/core"; // ui -import { CustomSelect, DangerButton, Input, SecondaryButton, Spinner } from "components/ui"; +import { + CustomSearchSelect, + CustomSelect, + DangerButton, + Input, + SecondaryButton, + Spinner, +} from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons import { UserIcon } from "@heroicons/react/24/outline"; @@ -23,6 +30,7 @@ import type { NextPage } from "next"; import type { IUser } from "types"; // constants import { USER_ROLES } from "constants/workspace"; +import { TIME_ZONES } from "constants/timezones"; const defaultValues: Partial = { avatar: "", @@ -31,6 +39,7 @@ const defaultValues: Partial = { last_name: "", email: "", role: "Product / Project Manager", + user_timezone: "Asia/Kolkata", }; const Profile: NextPage = () => { @@ -72,6 +81,7 @@ const Profile: NextPage = () => { cover_image: formData.cover_image, role: formData.role, display_name: formData.display_name, + user_timezone: formData.user_timezone, }; await userService @@ -128,6 +138,12 @@ const Profile: NextPage = () => { }); }; + const timeZoneOptions = TIME_ZONES.map((timeZone) => ({ + value: timeZone.value, + query: timeZone.label + " " + timeZone.value, + content: timeZone.label, + })); + return ( { {errors.role && Please select a role} +
+
+

Timezone

+

Select a timezone

+
+
+ ( + t.value === value)?.label ?? value + : "Select a timezone" + } + options={timeZoneOptions} + onChange={onChange} + verticalPosition="top" + optionsClassName="w-full" + input + /> + )} + /> + {errors.role && Please select a role} +
+
{isSubmitting ? "Updating..." : "Update profile"} diff --git a/apps/app/types/users.d.ts b/apps/app/types/users.d.ts index b41d1f8ef..55c95e5f3 100644 --- a/apps/app/types/users.d.ts +++ b/apps/app/types/users.d.ts @@ -36,6 +36,7 @@ export interface IUser { theme: ICustomTheme; updated_at: readonly Date; username: string; + user_timezone: string; [...rest: string]: any; } From fd0efb0242403af5c8132f47572496babf719a09 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 29 Aug 2023 20:11:38 +0530 Subject: [PATCH 012/137] fix: start date filter not working on the platform (#2007) --- apps/app/hooks/gantt-chart/cycle-issues-view.tsx | 1 + apps/app/hooks/gantt-chart/issue-view.tsx | 1 + apps/app/hooks/gantt-chart/module-issues-view.tsx | 1 + apps/app/hooks/use-calendar-issues-view.tsx | 1 + apps/app/hooks/use-spreadsheet-issues-view.tsx | 2 ++ 5 files changed, 6 insertions(+) diff --git a/apps/app/hooks/gantt-chart/cycle-issues-view.tsx b/apps/app/hooks/gantt-chart/cycle-issues-view.tsx index 7ef534fb4..8d32d5e4b 100644 --- a/apps/app/hooks/gantt-chart/cycle-issues-view.tsx +++ b/apps/app/hooks/gantt-chart/cycle-issues-view.tsx @@ -23,6 +23,7 @@ const useGanttChartCycleIssues = ( priority: filters?.priority ? filters?.priority.join(",") : undefined, labels: filters?.labels ? filters?.labels.join(",") : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, + start_date: filters?.start_date ? filters?.start_date.join(",") : undefined, target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, start_target_date: true, // to fetch only issues with a start and target date }; diff --git a/apps/app/hooks/gantt-chart/issue-view.tsx b/apps/app/hooks/gantt-chart/issue-view.tsx index 7e595a358..ba9465114 100644 --- a/apps/app/hooks/gantt-chart/issue-view.tsx +++ b/apps/app/hooks/gantt-chart/issue-view.tsx @@ -19,6 +19,7 @@ const useGanttChartIssues = (workspaceSlug: string | undefined, projectId: strin priority: filters?.priority ? filters?.priority.join(",") : undefined, labels: filters?.labels ? filters?.labels.join(",") : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, + start_date: filters?.start_date ? filters?.start_date.join(",") : undefined, target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, start_target_date: true, // to fetch only issues with a start and target date }; diff --git a/apps/app/hooks/gantt-chart/module-issues-view.tsx b/apps/app/hooks/gantt-chart/module-issues-view.tsx index 54dea3e2e..3d88cab4f 100644 --- a/apps/app/hooks/gantt-chart/module-issues-view.tsx +++ b/apps/app/hooks/gantt-chart/module-issues-view.tsx @@ -23,6 +23,7 @@ const useGanttChartModuleIssues = ( priority: filters?.priority ? filters?.priority.join(",") : undefined, labels: filters?.labels ? filters?.labels.join(",") : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, + start_date: filters?.start_date ? filters?.start_date.join(",") : undefined, target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, start_target_date: true, // to fetch only issues with a start and target date }; diff --git a/apps/app/hooks/use-calendar-issues-view.tsx b/apps/app/hooks/use-calendar-issues-view.tsx index d8daae922..d69864c2d 100644 --- a/apps/app/hooks/use-calendar-issues-view.tsx +++ b/apps/app/hooks/use-calendar-issues-view.tsx @@ -42,6 +42,7 @@ const useCalendarIssuesView = () => { type: filters?.type ? filters?.type : undefined, labels: filters?.labels ? filters?.labels.join(",") : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, + start_date: filters?.start_date ? filters?.start_date.join(",") : undefined, target_date: calendarDateRange, }; diff --git a/apps/app/hooks/use-spreadsheet-issues-view.tsx b/apps/app/hooks/use-spreadsheet-issues-view.tsx index 797ddf7d6..4471b4352 100644 --- a/apps/app/hooks/use-spreadsheet-issues-view.tsx +++ b/apps/app/hooks/use-spreadsheet-issues-view.tsx @@ -43,6 +43,8 @@ const useSpreadsheetIssuesView = () => { type: filters?.type ? filters?.type : undefined, labels: filters?.labels ? filters?.labels.join(",") : undefined, created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, + start_date: filters?.start_date ? filters?.start_date.join(",") : undefined, + target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, sub_issue: "false", }; From d8bbdc14ac0c6996541e92fbcbe3d4ebba7e2496 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 29 Aug 2023 20:14:13 +0530 Subject: [PATCH 013/137] feat: access selector for comment (#2012) * dev: access specifier for comment * chore: change access order --- .../components/issues/comment/add-comment.tsx | 112 ++++++++++++------ apps/app/components/issues/main-content.tsx | 9 +- apps/app/services/issues.service.ts | 14 +-- apps/app/types/issues.d.ts | 1 + apps/app/types/projects.d.ts | 2 +- 5 files changed, 93 insertions(+), 45 deletions(-) diff --git a/apps/app/components/issues/comment/add-comment.tsx b/apps/app/components/issues/comment/add-comment.tsx index 4d64a90ba..33d7f2289 100644 --- a/apps/app/components/issues/comment/add-comment.tsx +++ b/apps/app/components/issues/comment/add-comment.tsx @@ -3,38 +3,55 @@ import { useRouter } from "next/router"; // react-hook-form import { useForm, Controller } from "react-hook-form"; // components -import { SecondaryButton } from "components/ui"; import { TipTapEditor } from "components/tiptap"; +// ui +import { Icon, SecondaryButton, Tooltip } from "components/ui"; // types import type { IIssueComment } from "types"; const defaultValues: Partial = { - comment_json: "", + access: "INTERNAL", comment_html: "", }; type Props = { disabled?: boolean; onSubmit: (data: IIssueComment) => Promise; + showAccessSpecifier?: boolean; }; -export const AddComment: React.FC = ({ disabled = false, onSubmit }) => { - const { - control, - formState: { isSubmitting }, - handleSubmit, - reset, - setValue, - watch, - } = useForm({ defaultValues }); +const commentAccess = [ + { + icon: "lock", + key: "INTERNAL", + label: "Private", + }, + { + icon: "public", + key: "EXTERNAL", + label: "Public", + }, +]; +export const AddComment: React.FC = ({ + disabled = false, + onSubmit, + showAccessSpecifier = false, +}) => { const editorRef = React.useRef(null); const router = useRouter(); const { workspaceSlug } = router.query; + const { + control, + formState: { isSubmitting }, + handleSubmit, + reset, + } = useForm({ defaultValues }); + const handleAddComment = async (formData: IIssueComment) => { - if (!formData.comment_html || !formData.comment_json || isSubmitting) return; + if (!formData.comment_html || isSubmitting) return; await onSubmit(formData).then(() => { reset(defaultValues); @@ -45,30 +62,55 @@ export const AddComment: React.FC = ({ disabled = false, onSubmit }) => { return (
-
- ( - { - onChange(comment_html); - setValue("comment_json", comment_json); - }} - /> +
+
+ {showAccessSpecifier && ( +
+ ( +
+ {commentAccess.map((access) => ( + + + + ))} +
+ )} + /> +
)} - /> + ( +

" : value} + customClassName="p-3 min-h-[100px] shadow-sm" + debouncedUpdatesEnabled={false} + onChange={(comment_json: Object, comment_html: string) => onChange(comment_html)} + /> + )} + /> +
{isSubmitting ? "Adding..." : "Comment"} diff --git a/apps/app/components/issues/main-content.tsx b/apps/app/components/issues/main-content.tsx index ed559beb7..bab384523 100644 --- a/apps/app/components/issues/main-content.tsx +++ b/apps/app/components/issues/main-content.tsx @@ -8,6 +8,7 @@ import issuesService from "services/issues.service"; // hooks import useUserAuth from "hooks/use-user-auth"; import useToast from "hooks/use-toast"; +import useProjectDetails from "hooks/use-project-details"; // contexts import { useProjectMyMembership } from "contexts/project-member.context"; // components @@ -49,6 +50,8 @@ export const IssueMainContent: React.FC = ({ const { user } = useUserAuth(); const { memberRole } = useProjectMyMembership(); + const { projectDetails } = useProjectDetails(); + const { data: siblingIssues } = useSWR( workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null, workspaceSlug && projectId && issueDetails?.parent @@ -220,7 +223,11 @@ export const IssueMainContent: React.FC = ({ handleCommentUpdate={handleCommentUpdate} handleCommentDelete={handleCommentDelete} /> - +
); diff --git a/apps/app/services/issues.service.ts b/apps/app/services/issues.service.ts index 53c6c1a2d..b8875e6c5 100644 --- a/apps/app/services/issues.service.ts +++ b/apps/app/services/issues.service.ts @@ -182,7 +182,7 @@ class ProjectIssuesServices extends APIService { workspaceSlug: string, projectId: string, issueId: string, - data: any, + data: Partial, user: ICurrentUserResponse | undefined ): Promise { return this.post( @@ -468,20 +468,18 @@ class ProjectIssuesServices extends APIService { metadata: any; title: string; url: string; - }, - + } ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/${linkId}/`, data ) - .then((response) => response?.data) - .catch((error) => { - throw error?.response; - }); + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); } - async deleteIssueLink( workspaceSlug: string, projectId: string, diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts index 98d72041a..93e1598f3 100644 --- a/apps/app/types/issues.d.ts +++ b/apps/app/types/issues.d.ts @@ -198,6 +198,7 @@ export interface IIssueActivity { } export interface IIssueComment extends IIssueActivity { + access: "EXTERNAL" | "INTERNAL"; comment_html: string; comment_json: any; comment_stripped: string; diff --git a/apps/app/types/projects.d.ts b/apps/app/types/projects.d.ts index a3d8b997a..78ef4d953 100644 --- a/apps/app/types/projects.d.ts +++ b/apps/app/types/projects.d.ts @@ -39,6 +39,7 @@ export interface IProject { } | null; id: string; identifier: string; + is_deployed: boolean; is_favorite: boolean; is_member: boolean; member_role: 5 | 10 | 15 | 20 | null; @@ -57,7 +58,6 @@ export interface IProject { updated_by: string; workspace: IWorkspace | string; workspace_detail: IWorkspaceLite; - is_deployed: boolean; } export interface IProjectLite { From 168e79d6df2c896ef29a98c2361cd4dc691d64c8 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 29 Aug 2023 20:15:12 +0530 Subject: [PATCH 014/137] style: revamp of the issue details sidebar (#2014) --- .../issues/sidebar-select/assignee.tsx | 4 +- .../issues/sidebar-select/blocked.tsx | 12 +- .../issues/sidebar-select/blocker.tsx | 12 +- .../issues/sidebar-select/cycle.tsx | 107 +++++++-------- .../issues/sidebar-select/estimate.tsx | 4 +- .../issues/sidebar-select/module.tsx | 116 ++++++++-------- .../issues/sidebar-select/parent.tsx | 68 ++++------ .../issues/sidebar-select/priority.tsx | 2 +- .../issues/sidebar-select/state.tsx | 2 +- apps/app/components/issues/sidebar.tsx | 126 +++++++++++------- 10 files changed, 219 insertions(+), 234 deletions(-) diff --git a/apps/app/components/issues/sidebar-select/assignee.tsx b/apps/app/components/issues/sidebar-select/assignee.tsx index ad7db744f..61ece6f78 100644 --- a/apps/app/components/issues/sidebar-select/assignee.tsx +++ b/apps/app/components/issues/sidebar-select/assignee.tsx @@ -48,10 +48,10 @@ export const SidebarAssigneeSelect: React.FC = ({ value, onChange, disabl {value && value.length > 0 && Array.isArray(value) ? (
- {value.length} Assignees + {value.length} Assignees
) : ( - )} diff --git a/apps/app/components/issues/sidebar-select/blocked.tsx b/apps/app/components/issues/sidebar-select/blocked.tsx index 76373700c..fbe58b4b2 100644 --- a/apps/app/components/issues/sidebar-select/blocked.tsx +++ b/apps/app/components/issues/sidebar-select/blocked.tsx @@ -18,7 +18,6 @@ type Props = { issueId?: string; submitChanges: (formData: Partial) => void; watch: UseFormWatch; - userAuth: UserAuth; disabled?: boolean; }; @@ -26,7 +25,6 @@ export const SidebarBlockedSelect: React.FC = ({ issueId, submitChanges, watch, - userAuth, disabled = false, }) => { const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false); @@ -73,8 +71,6 @@ export const SidebarBlockedSelect: React.FC = ({ handleClose(); }; - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled; - return ( <> = ({
diff --git a/apps/app/components/issues/sidebar-select/blocker.tsx b/apps/app/components/issues/sidebar-select/blocker.tsx index c25adc49e..49a6bc505 100644 --- a/apps/app/components/issues/sidebar-select/blocker.tsx +++ b/apps/app/components/issues/sidebar-select/blocker.tsx @@ -18,7 +18,6 @@ type Props = { issueId?: string; submitChanges: (formData: Partial) => void; watch: UseFormWatch; - userAuth: UserAuth; disabled?: boolean; }; @@ -26,7 +25,6 @@ export const SidebarBlockerSelect: React.FC = ({ issueId, submitChanges, watch, - userAuth, disabled = false, }) => { const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false); @@ -73,8 +71,6 @@ export const SidebarBlockerSelect: React.FC = ({ handleClose(); }; - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled; - return ( <> = ({
diff --git a/apps/app/components/issues/sidebar-select/cycle.tsx b/apps/app/components/issues/sidebar-select/cycle.tsx index 1eacba245..3451be2c3 100644 --- a/apps/app/components/issues/sidebar-select/cycle.tsx +++ b/apps/app/components/issues/sidebar-select/cycle.tsx @@ -11,24 +11,20 @@ import cyclesService from "services/cycles.service"; import { Spinner, CustomSelect, Tooltip } from "components/ui"; // helper import { truncateText } from "helpers/string.helper"; -// icons -import { ContrastIcon } from "components/icons"; // types -import { ICycle, IIssue, UserAuth } from "types"; +import { ICycle, IIssue } from "types"; // fetch-keys import { CYCLE_ISSUES, INCOMPLETE_CYCLES_LIST, ISSUE_DETAILS } from "constants/fetch-keys"; type Props = { issueDetail: IIssue | undefined; handleCycleChange: (cycle: ICycle) => void; - userAuth: UserAuth; disabled?: boolean; }; export const SidebarCycleSelect: React.FC = ({ issueDetail, handleCycleChange, - userAuth, disabled = false, }) => { const router = useRouter(); @@ -63,59 +59,56 @@ export const SidebarCycleSelect: React.FC = ({ const issueCycle = issueDetail?.issue_cycle; - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled; - return ( -
-
- -

Cycle

-
-
- - - - {issueCycle ? truncateText(issueCycle.cycle_detail.name, 15) : "No cycle"} - - - - } - value={issueCycle ? issueCycle.cycle_detail.id : null} - onChange={(value: any) => { - !value - ? removeIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "") - : handleCycleChange(incompleteCycles?.find((c) => c.id === value) as ICycle); - }} - width="w-full" - position="right" - maxHeight="rg" - disabled={isNotAllowed} + - {incompleteCycles ? ( - incompleteCycles.length > 0 ? ( - <> - {incompleteCycles.map((option) => ( - - - {truncateText(option.name, 25)} - - - ))} - None - - ) : ( -
No cycles found
- ) - ) : ( - - )} -
-
-
+ + + } + value={issueCycle ? issueCycle.cycle_detail.id : null} + onChange={(value: any) => { + !value + ? removeIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "") + : handleCycleChange(incompleteCycles?.find((c) => c.id === value) as ICycle); + }} + width="w-full" + position="right" + maxHeight="rg" + disabled={disabled} + > + {incompleteCycles ? ( + incompleteCycles.length > 0 ? ( + <> + {incompleteCycles.map((option) => ( + + + {truncateText(option.name, 25)} + + + ))} + None + + ) : ( +
No cycles found
+ ) + ) : ( + + )} + ); }; diff --git a/apps/app/components/issues/sidebar-select/estimate.tsx b/apps/app/components/issues/sidebar-select/estimate.tsx index 063694141..7ebdfe2b9 100644 --- a/apps/app/components/issues/sidebar-select/estimate.tsx +++ b/apps/app/components/issues/sidebar-select/estimate.tsx @@ -14,9 +14,7 @@ type Props = { }; export const SidebarEstimateSelect: React.FC = ({ value, onChange, disabled = false }) => { - const { isEstimateActive, estimatePoints } = useEstimateOption(); - - if (!isEstimateActive) return null; + const { estimatePoints } = useEstimateOption(); return ( void; - userAuth: UserAuth; disabled?: boolean; }; export const SidebarModuleSelect: React.FC = ({ issueDetail, handleModuleChange, - userAuth, disabled = false, }) => { const router = useRouter(); @@ -57,66 +53,60 @@ export const SidebarModuleSelect: React.FC = ({ const issueModule = issueDetail?.issue_module; - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled; - return ( -
-
- -

Module

-
-
- m.id === issueModule?.module)?.name ?? "No module" + m.id === issueModule?.module)?.name ?? "No module" + }`} + > +
-
+ {modules?.find((m) => m.id === issueModule?.module)?.name ?? "No module"} + + + + } + value={issueModule ? issueModule.module_detail?.id : null} + onChange={(value: any) => { + !value + ? removeIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "") + : handleModuleChange(modules?.find((m) => m.id === value) as IModule); + }} + width="w-full" + position="right" + maxHeight="rg" + disabled={disabled} + > + {modules ? ( + modules.length > 0 ? ( + <> + {modules.map((option) => ( + + + {truncateText(option.name, 25)} + + + ))} + None + + ) : ( +
No modules found
+ ) + ) : ( + + )} +
); }; diff --git a/apps/app/components/issues/sidebar-select/parent.tsx b/apps/app/components/issues/sidebar-select/parent.tsx index 1e780dd57..dd5d4f55b 100644 --- a/apps/app/components/issues/sidebar-select/parent.tsx +++ b/apps/app/components/issues/sidebar-select/parent.tsx @@ -2,8 +2,6 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -// icons -import { UserIcon } from "@heroicons/react/24/outline"; // components import { ParentIssuesListModal } from "components/issues"; // types @@ -12,14 +10,12 @@ import { IIssue, ISearchIssueResponse, UserAuth } from "types"; type Props = { onChange: (value: string) => void; issueDetails: IIssue | undefined; - userAuth: UserAuth; disabled?: boolean; }; export const SidebarParentSelect: React.FC = ({ onChange, issueDetails, - userAuth, disabled = false, }) => { const [isParentModalOpen, setIsParentModalOpen] = useState(false); @@ -28,42 +24,34 @@ export const SidebarParentSelect: React.FC = ({ const router = useRouter(); const { projectId, issueId } = router.query; - const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled; - return ( -
-
- -

Parent

-
-
- setIsParentModalOpen(false)} - onChange={(issue) => { - onChange(issue.id); - setSelectedParentIssue(issue); - }} - issueId={issueId as string} - projectId={projectId as string} - /> - -
-
+ <> + setIsParentModalOpen(false)} + onChange={(issue) => { + onChange(issue.id); + setSelectedParentIssue(issue); + }} + issueId={issueId as string} + projectId={projectId as string} + /> + + ); }; diff --git a/apps/app/components/issues/sidebar-select/priority.tsx b/apps/app/components/issues/sidebar-select/priority.tsx index dfaa712e2..fd1c77f28 100644 --- a/apps/app/components/issues/sidebar-select/priority.tsx +++ b/apps/app/components/issues/sidebar-select/priority.tsx @@ -18,7 +18,7 @@ export const SidebarPrioritySelect: React.FC = ({ value, onChange, disabl customButton={
)} - {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && + isEstimateActive && ( +
+
+ +

Estimate

+
+
+ ( + + submitChanges({ estimate_point: val }) + } + disabled={memberRole.isGuest || memberRole.isViewer || uneditable} + /> + )} + /> +
+
+ )} + + )} + {showSecondSection && ( +
+ {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
- -

Estimate

+ +

Parent

( - - submitChanges({ estimate_point: val }) - } + name="parent" + render={({ field: { onChange } }) => ( + { + submitChanges({ parent: val }); + onChange(val); + }} + issueDetails={issueDetail} disabled={memberRole.isGuest || memberRole.isViewer || uneditable} /> )} @@ -426,34 +461,12 @@ export const IssueDetailsSidebar: React.FC = ({
)} -
- )} - {showSecondSection && ( -
- {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( - ( - { - submitChanges({ parent: val }); - onChange(val); - }} - issueDetails={issueDetail} - userAuth={memberRole} - disabled={uneditable} - /> - )} - /> - )} {(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && ( )} {(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && ( @@ -461,8 +474,7 @@ export const IssueDetailsSidebar: React.FC = ({ issueId={issueId as string} submitChanges={submitChanges} watch={watchIssue} - userAuth={memberRole} - disabled={uneditable} + disabled={memberRole.isGuest || memberRole.isViewer || uneditable} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && ( @@ -484,8 +496,7 @@ export const IssueDetailsSidebar: React.FC = ({ start_date: val, }) } - className="bg-custom-background-100" - wrapperClassName="w-full" + className="bg-custom-background-80 border-none" maxDate={maxDate ?? undefined} disabled={isNotAllowed || uneditable} /> @@ -513,8 +524,7 @@ export const IssueDetailsSidebar: React.FC = ({ target_date: val, }) } - className="bg-custom-background-100" - wrapperClassName="w-full" + className="bg-custom-background-80 border-none" minDate={minDate ?? undefined} disabled={isNotAllowed || uneditable} /> @@ -528,20 +538,34 @@ export const IssueDetailsSidebar: React.FC = ({ {showThirdSection && (
{(fieldsToShow.includes("all") || fieldsToShow.includes("cycle")) && ( - +
+
+ +

Cycle

+
+
+ +
+
)} {(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && ( - +
+
+ +

Module

+
+
+ +
+
)}
)} From 2e5ade05fe4e6439cedf010062fb58ed227ca84f Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 30 Aug 2023 11:43:47 +0530 Subject: [PATCH 015/137] chore: update module status icons and colors (#2011) * chore: update module status icons and colors * refactor: import statements * fix: add default alue to module status --- apps/app/components/icons/index.ts | 1 + apps/app/components/icons/module/backlog.tsx | 57 +++++++++++++++ .../app/components/icons/module/cancelled.tsx | 35 +++++++++ .../app/components/icons/module/completed.tsx | 28 ++++++++ .../components/icons/module/in-progress.tsx | 71 +++++++++++++++++++ apps/app/components/icons/module/index.ts | 7 ++ .../icons/module/module-status-icon.tsx | 37 ++++++++++ apps/app/components/icons/module/paused.tsx | 31 ++++++++ apps/app/components/icons/module/planned.tsx | 24 +++++++ .../components/modules/gantt-chart/blocks.tsx | 3 + apps/app/components/modules/select/status.tsx | 15 +--- apps/app/components/modules/sidebar.tsx | 2 +- apps/app/constants/module.ts | 21 ++++-- apps/app/types/modules.d.ts | 10 ++- 14 files changed, 321 insertions(+), 21 deletions(-) create mode 100644 apps/app/components/icons/module/backlog.tsx create mode 100644 apps/app/components/icons/module/cancelled.tsx create mode 100644 apps/app/components/icons/module/completed.tsx create mode 100644 apps/app/components/icons/module/in-progress.tsx create mode 100644 apps/app/components/icons/module/index.ts create mode 100644 apps/app/components/icons/module/module-status-icon.tsx create mode 100644 apps/app/components/icons/module/paused.tsx create mode 100644 apps/app/components/icons/module/planned.tsx diff --git a/apps/app/components/icons/index.ts b/apps/app/components/icons/index.ts index 183b20c97..d3b311e40 100644 --- a/apps/app/components/icons/index.ts +++ b/apps/app/components/icons/index.ts @@ -27,6 +27,7 @@ export * from "./started-state-icon"; export * from "./layer-diagonal-icon"; export * from "./lock-icon"; export * from "./menu-icon"; +export * from "./module"; export * from "./pencil-scribble-icon"; export * from "./plus-icon"; export * from "./person-running-icon"; diff --git a/apps/app/components/icons/module/backlog.tsx b/apps/app/components/icons/module/backlog.tsx new file mode 100644 index 000000000..5685c7498 --- /dev/null +++ b/apps/app/components/icons/module/backlog.tsx @@ -0,0 +1,57 @@ +import React from "react"; + +type Props = { + width?: string; + height?: string; + className?: string; + color?: string; +}; + +export const ModuleBacklogIcon: React.FC = ({ width = "20", height = "20", className }) => ( + + + + + + + + + + + + + + + + +); diff --git a/apps/app/components/icons/module/cancelled.tsx b/apps/app/components/icons/module/cancelled.tsx new file mode 100644 index 000000000..9bfc02943 --- /dev/null +++ b/apps/app/components/icons/module/cancelled.tsx @@ -0,0 +1,35 @@ +import React from "react"; + +type Props = { + width?: string; + height?: string; + className?: string; + color?: string; +}; + +export const ModuleCancelledIcon: React.FC = ({ + width = "20", + height = "20", + className, +}) => ( + + + + + + + + + + +); diff --git a/apps/app/components/icons/module/completed.tsx b/apps/app/components/icons/module/completed.tsx new file mode 100644 index 000000000..4c50ed3ad --- /dev/null +++ b/apps/app/components/icons/module/completed.tsx @@ -0,0 +1,28 @@ +import React from "react"; + +type Props = { + width?: string; + height?: string; + className?: string; + color?: string; +}; + +export const ModuleCompletedIcon: React.FC = ({ + width = "20", + height = "20", + className, +}) => ( + + + +); diff --git a/apps/app/components/icons/module/in-progress.tsx b/apps/app/components/icons/module/in-progress.tsx new file mode 100644 index 000000000..5892a94d6 --- /dev/null +++ b/apps/app/components/icons/module/in-progress.tsx @@ -0,0 +1,71 @@ +import React from "react"; + +type Props = { + width?: string; + height?: string; + className?: string; + color?: string; +}; + +export const ModuleInProgressIcon: React.FC = ({ + width = "20", + height = "20", + className, +}) => ( + + + + + + + + + + + + + + + + + + + + +); diff --git a/apps/app/components/icons/module/index.ts b/apps/app/components/icons/module/index.ts new file mode 100644 index 000000000..e82014b2f --- /dev/null +++ b/apps/app/components/icons/module/index.ts @@ -0,0 +1,7 @@ +export * from "./backlog"; +export * from "./cancelled"; +export * from "./completed"; +export * from "./in-progress"; +export * from "./module-status-icon"; +export * from "./paused"; +export * from "./planned"; diff --git a/apps/app/components/icons/module/module-status-icon.tsx b/apps/app/components/icons/module/module-status-icon.tsx new file mode 100644 index 000000000..e80497773 --- /dev/null +++ b/apps/app/components/icons/module/module-status-icon.tsx @@ -0,0 +1,37 @@ +// icons +import { + ModuleBacklogIcon, + ModuleCancelledIcon, + ModuleCompletedIcon, + ModuleInProgressIcon, + ModulePausedIcon, + ModulePlannedIcon, +} from "components/icons"; +// types +import { TModuleStatus } from "types"; + +type Props = { + status: TModuleStatus; + className?: string; + height?: string; + width?: string; +}; + +export const ModuleStatusIcon: React.FC = ({ + status, + className, + height = "12px", + width = "12px", +}) => { + if (status === "backlog") + return ; + else if (status === "cancelled") + return ; + else if (status === "completed") + return ; + else if (status === "in-progress") + return ; + else if (status === "paused") + return ; + else return ; +}; diff --git a/apps/app/components/icons/module/paused.tsx b/apps/app/components/icons/module/paused.tsx new file mode 100644 index 000000000..56ebcfd98 --- /dev/null +++ b/apps/app/components/icons/module/paused.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +type Props = { + width?: string; + height?: string; + className?: string; + color?: string; +}; + +export const ModulePausedIcon: React.FC = ({ width = "20", height = "20", className }) => ( + + + + + + + + + + +); diff --git a/apps/app/components/icons/module/planned.tsx b/apps/app/components/icons/module/planned.tsx new file mode 100644 index 000000000..97592057c --- /dev/null +++ b/apps/app/components/icons/module/planned.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +type Props = { + width?: string; + height?: string; + className?: string; + color?: string; +}; + +export const ModulePlannedIcon: React.FC = ({ width = "20", height = "20", className }) => ( + + + +); diff --git a/apps/app/components/modules/gantt-chart/blocks.tsx b/apps/app/components/modules/gantt-chart/blocks.tsx index bcf307098..c6400ad82 100644 --- a/apps/app/components/modules/gantt-chart/blocks.tsx +++ b/apps/app/components/modules/gantt-chart/blocks.tsx @@ -2,6 +2,8 @@ import { useRouter } from "next/router"; // ui import { Tooltip } from "components/ui"; +// icons +import { ModuleStatusIcon } from "components/icons"; // helpers import { renderShortDate } from "helpers/date-time.helper"; // types @@ -49,6 +51,7 @@ export const ModuleGanttSidebarBlock = ({ data }: { data: IModule }) => { className="relative w-full flex items-center gap-2 h-full" onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/modules/${data.id}`)} > +
{data.name}
); diff --git a/apps/app/components/modules/select/status.tsx b/apps/app/components/modules/select/status.tsx index 08192cd5d..8c16ca14c 100644 --- a/apps/app/components/modules/select/status.tsx +++ b/apps/app/components/modules/select/status.tsx @@ -6,6 +6,7 @@ import { Controller, FieldError, Control } from "react-hook-form"; import { CustomSelect } from "components/ui"; // icons import { Squares2X2Icon } from "@heroicons/react/24/outline"; +import { ModuleStatusIcon } from "components/icons"; // types import type { IModule } from "types"; // constants @@ -31,12 +32,7 @@ export const ModuleStatusSelect: React.FC = ({ control, error }) => ( }`} > {value ? ( - s.value === value)?.color, - }} - /> + ) : ( = ({ control, error }) => ( {MODULE_STATUS.map((status) => (
- + {status.label}
diff --git a/apps/app/components/modules/sidebar.tsx b/apps/app/components/modules/sidebar.tsx index 0407b95aa..e815d1950 100644 --- a/apps/app/components/modules/sidebar.tsx +++ b/apps/app/components/modules/sidebar.tsx @@ -48,7 +48,7 @@ const defaultValues: Partial = { members_list: [], start_date: null, target_date: null, - status: null, + status: "backlog", }; type Props = { diff --git a/apps/app/constants/module.ts b/apps/app/constants/module.ts index ffacdfa3c..058171328 100644 --- a/apps/app/constants/module.ts +++ b/apps/app/constants/module.ts @@ -1,8 +1,15 @@ -export const MODULE_STATUS = [ - { label: "Backlog", value: "backlog", color: "#5e6ad2" }, - { label: "Planned", value: "planned", color: "#26b5ce" }, - { label: "In Progress", value: "in-progress", color: "#f2c94c" }, - { label: "Paused", value: "paused", color: "#ff6900" }, - { label: "Completed", value: "completed", color: "#4cb782" }, - { label: "Cancelled", value: "cancelled", color: "#cc1d10" }, +// types +import { TModuleStatus } from "types"; + +export const MODULE_STATUS: { + label: string; + value: TModuleStatus; + color: string; +}[] = [ + { label: "Backlog", value: "backlog", color: "#a3a3a2" }, + { label: "Planned", value: "planned", color: "#3f76ff" }, + { label: "In Progress", value: "in-progress", color: "#f39e1f" }, + { label: "Paused", value: "paused", color: "#525252" }, + { label: "Completed", value: "completed", color: "#16a34a" }, + { label: "Cancelled", value: "cancelled", color: "#ef4444" }, ]; diff --git a/apps/app/types/modules.d.ts b/apps/app/types/modules.d.ts index e395f6f16..709d1d300 100644 --- a/apps/app/types/modules.d.ts +++ b/apps/app/types/modules.d.ts @@ -10,6 +10,14 @@ import type { linkDetails, } from "types"; +export type TModuleStatus = + | "backlog" + | "planned" + | "in-progress" + | "paused" + | "completed" + | "cancelled"; + export interface IModule { backlog_issues: number; cancelled_issues: number; @@ -39,7 +47,7 @@ export interface IModule { sort_order: number; start_date: string | null; started_issues: number; - status: "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled" | null; + status: TModuleStatus; target_date: string | null; total_issues: number; unstarted_issues: number; From 23f5d5d1726b250937154802e028118b804c74a4 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 30 Aug 2023 12:15:08 +0530 Subject: [PATCH 016/137] chore: track public board comments and reaction users for public deploy boards (#1972) * chore: track project deploy board comment and reaction users for public deploy boards * dev: remove tracking from project viewsets --- apiserver/plane/api/serializers/__init__.py | 1 + apiserver/plane/api/serializers/project.py | 15 +++++++- apiserver/plane/api/views/issue.py | 39 ++++++++++++++++++++- apiserver/plane/db/models/__init__.py | 1 + apiserver/plane/db/models/project.py | 15 ++++++++ 5 files changed, 69 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 93b21a7f2..2dc910caf 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -20,6 +20,7 @@ from .project import ( ProjectMemberLiteSerializer, ProjectDeployBoardSerializer, ProjectMemberAdminSerializer, + ProjectPublicMemberSerializer ) from .state import StateSerializer, StateLiteSerializer from .view import IssueViewSerializer, IssueViewFavoriteSerializer diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index 55847881d..49d986cae 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -15,6 +15,7 @@ from plane.db.models import ( ProjectIdentifier, ProjectFavorite, ProjectDeployBoard, + ProjectPublicMember, ) @@ -177,5 +178,17 @@ class ProjectDeployBoardSerializer(BaseSerializer): fields = "__all__" read_only_fields = [ "workspace", - "project" "anchor", + "project", "anchor", + ] + + +class ProjectPublicMemberSerializer(BaseSerializer): + + class Meta: + model = ProjectPublicMember + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "member", ] diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 05434aec5..802431d2e 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -75,6 +75,7 @@ from plane.db.models import ( CommentReaction, ProjectDeployBoard, IssueVote, + ProjectPublicMember, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results @@ -1545,6 +1546,16 @@ class IssueCommentPublicViewSet(BaseViewSet): project_id=str(project_id), current_instance=None, ) + if not ProjectMember.objects.filter( + project_id=project_id, + member=request.user, + ).exists(): + # Add the user for workspace tracking + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except Exception as e: @@ -1671,6 +1682,15 @@ class IssueReactionPublicViewSet(BaseViewSet): serializer.save( project_id=project_id, issue_id=issue_id, actor=request.user ) + if not ProjectMember.objects.filter( + project_id=project_id, + member=request.user, + ).exists(): + # Add the user for workspace tracking + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, + ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except ProjectDeployBoard.DoesNotExist: @@ -1756,6 +1776,14 @@ class CommentReactionPublicViewSet(BaseViewSet): serializer.save( project_id=project_id, comment_id=comment_id, actor=request.user ) + if not ProjectMember.objects.filter( + project_id=project_id, member=request.user + ).exists(): + # Add the user for workspace tracking + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, + ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except ProjectDeployBoard.DoesNotExist: @@ -1823,6 +1851,14 @@ class IssueVotePublicViewSet(BaseViewSet): project_id=project_id, issue_id=issue_id, ) + # Add the user for workspace tracking + if not ProjectMember.objects.filter( + project_id=project_id, member=request.user + ).exists(): + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, + ) issue_vote.vote = request.data.get("vote", 1) issue_vote.save() serializer = IssueVoteSerializer(issue_vote) @@ -2021,4 +2057,5 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, - ) \ No newline at end of file + ) + diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 659eea3eb..90532dc64 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -19,6 +19,7 @@ from .project import ( ProjectIdentifier, ProjectFavorite, ProjectDeployBoard, + ProjectPublicMember, ) from .issue import ( diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index 0c2b5cb96..da155af40 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -254,3 +254,18 @@ class ProjectDeployBoard(ProjectBaseModel): def __str__(self): """Return project and anchor""" return f"{self.anchor} <{self.project.name}>" + + +class ProjectPublicMember(ProjectBaseModel): + member = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="public_project_members", + ) + + class Meta: + unique_together = ["project", "member"] + verbose_name = "Project Public Member" + verbose_name_plural = "Project Public Members" + db_table = "project_public_members" + ordering = ("-created_at",) From 426f65898b24856b6c775955167a7322f0c4adf6 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 30 Aug 2023 12:18:18 +0530 Subject: [PATCH 017/137] feat: user timezones (#2009) * dev: user timezones * feat: user timezones --- apiserver/plane/api/views/base.py | 27 +++++++++++--- ..._alter_analyticview_created_by_and_more.py | 35 +++++++++++++++++++ apiserver/plane/db/models/user.py | 7 ++-- apiserver/plane/settings/common.py | 4 +-- 4 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/api/views/base.py index 3c260e03b..60b0ec0c6 100644 --- a/apiserver/plane/api/views/base.py +++ b/apiserver/plane/api/views/base.py @@ -1,24 +1,41 @@ +# Python imports +import zoneinfo + # Django imports from django.urls import resolve from django.conf import settings - +from django.utils import timezone # Third part imports + from rest_framework import status from rest_framework.viewsets import ModelViewSet from rest_framework.exceptions import APIException from rest_framework.views import APIView from rest_framework.filters import SearchFilter from rest_framework.permissions import IsAuthenticated -from rest_framework.exceptions import NotFound from sentry_sdk import capture_exception from django_filters.rest_framework import DjangoFilterBackend # Module imports -from plane.db.models import Workspace, Project from plane.utils.paginator import BasePaginator -class BaseViewSet(ModelViewSet, BasePaginator): +class TimezoneMixin: + """ + This enables timezone conversion according + to the user set timezone + """ + def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) + if request.user.is_authenticated: + timezone.activate(zoneinfo.ZoneInfo(request.user.user_timezone)) + else: + timezone.deactivate() + + + + +class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): model = None @@ -67,7 +84,7 @@ class BaseViewSet(ModelViewSet, BasePaginator): return self.kwargs.get("pk", None) -class BaseAPIView(APIView, BasePaginator): +class BaseAPIView(TimezoneMixin, APIView, BasePaginator): permission_classes = [ IsAuthenticated, diff --git a/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py b/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py new file mode 100644 index 000000000..f7d6a979d --- /dev/null +++ b/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.3 on 2023-08-29 06:26 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +def update_user_timezones(apps, schema_editor): + UserModel = apps.get_model("db", "User") + updated_users = [] + for obj in UserModel.objects.all(): + obj.user_timezone = "UTC" + updated_users.append(obj) + UserModel.objects.bulk_update(updated_users, ["user_timezone"], batch_size=100) + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0041_cycle_sort_order_issuecomment_access_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='user_timezone', + field=models.CharField(choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Asmera', 'Africa/Asmera'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Timbuktu', 'Africa/Timbuktu'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'America/Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Atka', 'America/Atka'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Buenos_Aires', 'America/Buenos_Aires'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Catamarca', 'America/Catamarca'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Ciudad_Juarez', 'America/Ciudad_Juarez'), ('America/Coral_Harbour', 'America/Coral_Harbour'), ('America/Cordoba', 'America/Cordoba'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Ensenada', 'America/Ensenada'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fort_Wayne', 'America/Fort_Wayne'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Indianapolis', 'America/Indianapolis'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Jujuy', 'America/Jujuy'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Knox_IN', 'America/Knox_IN'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Louisville', 'America/Louisville'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Mendoza', 'America/Mendoza'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montreal', 'America/Montreal'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Nuuk', 'America/Nuuk'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Acre', 'America/Porto_Acre'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Rosario', 'America/Rosario'), ('America/Santa_Isabel', 'America/Santa_Isabel'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Shiprock', 'America/Shiprock'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Virgin', 'America/Virgin'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/South_Pole', 'Antarctica/South_Pole'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Ashkhabad', 'Asia/Ashkhabad'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Calcutta', 'Asia/Calcutta'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Choibalsan', 'Asia/Choibalsan'), ('Asia/Chongqing', 'Asia/Chongqing'), ('Asia/Chungking', 'Asia/Chungking'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Dacca', 'Asia/Dacca'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Harbin', 'Asia/Harbin'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Istanbul', 'Asia/Istanbul'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kashgar', 'Asia/Kashgar'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Katmandu', 'Asia/Katmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macao', 'Asia/Macao'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Rangoon', 'Asia/Rangoon'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Saigon', 'Asia/Saigon'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Tel_Aviv', 'Asia/Tel_Aviv'), ('Asia/Thimbu', 'Asia/Thimbu'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ujung_Pandang', 'Asia/Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Ulan_Bator', 'Asia/Ulan_Bator'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faeroe', 'Atlantic/Faeroe'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Jan_Mayen', 'Atlantic/Jan_Mayen'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/ACT', 'Australia/ACT'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Canberra', 'Australia/Canberra'), ('Australia/Currie', 'Australia/Currie'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/LHI', 'Australia/LHI'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/NSW', 'Australia/NSW'), ('Australia/North', 'Australia/North'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Queensland', 'Australia/Queensland'), ('Australia/South', 'Australia/South'), ('Australia/Sydney', 'Australia/Sydney'), ('Australia/Tasmania', 'Australia/Tasmania'), ('Australia/Victoria', 'Australia/Victoria'), ('Australia/West', 'Australia/West'), ('Australia/Yancowinna', 'Australia/Yancowinna'), ('Brazil/Acre', 'Brazil/Acre'), ('Brazil/DeNoronha', 'Brazil/DeNoronha'), ('Brazil/East', 'Brazil/East'), ('Brazil/West', 'Brazil/West'), ('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Canada/Atlantic', 'Canada/Atlantic'), ('Canada/Central', 'Canada/Central'), ('Canada/Eastern', 'Canada/Eastern'), ('Canada/Mountain', 'Canada/Mountain'), ('Canada/Newfoundland', 'Canada/Newfoundland'), ('Canada/Pacific', 'Canada/Pacific'), ('Canada/Saskatchewan', 'Canada/Saskatchewan'), ('Canada/Yukon', 'Canada/Yukon'), ('Chile/Continental', 'Chile/Continental'), ('Chile/EasterIsland', 'Chile/EasterIsland'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('Etc/GMT', 'Etc/GMT'), ('Etc/GMT+0', 'Etc/GMT+0'), ('Etc/GMT+1', 'Etc/GMT+1'), ('Etc/GMT+10', 'Etc/GMT+10'), ('Etc/GMT+11', 'Etc/GMT+11'), ('Etc/GMT+12', 'Etc/GMT+12'), ('Etc/GMT+2', 'Etc/GMT+2'), ('Etc/GMT+3', 'Etc/GMT+3'), ('Etc/GMT+4', 'Etc/GMT+4'), ('Etc/GMT+5', 'Etc/GMT+5'), ('Etc/GMT+6', 'Etc/GMT+6'), ('Etc/GMT+7', 'Etc/GMT+7'), ('Etc/GMT+8', 'Etc/GMT+8'), ('Etc/GMT+9', 'Etc/GMT+9'), ('Etc/GMT-0', 'Etc/GMT-0'), ('Etc/GMT-1', 'Etc/GMT-1'), ('Etc/GMT-10', 'Etc/GMT-10'), ('Etc/GMT-11', 'Etc/GMT-11'), ('Etc/GMT-12', 'Etc/GMT-12'), ('Etc/GMT-13', 'Etc/GMT-13'), ('Etc/GMT-14', 'Etc/GMT-14'), ('Etc/GMT-2', 'Etc/GMT-2'), ('Etc/GMT-3', 'Etc/GMT-3'), ('Etc/GMT-4', 'Etc/GMT-4'), ('Etc/GMT-5', 'Etc/GMT-5'), ('Etc/GMT-6', 'Etc/GMT-6'), ('Etc/GMT-7', 'Etc/GMT-7'), ('Etc/GMT-8', 'Etc/GMT-8'), ('Etc/GMT-9', 'Etc/GMT-9'), ('Etc/GMT0', 'Etc/GMT0'), ('Etc/Greenwich', 'Etc/Greenwich'), ('Etc/UCT', 'Etc/UCT'), ('Etc/UTC', 'Etc/UTC'), ('Etc/Universal', 'Etc/Universal'), ('Etc/Zulu', 'Etc/Zulu'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belfast', 'Europe/Belfast'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Kyiv', 'Europe/Kyiv'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Nicosia', 'Europe/Nicosia'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Tiraspol', 'Europe/Tiraspol'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Uzhgorod', 'Europe/Uzhgorod'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zaporozhye', 'Europe/Zaporozhye'), ('Europe/Zurich', 'Europe/Zurich'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('GMT', 'GMT'), ('GMT+0', 'GMT+0'), ('GMT-0', 'GMT-0'), ('GMT0', 'GMT0'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('Mexico/BajaNorte', 'Mexico/BajaNorte'), ('Mexico/BajaSur', 'Mexico/BajaSur'), ('Mexico/General', 'Mexico/General'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Johnston', 'Pacific/Johnston'), ('Pacific/Kanton', 'Pacific/Kanton'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Ponape', 'Pacific/Ponape'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Samoa', 'Pacific/Samoa'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Truk', 'Pacific/Truk'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('Pacific/Yap', 'Pacific/Yap'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('US/Alaska', 'US/Alaska'), ('US/Aleutian', 'US/Aleutian'), ('US/Arizona', 'US/Arizona'), ('US/Central', 'US/Central'), ('US/East-Indiana', 'US/East-Indiana'), ('US/Eastern', 'US/Eastern'), ('US/Hawaii', 'US/Hawaii'), ('US/Indiana-Starke', 'US/Indiana-Starke'), ('US/Michigan', 'US/Michigan'), ('US/Mountain', 'US/Mountain'), ('US/Pacific', 'US/Pacific'), ('US/Samoa', 'US/Samoa'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')], default='UTC', max_length=255), + ), + migrations.AlterField( + model_name='issuelink', + name='title', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.RunPython(update_user_timezones) + ] diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index 3975a3b93..e90e19c5e 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -2,6 +2,7 @@ import uuid import string import random +import pytz # Django imports from django.db import models @@ -9,9 +10,6 @@ from django.db.models.signals import post_save from django.dispatch import receiver from django.contrib.auth.models import AbstractBaseUser, UserManager, PermissionsMixin from django.utils import timezone -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 # Third party imports @@ -66,7 +64,8 @@ class User(AbstractBaseUser, PermissionsMixin): billing_address = models.JSONField(null=True) has_billing_address = models.BooleanField(default=False) - user_timezone = models.CharField(max_length=255, default="Asia/Kolkata") + USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones)) + user_timezone = models.CharField(max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES) last_active = models.DateTimeField(default=timezone.now, null=True) last_login_time = models.DateTimeField(null=True) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 59e0bd31b..27da44d9c 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -49,7 +49,7 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", "crum.CurrentRequestUserMiddleware", "django.middleware.gzip.GZipMiddleware", -] + ] REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( @@ -161,7 +161,7 @@ MEDIA_URL = "/media/" LANGUAGE_CODE = "en-us" -TIME_ZONE = "Asia/Kolkata" +TIME_ZONE = "UTC" USE_I18N = True From 761a1eb41a68dc0479a715b9659521212c2c9a0e Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 30 Aug 2023 12:18:56 +0530 Subject: [PATCH 018/137] fix: user created by stats (#2016) --- apiserver/plane/api/views/workspace.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index cfdd0dd9b..b10fe3d42 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -1100,7 +1100,6 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView): created_issues = ( Issue.issue_objects.filter( workspace__slug=slug, - assignees__in=[user_id], project__project_projectmember__member=request.user, created_by_id=user_id, ) From 17aff1f369e15eeb0b5142a732208e964819665b Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 30 Aug 2023 12:20:13 +0530 Subject: [PATCH 019/137] fix: asset key validation (#1938) * fix: asset key validation * chore: asset key validation in user assets --------- Co-authored-by: Bavisetti Narayan --- apiserver/plane/api/views/asset.py | 32 ++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/apiserver/plane/api/views/asset.py b/apiserver/plane/api/views/asset.py index 0b935a4d3..d9b6e502d 100644 --- a/apiserver/plane/api/views/asset.py +++ b/apiserver/plane/api/views/asset.py @@ -18,10 +18,21 @@ class FileAssetEndpoint(BaseAPIView): """ def get(self, request, workspace_id, asset_key): - asset_key = str(workspace_id) + "/" + asset_key - files = FileAsset.objects.filter(asset=asset_key) - serializer = FileAssetSerializer(files, context={"request": request}, many=True) - return Response(serializer.data) + try: + asset_key = str(workspace_id) + "/" + asset_key + files = FileAsset.objects.filter(asset=asset_key) + if files.exists(): + serializer = FileAssetSerializer(files, context={"request": request}, many=True) + return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK) + else: + return Response({"error": "Asset key does not exist", "status": False}, 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, + ) + def post(self, request, slug): try: @@ -68,11 +79,16 @@ class UserAssetsEndpoint(BaseAPIView): def get(self, request, asset_key): try: files = FileAsset.objects.filter(asset=asset_key, created_by=request.user) - serializer = FileAssetSerializer(files, context={"request": request}) - return Response(serializer.data) - except FileAsset.DoesNotExist: + if files.exists(): + serializer = FileAssetSerializer(files, context={"request": request}) + return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK) + else: + return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) return Response( - {"error": "File Asset does not exist"}, status=status.HTTP_404_NOT_FOUND + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, ) def post(self, request): From f5a076e9a9e003f52bebd407765f8da7d4f268da Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 30 Aug 2023 13:26:28 +0530 Subject: [PATCH 020/137] dev: revamp peek overview (#2021) * dev: mobx for issues store * refactor: peek overview component * chore: update open issue button * fix: issue mutation after any crud action * chore: remove peek overview from gantt * chore: refactor code --- .../components/command-palette/command-k.tsx | 2 +- .../views/spreadsheet-view/single-issue.tsx | 24 +- .../spreadsheet-view/spreadsheet-view.tsx | 159 +++++------ .../components/issues/gantt-chart/blocks.tsx | 4 +- .../peek-overview/full-screen-peek-view.tsx | 84 +++--- .../issues/peek-overview/header.tsx | 32 +-- .../issues/peek-overview/issue-properties.tsx | 25 +- .../issues/peek-overview/layout.tsx | 247 ++++++++++++------ .../issues/peek-overview/side-peek-view.tsx | 83 +++--- .../app/hooks/use-spreadsheet-issues-view.tsx | 15 +- apps/app/store/issues.ts | 172 ++++++++++++ apps/app/store/root.ts | 3 + 12 files changed, 576 insertions(+), 274 deletions(-) create mode 100644 apps/app/store/issues.ts diff --git a/apps/app/components/command-palette/command-k.tsx b/apps/app/components/command-palette/command-k.tsx index a1525a348..d20a44290 100644 --- a/apps/app/components/command-palette/command-k.tsx +++ b/apps/app/components/command-palette/command-k.tsx @@ -665,7 +665,7 @@ export const CommandK: React.FC = ({ deleteIssue, isPaletteOpen, setIsPal className="focus:outline-none" >
- + Join our Discord
diff --git a/apps/app/components/core/views/spreadsheet-view/single-issue.tsx b/apps/app/components/core/views/spreadsheet-view/single-issue.tsx index 11a8c42c5..53869a638 100644 --- a/apps/app/components/core/views/spreadsheet-view/single-issue.tsx +++ b/apps/app/components/core/views/spreadsheet-view/single-issue.tsx @@ -6,7 +6,6 @@ import { mutate } from "swr"; // components import { - IssuePeekOverview, ViewAssigneeSelect, ViewDueDateSelect, ViewEstimateSelect, @@ -76,9 +75,6 @@ export const SingleSpreadsheetIssue: React.FC = ({ }) => { const [isOpen, setIsOpen] = useState(false); - // issue peek overview - const [issuePeekOverview, setIssuePeekOverview] = useState(false); - const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; @@ -161,6 +157,15 @@ export const SingleSpreadsheetIssue: React.FC = ({ [workspaceSlug, projectId, cycleId, moduleId, viewId, params, user] ); + const openPeekOverview = () => { + const { query } = router; + + router.push({ + pathname: router.pathname, + query: { ...query, peekIssue: issue.id }, + }); + }; + const handleCopyText = () => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; @@ -183,15 +188,6 @@ export const SingleSpreadsheetIssue: React.FC = ({ return ( <> - handleDeleteIssue(issue)} - handleUpdateIssue={async (formData) => partialUpdateIssue(formData, issue)} - issue={issue} - isOpen={issuePeekOverview} - onClose={() => setIssuePeekOverview(false)} - workspaceSlug={workspaceSlug?.toString() ?? ""} - readOnly={isNotAllowed} - />
= ({ diff --git a/apps/app/components/core/views/spreadsheet-view/spreadsheet-view.tsx b/apps/app/components/core/views/spreadsheet-view/spreadsheet-view.tsx index 0b2e785d6..1076f30d0 100644 --- a/apps/app/components/core/views/spreadsheet-view/spreadsheet-view.tsx +++ b/apps/app/components/core/views/spreadsheet-view/spreadsheet-view.tsx @@ -6,6 +6,7 @@ import { useRouter } from "next/router"; // components import { SpreadsheetColumns, SpreadsheetIssues } from "components/core"; import { CustomMenu, Spinner } from "components/ui"; +import { IssuePeekOverview } from "components/issues"; // hooks import useIssuesProperties from "hooks/use-issue-properties"; import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; @@ -38,7 +39,7 @@ export const SpreadsheetView: React.FC = ({ const type = cycleId ? "cycle" : moduleId ? "module" : "issue"; - const { spreadsheetIssues } = useSpreadsheetIssuesView(); + const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView(); const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); @@ -59,80 +60,88 @@ export const SpreadsheetView: React.FC = ({ .join(" "); return ( -
-
- -
- {spreadsheetIssues ? ( -
- {spreadsheetIssues.map((issue: IIssue, index) => ( - - ))} -
- {type === "issue" ? ( - - ) : ( - !disableUserActions && ( - - - Add Issue - - } - position="left" - optionsClassName="left-5 !w-36" - noBorder - > - { - const e = new KeyboardEvent("keydown", { key: "c" }); - document.dispatchEvent(e); - }} - > - Create new - - {openIssuesListModal && ( - - Add an existing issue - - )} - - ) - )} -
+ <> + mutateIssues()} + projectId={projectId?.toString() ?? ""} + workspaceSlug={workspaceSlug?.toString() ?? ""} + readOnly={disableUserActions} + /> +
+
+
- ) : ( - - )} -
+ {spreadsheetIssues ? ( +
+ {spreadsheetIssues.map((issue: IIssue, index) => ( + + ))} +
+ {type === "issue" ? ( + + ) : ( + !disableUserActions && ( + + + Add Issue + + } + position="left" + optionsClassName="left-5 !w-36" + noBorder + > + { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + Create new + + {openIssuesListModal && ( + + Add an existing issue + + )} + + ) + )} +
+
+ ) : ( + + )} +
+ ); }; diff --git a/apps/app/components/issues/gantt-chart/blocks.tsx b/apps/app/components/issues/gantt-chart/blocks.tsx index 2ad21c499..3ab7ea90b 100644 --- a/apps/app/components/issues/gantt-chart/blocks.tsx +++ b/apps/app/components/issues/gantt-chart/blocks.tsx @@ -15,7 +15,7 @@ export const IssueGanttBlock = ({ data }: { data: IIssue }) => { return (
router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)} > @@ -49,7 +49,7 @@ export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => { return (
router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)} > {getStateGroupIcon(data?.state_detail?.group, "14", "14", data?.state_detail?.color)} diff --git a/apps/app/components/issues/peek-overview/full-screen-peek-view.tsx b/apps/app/components/issues/peek-overview/full-screen-peek-view.tsx index d470f4910..9c04e0b6a 100644 --- a/apps/app/components/issues/peek-overview/full-screen-peek-view.tsx +++ b/apps/app/components/issues/peek-overview/full-screen-peek-view.tsx @@ -1,3 +1,4 @@ +// components import { PeekOverviewHeader, PeekOverviewIssueActivity, @@ -5,13 +6,16 @@ import { PeekOverviewIssueProperties, TPeekOverviewModes, } from "components/issues"; +// ui +import { Loader } from "components/ui"; +// types import { IIssue } from "types"; type Props = { handleClose: () => void; handleDeleteIssue: () => void; - handleUpdateIssue: (issue: Partial) => Promise; - issue: IIssue; + handleUpdateIssue: (formData: Partial) => Promise; + issue: IIssue | undefined; mode: TPeekOverviewModes; readOnly: boolean; setMode: (mode: TPeekOverviewModes) => void; @@ -40,39 +44,59 @@ export const FullScreenPeekView: React.FC = ({ workspaceSlug={workspaceSlug} />
-
- {/* issue title and description */} -
- + {issue ? ( +
+ {/* issue title and description */} +
+ +
+ {/* divider */} +
+ {/* issue activity/comments */} +
+ +
- {/* divider */} -
- {/* issue activity/comments */} -
- -
-
+ ) : ( + + +
+ + + +
+
+ )}
{/* issue properties */}
- + {issue ? ( + + ) : ( + + + + + + + )}
diff --git a/apps/app/components/issues/peek-overview/header.tsx b/apps/app/components/issues/peek-overview/header.tsx index 29e23a262..266b2edb8 100644 --- a/apps/app/components/issues/peek-overview/header.tsx +++ b/apps/app/components/issues/peek-overview/header.tsx @@ -1,18 +1,21 @@ +import Link from "next/link"; + // hooks import useToast from "hooks/use-toast"; // ui import { CustomSelect, Icon } from "components/ui"; +// icons +import { East, OpenInFull } from "@mui/icons-material"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types import { IIssue } from "types"; import { TPeekOverviewModes } from "./layout"; -import { ArrowRightAlt, CloseFullscreen, East, OpenInFull } from "@mui/icons-material"; type Props = { handleClose: () => void; handleDeleteIssue: () => void; - issue: IIssue; + issue: IIssue | undefined; mode: TPeekOverviewModes; setMode: (mode: TPeekOverviewModes) => void; workspaceSlug: string; @@ -47,12 +50,9 @@ export const PeekOverviewHeader: React.FC = ({ const { setToastAlert } = useToast(); const handleCopyLink = () => { - const originURL = - typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; + const urlToCopy = window.location.href; - copyTextToClipboard( - `${originURL}/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}` - ).then(() => { + copyTextToClipboard(urlToCopy).then(() => { setToastAlert({ type: "success", title: "Link copied!", @@ -73,23 +73,15 @@ export const PeekOverviewHeader: React.FC = ({ /> )} - {mode === "modal" || mode === "full" ? ( - - ) : ( - - )} + + setMode(val)} @@ -119,7 +111,7 @@ export const PeekOverviewHeader: React.FC = ({
{(mode === "side" || mode === "modal") && ( -
+
diff --git a/apps/app/components/issues/peek-overview/issue-properties.tsx b/apps/app/components/issues/peek-overview/issue-properties.tsx index bf1ecefd9..2c8b4d572 100644 --- a/apps/app/components/issues/peek-overview/issue-properties.tsx +++ b/apps/app/components/issues/peek-overview/issue-properties.tsx @@ -1,6 +1,11 @@ +// mobx +import { observer } from "mobx-react-lite"; // headless ui import { Disclosure } from "@headlessui/react"; import { getStateGroupIcon } from "components/icons"; +// hooks +import useToast from "hooks/use-toast"; +import useUser from "hooks/use-user"; // components import { SidebarAssigneeSelect, @@ -9,27 +14,27 @@ import { SidebarStateSelect, TPeekOverviewModes, } from "components/issues"; -// icons +// ui import { CustomDatePicker, Icon } from "components/ui"; +// helpers import { copyTextToClipboard } from "helpers/string.helper"; -import useToast from "hooks/use-toast"; // types import { IIssue } from "types"; type Props = { handleDeleteIssue: () => void; + handleUpdateIssue: (formData: Partial) => Promise; issue: IIssue; mode: TPeekOverviewModes; - onChange: (issueProperty: Partial) => void; readOnly: boolean; workspaceSlug: string; }; export const PeekOverviewIssueProperties: React.FC = ({ handleDeleteIssue, + handleUpdateIssue, issue, mode, - onChange, readOnly, workspaceSlug, }) => { @@ -86,7 +91,7 @@ export const PeekOverviewIssueProperties: React.FC = ({
onChange({ state: val })} + onChange={(val: string) => handleUpdateIssue({ state: val })} disabled={readOnly} />
@@ -99,7 +104,7 @@ export const PeekOverviewIssueProperties: React.FC = ({
onChange({ assignees_list: val })} + onChange={(val: string[]) => handleUpdateIssue({ assignees_list: val })} disabled={readOnly} />
@@ -112,7 +117,7 @@ export const PeekOverviewIssueProperties: React.FC = ({
onChange({ priority: val })} + onChange={(val: string) => handleUpdateIssue({ priority: val })} disabled={readOnly} />
@@ -128,7 +133,7 @@ export const PeekOverviewIssueProperties: React.FC = ({ placeholder="Start date" value={issue.start_date} onChange={(val) => - onChange({ + handleUpdateIssue({ start_date: val, }) } @@ -153,7 +158,7 @@ export const PeekOverviewIssueProperties: React.FC = ({ placeholder="Due date" value={issue.target_date} onChange={(val) => - onChange({ + handleUpdateIssue({ target_date: val, }) } @@ -175,7 +180,7 @@ export const PeekOverviewIssueProperties: React.FC = ({
onChange({ estimate_point: val })} + onChange={(val: number | null) =>handleUpdateIssue({ estimate_point: val })} disabled={readOnly} />
diff --git a/apps/app/components/issues/peek-overview/layout.tsx b/apps/app/components/issues/peek-overview/layout.tsx index 7196052f8..50fa5df68 100644 --- a/apps/app/components/issues/peek-overview/layout.tsx +++ b/apps/app/components/issues/peek-overview/layout.tsx @@ -1,107 +1,184 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; +import { useRouter } from "next/router"; + +// mobx +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // headless ui import { Dialog, Transition } from "@headlessui/react"; +// hooks +import useUser from "hooks/use-user"; +// components import { FullScreenPeekView, SidePeekView } from "components/issues"; // types import { IIssue } from "types"; type Props = { - handleDeleteIssue: () => void; - handleUpdateIssue: (issue: Partial) => Promise; - issue: IIssue | null; - isOpen: boolean; - onClose: () => void; - workspaceSlug: string; + handleMutation: () => void; + projectId: string; readOnly: boolean; + workspaceSlug: string; }; export type TPeekOverviewModes = "side" | "modal" | "full"; -export const IssuePeekOverview: React.FC = ({ - handleDeleteIssue, - handleUpdateIssue, - issue, - isOpen, - onClose, - workspaceSlug, - readOnly, -}) => { - const [peekOverviewMode, setPeekOverviewMode] = useState("side"); +export const IssuePeekOverview: React.FC = observer( + ({ handleMutation, projectId, readOnly, workspaceSlug }) => { + const [isSidePeekOpen, setIsSidePeekOpen] = useState(false); + const [isModalPeekOpen, setIsModalPeekOpen] = useState(false); + const [peekOverviewMode, setPeekOverviewMode] = useState("side"); - const handleClose = () => { - onClose(); - setPeekOverviewMode("side"); - }; + const router = useRouter(); + const { peekIssue } = router.query; - if (!issue || !isOpen) return null; + const { issues: issuesStore } = useMobxStore(); + const { deleteIssue, getIssueById, issues, updateIssue } = issuesStore; - return ( - - - {/* add backdrop conditionally */} - {(peekOverviewMode === "modal" || peekOverviewMode === "full") && ( - -
- - )} -
-
+ const issue = issues[peekIssue?.toString() ?? ""]; + + const { user } = useUser(); + + const handleClose = () => { + const { query } = router; + delete query.peekIssue; + + router.push({ + pathname: router.pathname, + query: { ...query }, + }); + }; + + const handleUpdateIssue = async (formData: Partial) => { + if (!issue || !user) return; + + await updateIssue(workspaceSlug, projectId, issue.id, formData, user); + handleMutation(); + }; + + const handleDeleteIssue = async () => { + if (!issue || !user) return; + + await deleteIssue(workspaceSlug, projectId, issue.id, user); + handleMutation(); + + handleClose(); + }; + + useEffect(() => { + if (!peekIssue) return; + + getIssueById(workspaceSlug, projectId, peekIssue.toString()); + }, [getIssueById, peekIssue, projectId, workspaceSlug]); + + useEffect(() => { + if (peekIssue) { + if (peekOverviewMode === "side") { + setIsSidePeekOpen(true); + setIsModalPeekOpen(false); + } else { + setIsModalPeekOpen(true); + setIsSidePeekOpen(false); + } + } else { + console.log("Triggered"); + setIsSidePeekOpen(false); + setIsModalPeekOpen(false); + } + }, [peekIssue, peekOverviewMode]); + + return ( + <> + + +
+
+ + + setPeekOverviewMode(mode)} + workspaceSlug={workspaceSlug} + /> + + +
+
+
+
+ + - - {(peekOverviewMode === "side" || peekOverviewMode === "modal") && ( - setPeekOverviewMode(mode)} - workspaceSlug={workspaceSlug} - /> - )} - {peekOverviewMode === "full" && ( - setPeekOverviewMode(mode)} - workspaceSlug={workspaceSlug} - /> - )} - +
-
-
-
-
- ); -}; +
+
+ + + {peekOverviewMode === "modal" && ( + setPeekOverviewMode(mode)} + workspaceSlug={workspaceSlug} + /> + )} + {peekOverviewMode === "full" && ( + setPeekOverviewMode(mode)} + workspaceSlug={workspaceSlug} + /> + )} + + +
+
+ + + + ); + } +); diff --git a/apps/app/components/issues/peek-overview/side-peek-view.tsx b/apps/app/components/issues/peek-overview/side-peek-view.tsx index f938c3805..1bdeed479 100644 --- a/apps/app/components/issues/peek-overview/side-peek-view.tsx +++ b/apps/app/components/issues/peek-overview/side-peek-view.tsx @@ -1,3 +1,4 @@ +// components import { PeekOverviewHeader, PeekOverviewIssueActivity, @@ -5,13 +6,16 @@ import { PeekOverviewIssueProperties, TPeekOverviewModes, } from "components/issues"; +// ui +import { Loader } from "components/ui"; +// types import { IIssue } from "types"; type Props = { handleClose: () => void; handleDeleteIssue: () => void; - handleUpdateIssue: (issue: Partial) => Promise; - issue: IIssue; + handleUpdateIssue: (formData: Partial) => Promise; + issue: IIssue | undefined; mode: TPeekOverviewModes; readOnly: boolean; setMode: (mode: TPeekOverviewModes) => void; @@ -39,37 +43,50 @@ export const SidePeekView: React.FC = ({ workspaceSlug={workspaceSlug} />
-
- {/* issue title and description */} -
- + {issue ? ( +
+ {/* issue title and description */} +
+ +
+ {/* issue properties */} +
+ +
+ {/* divider */} +
+ {/* issue activity/comments */} +
+ {issue && ( + + )} +
- {/* issue properties */} -
- -
- {/* divider */} -
- {/* issue activity/comments */} -
- -
-
+ ) : ( + + +
+ + + +
+
+ )}
); diff --git a/apps/app/hooks/use-spreadsheet-issues-view.tsx b/apps/app/hooks/use-spreadsheet-issues-view.tsx index 4471b4352..145313dac 100644 --- a/apps/app/hooks/use-spreadsheet-issues-view.tsx +++ b/apps/app/hooks/use-spreadsheet-issues-view.tsx @@ -48,7 +48,7 @@ const useSpreadsheetIssuesView = () => { sub_issue: "false", }; - const { data: projectSpreadsheetIssues } = useSWR( + const { data: projectSpreadsheetIssues, mutate: mutateProjectSpreadsheetIssues } = useSWR( workspaceSlug && projectId ? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params) : null, @@ -58,7 +58,7 @@ const useSpreadsheetIssuesView = () => { : null ); - const { data: cycleSpreadsheetIssues } = useSWR( + const { data: cycleSpreadsheetIssues, mutate: mutateCycleSpreadsheetIssues } = useSWR( workspaceSlug && projectId && cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) : null, @@ -73,7 +73,7 @@ const useSpreadsheetIssuesView = () => { : null ); - const { data: moduleSpreadsheetIssues } = useSWR( + const { data: moduleSpreadsheetIssues, mutate: mutateModuleSpreadsheetIssues } = useSWR( workspaceSlug && projectId && moduleId ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) : null, @@ -88,7 +88,7 @@ const useSpreadsheetIssuesView = () => { : null ); - const { data: viewSpreadsheetIssues } = useSWR( + const { data: viewSpreadsheetIssues, mutate: mutateViewSpreadsheetIssues } = useSWR( workspaceSlug && projectId && viewId && params ? VIEW_ISSUES(viewId.toString(), params) : null, workspaceSlug && projectId && viewId && params ? () => @@ -106,6 +106,13 @@ const useSpreadsheetIssuesView = () => { return { issueView, + mutateIssues: cycleId + ? mutateCycleSpreadsheetIssues + : moduleId + ? mutateModuleSpreadsheetIssues + : viewId + ? mutateViewSpreadsheetIssues + : mutateProjectSpreadsheetIssues, spreadsheetIssues: spreadsheetIssues ?? [], orderBy, setOrderBy, diff --git a/apps/app/store/issues.ts b/apps/app/store/issues.ts new file mode 100644 index 000000000..538c5e2a9 --- /dev/null +++ b/apps/app/store/issues.ts @@ -0,0 +1,172 @@ +// mobx +import { action, observable, runInAction, makeAutoObservable } from "mobx"; +// services +import issueService from "services/issues.service"; +// types +import type { ICurrentUserResponse, IIssue } from "types"; + +class IssuesStore { + issues: { [key: string]: IIssue } = {}; + isIssuesLoading: boolean = false; + rootStore: any | null = null; + + constructor(_rootStore: any | null = null) { + makeAutoObservable(this, { + issues: observable.ref, + loadIssues: action, + getIssueById: action, + isIssuesLoading: observable, + createIssue: action, + updateIssue: action, + deleteIssue: action, + }); + + this.rootStore = _rootStore; + } + + /** + * @description Fetch all issues of a project and hydrate issues field + */ + + loadIssues = async (workspaceSlug: string, projectId: string) => { + this.isIssuesLoading = true; + try { + const issuesResponse: IIssue[] = (await issueService.getIssuesWithParams( + workspaceSlug, + projectId + )) as IIssue[]; + + const issues: { [kye: string]: IIssue } = {}; + issuesResponse.forEach((issue) => { + issues[issue.id] = issue; + }); + + runInAction(() => { + this.issues = issues; + this.isIssuesLoading = false; + }); + } catch (error) { + this.isIssuesLoading = false; + console.error("Fetching issues error", error); + } + }; + + getIssueById = async ( + workspaceSlug: string, + projectId: string, + issueId: string + ): Promise => { + if (this.issues[issueId]) return this.issues[issueId]; + + try { + const issueResponse: IIssue = await issueService.retrieve(workspaceSlug, projectId, issueId); + + const issues = { + ...this.issues, + [issueId]: { ...issueResponse }, + }; + + runInAction(() => { + this.issues = issues; + }); + + return issueResponse; + } catch (error) { + throw error; + } + }; + + createIssue = async ( + workspaceSlug: string, + projectId: string, + issueForm: IIssue, + user: ICurrentUserResponse + ): Promise => { + try { + const issueResponse = await issueService.createIssues( + workspaceSlug, + projectId, + issueForm, + user + ); + + const issues = { + ...this.issues, + [issueResponse.id]: { ...issueResponse }, + }; + + runInAction(() => { + this.issues = issues; + }); + return issueResponse; + } catch (error) { + console.error("Creating issue error", error); + throw error; + } + }; + + updateIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + issueForm: Partial, + user: ICurrentUserResponse + ) => { + // keep a copy of the issue in the store + const originalIssue = { ...this.issues[issueId] }; + + // immediately update the issue in the store + const updatedIssue = { ...originalIssue, ...issueForm }; + + try { + runInAction(() => { + this.issues[issueId] = updatedIssue; + }); + + // make a patch request to update the issue + const issueResponse: IIssue = await issueService.patchIssue( + workspaceSlug, + projectId, + issueId, + issueForm, + user + ); + + const updatedIssues = { ...this.issues }; + updatedIssues[issueId] = { ...issueResponse }; + + runInAction(() => { + this.issues = updatedIssues; + }); + } catch (error) { + // if there is an error, revert the changes + runInAction(() => { + this.issues[issueId] = originalIssue; + }); + + return error; + } + }; + + deleteIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + user: ICurrentUserResponse + ) => { + const issues = { ...this.issues }; + delete issues[issueId]; + + try { + runInAction(() => { + this.issues = issues; + }); + + issueService.deleteIssue(workspaceSlug, projectId, issueId, user); + } catch (error) { + console.error("Deleting issue error", error); + } + }; +} + +export default IssuesStore; diff --git a/apps/app/store/root.ts b/apps/app/store/root.ts index 5895637a8..40dd62fe6 100644 --- a/apps/app/store/root.ts +++ b/apps/app/store/root.ts @@ -3,6 +3,7 @@ import { enableStaticRendering } from "mobx-react-lite"; // store imports import UserStore from "./user"; import ThemeStore from "./theme"; +import IssuesStore from "./issues"; import ProjectPublishStore, { IProjectPublishStore } from "./project-publish"; enableStaticRendering(typeof window === "undefined"); @@ -11,10 +12,12 @@ export class RootStore { user; theme; projectPublish: IProjectPublishStore; + issues: IssuesStore; constructor() { this.user = new UserStore(this); this.theme = new ThemeStore(this); this.projectPublish = new ProjectPublishStore(this); + this.issues = new IssuesStore(this); } } From 6c6b81bea7d123454c1c6e58ff43d1d94cf8e051 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 30 Aug 2023 16:38:04 +0530 Subject: [PATCH 021/137] chore: tracking the history of issue reactions and votes. (#2020) * chore: tracking the issues reaction and vote history * fix: changed the keywords for vote and reaction * chore: added validation --- apiserver/plane/api/views/issue.py | 114 +++++++++++- apiserver/plane/api/views/workspace.py | 1 + .../plane/bgtasks/issue_activites_task.py | 167 +++++++++++++++++- 3 files changed, 280 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 802431d2e..ac69e9d8d 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -486,7 +486,7 @@ class IssueActivityEndpoint(BaseAPIView): issue_activities = ( IssueActivity.objects.filter(issue_id=issue_id) .filter( - ~Q(field="comment"), + ~Q(field__in=["comment", "vote", "reaction"]), project__project_projectmember__member=self.request.user, ) .select_related("actor", "workspace", "issue", "project") @@ -1405,6 +1405,14 @@ class IssueReactionViewSet(BaseViewSet): project_id=self.kwargs.get("project_id"), actor=self.request.user, ) + issue_activity.delay( + type="issue_reaction.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + ) def destroy(self, request, slug, project_id, issue_id, reaction_code): try: @@ -1415,6 +1423,19 @@ class IssueReactionViewSet(BaseViewSet): reaction=reaction_code, actor=request.user, ) + issue_activity.delay( + type="issue_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(issue_reaction.id), + } + ), + ) issue_reaction.delete() return Response(status=status.HTTP_204_NO_CONTENT) except IssueReaction.DoesNotExist: @@ -1455,6 +1476,14 @@ class CommentReactionViewSet(BaseViewSet): comment_id=self.kwargs.get("comment_id"), project_id=self.kwargs.get("project_id"), ) + issue_activity.delay( + type="comment_reaction.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + ) def destroy(self, request, slug, project_id, comment_id, reaction_code): try: @@ -1465,6 +1494,20 @@ class CommentReactionViewSet(BaseViewSet): reaction=reaction_code, actor=request.user, ) + issue_activity.delay( + type="comment_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(comment_reaction.id), + "comment_id": str(comment_id) + } + ), + ) comment_reaction.delete() return Response(status=status.HTTP_204_NO_CONTENT) except CommentReaction.DoesNotExist: @@ -1691,6 +1734,14 @@ class IssueReactionPublicViewSet(BaseViewSet): project_id=project_id, member=request.user, ) + issue_activity.delay( + type="issue_reaction.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except ProjectDeployBoard.DoesNotExist: @@ -1722,6 +1773,19 @@ class IssueReactionPublicViewSet(BaseViewSet): reaction=reaction_code, actor=request.user, ) + issue_activity.delay( + type="issue_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(issue_reaction.id), + } + ), + ) issue_reaction.delete() return Response(status=status.HTTP_204_NO_CONTENT) except IssueReaction.DoesNotExist: @@ -1784,8 +1848,21 @@ class CommentReactionPublicViewSet(BaseViewSet): project_id=project_id, member=request.user, ) + issue_activity.delay( + type="comment_reaction.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except IssueComment.DoesNotExist: + return Response( + {"error": "Comment does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) except ProjectDeployBoard.DoesNotExist: return Response( {"error": "Project board does not exist"}, @@ -1816,6 +1893,20 @@ class CommentReactionPublicViewSet(BaseViewSet): reaction=reaction_code, actor=request.user, ) + issue_activity.delay( + type="comment_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(comment_reaction.id), + "comment_id": str(comment_id) + } + ), + ) comment_reaction.delete() return Response(status=status.HTTP_204_NO_CONTENT) except CommentReaction.DoesNotExist: @@ -1861,6 +1952,14 @@ class IssueVotePublicViewSet(BaseViewSet): ) issue_vote.vote = request.data.get("vote", 1) issue_vote.save() + issue_activity.delay( + type="issue_vote.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + ) serializer = IssueVoteSerializer(issue_vote) return Response(serializer.data, status=status.HTTP_201_CREATED) except Exception as e: @@ -1878,6 +1977,19 @@ class IssueVotePublicViewSet(BaseViewSet): issue_id=issue_id, actor_id=request.user.id, ) + issue_activity.delay( + type="issue_vote.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "vote": str(issue_vote.vote), + "identifier": str(issue_vote.id), + } + ), + ) issue_vote.delete() return Response(status=status.HTTP_204_NO_CONTENT) except Exception as e: diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index b10fe3d42..cbf62548f 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -1197,6 +1197,7 @@ class WorkspaceUserActivityEndpoint(BaseAPIView): projects = request.query_params.getlist("project", []) queryset = IssueActivity.objects.filter( + ~Q(field__in=["comment", "vote", "reaction"]), workspace__slug=slug, project__project_projectmember__member=request.user, actor=user_id, diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 1cc6c85cc..0cadac553 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -24,6 +24,9 @@ from plane.db.models import ( IssueSubscriber, Notification, IssueAssignee, + IssueReaction, + CommentReaction, + IssueComment, ) from plane.api.serializers import IssueActivitySerializer @@ -629,7 +632,7 @@ def update_issue_activity( "parent": track_parent, "priority": track_priority, "state": track_state, - "description": track_description, + "description_html": track_description, "target_date": track_target_date, "start_date": track_start_date, "labels_list": track_labels, @@ -1022,6 +1025,150 @@ def delete_attachment_activity( ) ) +def create_issue_reaction_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + requested_data = json.loads(requested_data) if requested_data is not None else None + if requested_data and requested_data.get("reaction") is not None: + issue_reaction = IssueReaction.objects.filter(reaction=requested_data.get("reaction"), project=project, actor=actor).values_list('id', flat=True).first() + if issue_reaction is not None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="created", + old_value=None, + new_value=requested_data.get("reaction"), + field="reaction", + project=project, + workspace=project.workspace, + comment="added the reaction", + old_identifier=None, + new_identifier=issue_reaction, + ) + ) + + +def delete_issue_reaction_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + current_instance = ( + json.loads(current_instance) if current_instance is not None else None + ) + if current_instance and current_instance.get("reaction") is not None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="deleted", + old_value=current_instance.get("reaction"), + new_value=None, + field="reaction", + project=project, + workspace=project.workspace, + comment="removed the reaction", + old_identifier=current_instance.get("identifier"), + new_identifier=None, + ) + ) + + +def create_comment_reaction_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + requested_data = json.loads(requested_data) if requested_data is not None else None + if requested_data and requested_data.get("reaction") is not None: + comment_reaction_id, comment_id = CommentReaction.objects.filter(reaction=requested_data.get("reaction"), project=project, actor=actor).values_list('id', 'comment__id').first() + comment = IssueComment.objects.get(pk=comment_id,project=project) + if comment is not None and comment_reaction_id is not None and comment_id is not None: + issue_activities.append( + IssueActivity( + issue_id=comment.issue_id, + actor=actor, + verb="created", + old_value=None, + new_value=requested_data.get("reaction"), + field="reaction", + project=project, + workspace=project.workspace, + comment="added the reaction", + old_identifier=None, + new_identifier=comment_reaction_id, + ) + ) + + +def delete_comment_reaction_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + current_instance = ( + json.loads(current_instance) if current_instance is not None else None + ) + if current_instance and current_instance.get("reaction") is not None: + issue_id = IssueComment.objects.filter(pk=current_instance.get("comment_id"), project=project).values_list('issue_id', flat=True).first() + if issue_id is not None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="deleted", + old_value=current_instance.get("reaction"), + new_value=None, + field="reaction", + project=project, + workspace=project.workspace, + comment="removed the reaction", + old_identifier=current_instance.get("identifier"), + new_identifier=None, + ) + ) + + +def create_issue_vote_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + requested_data = json.loads(requested_data) if requested_data is not None else None + if requested_data and requested_data.get("vote") is not None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="created", + old_value=None, + new_value=requested_data.get("vote"), + field="vote", + project=project, + workspace=project.workspace, + comment="added the vote", + old_identifier=None, + new_identifier=None, + ) + ) + + +def delete_issue_vote_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + current_instance = ( + json.loads(current_instance) if current_instance is not None else None + ) + if current_instance and current_instance.get("vote") is not None: + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor=actor, + verb="deleted", + old_value=current_instance.get("vote"), + new_value=None, + field="vote", + project=project, + workspace=project.workspace, + comment="removed the vote", + old_identifier=current_instance.get("identifier"), + new_identifier=None, + ) + ) + # Receive message from room group @shared_task @@ -1045,6 +1192,12 @@ def issue_activity( "cycle.activity.deleted", "module.activity.created", "module.activity.deleted", + "issue_reaction.activity.created", + "issue_reaction.activity.deleted", + "comment_reaction.activity.created", + "comment_reaction.activity.deleted", + "issue_vote.activity.created", + "issue_vote.activity.deleted", ]: issue = Issue.objects.filter(pk=issue_id).first() @@ -1080,6 +1233,12 @@ def issue_activity( "link.activity.deleted": delete_link_activity, "attachment.activity.created": create_attachment_activity, "attachment.activity.deleted": delete_attachment_activity, + "issue_reaction.activity.created": create_issue_reaction_activity, + "issue_reaction.activity.deleted": delete_issue_reaction_activity, + "comment_reaction.activity.created": create_comment_reaction_activity, + "comment_reaction.activity.deleted": delete_comment_reaction_activity, + "issue_vote.activity.created": create_issue_vote_activity, + "issue_vote.activity.deleted": delete_issue_vote_activity, } func = ACTIVITY_MAPPER.get(type) @@ -1119,6 +1278,12 @@ def issue_activity( "cycle.activity.deleted", "module.activity.created", "module.activity.deleted", + "issue_reaction.activity.created", + "issue_reaction.activity.deleted", + "comment_reaction.activity.created", + "comment_reaction.activity.deleted", + "issue_vote.activity.created", + "issue_vote.activity.deleted", ]: # Create Notifications bulk_notifications = [] From 54527cc2bb85bec500b01069f319fc58a6cd9c7e Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 30 Aug 2023 17:27:49 +0530 Subject: [PATCH 022/137] dev: revamp publish project modal (#2022) * dev: revamp publish project modal * chore: sidebar dropdown text --- .../project/publish-project/modal.tsx | 674 ++++++++++-------- .../project/publish-project/popover.tsx | 21 +- .../project/single-sidebar-project.tsx | 6 +- apps/app/store/project-publish.tsx | 22 +- 4 files changed, 377 insertions(+), 346 deletions(-) diff --git a/apps/app/components/project/publish-project/modal.tsx b/apps/app/components/project/publish-project/modal.tsx index 5f9d9ae2c..b22a496f5 100644 --- a/apps/app/components/project/publish-project/modal.tsx +++ b/apps/app/components/project/publish-project/modal.tsx @@ -1,28 +1,38 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; // next imports import { useRouter } from "next/router"; // react-hook-form -import { useForm } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; // headless ui import { Dialog, Transition } from "@headlessui/react"; // ui components -import { ToggleSwitch, PrimaryButton, SecondaryButton } from "components/ui"; +import { ToggleSwitch, PrimaryButton, SecondaryButton, Icon, DangerButton } from "components/ui"; import { CustomPopover } from "./popover"; // mobx react lite import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; -import { IProjectPublishSettingsViews } from "store/project-publish"; +import { IProjectPublishSettings, TProjectPublishViews } from "store/project-publish"; // hooks import useToast from "hooks/use-toast"; import useProjectDetails from "hooks/use-project-details"; +import useUser from "hooks/use-user"; type Props = { // user: ICurrentUserResponse | undefined; }; -const defaultValues: Partial = { +type FormData = { + id: string | null; + comments: boolean; + reactions: boolean; + votes: boolean; + inbox: string | null; + views: TProjectPublishViews[]; +}; + +const defaultValues: FormData = { id: null, comments: false, reactions: false, @@ -31,70 +41,73 @@ const defaultValues: Partial = { views: ["list", "kanban"], }; -const viewOptions = [ - { key: "list", value: "List" }, - { key: "kanban", value: "Kanban" }, - // { key: "calendar", value: "Calendar" }, - // { key: "gantt", value: "Gantt" }, - // { key: "spreadsheet", value: "Spreadsheet" }, +const viewOptions: { + key: TProjectPublishViews; + label: string; +}[] = [ + { key: "list", label: "List" }, + { key: "kanban", label: "Kanban" }, + // { key: "calendar", label: "Calendar" }, + // { key: "gantt", label: "Gantt" }, + // { key: "spreadsheet", label: "Spreadsheet" }, ]; export const PublishProjectModal: React.FC = observer(() => { - const store: RootStore = useMobxStore(); - const { projectPublish } = store; + const [isUnpublishing, setIsUnpublishing] = useState(false); + const [isUpdateRequired, setIsUpdateRequired] = useState(false); - const { projectDetails, mutateProjectDetails } = useProjectDetails(); - - const { setToastAlert } = useToast(); - const handleToastAlert = (title: string, type: string, message: string) => { - setToastAlert({ - title: title || "Title", - type: "error" || "warning", - message: message || "Message", - }); - }; - - const { NEXT_PUBLIC_DEPLOY_URL } = process.env; - const plane_deploy_url = NEXT_PUBLIC_DEPLOY_URL - ? NEXT_PUBLIC_DEPLOY_URL - : "http://localhost:3001"; + const plane_deploy_url = process.env.NEXT_PUBLIC_DEPLOY_URL ?? "http://localhost:4000"; const router = useRouter(); const { workspaceSlug } = router.query; + const store: RootStore = useMobxStore(); + const { projectPublish } = store; + + const { user } = useUser(); + + const { mutateProjectDetails } = useProjectDetails(); + + const { setToastAlert } = useToast(); + const { - formState: { errors, isSubmitting }, + control, + formState: { isSubmitting }, + getValues, handleSubmit, reset, watch, - setValue, - } = useForm({ + } = useForm({ defaultValues, - reValidateMode: "onChange", }); const handleClose = () => { projectPublish.handleProjectModal(null); + + setIsUpdateRequired(false); reset({ ...defaultValues }); }; + // prefill form with the saved settings if the project is already published useEffect(() => { if ( projectPublish.projectPublishSettings && - projectPublish.projectPublishSettings != "not-initialized" + projectPublish.projectPublishSettings !== "not-initialized" ) { - let userBoards: string[] = []; + let userBoards: TProjectPublishViews[] = []; + if (projectPublish.projectPublishSettings?.views) { - const _views: IProjectPublishSettingsViews | null = - projectPublish.projectPublishSettings?.views || null; - if (_views != null) { - if (_views.list) userBoards.push("list"); - if (_views.kanban) userBoards.push("kanban"); - if (_views.calendar) userBoards.push("calendar"); - if (_views.gantt) userBoards.push("gantt"); - if (_views.spreadsheet) userBoards.push("spreadsheet"); - userBoards = userBoards && userBoards.length > 0 ? userBoards : ["list"]; - } + const savedViews = projectPublish.projectPublishSettings?.views; + + if (!savedViews) return; + + if (savedViews.list) userBoards.push("list"); + if (savedViews.kanban) userBoards.push("kanban"); + if (savedViews.calendar) userBoards.push("calendar"); + if (savedViews.gantt) userBoards.push("gantt"); + if (savedViews.spreadsheet) userBoards.push("spreadsheet"); + + userBoards = userBoards && userBoards.length > 0 ? userBoards : ["list"]; } const updatedData = { @@ -105,126 +118,105 @@ export const PublishProjectModal: React.FC = observer(() => { inbox: projectPublish.projectPublishSettings?.inbox || null, views: userBoards, }; + reset({ ...updatedData }); } }, [reset, projectPublish.projectPublishSettings]); + // fetch publish settings useEffect(() => { + if (!workspaceSlug) return; + if ( projectPublish.projectPublishModal && - workspaceSlug && - projectPublish.project_id != null && + projectPublish.project_id !== null && projectPublish?.projectPublishSettings === "not-initialized" ) { projectPublish.getProjectSettingsAsync( - workspaceSlug as string, - projectPublish.project_id as string, + workspaceSlug.toString(), + projectPublish.project_id, null ); } }, [workspaceSlug, projectPublish, projectPublish.projectPublishModal]); - const onSettingsPublish = async (formData: any) => { - if (formData.views && formData.views.length > 0) { - const payload = { - comments: formData.comments || false, - reactions: formData.reactions || false, - votes: formData.votes || false, - inbox: formData.inbox || null, - views: { - list: formData.views.includes("list") || false, - kanban: formData.views.includes("kanban") || false, - calendar: formData.views.includes("calendar") || false, - gantt: formData.views.includes("gantt") || false, - spreadsheet: formData.views.includes("spreadsheet") || false, - }, - }; + const handlePublishProject = async (payload: IProjectPublishSettings) => { + if (!workspaceSlug || !user) return; - const _workspaceSlug = workspaceSlug; - const _projectId = projectPublish.project_id; - - return projectPublish - .createProjectSettingsAsync(_workspaceSlug as string, _projectId as string, payload, null) - .then((response) => { - mutateProjectDetails(); - handleClose(); - console.log("_projectId", _projectId); - if (_projectId) - window.open(`${plane_deploy_url}/${_workspaceSlug}/${_projectId}`, "_blank"); - return response; - }) - .catch((error) => { - console.error("error", error); - return error; - }); - } else { - handleToastAlert("Missing fields", "warning", "Please select at least one view to publish"); - } - }; - - const onSettingsUpdate = async (key: string, value: any) => { - const payload = { - comments: key === "comments" ? value : watch("comments"), - reactions: key === "reactions" ? value : watch("reactions"), - votes: key === "votes" ? value : watch("votes"), - inbox: key === "inbox" ? value : watch("inbox"), - views: - key === "views" - ? { - list: value.includes("list") ? true : false, - kanban: value.includes("kanban") ? true : false, - calendar: value.includes("calendar") ? true : false, - gantt: value.includes("gantt") ? true : false, - spreadsheet: value.includes("spreadsheet") ? true : false, - } - : { - list: watch("views").includes("list") ? true : false, - kanban: watch("views").includes("kanban") ? true : false, - calendar: watch("views").includes("calendar") ? true : false, - gantt: watch("views").includes("gantt") ? true : false, - spreadsheet: watch("views").includes("spreadsheet") ? true : false, - }, - }; + const projectId = projectPublish.project_id; return projectPublish - .updateProjectSettingsAsync( - workspaceSlug as string, - projectPublish.project_id as string, - watch("id"), + .createProjectSettingsAsync( + workspaceSlug.toString(), + projectId?.toString() ?? "", payload, - null + user ) .then((response) => { mutateProjectDetails(); + handleClose(); + if (projectId) window.open(`${plane_deploy_url}/${workspaceSlug}/${projectId}`, "_blank"); return response; }) + .catch((error) => { + console.error("error", error); + return error; + }); + }; + + const handleUpdatePublishSettings = async (payload: IProjectPublishSettings) => { + if (!workspaceSlug || !user) return; + + await projectPublish + .updateProjectSettingsAsync( + workspaceSlug.toString(), + projectPublish.project_id?.toString() ?? "", + payload.id ?? "", + payload, + user + ) + .then((res) => { + mutateProjectDetails(); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Publish settings updated successfully!", + }); + + handleClose(); + return res; + }) .catch((error) => { console.log("error", error); return error; }); }; - const onSettingsUnPublish = async (formData: any) => + const handleUnpublishProject = async (publishId: string) => { + if (!workspaceSlug || !publishId) return; + + setIsUnpublishing(true); + projectPublish .deleteProjectSettingsAsync( - workspaceSlug as string, + workspaceSlug.toString(), projectPublish.project_id as string, - formData?.id, + publishId, null ) - .then((response) => { + .then((res) => { mutateProjectDetails(); - reset({ ...defaultValues }); + handleClose(); - return response; + return res; }) - .catch((error) => { - console.error("error", error); - return error; - }); + .catch((err) => err) + .finally(() => setIsUnpublishing(false)); + }; const CopyLinkToClipboard = ({ copy_link }: { copy_link: string }) => { - const [status, setStatus] = React.useState(false); + const [status, setStatus] = useState(false); const copyText = () => { navigator.clipboard.writeText(copy_link); @@ -244,6 +236,68 @@ export const PublishProjectModal: React.FC = observer(() => { ); }; + const handleFormSubmit = async (formData: FormData) => { + if (!formData.views || formData.views.length === 0) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Please select at least one view layout to publish the project.", + }); + return; + } + + const payload = { + comments: formData.comments, + reactions: formData.reactions, + votes: formData.votes, + inbox: formData.inbox, + views: { + list: formData.views.includes("list"), + kanban: formData.views.includes("kanban"), + calendar: formData.views.includes("calendar"), + gantt: formData.views.includes("gantt"), + spreadsheet: formData.views.includes("spreadsheet"), + }, + }; + + if (watch("id")) await handleUpdatePublishSettings({ id: watch("id") ?? "", ...payload }); + else await handlePublishProject(payload); + }; + + // check if an update is required or not + const checkIfUpdateIsRequired = () => { + if ( + !projectPublish.projectPublishSettings || + projectPublish.projectPublishSettings === "not-initialized" + ) + return; + + const currentSettings = projectPublish.projectPublishSettings as IProjectPublishSettings; + const newSettings = getValues(); + + if ( + currentSettings.comments !== newSettings.comments || + currentSettings.reactions !== newSettings.reactions || + currentSettings.votes !== newSettings.votes + ) { + setIsUpdateRequired(true); + return; + } + + let viewCheckFlag = 0; + viewOptions.forEach((option) => { + if (currentSettings.views[option.key] !== newSettings.views.includes(option.key)) + viewCheckFlag++; + }); + + if (viewCheckFlag !== 0) { + setIsUpdateRequired(true); + return; + } + + setIsUpdateRequired(false); + }; + return ( @@ -270,200 +324,190 @@ export const PublishProjectModal: React.FC = observer(() => { leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - - {/* heading */} -
-
Publish
- {projectPublish.loader && ( -
Changes saved
- )} -
- close -
-
- - {/* content */} -
- {watch("id") && ( -
-
- - radio_button_checked - -
-
This project is live on web
-
- )} - -
-
- {`${plane_deploy_url}/${workspaceSlug}/${projectPublish.project_id}`} -
- - -
-
-
Views
-
- 0 - ? viewOptions - .filter( - (_view) => watch("views").includes(_view.key) && _view.value - ) - .map((_view) => _view.value) - .join(", ") - : `` - } - placeholder="Select views" - > - <> - {viewOptions && - viewOptions.length > 0 && - viewOptions.map((_view) => ( -
{ - const _views = - watch("views") && watch("views").length > 0 - ? watch("views").includes(_view?.key) - ? watch("views").filter((_o: string) => _o !== _view?.key) - : [...watch("views"), _view?.key] - : [_view?.key]; - setValue("views", _views); - if (watch("id") != null) onSettingsUpdate("views", _views); - }} - > -
{_view.value}
-
- {watch("views") && - watch("views").length > 0 && - watch("views").includes(_view.key) && ( - - done - - )} -
-
- ))} - -
-
-
- - {/*
-
Allow comments
-
- { - const _comments = !watch("comments"); - setValue("comments", _comments); - if (watch("id") != null) onSettingsUpdate("comments", _comments); - }} - size="sm" - /> -
-
*/} - - {/*
-
Allow reactions
-
- { - const _reactions = !watch("reactions"); - setValue("reactions", _reactions); - if (watch("id") != null) onSettingsUpdate("reactions", _reactions); - }} - size="sm" - /> -
-
*/} - - {/*
-
Allow Voting
-
- { - const _votes = !watch("votes"); - setValue("votes", _votes); - if (watch("id") != null) onSettingsUpdate("votes", _votes); - }} - size="sm" - /> -
-
*/} - - {/*
-
Allow issue proposals
-
- { - setValue("inbox", !watch("inbox")); - }} - size="sm" - /> -
-
*/} -
-
- - {/* modal handlers */} -
-
-
- public -
-
Anyone with the link can access
-
-
- Cancel - {watch("id") != null ? ( - - {isSubmitting ? "Unpublishing..." : "Unpublish"} - - ) : ( - - {isSubmitting ? "Publishing..." : "Publish"} - + {isUnpublishing ? "Unpublishing..." : "Unpublish"} + )}
-
+ + {/* content */} +
+
+
+ {`${plane_deploy_url}/${workspaceSlug}/${projectPublish.project_id}`} +
+
+ +
+
+ + {watch("id") && ( +
+
+ +
+
This project is live on web
+
+ )} + +
+
+
Views
+ ( + 0 + ? viewOptions + .filter((v) => value.includes(v.key)) + .map((v) => v.label) + .join(", ") + : `` + } + placeholder="Select views" + > + <> + {viewOptions.map((option) => ( +
{ + const _views = + value.length > 0 + ? value.includes(option.key) + ? value.filter((_o: string) => _o !== option.key) + : [...value, option.key] + : [option.key]; + + if (_views.length === 0) return; + + onChange(_views); + checkIfUpdateIsRequired(); + }} + > +
{option.label}
+
+ {value.length > 0 && value.includes(option.key) && ( + + )} +
+
+ ))} + +
+ )} + /> +
+ +
+
Allow comments
+ ( + { + onChange(val); + checkIfUpdateIsRequired(); + }} + size="sm" + /> + )} + /> +
+
+
Allow reactions
+ ( + { + onChange(val); + checkIfUpdateIsRequired(); + }} + size="sm" + /> + )} + /> +
+
+
Allow voting
+ ( + { + onChange(val); + checkIfUpdateIsRequired(); + }} + size="sm" + /> + )} + /> +
+ + {/*
+
Allow issue proposals
+ ( + + )} + /> +
*/} +
+
+ + {/* modal handlers */} +
+
+ +
Anyone with the link can access
+
+
+ Cancel + {watch("id") ? ( + <> + {isUpdateRequired && ( + + {isSubmitting ? "Updating..." : "Update settings"} + + )} + + ) : ( + + {isSubmitting ? "Publishing..." : "Publish"} + + )} +
+
+
diff --git a/apps/app/components/project/publish-project/popover.tsx b/apps/app/components/project/publish-project/popover.tsx index 623675b9f..5ab2d6432 100644 --- a/apps/app/components/project/publish-project/popover.tsx +++ b/apps/app/components/project/publish-project/popover.tsx @@ -1,6 +1,9 @@ import React, { Fragment } from "react"; + // headless ui import { Popover, Transition } from "@headlessui/react"; +// icons +import { Icon } from "components/ui"; export const CustomPopover = ({ children, @@ -16,18 +19,14 @@ export const CustomPopover = ({ {({ open }) => ( <> -
- {label ? label : placeholder ? placeholder : "Select"} -
-
+
{label ?? placeholder}
+
{!open ? ( - expand_more + ) : ( - expand_less + )}
@@ -41,8 +40,8 @@ export const CustomPopover = ({ leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 translate-y-1" > - -
+ +
{children}
diff --git a/apps/app/components/project/single-sidebar-project.tsx b/apps/app/components/project/single-sidebar-project.tsx index 7bfca0d2c..6fbdbbaf0 100644 --- a/apps/app/components/project/single-sidebar-project.tsx +++ b/apps/app/components/project/single-sidebar-project.tsx @@ -26,7 +26,6 @@ import { SettingsOutlined, } from "@mui/icons-material"; // helpers -import { truncateText } from "helpers/string.helper"; import { renderEmoji } from "helpers/emoji.helper"; // types import { IProject } from "types"; @@ -265,11 +264,10 @@ export const SingleSidebarProject: React.FC = observer( >
- ios_share +
-
Publish
+
{project.is_deployed ? "Publish settings" : "Publish"}
- {/* */} )} diff --git a/apps/app/store/project-publish.tsx b/apps/app/store/project-publish.tsx index 1b27d5fff..ffc45f546 100644 --- a/apps/app/store/project-publish.tsx +++ b/apps/app/store/project-publish.tsx @@ -4,21 +4,11 @@ import { RootStore } from "./root"; // services import ProjectServices from "services/project-publish.service"; -export type IProjectPublishSettingsViewKeys = - | "list" - | "gantt" - | "kanban" - | "calendar" - | "spreadsheet" - | string; +export type TProjectPublishViews = "list" | "gantt" | "kanban" | "calendar" | "spreadsheet"; -export interface IProjectPublishSettingsViews { - list: boolean; - gantt: boolean; - kanban: boolean; - calendar: boolean; - spreadsheet: boolean; -} +export type TProjectPublishViewsSettings = { + [key in TProjectPublishViews]: boolean; +}; export interface IProjectPublishSettings { id?: string; @@ -26,8 +16,8 @@ export interface IProjectPublishSettings { comments: boolean; reactions: boolean; votes: boolean; - views: IProjectPublishSettingsViews; - inbox: null; + views: TProjectPublishViewsSettings; + inbox: string | null; } export interface IProjectPublishStore { From 5e00ffee0563215f8ee0503dfe242fb16fb3365c Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 30 Aug 2023 17:28:17 +0530 Subject: [PATCH 023/137] fix: bugs on the user profile page (#2018) --- apps/app/components/profile/sidebar.tsx | 19 ++----------- apps/app/contexts/profile-issues-context.tsx | 29 +++++++++++--------- apps/app/hooks/use-profile-issues.tsx | 17 +++++++++++- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/apps/app/components/profile/sidebar.tsx b/apps/app/components/profile/sidebar.tsx index ba574441a..a1236b397 100644 --- a/apps/app/components/profile/sidebar.tsx +++ b/apps/app/components/profile/sidebar.tsx @@ -3,8 +3,6 @@ import Link from "next/link"; import useSWR from "swr"; -// next-themes -import { useTheme } from "next-themes"; // headless ui import { Disclosure, Transition } from "@headlessui/react"; // services @@ -25,8 +23,6 @@ export const ProfileSidebar = () => { const router = useRouter(); const { workspaceSlug, userId } = router.query; - const { theme } = useTheme(); - const { user } = useUser(); const { data: userProjectsData } = useSWR( @@ -56,15 +52,7 @@ export const ProfileSidebar = () => { ]; return ( -
+
{userProjectsData ? ( <>
@@ -127,12 +115,11 @@ export const ProfileSidebar = () => { project.assigned_issues + project.pending_issues + project.completed_issues; - const totalAssignedIssues = totalIssues - project.created_issues; const completedIssuePercentage = - totalAssignedIssues === 0 + project.assigned_issues === 0 ? 0 - : Math.round((project.completed_issues / totalAssignedIssues) * 100); + : Math.round((project.completed_issues / project.assigned_issues) * 100); return ( { const [state, dispatch] = useReducer(reducer, initialState); - const setIssueView = useCallback((property: TIssueViewOptions) => { - dispatch({ - type: "SET_ISSUE_VIEW", - payload: { - issueView: property, - }, - }); - - if (property === "kanban") { + const setIssueView = useCallback( + (property: TIssueViewOptions) => { dispatch({ - type: "SET_GROUP_BY_PROPERTY", + type: "SET_ISSUE_VIEW", payload: { - groupByProperty: "state_detail.group", + issueView: property, }, }); - } - }, []); + + if (property === "kanban" && state.groupByProperty === null) { + dispatch({ + type: "SET_GROUP_BY_PROPERTY", + payload: { + groupByProperty: "state_detail.group", + }, + }); + } + }, + [state] + ); const setGroupByProperty = useCallback((property: TIssueGroupByOptions) => { dispatch({ diff --git a/apps/app/hooks/use-profile-issues.tsx b/apps/app/hooks/use-profile-issues.tsx index 6b4d4abfa..c232199bc 100644 --- a/apps/app/hooks/use-profile-issues.tsx +++ b/apps/app/hooks/use-profile-issues.tsx @@ -71,8 +71,23 @@ const useProfileIssues = (workspaceSlug: string | undefined, userId: string | un allIssues: userProfileIssues, }; + if (groupByProperty === "state_detail.group") { + return userProfileIssues + ? Object.assign( + { + backlog: [], + unstarted: [], + started: [], + completed: [], + cancelled: [], + }, + userProfileIssues + ) + : undefined; + } + return userProfileIssues; - }, [userProfileIssues]); + }, [groupByProperty, userProfileIssues]); useEffect(() => { if (!userId || !filters) return; From 320608ea73ac3bd043250369af5601ac8921540b Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Thu, 31 Aug 2023 11:32:58 +0530 Subject: [PATCH 024/137] chore: return issue votes in public issue list endpoint (#2026) --- apiserver/plane/api/serializers/issue.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 2a75b2f48..1f4d814a4 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -681,6 +681,7 @@ class IssuePublicSerializer(BaseSerializer): project_detail = ProjectLiteSerializer(read_only=True, source="project") state_detail = StateLiteSerializer(read_only=True, source="state") issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True) + votes = IssueVoteSerializer(read_only=True, many=True) class Meta: model = Issue @@ -697,6 +698,7 @@ class IssuePublicSerializer(BaseSerializer): "priority", "target_date", "issue_reactions", + "votes", ] read_only_fields = fields From 38b7f4382fda94d04f9c833adc09263df6b0ea4e Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:41:41 +0530 Subject: [PATCH 025/137] [feat]: Tiptap table integration (#2008) * added basic table support * fixed table position at bottom * fixed image node deletion logic's regression issue * added compatible styles * enabled slash commands * disabled slash command and bubble menu's node selector for table cells * added dropcursor support to type below the table/image * blocked image uploads for handledrop and paste actions --- .../components/tiptap/bubble-menu/index.tsx | 4 +- .../tiptap/bubble-menu/link-selector.tsx | 5 +- .../components/tiptap/extensions/index.tsx | 15 ++- .../tiptap/extensions/table/table-cell.ts | 31 ++++++ .../tiptap/extensions/table/table-header.ts | 7 ++ .../tiptap/extensions/table/table.ts | 9 ++ apps/app/components/tiptap/index.tsx | 2 + .../tiptap/plugins/delete-image.tsx | 2 +- .../tiptap/plugins/upload-image.tsx | 2 - apps/app/components/tiptap/props.tsx | 19 ++++ .../components/tiptap/slash-command/index.tsx | 13 +++ .../components/tiptap/table-menu/index.tsx | 96 +++++++++++++++++++ apps/app/package.json | 5 + apps/app/styles/editor.css | 79 +++++++++++++++ yarn.lock | 25 +++++ 15 files changed, 306 insertions(+), 8 deletions(-) create mode 100644 apps/app/components/tiptap/extensions/table/table-cell.ts create mode 100644 apps/app/components/tiptap/extensions/table/table-header.ts create mode 100644 apps/app/components/tiptap/extensions/table/table.ts create mode 100644 apps/app/components/tiptap/table-menu/index.tsx diff --git a/apps/app/components/tiptap/bubble-menu/index.tsx b/apps/app/components/tiptap/bubble-menu/index.tsx index e68900782..7e72963b6 100644 --- a/apps/app/components/tiptap/bubble-menu/index.tsx +++ b/apps/app/components/tiptap/bubble-menu/index.tsx @@ -77,14 +77,14 @@ export const EditorBubbleMenu: FC = (props: any) => { {...bubbleMenuProps} className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl" > - { setIsNodeSelectorOpen(!isNodeSelectorOpen); setIsLinkSelectorOpen(false); }} - /> + />} >; + setIsOpen: Dispatch> } @@ -52,7 +52,8 @@ export const LinkSelector: FC = ({ editor, isOpen, setIsOpen className="fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 dow-xl animate-in fade-in slide-in-from-top-1" onKeyDown={(e) => { if (e.key === "Enter") { - e.preventDefault(); onLinkSubmit(); + e.preventDefault(); + onLinkSubmit(); } }} > diff --git a/apps/app/components/tiptap/extensions/index.tsx b/apps/app/components/tiptap/extensions/index.tsx index 2c5ffd10a..fa257b20a 100644 --- a/apps/app/components/tiptap/extensions/index.tsx +++ b/apps/app/components/tiptap/extensions/index.tsx @@ -13,6 +13,7 @@ import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; import { lowlight } from "lowlight/lib/core"; import SlashCommand from "../slash-command"; import { InputRule } from "@tiptap/core"; +import Gapcursor from '@tiptap/extension-gapcursor' import ts from "highlight.js/lib/languages/typescript"; @@ -20,6 +21,10 @@ import "highlight.js/styles/github-dark.css"; import UniqueID from "@tiptap-pro/extension-unique-id"; import UpdatedImage from "./updated-image"; import isValidHttpUrl from "../bubble-menu/utils/link-validator"; +import { CustomTableCell } from "./table/table-cell"; +import { Table } from "./table/table"; +import { TableHeader } from "./table/table-header"; +import { TableRow } from "@tiptap/extension-table-row"; lowlight.registerLanguage("ts", ts); @@ -55,7 +60,7 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub codeBlock: false, horizontalRule: false, dropcursor: { - color: "#DBEAFE", + color: "rgba(var(--color-text-100))", width: 2, }, gapcursor: false, @@ -86,6 +91,7 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub class: "mb-6 border-t border-custom-border-300", }, }), + Gapcursor, TiptapLink.configure({ protocols: ["http", "https"], validate: (url) => isValidHttpUrl(url), @@ -104,6 +110,9 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub if (node.type.name === "heading") { return `Heading ${node.attrs.level}`; } + if (node.type.name === "image" || node.type.name === "table") { + return "" + } return "Press '/' for commands..."; }, @@ -134,4 +143,8 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub html: true, transformCopiedText: true, }), + Table, + TableHeader, + CustomTableCell, + TableRow ]; diff --git a/apps/app/components/tiptap/extensions/table/table-cell.ts b/apps/app/components/tiptap/extensions/table/table-cell.ts new file mode 100644 index 000000000..94c5aced2 --- /dev/null +++ b/apps/app/components/tiptap/extensions/table/table-cell.ts @@ -0,0 +1,31 @@ +import { TableCell } from "@tiptap/extension-table-cell"; + +export const CustomTableCell = TableCell.extend({ + addAttributes() { + return { + ...this.parent?.(), + isHeader: { + default: false, + parseHTML: (element) => { isHeader: element.tagName === "TD" }, + renderHTML: (attributes) => { tag: attributes.isHeader ? "th" : "td" } + }, + }; + }, + renderHTML({ HTMLAttributes }) { + if (HTMLAttributes.isHeader) { + return [ + "th", + { + ...HTMLAttributes, + class: `relative ${HTMLAttributes.class}`, + }, + [ + "span", + { class: "absolute top-0 right-0" }, + ], + 0, + ]; + } + return ["td", HTMLAttributes, 0]; + }, +}); diff --git a/apps/app/components/tiptap/extensions/table/table-header.ts b/apps/app/components/tiptap/extensions/table/table-header.ts new file mode 100644 index 000000000..d04fe85d3 --- /dev/null +++ b/apps/app/components/tiptap/extensions/table/table-header.ts @@ -0,0 +1,7 @@ +import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header"; + +const TableHeader = BaseTableHeader.extend({ + content: "paragraph" +}); + +export { TableHeader }; diff --git a/apps/app/components/tiptap/extensions/table/table.ts b/apps/app/components/tiptap/extensions/table/table.ts new file mode 100644 index 000000000..b05dedb3b --- /dev/null +++ b/apps/app/components/tiptap/extensions/table/table.ts @@ -0,0 +1,9 @@ +import { Table as BaseTable } from "@tiptap/extension-table"; + +const Table = BaseTable.configure({ + resizable: true, + cellMinWidth: 100, + allowTableNodeSelection: true +}); + +export { Table }; diff --git a/apps/app/components/tiptap/index.tsx b/apps/app/components/tiptap/index.tsx index f0315cad4..869e44aad 100644 --- a/apps/app/components/tiptap/index.tsx +++ b/apps/app/components/tiptap/index.tsx @@ -5,6 +5,7 @@ import { TiptapExtensions } from "./extensions"; import { TiptapEditorProps } from "./props"; import { useImperativeHandle, useRef, forwardRef } from "react"; import { ImageResizer } from "./extensions/image-resize"; +import { TableMenu } from "./table-menu"; export interface ITipTapRichTextEditor { value: string; @@ -92,6 +93,7 @@ const Tiptap = (props: ITipTapRichTextEditor) => { {editor && }
+ {editor?.isActive("table") && } {editor?.isActive("image") && }
diff --git a/apps/app/components/tiptap/plugins/delete-image.tsx b/apps/app/components/tiptap/plugins/delete-image.tsx index 57ab65c63..262a3f591 100644 --- a/apps/app/components/tiptap/plugins/delete-image.tsx +++ b/apps/app/components/tiptap/plugins/delete-image.tsx @@ -16,6 +16,7 @@ const TrackImageDeletionPlugin = () => oldState.doc.descendants((oldNode, oldPos) => { if (oldNode.type.name !== 'image') return; + if (oldPos < 0 || oldPos > newState.doc.content.size) return; if (!newState.doc.resolve(oldPos).parent) return; const newNode = newState.doc.nodeAt(oldPos); @@ -28,7 +29,6 @@ const TrackImageDeletionPlugin = () => nodeExists = true; } }); - if (!nodeExists) { removedImages.push(oldNode as ProseMirrorNode); } diff --git a/apps/app/components/tiptap/plugins/upload-image.tsx b/apps/app/components/tiptap/plugins/upload-image.tsx index 0657bc82b..a13f8e18a 100644 --- a/apps/app/components/tiptap/plugins/upload-image.tsx +++ b/apps/app/components/tiptap/plugins/upload-image.tsx @@ -60,8 +60,6 @@ function findPlaceholder(state: EditorState, id: {}) { export async function startImageUpload(file: File, view: EditorView, pos: number, workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) { if (!file.type.includes("image/")) { return; - } else if (file.size / 1024 / 1024 > 20) { - return; } const id = {}; diff --git a/apps/app/components/tiptap/props.tsx b/apps/app/components/tiptap/props.tsx index d50fc29b0..69cddca1f 100644 --- a/apps/app/components/tiptap/props.tsx +++ b/apps/app/components/tiptap/props.tsx @@ -1,5 +1,6 @@ import { EditorProps } from "@tiptap/pm/view"; import { startImageUpload } from "./plugins/upload-image"; +import { findTableAncestor } from "./table-menu"; export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void): EditorProps { return { @@ -18,6 +19,15 @@ export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSu }, }, handlePaste: (view, event) => { + if (typeof window !== "undefined") { + const selection: any = window?.getSelection(); + if (selection.rangeCount !== 0) { + const range = selection.getRangeAt(0); + if (findTableAncestor(range.startContainer)) { + return; + } + } + } if ( event.clipboardData && event.clipboardData.files && @@ -32,6 +42,15 @@ export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSu return false; }, handleDrop: (view, event, _slice, moved) => { + if (typeof window !== "undefined") { + const selection: any = window?.getSelection(); + if (selection.rangeCount !== 0) { + const range = selection.getRangeAt(0); + if (findTableAncestor(range.startContainer)) { + return; + } + } + } if ( !moved && event.dataTransfer && diff --git a/apps/app/components/tiptap/slash-command/index.tsx b/apps/app/components/tiptap/slash-command/index.tsx index 38f5c9c0a..c843f0762 100644 --- a/apps/app/components/tiptap/slash-command/index.tsx +++ b/apps/app/components/tiptap/slash-command/index.tsx @@ -15,6 +15,7 @@ import { MinusSquare, CheckSquare, ImageIcon, + Table, } from "lucide-react"; import { startImageUpload } from "../plugins/upload-image"; import { cn } from "../utils"; @@ -46,6 +47,9 @@ const Command = Extension.create({ return [ Suggestion({ editor: this.editor, + allow({ editor }) { + return !editor.isActive("table"); + }, ...this.options.suggestion, }), ]; @@ -117,6 +121,15 @@ const getSuggestionItems = (workspaceSlug: string, setIsSubmitting?: (isSubmitti editor.chain().focus().deleteRange(range).setHorizontalRule().run(); }, }, + { + title: "Table", + description: "Create a Table", + searchTerms: ["table", "cell", "db", "data", "tabular"], + icon: , + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); + }, + }, { title: "Numbered List", description: "Create a list with numbering.", diff --git a/apps/app/components/tiptap/table-menu/index.tsx b/apps/app/components/tiptap/table-menu/index.tsx new file mode 100644 index 000000000..878679a29 --- /dev/null +++ b/apps/app/components/tiptap/table-menu/index.tsx @@ -0,0 +1,96 @@ +import { useState, useEffect } from "react"; +import { Rows, Columns, ToggleRight } from "lucide-react"; +import { cn } from "../utils"; + +interface TableMenuItem { + name: string; + command: () => void; + icon: any; +} + +export const findTableAncestor = (node: Node | null): HTMLTableElement | null => { + while (node !== null && node.nodeName !== "TABLE") { + node = node.parentNode; + } + return node as HTMLTableElement; +}; + +export const TableMenu = ({ editor }: { editor: any }) => { + const [tableLocation, setTableLocation] = useState({ bottom: 0, left: 0 }); + const items: TableMenuItem[] = [ + { + name: "Insert Column right", + command: () => editor.chain().focus().addColumnBefore().run(), + icon: Columns, + }, + { + name: "Insert Row below", + command: () => editor.chain().focus().addRowAfter().run(), + icon: Rows, + }, + { + name: "Delete Column", + command: () => editor.chain().focus().deleteColumn().run(), + icon: Columns, + }, + { + name: "Delete Rows", + command: () => editor.chain().focus().deleteRow().run(), + icon: Rows, + }, + { + name: "Toggle Header Row", + command: () => editor.chain().focus().toggleHeaderRow().run(), + icon: ToggleRight, + } + + ]; + + useEffect(() => { + if (typeof window !== "undefined") { + const handleWindowClick = () => { + const selection: any = window?.getSelection(); + if (selection.rangeCount !== 0) { + const range = selection.getRangeAt(0); + const tableNode = findTableAncestor(range.startContainer); + if (tableNode) { + const tableRect = tableNode.getBoundingClientRect(); + const tableCenter = tableRect.left + tableRect.width / 2; + const menuWidth = 45; + const menuLeft = tableCenter - menuWidth / 2; + const tableBottom = tableRect.bottom; + setTableLocation({ bottom: tableBottom, left: menuLeft }); + } + } + } + + window.addEventListener("click", handleWindowClick); + + return () => { + window.removeEventListener("click", handleWindowClick); + }; + } + }, [tableLocation]); + + return ( +
+ {items.map((item, index) => ( + + ))} +
+ ); +}; diff --git a/apps/app/package.json b/apps/app/package.json index 578a95716..b9f3bf25e 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -30,11 +30,16 @@ "@tiptap-pro/extension-unique-id": "^2.1.0", "@tiptap/extension-code-block-lowlight": "^2.0.4", "@tiptap/extension-color": "^2.0.4", + "@tiptap/extension-gapcursor": "^2.1.7", "@tiptap/extension-highlight": "^2.0.4", "@tiptap/extension-horizontal-rule": "^2.0.4", "@tiptap/extension-image": "^2.0.4", "@tiptap/extension-link": "^2.0.4", "@tiptap/extension-placeholder": "^2.0.4", + "@tiptap/extension-table": "^2.1.6", + "@tiptap/extension-table-cell": "^2.1.6", + "@tiptap/extension-table-header": "^2.1.6", + "@tiptap/extension-table-row": "^2.1.6", "@tiptap/extension-task-item": "^2.0.4", "@tiptap/extension-task-list": "^2.0.4", "@tiptap/extension-text-style": "^2.0.4", diff --git a/apps/app/styles/editor.css b/apps/app/styles/editor.css index 57c23c911..3332185d2 100644 --- a/apps/app/styles/editor.css +++ b/apps/app/styles/editor.css @@ -30,6 +30,10 @@ } } +.ProseMirror-gapcursor:after { + border-top: 1px solid rgb(var(--color-text-100)) !important; +} + /* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */ ul[data-type="taskList"] li > label { @@ -150,3 +154,78 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { transform: rotate(360deg); } } + +#tiptap-container { + table { + border-collapse: collapse; + table-layout: fixed; + margin: 0; + margin-bottom: 1.5rem; + margin-top: 1.5rem; + border: 2px solid rgb(var(--color-border-100)); + width: 100%; + box-shadow: 0 0 10px rgba(0,0,0,0.1); + + td, + th { + min-width: 1em; + border: 2px solid rgb(var(--color-border-400)); + padding: 10px 15px; + vertical-align: top; + box-sizing: border-box; + position: relative; + transition: background-color 0.3s ease; + + > * { + margin-bottom: 0; + } + } + + th { + font-weight: bold; + text-align: left; + background-color: rgb(var(--color-primary-300)); + } + + td:hover { + background-color: rgba(var(--color-primary-300), 0.1); + } + + .selectedCell:after { + z-index: 2; + position: absolute; + content: ""; + left: 0; right: 0; top: 0; bottom: 0; + background-color: rgba(var(--color-primary-300), 0.1); + pointer-events: none; + } + + .column-resize-handle { + position: absolute; + right: -2px; + top: 0; + bottom: -2px; + width: 2px; + background-color: rgb(var(--color-primary-400)); + pointer-events: none; + } + } +} + +.tableWrapper { + overflow-x: auto; +} + +.resize-cursor { + cursor: ew-resize; + cursor: col-resize; +} + +.ProseMirror table * p { + padding: 0px 1px; + margin: 6px 2px; +} + +.ProseMirror table * .is-empty::before { + opacity: 0; +} diff --git a/yarn.lock b/yarn.lock index ac134d60f..3eafeb5b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2282,6 +2282,11 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.4.tgz#c100a792fd41535ad6382aa8133d0d9c0b2cb2b8" integrity sha512-VxmKfBQjSSu1mNvHlydA4dJW/zawGKyqmnryiFNcUV9s+/HWLR5i9SiUl4wJM/B8sG8cQxClne5/LrCAeGNYuA== +"@tiptap/extension-gapcursor@^2.1.7": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.1.7.tgz#5c0303ba37b4c066f3a3c5835fd0b298f0d3e919" + integrity sha512-7eoInzzk1sssoD3RMkwFC86U15Ja4ANve+8wIC+xhN4R3Oe3PY3lFbp1GQxCmaJj8b3rtjNKIQZ2zO0PH58afA== + "@tiptap/extension-hard-break@^2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.0.4.tgz#a4f70fa9a473270f7ec89f20a14b9122af5657bc" @@ -2349,6 +2354,26 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.0.4.tgz#13286dcf8780c55610ed65b24238b8395a5be824" integrity sha512-Men7LK6N/Dh3/G4/z2Z9WkDHM2Gxx1XyxYix2ZMf5CnqY37SeDNUnGDqit65pdIN3Y/TQnOZTkKSBilSAtXfJA== +"@tiptap/extension-table-cell@^2.1.6": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-cell/-/extension-table-cell-2.1.7.tgz#87841144b8368c9611ad46f2134b637e2c33c8bc" + integrity sha512-p3e4FNdbKVIjOLHDcXrRtlP6FYPoN6hBUFjq6QZbf5g4+ao2Uq4bQCL+eKbYMxUVERl8g/Qu9X+jG99fVsBDjA== + +"@tiptap/extension-table-header@^2.1.6": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-header/-/extension-table-header-2.1.7.tgz#4757834655e2c4edffa65bc6f6807eb59401e0d8" + integrity sha512-rolSUQxFJf/CEj2XBJpeMsLiLHASKrVIzZ2A/AZ9pT6WpFqmECi8r9xyutpJpx21n2Hrk46Y+uGFOKhyvbZ5ug== + +"@tiptap/extension-table-row@^2.1.6": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-row/-/extension-table-row-2.1.7.tgz#f736a61035b271423ef18f65a25f8d1e240263a1" + integrity sha512-DBCaEMEuCCoOmr4fdDfp2jnmyWPt672rmCZ5WUuenJ47Cy4Ox2dV+qk5vBZ/yDQcq12WvzLMhdSnAo9pMMMa6Q== + +"@tiptap/extension-table@^2.1.6": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.1.7.tgz#c8a83744f60c76ae1e41438b04d5ac9e984afa66" + integrity sha512-nlKs35vTQOFW9lfw76S7kJvqVJAfHUlz1muQgWT0gNUlKJYINMXjUIg4Wcx8LTaITCCkp0lMGrLETGRNI+RyxA== + "@tiptap/extension-task-item@^2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@tiptap/extension-task-item/-/extension-task-item-2.0.4.tgz#71f46d35ac629ca10c5c23d4ad170007338a436e" From af929ab7414463a624a04ad3e4d3cc95239e7c21 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Thu, 31 Aug 2023 16:30:28 +0530 Subject: [PATCH 026/137] style: tiptap table (#2033) --- apps/app/components/tiptap/index.tsx | 6 +- .../components/tiptap/table-menu/index.tsx | 113 +++++++++++------- apps/app/styles/editor.css | 22 ++-- apps/app/styles/globals.css | 4 + 4 files changed, 90 insertions(+), 55 deletions(-) diff --git a/apps/app/components/tiptap/index.tsx b/apps/app/components/tiptap/index.tsx index 869e44aad..2ab6bf288 100644 --- a/apps/app/components/tiptap/index.tsx +++ b/apps/app/components/tiptap/index.tsx @@ -76,8 +76,8 @@ const Tiptap = (props: ITipTapRichTextEditor) => { const editorClassNames = `relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md ${noBorder ? "" : "border border-custom-border-200"} ${ - borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" - } ${customClassName}`; + borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" + } ${customClassName}`; if (!editor) return null; editorRef.current = editor; @@ -93,7 +93,7 @@ const Tiptap = (props: ITipTapRichTextEditor) => { {editor && }
- {editor?.isActive("table") && } + {editor?.isActive("image") && }
diff --git a/apps/app/components/tiptap/table-menu/index.tsx b/apps/app/components/tiptap/table-menu/index.tsx index 878679a29..0da68410e 100644 --- a/apps/app/components/tiptap/table-menu/index.tsx +++ b/apps/app/components/tiptap/table-menu/index.tsx @@ -1,11 +1,13 @@ import { useState, useEffect } from "react"; import { Rows, Columns, ToggleRight } from "lucide-react"; import { cn } from "../utils"; +import { Tooltip } from "components/ui"; interface TableMenuItem { - name: string; command: () => void; icon: any; + key: string; + name: string; } export const findTableAncestor = (node: Node | null): HTMLTableElement | null => { @@ -17,79 +19,108 @@ export const findTableAncestor = (node: Node | null): HTMLTableElement | null => export const TableMenu = ({ editor }: { editor: any }) => { const [tableLocation, setTableLocation] = useState({ bottom: 0, left: 0 }); + const isOpen = editor?.isActive("table"); + const items: TableMenuItem[] = [ { - name: "Insert Column right", command: () => editor.chain().focus().addColumnBefore().run(), icon: Columns, + key: "insert-column-right", + name: "Insert 1 column right", }, { - name: "Insert Row below", command: () => editor.chain().focus().addRowAfter().run(), icon: Rows, + key: "insert-row-below", + name: "Insert 1 row below", }, { - name: "Delete Column", command: () => editor.chain().focus().deleteColumn().run(), icon: Columns, + key: "delete-column", + name: "Delete column", }, { - name: "Delete Rows", command: () => editor.chain().focus().deleteRow().run(), icon: Rows, + key: "delete-row", + name: "Delete row", }, { - name: "Toggle Header Row", command: () => editor.chain().focus().toggleHeaderRow().run(), icon: ToggleRight, - } - + key: "toggle-header-row", + name: "Toggle header row", + }, ]; useEffect(() => { - if (typeof window !== "undefined") { - const handleWindowClick = () => { - const selection: any = window?.getSelection(); - if (selection.rangeCount !== 0) { - const range = selection.getRangeAt(0); - const tableNode = findTableAncestor(range.startContainer); - if (tableNode) { - const tableRect = tableNode.getBoundingClientRect(); - const tableCenter = tableRect.left + tableRect.width / 2; - const menuWidth = 45; - const menuLeft = tableCenter - menuWidth / 2; - const tableBottom = tableRect.bottom; - setTableLocation({ bottom: tableBottom, left: menuLeft }); + if (!window) return; + + const handleWindowClick = () => { + const selection: any = window?.getSelection(); + + if (selection.rangeCount !== 0) { + const range = selection.getRangeAt(0); + const tableNode = findTableAncestor(range.startContainer); + + let parent = tableNode?.parentElement; + + if (tableNode) { + const tableRect = tableNode.getBoundingClientRect(); + const tableCenter = tableRect.left + tableRect.width / 2; + const menuWidth = 45; + const menuLeft = tableCenter - menuWidth / 2; + const tableBottom = tableRect.bottom; + + setTableLocation({ bottom: tableBottom, left: menuLeft }); + + while (parent) { + if (!parent.classList.contains("disable-scroll")) + parent.classList.add("disable-scroll"); + parent = parent.parentElement; } + } else { + const scrollDisabledContainers = document.querySelectorAll(".disable-scroll"); + + scrollDisabledContainers.forEach((container) => { + container.classList.remove("disable-scroll"); + }); } } + }; - window.addEventListener("click", handleWindowClick); + window.addEventListener("click", handleWindowClick); - return () => { - window.removeEventListener("click", handleWindowClick); - }; - } - }, [tableLocation]); + return () => { + window.removeEventListener("click", handleWindowClick); + }; + }, [tableLocation, editor]); return (
{items.map((item, index) => ( - + + + ))}
); diff --git a/apps/app/styles/editor.css b/apps/app/styles/editor.css index 3332185d2..9da250dd1 100644 --- a/apps/app/styles/editor.css +++ b/apps/app/styles/editor.css @@ -144,7 +144,7 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { height: 20px; border-radius: 50%; border: 3px solid rgba(var(--color-text-200)); - border-top-color: rgba(var(--color-text-800)); + border-top-color: rgba(var(--color-text-800)); animation: spinning 0.6s linear infinite; } } @@ -160,16 +160,13 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { border-collapse: collapse; table-layout: fixed; margin: 0; - margin-bottom: 1.5rem; - margin-top: 1.5rem; - border: 2px solid rgb(var(--color-border-100)); + border: 1px solid rgb(var(--color-border-200)); width: 100%; - box-shadow: 0 0 10px rgba(0,0,0,0.1); td, th { min-width: 1em; - border: 2px solid rgb(var(--color-border-400)); + border: 1px solid rgb(var(--color-border-200)); padding: 10px 15px; vertical-align: top; box-sizing: border-box; @@ -183,8 +180,8 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { th { font-weight: bold; - text-align: left; - background-color: rgb(var(--color-primary-300)); + text-align: left; + background-color: rgb(var(--color-primary-100)); } td:hover { @@ -195,7 +192,10 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { z-index: 2; position: absolute; content: ""; - left: 0; right: 0; top: 0; bottom: 0; + left: 0; + right: 0; + top: 0; + bottom: 0; background-color: rgba(var(--color-primary-300), 0.1); pointer-events: none; } @@ -222,8 +222,8 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { } .ProseMirror table * p { - padding: 0px 1px; - margin: 6px 2px; + padding: 0px 1px; + margin: 6px 2px; } .ProseMirror table * .is-empty::before { diff --git a/apps/app/styles/globals.css b/apps/app/styles/globals.css index cdb67cbc5..3de1e2c57 100644 --- a/apps/app/styles/globals.css +++ b/apps/app/styles/globals.css @@ -355,3 +355,7 @@ body { .bp4-overlay-content { z-index: 555 !important; } + +.disable-scroll { + overflow: hidden !important; +} From b496a62540ca17f74002862857f4dea8b52f8579 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Thu, 31 Aug 2023 19:07:56 +0530 Subject: [PATCH 027/137] fix: subscribed issues are filtering (#2037) --- apiserver/plane/api/views/issue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index ac69e9d8d..cbcd40f04 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -337,7 +337,7 @@ class UserWorkSpaceIssues(BaseAPIView): issue_queryset = ( Issue.issue_objects.filter( - (Q(assignees__in=[request.user]) | Q(created_by=request.user)), + (Q(assignees__in=[request.user]) | Q(created_by=request.user) | Q(issue_subscribers__subscriber=request.user)), workspace__slug=slug, ) .annotate( From 099bce87b5c02a6e2e0773ca677da06e12a4d38b Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 1 Sep 2023 00:08:40 +0530 Subject: [PATCH 028/137] chore: public board endpoints (#2030) --- apiserver/plane/api/serializers/issue.py | 5 ++-- apiserver/plane/api/views/issue.py | 37 +++++++++++++++--------- apiserver/plane/db/models/issue.py | 2 +- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 1f4d814a4..6cd06a767 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -680,7 +680,7 @@ class IssueLiteSerializer(BaseSerializer): class IssuePublicSerializer(BaseSerializer): project_detail = ProjectLiteSerializer(read_only=True, source="project") state_detail = StateLiteSerializer(read_only=True, source="state") - issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True) + reactions = IssueReactionLiteSerializer(read_only=True, many=True, source="issue_reactions") votes = IssueVoteSerializer(read_only=True, many=True) class Meta: @@ -697,12 +697,13 @@ class IssuePublicSerializer(BaseSerializer): "workspace", "priority", "target_date", - "issue_reactions", + "reactions", "votes", ] read_only_fields = fields + class IssueSubscriberSerializer(BaseSerializer): class Meta: model = IssueSubscriber diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index cbcd40f04..74b574423 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -28,7 +28,7 @@ from django.conf import settings from rest_framework.response import Response from rest_framework import status from rest_framework.parsers import MultiPartParser, FormParser -from rest_framework.permissions import AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated from sentry_sdk import capture_exception # Module imports @@ -1504,7 +1504,7 @@ class CommentReactionViewSet(BaseViewSet): { "reaction": str(reaction_code), "identifier": str(comment_reaction.id), - "comment_id": str(comment_id) + "comment_id": str(comment_id), } ), ) @@ -1532,6 +1532,18 @@ class IssueCommentPublicViewSet(BaseViewSet): "workspace__id", ] + def get_permissions(self): + if self.action in ["list", "retrieve"]: + self.permission_classes = [ + AllowAny, + ] + else: + self.permission_classes = [ + IsAuthenticated, + ] + + return super(IssueCommentPublicViewSet, self).get_permissions() + def get_queryset(self): project_deploy_board = ProjectDeployBoard.objects.get( workspace__slug=self.kwargs.get("slug"), @@ -1741,7 +1753,7 @@ class IssueReactionPublicViewSet(BaseViewSet): issue_id=str(self.kwargs.get("issue_id", None)), project_id=str(self.kwargs.get("project_id", None)), current_instance=None, - ) + ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except ProjectDeployBoard.DoesNotExist: @@ -1855,7 +1867,7 @@ class CommentReactionPublicViewSet(BaseViewSet): issue_id=None, project_id=str(self.kwargs.get("project_id", None)), current_instance=None, - ) + ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except IssueComment.DoesNotExist: @@ -1903,7 +1915,7 @@ class CommentReactionPublicViewSet(BaseViewSet): { "reaction": str(reaction_code), "identifier": str(comment_reaction.id), - "comment_id": str(comment_id) + "comment_id": str(comment_id), } ), ) @@ -1953,13 +1965,13 @@ class IssueVotePublicViewSet(BaseViewSet): issue_vote.vote = request.data.get("vote", 1) issue_vote.save() issue_activity.delay( - type="issue_vote.activity.created", - requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=None, - ) + type="issue_vote.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + ) serializer = IssueVoteSerializer(issue_vote) return Response(serializer.data, status=status.HTTP_201_CREATED) except Exception as e: @@ -2170,4 +2182,3 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) - diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 1633cbaf9..8f085b2a2 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -293,7 +293,7 @@ class IssueComment(ProjectBaseModel): comment_json = models.JSONField(blank=True, default=dict) comment_html = models.TextField(blank=True, default="

") attachments = ArrayField(models.URLField(), size=10, blank=True, default=list) - issue = models.ForeignKey(Issue, on_delete=models.CASCADE) + issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_comments") # System can also create comment actor = models.ForeignKey( settings.AUTH_USER_MODEL, From eab1d9329bb0aa0e44c677c8513ce12da82f9661 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Fri, 1 Sep 2023 10:59:17 +0530 Subject: [PATCH 029/137] feat: editor for issue description (#2038) --- apps/app/pages/[workspaceSlug]/editor.tsx | 192 ++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 apps/app/pages/[workspaceSlug]/editor.tsx diff --git a/apps/app/pages/[workspaceSlug]/editor.tsx b/apps/app/pages/[workspaceSlug]/editor.tsx new file mode 100644 index 000000000..73f0932ea --- /dev/null +++ b/apps/app/pages/[workspaceSlug]/editor.tsx @@ -0,0 +1,192 @@ +import { TipTapEditor } from "components/tiptap"; +import type { NextPage } from "next"; +import { useCallback, useEffect, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import issuesService from "services/issues.service"; +import { ICurrentUserResponse, IIssue } from "types"; +import useReloadConfirmations from "hooks/use-reload-confirmation"; +import { Spinner } from "components/ui"; +import Image404 from "public/404.svg"; +import DefaultLayout from "layouts/default-layout"; +import Image from "next/image"; +import userService from "services/user.service"; +import { useRouter } from "next/router"; + +const Editor: NextPage = () => { + const [user, setUser] = useState(); + const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); + const [isLoading, setIsLoading] = useState("false"); + const { setShowAlert } = useReloadConfirmations(); + const [cookies, setCookies] = useState({}); + const [issueDetail, setIssueDetail] = useState(null); + const router = useRouter(); + const { editable } = router.query; + const { + handleSubmit, + watch, + setValue, + control, + formState: { errors }, + } = useForm({ + defaultValues: { + name: "", + description: "", + description_html: "", + }, + }); + + const getCookies = () => { + const cookies = document.cookie.split(";"); + const cookieObj: any = {}; + cookies.forEach((cookie) => { + const cookieArr = cookie.split("="); + cookieObj[cookieArr[0].trim()] = cookieArr[1]; + }); + + setCookies(cookieObj); + return cookieObj; + }; + + const getIssueDetail = async (cookiesData: any) => { + try { + setIsLoading("true"); + const userData = await userService.currentUser(); + setUser(userData); + const issueDetail = await issuesService.retrieve( + cookiesData.MOBILE_slug, + cookiesData.MOBILE_project_id, + cookiesData.MOBILE_issue_id + ); + setIssueDetail(issueDetail); + setIsLoading("false"); + setValue("description_html", issueDetail.description_html); + setValue("description", issueDetail.description); + } catch (e) { + setIsLoading("error"); + console.log(e); + } + }; + useEffect(() => { + const cookiesData = getCookies(); + + getIssueDetail(cookiesData); + }, []); + + useEffect(() => { + if (isSubmitting === "submitted") { + setShowAlert(false); + setTimeout(async () => { + setIsSubmitting("saved"); + }, 2000); + } else if (isSubmitting === "submitting") { + setShowAlert(true); + } + }, [isSubmitting, setShowAlert]); + + const submitChanges = async ( + formData: Partial, + workspaceSlug: string, + projectId: string, + issueId: string + ) => { + if (!workspaceSlug || !projectId || !issueId) return; + + const payload: Partial = { + ...formData, + }; + + delete payload.blocker_issues; + delete payload.blocked_issues; + await issuesService + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) + .catch((e) => { + console.log(e); + }); + }; + + const handleDescriptionFormSubmit = useCallback( + async (formData: Partial) => { + if (!formData) return; + + await submitChanges( + { + name: issueDetail?.name ?? "", + description: formData.description ?? "", + description_html: formData.description_html ?? "

", + }, + cookies.MOBILE_slug, + cookies.MOBILE_project_id, + cookies.MOBILE_issue_id + ); + }, + [submitChanges] + ); + + return isLoading === "error" ? ( + + ) : isLoading === "true" ? ( +
+ +
+ ) : ( +
+ ( + { + setShowAlert(true); + setIsSubmitting("submitting"); + onChange(description_html); + setValue("description", description); + handleSubmit(handleDescriptionFormSubmit)().finally(() => { + setIsSubmitting("submitted"); + }); + }} + /> + )} + /> +
+ {isSubmitting === "submitting" ? "Saving..." : "Saved"} +
+
+ ); +}; + +const ErrorEncountered: NextPage = () => ( + +
+
+
+ +
+
+

Oops! Something went wrong.

+
+
+
+
+); + +export default Editor; From 3a0d96a48d9756817d168b6774faea3fb6dbab6f Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 1 Sep 2023 11:21:34 +0530 Subject: [PATCH 030/137] chore: cycle endpoint to return display name as well in the assignee distribution (#2041) * chore: cycle endpoint to return display name as well in the assignee distribution * fix: value error --- apiserver/plane/api/views/cycle.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index a3d89fa81..3dca6c312 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -191,11 +191,10 @@ class CycleViewSet(BaseViewSet): workspace__slug=slug, project_id=project_id, ) - .annotate(first_name=F("assignees__first_name")) - .annotate(last_name=F("assignees__last_name")) + .annotate(display_name=F("assignees__display_name")) .annotate(assignee_id=F("assignees__id")) .annotate(avatar=F("assignees__avatar")) - .values("first_name", "last_name", "assignee_id", "avatar") + .values("display_name", "assignee_id", "avatar") .annotate(total_issues=Count("assignee_id")) .annotate( completed_issues=Count( @@ -209,7 +208,7 @@ class CycleViewSet(BaseViewSet): filter=Q(completed_at__isnull=True), ) ) - .order_by("first_name", "last_name") + .order_by("display_name") ) label_distribution = ( From 0d4bcd2758671f2b68508a8ec1cafa5158311342 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 1 Sep 2023 11:23:43 +0530 Subject: [PATCH 031/137] fix: Gantt chart bugs (#2024) * fix: only left mouse button should trigger all the events * fix: extra block shadow --- .../app/components/gantt-chart/helpers/draggable.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/app/components/gantt-chart/helpers/draggable.tsx b/apps/app/components/gantt-chart/helpers/draggable.tsx index 20423ff59..b665bf5d3 100644 --- a/apps/app/components/gantt-chart/helpers/draggable.tsx +++ b/apps/app/components/gantt-chart/helpers/draggable.tsx @@ -73,9 +73,11 @@ export const ChartDraggable: React.FC = ({ }; // handle block resize from the left end - const handleBlockLeftResize = () => { + const handleBlockLeftResize = (e: React.MouseEvent) => { if (!currentViewData || !resizableRef.current || !block.position) return; + if (e.button !== 0) return; + const resizableDiv = resizableRef.current; const columnWidth = currentViewData.data.width; @@ -126,9 +128,11 @@ export const ChartDraggable: React.FC = ({ }; // handle block resize from the right end - const handleBlockRightResize = () => { + const handleBlockRightResize = (e: React.MouseEvent) => { if (!currentViewData || !resizableRef.current || !block.position) return; + if (e.button !== 0) return; + const resizableDiv = resizableRef.current; const columnWidth = currentViewData.data.width; @@ -173,6 +177,8 @@ export const ChartDraggable: React.FC = ({ const handleBlockMove = (e: React.MouseEvent) => { if (!enableBlockMove || !currentViewData || !resizableRef.current || !block.position) return; + if (e.button !== 0) return; + e.preventDefault(); e.stopPropagation(); @@ -266,7 +272,7 @@ export const ChartDraggable: React.FC = ({
Date: Fri, 1 Sep 2023 13:20:52 +0530 Subject: [PATCH 032/137] dev: migrations for v0.12 release (#2044) --- ..._alter_analyticview_created_by_and_more.py | 35 ++++++++++++++++++- ..._together_alter_issuevote_vote_and_more.py | 26 -------------- 2 files changed, 34 insertions(+), 27 deletions(-) delete mode 100644 apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py diff --git a/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py b/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py index f7d6a979d..7db02c53d 100644 --- a/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py +++ b/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py @@ -31,5 +31,38 @@ class Migration(migrations.Migration): name='title', field=models.CharField(blank=True, max_length=255, null=True), ), - migrations.RunPython(update_user_timezones) + migrations.RunPython(update_user_timezones), + migrations.AlterUniqueTogether( + name='issuevote', + unique_together=set(), + ), + migrations.AlterField( + model_name='issuevote', + name='vote', + field=models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')], default=1), + ), + migrations.AlterUniqueTogether( + name='issuevote', + unique_together={('issue', 'actor', 'vote')}, + ), + migrations.CreateModel( + name='ProjectPublicMember', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='public_project_members', to=settings.AUTH_USER_MODEL)), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')), + ], + options={ + 'verbose_name': 'Project Public Member', + 'verbose_name_plural': 'Project Public Members', + 'db_table': 'project_public_members', + 'ordering': ('-created_at',), + 'unique_together': {('project', 'member')}, + }, + ), ] diff --git a/apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py b/apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py deleted file mode 100644 index d8063acc0..000000000 --- a/apiserver/plane/db/migrations/0042_alter_issuevote_unique_together_alter_issuevote_vote_and_more.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 4.2.3 on 2023-08-29 07:58 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('db', '0041_cycle_sort_order_issuecomment_access_and_more'), - ] - - operations = [ - migrations.AlterUniqueTogether( - name='issuevote', - unique_together=set(), - ), - migrations.AlterField( - model_name='issuevote', - name='vote', - field=models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')], default=1), - ), - migrations.AlterUniqueTogether( - name='issuevote', - unique_together={('issue', 'actor', 'vote')}, - ), - ] From 4ba3ef5c2488f023d1e7d1cd4dc1f8b620f7186a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 1 Sep 2023 13:52:55 +0530 Subject: [PATCH 033/137] fix: peek overview bugs (#2043) * fix: side peek modal shaking * refactor: peek overview layout * fix: date selector, activity mutation * fix: delete issue handler * fix: assignees mutation --- .../components/issues/delete-issue-modal.tsx | 11 +- .../issues/peek-overview/issue-properties.tsx | 60 +++----- .../issues/peek-overview/layout.tsx | 145 +++++++++--------- .../issues/sidebar-select/assignee.tsx | 5 +- .../issues/sidebar-select/priority.tsx | 2 +- .../components/ui/dropdowns/custom-select.tsx | 2 +- apps/app/store/issues.ts | 5 +- 7 files changed, 120 insertions(+), 110 deletions(-) diff --git a/apps/app/components/issues/delete-issue-modal.tsx b/apps/app/components/issues/delete-issue-modal.tsx index f46dae9aa..d847126ed 100644 --- a/apps/app/components/issues/delete-issue-modal.tsx +++ b/apps/app/components/issues/delete-issue-modal.tsx @@ -33,10 +33,17 @@ type Props = { isOpen: boolean; handleClose: () => void; data: IIssue | null; + onSubmit?: () => Promise; user: ICurrentUserResponse | undefined; }; -export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, user }) => { +export const DeleteIssueModal: React.FC = ({ + isOpen, + handleClose, + data, + onSubmit, + user, +}) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); @@ -116,6 +123,8 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, u else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, params)); } + if (onSubmit) onSubmit(); + handleClose(); setToastAlert({ title: "Success", diff --git a/apps/app/components/issues/peek-overview/issue-properties.tsx b/apps/app/components/issues/peek-overview/issue-properties.tsx index 2c8b4d572..1f2d618ac 100644 --- a/apps/app/components/issues/peek-overview/issue-properties.tsx +++ b/apps/app/components/issues/peek-overview/issue-properties.tsx @@ -103,7 +103,7 @@ export const PeekOverviewIssueProperties: React.FC = ({
handleUpdateIssue({ assignees_list: val })} disabled={readOnly} /> @@ -128,23 +128,18 @@ export const PeekOverviewIssueProperties: React.FC = ({ Start date
- {issue.start_date ? ( - - handleUpdateIssue({ - start_date: val, - }) - } - className="bg-custom-background-100" - wrapperClassName="w-full" - maxDate={maxDate ?? undefined} - disabled={readOnly} - /> - ) : ( - Empty - )} + + handleUpdateIssue({ + start_date: val, + }) + } + className="bg-custom-background-80 border-none" + maxDate={maxDate ?? undefined} + disabled={readOnly} + />
@@ -153,23 +148,18 @@ export const PeekOverviewIssueProperties: React.FC = ({ Due date
- {issue.target_date ? ( - - handleUpdateIssue({ - target_date: val, - }) - } - className="bg-custom-background-100" - wrapperClassName="w-full" - minDate={minDate ?? undefined} - disabled={readOnly} - /> - ) : ( - Empty - )} + + handleUpdateIssue({ + target_date: val, + }) + } + className="bg-custom-background-80 border-none" + minDate={minDate ?? undefined} + disabled={readOnly} + />
{/*
diff --git a/apps/app/components/issues/peek-overview/layout.tsx b/apps/app/components/issues/peek-overview/layout.tsx index 50fa5df68..40737b6e8 100644 --- a/apps/app/components/issues/peek-overview/layout.tsx +++ b/apps/app/components/issues/peek-overview/layout.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; +import { mutate } from "swr"; // mobx import { observer } from "mobx-react-lite"; import { useMobxStore } from "lib/mobx/store-provider"; @@ -10,9 +11,11 @@ import { Dialog, Transition } from "@headlessui/react"; // hooks import useUser from "hooks/use-user"; // components -import { FullScreenPeekView, SidePeekView } from "components/issues"; +import { DeleteIssueModal, FullScreenPeekView, SidePeekView } from "components/issues"; // types import { IIssue } from "types"; +// fetch-keys +import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; type Props = { handleMutation: () => void; @@ -28,6 +31,7 @@ export const IssuePeekOverview: React.FC = observer( const [isSidePeekOpen, setIsSidePeekOpen] = useState(false); const [isModalPeekOpen, setIsModalPeekOpen] = useState(false); const [peekOverviewMode, setPeekOverviewMode] = useState("side"); + const [deleteIssueModal, setDeleteIssueModal] = useState(false); const router = useRouter(); const { peekIssue } = router.query; @@ -53,6 +57,7 @@ export const IssuePeekOverview: React.FC = observer( if (!issue || !user) return; await updateIssue(workspaceSlug, projectId, issue.id, formData, user); + mutate(PROJECT_ISSUES_ACTIVITY(issue.id)); handleMutation(); }; @@ -81,7 +86,6 @@ export const IssuePeekOverview: React.FC = observer( setIsSidePeekOpen(false); } } else { - console.log("Triggered"); setIsSidePeekOpen(false); setIsModalPeekOpen(false); } @@ -89,33 +93,38 @@ export const IssuePeekOverview: React.FC = observer( return ( <> + setDeleteIssueModal(false)} + data={issue ? { ...issue } : null} + onSubmit={handleDeleteIssue} + user={user} + /> -
-
- - - setPeekOverviewMode(mode)} - workspaceSlug={workspaceSlug} - /> - - -
+
+ + + setDeleteIssueModal(true)} + handleUpdateIssue={handleUpdateIssue} + issue={issue} + mode={peekOverviewMode} + readOnly={readOnly} + setMode={(mode) => setPeekOverviewMode(mode)} + workspaceSlug={workspaceSlug} + /> + +
@@ -132,49 +141,47 @@ export const IssuePeekOverview: React.FC = observer( >
-
-
- + + - - {peekOverviewMode === "modal" && ( - setPeekOverviewMode(mode)} - workspaceSlug={workspaceSlug} - /> - )} - {peekOverviewMode === "full" && ( - setPeekOverviewMode(mode)} - workspaceSlug={workspaceSlug} - /> - )} - - -
+ {peekOverviewMode === "modal" && ( + setDeleteIssueModal(true)} + handleUpdateIssue={handleUpdateIssue} + issue={issue} + mode={peekOverviewMode} + readOnly={readOnly} + setMode={(mode) => setPeekOverviewMode(mode)} + workspaceSlug={workspaceSlug} + /> + )} + {peekOverviewMode === "full" && ( + setDeleteIssueModal(true)} + handleUpdateIssue={handleUpdateIssue} + issue={issue} + mode={peekOverviewMode} + readOnly={readOnly} + setMode={(mode) => setPeekOverviewMode(mode)} + workspaceSlug={workspaceSlug} + /> + )} + +
diff --git a/apps/app/components/issues/sidebar-select/assignee.tsx b/apps/app/components/issues/sidebar-select/assignee.tsx index 61ece6f78..8b9872368 100644 --- a/apps/app/components/issues/sidebar-select/assignee.tsx +++ b/apps/app/components/issues/sidebar-select/assignee.tsx @@ -51,7 +51,10 @@ export const SidebarAssigneeSelect: React.FC = ({ value, onChange, disabl {value.length} Assignees
) : ( - )} diff --git a/apps/app/components/issues/sidebar-select/priority.tsx b/apps/app/components/issues/sidebar-select/priority.tsx index fd1c77f28..67ae5133d 100644 --- a/apps/app/components/issues/sidebar-select/priority.tsx +++ b/apps/app/components/issues/sidebar-select/priority.tsx @@ -27,7 +27,7 @@ export const SidebarPrioritySelect: React.FC = ({ value, onChange, disabl ? "border-yellow-500/20 bg-yellow-500/20 text-yellow-500" : value === "low" ? "border-green-500/20 bg-green-500/20 text-green-500" - : "bg-custom-background-80 border-custom-border-200" + : "bg-custom-background-80 border-custom-border-200 text-custom-text-200" }`} > diff --git a/apps/app/components/ui/dropdowns/custom-select.tsx b/apps/app/components/ui/dropdowns/custom-select.tsx index 4e495a210..ae814dccb 100644 --- a/apps/app/components/ui/dropdowns/custom-select.tsx +++ b/apps/app/components/ui/dropdowns/custom-select.tsx @@ -41,7 +41,7 @@ const CustomSelect = ({ > <> {customButton ? ( - {customButton} + {customButton} ) : ( { - this.issues[issueId] = updatedIssue; + this.issues[issueId] = { ...updatedIssue }; }); // make a patch request to update the issue From 1e9f0823f8c63838345fa967c5f1f3be8e6b3935 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Fri, 1 Sep 2023 14:41:20 +0530 Subject: [PATCH 034/137] fix: imported uuid (#2048) --- .../migrations/0042_alter_analyticview_created_by_and_more.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py b/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py index 7db02c53d..62f08038c 100644 --- a/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py +++ b/apiserver/plane/db/migrations/0042_alter_analyticview_created_by_and_more.py @@ -3,7 +3,7 @@ from django.conf import settings from django.db import migrations, models import django.db.models.deletion - +import uuid def update_user_timezones(apps, schema_editor): UserModel = apps.get_model("db", "User") From 42ece0d784ebf172ac15cf326270a29f3c9735ff Mon Sep 17 00:00:00 2001 From: Kritika Upadhyay <78753387+Kritikkkaaa@users.noreply.github.com> Date: Fri, 1 Sep 2023 15:37:27 +0530 Subject: [PATCH 035/137] chore: updates project invite placeholder (#2049) --- apps/app/components/project/send-project-invitation-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/app/components/project/send-project-invitation-modal.tsx b/apps/app/components/project/send-project-invitation-modal.tsx index 414ef7a6c..035a680f2 100644 --- a/apps/app/components/project/send-project-invitation-modal.tsx +++ b/apps/app/components/project/send-project-invitation-modal.tsx @@ -218,7 +218,7 @@ const SendProjectInvitationModal: React.FC = (props) => { }
) : ( -
Select co-worker’s email
+
Select co-worker
)}