From 1a668c19a574942d3a9b2453016e6aaa62f03c2b Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 28 Jun 2023 17:27:19 +0530 Subject: [PATCH 01/27] style: issue detail page layout (#1424) --- .../[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 93bfecfe0..cb57ff9fd 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -120,10 +120,10 @@ const IssueDetailsPage: NextPage = () => { > {issueDetails && projectId ? (
{cycle.description}
++ {cycle.description} +
No matching results
+ + {noResultIcon && noResultIcon} +No matching results
+ ) ) : (Loading...
From 110eb39a512adc78acdc34c2bd1e61c238aec7dd Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 30 Jun 2023 19:37:26 +0530 Subject: [PATCH 14/27] dev: update packages to latest version (#1431) --- apiserver/plane/settings/local.py | 1 + apiserver/plane/settings/production.py | 1 + apiserver/plane/settings/staging.py | 1 + apiserver/requirements/base.txt | 28 +++++++++++++------------- apiserver/requirements/production.txt | 6 +++--- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index 1b862c013..e6f5f8e39 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -63,6 +63,7 @@ if os.environ.get("SENTRY_DSN", False): send_default_pii=True, environment="local", traces_sample_rate=0.7, + profiles_sample_rate=1.0, ) REDIS_HOST = "localhost" diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index 29b75fc8b..983931110 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -84,6 +84,7 @@ if bool(os.environ.get("SENTRY_DSN", False)): traces_sample_rate=1, send_default_pii=True, environment="production", + profiles_sample_rate=1.0, ) if DOCKERIZED and USE_MINIO: diff --git a/apiserver/plane/settings/staging.py b/apiserver/plane/settings/staging.py index 11ff7a372..5a43e266e 100644 --- a/apiserver/plane/settings/staging.py +++ b/apiserver/plane/settings/staging.py @@ -66,6 +66,7 @@ sentry_sdk.init( traces_sample_rate=1, send_default_pii=True, environment="staging", + profiles_sample_rate=1.0, ) # The AWS region to connect to. diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 2bc109968..3cd196830 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -2,30 +2,30 @@ Django==3.2.19 django-braces==1.15.0 -django-taggit==3.1.0 -psycopg2==2.9.5 -django-oauth-toolkit==2.2.0 +django-taggit==4.0.0 +psycopg2==2.9.6 +django-oauth-toolkit==2.3.0 mistune==2.0.4 djangorestframework==3.14.0 -redis==4.5.4 +redis==4.6.0 django-nested-admin==4.0.2 -django-cors-headers==3.13.0 +django-cors-headers==4.1.0 whitenoise==6.3.0 -django-allauth==0.52.0 +django-allauth==0.54.0 faker==13.4.0 -django-filter==22.1 +django-filter==23.2 jsonmodels==2.6.0 djangorestframework-simplejwt==5.2.2 -sentry-sdk==1.14.0 -django-s3-storage==0.13.11 +sentry-sdk==1.26.0 +django-s3-storage==0.14.0 django-crum==0.7.9 django-guardian==2.4.0 dj_rest_auth==2.2.5 google-auth==2.16.0 google-api-python-client==2.75.0 -django-redis==5.2.0 -uvicorn==0.20.0 +django-redis==5.3.0 +uvicorn==0.22.0 channels==4.0.0 -openai==0.27.2 -slack-sdk==3.20.2 -celery==5.2.7 \ No newline at end of file +openai==0.27.8 +slack-sdk==3.21.3 +celery==5.3.1 \ No newline at end of file diff --git a/apiserver/requirements/production.txt b/apiserver/requirements/production.txt index c37e98ffd..13b3e9aed 100644 --- a/apiserver/requirements/production.txt +++ b/apiserver/requirements/production.txt @@ -1,11 +1,11 @@ -r base.txt -dj-database-url==1.2.0 +dj-database-url==2.0.0 gunicorn==20.1.0 whitenoise==6.3.0 django-storages==1.13.2 -boto3==1.26.136 -django-anymail==9.0 +boto3==1.26.163 +django-anymail==10.0 twilio==7.16.2 django-debug-toolbar==3.8.1 gevent==22.10.2 From e23e9ccdbb4a19f5602a343af6538ea3fdbe596b Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 4 Jul 2023 13:54:37 +0530 Subject: [PATCH 15/27] fix: project member list endpoint n+1 (#1458) --- apiserver/plane/api/serializers/project.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index 18ee19e7b..ed369f20a 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -77,6 +77,13 @@ class ProjectSerializer(BaseSerializer): raise serializers.ValidationError(detail="Project Identifier is already taken") +class ProjectLiteSerializer(BaseSerializer): + class Meta: + model = Project + fields = ["id", "identifier", "name"] + read_only_fields = fields + + class ProjectDetailSerializer(BaseSerializer): workspace = WorkSpaceSerializer(read_only=True) default_assignee = UserLiteSerializer(read_only=True) @@ -94,7 +101,7 @@ class ProjectDetailSerializer(BaseSerializer): class ProjectMemberSerializer(BaseSerializer): workspace = WorkSpaceSerializer(read_only=True) - project = ProjectSerializer(read_only=True) + project = ProjectLiteSerializer(read_only=True) member = UserLiteSerializer(read_only=True) class Meta: @@ -127,10 +134,3 @@ class ProjectFavoriteSerializer(BaseSerializer): "workspace", "user", ] - - -class ProjectLiteSerializer(BaseSerializer): - class Meta: - model = Project - fields = ["id", "identifier", "name"] - read_only_fields = fields From e4ee6a5bfbedab880b925c6d5e511b6d0fd37fa4 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 4 Jul 2023 13:54:48 +0530 Subject: [PATCH 16/27] chore: workspace char name and slug maximum length (#1453) --- apiserver/plane/api/views/workspace.py | 88 +++++++++++++++++++------- apiserver/plane/db/models/workspace.py | 4 +- 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 26c82d54c..869f21384 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -3,6 +3,7 @@ import jwt from datetime import date, datetime from dateutil.relativedelta import relativedelta from uuid import uuid4 + # Django imports from django.db import IntegrityError from django.db.models import Prefetch @@ -93,14 +94,35 @@ class WorkSpaceViewSet(BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - return self.filter_queryset( - super().get_queryset().select_related("owner") - ).order_by("name").filter(workspace_member__member=self.request.user).annotate(total_members=member_count).annotate(total_issues=issue_count) + return ( + self.filter_queryset(super().get_queryset().select_related("owner")) + .order_by("name") + .filter(workspace_member__member=self.request.user) + .annotate(total_members=member_count) + .annotate(total_issues=issue_count) + ) def create(self, request): try: serializer = WorkSpaceSerializer(data=request.data) + slug = request.data.get("slug", False) + name = request.data.get("name", False) + + if not name or not slug: + return Response( + {"error": "Both name and slug are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if len(name) > 80 or len(slug) > 48: + return Response( + { + "error": "The maximum length for name is 80 and for slug is 48" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + if serializer.is_valid(): serializer.save(owner=request.user) # Create Workspace member @@ -160,14 +182,20 @@ class UserWorkSpacesEndpoint(BaseAPIView): ) workspace = ( - Workspace.objects.prefetch_related( - Prefetch("workspace_member", queryset=WorkspaceMember.objects.all()) + ( + Workspace.objects.prefetch_related( + Prefetch( + "workspace_member", queryset=WorkspaceMember.objects.all() + ) + ) + .filter( + workspace_member__member=request.user, + ) + .select_related("owner") ) - .filter( - workspace_member__member=request.user, - ) - .select_related("owner") - ).annotate(total_members=member_count).annotate(total_issues=issue_count) + .annotate(total_members=member_count) + .annotate(total_issues=issue_count) + ) serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True) return Response(serializer.data, status=status.HTTP_200_OK) @@ -216,9 +244,20 @@ class InviteWorkspaceEndpoint(BaseAPIView): ) # check for role level - requesting_user = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user) - if len([email for email in emails if int(email.get("role", 10)) > requesting_user.role]): - return Response({"error": "You cannot invite a user with higher role"}, status=status.HTTP_400_BAD_REQUEST) + requesting_user = WorkspaceMember.objects.get( + workspace__slug=slug, member=request.user + ) + if len( + [ + email + for email in emails + if int(email.get("role", 10)) > requesting_user.role + ] + ): + return Response( + {"error": "You cannot invite a user with higher role"}, + status=status.HTTP_400_BAD_REQUEST, + ) workspace = Workspace.objects.get(slug=slug) @@ -276,14 +315,17 @@ class InviteWorkspaceEndpoint(BaseAPIView): # create the user if signup is disabled if settings.DOCKERIZED and not settings.ENABLE_SIGNUP: - _ = User.objects.bulk_create([ - User( - email=email.get("email"), - password=str(uuid4().hex), - is_password_autoset=True - ) - for email in emails - ], batch_size=100) + _ = User.objects.bulk_create( + [ + User( + email=email.get("email"), + password=str(uuid4().hex), + is_password_autoset=True, + ) + for email in emails + ], + batch_size=100, + ) for invitation in workspace_invitations: workspace_invitation.delay( @@ -865,7 +907,9 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView): ) state_distribution = ( - Issue.issue_objects.filter(workspace__slug=slug, assignees__in=[request.user]) + Issue.issue_objects.filter( + workspace__slug=slug, assignees__in=[request.user] + ) .annotate(state_group=F("state__group")) .values("state_group") .annotate(state_count=Count("state_group")) diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index b00d53013..f071bac2a 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -15,14 +15,14 @@ ROLE_CHOICES = ( class Workspace(BaseModel): - name = models.CharField(max_length=255, verbose_name="Workspace Name") + name = models.CharField(max_length=80, verbose_name="Workspace Name") logo = models.URLField(verbose_name="Logo", blank=True, null=True) owner = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="owner_workspace", ) - slug = models.SlugField(max_length=100, db_index=True, unique=True) + slug = models.SlugField(max_length=48, db_index=True, unique=True) company_size = models.PositiveIntegerField(default=10) def __str__(self): From 1a72a0dff439658176bb119f066cbf458b3743fc Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 4 Jul 2023 13:56:51 +0530 Subject: [PATCH 17/27] fix: user invitation workflow for self hosted version (#1441) --- apiserver/plane/api/views/workspace.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 869f21384..7263c5647 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -22,6 +22,7 @@ from django.db.models import ( ) from django.db.models.functions import ExtractWeek, Cast, ExtractDay from django.db.models.fields import DateField +from django.contrib.auth.hashers import make_password # Third party modules from rest_framework import status @@ -318,11 +319,12 @@ class InviteWorkspaceEndpoint(BaseAPIView): _ = User.objects.bulk_create( [ User( - email=email.get("email"), - password=str(uuid4().hex), + username=str(uuid4().hex), + email=invitation.email, + password=make_password(uuid4().hex), is_password_autoset=True, ) - for email in emails + for invitation in workspace_invitations ], batch_size=100, ) From 5a6fd0efdb8dbc54fbcdd5e1b57307710f5621a1 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 4 Jul 2023 16:48:59 +0530 Subject: [PATCH 18/27] chore: due date filter (#1460) --- apiserver/plane/utils/issue_filters.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 74acb2044..6a9e8b8e8 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -166,16 +166,16 @@ def filter_target_date(params, filter, method): for query in target_dates: target_date_query = query.split(";") if len(target_date_query) == 2 and "after" in target_date_query: - filter["target_date__gte"] = target_date_query[0] + filter["target_date__gt"] = target_date_query[0] else: - filter["target_date__lte"] = target_date_query[0] + filter["target_date__lt"] = target_date_query[0] else: if params.get("target_date", None) and len(params.get("target_date")): for query in params.get("target_date"): if query.get("timeline", "after") == "after": - filter["target_date__gte"] = query.get("datetime") + filter["target_date__gt"] = query.get("datetime") else: - filter["target_date__lte"] = query.get("datetime") + filter["target_date__lt"] = query.get("datetime") return filter From 4ede04d72f25f54a2b6d6a11720e022098a4a4c0 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 4 Jul 2023 18:19:19 +0530 Subject: [PATCH 19/27] refactor: standardized date format throughout the platform (#1461) --- apps/app/components/core/feeds.tsx | 4 ++-- apps/app/components/cycles/sidebar.tsx | 6 +++--- apps/app/components/inbox/inbox-issue-card.tsx | 8 ++++---- apps/app/components/inbox/inbox-main-content.tsx | 10 +++++++--- apps/app/components/issues/activity.tsx | 4 ++-- apps/app/components/issues/select/date.tsx | 4 ++-- apps/app/components/issues/view-select/due-date.tsx | 6 ++++-- apps/app/components/modules/sidebar.tsx | 12 +++++++++--- apps/app/components/project/single-project-card.tsx | 6 +++--- apps/app/components/ui/date.tsx | 4 ++-- apps/app/components/ui/datepicker.tsx | 6 +++--- apps/app/components/views/single-view-item.tsx | 4 ++-- apps/app/components/workspace/activity-graph.tsx | 4 ++-- apps/app/helpers/date-time.helper.ts | 5 +++-- 14 files changed, 48 insertions(+), 35 deletions(-) diff --git a/apps/app/components/core/feeds.tsx b/apps/app/components/core/feeds.tsx index cce482da5..d00804ec8 100644 --- a/apps/app/components/core/feeds.tsx +++ b/apps/app/components/core/feeds.tsx @@ -19,7 +19,7 @@ import { } from "@heroicons/react/24/outline"; import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons"; // helpers -import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper"; +import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper"; // types import RemirrorRichTextEditor from "components/rich-text-editor"; @@ -187,7 +187,7 @@ export const Feeds: React.FCThis issue was snoozed till{" "} - {renderShortNumericDateFormat(issueDetails.issue_inbox[0].snoozed_till ?? "")} + {renderShortDateWithYearFormat( + issueDetails.issue_inbox[0].snoozed_till ?? "" + )} .
) : (This issue has been snoozed till{" "} - {renderShortNumericDateFormat(issueDetails.issue_inbox[0].snoozed_till ?? "")} + {renderShortDateWithYearFormat( + issueDetails.issue_inbox[0].snoozed_till ?? "" + )} .
)} diff --git a/apps/app/components/issues/activity.tsx b/apps/app/components/issues/activity.tsx index 8d708b856..e30f00b0b 100644 --- a/apps/app/components/issues/activity.tsx +++ b/apps/app/components/issues/activity.tsx @@ -26,7 +26,7 @@ import { } from "@heroicons/react/24/outline"; import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons"; // helpers -import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper"; +import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper"; // types import { ICurrentUserResponse, IIssueComment, IIssueLabels } from "types"; @@ -299,7 +299,7 @@ export const IssueActivitySection: React.FC
{renderShortTime(view.updated_at)}
diff --git a/apps/app/components/workspace/activity-graph.tsx b/apps/app/components/workspace/activity-graph.tsx
index 1f9db203d..b49446eeb 100644
--- a/apps/app/components/workspace/activity-graph.tsx
+++ b/apps/app/components/workspace/activity-graph.tsx
@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react";
// ui
import { Tooltip } from "components/ui";
// helpers
-import { renderDateFormat, renderShortNumericDateFormat } from "helpers/date-time.helper";
+import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types
import { IUserActivity } from "types";
// constants
@@ -109,7 +109,7 @@ export const ActivityGraph: React.FC