From 592fe94cb40940e35e27468944c04bbc223fdbb5 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 18:23:39 +0530
Subject: [PATCH 01/59] chore: enable/disable signup in self hosted
environments (#1271)
* dev: new onboarding workflow for self hosted instance
* dev: additional flag on user creation
* dev: segregate sign up and sign in endpoint
* dev: update sign in endpoint for not existing users
---
.env.example | 2 +
apiserver/plane/api/urls.py | 2 +
apiserver/plane/api/views/__init__.py | 1 +
apiserver/plane/api/views/authentication.py | 242 ++++++++++++--------
apiserver/plane/api/views/workspace.py | 13 +-
apiserver/plane/settings/local.py | 2 +
apiserver/plane/settings/production.py | 3 +
apiserver/plane/settings/staging.py | 2 +
docker-compose.yml | 2 +
9 files changed, 174 insertions(+), 95 deletions(-)
diff --git a/.env.example b/.env.example
index 42d98677b..29eae7cbe 100644
--- a/.env.example
+++ b/.env.example
@@ -65,4 +65,6 @@ NGINX_PORT=80
DEFAULT_EMAIL="captain@plane.so"
DEFAULT_PASSWORD="password123"
+# SignUps
+ENABLE_SIGNUP="1"
# Auto generated and Required that will be generated from setup.sh
\ No newline at end of file
diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py
index bf5180ff8..23277f294 100644
--- a/apiserver/plane/api/urls.py
+++ b/apiserver/plane/api/urls.py
@@ -5,6 +5,7 @@ from django.urls import path
from plane.api.views import (
# Authentication
+ SignUpEndpoint,
SignInEndpoint,
SignOutEndpoint,
MagicSignInEndpoint,
@@ -154,6 +155,7 @@ urlpatterns = [
# Social Auth
path("social-auth/", OauthEndpoint.as_view(), name="oauth"),
# Auth
+ path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"),
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
# Magic Sign In/Up
diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py
index 4177b1371..45ea35f71 100644
--- a/apiserver/plane/api/views/__init__.py
+++ b/apiserver/plane/api/views/__init__.py
@@ -79,6 +79,7 @@ from .auth_extended import (
from .authentication import (
+ SignUpEndpoint,
SignInEndpoint,
SignOutEndpoint,
MagicSignInEndpoint,
diff --git a/apiserver/plane/api/views/authentication.py b/apiserver/plane/api/views/authentication.py
index a63f199ad..385ec7568 100644
--- a/apiserver/plane/api/views/authentication.py
+++ b/apiserver/plane/api/views/authentication.py
@@ -36,6 +36,99 @@ def get_tokens_for_user(user):
)
+class SignUpEndpoint(BaseAPIView):
+ permission_classes = (AllowAny,)
+
+ def post(self, request):
+ try:
+ if not settings.ENABLE_SIGNUP:
+ return Response(
+ {
+ "error": "New account creation is disabled. Please contact your site administrator"
+ },
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ email = request.data.get("email", False)
+ password = request.data.get("password", False)
+
+ ## Raise exception if any of the above are missing
+ if not email or not password:
+ return Response(
+ {"error": "Both email and password are required"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ email = email.strip().lower()
+
+ try:
+ validate_email(email)
+ except ValidationError as e:
+ return Response(
+ {"error": "Please provide a valid email address."},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ # Check if the user already exists
+ if User.objects.filter(email=email).exists():
+ return Response(
+ {"error": "User already exist please sign in"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ user = User.objects.create(email=email, username=uuid.uuid4().hex)
+ user.set_password(password)
+
+ # settings last actives for the user
+ user.last_active = timezone.now()
+ user.last_login_time = timezone.now()
+ user.last_login_ip = request.META.get("REMOTE_ADDR")
+ user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
+ user.token_updated_at = timezone.now()
+ user.save()
+
+ serialized_user = UserSerializer(user).data
+
+ access_token, refresh_token = get_tokens_for_user(user)
+
+ data = {
+ "access_token": access_token,
+ "refresh_token": refresh_token,
+ "user": serialized_user,
+ }
+
+ # Send Analytics
+ if settings.ANALYTICS_BASE_API:
+ _ = requests.post(
+ settings.ANALYTICS_BASE_API,
+ headers={
+ "Content-Type": "application/json",
+ "X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
+ },
+ json={
+ "event_id": uuid.uuid4().hex,
+ "event_data": {
+ "medium": "email",
+ },
+ "user": {"email": email, "id": str(user.id)},
+ "device_ctx": {
+ "ip": request.META.get("REMOTE_ADDR"),
+ "user_agent": request.META.get("HTTP_USER_AGENT"),
+ },
+ "event_type": "SIGN_UP",
+ },
+ )
+
+ return Response(data, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ capture_exception(e)
+ return Response(
+ {"error": "Something went wrong please try again later"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+
class SignInEndpoint(BaseAPIView):
permission_classes = (AllowAny,)
@@ -63,108 +156,69 @@ class SignInEndpoint(BaseAPIView):
user = User.objects.filter(email=email).first()
- # Sign up Process
if user is None:
- user = User.objects.create(email=email, username=uuid.uuid4().hex)
- user.set_password(password)
+ return Response(
+ {
+ "error": "Sorry, we could not find a user with the provided credentials. Please try again."
+ },
+ status=status.HTTP_403_FORBIDDEN,
+ )
- # settings last actives for the user
- user.last_active = timezone.now()
- user.last_login_time = timezone.now()
- user.last_login_ip = request.META.get("REMOTE_ADDR")
- user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
- user.token_updated_at = timezone.now()
- user.save()
+ # Sign up Process
+ if not user.check_password(password):
+ return Response(
+ {
+ "error": "Sorry, we could not find a user with the provided credentials. Please try again."
+ },
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ if not user.is_active:
+ return Response(
+ {
+ "error": "Your account has been deactivated. Please contact your site administrator."
+ },
+ status=status.HTTP_403_FORBIDDEN,
+ )
- serialized_user = UserSerializer(user).data
+ serialized_user = UserSerializer(user).data
- access_token, refresh_token = get_tokens_for_user(user)
+ # settings last active for the user
+ user.last_active = timezone.now()
+ user.last_login_time = timezone.now()
+ user.last_login_ip = request.META.get("REMOTE_ADDR")
+ user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
+ user.token_updated_at = timezone.now()
+ user.save()
- data = {
- "access_token": access_token,
- "refresh_token": refresh_token,
- "user": serialized_user,
- }
-
- # Send Analytics
- if settings.ANALYTICS_BASE_API:
- _ = requests.post(
- settings.ANALYTICS_BASE_API,
- headers={
- "Content-Type": "application/json",
- "X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
+ access_token, refresh_token = get_tokens_for_user(user)
+ # Send Analytics
+ if settings.ANALYTICS_BASE_API:
+ _ = requests.post(
+ settings.ANALYTICS_BASE_API,
+ headers={
+ "Content-Type": "application/json",
+ "X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
+ },
+ json={
+ "event_id": uuid.uuid4().hex,
+ "event_data": {
+ "medium": "email",
},
- json={
- "event_id": uuid.uuid4().hex,
- "event_data": {
- "medium": "email",
- },
- "user": {"email": email, "id": str(user.id)},
- "device_ctx": {
- "ip": request.META.get("REMOTE_ADDR"),
- "user_agent": request.META.get("HTTP_USER_AGENT"),
- },
- "event_type": "SIGN_UP",
+ "user": {"email": email, "id": str(user.id)},
+ "device_ctx": {
+ "ip": request.META.get("REMOTE_ADDR"),
+ "user_agent": request.META.get("HTTP_USER_AGENT"),
},
- )
+ "event_type": "SIGN_IN",
+ },
+ )
+ data = {
+ "access_token": access_token,
+ "refresh_token": refresh_token,
+ "user": serialized_user,
+ }
- return Response(data, status=status.HTTP_200_OK)
- # Sign in Process
- else:
- if not user.check_password(password):
- return Response(
- {
- "error": "Sorry, we could not find a user with the provided credentials. Please try again."
- },
- status=status.HTTP_403_FORBIDDEN,
- )
- if not user.is_active:
- return Response(
- {
- "error": "Your account has been deactivated. Please contact your site administrator."
- },
- status=status.HTTP_403_FORBIDDEN,
- )
-
- serialized_user = UserSerializer(user).data
-
- # settings last active for the user
- user.last_active = timezone.now()
- user.last_login_time = timezone.now()
- user.last_login_ip = request.META.get("REMOTE_ADDR")
- user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
- user.token_updated_at = timezone.now()
- user.save()
-
- access_token, refresh_token = get_tokens_for_user(user)
- # Send Analytics
- if settings.ANALYTICS_BASE_API:
- _ = requests.post(
- settings.ANALYTICS_BASE_API,
- headers={
- "Content-Type": "application/json",
- "X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
- },
- json={
- "event_id": uuid.uuid4().hex,
- "event_data": {
- "medium": "email",
- },
- "user": {"email": email, "id": str(user.id)},
- "device_ctx": {
- "ip": request.META.get("REMOTE_ADDR"),
- "user_agent": request.META.get("HTTP_USER_AGENT"),
- },
- "event_type": "SIGN_IN",
- },
- )
- data = {
- "access_token": access_token,
- "refresh_token": refresh_token,
- "user": serialized_user,
- }
-
- return Response(data, status=status.HTTP_200_OK)
+ return Response(data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py
index 2f3fcb558..c1cdcc4b4 100644
--- a/apiserver/plane/api/views/workspace.py
+++ b/apiserver/plane/api/views/workspace.py
@@ -2,7 +2,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
@@ -249,6 +249,17 @@ class InviteWorkspaceEndpoint(BaseAPIView):
email__in=[email.get("email") for email in emails]
).select_related("workspace")
+ # 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)
+
for invitation in workspace_invitations:
workspace_invitation.delay(
invitation.email,
diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py
index 20b257a27..3a3a3d9a3 100644
--- a/apiserver/plane/settings/local.py
+++ b/apiserver/plane/settings/local.py
@@ -91,3 +91,5 @@ CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL")
CELERY_BROKER_URL = os.environ.get("REDIS_URL")
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
+
+ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
\ No newline at end of file
diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py
index 7e7f4186f..d5fcd3d04 100644
--- a/apiserver/plane/settings/production.py
+++ b/apiserver/plane/settings/production.py
@@ -258,3 +258,6 @@ else:
CELERY_BROKER_URL = broker_url
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
+
+
+ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
diff --git a/apiserver/plane/settings/staging.py b/apiserver/plane/settings/staging.py
index c6ffcaf22..851ad77f2 100644
--- a/apiserver/plane/settings/staging.py
+++ b/apiserver/plane/settings/staging.py
@@ -211,3 +211,5 @@ CELERY_RESULT_BACKEND = broker_url
CELERY_BROKER_URL = broker_url
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
+
+ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
diff --git a/docker-compose.yml b/docker-compose.yml
index 45a74afb6..ec998ab76 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -54,6 +54,7 @@ services:
DEFAULT_EMAIL: ${DEFAULT_EMAIL}
DEFAULT_PASSWORD: ${DEFAULT_PASSWORD}
USE_MINIO: ${USE_MINIO}
+ ENABLE_SIGNUP: ${ENABLE_SIGNUP}
depends_on:
- plane-db
- plane-redis
@@ -91,6 +92,7 @@ services:
DEFAULT_EMAIL: ${DEFAULT_EMAIL:-captain@plane.so}
DEFAULT_PASSWORD: ${DEFAULT_PASSWORD:-password123}
USE_MINIO: ${USE_MINIO}
+ ENABLE_SIGNUP: ${ENABLE_SIGNUP}
depends_on:
- plane-api
- plane-db
From 1e2c1cac9c0d9fc2ffeb0edfd2595d6a744fcd0a Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 18:52:58 +0530
Subject: [PATCH 02/59] chore: project list endpoint to show is_member status
(#1166)
---
apiserver/plane/api/serializers/project.py | 1 +
apiserver/plane/api/views/project.py | 10 ++++++++++
2 files changed, 11 insertions(+)
diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py
index 0a8ad1cf8..18ee19e7b 100644
--- a/apiserver/plane/api/serializers/project.py
+++ b/apiserver/plane/api/serializers/project.py
@@ -85,6 +85,7 @@ class ProjectDetailSerializer(BaseSerializer):
total_members = serializers.IntegerField(read_only=True)
total_cycles = serializers.IntegerField(read_only=True)
total_modules = serializers.IntegerField(read_only=True)
+ is_member = serializers.BooleanField(read_only=True)
class Meta:
model = Project
diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py
index bdb758ac9..e49c2fe78 100644
--- a/apiserver/plane/api/views/project.py
+++ b/apiserver/plane/api/views/project.py
@@ -72,6 +72,7 @@ class ProjectViewSet(BaseViewSet):
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
)
+
return self.filter_queryset(
super()
.get_queryset()
@@ -81,6 +82,15 @@ class ProjectViewSet(BaseViewSet):
"workspace", "workspace__owner", "default_assignee", "project_lead"
)
.annotate(is_favorite=Exists(subquery))
+ .annotate(
+ is_member=Exists(
+ ProjectMember.objects.filter(
+ member=self.request.user,
+ project_id=OuterRef("pk"),
+ workspace__slug=self.kwargs.get("slug"),
+ )
+ )
+ )
.distinct()
)
From 963ccd808d98c9f22201e0bb67be4b9b05d6fbda Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 18:54:01 +0530
Subject: [PATCH 03/59] fix: email ssl setting for docker environment (#1299)
---
.env.example | 1 +
apiserver/plane/settings/common.py | 1 +
docker-compose.yml | 2 ++
3 files changed, 4 insertions(+)
diff --git a/.env.example b/.env.example
index 29eae7cbe..578aa0fa4 100644
--- a/.env.example
+++ b/.env.example
@@ -35,6 +35,7 @@ EMAIL_HOST_PASSWORD=""
EMAIL_PORT=587
EMAIL_FROM="Team Plane "
EMAIL_USE_TLS="1"
+EMAIL_USE_SSL="0"
# AWS Settings
AWS_REGION=""
diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py
index f5bff248b..2e0266159 100644
--- a/apiserver/plane/settings/common.py
+++ b/apiserver/plane/settings/common.py
@@ -179,6 +179,7 @@ EMAIL_PORT = int(os.environ.get("EMAIL_PORT", 587))
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "1") == "1"
+EMAIL_USE_SSL = os.environ.get("EMAIL_USE_SSL", "0") == "1"
EMAIL_FROM = os.environ.get("EMAIL_FROM", "Team Plane ")
diff --git a/docker-compose.yml b/docker-compose.yml
index ec998ab76..da275a1fb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -38,6 +38,7 @@ services:
EMAIL_PORT: ${EMAIL_PORT}
EMAIL_FROM: ${EMAIL_FROM}
EMAIL_USE_TLS: ${EMAIL_USE_TLS}
+ EMAIL_USE_SSL: ${EMAIL_USE_SSL}
AWS_REGION: ${AWS_REGION}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
@@ -76,6 +77,7 @@ services:
EMAIL_PORT: ${EMAIL_PORT}
EMAIL_FROM: ${EMAIL_FROM}
EMAIL_USE_TLS: ${EMAIL_USE_TLS}
+ EMAIL_USE_SSL: ${EMAIL_USE_SSL}
AWS_REGION: ${AWS_REGION}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
From e9a0eb87cc1f2a009fcf066dbb6cf86917d5e18e Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 18:57:17 +0530
Subject: [PATCH 04/59] feat: inbox (#1023)
* dev: initialize inbox
* dev: inbox and inbox issues models, views and serializers
* dev: issue object filter for inbox
* dev: filter for search issues
* dev: inbox snooze and duplicates
* dev: set duplicate to null by default
* feat: inbox ui and services
* feat: project detail in inbox
* style: layout, popover, icons, sidebar
* dev: default inbox for project and pending issues count
* dev: fix exception when creating default inbox
* fix: empty state for inbox
* dev: auto issue state updation when rejected or marked duplicate
* fix: inbox update status
* fix: hydrating chose with old values
filters workflow
* feat: inbox issue filtering
* fix: issue inbox filtering
* feat: filter inbox issues
* refactor: analytics, border colors
* dev: filters and views for inbox
* dev: source for inboxissue and update list inbox issue
* dev: update list endpoint to house filters and additional data
* dev: bridge id for list
* dev: remove print logs
* dev: update inbox issue workflow
* dev: add description_html in issue details
* fix: inbox track event auth, chore: inbox issue action authorization
* fix: removed unnecessary api calls
* style: viewed issues
* fix: priority validation
* dev: remove print logs
* dev: update issue inbox update workflow
* chore: added inbox view context
* fix: type errors
* fix: build errors and warnings
* dev: update issue inbox workflow and log all the changes
* fix: filters logic, sidebar fields to show
* dev: update issue filtering status
* chore: update create inbox issue modal, fix: mutation issues
* dev: update issue accept workflow
* chore: add comment to inbox issues
* chore: remove inboxIssueId from url after deleting
* dev: update the issue triage workflow
* fix: mutation after issue status change
* chore: issue details sidebar divider
* fix: issue activity for inbox issues
* dev: update inbox perrmissions
* dev: create new permission layer
* chore: auth layer for inbox
* chore: show accepting status
* chore: show issue status at the top of issue details
---------
Co-authored-by: Dakshesh Jain
Co-authored-by: gurusainath
Co-authored-by: Aaryan Khandelwal
---
apiserver/plane/api/permissions/__init__.py | 4 +-
apiserver/plane/api/permissions/project.py | 13 +
apiserver/plane/api/permissions/workspace.py | 12 +-
apiserver/plane/api/serializers/__init__.py | 7 +-
apiserver/plane/api/serializers/inbox.py | 58 ++
apiserver/plane/api/serializers/issue.py | 1 +
apiserver/plane/api/urls.py | 48 +
apiserver/plane/api/views/__init__.py | 1 +
apiserver/plane/api/views/analytic.py | 8 +-
apiserver/plane/api/views/cycle.py | 6 +-
apiserver/plane/api/views/importer.py | 2 +-
apiserver/plane/api/views/inbox.py | 349 ++++++++
apiserver/plane/api/views/issue.py | 24 +-
apiserver/plane/api/views/module.py | 6 +-
apiserver/plane/api/views/people.py | 4 +-
apiserver/plane/api/views/project.py | 20 +-
apiserver/plane/api/views/search.py | 10 +-
apiserver/plane/api/views/state.py | 2 +-
apiserver/plane/api/views/view.py | 2 +-
apiserver/plane/api/views/workspace.py | 18 +-
.../plane/bgtasks/analytic_plot_export.py | 4 +-
.../plane/bgtasks/issue_activites_task.py | 2 +-
apiserver/plane/db/models/__init__.py | 3 +-
apiserver/plane/db/models/inbox.py | 51 ++
apiserver/plane/db/models/issue.py | 17 +
apiserver/plane/db/models/project.py | 1 +
apiserver/plane/utils/issue_filters.py | 12 +
.../command-palette/command-pallette.tsx | 5 +-
.../core/board-view/single-issue.tsx | 13 +-
.../core/calendar-view/single-issue.tsx | 2 +-
apps/app/components/core/issues-view.tsx | 1 +
.../core/list-view/single-issue.tsx | 13 +-
apps/app/components/icons/inbox-icon.tsx | 24 +
apps/app/components/icons/index.ts | 2 +
.../icons/stacked-layers-horizontal-icon.tsx | 24 +
.../components/inbox/decline-issue-modal.tsx | 164 ++++
.../components/inbox/delete-issue-modal.tsx | 150 ++++
.../app/components/inbox/filters-dropdown.tsx | 63 ++
.../components/inbox/inbox-action-headers.tsx | 226 +++++
.../app/components/inbox/inbox-issue-card.tsx | 117 +++
.../components/inbox/inbox-main-content.tsx | 259 ++++++
apps/app/components/inbox/index.ts | 8 +
.../components/inbox/issues-list-sidebar.tsx | 44 +
.../app/components/inbox/select-duplicate.tsx | 193 ++++
apps/app/components/issues/activity.tsx | 30 +-
.../components/issues/comment/add-comment.tsx | 5 +-
.../components/issues/delete-issue-modal.tsx | 2 +-
.../components/issues/description-form.tsx | 22 +-
apps/app/components/issues/form.tsx | 456 +++++-----
apps/app/components/issues/modal.tsx | 119 ++-
.../components/issues/my-issues-list-item.tsx | 2 +-
apps/app/components/issues/sidebar.tsx | 832 ++++++++++--------
.../pages/create-update-block-inline.tsx | 4 +-
.../ui/buttons/secondary-button.tsx | 6 +-
apps/app/components/views/select-filters.tsx | 2 +-
apps/app/constants/fetch-keys.ts | 26 +
apps/app/constants/inbox.ts | 9 +
apps/app/contexts/inbox-view-context.tsx | 163 ++++
apps/app/contexts/issue-view.context.tsx | 1 +
apps/app/hooks/use-inbox-view.tsx | 60 ++
.../projects/[projectId]/inbox/[inboxId].tsx | 248 ++++++
.../projects/[projectId]/issues/[issueId].tsx | 19 +-
.../projects/[projectId]/issues/index.tsx | 28 +-
.../[projectId]/settings/features.tsx | 20 +-
apps/app/public/empty-state/empty-inbox.svg | 57 ++
apps/app/services/inbox.service.ts | 183 ++++
apps/app/services/track-event.service.ts | 64 +-
apps/app/types/inbox.d.ts | 85 ++
apps/app/types/index.d.ts | 1 +
apps/app/types/issues.d.ts | 8 +-
apps/app/types/projects.d.ts | 4 +
71 files changed, 3712 insertions(+), 737 deletions(-)
create mode 100644 apiserver/plane/api/serializers/inbox.py
create mode 100644 apiserver/plane/api/views/inbox.py
create mode 100644 apiserver/plane/db/models/inbox.py
create mode 100644 apps/app/components/icons/inbox-icon.tsx
create mode 100644 apps/app/components/icons/stacked-layers-horizontal-icon.tsx
create mode 100644 apps/app/components/inbox/decline-issue-modal.tsx
create mode 100644 apps/app/components/inbox/delete-issue-modal.tsx
create mode 100644 apps/app/components/inbox/filters-dropdown.tsx
create mode 100644 apps/app/components/inbox/inbox-action-headers.tsx
create mode 100644 apps/app/components/inbox/inbox-issue-card.tsx
create mode 100644 apps/app/components/inbox/inbox-main-content.tsx
create mode 100644 apps/app/components/inbox/index.ts
create mode 100644 apps/app/components/inbox/issues-list-sidebar.tsx
create mode 100644 apps/app/components/inbox/select-duplicate.tsx
create mode 100644 apps/app/constants/inbox.ts
create mode 100644 apps/app/contexts/inbox-view-context.tsx
create mode 100644 apps/app/hooks/use-inbox-view.tsx
create mode 100644 apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx
create mode 100644 apps/app/public/empty-state/empty-inbox.svg
create mode 100644 apps/app/services/inbox.service.ts
create mode 100644 apps/app/types/inbox.d.ts
diff --git a/apiserver/plane/api/permissions/__init__.py b/apiserver/plane/api/permissions/__init__.py
index 71ec4815d..91b3aea35 100644
--- a/apiserver/plane/api/permissions/__init__.py
+++ b/apiserver/plane/api/permissions/__init__.py
@@ -1,2 +1,2 @@
-from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission
-from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission
+from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission
+from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, ProjectLitePermission
diff --git a/apiserver/plane/api/permissions/project.py b/apiserver/plane/api/permissions/project.py
index eea5192d5..e4e3e0f9b 100644
--- a/apiserver/plane/api/permissions/project.py
+++ b/apiserver/plane/api/permissions/project.py
@@ -89,3 +89,16 @@ class ProjectEntityPermission(BasePermission):
role__in=[Admin, Member],
project_id=view.project_id,
).exists()
+
+
+class ProjectLitePermission(BasePermission):
+
+ def has_permission(self, request, view):
+ if request.user.is_anonymous:
+ return False
+
+ return ProjectMember.objects.filter(
+ workspace__slug=view.workspace_slug,
+ member=request.user,
+ project_id=view.project_id,
+ ).exists()
\ No newline at end of file
diff --git a/apiserver/plane/api/permissions/workspace.py b/apiserver/plane/api/permissions/workspace.py
index 2a2e1d339..7fccc455e 100644
--- a/apiserver/plane/api/permissions/workspace.py
+++ b/apiserver/plane/api/permissions/workspace.py
@@ -5,7 +5,6 @@ from rest_framework.permissions import BasePermission, SAFE_METHODS
from plane.db.models import WorkspaceMember
-
# Permission Mappings
Owner = 20
Admin = 15
@@ -44,7 +43,6 @@ class WorkSpaceBasePermission(BasePermission):
class WorkSpaceAdminPermission(BasePermission):
def has_permission(self, request, view):
-
if request.user.is_anonymous:
return False
@@ -53,3 +51,13 @@ class WorkSpaceAdminPermission(BasePermission):
workspace__slug=view.workspace_slug,
role__in=[Owner, Admin],
).exists()
+
+
+class WorkspaceEntityPermission(BasePermission):
+ def has_permission(self, request, view):
+ if request.user.is_anonymous:
+ return False
+
+ return WorkspaceMember.objects.filter(
+ member=request.user, workspace__slug=view.workspace_slug
+ ).exists()
diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py
index 505a9978d..cb94f8068 100644
--- a/apiserver/plane/api/serializers/__init__.py
+++ b/apiserver/plane/api/serializers/__init__.py
@@ -69,6 +69,11 @@ from .importer import ImporterSerializer
from .page import PageSerializer, PageBlockSerializer, PageFavoriteSerializer
-from .estimate import EstimateSerializer, EstimatePointSerializer, EstimateReadSerializer
+from .estimate import (
+ EstimateSerializer,
+ EstimatePointSerializer,
+ EstimateReadSerializer,
+)
+from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer
from .analytic import AnalyticViewSerializer
diff --git a/apiserver/plane/api/serializers/inbox.py b/apiserver/plane/api/serializers/inbox.py
new file mode 100644
index 000000000..ae17b749b
--- /dev/null
+++ b/apiserver/plane/api/serializers/inbox.py
@@ -0,0 +1,58 @@
+# Third party frameworks
+from rest_framework import serializers
+
+# Module imports
+from .base import BaseSerializer
+from .issue import IssueFlatSerializer, LabelLiteSerializer
+from .project import ProjectLiteSerializer
+from .state import StateLiteSerializer
+from .project import ProjectLiteSerializer
+from .user import UserLiteSerializer
+from plane.db.models import Inbox, InboxIssue, Issue
+
+
+class InboxSerializer(BaseSerializer):
+ project_detail = ProjectLiteSerializer(source="project", read_only=True)
+ pending_issue_count = serializers.IntegerField(read_only=True)
+
+ class Meta:
+ model = Inbox
+ fields = "__all__"
+ read_only_fields = [
+ "project",
+ "workspace",
+ ]
+
+
+class InboxIssueSerializer(BaseSerializer):
+ issue_detail = IssueFlatSerializer(source="issue", read_only=True)
+ project_detail = ProjectLiteSerializer(source="project", read_only=True)
+
+ class Meta:
+ model = InboxIssue
+ fields = "__all__"
+ read_only_fields = [
+ "project",
+ "workspace",
+ ]
+
+
+class InboxIssueLiteSerializer(BaseSerializer):
+ class Meta:
+ model = InboxIssue
+ fields = ["id", "status", "duplicate_to", "snoozed_till", "source"]
+ read_only_fields = fields
+
+
+class IssueStateInboxSerializer(BaseSerializer):
+ state_detail = StateLiteSerializer(read_only=True, source="state")
+ project_detail = ProjectLiteSerializer(read_only=True, source="project")
+ label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
+ assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
+ sub_issues_count = serializers.IntegerField(read_only=True)
+ bridge_id = serializers.UUIDField(read_only=True)
+ issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
+
+ class Meta:
+ model = Issue
+ fields = "__all__"
diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py
index d3c17d057..e48259370 100644
--- a/apiserver/plane/api/serializers/issue.py
+++ b/apiserver/plane/api/serializers/issue.py
@@ -41,6 +41,7 @@ class IssueFlatSerializer(BaseSerializer):
"id",
"name",
"description",
+ "description_html",
"priority",
"start_date",
"target_date",
diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py
index 23277f294..7b2c6f76e 100644
--- a/apiserver/plane/api/urls.py
+++ b/apiserver/plane/api/urls.py
@@ -141,6 +141,10 @@ from plane.api.views import (
# Release Notes
ReleaseNotesEndpoint,
## End Release Notes
+ # Inbox
+ InboxViewSet,
+ InboxIssueViewSet,
+ ## End Inbox
# Analytics
AnalyticsEndpoint,
AnalyticViewViewset,
@@ -1244,6 +1248,50 @@ urlpatterns = [
name="release-notes",
),
## End Release Notes
+ # Inbox
+ path(
+ "workspaces//projects//inboxes/",
+ InboxViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="inbox",
+ ),
+ path(
+ "workspaces//projects//inboxes//",
+ InboxViewSet.as_view(
+ {
+ "get": "retrieve",
+ "patch": "partial_update",
+ "delete": "destroy",
+ }
+ ),
+ name="inbox",
+ ),
+ path(
+ "workspaces//projects//inboxes//inbox-issues/",
+ InboxIssueViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="inbox-issue",
+ ),
+ path(
+ "workspaces//projects//inboxes//inbox-issues//",
+ InboxIssueViewSet.as_view(
+ {
+ "get": "retrieve",
+ "patch": "partial_update",
+ "delete": "destroy",
+ }
+ ),
+ name="inbox-issue",
+ ),
+ ## End Inbox
# Analytics
path(
"workspaces//analytics/",
diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py
index 45ea35f71..12299f79b 100644
--- a/apiserver/plane/api/views/__init__.py
+++ b/apiserver/plane/api/views/__init__.py
@@ -134,6 +134,7 @@ from .estimate import (
from .release import ReleaseNotesEndpoint
+from .inbox import InboxViewSet, InboxIssueViewSet
from .analytic import (
AnalyticsEndpoint,
AnalyticViewViewset,
diff --git a/apiserver/plane/api/views/analytic.py b/apiserver/plane/api/views/analytic.py
index 56ca12bae..a096c2700 100644
--- a/apiserver/plane/api/views/analytic.py
+++ b/apiserver/plane/api/views/analytic.py
@@ -40,7 +40,7 @@ class AnalyticsEndpoint(BaseAPIView):
segment = request.GET.get("segment", False)
filters = issue_filters(request.GET, "GET")
- queryset = Issue.objects.filter(workspace__slug=slug, **filters)
+ queryset = Issue.issue_objects.filter(workspace__slug=slug, **filters)
total_issues = queryset.count()
distribution = build_graph_plot(
@@ -79,7 +79,7 @@ class AnalyticsEndpoint(BaseAPIView):
assignee_details = {}
if x_axis in ["assignees__email"] or segment in ["assignees__email"]:
assignee_details = (
- Issue.objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
+ Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
.order_by("assignees__id")
.distinct("assignees__id")
.values("assignees__avatar", "assignees__email", "assignees__first_name", "assignees__last_name")
@@ -132,7 +132,7 @@ class SavedAnalyticEndpoint(BaseAPIView):
)
filter = analytic_view.query
- queryset = Issue.objects.filter(**filter)
+ queryset = Issue.issue_objects.filter(**filter)
x_axis = analytic_view.query_dict.get("x_axis", False)
y_axis = analytic_view.query_dict.get("y_axis", False)
@@ -209,7 +209,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
try:
filters = issue_filters(request.GET, "GET")
- queryset = Issue.objects.filter(workspace__slug=slug, **filters)
+ queryset = Issue.issue_objects.filter(workspace__slug=slug, **filters)
total_issues = queryset.count()
diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py
index 63c832e71..86c1002d1 100644
--- a/apiserver/plane/api/views/cycle.py
+++ b/apiserver/plane/api/views/cycle.py
@@ -323,7 +323,7 @@ class CycleIssueViewSet(BaseViewSet):
super()
.get_queryset()
.annotate(
- sub_issues_count=Issue.objects.filter(parent=OuterRef("issue_id"))
+ sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -347,9 +347,9 @@ class CycleIssueViewSet(BaseViewSet):
group_by = request.GET.get("group_by", False)
filters = issue_filters(request.query_params, "GET")
issues = (
- Issue.objects.filter(issue_cycle__cycle_id=cycle_id)
+ Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
.annotate(
- sub_issues_count=Issue.objects.filter(parent=OuterRef("id"))
+ sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
diff --git a/apiserver/plane/api/views/importer.py b/apiserver/plane/api/views/importer.py
index 2e0f1cec0..53bcae0e8 100644
--- a/apiserver/plane/api/views/importer.py
+++ b/apiserver/plane/api/views/importer.py
@@ -241,7 +241,7 @@ class ImportServiceEndpoint(BaseAPIView):
)
# Delete all imported Issues
imported_issues = importer.imported_data.get("issues", [])
- Issue.objects.filter(id__in=imported_issues).delete()
+ Issue.issue_objects.filter(id__in=imported_issues).delete()
# Delete all imported Labels
imported_labels = importer.imported_data.get("labels", [])
diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py
new file mode 100644
index 000000000..f76638c17
--- /dev/null
+++ b/apiserver/plane/api/views/inbox.py
@@ -0,0 +1,349 @@
+# Python imports
+import json
+
+# Django import
+from django.utils import timezone
+from django.db.models import Q, Count, OuterRef, Func, F, Prefetch
+from django.core.serializers.json import DjangoJSONEncoder
+
+# Third party imports
+from rest_framework import status
+from rest_framework.response import Response
+from sentry_sdk import capture_exception
+
+# Module imports
+from .base import BaseViewSet
+from plane.api.permissions import ProjectBasePermission, ProjectLitePermission
+from plane.db.models import (
+ Project,
+ Inbox,
+ InboxIssue,
+ Issue,
+ State,
+ IssueLink,
+ IssueAttachment,
+ IssueActivity,
+)
+from plane.api.serializers import (
+ IssueSerializer,
+ InboxSerializer,
+ InboxIssueSerializer,
+ IssueCreateSerializer,
+ IssueStateInboxSerializer,
+)
+from plane.utils.issue_filters import issue_filters
+from plane.bgtasks.issue_activites_task import issue_activity
+
+
+class InboxViewSet(BaseViewSet):
+ permission_classes = [
+ ProjectBasePermission,
+ ]
+
+ serializer_class = InboxSerializer
+ model = Inbox
+
+ def get_queryset(self):
+ return (
+ super()
+ .get_queryset()
+ .filter(
+ workspace__slug=self.kwargs.get("slug"),
+ project_id=self.kwargs.get("project_id"),
+ )
+ .annotate(
+ pending_issue_count=Count(
+ "issue_inbox",
+ filter=Q(issue_inbox__status=-2),
+ )
+ )
+ .select_related("workspace", "project")
+ )
+
+ def perform_create(self, serializer):
+ serializer.save(project_id=self.kwargs.get("project_id"))
+
+ def destroy(self, request, slug, project_id, pk):
+ try:
+ inbox = Inbox.objects.get(
+ workspace__slug=slug, project_id=project_id, pk=pk
+ )
+
+ if inbox.is_default:
+ return Response(
+ {"error": "You cannot delete the default inbox"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ inbox.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
+ except Exception as e:
+ capture_exception(e)
+ return Response(
+ {"error": "Something went wronf please try again later"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+
+class InboxIssueViewSet(BaseViewSet):
+ permission_classes = [
+ ProjectLitePermission,
+ ]
+
+ serializer_class = InboxIssueSerializer
+ model = InboxIssue
+
+ filterset_fields = [
+ "status",
+ ]
+
+ def get_queryset(self):
+ return self.filter_queryset(
+ super()
+ .get_queryset()
+ .filter(
+ Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
+ workspace__slug=self.kwargs.get("slug"),
+ project_id=self.kwargs.get("project_id"),
+ inbox_id=self.kwargs.get("inbox_id"),
+ )
+ .select_related("issue", "workspace", "project")
+ )
+
+ def list(self, request, slug, project_id, inbox_id):
+ try:
+ order_by = request.GET.get("order_by", "created_at")
+ filters = issue_filters(request.query_params, "GET")
+ issues = (
+ Issue.objects.filter(
+ issue_inbox__inbox_id=inbox_id,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ .annotate(
+ sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
+ .order_by()
+ .annotate(count=Func(F("id"), function="Count"))
+ .values("count")
+ )
+ .annotate(bridge_id=F("issue_inbox__id"))
+ .filter(project_id=project_id)
+ .filter(workspace__slug=slug)
+ .select_related("project")
+ .select_related("workspace")
+ .select_related("state")
+ .select_related("parent")
+ .prefetch_related("assignees")
+ .prefetch_related("labels")
+ .order_by(order_by)
+ .filter(**filters)
+ .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")
+ )
+ .prefetch_related(
+ Prefetch(
+ "issue_inbox",
+ queryset=InboxIssue.objects.only(
+ "status", "duplicate_to", "snoozed_till", "source"
+ ),
+ )
+ )
+ )
+ issues_data = IssueStateInboxSerializer(issues, many=True).data
+ return Response(
+ issues_data,
+ status=status.HTTP_200_OK,
+ )
+
+ except Exception as e:
+ capture_exception(e)
+ return Response(
+ {"error": "Something went wrong please try again later"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ def create(self, request, slug, project_id, inbox_id):
+ try:
+ if not request.data.get("issue", {}).get("name", False):
+ return Response(
+ {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
+ )
+
+ if not request.data.get("issue", {}).get("priority", "low") in [
+ "low",
+ "medium",
+ "high",
+ "urgent",
+ None,
+ ]:
+ return Response(
+ {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # Create or get state
+ state, _ = State.objects.get_or_create(
+ name="Triage",
+ group="backlog",
+ description="Default state for managing all Inbox Issues",
+ project_id=project_id,
+ color="#ff7700",
+ )
+
+ # create an issue
+ issue = Issue.objects.create(
+ name=request.data.get("issue", {}).get("name"),
+ description=request.data.get("issue", {}).get("description", {}),
+ description_html=request.data.get("issue", {}).get(
+ "description_html", "
"
+ ),
+ priority=request.data.get("issue", {}).get("priority", "low"),
+ project_id=project_id,
+ state=state,
+ )
+
+ # Create an Issue Activity
+ # Track the issue
+ issue_activity.delay(
+ type="issue.activity.created",
+ requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
+ actor_id=str(request.user.id),
+ issue_id=str(issue.id),
+ project_id=str(project_id),
+ current_instance=None,
+ )
+ # create an inbox issue
+ InboxIssue.objects.create(
+ inbox_id=inbox_id,
+ project_id=project_id,
+ issue=issue,
+ source=request.data.get("source", "in-app"),
+ )
+
+ serializer = IssueStateInboxSerializer(issue)
+
+ return Response(serializer.data, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ capture_exception(e)
+ return Response(
+ {"error": "Something went wrong please try again later"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ def partial_update(self, request, slug, project_id, inbox_id, pk):
+ try:
+ inbox_issue = InboxIssue.objects.get(
+ pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
+ )
+
+ issue_data = request.data.pop("issue", False)
+
+ if bool(issue_data):
+ issue = Issue.objects.get(
+ pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
+ )
+ issue_serializer = IssueCreateSerializer(
+ issue, data=issue_data, partial=True
+ )
+
+ if issue_serializer.is_valid():
+ current_instance = issue
+ issue_serializer.save()
+ # Log all the updates
+ requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
+ if issue is not None:
+ issue_activity.delay(
+ type="issue.activity.updated",
+ requested_data=requested_data,
+ actor_id=str(request.user.id),
+ issue_id=str(issue.id),
+ project_id=str(project_id),
+ current_instance=json.dumps(
+ IssueSerializer(current_instance).data,
+ cls=DjangoJSONEncoder,
+ ),
+ )
+ else:
+ return Response(
+ issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
+ )
+
+ serializer = InboxIssueSerializer(
+ inbox_issue, data=request.data, partial=True
+ )
+
+ if serializer.is_valid():
+ serializer.save()
+ # Update the issue state if the issue is rejected or marked as duplicate
+ if serializer.data["status"] in [-1, 2]:
+ issue = Issue.objects.get(
+ pk=inbox_issue.issue_id,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ state = State.objects.filter(
+ group="cancelled", workspace__slug=slug, project_id=project_id
+ ).first()
+ if state is not None:
+ issue.state = state
+ issue.save()
+
+ # Update the issue state if it is accepted
+ if serializer.data["status"] in [1]:
+ issue = Issue.objects.get(
+ pk=inbox_issue.issue_id,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+
+ # Update the issue state only if it is in triage state
+ if issue.state.name == "Triage":
+ # Move to default state
+ state = State.objects.filter(
+ workspace__slug=slug, project_id=project_id, default=True
+ ).first()
+ if state is not None:
+ issue.state = state
+ issue.save()
+
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ except InboxIssue.DoesNotExist:
+ return Response(
+ {"error": "Inbox Issue does not exist"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+ except Exception as e:
+ capture_exception(e)
+ return Response(
+ {"error": "Something went wrong please try again later"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ def retrieve(self, request, slug, project_id, inbox_id, pk):
+ try:
+ inbox_issue = InboxIssue.objects.get(
+ pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
+ )
+ issue = Issue.objects.get(
+ pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
+ )
+ serializer = IssueStateInboxSerializer(issue)
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ except Exception as e:
+ capture_exception(e)
+ return Response(
+ {"error": "Something went wrong please try again later"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py
index e6c37374b..794fecf5c 100644
--- a/apiserver/plane/api/views/issue.py
+++ b/apiserver/plane/api/views/issue.py
@@ -132,10 +132,8 @@ class IssueViewSet(BaseViewSet):
def get_queryset(self):
return (
- super()
- .get_queryset()
- .annotate(
- sub_issues_count=Issue.objects.filter(parent=OuterRef("id"))
+ 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")
@@ -248,7 +246,7 @@ class IssueViewSet(BaseViewSet):
def retrieve(self, request, slug, project_id, pk=None):
try:
- issue = Issue.objects.get(
+ issue = Issue.issue_objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
@@ -263,9 +261,9 @@ class UserWorkSpaceIssues(BaseAPIView):
def get(self, request, slug):
try:
issues = (
- Issue.objects.filter(assignees__in=[request.user], workspace__slug=slug)
+ Issue.issue_objects.filter(assignees__in=[request.user], workspace__slug=slug)
.annotate(
- sub_issues_count=Issue.objects.filter(parent=OuterRef("id"))
+ sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -311,7 +309,7 @@ class WorkSpaceIssuesEndpoint(BaseAPIView):
def get(self, request, slug):
try:
issues = (
- Issue.objects.filter(workspace__slug=slug)
+ Issue.issue_objects.filter(workspace__slug=slug)
.filter(project__project_projectmember__member=self.request.user)
.order_by("-created_at")
)
@@ -581,7 +579,7 @@ class BulkDeleteIssuesEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
- issues = Issue.objects.filter(
+ issues = Issue.issue_objects.filter(
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
)
@@ -610,7 +608,7 @@ class SubIssuesEndpoint(BaseAPIView):
def get(self, request, slug, project_id, issue_id):
try:
sub_issues = (
- Issue.objects.filter(
+ Issue.issue_objects.filter(
parent_id=issue_id, workspace__slug=slug, project_id=project_id
)
.select_related("project")
@@ -656,7 +654,7 @@ class SubIssuesEndpoint(BaseAPIView):
# Assign multiple sub issues
def post(self, request, slug, project_id, issue_id):
try:
- parent_issue = Issue.objects.get(pk=issue_id)
+ parent_issue = Issue.issue_objects.get(pk=issue_id)
sub_issue_ids = request.data.get("sub_issue_ids", [])
if not len(sub_issue_ids):
@@ -665,14 +663,14 @@ class SubIssuesEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
- sub_issues = Issue.objects.filter(id__in=sub_issue_ids)
+ sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids)
for sub_issue in sub_issues:
sub_issue.parent = parent_issue
_ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10)
- updated_sub_issues = Issue.objects.filter(id__in=sub_issue_ids)
+ updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids)
return Response(
IssueFlatSerializer(updated_sub_issues, many=True).data,
diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py
index 8f0cabeaf..7c285c242 100644
--- a/apiserver/plane/api/views/module.py
+++ b/apiserver/plane/api/views/module.py
@@ -201,7 +201,7 @@ class ModuleIssueViewSet(BaseViewSet):
super()
.get_queryset()
.annotate(
- sub_issues_count=Issue.objects.filter(parent=OuterRef("issue"))
+ sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -226,9 +226,9 @@ class ModuleIssueViewSet(BaseViewSet):
group_by = request.GET.get("group_by", False)
filters = issue_filters(request.query_params, "GET")
issues = (
- Issue.objects.filter(issue_module__module_id=module_id)
+ Issue.issue_objects.filter(issue_module__module_id=module_id)
.annotate(
- sub_issues_count=Issue.objects.filter(parent=OuterRef("id"))
+ sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
diff --git a/apiserver/plane/api/views/people.py b/apiserver/plane/api/views/people.py
index fcf95ff64..9dad5380b 100644
--- a/apiserver/plane/api/views/people.py
+++ b/apiserver/plane/api/views/people.py
@@ -37,7 +37,7 @@ class UserEndpoint(BaseViewSet):
workspace_invites = WorkspaceMemberInvite.objects.filter(
email=request.user.email
).count()
- assigned_issues = Issue.objects.filter(assignees__in=[request.user]).count()
+ assigned_issues = Issue.issue_objects.filter(assignees__in=[request.user]).count()
serialized_data = UserSerializer(request.user).data
serialized_data["workspace"] = {
@@ -59,7 +59,7 @@ class UserEndpoint(BaseViewSet):
workspace_invites = WorkspaceMemberInvite.objects.filter(
email=request.user.email
).count()
- assigned_issues = Issue.objects.filter(assignees__in=[request.user]).count()
+ assigned_issues = Issue.issue_objects.filter(assignees__in=[request.user]).count()
fallback_workspace = Workspace.objects.filter(
workspace_member__member=request.user
diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py
index e49c2fe78..68a34ab48 100644
--- a/apiserver/plane/api/views/project.py
+++ b/apiserver/plane/api/views/project.py
@@ -47,9 +47,9 @@ from plane.db.models import (
Page,
IssueAssignee,
ModuleMember,
+ Inbox,
)
-
from plane.bgtasks.project_invitation_task import project_invitation
@@ -248,6 +248,20 @@ class ProjectViewSet(BaseViewSet):
if serializer.is_valid():
serializer.save()
+ if serializer.data["inbox_view"]:
+ Inbox.objects.get_or_create(
+ name=f"{project.name} Inbox", project=project, is_default=True
+ )
+
+ # Create the triage state in Backlog group
+ State.objects.get_or_create(
+ name="Triage",
+ group="backlog",
+ description="Default state for managing all Inbox Issues",
+ project_id=pk,
+ color="#ff7700"
+ )
+
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -477,7 +491,9 @@ class ProjectMemberViewSet(BaseViewSet):
)
if requesting_project_member.role < project_member.role:
return Response(
- {"error": "You cannot remove a user having role higher than yourself"},
+ {
+ "error": "You cannot remove a user having role higher than yourself"
+ },
status=status.HTTP_400_BAD_REQUEST,
)
diff --git a/apiserver/plane/api/views/search.py b/apiserver/plane/api/views/search.py
index 88dddc43c..078a9a6a5 100644
--- a/apiserver/plane/api/views/search.py
+++ b/apiserver/plane/api/views/search.py
@@ -57,7 +57,7 @@ class GlobalSearchEndpoint(BaseAPIView):
else:
q |= Q(**{f"{field}__icontains": query})
return (
- Issue.objects.filter(
+ Issue.issue_objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
@@ -210,7 +210,7 @@ class IssueSearchEndpoint(BaseAPIView):
blocker_blocked_by = request.query_params.get("blocker_blocked_by", False)
issue_id = request.query_params.get("issue_id", False)
- issues = Issue.objects.filter(
+ issues = Issue.issue_objects.filter(
workspace__slug=slug,
project_id=project_id,
project__project_projectmember__member=self.request.user,
@@ -220,16 +220,16 @@ class IssueSearchEndpoint(BaseAPIView):
issues = search_issues(query, issues)
if parent == "true" and issue_id:
- issue = Issue.objects.get(pk=issue_id)
+ issue = Issue.issue_objects.get(pk=issue_id)
issues = issues.filter(
~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True
).exclude(
- pk__in=Issue.objects.filter(parent__isnull=False).values_list(
+ pk__in=Issue.issue_objects.filter(parent__isnull=False).values_list(
"parent_id", flat=True
)
)
if blocker_blocked_by == "true" and issue_id:
- issue = Issue.objects.get(pk=issue_id)
+ issue = Issue.issue_objects.get(pk=issue_id)
issues = issues.filter(
~Q(pk=issue_id),
~Q(blocked_issues__block=issue),
diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py
index b217a662d..29cba7a74 100644
--- a/apiserver/plane/api/views/state.py
+++ b/apiserver/plane/api/views/state.py
@@ -89,7 +89,7 @@ class StateViewSet(BaseViewSet):
)
# Check for any issues in the state
- issue_exist = Issue.objects.filter(state=pk).exists()
+ issue_exist = Issue.issue_objects.filter(state=pk).exists()
if issue_exist:
return Response(
diff --git a/apiserver/plane/api/views/view.py b/apiserver/plane/api/views/view.py
index 1b6fb42cc..874bb94fb 100644
--- a/apiserver/plane/api/views/view.py
+++ b/apiserver/plane/api/views/view.py
@@ -67,7 +67,7 @@ class ViewIssuesEndpoint(BaseAPIView):
filters = issue_filters(request.query_params, "GET")
issues = (
- Issue.objects.filter(
+ Issue.issue_objects.filter(
**queries, project_id=project_id, workspace__slug=slug
)
.filter(**filters)
diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py
index c1cdcc4b4..9b7347b4d 100644
--- a/apiserver/plane/api/views/workspace.py
+++ b/apiserver/plane/api/views/workspace.py
@@ -755,7 +755,7 @@ class UserIssueCompletedGraphEndpoint(BaseAPIView):
month = request.GET.get("month", 1)
issues = (
- Issue.objects.filter(
+ Issue.issue_objects.filter(
assignees__in=[request.user],
workspace__slug=slug,
completed_at__month=month,
@@ -800,7 +800,7 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
month = request.GET.get("month", 1)
completed_issues = (
- Issue.objects.filter(
+ Issue.issue_objects.filter(
assignees__in=[request.user],
workspace__slug=slug,
completed_at__month=month,
@@ -813,24 +813,24 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
.order_by("week_in_month")
)
- assigned_issues = Issue.objects.filter(
+ assigned_issues = Issue.issue_objects.filter(
workspace__slug=slug, assignees__in=[request.user]
).count()
- pending_issues_count = Issue.objects.filter(
+ pending_issues_count = Issue.issue_objects.filter(
~Q(state__group__in=["completed", "cancelled"]),
workspace__slug=slug,
assignees__in=[request.user],
).count()
- completed_issues_count = Issue.objects.filter(
+ completed_issues_count = Issue.issue_objects.filter(
workspace__slug=slug,
assignees__in=[request.user],
state__group="completed",
).count()
issues_due_week = (
- Issue.objects.filter(
+ Issue.issue_objects.filter(
workspace__slug=slug,
assignees__in=[request.user],
)
@@ -840,14 +840,14 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
)
state_distribution = (
- 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"))
.order_by("state_group")
)
- overdue_issues = Issue.objects.filter(
+ overdue_issues = Issue.issue_objects.filter(
~Q(state__group__in=["completed", "cancelled"]),
workspace__slug=slug,
assignees__in=[request.user],
@@ -855,7 +855,7 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
completed_at__isnull=True,
).values("id", "name", "workspace__slug", "project_id", "target_date")
- upcoming_issues = Issue.objects.filter(
+ upcoming_issues = Issue.issue_objects.filter(
~Q(state__group__in=["completed", "cancelled"]),
target_date__gte=timezone.now(),
workspace__slug=slug,
diff --git a/apiserver/plane/bgtasks/analytic_plot_export.py b/apiserver/plane/bgtasks/analytic_plot_export.py
index 7f276be82..37362416f 100644
--- a/apiserver/plane/bgtasks/analytic_plot_export.py
+++ b/apiserver/plane/bgtasks/analytic_plot_export.py
@@ -36,7 +36,7 @@ row_mapping = {
def analytic_export_task(email, data, slug):
try:
filters = issue_filters(data, "POST")
- queryset = Issue.objects.filter(**filters, workspace__slug=slug)
+ queryset = Issue.issue_objects.filter(**filters, workspace__slug=slug)
x_axis = data.get("x_axis", False)
y_axis = data.get("y_axis", False)
@@ -53,7 +53,7 @@ def analytic_export_task(email, data, slug):
assignee_details = {}
if x_axis in ["assignees__email"] or segment in ["assignees__email"]:
assignee_details = (
- Issue.objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
+ Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
.order_by("assignees__id")
.distinct("assignees__id")
.values("assignees__avatar", "assignees__email", "assignees__first_name", "assignees__last_name")
diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py
index 417fe2324..b1096e30b 100644
--- a/apiserver/plane/bgtasks/issue_activites_task.py
+++ b/apiserver/plane/bgtasks/issue_activites_task.py
@@ -44,7 +44,7 @@ def track_name(
field="name",
project=project,
workspace=project.workspace,
- comment=f"{actor.email} updated the start date to {requested_data.get('name')}",
+ comment=f"{actor.email} updated the name to {requested_data.get('name')}",
)
)
diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py
index 53b501716..0d236fe21 100644
--- a/apiserver/plane/db/models/__init__.py
+++ b/apiserver/plane/db/models/__init__.py
@@ -68,4 +68,5 @@ from .page import Page, PageBlock, PageFavorite, PageLabel
from .estimate import Estimate, EstimatePoint
-from .analytic import AnalyticView
\ No newline at end of file
+from .inbox import Inbox, InboxIssue
+from .analytic import AnalyticView
diff --git a/apiserver/plane/db/models/inbox.py b/apiserver/plane/db/models/inbox.py
new file mode 100644
index 000000000..497a20f00
--- /dev/null
+++ b/apiserver/plane/db/models/inbox.py
@@ -0,0 +1,51 @@
+# Django imports
+from django.db import models
+
+# Module imports
+from plane.db.models import ProjectBaseModel
+
+
+class Inbox(ProjectBaseModel):
+ name = models.CharField(max_length=255)
+ description = models.TextField(verbose_name="Inbox Description", blank=True)
+ is_default = models.BooleanField(default=False)
+ view_props = models.JSONField(default=dict)
+
+ def __str__(self):
+ """Return name of the Inbox"""
+ return f"{self.name} <{self.project.name}>"
+
+ class Meta:
+ unique_together = ["name", "project"]
+ verbose_name = "Inbox"
+ verbose_name_plural = "Inboxes"
+ db_table = "inboxes"
+ ordering = ("name",)
+
+
+class InboxIssue(ProjectBaseModel):
+ inbox = models.ForeignKey(
+ "db.Inbox", related_name="issue_inbox", on_delete=models.CASCADE
+ )
+ issue = models.ForeignKey(
+ "db.Issue", related_name="issue_inbox", on_delete=models.CASCADE
+ )
+ status = models.IntegerField(
+ choices=((-2, "Pending"), (-1, "Rejected"), (0, "Snoozed"), (1, "Accepted"), (2, "Duplicate")),
+ default=-2,
+ )
+ snoozed_till = models.DateTimeField(null=True)
+ duplicate_to = models.ForeignKey(
+ "db.Issue", related_name="inbox_duplicate", on_delete=models.SET_NULL, null=True
+ )
+ source = models.TextField(blank=True, null=True)
+
+ class Meta:
+ verbose_name = "InboxIssue"
+ verbose_name_plural = "InboxIssues"
+ db_table = "inbox_issues"
+ ordering = ("-created_at",)
+
+ def __str__(self):
+ """Return name of the Issue"""
+ return f"{self.issue.name} <{self.inbox.name}>"
diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py
index e25695c42..dcb7d20c4 100644
--- a/apiserver/plane/db/models/issue.py
+++ b/apiserver/plane/db/models/issue.py
@@ -17,6 +17,20 @@ from plane.utils.html_processor import strip_tags
# TODO: Handle identifiers for Bulk Inserts - nk
+class IssueManager(models.Manager):
+ def get_queryset(self):
+ return (
+ super()
+ .get_queryset()
+ .filter(
+ models.Q(issue_inbox__status=1)
+ | models.Q(issue_inbox__status=-1)
+ | models.Q(issue_inbox__status=2)
+ | models.Q(issue_inbox__isnull=True)
+ )
+ )
+
+
class Issue(ProjectBaseModel):
PRIORITY_CHOICES = (
("urgent", "Urgent"),
@@ -68,6 +82,9 @@ class Issue(ProjectBaseModel):
sort_order = models.FloatField(default=65535)
completed_at = models.DateTimeField(null=True)
+ objects = models.Manager()
+ issue_objects = IssueManager()
+
class Meta:
verbose_name = "Issue"
verbose_name_plural = "Issues"
diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py
index 41b1ac654..0b6c4b50d 100644
--- a/apiserver/plane/db/models/project.py
+++ b/apiserver/plane/db/models/project.py
@@ -69,6 +69,7 @@ class Project(BaseModel):
cycle_view = models.BooleanField(default=True)
issue_views_view = models.BooleanField(default=True)
page_view = models.BooleanField(default=True)
+ inbox_view = models.BooleanField(default=False)
cover_image = models.URLField(blank=True, null=True, max_length=800)
estimate = models.ForeignKey(
"db.Estimate", on_delete=models.SET_NULL, related_name="projects", null=True
diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py
index 944906f92..c5f147ea1 100644
--- a/apiserver/plane/utils/issue_filters.py
+++ b/apiserver/plane/utils/issue_filters.py
@@ -231,6 +231,17 @@ def filter_module(params, filter, method):
return filter
+def filter_inbox_status(params, filter, method):
+ if method == "GET":
+ status = params.get("inbox_status").split(",")
+ if len(status) and "" not in status:
+ filter["issue_inbox__status__in"] = status
+ else:
+ if params.get("inbox_status", None) and len(params.get("inbox_status")):
+ filter["issue_inbox__status__in"] = params.get("inbox_status")
+ return filter
+
+
def issue_filters(query_params, method):
filter = dict()
@@ -252,6 +263,7 @@ def issue_filters(query_params, method):
"project": filter_project,
"cycle": filter_cycle,
"module": filter_module,
+ "inbox_status": filter_inbox_status
}
for key, value in ISSUE_FILTER.items():
diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx
index ff889898e..ffbe67ca5 100644
--- a/apps/app/components/command-palette/command-pallette.tsx
+++ b/apps/app/components/command-palette/command-pallette.tsx
@@ -102,7 +102,7 @@ export const CommandPalette: React.FC = () => {
const page = pages[pages.length - 1];
const router = useRouter();
- const { workspaceSlug, projectId, issueId } = router.query;
+ const { workspaceSlug, projectId, issueId, inboxId } = router.query;
const { user } = useUser();
const { setToastAlert } = useToast();
@@ -145,7 +145,7 @@ export const CommandPalette: React.FC = () => {
console.error(e);
});
},
- [workspaceSlug, issueId, projectId]
+ [workspaceSlug, issueId, projectId, user]
);
const handleIssueAssignees = (assignee: string) => {
@@ -372,6 +372,7 @@ export const CommandPalette: React.FC = () => {
setIsIssueModalOpen(false)}
+ fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]}
/>
= ({
console.log(error);
});
},
- [workspaceSlug, projectId, cycleId, moduleId, groupTitle, index, selectedGroup, orderBy, params]
+ [
+ workspaceSlug,
+ projectId,
+ cycleId,
+ moduleId,
+ groupTitle,
+ index,
+ selectedGroup,
+ orderBy,
+ params,
+ user,
+ ]
);
const getStyle = (
diff --git a/apps/app/components/core/calendar-view/single-issue.tsx b/apps/app/components/core/calendar-view/single-issue.tsx
index 4fa9def3b..12fa60d01 100644
--- a/apps/app/components/core/calendar-view/single-issue.tsx
+++ b/apps/app/components/core/calendar-view/single-issue.tsx
@@ -105,7 +105,7 @@ export const SingleCalendarIssue: React.FC = ({
console.log(error);
});
},
- [workspaceSlug, projectId, cycleId, moduleId, params]
+ [workspaceSlug, projectId, cycleId, moduleId, viewId, params, user]
);
const handleCopyText = () => {
diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx
index bfa6b9f46..2e9e14ae3 100644
--- a/apps/app/components/core/issues-view.tsx
+++ b/apps/app/components/core/issues-view.tsx
@@ -276,6 +276,7 @@ export const IssuesView: React.FC = ({
handleDeleteIssue,
params,
states,
+ user,
]
);
diff --git a/apps/app/components/core/list-view/single-issue.tsx b/apps/app/components/core/list-view/single-issue.tsx
index ea4ebc811..f2a587899 100644
--- a/apps/app/components/core/list-view/single-issue.tsx
+++ b/apps/app/components/core/list-view/single-issue.tsx
@@ -154,7 +154,18 @@ export const SingleListIssue: React.FC = ({
} else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
});
},
- [workspaceSlug, projectId, cycleId, moduleId, groupTitle, index, selectedGroup, orderBy, params]
+ [
+ workspaceSlug,
+ projectId,
+ cycleId,
+ moduleId,
+ groupTitle,
+ index,
+ selectedGroup,
+ orderBy,
+ params,
+ user,
+ ]
);
const handleCopyText = () => {
diff --git a/apps/app/components/icons/inbox-icon.tsx b/apps/app/components/icons/inbox-icon.tsx
new file mode 100644
index 000000000..6013c8861
--- /dev/null
+++ b/apps/app/components/icons/inbox-icon.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+
+import type { Props } from "./types";
+
+export const InboxIcon: React.FC = ({
+ width = "24",
+ height = "24",
+ color = "#858E96",
+ className,
+}) => (
+
+
+
+);
diff --git a/apps/app/components/icons/index.ts b/apps/app/components/icons/index.ts
index 968a14a93..07ecafd24 100644
--- a/apps/app/components/icons/index.ts
+++ b/apps/app/components/icons/index.ts
@@ -78,3 +78,5 @@ export * from "./video-file-icon";
export * from "./audio-file-icon";
export * from "./command-icon";
export * from "./color-picker-icon";
+export * from "./inbox-icon";
+export * from "./stacked-layers-horizontal-icon";
diff --git a/apps/app/components/icons/stacked-layers-horizontal-icon.tsx b/apps/app/components/icons/stacked-layers-horizontal-icon.tsx
new file mode 100644
index 000000000..2d5f75ba9
--- /dev/null
+++ b/apps/app/components/icons/stacked-layers-horizontal-icon.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+
+import type { Props } from "./types";
+
+export const StackedLayersHorizontalIcon: React.FC = ({
+ width = "24",
+ height = "24",
+ className,
+ color = "#858e96",
+}) => (
+
+
+
+);
diff --git a/apps/app/components/inbox/decline-issue-modal.tsx b/apps/app/components/inbox/decline-issue-modal.tsx
new file mode 100644
index 000000000..64fe1682a
--- /dev/null
+++ b/apps/app/components/inbox/decline-issue-modal.tsx
@@ -0,0 +1,164 @@
+import React, { useEffect, useState } from "react";
+
+import { useRouter } from "next/router";
+
+import { mutate } from "swr";
+
+// headless ui
+import { Dialog, Transition } from "@headlessui/react";
+// services
+import inboxServices from "services/inbox.service";
+// hooks
+import useToast from "hooks/use-toast";
+import useInboxView from "hooks/use-inbox-view";
+import useUser from "hooks/use-user";
+// icons
+import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
+// ui
+import { SecondaryButton, DangerButton } from "components/ui";
+// types
+import type { IInboxIssue, ICurrentUserResponse, IInboxIssueDetail } from "types";
+// fetch-keys
+import { INBOX_ISSUES, INBOX_ISSUE_DETAILS } from "constants/fetch-keys";
+
+type Props = {
+ isOpen: boolean;
+ handleClose: () => void;
+ data: IInboxIssue | undefined;
+};
+
+export const DeclineIssueModal: React.FC = ({ isOpen, handleClose, data }) => {
+ const [isDeclining, setIsDeclining] = useState(false);
+
+ const router = useRouter();
+ const { workspaceSlug, projectId, inboxId } = router.query;
+
+ const { user } = useUser();
+ const { setToastAlert } = useToast();
+ const { params } = useInboxView();
+
+ const onClose = () => {
+ setIsDeclining(false);
+ handleClose();
+ };
+
+ const handleDecline = () => {
+ if (!workspaceSlug || !projectId || !inboxId || !data) return;
+
+ setIsDeclining(true);
+
+ inboxServices
+ .markInboxStatus(
+ workspaceSlug.toString(),
+ projectId.toString(),
+ inboxId.toString(),
+ data.bridge_id,
+ {
+ status: -1,
+ },
+ user
+ )
+ .then(() => {
+ mutate(
+ INBOX_ISSUE_DETAILS(inboxId.toString(), data.bridge_id),
+ (prevData) => {
+ if (!prevData) return prevData;
+
+ return {
+ ...prevData,
+ issue_inbox: [{ ...prevData.issue_inbox[0], status: -1 }],
+ };
+ },
+ false
+ );
+ mutate(
+ INBOX_ISSUES(inboxId.toString(), params),
+ (prevData) =>
+ prevData?.map((i) =>
+ i.bridge_id === data.bridge_id
+ ? { ...i, issue_inbox: [{ ...i.issue_inbox[0], status: -1 }] }
+ : i
+ ),
+ false
+ );
+
+ setToastAlert({
+ type: "success",
+ title: "Success!",
+ message: "Issue declined successfully.",
+ });
+ onClose();
+ })
+ .catch(() =>
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Issue could not be declined. Please try again.",
+ })
+ )
+ .finally(() => setIsDeclining(false));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Decline Issue
+
+
+
+
+ Are you sure you want to decline issue{" "}
+
+ {data?.project_detail?.identifier}-{data?.sequence_id}
+
+ {""}? This action cannot be undone.
+
+
+
+ Cancel
+
+ {isDeclining ? "Declining..." : "Decline Issue"}
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/app/components/inbox/delete-issue-modal.tsx b/apps/app/components/inbox/delete-issue-modal.tsx
new file mode 100644
index 000000000..46ba1ebdd
--- /dev/null
+++ b/apps/app/components/inbox/delete-issue-modal.tsx
@@ -0,0 +1,150 @@
+import React, { useState } from "react";
+
+import { useRouter } from "next/router";
+
+import { mutate } from "swr";
+
+// headless ui
+import { Dialog, Transition } from "@headlessui/react";
+// services
+import inboxServices from "services/inbox.service";
+// hooks
+import useToast from "hooks/use-toast";
+import useInboxView from "hooks/use-inbox-view";
+import useUser from "hooks/use-user";
+// icons
+import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
+// ui
+import { SecondaryButton, DangerButton } from "components/ui";
+// types
+import type { IInboxIssue } from "types";
+// fetch-keys
+import { INBOX_ISSUES } from "constants/fetch-keys";
+
+type Props = {
+ isOpen: boolean;
+ handleClose: () => void;
+ data: IInboxIssue | undefined;
+};
+
+export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data }) => {
+ const [isDeleting, setIsDeleting] = useState(false);
+
+ const router = useRouter();
+ const { workspaceSlug, projectId, inboxId } = router.query;
+
+ const { user } = useUser();
+ const { setToastAlert } = useToast();
+ const { params } = useInboxView();
+
+ const onClose = () => {
+ setIsDeleting(false);
+ handleClose();
+ };
+
+ const handleDelete = () => {
+ if (!workspaceSlug || !projectId || !inboxId || !data) return;
+
+ setIsDeleting(true);
+
+ inboxServices
+ .deleteInboxIssue(
+ workspaceSlug.toString(),
+ projectId.toString(),
+ inboxId.toString(),
+ data.bridge_id.toString(),
+ user
+ )
+ .then(() => {
+ mutate(
+ INBOX_ISSUES(inboxId.toString(), params),
+ (prevData) => (prevData ?? []).filter((i) => i.id !== data.id),
+ false
+ );
+
+ setToastAlert({
+ type: "success",
+ title: "Success!",
+ message: "Issue deleted successfully.",
+ });
+
+ // remove inboxIssueId from the url
+ router.push({
+ pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
+ });
+
+ onClose();
+ })
+ .catch(() =>
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Issue could not be deleted. Please try again.",
+ })
+ )
+ .finally(() => setIsDeleting(false));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Delete Issue
+
+
+
+
+ Are you sure you want to delete issue{" "}
+
+ {data?.project_detail?.identifier}-{data?.sequence_id}
+
+ {""}? This action cannot be undone.
+
+
+
+ Cancel
+
+ {isDeleting ? "Deleting..." : "Delete Issue"}
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/app/components/inbox/filters-dropdown.tsx b/apps/app/components/inbox/filters-dropdown.tsx
new file mode 100644
index 000000000..938e4fc01
--- /dev/null
+++ b/apps/app/components/inbox/filters-dropdown.tsx
@@ -0,0 +1,63 @@
+// ui
+import { MultiLevelDropdown } from "components/ui";
+// icons
+import { getPriorityIcon } from "components/icons";
+// types
+import { IInboxFilterOptions } from "types";
+// constants
+import { PRIORITIES } from "constants/project";
+import { STATUS } from "constants/inbox";
+
+type Props = {
+ filters: Partial;
+ onSelect: (option: any) => void;
+ direction?: "left" | "right";
+ height?: "sm" | "md" | "rg" | "lg";
+};
+
+export const FiltersDropdown: React.FC = ({ filters, onSelect, direction, height }) => (
+ ({
+ id: priority ?? "none",
+ label: (
+
+ {getPriorityIcon(priority)} {priority ?? "None"}
+
+ ),
+ value: {
+ key: "priority",
+ value: priority,
+ },
+ selected: filters?.priority?.includes(priority ?? "none"),
+ })),
+ ],
+ },
+ {
+ id: "inbox_status",
+ label: "Status",
+ value: Object.values(STATUS),
+ children: [
+ ...Object.keys(STATUS).map((status) => ({
+ id: status,
+ label: status,
+ value: {
+ key: "inbox_status",
+ value: STATUS[status],
+ },
+ selected: filters?.inbox_status?.includes(STATUS[status]),
+ })),
+ ],
+ },
+ ]}
+ />
+);
diff --git a/apps/app/components/inbox/inbox-action-headers.tsx b/apps/app/components/inbox/inbox-action-headers.tsx
new file mode 100644
index 000000000..8be550d98
--- /dev/null
+++ b/apps/app/components/inbox/inbox-action-headers.tsx
@@ -0,0 +1,226 @@
+import { useEffect, useState } from "react";
+
+import { useRouter } from "next/router";
+
+// react-datepicker
+import DatePicker from "react-datepicker";
+// headless ui
+import { Popover } from "@headlessui/react";
+// contexts
+import { useProjectMyMembership } from "contexts/project-member.context";
+// hooks
+import useInboxView from "hooks/use-inbox-view";
+import useUserAuth from "hooks/use-user-auth";
+// components
+import { FiltersDropdown } from "components/inbox";
+// ui
+import { PrimaryButton, SecondaryButton } from "components/ui";
+// icons
+import { InboxIcon, StackedLayersHorizontalIcon } from "components/icons";
+import {
+ ChevronDownIcon,
+ ChevronUpIcon,
+ CheckCircleIcon,
+ ClockIcon,
+ XCircleIcon,
+ TrashIcon,
+} from "@heroicons/react/24/outline";
+// types
+import type { IInboxIssue } from "types";
+
+type Props = {
+ issueCount: number;
+ currentIssueIndex: number;
+ issue?: IInboxIssue;
+ onAccept: () => Promise;
+ onDecline: () => void;
+ onMarkAsDuplicate: () => void;
+ onSnooze: (date: Date | string) => void;
+ onDelete: () => void;
+};
+
+export const InboxActionHeader: React.FC = (props) => {
+ const {
+ issueCount,
+ currentIssueIndex,
+ onAccept,
+ onDecline,
+ onMarkAsDuplicate,
+ onSnooze,
+ onDelete,
+ issue,
+ } = props;
+
+ const [isAccepting, setIsAccepting] = useState(false);
+ const [date, setDate] = useState(new Date());
+
+ const router = useRouter();
+ const { inboxIssueId } = router.query;
+
+ const { memberRole } = useProjectMyMembership();
+ const { filters, setFilters, filtersLength } = useInboxView();
+ const { user } = useUserAuth();
+
+ const handleAcceptIssue = () => {
+ setIsAccepting(true);
+
+ onAccept().finally(() => setIsAccepting(false));
+ };
+
+ useEffect(() => {
+ if (!issue?.issue_inbox[0].snoozed_till) return;
+
+ setDate(new Date(issue.issue_inbox[0].snoozed_till));
+ }, [issue]);
+
+ const issueStatus = issue?.issue_inbox[0].status;
+ const isAllowed = memberRole.isMember || memberRole.isOwner;
+
+ return (
+
+
+
+
+
Inbox
+
+
+
{
+ const key = option.key as keyof typeof filters;
+
+ const valueExists = (filters[key] as any[])?.includes(option.value);
+
+ if (valueExists) {
+ setFilters({
+ [option.key]: ((filters[key] ?? []) as any[])?.filter(
+ (val) => val !== option.value
+ ),
+ });
+ } else {
+ setFilters({
+ [option.key]: [...((filters[key] ?? []) as any[]), option.value],
+ });
+ }
+ }}
+ direction="right"
+ height="rg"
+ />
+ {filtersLength > 0 && (
+
+ {filtersLength}
+
+ )}
+
+
+ {inboxIssueId && (
+
+
+
{
+ const e = new KeyboardEvent("keydown", { key: "ArrowUp" });
+ document.dispatchEvent(e);
+ }}
+ >
+
+
+
{
+ const e = new KeyboardEvent("keydown", { key: "ArrowDown" });
+ document.dispatchEvent(e);
+ }}
+ >
+
+
+
+ {currentIssueIndex + 1}/{issueCount}
+
+
+
+ {isAllowed && (
+
+
+
+
+
+ Snooze
+
+
+
+ {({ close }) => (
+
+
{
+ if (!val) return;
+ setDate(val);
+ }}
+ dateFormat="dd-MM-yyyy"
+ inline
+ />
+ {
+ close();
+ onSnooze(date);
+ }}
+ >
+ Snooze
+
+
+ )}
+
+
+
+
+ Mark as duplicate
+
+
+
+ {isAccepting ? "Accepting..." : "Accept"}
+
+
+
+ Decline
+
+
+ )}
+ {(isAllowed || user?.id === issue?.created_by) && (
+
+
+
+ Delete
+
+
+ )}
+
+
+ )}
+
+ );
+};
diff --git a/apps/app/components/inbox/inbox-issue-card.tsx b/apps/app/components/inbox/inbox-issue-card.tsx
new file mode 100644
index 000000000..682c305a7
--- /dev/null
+++ b/apps/app/components/inbox/inbox-issue-card.tsx
@@ -0,0 +1,117 @@
+import { useRouter } from "next/router";
+import Link from "next/link";
+
+// ui
+import { Tooltip } from "components/ui";
+// icons
+import { getPriorityIcon, getStateGroupIcon } from "components/icons";
+import { CalendarDaysIcon, ClockIcon } from "@heroicons/react/24/outline";
+// helpers
+import { renderShortNumericDateFormat } from "helpers/date-time.helper";
+import { addSpaceIfCamelCase } from "helpers/string.helper";
+// types
+import type { IInboxIssue } from "types";
+
+type Props = {
+ issue: IInboxIssue;
+ active: boolean;
+};
+
+export const InboxIssueCard: React.FC = (props) => {
+ const { issue, active } = props;
+
+ const router = useRouter();
+ const { workspaceSlug, projectId, inboxId } = router.query;
+
+ const issueStatus = issue.issue_inbox[0].status;
+
+ return (
+
+
+
+
+
+
+ {issue.project_detail?.identifier}-{issue.sequence_id}
+
+
{issue.name}
+
+
+
+
+ {getStateGroupIcon(
+ issue.state_detail?.group ?? "backlog",
+ "14",
+ "14",
+ issue.state_detail?.color
+ )}
+ {issue.state_detail?.name ?? "Triage"}
+
+
+
+
+ {getPriorityIcon(
+ issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
+ "text-sm"
+ )}
+
+
+
+
+
+ {renderShortNumericDateFormat(issue.created_at ?? "")}
+
+
+ {issue.issue_inbox[0].snoozed_till && (
+
+
+
+ Snoozed till {renderShortNumericDateFormat(issue.issue_inbox[0].snoozed_till)}
+
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx
new file mode 100644
index 000000000..1bf330e5e
--- /dev/null
+++ b/apps/app/components/inbox/inbox-main-content.tsx
@@ -0,0 +1,259 @@
+import { useCallback, useEffect } from "react";
+
+import { useRouter } from "next/router";
+
+import useSWR, { mutate } from "swr";
+
+// react hook form
+import { useForm } from "react-hook-form";
+// contexts
+import { useProjectMyMembership } from "contexts/project-member.context";
+// services
+import inboxServices from "services/inbox.service";
+// hooks
+import useInboxView from "hooks/use-inbox-view";
+import useUserAuth from "hooks/use-user-auth";
+// components
+import {
+ AddComment,
+ IssueActivitySection,
+ IssueDescriptionForm,
+ IssueDetailsSidebar,
+} from "components/issues";
+// ui
+import { Loader } from "components/ui";
+// icons
+import {
+ ArrowTopRightOnSquareIcon,
+ CheckCircleIcon,
+ ClockIcon,
+ DocumentDuplicateIcon,
+ ExclamationTriangleIcon,
+ XCircleIcon,
+} from "@heroicons/react/24/outline";
+// helpers
+import { renderShortNumericDateFormat } from "helpers/date-time.helper";
+// types
+import type { IInboxIssue, IIssue } from "types";
+// fetch-keys
+import { INBOX_ISSUES, INBOX_ISSUE_DETAILS } from "constants/fetch-keys";
+
+const defaultValues = {
+ name: "",
+ description: "",
+ description_html: "",
+ estimate_point: null,
+ assignees_list: [],
+ priority: "low",
+ target_date: new Date().toString(),
+ labels_list: [],
+};
+
+export const InboxMainContent: React.FC = () => {
+ const router = useRouter();
+ const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
+
+ const { user } = useUserAuth();
+ const { memberRole } = useProjectMyMembership();
+ const { params } = useInboxView();
+
+ const { reset, control, watch } = useForm({
+ defaultValues,
+ });
+
+ const { data: issueDetails, mutate: mutateIssueDetails } = useSWR(
+ workspaceSlug && projectId && inboxId && inboxIssueId
+ ? INBOX_ISSUE_DETAILS(inboxId.toString(), inboxIssueId.toString())
+ : null,
+ workspaceSlug && projectId && inboxId && inboxIssueId
+ ? () =>
+ inboxServices.getInboxIssueById(
+ workspaceSlug.toString(),
+ projectId.toString(),
+ inboxId.toString(),
+ inboxIssueId.toString()
+ )
+ : null
+ );
+
+ useEffect(() => {
+ if (!issueDetails || !inboxIssueId) return;
+
+ reset({
+ ...issueDetails,
+ assignees_list:
+ issueDetails.assignees_list ?? (issueDetails.assignee_details ?? []).map((user) => user.id),
+ labels_list: issueDetails.labels_list ?? issueDetails.labels,
+ });
+ }, [issueDetails, reset, inboxIssueId]);
+
+ const submitChanges = useCallback(
+ async (formData: Partial) => {
+ if (!workspaceSlug || !projectId || !inboxIssueId || !inboxId || !issueDetails) return;
+
+ mutateIssueDetails((prevData: any) => {
+ if (!prevData) return prevData;
+
+ return {
+ ...prevData,
+ ...formData,
+ };
+ }, false);
+ mutate(
+ INBOX_ISSUES(inboxId.toString(), params),
+ (prevData) =>
+ (prevData ?? []).map((i) => {
+ if (i.bridge_id === inboxIssueId) {
+ return {
+ ...i,
+ ...formData,
+ };
+ }
+
+ return i;
+ }),
+ false
+ );
+
+ const payload = { issue: { ...formData } };
+
+ await inboxServices
+ .patchInboxIssue(
+ workspaceSlug.toString(),
+ projectId.toString(),
+ inboxId.toString(),
+ issueDetails.issue_inbox[0].id,
+ payload,
+ user
+ )
+ .then(() => {
+ mutateIssueDetails();
+ mutate(INBOX_ISSUES(inboxId.toString(), params));
+ });
+ },
+ [
+ workspaceSlug,
+ inboxIssueId,
+ projectId,
+ mutateIssueDetails,
+ inboxId,
+ user,
+ issueDetails,
+ params,
+ ]
+ );
+
+ const issueStatus = issueDetails?.issue_inbox[0].status;
+
+ return (
+ <>
+ {issueDetails ? (
+
+
+
+ {issueStatus === -2 ? (
+ <>
+
+
This issue is still pending.
+ >
+ ) : issueStatus === -1 ? (
+ <>
+
+
This issue has been declined.
+ >
+ ) : issueStatus === 0 ? (
+ <>
+
+
+ This issue has been snoozed till{" "}
+ {renderShortNumericDateFormat(issueDetails.issue_inbox[0].snoozed_till ?? "")}.
+
+ >
+ ) : issueStatus === 1 ? (
+ <>
+
+
This issue has been accepted.
+ >
+ ) : issueStatus === 2 ? (
+ <>
+
+
+ This issue has been marked as a duplicate of
+
+ this issue
+
+ .
+
+ >
+ ) : null}
+
+
+
+
+
+
Comments/Activity
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+
+ return null;
+};
diff --git a/apps/app/components/inbox/index.ts b/apps/app/components/inbox/index.ts
new file mode 100644
index 000000000..3f66790bc
--- /dev/null
+++ b/apps/app/components/inbox/index.ts
@@ -0,0 +1,8 @@
+export * from "./decline-issue-modal";
+export * from "./delete-issue-modal";
+export * from "./filters-dropdown";
+export * from "./inbox-action-headers";
+export * from "./inbox-issue-card";
+export * from "./inbox-main-content";
+export * from "./issues-list-sidebar";
+export * from "./select-duplicate";
diff --git a/apps/app/components/inbox/issues-list-sidebar.tsx b/apps/app/components/inbox/issues-list-sidebar.tsx
new file mode 100644
index 000000000..02181c02a
--- /dev/null
+++ b/apps/app/components/inbox/issues-list-sidebar.tsx
@@ -0,0 +1,44 @@
+import { useRouter } from "next/router";
+
+// hooks
+import useInboxView from "hooks/use-inbox-view";
+// components
+import { InboxIssueCard } from "components/inbox";
+// ui
+import { Loader } from "components/ui";
+
+export const IssuesListSidebar = () => {
+ const router = useRouter();
+ const { inboxIssueId } = router.query;
+
+ const { issues: inboxIssues } = useInboxView();
+
+ return (
+ <>
+ {inboxIssues ? (
+ inboxIssues.length > 0 ? (
+
+ {inboxIssues.map((issue) => (
+
+ ))}
+
+ ) : (
+
+ No issues found for the selected filters. Try changing the filters.
+
+ )
+ ) : (
+
+
+
+
+
+
+ )}
+ >
+ );
+};
diff --git a/apps/app/components/inbox/select-duplicate.tsx b/apps/app/components/inbox/select-duplicate.tsx
new file mode 100644
index 000000000..fdf7034df
--- /dev/null
+++ b/apps/app/components/inbox/select-duplicate.tsx
@@ -0,0 +1,193 @@
+import React, { useEffect, useState } from "react";
+
+import { useRouter } from "next/router";
+
+import useSWR from "swr";
+
+// react-hook-form
+import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form";
+// headless ui
+import { Combobox, Dialog, Transition } from "@headlessui/react";
+// hooks
+import useToast from "hooks/use-toast";
+// services
+import issuesServices from "services/issues.service";
+// ui
+import { PrimaryButton, SecondaryButton } from "components/ui";
+// icons
+import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
+import { LayerDiagonalIcon } from "components/icons";
+// fetch-keys
+import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
+
+type Props = {
+ isOpen: boolean;
+ value?: string | null;
+ onClose: () => void;
+ onSubmit: (issueId: string) => void;
+};
+
+export const SelectDuplicateInboxIssueModal: React.FC = (props) => {
+ const { isOpen, onClose, onSubmit, value } = props;
+
+ const [query, setQuery] = useState("");
+ const [selectedItem, setSelectedItem] = useState("");
+
+ const { setToastAlert } = useToast();
+
+ const router = useRouter();
+ const { workspaceSlug, projectId, issueId } = router.query;
+
+ const { data: issues } = useSWR(
+ workspaceSlug && projectId
+ ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
+ : null,
+ workspaceSlug && projectId
+ ? () =>
+ issuesServices
+ .getIssues(workspaceSlug as string, projectId as string)
+ .then((res) => res.filter((issue) => issue.id !== issueId))
+ : null
+ );
+
+ useEffect(() => {
+ if (!value) {
+ setSelectedItem("");
+ return;
+ } else setSelectedItem(value);
+ }, [value]);
+
+ const handleClose = () => {
+ onClose();
+ };
+
+ const handleSubmit = () => {
+ if (!selectedItem || selectedItem.length === 0)
+ return setToastAlert({
+ title: "Error",
+ type: "error",
+ });
+ onSubmit(selectedItem);
+ handleClose();
+ };
+
+ const filteredIssues =
+ (query === "" ? issues : issues?.filter((issue) => issue.name.includes(query))) ?? [];
+
+ return (
+ setQuery("")} appear>
+
+
+
+
+
+
+
+
+
+
+ {
+ setSelectedItem(value);
+ }}
+ >
+
+
+ setQuery(e.target.value)}
+ />
+
+
+
+ {filteredIssues.length > 0 ? (
+
+ {query === "" && (
+
+ Select issue
+
+ )}
+
+
+ ) : (
+
+
+
+ No issues found. Create a new issue with{" "}
+ C .
+
+
+ )}
+
+
+
+ {filteredIssues.length > 0 && (
+
+
Cancel
+
Mark as original
+
+ )}
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/app/components/issues/activity.tsx b/apps/app/components/issues/activity.tsx
index 3b9ecc853..8d708b856 100644
--- a/apps/app/components/issues/activity.tsx
+++ b/apps/app/components/issues/activity.tsx
@@ -4,6 +4,14 @@ import { useRouter } from "next/router";
import useSWR from "swr";
+// services
+import issuesService from "services/issues.service";
+// hooks
+import useEstimateOption from "hooks/use-estimate-option";
+// components
+import { CommentCard } from "components/issues/comment";
+// ui
+import { Loader } from "components/ui";
// icons
import {
CalendarDaysIcon,
@@ -17,20 +25,13 @@ import {
UserIcon,
} from "@heroicons/react/24/outline";
import { BlockedIcon, BlockerIcon, CyclesIcon, TagIcon, UserGroupIcon } from "components/icons";
-// services
-import issuesService from "services/issues.service";
-// components
-import { CommentCard } from "components/issues/comment";
-// ui
-import { Loader } from "components/ui";
-
// helpers
import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { ICurrentUserResponse, IIssueComment, IIssueLabels } from "types";
+// fetch-keys
import { PROJECT_ISSUES_ACTIVITY, PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
-import useEstimateOption from "hooks/use-estimate-option";
const activityDetails: {
[key: string]: {
@@ -60,7 +61,7 @@ const activityDetails: {
},
estimate_point: {
message: "set the estimate point to",
- icon: ,
+ icon: ,
},
labels: {
icon: ,
@@ -99,25 +100,26 @@ const activityDetails: {
},
estimate: {
message: "updated the estimate",
- icon: ,
+ icon: ,
},
link: {
message: "updated the link",
- icon: ,
+ icon: ,
},
attachment: {
message: "updated the attachment",
- icon: ,
+ icon: ,
},
};
type Props = {
+ issueId: string;
user: ICurrentUserResponse | undefined;
};
-export const IssueActivitySection: React.FC = ({ user }) => {
+export const IssueActivitySection: React.FC = ({ issueId, user }) => {
const router = useRouter();
- const { workspaceSlug, projectId, issueId } = router.query;
+ const { workspaceSlug, projectId } = router.query;
const { isEstimateActive, estimatePoints } = useEstimateOption();
diff --git a/apps/app/components/issues/comment/add-comment.tsx b/apps/app/components/issues/comment/add-comment.tsx
index 7a5e62636..1f4873372 100644
--- a/apps/app/components/issues/comment/add-comment.tsx
+++ b/apps/app/components/issues/comment/add-comment.tsx
@@ -41,10 +41,11 @@ const defaultValues: Partial = {
};
type Props = {
+ issueId: string;
user: ICurrentUserResponse | undefined;
};
-export const AddComment: React.FC = ({ user }) => {
+export const AddComment: React.FC = ({ issueId, user }) => {
const {
handleSubmit,
control,
@@ -56,7 +57,7 @@ export const AddComment: React.FC = ({ user }) => {
const editorRef = React.useRef(null);
const router = useRouter();
- const { workspaceSlug, projectId, issueId } = router.query;
+ const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
diff --git a/apps/app/components/issues/delete-issue-modal.tsx b/apps/app/components/issues/delete-issue-modal.tsx
index 525858b0c..d0ef4b6e9 100644
--- a/apps/app/components/issues/delete-issue-modal.tsx
+++ b/apps/app/components/issues/delete-issue-modal.tsx
@@ -17,7 +17,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ui
import { SecondaryButton, DangerButton } from "components/ui";
// types
-import type { ICurrentUserResponse, IIssue } from "types";
+import type { IIssue, ICurrentUserResponse } from "types";
// fetch-keys
import {
CYCLE_ISSUES_WITH_PARAMS,
diff --git a/apps/app/components/issues/description-form.tsx b/apps/app/components/issues/description-form.tsx
index 2a61e85a6..492110a8c 100644
--- a/apps/app/components/issues/description-form.tsx
+++ b/apps/app/components/issues/description-form.tsx
@@ -4,8 +4,6 @@ import dynamic from "next/dynamic";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
-// contexts
-import { useProjectMyMembership } from "contexts/project-member.context";
// hooks
import useReloadConfirmations from "hooks/use-reload-confirmation";
// components
@@ -28,16 +26,23 @@ export interface IssueDescriptionFormValues {
}
export interface IssueDetailsProps {
- issue: IIssue;
+ issue: {
+ name: string;
+ description: string;
+ description_html: string;
+ };
handleFormSubmit: (value: IssueDescriptionFormValues) => Promise;
+ isAllowed: boolean;
}
-export const IssueDescriptionForm: FC = ({ issue, handleFormSubmit }) => {
+export const IssueDescriptionForm: FC = ({
+ issue,
+ handleFormSubmit,
+ isAllowed,
+}) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [characterLimit, setCharacterLimit] = useState(false);
- const { memberRole } = useProjectMyMembership();
-
const { setShowAlert } = useReloadConfirmations();
const {
@@ -78,8 +83,6 @@ export const IssueDescriptionForm: FC = ({ issue, handleFormS
});
}, [issue, reset]);
- const isNotAllowed = memberRole.isGuest || memberRole.isViewer;
-
return (
@@ -106,6 +109,7 @@ export const IssueDescriptionForm: FC
= ({ issue, handleFormS
overflow-hidden rounded border-none bg-transparent
px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-theme"
role="textbox"
+ disabled={!isAllowed}
/>
{characterLimit && (
@@ -156,7 +160,7 @@ export const IssueDescriptionForm: FC
= ({ issue, handleFormS
});
}}
placeholder="Description"
- editable={!isNotAllowed}
+ editable={isAllowed}
/>
);
}}
diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx
index 894830bd7..e59e9c2cb 100644
--- a/apps/app/components/issues/form.tsx
+++ b/apps/app/components/issues/form.tsx
@@ -92,6 +92,19 @@ export interface IssueFormProps {
handleClose: () => void;
status: boolean;
user: ICurrentUserResponse | undefined;
+ fieldsToShow: (
+ | "project"
+ | "name"
+ | "description"
+ | "state"
+ | "priority"
+ | "assignee"
+ | "label"
+ | "dueDate"
+ | "estimate"
+ | "parent"
+ | "all"
+ )[];
}
export const IssueForm: FC = ({
@@ -105,6 +118,7 @@ export const IssueForm: FC = ({
handleClose,
status,
user,
+ fieldsToShow,
}) => {
// states
const [mostSimilarIssue, setMostSimilarIssue] = useState();
@@ -252,243 +266,271 @@ export const IssueForm: FC = ({
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx
index e48cd1f90..2270ce7f4 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx
@@ -1,11 +1,13 @@
import { useState } from "react";
+import Link from "next/link";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import projectService from "services/project.service";
+import inboxService from "services/inbox.service";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// contexts
@@ -23,7 +25,7 @@ import { PlusIcon } from "@heroicons/react/24/outline";
// types
import type { NextPage } from "next";
// fetch-keys
-import { PROJECT_DETAILS } from "constants/fetch-keys";
+import { PROJECT_DETAILS, INBOX_LIST } from "constants/fetch-keys";
const ProjectIssues: NextPage = () => {
const [analyticsModal, setAnalyticsModal] = useState(false);
@@ -38,6 +40,13 @@ const ProjectIssues: NextPage = () => {
: null
);
+ const { data: inboxList } = useSWR(
+ workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null,
+ workspaceSlug && projectId
+ ? () => inboxService.getInboxes(workspaceSlug as string, projectId as string)
+ : null
+ );
+
return (
{
>
Analytics
+ {projectDetails && projectDetails.inbox_view && (
+
+
+
+ Inbox
+ {inboxList && inboxList?.[0]?.pending_issue_count !== 0 && (
+
+ {inboxList?.[0]?.pending_issue_count}
+
+ )}
+
+
+
+ )}
{
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx
index 6932ffbd7..e10ce5aa6 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx
@@ -18,7 +18,7 @@ import { SettingsHeader } from "components/project";
import { SecondaryButton, ToggleSwitch } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
-import { ContrastIcon, PeopleGroupIcon, ViewListIcon } from "components/icons";
+import { ContrastIcon, PeopleGroupIcon, ViewListIcon, InboxIcon } from "components/icons";
import { DocumentTextIcon } from "@heroicons/react/24/outline";
// types
import { IFavoriteProject, IProject } from "types";
@@ -55,6 +55,13 @@ const featuresList = [
icon: ,
property: "page_view",
},
+ {
+ title: "Inbox",
+ description:
+ "Inbox are enabled for all the projects in this workspace. Access it from the issues views page.",
+ icon: ,
+ property: "inbox_view",
+ },
];
const getEventType = (feature: string, toggle: boolean): MiscellaneousEventType => {
@@ -67,8 +74,10 @@ const getEventType = (feature: string, toggle: boolean): MiscellaneousEventType
return toggle ? "TOGGLE_VIEW_ON" : "TOGGLE_VIEW_OFF";
case "Pages":
return toggle ? "TOGGLE_PAGES_ON" : "TOGGLE_PAGES_OFF";
+ case "Inbox":
+ return toggle ? "TOGGLE_INBOX_ON" : "TOGGLE_INBOX_OFF";
default:
- return toggle ? "TOGGLE_PAGES_ON" : "TOGGLE_PAGES_OFF";
+ throw new Error("Invalid feature");
}
};
@@ -195,9 +204,10 @@ const FeaturesSettings: NextPage = () => {
projectIdentifier: projectDetails?.identifier,
projectName: projectDetails?.name,
},
- !projectDetails?.[feature.property as keyof IProject]
- ? getEventType(feature.title, true)
- : getEventType(feature.title, false),
+ getEventType(
+ feature.title,
+ !projectDetails?.[feature.property as keyof IProject]
+ ),
user
);
handleSubmit({
diff --git a/apps/app/public/empty-state/empty-inbox.svg b/apps/app/public/empty-state/empty-inbox.svg
new file mode 100644
index 000000000..39d7ca5e2
--- /dev/null
+++ b/apps/app/public/empty-state/empty-inbox.svg
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app/services/inbox.service.ts b/apps/app/services/inbox.service.ts
new file mode 100644
index 000000000..9ee20923c
--- /dev/null
+++ b/apps/app/services/inbox.service.ts
@@ -0,0 +1,183 @@
+import APIService from "services/api.service";
+import trackEventServices from "services/track-event.service";
+
+const { NEXT_PUBLIC_API_BASE_URL } = process.env;
+
+const trackEvent =
+ process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
+
+// types
+import type {
+ IInboxIssue,
+ IInbox,
+ TInboxStatus,
+ IInboxIssueDetail,
+ ICurrentUserResponse,
+ IInboxFilterOptions,
+ IInboxQueryParams,
+} from "types";
+
+class InboxServices extends APIService {
+ constructor() {
+ super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
+ }
+
+ async getInboxes(workspaceSlug: string, projectId: string): Promise {
+ return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async getInboxById(workspaceSlug: string, projectId: string, inboxId: string): Promise {
+ return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async patchInbox(
+ workspaceSlug: string,
+ projectId: string,
+ inboxId: string,
+ data: Partial
+ ): Promise {
+ return this.patch(
+ `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`,
+ data
+ )
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async getInboxIssues(
+ workspaceSlug: string,
+ projectId: string,
+ inboxId: string,
+ params?: IInboxQueryParams
+ ): Promise {
+ return this.get(
+ `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`,
+ { params }
+ )
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async getInboxIssueById(
+ workspaceSlug: string,
+ projectId: string,
+ inboxId: string,
+ inboxIssueId: string
+ ): Promise {
+ return this.get(
+ `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`
+ )
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async deleteInboxIssue(
+ workspaceSlug: string,
+ projectId: string,
+ inboxId: string,
+ inboxIssueId: string,
+ user: ICurrentUserResponse | undefined
+ ): Promise {
+ return this.delete(
+ `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`
+ )
+ .then((response) => {
+ if (trackEvent)
+ trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_DELETE", user);
+ return response?.data;
+ })
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async markInboxStatus(
+ workspaceSlug: string,
+ projectId: string,
+ inboxId: string,
+ inboxIssueId: string,
+ data: TInboxStatus,
+ user: ICurrentUserResponse | undefined
+ ): Promise {
+ return this.patch(
+ `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`,
+ data
+ )
+ .then((response) => {
+ const action =
+ data.status === -1
+ ? "INBOX_ISSUE_REJECTED"
+ : data.status === 0
+ ? "INBOX_ISSUE_SNOOZED"
+ : data.status === 1
+ ? "INBOX_ISSUE_ACCEPTED"
+ : "INBOX_ISSUE_DUPLICATED";
+ if (trackEvent) trackEventServices.trackInboxEvent(response?.data, action, user);
+ return response?.data;
+ })
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async patchInboxIssue(
+ workspaceSlug: string,
+ projectId: string,
+ inboxId: string,
+ inboxIssueId: string,
+ data: { issue: Partial },
+ user: ICurrentUserResponse | undefined
+ ): Promise {
+ return this.patch(
+ `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`,
+ data
+ )
+ .then((response) => {
+ if (trackEvent)
+ trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_UPDATE", user);
+ return response?.data;
+ })
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async createInboxIssue(
+ workspaceSlug: string,
+ projectId: string,
+ inboxId: string,
+ data: any,
+ user: ICurrentUserResponse | undefined
+ ): Promise {
+ return this.post(
+ `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`,
+ data
+ )
+ .then((response) => {
+ if (trackEvent)
+ trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_CREATE", user);
+ return response?.data;
+ })
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+}
+
+const inboxServices = new InboxServices();
+
+export default inboxServices;
diff --git a/apps/app/services/track-event.service.ts b/apps/app/services/track-event.service.ts
index cfd7e155c..00eb4fc97 100644
--- a/apps/app/services/track-event.service.ts
+++ b/apps/app/services/track-event.service.ts
@@ -52,17 +52,15 @@ type IssueCommentEventType =
| "ISSUE_COMMENT_UPDATE"
| "ISSUE_COMMENT_DELETE";
-export type MiscellaneousEventType =
- | "TOGGLE_CYCLE_ON"
- | "TOGGLE_CYCLE_OFF"
- | "TOGGLE_MODULE_ON"
- | "TOGGLE_MODULE_OFF"
- | "TOGGLE_VIEW_ON"
- | "TOGGLE_VIEW_OFF"
- | "TOGGLE_PAGES_ON"
- | "TOGGLE_PAGES_OFF"
- | "TOGGLE_STATE_ON"
- | "TOGGLE_STATE_OFF";
+type Toggle =
+ | "TOGGLE_CYCLE"
+ | "TOGGLE_MODULE"
+ | "TOGGLE_VIEW"
+ | "TOGGLE_PAGES"
+ | "TOGGLE_STATE"
+ | "TOGGLE_INBOX";
+
+export type MiscellaneousEventType = `${Toggle}_ON` | `${Toggle}_OFF`;
type IntegrationEventType = "ADD_WORKSPACE_INTEGRATION" | "REMOVE_WORKSPACE_INTEGRATION";
@@ -80,6 +78,18 @@ type GptEventType = "ASK_GPT" | "USE_GPT_RESPONSE_IN_ISSUE" | "USE_GPT_RESPONSE_
type IssueEstimateEventType = "ESTIMATE_CREATE" | "ESTIMATE_UPDATE" | "ESTIMATE_DELETE";
+type InboxEventType =
+ | "INBOX_CREATE"
+ | "INBOX_UPDATE"
+ | "INBOX_DELETE"
+ | "INBOX_ISSUE_CREATE"
+ | "INBOX_ISSUE_UPDATE"
+ | "INBOX_ISSUE_DELETE"
+ | "INBOX_ISSUE_DUPLICATED"
+ | "INBOX_ISSUE_ACCEPTED"
+ | "INBOX_ISSUE_SNOOZED"
+ | "INBOX_ISSUE_REJECTED";
+
type ImporterEventType =
| "GITHUB_IMPORTER_CREATE"
| "GITHUB_IMPORTER_DELETE"
@@ -740,6 +750,38 @@ class TrackEventServices extends APIService {
},
});
}
+
+ // TODO: add types to the data
+ async trackInboxEvent(
+ data: any,
+ eventName: InboxEventType,
+ user: ICurrentUserResponse | undefined
+ ): Promise {
+ let payload: any;
+ if (eventName !== "INBOX_DELETE")
+ payload = {
+ issue: data?.issue?.id,
+ inbox: data?.id,
+ workspaceId: data?.issue?.workspace_detail?.id,
+ workspaceName: data?.issue?.workspace_detail?.name,
+ workspaceSlug: data?.issue?.workspace_detail?.slug,
+ projectId: data?.issue?.project_detail?.id,
+ projectName: data?.issue?.project_detail?.name,
+ };
+ else payload = data;
+
+ return this.request({
+ url: "/api/track-event",
+ method: "POST",
+ data: {
+ eventName,
+ extra: {
+ ...payload,
+ },
+ user: user,
+ },
+ });
+ }
}
const trackEventServices = new TrackEventServices();
diff --git a/apps/app/types/inbox.d.ts b/apps/app/types/inbox.d.ts
new file mode 100644
index 000000000..5c922e059
--- /dev/null
+++ b/apps/app/types/inbox.d.ts
@@ -0,0 +1,85 @@
+import { IIssue, IIssueFilterOptions, IIssueLabels } from "./issues";
+import type { IProjectLite } from "./projects";
+import { IState } from "./state";
+import { IUserLite } from "./users";
+
+export interface IInboxIssue extends Partial {
+ bridge_id: string;
+ issue_inbox: {
+ duplicate_to: string | null;
+ snoozed_till: Date | null;
+ source: string;
+ status: -2 | -1 | 0 | 1 | 2;
+ }[];
+}
+
+export interface IInboxIssueDetail extends IIssue {
+ id: string;
+ project_detail: IProjectLite;
+ created_at: string;
+ updated_at: string;
+ issue_inbox: {
+ duplicate_to: string | null;
+ id: string;
+ snoozed_till: Date | null;
+ source: string;
+ status: -2 | -1 | 0 | 1 | 2;
+ }[];
+ created_by: string;
+ updated_by: string;
+ project: string;
+ workspace: string;
+}
+export interface IInbox {
+ id: string;
+ project_detail: IProjectLite;
+ pending_issue_count: number;
+ created_at: Date;
+ updated_at: Date;
+ name: string;
+ description: string;
+ is_default: boolean;
+ created_by: string;
+ updated_by: string;
+ project: string;
+ view_props: { filters: IInboxFilterOptions };
+ workspace: string;
+}
+
+interface StatePending {
+ readonly status: -2;
+}
+interface StatusReject {
+ status: -1;
+}
+
+interface StatusSnoozed {
+ status: 0;
+ snoozed_till: Date;
+}
+
+interface StatusAccepted {
+ status: 1;
+}
+
+interface StatusDuplicate {
+ status: 2;
+ duplicate_to: string;
+}
+
+export type TInboxStatus =
+ | StatusReject
+ | StatusSnoozed
+ | StatusAccepted
+ | StatusDuplicate
+ | StatePending;
+
+export interface IInboxFilterOptions {
+ priority: string[] | null;
+ inbox_status: number[] | null;
+}
+
+export interface IInboxQueryParams {
+ priority: string | null;
+ inbox_status: string | null;
+}
diff --git a/apps/app/types/index.d.ts b/apps/app/types/index.d.ts
index d00dd6d49..a8dcce3bc 100644
--- a/apps/app/types/index.d.ts
+++ b/apps/app/types/index.d.ts
@@ -12,6 +12,7 @@ export * from "./pages";
export * from "./ai";
export * from "./estimate";
export * from "./importer";
+export * from "./inbox";
export * from "./analytics";
export * from "./calendar";
diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts
index e06d81169..0ac0207be 100644
--- a/apps/app/types/issues.d.ts
+++ b/apps/app/types/issues.d.ts
@@ -79,7 +79,7 @@ export interface IIssue {
blocks_list: string[];
bridge_id?: string | null;
completed_at: Date;
- created_at: Date;
+ created_at: string;
created_by: string;
cycle: string | null;
cycle_id: string | null;
@@ -100,7 +100,9 @@ export interface IIssue {
url: string;
}[];
issue_module: IIssueModule | null;
+ labels: string[];
label_details: any[];
+ labels_list: string[];
links_list: IIssueLink[];
link_count: number;
module: string | null;
@@ -119,12 +121,10 @@ export interface IIssue {
state_detail: IState;
sub_issues_count: number;
target_date: string | null;
- updated_at: Date;
+ updated_at: string;
updated_by: string;
workspace: string;
workspace_detail: IWorkspaceLite;
- labels: any[];
- labels_list: string[];
}
export interface ISubIssuesState {
diff --git a/apps/app/types/projects.d.ts b/apps/app/types/projects.d.ts
index 60195f850..51cc4ba9a 100644
--- a/apps/app/types/projects.d.ts
+++ b/apps/app/types/projects.d.ts
@@ -13,6 +13,10 @@ export interface IProject {
created_by: string;
cover_image: string | null;
cycle_view: boolean;
+ issue_views_view: boolean;
+ module_view: boolean;
+ page_view: boolean;
+ inbox_view: boolean;
default_assignee: IUser | string | null;
description: string;
emoji: string | null;
From 6b1d20449b538cc0bf3ea01844f2d182d1933927 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 18:57:49 +0530
Subject: [PATCH 05/59] chore: add labels data in cycles (#1223)
* dev: add labels data for all cycles
* dev: add assignees and labels percentage
* dev: initial peice on cycle burn down chart
* dev: cycles burn down chat
---
apiserver/plane/api/serializers/cycle.py | 25 +++-
apiserver/plane/api/views/cycle.py | 177 +++++++++++++++++++++--
apiserver/plane/utils/analytics_plot.py | 46 ++++++
3 files changed, 237 insertions(+), 11 deletions(-)
diff --git a/apiserver/plane/api/serializers/cycle.py b/apiserver/plane/api/serializers/cycle.py
index 5c1c68fb8..760f42dcc 100644
--- a/apiserver/plane/api/serializers/cycle.py
+++ b/apiserver/plane/api/serializers/cycle.py
@@ -1,3 +1,6 @@
+# Django imports
+from django.db.models.functions import TruncDate
+
# Third party imports
from rest_framework import serializers
@@ -20,13 +23,13 @@ class CycleSerializer(BaseSerializer):
unstarted_issues = serializers.IntegerField(read_only=True)
backlog_issues = serializers.IntegerField(read_only=True)
assignees = serializers.SerializerMethodField(read_only=True)
+ labels = serializers.SerializerMethodField(read_only=True)
total_estimates = serializers.IntegerField(read_only=True)
completed_estimates = serializers.IntegerField(read_only=True)
started_estimates = serializers.IntegerField(read_only=True)
-
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
-
+
def get_assignees(self, obj):
members = [
{
@@ -44,6 +47,24 @@ class CycleSerializer(BaseSerializer):
unique_list = [dict(item) for item in unique_objects]
return unique_list
+
+ def get_labels(self, obj):
+ labels = [
+ {
+ "name": label.name,
+ "color": label.color,
+ "id": label.id,
+ }
+ for issue_cycle in obj.issue_cycle.all()
+ for label in issue_cycle.issue.labels.all()
+ ]
+ # Use a set comprehension to return only the unique objects
+ unique_objects = {frozenset(item.items()) for item in labels}
+
+ # Convert the set back to a list of dictionaries
+ unique_list = [dict(item) for item in unique_objects]
+
+ return unique_list
class Meta:
model = Cycle
diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py
index 86c1002d1..f959f62b5 100644
--- a/apiserver/plane/api/views/cycle.py
+++ b/apiserver/plane/api/views/cycle.py
@@ -1,5 +1,6 @@
# Python imports
import json
+from datetime import datetime, timedelta
# Django imports
from django.db import IntegrityError
@@ -14,6 +15,7 @@ from django.db.models import (
Prefetch,
Sum,
)
+from django.db.models.functions import TruncDate
from django.core import serializers
from django.utils import timezone
from django.utils.decorators import method_decorator
@@ -41,10 +43,12 @@ from plane.db.models import (
CycleFavorite,
IssueLink,
IssueAttachment,
+ Label,
)
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.utils.analytics_plot import burndown_plot
class CycleViewSet(BaseViewSet):
@@ -148,12 +152,18 @@ class CycleViewSet(BaseViewSet):
queryset=User.objects.only("avatar", "first_name", "id").distinct(),
)
)
+ .prefetch_related(
+ Prefetch(
+ "issue_cycle__issue__labels",
+ queryset=Label.objects.only("name", "color", "id").distinct(),
+ )
+ )
.order_by("-is_favorite", "name")
.distinct()
)
def list(self, request, slug, project_id):
- try:
+ # try:
queryset = self.get_queryset()
cycle_view = request.GET.get("cycle_view", False)
if not cycle_view:
@@ -167,15 +177,83 @@ class CycleViewSet(BaseViewSet):
return Response(
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
-
+
# Current Cycle
if cycle_view == "current":
queryset = queryset.filter(
start_date__lte=timezone.now(),
end_date__gte=timezone.now(),
)
+
+ data = CycleSerializer(queryset, many=True).data
+
+ if len(data):
+ assignee_distribution = (
+ Issue.objects.filter(
+ issue_cycle__cycle_id=data[0]["id"],
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ .annotate(first_name=F("assignees__first_name"))
+ .annotate(last_name=F("assignees__last_name"))
+ .annotate(assignee_id=F("assignees__id"))
+ .values("first_name", "last_name", "assignee_id")
+ .annotate(total_issues=Count("assignee_id"))
+ .annotate(
+ completed_issues=Count(
+ "assignee_id",
+ filter=Q(completed_at__isnull=False),
+ )
+ )
+ .annotate(
+ pending_issues=Count(
+ "assignee_id",
+ filter=Q(completed_at__isnull=True),
+ )
+ )
+ .order_by("first_name", "last_name")
+ )
+
+ label_distribution = (
+ Issue.objects.filter(
+ issue_cycle__cycle_id=data[0]["id"],
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ .annotate(label_name=F("labels__name"))
+ .annotate(color=F("labels__color"))
+ .annotate(label_id=F("labels__id"))
+ .values("label_name", "color", "label_id")
+ .annotate(total_issues=Count("label_id"))
+ .annotate(
+ completed_issues=Count(
+ "label_id",
+ filter=Q(completed_at__isnull=False),
+ )
+ )
+ .annotate(
+ pending_issues=Count(
+ "label_id",
+ filter=Q(completed_at__isnull=True),
+ )
+ )
+ .order_by("label_name")
+ )
+ data[0]["distribution"] = {
+ "assignees": assignee_distribution,
+ "labels": label_distribution,
+ "completion_chart": {},
+ }
+ if data[0]["start_date"] and data[0]["end_date"]:
+ data[0]["distribution"]["completion_chart"] = burndown_plot(
+ queryset=queryset.first(),
+ slug=slug,
+ project_id=project_id,
+ cycle_id=data[0]["id"],
+ )
+
return Response(
- CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
+ data, status=status.HTTP_200_OK
)
# Upcoming Cycles
@@ -198,6 +276,7 @@ class CycleViewSet(BaseViewSet):
end_date=None,
start_date=None,
)
+
return Response(
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
@@ -214,12 +293,12 @@ class CycleViewSet(BaseViewSet):
return Response(
{"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST
)
- except Exception as e:
- capture_exception(e)
- return Response(
- {"error": "Something went wrong please try again later"},
- 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,
+ # )
def create(self, request, slug, project_id):
try:
@@ -282,6 +361,86 @@ class CycleViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
+ def retrieve(self, request, slug, project_id, pk):
+ try:
+ queryset = self.get_queryset().get(pk=pk)
+
+ assignee_distribution = (
+ Issue.objects.filter(
+ issue_cycle__cycle_id=pk,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ .annotate(first_name=F("assignees__first_name"))
+ .annotate(last_name=F("assignees__last_name"))
+ .annotate(assignee_id=F("assignees__id"))
+ .values("first_name", "last_name", "assignee_id")
+ .annotate(total_issues=Count("assignee_id"))
+ .annotate(
+ completed_issues=Count(
+ "assignee_id",
+ filter=Q(completed_at__isnull=False),
+ )
+ )
+ .annotate(
+ pending_issues=Count(
+ "assignee_id",
+ filter=Q(completed_at__isnull=True),
+ )
+ )
+ .order_by("first_name", "last_name")
+ )
+
+ label_distribution = (
+ Issue.objects.filter(
+ issue_cycle__cycle_id=pk,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ .annotate(label_name=F("labels__name"))
+ .annotate(color=F("labels__color"))
+ .annotate(label_id=F("labels__id"))
+ .values("label_name", "color", "label_id")
+ .annotate(total_issues=Count("label_id"))
+ .annotate(
+ completed_issues=Count(
+ "label_id",
+ filter=Q(completed_at__isnull=False),
+ )
+ )
+ .annotate(
+ pending_issues=Count(
+ "label_id",
+ filter=Q(completed_at__isnull=True),
+ )
+ )
+ .order_by("label_name")
+ )
+
+ data = CycleSerializer(queryset).data
+ data["distribution"] = {
+ "assignees": assignee_distribution,
+ "labels": label_distribution,
+ "completion_chart": {},
+ }
+
+ if queryset.start_date and queryset.end_date:
+ data["distribution"]["completion_chart"] = burndown_plot(
+ queryset=queryset, slug=slug, project_id=project_id, cycle_id=pk
+ )
+
+ return Response(
+ data,
+ status=status.HTTP_200_OK,
+ )
+
+ except Exception as e:
+ capture_exception(e)
+ return Response(
+ {"error": "Something went wrong please try again later"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
class CycleIssueViewSet(BaseViewSet):
serializer_class = CycleIssueSerializer
diff --git a/apiserver/plane/utils/analytics_plot.py b/apiserver/plane/utils/analytics_plot.py
index 161f6497e..045e2bf26 100644
--- a/apiserver/plane/utils/analytics_plot.py
+++ b/apiserver/plane/utils/analytics_plot.py
@@ -1,11 +1,16 @@
# Python imports
from itertools import groupby
+from datetime import timedelta
# Django import
from django.db import models
+from django.db.models.functions import TruncDate
from django.db.models import Count, F, Sum, Value, Case, When, CharField
from django.db.models.functions import Coalesce, ExtractMonth, ExtractYear, Concat
+# Module imports
+from plane.db.models import Issue
+
def build_graph_plot(queryset, x_axis, y_axis, segment=None):
@@ -74,3 +79,44 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
else:
sorted_data = dict(sorted(grouped_data.items(), key=lambda x: (x[0] == "None", x[0])))
return sorted_data
+
+
+def burndown_plot(queryset, slug, project_id, cycle_id):
+ # Get all dates between the two dates
+ date_range = [
+ queryset.start_date + timedelta(days=x)
+ for x in range((queryset.end_date - queryset.start_date).days + 1)
+ ]
+
+ chart_data = {str(date): 0 for date in date_range}
+
+ # Total Issues in Cycle
+ total_issues = queryset.total_issues
+
+ completed_issues_distribution = (
+ Issue.objects.filter(
+ workspace__slug=slug,
+ project_id=project_id,
+ issue_cycle__cycle_id=cycle_id,
+ )
+ .annotate(date=TruncDate("completed_at"))
+ .values("date")
+ .annotate(total_completed=Count("id"))
+ .values("date", "total_completed")
+ .order_by("date")
+ )
+
+ for date in date_range:
+ cumulative_pending_issues = total_issues
+ total_completed = 0
+ total_completed = sum(
+ [
+ item["total_completed"]
+ for item in completed_issues_distribution
+ if item["date"] is not None and item["date"] <= date
+ ]
+ )
+ cumulative_pending_issues -= total_completed
+ chart_data[str(date)] = cumulative_pending_issues
+
+ return chart_data
\ No newline at end of file
From 885f5deebebbf6cd0f2352ed58d03b3cfac594cd Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 18:58:08 +0530
Subject: [PATCH 06/59] chore: update docker setup to mount env file (#1270)
---
.env.example | 6 ++++++
docker-compose-hub.yml | 27 ++++++++++++++++++++++-----
docker-compose.yml | 22 +++++++++++++++++++++-
3 files changed, 49 insertions(+), 6 deletions(-)
diff --git a/.env.example b/.env.example
index 578aa0fa4..f8c15a1cb 100644
--- a/.env.example
+++ b/.env.example
@@ -27,6 +27,12 @@ PGUSER="plane"
PGPASSWORD="plane"
PGHOST="plane-db"
PGDATABASE="plane"
+DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
+
+# Redis Settings
+REDIS_HOST="plane-redis"
+REDIS_PORT="6379"
+REDIS_URL="redis://${REDIS_HOST}:6379/"
# Email Settings
EMAIL_HOST=""
diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml
index f259f6391..bc476a6b1 100644
--- a/docker-compose-hub.yml
+++ b/docker-compose-hub.yml
@@ -6,6 +6,8 @@ services:
image: makeplane/plane-frontend:latest
restart: always
command: /usr/local/bin/start.sh
+ env_file:
+ - .env
environment:
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
NEXT_PUBLIC_GOOGLE_CLIENTID: 0
@@ -14,12 +16,17 @@ services:
NEXT_PUBLIC_SENTRY_DSN: 0
NEXT_PUBLIC_ENABLE_OAUTH: 0
NEXT_PUBLIC_ENABLE_SENTRY: 0
+ depends_on:
+ - plane-api
+ - plane-worker
plane-api:
container_name: planebackend
image: makeplane/plane-backend:latest
restart: always
command: ./bin/takeoff
+ env_file:
+ - .env
environment:
DJANGO_SETTINGS_MODULE: plane.settings.production
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
@@ -54,6 +61,8 @@ services:
image: makeplane/plane-worker:latest
restart: always
command: ./bin/worker
+ env_file:
+ - .env
environment:
DJANGO_SETTINGS_MODULE: plane.settings.production
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
@@ -89,6 +98,8 @@ services:
image: postgres:15.2-alpine
restart: always
command: postgres -c 'max_connections=1000'
+ env_file:
+ - .env
environment:
POSTGRES_USER: ${PGUSER}
POSTGRES_DB: ${PGDATABASE}
@@ -108,15 +119,15 @@ services:
image: minio/minio
volumes:
- uploads:/export
+ command: server /export --console-address ":9090"
+ env_file:
+ - .env
environment:
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
- command: server /export --console-address ":9090"
createbuckets:
image: minio/mc
- depends_on:
- - plane-minio
entrypoint: >
/bin/sh -c "
/usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY;
@@ -124,6 +135,10 @@ services:
/usr/bin/mc anonymous set download plane-minio/\$AWS_S3_BUCKET_NAME;
exit 0;
"
+ env_file:
+ - .env
+ depends_on:
+ - plane-minio
# Comment this if you already have a reverse proxy running
plane-proxy:
@@ -131,12 +146,14 @@ services:
image: makeplane/plane-proxy:latest
ports:
- ${NGINX_PORT}:80
+ env_file:
+ - .env
environment:
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
depends_on:
- - plane-web
- - plane-api
+ - plane-web
+ - plane-api
volumes:
diff --git a/docker-compose.yml b/docker-compose.yml
index da275a1fb..df8c239e2 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,6 +10,8 @@ services:
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
restart: always
command: [ "/usr/local/bin/start.sh" ]
+ env_file:
+ - .env
environment:
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
NEXT_PUBLIC_GOOGLE_CLIENTID: "0"
@@ -20,6 +22,10 @@ services:
NEXT_PUBLIC_ENABLE_SENTRY: "0"
NEXT_PUBLIC_ENABLE_SESSION_RECORDER: "0"
NEXT_PUBLIC_TRACK_EVENTS: "0"
+ depends_on:
+ - plane-api
+ - plane-worker
+
plane-api:
container_name: planebackend
@@ -28,6 +34,8 @@ services:
dockerfile: Dockerfile.api
restart: always
command: ./bin/takeoff
+ env_file:
+ - .env
environment:
DJANGO_SETTINGS_MODULE: plane.settings.production
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
@@ -67,6 +75,8 @@ services:
dockerfile: Dockerfile.api
restart: always
command: ./bin/worker
+ env_file:
+ - .env
environment:
DJANGO_SETTINGS_MODULE: plane.settings.production
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
@@ -107,6 +117,8 @@ services:
command: postgres -c 'max_connections=1000'
volumes:
- pgdata:/var/lib/postgresql/data
+ env_file:
+ - .env
environment:
POSTGRES_USER: ${PGUSER}
POSTGRES_DB: ${PGDATABASE}
@@ -127,6 +139,8 @@ services:
command: server /export --console-address ":9090"
volumes:
- uploads:/export
+ env_file:
+ - .env
environment:
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
@@ -134,7 +148,11 @@ services:
createbuckets:
image: minio/mc
entrypoint: >
- /bin/sh -c " /usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY; /usr/bin/mc mb plane-minio/\$AWS_S3_BUCKET_NAME; /usr/bin/mc anonymous set download plane-minio/\$AWS_S3_BUCKET_NAME; exit 0; "
+ /bin/sh -c " /usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY;
+ /usr/bin/mc mb plane-minio/\$AWS_S3_BUCKET_NAME;
+ /usr/bin/mc anonymous set download plane-minio/\$AWS_S3_BUCKET_NAME; exit 0; "
+ env_file:
+ - .env
depends_on:
- plane-minio
@@ -146,6 +164,8 @@ services:
restart: always
ports:
- ${NGINX_PORT}:80
+ env_file:
+ - .env
environment:
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
From 78f9028b2f1d4998a89b0f9d7ea71b6d4172eec6 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 18:58:29 +0530
Subject: [PATCH 07/59] fix: member invite (#1303)
* fix: member invite
* dev: fix integer errors
---
apiserver/plane/api/views/workspace.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py
index 9b7347b4d..6047765e6 100644
--- a/apiserver/plane/api/views/workspace.py
+++ b/apiserver/plane/api/views/workspace.py
@@ -195,6 +195,11 @@ class InviteWorkspaceEndpoint(BaseAPIView):
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
)
+ # 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)
+
workspace = Workspace.objects.get(slug=slug)
# Check if user is already a member of workspace
From bfe581d3bd8ee063c14ef664bac50a804ea71128 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 18:58:56 +0530
Subject: [PATCH 08/59] dev: workspace issue count (#1298)
---
apiserver/plane/api/serializers/workspace.py | 1 +
apiserver/plane/api/views/workspace.py | 24 ++++++++++++++++++--
2 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/apiserver/plane/api/serializers/workspace.py b/apiserver/plane/api/serializers/workspace.py
index 078a4bf08..4d83d6262 100644
--- a/apiserver/plane/api/serializers/workspace.py
+++ b/apiserver/plane/api/serializers/workspace.py
@@ -19,6 +19,7 @@ from plane.db.models import (
class WorkSpaceSerializer(BaseSerializer):
owner = UserLiteSerializer(read_only=True)
total_members = serializers.IntegerField(read_only=True)
+ total_issues = serializers.IntegerField(read_only=True)
class Meta:
model = Workspace
diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py
index 6047765e6..26c82d54c 100644
--- a/apiserver/plane/api/views/workspace.py
+++ b/apiserver/plane/api/views/workspace.py
@@ -80,9 +80,22 @@ class WorkSpaceViewSet(BaseViewSet):
lookup_field = "slug"
def get_queryset(self):
+ member_count = (
+ WorkspaceMember.objects.filter(workspace=OuterRef("id"))
+ .order_by()
+ .annotate(count=Func(F("id"), function="Count"))
+ .values("count")
+ )
+
+ issue_count = (
+ Issue.objects.filter(workspace=OuterRef("id"))
+ .order_by()
+ .annotate(count=Func(F("id"), function="Count"))
+ .values("count")
+ )
return self.filter_queryset(
super().get_queryset().select_related("owner")
- ).order_by("name")
+ ).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:
@@ -139,6 +152,13 @@ class UserWorkSpacesEndpoint(BaseAPIView):
.values("count")
)
+ issue_count = (
+ Issue.objects.filter(workspace=OuterRef("id"))
+ .order_by()
+ .annotate(count=Func(F("id"), function="Count"))
+ .values("count")
+ )
+
workspace = (
Workspace.objects.prefetch_related(
Prefetch("workspace_member", queryset=WorkspaceMember.objects.all())
@@ -147,7 +167,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
workspace_member__member=request.user,
)
.select_related("owner")
- ).annotate(total_members=member_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)
From 56a4e18a3c681901ba56da8249403c6d91e08309 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 18:59:13 +0530
Subject: [PATCH 09/59] chore: link and attachment count for cycles and modules
(#1307)
---
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 e48259370..01c2c977d 100644
--- a/apiserver/plane/api/serializers/issue.py
+++ b/apiserver/plane/api/serializers/issue.py
@@ -483,6 +483,8 @@ class IssueStateSerializer(BaseSerializer):
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True)
+ attachment_count = serializers.IntegerField(read_only=True)
+ link_count = serializers.IntegerField(read_only=True)
class Meta:
model = Issue
From 81f656216867b13d7c6b7c231e8922bc40a39b88 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Fri, 16 Jun 2023 19:00:04 +0530
Subject: [PATCH 10/59] feat: sign up page added (#1306)
---
.../account/email-password-form.tsx | 81 +++++++++--------
.../workspace/create-workspace-form.tsx | 6 +-
apps/app/pages/index.tsx | 47 ++++++----
apps/app/pages/sign-up.tsx | 88 +++++++++++++++++++
apps/app/services/api.service.ts | 1 +
apps/app/services/authentication.service.ts | 12 +++
6 files changed, 175 insertions(+), 60 deletions(-)
create mode 100644 apps/app/pages/sign-up.tsx
diff --git a/apps/app/components/account/email-password-form.tsx b/apps/app/components/account/email-password-form.tsx
index 920ec0829..8a0dc3a33 100644
--- a/apps/app/components/account/email-password-form.tsx
+++ b/apps/app/components/account/email-password-form.tsx
@@ -1,11 +1,10 @@
import React, { useState } from "react";
+import { useRouter } from "next/router";
+import Link from "next/link";
+
// react hook form
import { useForm } from "react-hook-form";
-// services
-import authenticationService from "services/authentication.service";
-// hooks
-import useToast from "hooks/use-toast";
// components
import { EmailResetPasswordForm } from "components/account";
// ui
@@ -17,15 +16,19 @@ type EmailPasswordFormValues = {
medium?: string;
};
-export const EmailPasswordForm = ({ handleSignIn }: any) => {
+type Props = {
+ onSubmit: (formData: EmailPasswordFormValues) => Promise;
+};
+
+export const EmailPasswordForm: React.FC = ({ onSubmit }) => {
const [isResettingPassword, setIsResettingPassword] = useState(false);
- const { setToastAlert } = useToast();
+ const router = useRouter();
+ const isSignUpPage = router.pathname === "/sign-up";
const {
register,
handleSubmit,
- setError,
formState: { errors, isSubmitting, isValid, isDirty },
} = useForm({
defaultValues: {
@@ -37,31 +40,6 @@ export const EmailPasswordForm = ({ handleSignIn }: any) => {
reValidateMode: "onChange",
});
- const onSubmit = (formData: EmailPasswordFormValues) => {
- authenticationService
- .emailLogin(formData)
- .then((response) => {
- if (handleSignIn) handleSignIn(response);
- })
- .catch((error) => {
- console.log(error);
- setToastAlert({
- title: "Oops!",
- type: "error",
- message: "Enter the correct email address and password to sign in",
- });
- if (!error?.response?.data) return;
- Object.keys(error.response.data).forEach((key) => {
- const err = error.response.data[key];
- console.log(err);
- setError(key as keyof EmailPasswordFormValues, {
- type: "manual",
- message: Array.isArray(err) ? err.join(", ") : err,
- });
- });
- });
- };
-
return (
<>
{isResettingPassword ? (
@@ -82,7 +60,7 @@ export const EmailPasswordForm = ({ handleSignIn }: any) => {
) || "Email ID is not valid",
}}
error={errors.email}
- placeholder="Enter your Email ID"
+ placeholder="Enter your email ID"
/>
@@ -100,13 +78,21 @@ export const EmailPasswordForm = ({ handleSignIn }: any) => {
-
setIsResettingPassword(true)}
- className="font-medium text-brand-accent hover:text-brand-accent"
- >
- Forgot your password?
-
+ {isSignUpPage ? (
+
+
+ Already have an account? Sign in.
+
+
+ ) : (
+
setIsResettingPassword(true)}
+ className="font-medium text-brand-accent hover:text-brand-accent"
+ >
+ Forgot your password?
+
+ )}
@@ -116,8 +102,21 @@ export const EmailPasswordForm = ({ handleSignIn }: any) => {
disabled={!isValid && isDirty}
loading={isSubmitting}
>
- {isSubmitting ? "Signing in..." : "Sign In"}
+ {isSignUpPage
+ ? isSubmitting
+ ? "Signing up..."
+ : "Sign Up"
+ : isSubmitting
+ ? "Signing in..."
+ : "Sign In"}
+ {!isSignUpPage && (
+
+
+ Don{"'"}t have an account? Sign up.
+
+
+ )}
)}
diff --git a/apps/app/components/workspace/create-workspace-form.tsx b/apps/app/components/workspace/create-workspace-form.tsx
index 507d88ea7..42078fe91 100644
--- a/apps/app/components/workspace/create-workspace-form.tsx
+++ b/apps/app/components/workspace/create-workspace-form.tsx
@@ -31,16 +31,16 @@ type Props = {
const restrictedUrls = [
"api",
+ "installations",
+ "404",
"create-workspace",
"error",
- "installations",
"invitations",
"magic-sign-in",
"onboarding",
"reset-password",
- "signin",
+ "sign-up",
"workspace-member-invitation",
- "404",
];
export const CreateWorkspaceForm: React.FC = ({
diff --git a/apps/app/pages/index.tsx b/apps/app/pages/index.tsx
index 96d8441d7..ab2633241 100644
--- a/apps/app/pages/index.tsx
+++ b/apps/app/pages/index.tsx
@@ -21,6 +21,12 @@ import {
import { Spinner } from "components/ui";
// icons
import Logo from "public/logo.png";
+// types
+type EmailPasswordFormValues = {
+ email: string;
+ password?: string;
+ medium?: string;
+};
const HomePage: NextPage = () => {
const { user, isLoading, mutateUser } = useUserAuth("sign-in");
@@ -66,7 +72,6 @@ const HomePage: NextPage = () => {
throw Error("Cant find credentials");
}
} catch (error: any) {
- console.log(error);
setToastAlert({
title: "Error signing in!",
type: "error",
@@ -77,19 +82,30 @@ const HomePage: NextPage = () => {
}
};
- const handleEmailPasswordSignIn = async (response: any) => {
- try {
- if (response) mutateUser();
- } catch (error: any) {
- console.log(error);
- setToastAlert({
- title: "Error signing in!",
- type: "error",
- message:
- error?.error ||
- "Something went wrong. Please try again later or contact the support team.",
- });
- }
+ const handlePasswordSignIn = async (formData: EmailPasswordFormValues) => {
+ await authenticationService
+ .emailLogin(formData)
+ .then((response) => {
+ try {
+ if (response) mutateUser();
+ } catch (error: any) {
+ console.log(error);
+ setToastAlert({
+ title: "Error signing in!",
+ type: "error",
+ message:
+ error?.error ||
+ "Something went wrong. Please try again later or contact the support team.",
+ });
+ }
+ })
+ .catch(() =>
+ setToastAlert({
+ title: "Oops!",
+ type: "error",
+ message: "Enter the correct email address and password to sign in",
+ })
+ );
};
const handleEmailCodeSignIn = async (response: any) => {
@@ -114,7 +130,6 @@ const HomePage: NextPage = () => {
- {/* Validating authentication
*/}
) : (
@@ -137,7 +152,7 @@ const HomePage: NextPage = () => {
>
) : (
-
+
)}
diff --git a/apps/app/pages/sign-up.tsx b/apps/app/pages/sign-up.tsx
new file mode 100644
index 000000000..eeffb53bd
--- /dev/null
+++ b/apps/app/pages/sign-up.tsx
@@ -0,0 +1,88 @@
+import React from "react";
+
+import Image from "next/image";
+import { useRouter } from "next/router";
+
+// services
+import authenticationService from "services/authentication.service";
+// hooks
+import useUserAuth from "hooks/use-user-auth";
+import useToast from "hooks/use-toast";
+// layouts
+import DefaultLayout from "layouts/default-layout";
+// components
+import { EmailPasswordForm } from "components/account";
+// images
+import Logo from "public/logo.png";
+// types
+import type { NextPage } from "next";
+type EmailPasswordFormValues = {
+ email: string;
+ password?: string;
+ medium?: string;
+};
+
+const SignUp: NextPage = () => {
+ const router = useRouter();
+
+ const { setToastAlert } = useToast();
+
+ const { mutateUser } = useUserAuth("sign-in");
+
+ const handleSignUp = async (formData: EmailPasswordFormValues) => {
+ const payload = {
+ email: formData.email,
+ password: formData.password ?? "",
+ };
+
+ await authenticationService
+ .emailSignUp(payload)
+ .then(async (response) => {
+ setToastAlert({
+ type: "success",
+ title: "Success!",
+ message: "Account created successfully.",
+ });
+
+ if (response) await mutateUser();
+ router.push("/");
+ })
+ .catch((err) => {
+ if (err.status === 400)
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "An user already exists with this Email ID.",
+ });
+ else
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Something went wrong. Please try again later or contact the support team.",
+ });
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+ Create a new Plane Account
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SignUp;
diff --git a/apps/app/services/api.service.ts b/apps/app/services/api.service.ts
index ef198c221..361fea03e 100644
--- a/apps/app/services/api.service.ts
+++ b/apps/app/services/api.service.ts
@@ -7,6 +7,7 @@ const nonValidatedRoutes = [
"/magic-sign-in",
"/reset-password",
"/workspace-member-invitation",
+ "/sign-up",
];
const validateRouteCheck = (route: string): boolean => {
diff --git a/apps/app/services/authentication.service.ts b/apps/app/services/authentication.service.ts
index 291f255d8..f0d19da24 100644
--- a/apps/app/services/authentication.service.ts
+++ b/apps/app/services/authentication.service.ts
@@ -20,6 +20,18 @@ class AuthService extends APIService {
});
}
+ async emailSignUp(data: { email: string; password: string }) {
+ return this.post("/api/sign-up/", data, { headers: {} })
+ .then((response) => {
+ this.setAccessToken(response?.data?.access_token);
+ this.setRefreshToken(response?.data?.refresh_token);
+ return response?.data;
+ })
+ .catch((error) => {
+ throw error?.response;
+ });
+ }
+
async socialAuth(data: any) {
return this.post("/api/social-auth/", data, { headers: {} })
.then((response) => {
From 0dfa06e55b34a409fb62edea87cf96814484203a Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Fri, 16 Jun 2023 19:06:34 +0530
Subject: [PATCH 11/59] fix: lower role user cannot invite higher role user
(#1302)
---
.../project/send-project-invitation-modal.tsx | 21 +-
.../send-workspace-invitation-modal.tsx | 26 +-
.../app/contexts/workspace-member.context.tsx | 61 ++++
.../workspace-authorization-wrapper.tsx | 86 +++---
.../projects/[projectId]/settings/members.tsx | 280 +++++++++---------
.../[workspaceSlug]/settings/members.tsx | 280 +++++++++---------
6 files changed, 416 insertions(+), 338 deletions(-)
create mode 100644 apps/app/contexts/workspace-member.context.tsx
diff --git a/apps/app/components/project/send-project-invitation-modal.tsx b/apps/app/components/project/send-project-invitation-modal.tsx
index e08b92e8c..204633a85 100644
--- a/apps/app/components/project/send-project-invitation-modal.tsx
+++ b/apps/app/components/project/send-project-invitation-modal.tsx
@@ -9,11 +9,13 @@ import { useForm, Controller } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react";
// ui
import { CustomSelect, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
-// hooks
-import useToast from "hooks/use-toast";
// services
import projectService from "services/project.service";
import workspaceService from "services/workspace.service";
+// contexts
+import { useProjectMyMembership } from "contexts/project-member.context";
+// hooks
+import useToast from "hooks/use-toast";
// types
import { ICurrentUserResponse, IProjectMemberInvitation } from "types";
// fetch-keys
@@ -46,6 +48,7 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
+ const { memberDetails } = useProjectMyMembership();
const { data: people } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null,
@@ -202,11 +205,15 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member
input
width="w-full"
>
- {Object.entries(ROLE).map(([key, label]) => (
-
- {label}
-
- ))}
+ {Object.entries(ROLE).map(([key, label]) => {
+ if (parseInt(key) > (memberDetails?.role ?? 5)) return null;
+
+ return (
+
+ {label}
+
+ );
+ })}
)}
/>
diff --git a/apps/app/components/workspace/send-workspace-invitation-modal.tsx b/apps/app/components/workspace/send-workspace-invitation-modal.tsx
index 52dc74149..56d9385b1 100644
--- a/apps/app/components/workspace/send-workspace-invitation-modal.tsx
+++ b/apps/app/components/workspace/send-workspace-invitation-modal.tsx
@@ -1,17 +1,22 @@
import React from "react";
+
import { mutate } from "swr";
+
+// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless
import { Dialog, Transition } from "@headlessui/react";
// services
import workspaceService from "services/workspace.service";
-// ui
-import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui";
+// contexts
+import { useWorkspaceMyMembership } from "contexts/workspace-member.context";
// hooks
import useToast from "hooks/use-toast";
+// ui
+import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui";
// types
import { ICurrentUserResponse, IWorkspaceMemberInvitation } from "types";
-// fetch keys
+// fetch-keys
import { WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// constants
import { ROLE } from "constants/workspace";
@@ -37,6 +42,7 @@ const SendWorkspaceInvitationModal: React.FC = ({
user,
}) => {
const { setToastAlert } = useToast();
+ const { memberDetails } = useWorkspaceMyMembership();
const {
control,
@@ -145,11 +151,15 @@ const SendWorkspaceInvitationModal: React.FC = ({
width="w-full"
input
>
- {Object.entries(ROLE).map(([key, value]) => (
-
- {value}
-
- ))}
+ {Object.entries(ROLE).map(([key, value]) => {
+ if (parseInt(key) > (memberDetails?.role ?? 5)) return null;
+
+ return (
+
+ {value}
+
+ );
+ })}
)}
/>
diff --git a/apps/app/contexts/workspace-member.context.tsx b/apps/app/contexts/workspace-member.context.tsx
new file mode 100644
index 000000000..5f34bd28b
--- /dev/null
+++ b/apps/app/contexts/workspace-member.context.tsx
@@ -0,0 +1,61 @@
+import { createContext, useContext } from "react";
+
+// next
+import { useRouter } from "next/router";
+
+import useSWR from "swr";
+// services
+import workspaceService from "services/workspace.service";
+// types
+import { IWorkspaceMember } from "types";
+// fetch-keys
+import { WORKSPACE_MEMBERS_ME } from "constants/fetch-keys";
+
+type ContextType = {
+ loading: boolean;
+ memberDetails?: IWorkspaceMember;
+ error: any;
+};
+
+export const WorkspaceMemberContext = createContext({} as ContextType);
+
+type Props = {
+ children: React.ReactNode;
+};
+
+export const WorkspaceMemberProvider: React.FC = (props) => {
+ const { children } = props;
+
+ const router = useRouter();
+ const { workspaceSlug } = router.query;
+
+ const { data: memberDetails, error } = useSWR(
+ workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug.toString()) : null,
+ workspaceSlug ? () => workspaceService.workspaceMemberMe(workspaceSlug.toString()) : null
+ );
+
+ const loading = !memberDetails && !error;
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useWorkspaceMyMembership = () => {
+ const context = useContext(WorkspaceMemberContext);
+
+ if (context === undefined)
+ throw new Error(`useWorkspaceMember must be used within a WorkspaceMemberProvider.`);
+
+ return {
+ ...context,
+ memberRole: {
+ isOwner: context.memberDetails?.role === 20,
+ isMember: context.memberDetails?.role === 15,
+ isViewer: context.memberDetails?.role === 10,
+ isGuest: context.memberDetails?.role === 5,
+ },
+ };
+};
diff --git a/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx b/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx
index e67424758..201dd5a72 100644
--- a/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx
+++ b/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx
@@ -7,6 +7,8 @@ import useSWR from "swr";
// services
import workspaceServices from "services/workspace.service";
+// contexts
+import { WorkspaceMemberProvider } from "contexts/workspace-member.context";
// layouts
import AppSidebar from "layouts/app-layout/app-sidebar";
import AppHeader from "layouts/app-layout/app-header";
@@ -78,48 +80,50 @@ export const WorkspaceAuthorizationLayout: React.FC = ({
return (
-
-
-
- {settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
-
-
-
- Go to workspace
-
-
-
- }
- type="workspace"
- />
- ) : (
-
- {!noHeader && (
-
- )}
-
-
- {children}
+
+
+
+
+ {settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
+
+
+
+ Go to workspace
+
+
+
+ }
+ type="workspace"
+ />
+ ) : (
+
+ {!noHeader && (
+
+ )}
+
-
-
- )}
-
+
+ )}
+
+
);
};
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx
index 92f490687..a0d53df35 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx
@@ -89,7 +89,17 @@ const MembersSettings: NextPage = () => {
const currentUser = projectMembers?.find((item) => item.member.id === user?.id);
return (
- <>
+
+
+
+
+ }
+ >
{
@@ -136,149 +146,137 @@ const MembersSettings: NextPage = () => {
members={members}
user={user}
/>
-
-
-
-
- }
- >
-
-
-
-
-
Members
-
setInviteModal(true)}
- >
-
- Add Member
-
-
- {!projectMembers || !projectInvitations ? (
-
-
-
-
-
-
- ) : (
-
- {members.length > 0
- ? members.map((member) => (
-
-
-
- {member.avatar && member.avatar !== "" ? (
-
- ) : member.first_name !== "" ? (
- member.first_name.charAt(0)
- ) : (
- member.email.charAt(0)
- )}
-
-
-
- {member.first_name} {member.last_name}
-
-
{member.email}
-
-
-
- {!member.member && (
-
- Pending
-
+
+
+
+
+
Members
+
setInviteModal(true)}
+ >
+
+ Add Member
+
+
+ {!projectMembers || !projectInvitations ? (
+
+
+
+
+
+
+ ) : (
+
+ {members.length > 0
+ ? members.map((member) => (
+
+
+
+ {member.avatar && member.avatar !== "" ? (
+
+ ) : member.first_name !== "" ? (
+ member.first_name.charAt(0)
+ ) : (
+ member.email.charAt(0)
)}
-
{
- if (!activeWorkspace || !projectDetails) return;
-
- mutateMembers(
- (prevData: any) =>
- prevData.map((m: any) =>
- m.id === member.id ? { ...m, role: value } : m
- ),
- false
- );
-
- projectService
- .updateProjectMember(
- activeWorkspace.slug,
- projectDetails.id,
- member.id,
- {
- role: value,
- }
- )
- .catch(() => {
- setToastAlert({
- type: "error",
- title: "Error!",
- message:
- "An error occurred while updating member role. Please try again.",
- });
- });
- }}
- position="right"
- disabled={
- member.memberId === user?.id ||
- !member.member ||
- (currentUser &&
- currentUser.role !== 20 &&
- currentUser.role < member.role)
- }
- >
- {Object.keys(ROLE).map((key) => {
- if (
- currentUser &&
- currentUser.role !== 20 &&
- currentUser.role < parseInt(key)
- )
- return null;
-
- return (
-
- <>{ROLE[parseInt(key) as keyof typeof ROLE]}>
-
- );
- })}
-
-
- {
- if (member.member) setSelectedRemoveMember(member.id);
- else setSelectedInviteRemoveMember(member.id);
- }}
- >
-
-
- Remove member
-
-
-
+
+
+
+ {member.first_name} {member.last_name}
+
+
{member.email}
- ))
- : null}
-
- )}
-
-
-
- >
+
+ {!member.member && (
+
+ Pending
+
+ )}
+
{
+ if (!activeWorkspace || !projectDetails) return;
+
+ mutateMembers(
+ (prevData: any) =>
+ prevData.map((m: any) =>
+ m.id === member.id ? { ...m, role: value } : m
+ ),
+ false
+ );
+
+ projectService
+ .updateProjectMember(
+ activeWorkspace.slug,
+ projectDetails.id,
+ member.id,
+ {
+ role: value,
+ }
+ )
+ .catch(() => {
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message:
+ "An error occurred while updating member role. Please try again.",
+ });
+ });
+ }}
+ position="right"
+ disabled={
+ member.memberId === user?.id ||
+ !member.member ||
+ (currentUser &&
+ currentUser.role !== 20 &&
+ currentUser.role < member.role)
+ }
+ >
+ {Object.keys(ROLE).map((key) => {
+ if (
+ currentUser &&
+ currentUser.role !== 20 &&
+ currentUser.role < parseInt(key)
+ )
+ return null;
+
+ return (
+
+ <>{ROLE[parseInt(key) as keyof typeof ROLE]}>
+
+ );
+ })}
+
+
+ {
+ if (member.member) setSelectedRemoveMember(member.id);
+ else setSelectedInviteRemoveMember(member.id);
+ }}
+ >
+
+
+ Remove member
+
+
+
+
+
+ ))
+ : null}
+
+ )}
+
+
+
);
};
diff --git a/apps/app/pages/[workspaceSlug]/settings/members.tsx b/apps/app/pages/[workspaceSlug]/settings/members.tsx
index e01082fd3..da8fd52cb 100644
--- a/apps/app/pages/[workspaceSlug]/settings/members.tsx
+++ b/apps/app/pages/[workspaceSlug]/settings/members.tsx
@@ -85,7 +85,17 @@ const MembersSettings: NextPage = () => {
const currentUser = workspaceMembers?.find((item) => item.member?.id === user?.id);
return (
- <>
+
+
+
+
+ }
+ >
{
@@ -137,149 +147,137 @@ const MembersSettings: NextPage = () => {
members={members}
user={user}
/>
-
-
-
-
- }
- >
-
-
-
-
-
Members
-
setInviteModal(true)}
- >
-
- Add Member
-
-
- {!workspaceMembers || !workspaceInvitations ? (
-
-
-
-
-
-
- ) : (
-
- {members.length > 0
- ? members.map((member) => (
-
-
-
- {member.avatar && member.avatar !== "" ? (
-
- ) : member.first_name !== "" ? (
- member.first_name.charAt(0)
- ) : (
- member.email.charAt(0)
- )}
-
-
-
- {member.first_name} {member.last_name}
-
-
{member.email}
-
+
+
+
+
+
Members
+
setInviteModal(true)}
+ >
+
+ Add Member
+
+
+ {!workspaceMembers || !workspaceInvitations ? (
+
+
+
+
+
+
+ ) : (
+
+ {members.length > 0
+ ? members.map((member) => (
+
+
+
+ {member.avatar && member.avatar !== "" ? (
+
+ ) : member.first_name !== "" ? (
+ member.first_name.charAt(0)
+ ) : (
+ member.email.charAt(0)
+ )}
-
- {!member?.status && (
-
- )}
- {member?.status && !member?.accountCreated && (
-
- )}
-
{
- if (!workspaceSlug) return;
-
- mutateMembers(
- (prevData) =>
- prevData?.map((m) =>
- m.id === member.id ? { ...m, role: value } : m
- ),
- false
- );
-
- workspaceService
- .updateWorkspaceMember(workspaceSlug?.toString(), member.id, {
- role: value,
- })
- .catch(() => {
- setToastAlert({
- type: "error",
- title: "Error!",
- message:
- "An error occurred while updating member role. Please try again.",
- });
- });
- }}
- position="right"
- disabled={
- member.memberId === currentUser?.member.id ||
- !member.status ||
- (currentUser &&
- currentUser.role !== 20 &&
- currentUser.role < member.role)
- }
- >
- {Object.keys(ROLE).map((key) => {
- if (
- currentUser &&
- currentUser.role !== 20 &&
- currentUser.role < parseInt(key)
- )
- return null;
-
- return (
-
- <>{ROLE[parseInt(key) as keyof typeof ROLE]}>
-
- );
- })}
-
-
- {
- if (member.member) {
- setSelectedRemoveMember(member.id);
- } else {
- setSelectedInviteRemoveMember(member.id);
- }
- }}
- >
- Remove member
-
-
+
+
+ {member.first_name} {member.last_name}
+
+
{member.email}
- ))
- : null}
-
- )}
-
-
-
- >
+
+ {!member?.status && (
+
+ )}
+ {member?.status && !member?.accountCreated && (
+
+ )}
+
{
+ if (!workspaceSlug) return;
+
+ mutateMembers(
+ (prevData) =>
+ prevData?.map((m) =>
+ m.id === member.id ? { ...m, role: value } : m
+ ),
+ false
+ );
+
+ workspaceService
+ .updateWorkspaceMember(workspaceSlug?.toString(), member.id, {
+ role: value,
+ })
+ .catch(() => {
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message:
+ "An error occurred while updating member role. Please try again.",
+ });
+ });
+ }}
+ position="right"
+ disabled={
+ member.memberId === currentUser?.member.id ||
+ !member.status ||
+ (currentUser &&
+ currentUser.role !== 20 &&
+ currentUser.role < member.role)
+ }
+ >
+ {Object.keys(ROLE).map((key) => {
+ if (
+ currentUser &&
+ currentUser.role !== 20 &&
+ currentUser.role < parseInt(key)
+ )
+ return null;
+
+ return (
+
+ <>{ROLE[parseInt(key) as keyof typeof ROLE]}>
+
+ );
+ })}
+
+
+ {
+ if (member.member) {
+ setSelectedRemoveMember(member.id);
+ } else {
+ setSelectedInviteRemoveMember(member.id);
+ }
+ }}
+ >
+ Remove member
+
+
+
+
+ ))
+ : null}
+
+ )}
+
+
+
);
};
From 59c0de9b57848617f0caa2c8c25cc022b9b08e09 Mon Sep 17 00:00:00 2001
From: Sai Tharun <65693649+saitharunsai@users.noreply.github.com>
Date: Mon, 19 Jun 2023 08:23:44 +0530
Subject: [PATCH 12/59] :hammer: updated missing migration file (#1321)
Co-authored-by: saitharunsai
---
.gitignore | 1 +
.../db/migrations/0033_auto_20230618_2125.py | 83 +++++++++++++++++++
apiserver/plane/settings/local.py | 4 +-
apiserver/plane/settings/staging.py | 4 +-
4 files changed, 88 insertions(+), 4 deletions(-)
create mode 100644 apiserver/plane/db/migrations/0033_auto_20230618_2125.py
diff --git a/.gitignore b/.gitignore
index 3562ab0b3..921881df4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,6 +43,7 @@ yarn-error.log*
## Django ##
venv
+.venv
*.pyc
staticfiles
mediafiles
diff --git a/apiserver/plane/db/migrations/0033_auto_20230618_2125.py b/apiserver/plane/db/migrations/0033_auto_20230618_2125.py
new file mode 100644
index 000000000..8eb2eda62
--- /dev/null
+++ b/apiserver/plane/db/migrations/0033_auto_20230618_2125.py
@@ -0,0 +1,83 @@
+# Generated by Django 3.2.19 on 2023-06-18 15:55
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('db', '0032_auto_20230520_2015'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Inbox',
+ 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)),
+ ('name', models.CharField(max_length=255)),
+ ('description', models.TextField(blank=True, verbose_name='Inbox Description')),
+ ('is_default', models.BooleanField(default=False)),
+ ('view_props', models.JSONField(default=dict)),
+ ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inbox_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
+ ],
+ options={
+ 'verbose_name': 'Inbox',
+ 'verbose_name_plural': 'Inboxes',
+ 'db_table': 'inboxes',
+ 'ordering': ('name',),
+ },
+ ),
+ migrations.AddField(
+ model_name='project',
+ name='inbox_view',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.CreateModel(
+ name='InboxIssue',
+ 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)),
+ ('status', models.IntegerField(choices=[(-2, 'Pending'), (-1, 'Rejected'), (0, 'Snoozed'), (1, 'Accepted'), (2, 'Duplicate')], default=-2)),
+ ('snoozed_till', models.DateTimeField(null=True)),
+ ('source', models.TextField(blank=True, null=True)),
+ ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inboxissue_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
+ ('duplicate_to', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inbox_duplicate', to='db.issue')),
+ ('inbox', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_inbox', to='db.inbox')),
+ ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_inbox', to='db.issue')),
+ ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_inboxissue', to='db.project')),
+ ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inboxissue_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_inboxissue', to='db.workspace')),
+ ],
+ options={
+ 'verbose_name': 'InboxIssue',
+ 'verbose_name_plural': 'InboxIssues',
+ 'db_table': 'inbox_issues',
+ 'ordering': ('-created_at',),
+ },
+ ),
+ migrations.AddField(
+ model_name='inbox',
+ name='project',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_inbox', to='db.project'),
+ ),
+ migrations.AddField(
+ model_name='inbox',
+ name='updated_by',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inbox_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'),
+ ),
+ migrations.AddField(
+ model_name='inbox',
+ name='workspace',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_inbox', to='db.workspace'),
+ ),
+ migrations.AlterUniqueTogether(
+ name='inbox',
+ unique_together={('name', 'project')},
+ ),
+ ]
diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py
index 3a3a3d9a3..c7d8b1bdf 100644
--- a/apiserver/plane/settings/local.py
+++ b/apiserver/plane/settings/local.py
@@ -18,10 +18,10 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
- "NAME": "plane",
+ "NAME": os.environ.get("PGUSER", "plane"),
"USER": "",
"PASSWORD": "",
- "HOST": "",
+ "HOST": os.environ.get("PGHOST", "localhost"),
}
}
diff --git a/apiserver/plane/settings/staging.py b/apiserver/plane/settings/staging.py
index 851ad77f2..2dab632f1 100644
--- a/apiserver/plane/settings/staging.py
+++ b/apiserver/plane/settings/staging.py
@@ -16,10 +16,10 @@ DEBUG = True
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
- "NAME": "plane",
+ "NAME": os.environ.get("PGUSER", "plane"),
"USER": "",
"PASSWORD": "",
- "HOST": "",
+ "HOST": os.environ.get("PGHOST", "localhost"),
}
}
From 1da86b80b2e1a5b2f9554ae95afc90f4dc555576 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Mon, 19 Jun 2023 12:59:57 +0530
Subject: [PATCH 13/59] chore: change charts library (#1305)
* fix: dashboard charts
* fix: cycles new charts
* chore: sidebar burn down chart and calendar graph
* chore: update dashboard line and pie graph
* chore: update axes width of burndown chart
---------
Co-authored-by: Dakshesh Jain
---
.../core/sidebar/progress-chart.tsx | 146 +++++++----
apps/app/components/cycles/sidebar.tsx | 6 +-
apps/app/components/ui/progress-bar.tsx | 2 +-
.../components/workspace/activity-graph.tsx | 153 ++---------
.../workspace/completed-issues-graph.tsx | 67 +++--
.../components/workspace/issues-pie-chart.tsx | 165 ++++--------
.../app/components/workspace/issues-stats.tsx | 5 +-
apps/app/package.json | 1 -
.../projects/[projectId]/cycles/[cycleId].tsx | 10 +-
yarn.lock | 240 +-----------------
10 files changed, 211 insertions(+), 584 deletions(-)
diff --git a/apps/app/components/core/sidebar/progress-chart.tsx b/apps/app/components/core/sidebar/progress-chart.tsx
index 9006b58fc..93e11f762 100644
--- a/apps/app/components/core/sidebar/progress-chart.tsx
+++ b/apps/app/components/core/sidebar/progress-chart.tsx
@@ -1,12 +1,11 @@
import React from "react";
-import { XAxis, YAxis, Tooltip, AreaChart, Area, ReferenceLine, TooltipProps} from "recharts";
-
+// ui
+import { LineGraph } from "components/ui";
+// helpers
+import { getDatesInRange, renderShortNumericDateFormat } from "helpers/date-time.helper";
//types
import { IIssue } from "types";
-import { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent";
-// helper
-import { getDatesInRange, renderShortNumericDateFormat } from "helpers/date-time.helper";
type Props = {
issues: IIssue[];
@@ -16,12 +15,40 @@ type Props = {
height?: number;
};
-const ProgressChart: React.FC = ({ issues, start, end, width = 360, height = 160 }) => {
+const styleById = {
+ ideal: {
+ strokeDasharray: "6, 3",
+ strokeWidth: 1,
+ },
+ default: {
+ strokeWidth: 1,
+ },
+};
+
+const DashedLine = ({ series, lineGenerator, xScale, yScale }: any) =>
+ series.map(({ id, data, color }: any) => (
+ ({
+ x: xScale(d.data.x),
+ y: yScale(d.data.y),
+ }))
+ )}
+ fill="none"
+ stroke={color ?? "#ddd"}
+ style={styleById[id as keyof typeof styleById] || styleById.default}
+ />
+ ));
+
+const ProgressChart: React.FC = ({ issues, start, end }) => {
const startDate = new Date(start);
const endDate = new Date(end);
+
const getChartData = () => {
const dateRangeArray = getDatesInRange(startDate, endDate);
let count = 0;
+
const dateWiseData = dateRangeArray.map((d) => {
const current = d.toISOString().split("T")[0];
const total = issues.length;
@@ -39,56 +66,67 @@ const ProgressChart: React.FC = ({ issues, start, end, width = 360, heigh
return dateWiseData;
};
- const CustomTooltip = ({ active, payload }: TooltipProps) => {
- if (active && payload && payload.length) {
- return (
-
-
{payload[0].payload.currentDate}
-
- );
- }
- return null;
- };
- const ChartData = getChartData();
+ const chartData = getChartData();
+
return (
-
-
+ ({
+ index,
+ x: item.currentDate,
+ y: item.pending,
+ color: "#3F76FF",
+ })),
+ enableArea: true,
+ },
+ {
+ id: "ideal",
+ color: "#a9bbd0",
+ fill: "transparent",
+ data: [
+ {
+ x: chartData[0].currentDate,
+ y: issues.length,
+ },
+ {
+ x: chartData[chartData.length - 1].currentDate,
+ y: 0,
+ },
+ ],
+ },
+ ]}
+ layers={["grid", "markers", "areas", DashedLine, "slices", "points", "axes", "legends"]}
+ axisBottom={{
+ tickValues: chartData.map((item, index) => (index % 2 === 0 ? item.currentDate : "")),
}}
- >
-
-
-
-
-
-
-
-
-
- } />
-
-
-
+ enablePoints={false}
+ enableArea
+ colors={(datum) => datum.color ?? "#3F76FF"}
+ customYAxisTickValues={[0, issues.length]}
+ gridXValues={chartData.map((item, index) => (index % 2 === 0 ? item.currentDate : ""))}
+ theme={{
+ background: "rgb(var(--color-bg-sidebar))",
+ axis: {
+ domain: {
+ line: {
+ stroke: "rgb(var(--color-border))",
+ strokeWidth: 1,
+ },
+ },
+ },
+ }}
+ />
);
};
diff --git a/apps/app/components/cycles/sidebar.tsx b/apps/app/components/cycles/sidebar.tsx
index 03747fc3b..2cb5b9fcb 100644
--- a/apps/app/components/cycles/sidebar.tsx
+++ b/apps/app/components/cycles/sidebar.tsx
@@ -485,7 +485,6 @@ export const CycleDetailsSidebar: React.FC = ({
-
{({ open }) => (
@@ -552,7 +551,7 @@ export const CycleDetailsSidebar: React.FC = ({
-
+
-
{({ open }) => (
@@ -604,7 +602,7 @@ export const CycleDetailsSidebar: React.FC = ({
{cycle.total_issues > 0 ? (
-
+
= ({
return (
{renderOuterCircle()}
-
+
);
};
diff --git a/apps/app/components/workspace/activity-graph.tsx b/apps/app/components/workspace/activity-graph.tsx
index 1f9db203d..ec8e1dfd4 100644
--- a/apps/app/components/workspace/activity-graph.tsx
+++ b/apps/app/components/workspace/activity-graph.tsx
@@ -1,141 +1,34 @@
-import { useEffect, useRef, useState } from "react";
-
// ui
-import { Tooltip } from "components/ui";
+import { CalendarGraph } from "components/ui";
// helpers
-import { renderDateFormat, renderShortNumericDateFormat } from "helpers/date-time.helper";
+import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types
import { IUserActivity } from "types";
-// constants
-import { DAYS, MONTHS } from "constants/project";
type Props = {
activities: IUserActivity[] | undefined;
};
-export const ActivityGraph: React.FC = ({ activities }) => {
- const ref = useRef(null);
-
- const [width, setWidth] = useState(0);
-
- const today = new Date();
- const lastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1);
- const twoMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 2, 1);
- const threeMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 3, 1);
- const fourMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 4, 1);
- const fiveMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 5, 1);
-
- const recentMonths = [
- fiveMonthsAgo,
- fourMonthsAgo,
- threeMonthsAgo,
- twoMonthsAgo,
- lastMonth,
- today,
- ];
-
- const getDatesOfMonth = (dateOfMonth: Date) => {
- const month = dateOfMonth.getMonth();
- const year = dateOfMonth.getFullYear();
-
- const dates = [];
- const date = new Date(year, month, 1);
-
- while (date.getMonth() === month && date < new Date()) {
- dates.push(renderDateFormat(new Date(date)));
- date.setDate(date.getDate() + 1);
+export const ActivityGraph: React.FC = ({ activities }) => (
+ ({
+ day: activity.created_date,
+ value: activity.activity_count,
+ })) ?? []
}
-
- return dates;
- };
-
- const recentDates = [
- ...getDatesOfMonth(recentMonths[0]),
- ...getDatesOfMonth(recentMonths[1]),
- ...getDatesOfMonth(recentMonths[2]),
- ...getDatesOfMonth(recentMonths[3]),
- ...getDatesOfMonth(recentMonths[4]),
- ...getDatesOfMonth(recentMonths[5]),
- ];
-
- const activitiesIntensity = (activityCount: number) => {
- if (activityCount <= 3) return "opacity-20";
- else if (activityCount > 3 && activityCount <= 6) return "opacity-40";
- else if (activityCount > 6 && activityCount <= 9) return "opacity-80";
- else return "";
- };
-
- const addPaddingTiles = () => {
- const firstDateDay = new Date(recentDates[0]).getDay();
-
- for (let i = 0; i < firstDateDay; i++) recentDates.unshift("");
- };
- addPaddingTiles();
-
- useEffect(() => {
- if (!ref.current) return;
-
- setWidth(ref.current.offsetWidth);
- }, [ref]);
-
- return (
-
-
-
- {DAYS.map((day, index) => (
-
- {index % 2 === 0 && day.substring(0, 3)}
-
- ))}
-
-
-
- {recentMonths.map((month, index) => (
-
- {MONTHS[month.getMonth()].substring(0, 3)}
-
- ))}
-
-
- {recentDates.map((date, index) => {
- const isActive = activities?.find((a) => a.created_date === date);
-
- return (
-
-
-
- );
- })}
-
-
- Less
-
-
-
-
-
- More
-
-
+ from={activities?.length ? activities[0].created_date : new Date()}
+ to={activities?.length ? activities[activities.length - 1].created_date : new Date()}
+ height="200px"
+ margin={{ bottom: 0, left: 10, right: 10, top: 0 }}
+ tooltip={(datum) => (
+
+ {renderShortDateWithYearFormat(datum.day)}: {" "}
+ {datum.value}
-
- );
-};
+ )}
+ theme={{
+ background: "rgb(var(--color-bg-base))",
+ }}
+ />
+);
diff --git a/apps/app/components/workspace/completed-issues-graph.tsx b/apps/app/components/workspace/completed-issues-graph.tsx
index e34f1a939..9a1ada618 100644
--- a/apps/app/components/workspace/completed-issues-graph.tsx
+++ b/apps/app/components/workspace/completed-issues-graph.tsx
@@ -1,15 +1,5 @@
-// recharts
-import {
- CartesianGrid,
- Line,
- LineChart,
- ResponsiveContainer,
- Tooltip,
- XAxis,
- YAxis,
-} from "recharts";
// ui
-import { CustomMenu } from "components/ui";
+import { CustomMenu, LineGraph } from "components/ui";
// constants
import { MONTHS } from "constants/project";
@@ -36,13 +26,6 @@ export const CompletedIssuesGraph: React.FC
= ({ month, issues, setMonth
});
}
- const CustomTooltip = ({ payload, label }: any) => (
-
-
{label}
- Completed issues: {payload[0]?.value}
-
- );
-
return (
@@ -56,25 +39,37 @@ export const CompletedIssuesGraph: React.FC
= ({ month, issues, setMonth
-
-
-
-
-
- } />
- item.completed_count === 0) ? (
+
+
No issues closed this month
+
+ ) : (
+ <>
+ ({
+ x: item.week_in_month,
+ y: item.completed_count,
+ })),
+ },
+ ]}
+ margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
+ customYAxisTickValues={data.map((item) => item.completed_count)}
+ colors={(datum) => datum.color}
+ theme={{
+ background: "rgb(var(--color-bg-base))",
+ }}
/>
-
-
-
-
- Completed Issues
-
+
+
+ Completed Issues
+
+ >
+ )}
);
diff --git a/apps/app/components/workspace/issues-pie-chart.tsx b/apps/app/components/workspace/issues-pie-chart.tsx
index d65e80c4a..ada35d080 100644
--- a/apps/app/components/workspace/issues-pie-chart.tsx
+++ b/apps/app/components/workspace/issues-pie-chart.tsx
@@ -1,7 +1,7 @@
-import { useCallback, useState } from "react";
-
-// recharts
-import { Cell, Legend, Pie, PieChart, ResponsiveContainer, Sector } from "recharts";
+// ui
+import { PieGraph } from "components/ui";
+// helpers
+import { capitalizeFirstLetter } from "helpers/string.helper";
// types
import { IUserStateDistribution } from "types";
// constants
@@ -11,113 +11,52 @@ type Props = {
groupedIssues: IUserStateDistribution[] | undefined;
};
-export const IssuesPieChart: React.FC = ({ groupedIssues }) => {
- const [activeIndex, setActiveIndex] = useState(0);
-
- const onPieEnter = useCallback(
- (_: any, index: number) => {
- setActiveIndex(index);
- },
- [setActiveIndex]
- );
-
- const renderActiveShape = ({
- cx,
- cy,
- midAngle,
- innerRadius,
- outerRadius,
- startAngle,
- endAngle,
- fill,
- payload,
- value,
- }: any) => {
- const RADIAN = Math.PI / 180;
- const sin = Math.sin(-RADIAN * midAngle);
- const cos = Math.cos(-RADIAN * midAngle);
- const sx = cx + (outerRadius + 10) * cos;
- const sy = cy + (outerRadius + 10) * sin;
- const mx = cx + (outerRadius + 30) * cos;
- const my = cy + (outerRadius + 30) * sin;
- const ex = mx + (cos >= 0 ? 1 : -1) * 22;
- const ey = my;
- const textAnchor = cos >= 0 ? "start" : "end";
-
- return (
-
-
- {payload.state_group}
-
-
-
-
-
- = 0 ? 1 : -1) * 12} y={ey} textAnchor={textAnchor} fill="#858e96">
- {value} issues
-
-
- );
- };
-
- return (
-
-
Issues by States
-
-
-
-
- {groupedIssues?.map((cell) => (
- |
- ))}
-
-
-
-
-
+export const IssuesPieChart: React.FC
= ({ groupedIssues }) => (
+
+
Issues by States
+
+
({
+ id: cell.state_group,
+ label: cell.state_group,
+ value: cell.state_count,
+ color: STATE_GROUP_COLORS[cell.state_group.toLowerCase()],
+ })) ?? []
+ }
+ height="320px"
+ innerRadius={0.5}
+ arcLinkLabel={(cell) => `${capitalizeFirstLetter(cell.label.toString())} (${cell.value})`}
+ legends={[
+ {
+ anchor: "right",
+ direction: "column",
+ justify: false,
+ translateX: 0,
+ translateY: 56,
+ itemsSpacing: 10,
+ itemWidth: 100,
+ itemHeight: 18,
+ itemTextColor: "rgb(var(--color-text-secondary))",
+ itemDirection: "left-to-right",
+ itemOpacity: 1,
+ symbolSize: 12,
+ symbolShape: "square",
+ data:
+ groupedIssues?.map((cell) => ({
+ id: cell.state_group,
+ label: capitalizeFirstLetter(cell.state_group),
+ value: cell.state_count,
+ color: STATE_GROUP_COLORS[cell.state_group.toLowerCase()],
+ })) ?? [],
+ },
+ ]}
+ activeInnerRadiusOffset={5}
+ colors={(datum) => datum.data.color}
+ theme={{
+ background: "rgb(var(--color-bg-base))",
+ }}
+ />
- );
-};
+
+);
diff --git a/apps/app/components/workspace/issues-stats.tsx b/apps/app/components/workspace/issues-stats.tsx
index 6117ec2a1..bc7f0364f 100644
--- a/apps/app/components/workspace/issues-stats.tsx
+++ b/apps/app/components/workspace/issues-stats.tsx
@@ -1,8 +1,7 @@
// components
-import { Loader } from "components/ui";
import { ActivityGraph } from "components/workspace";
-// helpers
-import { groupBy } from "helpers/array.helper";
+// ui
+import { Loader } from "components/ui";
// types
import { IUserWorkspaceDashboard } from "types";
diff --git a/apps/app/package.json b/apps/app/package.json
index 01287f068..64eb1aa99 100644
--- a/apps/app/package.json
+++ b/apps/app/package.json
@@ -46,7 +46,6 @@
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.38.0",
"react-markdown": "^8.0.7",
- "recharts": "^2.3.2",
"remirror": "^2.0.23",
"swr": "^2.1.3",
"tlds": "^1.238.0",
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx
index 718a8bd3a..af9f073a8 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx
@@ -75,11 +75,6 @@ const SingleCycle: React.FC = () => {
: null
);
- const cycleStatus =
- cycleDetails?.start_date && cycleDetails?.end_date
- ? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
- : "draft";
-
const { data: issues } = useSWR(
workspaceSlug && projectId
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
@@ -89,6 +84,11 @@ const SingleCycle: React.FC = () => {
: null
);
+ const cycleStatus =
+ cycleDetails?.start_date && cycleDetails?.end_date
+ ? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
+ : "draft";
+
const openIssuesListModal = () => {
setCycleIssuesListModal(true);
};
diff --git a/yarn.lock b/yarn.lock
index 5fb2c1578..652df3ed8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3035,57 +3035,6 @@
dependencies:
"@types/tern" "*"
-"@types/d3-array@^3.0.3":
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.4.tgz#44eebe40be57476cad6a0cd6a85b0f57d54185a2"
- integrity sha512-nwvEkG9vYOc0Ic7G7kwgviY4AQlTfYGIZ0fqB7CQHXGyYM6nO7kJh5EguSNA3jfh4rq7Sb7eMVq8isuvg2/miQ==
-
-"@types/d3-color@*":
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.0.tgz#6594da178ded6c7c3842f3cc0ac84b156f12f2d4"
- integrity sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==
-
-"@types/d3-ease@^3.0.0":
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.0.tgz#c29926f8b596f9dadaeca062a32a45365681eae0"
- integrity sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==
-
-"@types/d3-interpolate@^3.0.1":
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc"
- integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==
- dependencies:
- "@types/d3-color" "*"
-
-"@types/d3-path@*":
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b"
- integrity sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==
-
-"@types/d3-scale@^4.0.2":
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.3.tgz#7a5780e934e52b6f63ad9c24b105e33dd58102b5"
- integrity sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==
- dependencies:
- "@types/d3-time" "*"
-
-"@types/d3-shape@^3.1.0":
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.1.tgz#15cc497751dac31192d7aef4e67a8d2c62354b95"
- integrity sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A==
- dependencies:
- "@types/d3-path" "*"
-
-"@types/d3-time@*", "@types/d3-time@^3.0.0":
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.0.tgz#e1ac0f3e9e195135361fa1a1d62f795d87e6e819"
- integrity sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==
-
-"@types/d3-timer@^3.0.0":
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.0.tgz#e2505f1c21ec08bda8915238e397fb71d2fc54ce"
- integrity sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==
-
"@types/debug@^4.0.0":
version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
@@ -3972,7 +3921,7 @@ chokidar@^3.5.3:
optionalDependencies:
fsevents "~2.3.2"
-classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1, classnames@^2.3.2:
+classnames@^2.2.6, classnames@^2.3.1, classnames@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
@@ -4165,11 +4114,6 @@ css-in-js-utils@^3.1.0:
dependencies:
hyphenate-style-name "^1.0.3"
-css-unit-converter@^1.1.1:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21"
- integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==
-
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
@@ -4187,23 +4131,11 @@ d3-array@2, d3-array@^2.3.0:
dependencies:
internmap "^1.0.0"
-"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6:
- version "3.2.3"
- resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.3.tgz#39f1f4954e4a09ff69ac597c2d61906b04e84740"
- integrity sha512-JRHwbQQ84XuAESWhvIPaUV4/1UYTBOLiOPGWqgFDHZS1D5QN9c57FbH3QpEnQMYiOXNzKUQyGTZf+EVO7RT5TQ==
- dependencies:
- internmap "1 - 2"
-
"d3-color@1 - 2", d3-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e"
integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==
-"d3-color@1 - 3":
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
- integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
-
d3-delaunay@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-5.3.0.tgz#b47f05c38f854a4e7b3cea80e0bb12e57398772d"
@@ -4211,21 +4143,11 @@ d3-delaunay@^5.3.0:
dependencies:
delaunator "4"
-d3-ease@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
- integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
-
"d3-format@1 - 2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767"
integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==
-"d3-format@1 - 3":
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
- integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
-
d3-format@^1.4.4:
version "1.4.5"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4"
@@ -4238,23 +4160,11 @@ d3-format@^1.4.4:
dependencies:
d3-color "1 - 2"
-"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
- integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
- dependencies:
- d3-color "1 - 3"
-
d3-path@1:
version "1.0.9"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
-d3-path@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
- integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
-
d3-scale-chromatic@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz#c13f3af86685ff91323dc2f0ebd2dabbd72d8bab"
@@ -4274,17 +4184,6 @@ d3-scale@^3.2.3:
d3-time "^2.1.1"
d3-time-format "2 - 3"
-d3-scale@^4.0.2:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
- integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
- dependencies:
- d3-array "2.10.0 - 3"
- d3-format "1 - 3"
- d3-interpolate "1.2.0 - 3"
- d3-time "2.1.1 - 3"
- d3-time-format "2 - 4"
-
d3-shape@^1.3.5:
version "1.3.7"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
@@ -4292,13 +4191,6 @@ d3-shape@^1.3.5:
dependencies:
d3-path "1"
-d3-shape@^3.1.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
- integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
- dependencies:
- d3-path "^3.1.0"
-
"d3-time-format@2 - 3", d3-time-format@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6"
@@ -4306,13 +4198,6 @@ d3-shape@^3.1.0:
dependencies:
d3-time "1 - 2"
-"d3-time-format@2 - 4":
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
- integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
- dependencies:
- d3-time "1 - 3"
-
"d3-time@1 - 2", d3-time@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682"
@@ -4320,23 +4205,11 @@ d3-shape@^3.1.0:
dependencies:
d3-array "2"
-"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
- integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
- dependencies:
- d3-array "2 - 3"
-
d3-time@^1.0.10, d3-time@^1.0.11:
version "1.1.0"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1"
integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==
-d3-timer@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
- integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
-
damerau-levenshtein@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@@ -4368,11 +4241,6 @@ debug@^3.2.7:
dependencies:
ms "^2.1.1"
-decimal.js-light@^2.4.1:
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
- integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
-
decode-named-character-reference@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e"
@@ -4513,13 +4381,6 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
-dom-helpers@^3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
- integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
- dependencies:
- "@babel/runtime" "^7.1.2"
-
dom-helpers@^5.0.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
@@ -5072,11 +4933,6 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
-eventemitter3@^4.0.1:
- version "4.0.7"
- resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
- integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
-
extend@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
@@ -5092,11 +4948,6 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
-fast-equals@^4.0.3:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-4.0.3.tgz#72884cc805ec3c6679b99875f6b7654f39f0e8c7"
- integrity sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==
-
fast-glob@^3.2.12, fast-glob@^3.2.9:
version "3.2.12"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
@@ -5591,11 +5442,6 @@ internal-slot@^1.0.3, internal-slot@^1.0.4, internal-slot@^1.0.5:
has "^1.0.3"
side-channel "^1.0.4"
-"internmap@1 - 2":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
- integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
-
internmap@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95"
@@ -6170,7 +6016,7 @@ lodash.truncate@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==
-lodash@^4.0.1, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21:
+lodash@^4.0.1, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -7057,11 +6903,6 @@ postcss-selector-parser@^6.0.11:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
-postcss-value-parser@^3.3.0:
- version "3.3.1"
- resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
- integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
-
postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
@@ -7391,7 +7232,7 @@ react-hook-form@^7.38.0:
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.43.9.tgz#84b56ac2f38f8e946c6032ccb760e13a1037c66d"
integrity sha512-AUDN3Pz2NSeoxQ7Hs6OhQhDr6gtF9YRuutGDwPQqhSUAHJSgGl2VeY3qN19MG0SucpjgDiuMJ4iC5T5uB+eaNQ==
-react-is@^16.10.2, react-is@^16.13.1, react-is@^16.7.0:
+react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -7489,21 +7330,6 @@ react-remove-scroll@2.5.4:
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
-react-resize-detector@^8.0.4:
- version "8.1.0"
- resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-8.1.0.tgz#1c7817db8bc886e2dbd3fbe3b26ea8e56be0524a"
- integrity sha512-S7szxlaIuiy5UqLhLL1KY3aoyGHbZzsTpYal9eYMwCyKqoqoVLCmIgAgNyIM1FhnP2KyBygASJxdhejrzjMb+w==
- dependencies:
- lodash "^4.17.21"
-
-react-smooth@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-2.0.2.tgz#0ef24213628cb13bf4305194a050e1db4302a3a1"
- integrity sha512-pgqSp1q8rAGtF1bXQE0m3CHGLNfZZh5oA5o1tsPLXRHnKtkujMIJ8Ws5nO1mTySZf1c4vgwlEk+pHi3Ln6eYLw==
- dependencies:
- fast-equals "^4.0.3"
- react-transition-group "2.9.0"
-
react-style-singleton@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
@@ -7513,16 +7339,6 @@ react-style-singleton@^2.2.1:
invariant "^2.2.4"
tslib "^2.0.0"
-react-transition-group@2.9.0:
- version "2.9.0"
- resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
- integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
- dependencies:
- dom-helpers "^3.4.0"
- loose-envify "^1.4.0"
- prop-types "^15.6.2"
- react-lifecycles-compat "^3.0.4"
-
react-transition-group@^4.4.5:
version "4.4.5"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
@@ -7561,36 +7377,6 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
-recharts-scale@^0.4.4:
- version "0.4.5"
- resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9"
- integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==
- dependencies:
- decimal.js-light "^2.4.1"
-
-recharts@^2.3.2:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.5.0.tgz#34452852509099502690f9d2a72bde1d4cd65648"
- integrity sha512-0EQYz3iA18r1Uq8VqGZ4dABW52AKBnio37kJgnztIqprELJXpOEsa0SzkqU1vjAhpCXCv52Dx1hiL9119xsqsQ==
- dependencies:
- classnames "^2.2.5"
- eventemitter3 "^4.0.1"
- lodash "^4.17.19"
- react-is "^16.10.2"
- react-resize-detector "^8.0.4"
- react-smooth "^2.0.2"
- recharts-scale "^0.4.4"
- reduce-css-calc "^2.1.8"
- victory-vendor "^36.6.8"
-
-reduce-css-calc@^2.1.8:
- version "2.1.8"
- resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz#7ef8761a28d614980dc0c982f772c93f7a99de03"
- integrity sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==
- dependencies:
- css-unit-converter "^1.1.1"
- postcss-value-parser "^3.3.0"
-
redux@^4.0.0, redux@^4.0.4:
version "4.2.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
@@ -8730,26 +8516,6 @@ vfile@^5.0.0:
unist-util-stringify-position "^3.0.0"
vfile-message "^3.0.0"
-victory-vendor@^36.6.8:
- version "36.6.10"
- resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.6.10.tgz#e7e3646deaf0e850bc60dffdad6d7a4abee40632"
- integrity sha512-7YqYGtsA4mByokBhCjk+ewwPhUfzhR1I3Da6/ZsZUv/31ceT77RKoaqrxRq5Ki+9we4uzf7+A+7aG2sfYhm7nA==
- dependencies:
- "@types/d3-array" "^3.0.3"
- "@types/d3-ease" "^3.0.0"
- "@types/d3-interpolate" "^3.0.1"
- "@types/d3-scale" "^4.0.2"
- "@types/d3-shape" "^3.1.0"
- "@types/d3-time" "^3.0.0"
- "@types/d3-timer" "^3.0.0"
- d3-array "^3.1.6"
- d3-ease "^3.0.1"
- d3-interpolate "^3.0.1"
- d3-scale "^4.0.2"
- d3-shape "^3.1.0"
- d3-time "^3.0.0"
- d3-timer "^3.0.1"
-
w3c-keyname@^2.2.0, w3c-keyname@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.6.tgz#8412046116bc16c5d73d4e612053ea10a189c85f"
From a3f6d61347ef98171f17d6b59bd4cacf8ebaf14f Mon Sep 17 00:00:00 2001
From: vamsi
Date: Mon, 19 Jun 2023 18:47:39 +0530
Subject: [PATCH 14/59] LICENSE change for Plane
---
LICENSE.txt | 798 +++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 629 insertions(+), 169 deletions(-)
diff --git a/LICENSE.txt b/LICENSE.txt
index 0320895f7..5087e61e2 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,201 +1,661 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
- 1. Definitions.
+ Preamble
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
+ The precise terms and conditions for copying, distribution and
+modification follow.
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
+ TERMS AND CONDITIONS
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
+ 0. Definitions.
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
+ "This License" refers to version 3 of the GNU Affero General Public License.
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
+ 1. Source Code.
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
- END OF TERMS AND CONDITIONS
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
- APPENDIX: How to apply the Apache License to your work.
+ The Corresponding Source for a work in source code form is that
+same work.
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
+ 2. Basic Permissions.
- Copyright 2022 Plane Software Labs Private Limited
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
- http://www.apache.org/licenses/LICENSE-2.0
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
\ No newline at end of file
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+ .
\ No newline at end of file
From f2ebac1bb486b614447c257a4faad368893c9630 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Mon, 19 Jun 2023 23:39:47 +0530
Subject: [PATCH 15/59] fix: issue details page auth (#1331)
---
apps/app/components/issues/index.ts | 1 +
apps/app/components/issues/main-content.tsx | 118 ++++++++++++++++++
.../projects/[projectId]/issues/[issueId].tsx | 100 +--------------
3 files changed, 123 insertions(+), 96 deletions(-)
create mode 100644 apps/app/components/issues/main-content.tsx
diff --git a/apps/app/components/issues/index.ts b/apps/app/components/issues/index.ts
index c5322645f..7549d19d4 100644
--- a/apps/app/components/issues/index.ts
+++ b/apps/app/components/issues/index.ts
@@ -5,6 +5,7 @@ export * from "./activity";
export * from "./delete-issue-modal";
export * from "./description-form";
export * from "./form";
+export * from "./main-content";
export * from "./modal";
export * from "./my-issues-list-item";
export * from "./parent-issues-list-modal";
diff --git a/apps/app/components/issues/main-content.tsx b/apps/app/components/issues/main-content.tsx
new file mode 100644
index 000000000..6123556a5
--- /dev/null
+++ b/apps/app/components/issues/main-content.tsx
@@ -0,0 +1,118 @@
+import Link from "next/link";
+import { useRouter } from "next/router";
+
+import useSWR from "swr";
+
+// services
+import issuesService from "services/issues.service";
+// hooks
+import useUserAuth from "hooks/use-user-auth";
+// contexts
+import { useProjectMyMembership } from "contexts/project-member.context";
+// components
+import {
+ AddComment,
+ IssueActivitySection,
+ IssueAttachmentUpload,
+ IssueAttachments,
+ IssueDescriptionForm,
+ SubIssuesList,
+} from "components/issues";
+// ui
+import { CustomMenu } from "components/ui";
+// types
+import { IIssue } from "types";
+// fetch-keys
+import { SUB_ISSUES } from "constants/fetch-keys";
+
+type Props = {
+ issueDetails: IIssue;
+ submitChanges: (formData: Partial) => Promise;
+};
+
+export const IssueMainContent: React.FC = ({ issueDetails, submitChanges }) => {
+ const router = useRouter();
+ const { workspaceSlug, projectId, issueId } = router.query;
+
+ const { user } = useUserAuth();
+ const { memberRole } = useProjectMyMembership();
+
+ const { data: siblingIssues } = useSWR(
+ workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null,
+ workspaceSlug && projectId && issueDetails?.parent
+ ? () =>
+ issuesService.subIssues(
+ workspaceSlug as string,
+ projectId as string,
+ issueDetails.parent ?? ""
+ )
+ : null
+ );
+
+ return (
+ <>
+
+ {issueDetails?.parent && issueDetails.parent !== "" ? (
+
+ ) : null}
+
+
+
+
+
+
+
+
Comments/Activity
+
+
+
+ >
+ );
+};
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx
index e1c883c0b..516a2471b 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx
@@ -1,14 +1,11 @@
import React, { useCallback, useEffect } from "react";
-import Link from "next/link";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// react-hook-form
import { useForm } from "react-hook-form";
-// contexts
-import { useProjectMyMembership } from "contexts/project-member.context";
// services
import issuesService from "services/issues.service";
// hooks
@@ -16,23 +13,15 @@ import useUserAuth from "hooks/use-user-auth";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// components
-import {
- IssueDescriptionForm,
- SubIssuesList,
- IssueDetailsSidebar,
- IssueActivitySection,
- AddComment,
- IssueAttachmentUpload,
- IssueAttachments,
-} from "components/issues";
+import { IssueDetailsSidebar, IssueMainContent } from "components/issues";
// ui
-import { Loader, CustomMenu } from "components/ui";
+import { Loader } from "components/ui";
import { Breadcrumbs } from "components/breadcrumbs";
// types
import { IIssue } from "types";
import type { NextPage } from "next";
// fetch-keys
-import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
+import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS } from "constants/fetch-keys";
const defaultValues = {
name: "",
@@ -55,7 +44,6 @@ const IssueDetailsPage: NextPage = () => {
const { workspaceSlug, projectId, issueId } = router.query;
const { user } = useUserAuth();
- const { memberRole } = useProjectMyMembership();
const { data: issueDetails, mutate: mutateIssueDetails } = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
@@ -65,18 +53,6 @@ const IssueDetailsPage: NextPage = () => {
: null
);
- const { data: siblingIssues } = useSWR(
- workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null,
- workspaceSlug && projectId && issueDetails?.parent
- ? () =>
- issuesService.subIssues(
- workspaceSlug as string,
- projectId as string,
- issueDetails.parent ?? ""
- )
- : null
- );
-
const { reset, control, watch } = useForm({
defaultValues,
});
@@ -149,75 +125,7 @@ const IssueDetailsPage: NextPage = () => {
{issueDetails && projectId ? (
-
- {issueDetails?.parent && issueDetails.parent !== "" ? (
-
- ) : null}
-
-
-
-
-
-
-
-
Comments/Activity
-
-
-
+
Date: Tue, 20 Jun 2023 09:36:08 +0530
Subject: [PATCH 16/59] chore: added DEBUG value for docker setup (#1327)
* chore: add DEBUG value for docker setup
* refactor: removed the extra DEFAULT value
---
.env.example | 3 +++
apiserver/plane/settings/local.py | 4 +++-
apiserver/plane/settings/production.py | 5 ++++-
apiserver/plane/settings/staging.py | 4 +++-
docker-compose-hub.yml | 2 ++
docker-compose.yml | 2 ++
6 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/.env.example b/.env.example
index f8c15a1cb..166fbb9b7 100644
--- a/.env.example
+++ b/.env.example
@@ -22,6 +22,9 @@ NEXT_PUBLIC_SLACK_CLIENT_ID=""
# Backend
+# Debug value for api server use it as 0 for production use
+DEBUG=0
+
# Database Settings
PGUSER="plane"
PGPASSWORD="plane"
diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py
index c7d8b1bdf..1b862c013 100644
--- a/apiserver/plane/settings/local.py
+++ b/apiserver/plane/settings/local.py
@@ -10,7 +10,9 @@ from sentry_sdk.integrations.redis import RedisIntegration
from .common import * # noqa
-DEBUG = True
+DEBUG = int(os.environ.get(
+ "DEBUG", 1
+)) == 1
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py
index d5fcd3d04..29b75fc8b 100644
--- a/apiserver/plane/settings/production.py
+++ b/apiserver/plane/settings/production.py
@@ -13,7 +13,10 @@ from sentry_sdk.integrations.redis import RedisIntegration
from .common import * # noqa
# Database
-DEBUG = False
+DEBUG = int(os.environ.get(
+ "DEBUG", 0
+)) == 1
+
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
diff --git a/apiserver/plane/settings/staging.py b/apiserver/plane/settings/staging.py
index 2dab632f1..11ff7a372 100644
--- a/apiserver/plane/settings/staging.py
+++ b/apiserver/plane/settings/staging.py
@@ -12,7 +12,9 @@ from sentry_sdk.integrations.redis import RedisIntegration
from .common import * # noqa
# Database
-DEBUG = True
+DEBUG = int(os.environ.get(
+ "DEBUG", 1
+)) == 1
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml
index bc476a6b1..8087bd427 100644
--- a/docker-compose-hub.yml
+++ b/docker-compose-hub.yml
@@ -52,6 +52,7 @@ services:
DEFAULT_EMAIL: ${DEFAULT_EMAIL}
DEFAULT_PASSWORD: ${DEFAULT_PASSWORD}
USE_MINIO: ${USE_MINIO}
+ DEBUG: ${DEBUG}
depends_on:
- plane-db
- plane-redis
@@ -88,6 +89,7 @@ services:
DEFAULT_EMAIL: ${DEFAULT_EMAIL}
DEFAULT_PASSWORD: ${DEFAULT_PASSWORD}
USE_MINIO: ${USE_MINIO}
+ DEBUG: ${DEBUG}
depends_on:
- plane-api
- plane-db
diff --git a/docker-compose.yml b/docker-compose.yml
index df8c239e2..bacfe5cb2 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -37,6 +37,7 @@ services:
env_file:
- .env
environment:
+ DEBUG: ${DEBUG}
DJANGO_SETTINGS_MODULE: plane.settings.production
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
REDIS_URL: redis://plane-redis:6379/
@@ -78,6 +79,7 @@ services:
env_file:
- .env
environment:
+ DEBUG: ${DEBUG}
DJANGO_SETTINGS_MODULE: plane.settings.production
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
REDIS_URL: redis://plane-redis:6379/
From c9ebc20a8ee2f2c65346ee670266aa7a03e36746 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Tue, 20 Jun 2023 10:24:38 +0530
Subject: [PATCH 17/59] fix: importer delete when imported_data is None (#1328)
---
apiserver/plane/api/views/importer.py | 20 +++++++++++---------
1 file changed, 11 insertions(+), 9 deletions(-)
diff --git a/apiserver/plane/api/views/importer.py b/apiserver/plane/api/views/importer.py
index 53bcae0e8..e045a2ec1 100644
--- a/apiserver/plane/api/views/importer.py
+++ b/apiserver/plane/api/views/importer.py
@@ -239,17 +239,19 @@ class ImportServiceEndpoint(BaseAPIView):
importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug
)
- # Delete all imported Issues
- imported_issues = importer.imported_data.get("issues", [])
- Issue.issue_objects.filter(id__in=imported_issues).delete()
- # Delete all imported Labels
- imported_labels = importer.imported_data.get("labels", [])
- Label.objects.filter(id__in=imported_labels).delete()
+ if importer.imported_data is not None:
+ # Delete all imported Issues
+ imported_issues = importer.imported_data.get("issues", [])
+ Issue.issue_objects.filter(id__in=imported_issues).delete()
- if importer.service == "jira":
- imported_modules = importer.imported_data.get("modules", [])
- Module.objects.filter(id__in=imported_modules).delete()
+ # Delete all imported Labels
+ imported_labels = importer.imported_data.get("labels", [])
+ Label.objects.filter(id__in=imported_labels).delete()
+
+ if importer.service == "jira":
+ imported_modules = importer.imported_data.get("modules", [])
+ Module.objects.filter(id__in=imported_modules).delete()
importer.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
From 464c0f2308ed171ce252b0e453ecad17c674f106 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Tue, 20 Jun 2023 10:25:47 +0530
Subject: [PATCH 18/59] chore: add assignee avatar and minor refactor on cycles
list and retrieve endpoint (#1320)
---
apiserver/plane/api/views/cycle.py | 41 +++++++++++++++---------------
1 file changed, 20 insertions(+), 21 deletions(-)
diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py
index f959f62b5..0dd5d67d0 100644
--- a/apiserver/plane/api/views/cycle.py
+++ b/apiserver/plane/api/views/cycle.py
@@ -1,6 +1,5 @@
# Python imports
import json
-from datetime import datetime, timedelta
# Django imports
from django.db import IntegrityError
@@ -15,7 +14,6 @@ from django.db.models import (
Prefetch,
Sum,
)
-from django.db.models.functions import TruncDate
from django.core import serializers
from django.utils import timezone
from django.utils.decorators import method_decorator
@@ -163,14 +161,9 @@ class CycleViewSet(BaseViewSet):
)
def list(self, request, slug, project_id):
- # try:
+ try:
queryset = self.get_queryset()
- cycle_view = request.GET.get("cycle_view", False)
- if not cycle_view:
- return Response(
- {"error": "Cycle View parameter is required"},
- status=status.HTTP_400_BAD_REQUEST,
- )
+ cycle_view = request.GET.get("cycle_view", "all")
# All Cycles
if cycle_view == "all":
@@ -197,7 +190,8 @@ class CycleViewSet(BaseViewSet):
.annotate(first_name=F("assignees__first_name"))
.annotate(last_name=F("assignees__last_name"))
.annotate(assignee_id=F("assignees__id"))
- .values("first_name", "last_name", "assignee_id")
+ .annotate(avatar=F("assignees__avatar"))
+ .values("first_name", "last_name", "assignee_id", "avatar")
.annotate(total_issues=Count("assignee_id"))
.annotate(
completed_issues=Count(
@@ -252,9 +246,7 @@ class CycleViewSet(BaseViewSet):
cycle_id=data[0]["id"],
)
- return Response(
- data, status=status.HTTP_200_OK
- )
+ return Response(data, status=status.HTTP_200_OK)
# Upcoming Cycles
if cycle_view == "upcoming":
@@ -293,12 +285,13 @@ class CycleViewSet(BaseViewSet):
return Response(
{"error": "No matching view found"}, 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,
- # )
+
+ except Exception as e:
+ capture_exception(e)
+ return Response(
+ {"error": "Something went wrong please try again later"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
def create(self, request, slug, project_id):
try:
@@ -365,6 +358,7 @@ class CycleViewSet(BaseViewSet):
try:
queryset = self.get_queryset().get(pk=pk)
+ # Assignee Distribution
assignee_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=pk,
@@ -374,7 +368,8 @@ class CycleViewSet(BaseViewSet):
.annotate(first_name=F("assignees__first_name"))
.annotate(last_name=F("assignees__last_name"))
.annotate(assignee_id=F("assignees__id"))
- .values("first_name", "last_name", "assignee_id")
+ .annotate(avatar=F("assignees__avatar"))
+ .values("first_name", "last_name", "assignee_id", "avatar")
.annotate(total_issues=Count("assignee_id"))
.annotate(
completed_issues=Count(
@@ -391,6 +386,7 @@ class CycleViewSet(BaseViewSet):
.order_by("first_name", "last_name")
)
+ # Label Distribution
label_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=pk,
@@ -433,7 +429,10 @@ class CycleViewSet(BaseViewSet):
data,
status=status.HTTP_200_OK,
)
-
+ except Cycle.DoesNotExist:
+ return Response(
+ {"error": "Cycle Does not exists"}, status=status.HTTP_400_BAD_REQUEST
+ )
except Exception as e:
capture_exception(e)
return Response(
From f797bb20f993e5115ebfc105650895e1c8e87fd2 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Tue, 20 Jun 2023 10:26:31 +0530
Subject: [PATCH 19/59] remove: time line issues (#1316)
---
apiserver/plane/api/serializers/__init__.py | 1 -
apiserver/plane/api/serializers/issue.py | 16 ----------
apiserver/plane/api/urls.py | 25 ---------------
apiserver/plane/api/views/__init__.py | 1 -
apiserver/plane/api/views/issue.py | 35 ---------------------
apiserver/plane/db/models/__init__.py | 1 -
apiserver/plane/db/models/issue.py | 18 -----------
7 files changed, 97 deletions(-)
diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py
index cb94f8068..9fe63d723 100644
--- a/apiserver/plane/api/serializers/__init__.py
+++ b/apiserver/plane/api/serializers/__init__.py
@@ -31,7 +31,6 @@ from .issue import (
IssueCreateSerializer,
IssueActivitySerializer,
IssueCommentSerializer,
- TimeLineIssueSerializer,
IssuePropertySerializer,
BlockerIssueSerializer,
BlockedIssueSerializer,
diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py
index 01c2c977d..14782dbe5 100644
--- a/apiserver/plane/api/serializers/issue.py
+++ b/apiserver/plane/api/serializers/issue.py
@@ -16,7 +16,6 @@ from plane.db.models import (
Issue,
IssueActivity,
IssueComment,
- TimelineIssue,
IssueProperty,
IssueBlocker,
IssueAssignee,
@@ -288,21 +287,6 @@ class IssueCommentSerializer(BaseSerializer):
]
-class TimeLineIssueSerializer(BaseSerializer):
- class Meta:
- model = TimelineIssue
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project",
- "issue",
- "created_by",
- "updated_by",
- "created_at",
- "updated_at",
- ]
-
-
class IssuePropertySerializer(BaseSerializer):
class Meta:
model = IssueProperty
diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py
index 7b2c6f76e..e207a29d6 100644
--- a/apiserver/plane/api/urls.py
+++ b/apiserver/plane/api/urls.py
@@ -70,7 +70,6 @@ from plane.api.views import (
BulkDeleteIssuesEndpoint,
BulkImportIssuesEndpoint,
ProjectUserViewsEndpoint,
- TimeLineIssueViewSet,
IssuePropertyViewSet,
LabelViewSet,
SubIssuesEndpoint,
@@ -826,30 +825,6 @@ urlpatterns = [
name="project-issue-comment",
),
## End IssueComments
- ## Roadmap
- path(
- "workspaces//projects//issues//roadmaps/",
- TimeLineIssueViewSet.as_view(
- {
- "get": "list",
- "post": "create",
- }
- ),
- name="project-issue-roadmap",
- ),
- path(
- "workspaces//projects//issues//roadmaps//",
- TimeLineIssueViewSet.as_view(
- {
- "get": "retrieve",
- "put": "update",
- "patch": "partial_update",
- "delete": "destroy",
- }
- ),
- name="project-issue-roadmap",
- ),
- ## End Roadmap
## IssueProperty
path(
"workspaces//projects//issue-properties/",
diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py
index 12299f79b..c525f25c8 100644
--- a/apiserver/plane/api/views/__init__.py
+++ b/apiserver/plane/api/views/__init__.py
@@ -58,7 +58,6 @@ from .issue import (
WorkSpaceIssuesEndpoint,
IssueActivityEndpoint,
IssueCommentViewSet,
- TimeLineIssueViewSet,
IssuePropertyViewSet,
LabelViewSet,
BulkDeleteIssuesEndpoint,
diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py
index 794fecf5c..f5471dc6c 100644
--- a/apiserver/plane/api/views/issue.py
+++ b/apiserver/plane/api/views/issue.py
@@ -34,7 +34,6 @@ from plane.api.serializers import (
IssueCreateSerializer,
IssueActivitySerializer,
IssueCommentSerializer,
- TimeLineIssueSerializer,
IssuePropertySerializer,
LabelSerializer,
IssueSerializer,
@@ -54,7 +53,6 @@ from plane.db.models import (
Issue,
IssueActivity,
IssueComment,
- TimelineIssue,
IssueProperty,
Label,
IssueLink,
@@ -443,39 +441,6 @@ class IssueCommentViewSet(BaseViewSet):
)
-class TimeLineIssueViewSet(BaseViewSet):
- serializer_class = TimeLineIssueSerializer
- model = TimelineIssue
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- filterset_fields = [
- "issue__id",
- "workspace__id",
- ]
-
- def perform_create(self, serializer):
- serializer.save(
- project_id=self.kwargs.get("project_id"),
- issue_id=self.kwargs.get("issue_id"),
- )
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(issue_id=self.kwargs.get("issue_id"))
- .filter(project__project_projectmember__member=self.request.user)
- .select_related("project")
- .select_related("workspace")
- .select_related("issue")
- .distinct()
- )
-
-
class IssuePropertyViewSet(BaseViewSet):
serializer_class = IssuePropertySerializer
model = IssueProperty
diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py
index 0d236fe21..7f1d1c03f 100644
--- a/apiserver/plane/db/models/__init__.py
+++ b/apiserver/plane/db/models/__init__.py
@@ -23,7 +23,6 @@ from .project import (
from .issue import (
Issue,
IssueActivity,
- TimelineIssue,
IssueProperty,
IssueComment,
IssueBlocker,
diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py
index dcb7d20c4..7efe86d46 100644
--- a/apiserver/plane/db/models/issue.py
+++ b/apiserver/plane/db/models/issue.py
@@ -293,24 +293,6 @@ class IssueActivity(ProjectBaseModel):
return str(self.issue)
-class TimelineIssue(ProjectBaseModel):
- issue = models.ForeignKey(
- Issue, on_delete=models.CASCADE, related_name="issue_timeline"
- )
- sequence_id = models.FloatField(default=1.0)
- links = models.JSONField(default=dict, blank=True)
-
- class Meta:
- verbose_name = "Timeline Issue"
- verbose_name_plural = "Timeline Issues"
- db_table = "issue_timelines"
- ordering = ("-created_at",)
-
- def __str__(self):
- """Return project of the project member"""
- return str(self.issue)
-
-
class IssueComment(ProjectBaseModel):
comment_stripped = models.TextField(verbose_name="Comment", blank=True)
comment_json = models.JSONField(blank=True, default=dict)
From d1d872252565ba93f93beb3aa3dfa6142f46293f Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Tue, 20 Jun 2023 10:26:44 +0530
Subject: [PATCH 20/59] remove: shortcut module (#1315)
---
apiserver/plane/api/serializers/__init__.py | 1 -
apiserver/plane/api/serializers/shortcut.py | 14 ----------
apiserver/plane/api/urls.py | 27 -------------------
apiserver/plane/api/views/__init__.py | 1 -
apiserver/plane/api/views/shortcut.py | 29 ---------------------
apiserver/plane/db/models/__init__.py | 2 --
apiserver/plane/db/models/shortcut.py | 26 ------------------
7 files changed, 100 deletions(-)
delete mode 100644 apiserver/plane/api/serializers/shortcut.py
delete mode 100644 apiserver/plane/api/views/shortcut.py
delete mode 100644 apiserver/plane/db/models/shortcut.py
diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py
index 9fe63d723..2b72c5ae1 100644
--- a/apiserver/plane/api/serializers/__init__.py
+++ b/apiserver/plane/api/serializers/__init__.py
@@ -23,7 +23,6 @@ from .project import (
ProjectLiteSerializer,
)
from .state import StateSerializer, StateLiteSerializer
-from .shortcut import ShortCutSerializer
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer
from .asset import FileAssetSerializer
diff --git a/apiserver/plane/api/serializers/shortcut.py b/apiserver/plane/api/serializers/shortcut.py
deleted file mode 100644
index 18c2bd049..000000000
--- a/apiserver/plane/api/serializers/shortcut.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Module imports
-from .base import BaseSerializer
-
-from plane.db.models import Shortcut
-
-
-class ShortCutSerializer(BaseSerializer):
- class Meta:
- model = Shortcut
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "project",
- ]
diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py
index e207a29d6..936fd73ab 100644
--- a/apiserver/plane/api/urls.py
+++ b/apiserver/plane/api/urls.py
@@ -84,9 +84,6 @@ from plane.api.views import (
ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint,
## End Estimates
- # Shortcuts
- ShortCutViewSet,
- ## End Shortcuts
# Views
IssueViewViewSet,
ViewIssuesEndpoint,
@@ -539,30 +536,6 @@ urlpatterns = [
name="bulk-create-estimate-points",
),
# End Estimates ##
- # Shortcuts
- path(
- "workspaces//projects//shortcuts/",
- ShortCutViewSet.as_view(
- {
- "get": "list",
- "post": "create",
- }
- ),
- name="project-shortcut",
- ),
- path(
- "workspaces//projects//shortcuts//",
- ShortCutViewSet.as_view(
- {
- "get": "retrieve",
- "put": "update",
- "patch": "partial_update",
- "delete": "destroy",
- }
- ),
- name="project-shortcut",
- ),
- ## End Shortcuts
# Views
path(
"workspaces//projects//views/",
diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py
index c525f25c8..f8d170532 100644
--- a/apiserver/plane/api/views/__init__.py
+++ b/apiserver/plane/api/views/__init__.py
@@ -43,7 +43,6 @@ from .workspace import (
WorkspaceThemeViewSet,
)
from .state import StateViewSet
-from .shortcut import ShortCutViewSet
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
from .cycle import (
CycleViewSet,
diff --git a/apiserver/plane/api/views/shortcut.py b/apiserver/plane/api/views/shortcut.py
deleted file mode 100644
index 49453fb14..000000000
--- a/apiserver/plane/api/views/shortcut.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# Module imports
-from . import BaseViewSet
-from plane.api.serializers import ShortCutSerializer
-from plane.api.permissions import ProjectEntityPermission
-from plane.db.models import Shortcut
-
-
-class ShortCutViewSet(BaseViewSet):
-
- serializer_class = ShortCutSerializer
- model = Shortcut
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- def perform_create(self, serializer):
- serializer.save(project_id=self.kwargs.get("project_id"))
-
- def get_queryset(self):
- return self.filter_queryset(
- super()
- .get_queryset()
- .filter(workspace__slug=self.kwargs.get("slug"))
- .filter(project_id=self.kwargs.get("project_id"))
- .filter(project__project_projectmember__member=self.request.user)
- .select_related("project")
- .select_related("workspace")
- .distinct()
- )
diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py
index 7f1d1c03f..96c649a83 100644
--- a/apiserver/plane/db/models/__init__.py
+++ b/apiserver/plane/db/models/__init__.py
@@ -43,8 +43,6 @@ from .state import State
from .cycle import Cycle, CycleIssue, CycleFavorite
-from .shortcut import Shortcut
-
from .view import IssueView, IssueViewFavorite
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
diff --git a/apiserver/plane/db/models/shortcut.py b/apiserver/plane/db/models/shortcut.py
deleted file mode 100644
index bdc09c1f2..000000000
--- a/apiserver/plane/db/models/shortcut.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Django imports
-from django.db import models
-
-
-# Module imports
-from . import ProjectBaseModel
-
-
-class Shortcut(ProjectBaseModel):
- TYPE_CHOICES = (("repo", "Repo"), ("direct", "Direct"))
- name = models.CharField(max_length=255, verbose_name="Cycle Name")
- description = models.TextField(verbose_name="Cycle Description", blank=True)
- type = models.CharField(
- max_length=255, verbose_name="Shortcut Type", choices=TYPE_CHOICES
- )
- url = models.URLField(verbose_name="URL", blank=True, null=True)
-
- class Meta:
- verbose_name = "Shortcut"
- verbose_name_plural = "Shortcuts"
- db_table = "shortcuts"
- ordering = ("-created_at",)
-
- def __str__(self):
- """Return name of the shortcut"""
- return f"{self.name} <{self.project.name}>"
From 8496422d203c8472d1ea54af6af9c977c9ac57fd Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Tue, 20 Jun 2023 10:27:01 +0530
Subject: [PATCH 21/59] fix: inbox issue activity (#1310)
---
apiserver/plane/api/views/inbox.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py
index f76638c17..738c6ef9f 100644
--- a/apiserver/plane/api/views/inbox.py
+++ b/apiserver/plane/api/views/inbox.py
@@ -259,7 +259,6 @@ class InboxIssueViewSet(BaseViewSet):
if issue_serializer.is_valid():
current_instance = issue
- issue_serializer.save()
# Log all the updates
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
if issue is not None:
@@ -274,6 +273,7 @@ class InboxIssueViewSet(BaseViewSet):
cls=DjangoJSONEncoder,
),
)
+ issue_serializer.save()
else:
return Response(
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
From ac6fae44e808e14d6206e6a691a079d92722eaac Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Tue, 20 Jun 2023 10:27:21 +0530
Subject: [PATCH 22/59] chore: toggle sub issue view and sub issue count in sub
issues (#1312)
---
apiserver/plane/api/views/issue.py | 10 +++++++++-
apiserver/plane/utils/issue_filters.py | 15 ++++++++++++++-
2 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py
index f5471dc6c..cd9f65e48 100644
--- a/apiserver/plane/api/views/issue.py
+++ b/apiserver/plane/api/views/issue.py
@@ -259,7 +259,9 @@ class UserWorkSpaceIssues(BaseAPIView):
def get(self, request, slug):
try:
issues = (
- Issue.issue_objects.filter(assignees__in=[request.user], workspace__slug=slug)
+ Issue.issue_objects.filter(
+ assignees__in=[request.user], workspace__slug=slug
+ )
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
@@ -582,6 +584,12 @@ class SubIssuesEndpoint(BaseAPIView):
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
+ .annotate(
+ sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
+ .order_by()
+ .annotate(count=Func(F("id"), function="Count"))
+ .values("count")
+ )
)
state_distribution = (
diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py
index c5f147ea1..f348f642a 100644
--- a/apiserver/plane/utils/issue_filters.py
+++ b/apiserver/plane/utils/issue_filters.py
@@ -242,6 +242,18 @@ def filter_inbox_status(params, filter, method):
return filter
+def filter_sub_issue_toggle(params, filter, method):
+ if method == "GET":
+ sub_issue = params.get("sub_issue", "false")
+ if sub_issue == "false":
+ filter["parent__isnull"] = True
+ else:
+ sub_issue = params.get("sub_issue", "false")
+ if sub_issue == "false":
+ filter["parent__isnull"] = True
+ return filter
+
+
def issue_filters(query_params, method):
filter = dict()
@@ -263,7 +275,8 @@ def issue_filters(query_params, method):
"project": filter_project,
"cycle": filter_cycle,
"module": filter_module,
- "inbox_status": filter_inbox_status
+ "inbox_status": filter_inbox_status,
+ "sub_issue": filter_sub_issue_toggle,
}
for key, value in ISSUE_FILTER.items():
From 7a991720a899e07bf6fe185b43edc9afdf039760 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Tue, 20 Jun 2023 10:27:34 +0530
Subject: [PATCH 23/59] chore: add assignee, label and burndown plot in module
details (#1313)
* chore: add assignee, label and burndown plot in module details
* dev: fix typo and key error
* dev: add avatar in module retrieve
---
apiserver/plane/api/views/module.py | 83 ++++++++++++++++++++++++-
apiserver/plane/utils/analytics_plot.py | 67 +++++++++++++-------
2 files changed, 128 insertions(+), 22 deletions(-)
diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py
index 7c285c242..5a235ba8f 100644
--- a/apiserver/plane/api/views/module.py
+++ b/apiserver/plane/api/views/module.py
@@ -37,7 +37,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.utils.analytics_plot import burndown_plot
class ModuleViewSet(BaseViewSet):
model = Module
@@ -160,6 +160,87 @@ class ModuleViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
+ def retrieve(self, request, slug, project_id, pk):
+ try:
+ queryset = self.get_queryset().get(pk=pk)
+
+ assignee_distribution = (
+ Issue.objects.filter(
+ issue_module__module_id=pk,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ .annotate(first_name=F("assignees__first_name"))
+ .annotate(last_name=F("assignees__last_name"))
+ .annotate(assignee_id=F("assignees__id"))
+ .annotate(avatar=F("assignees__avatar"))
+ .values("first_name", "last_name", "assignee_id", "avatar")
+ .annotate(total_issues=Count("assignee_id"))
+ .annotate(
+ completed_issues=Count(
+ "assignee_id",
+ filter=Q(completed_at__isnull=False),
+ )
+ )
+ .annotate(
+ pending_issues=Count(
+ "assignee_id",
+ filter=Q(completed_at__isnull=True),
+ )
+ )
+ .order_by("first_name", "last_name")
+ )
+
+ label_distribution = (
+ Issue.objects.filter(
+ issue_module__module_id=pk,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+ .annotate(label_name=F("labels__name"))
+ .annotate(color=F("labels__color"))
+ .annotate(label_id=F("labels__id"))
+ .values("label_name", "color", "label_id")
+ .annotate(total_issues=Count("label_id"))
+ .annotate(
+ completed_issues=Count(
+ "label_id",
+ filter=Q(completed_at__isnull=False),
+ )
+ )
+ .annotate(
+ pending_issues=Count(
+ "label_id",
+ filter=Q(completed_at__isnull=True),
+ )
+ )
+ .order_by("label_name")
+ )
+
+ data = ModuleSerializer(queryset).data
+ data["distribution"] = {
+ "assignees": assignee_distribution,
+ "labels": label_distribution,
+ "completion_chart": {},
+ }
+
+ if queryset.start_date and queryset.target_date:
+ data["distribution"]["completion_chart"] = burndown_plot(
+ queryset=queryset, slug=slug, project_id=project_id, module_id=pk
+ )
+
+ return Response(
+ data,
+ status=status.HTTP_200_OK,
+ )
+
+ except Exception as e:
+ capture_exception(e)
+ return Response(
+ {"error": "Something went wrong please try again later"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
class ModuleIssueViewSet(BaseViewSet):
serializer_class = ModuleIssueSerializer
diff --git a/apiserver/plane/utils/analytics_plot.py b/apiserver/plane/utils/analytics_plot.py
index 045e2bf26..033452e0d 100644
--- a/apiserver/plane/utils/analytics_plot.py
+++ b/apiserver/plane/utils/analytics_plot.py
@@ -81,30 +81,55 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
return sorted_data
-def burndown_plot(queryset, slug, project_id, cycle_id):
- # Get all dates between the two dates
- date_range = [
- queryset.start_date + timedelta(days=x)
- for x in range((queryset.end_date - queryset.start_date).days + 1)
- ]
-
- chart_data = {str(date): 0 for date in date_range}
-
- # Total Issues in Cycle
+def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
+ # Total Issues in Cycle or Module
total_issues = queryset.total_issues
- completed_issues_distribution = (
- Issue.objects.filter(
- workspace__slug=slug,
- project_id=project_id,
- issue_cycle__cycle_id=cycle_id,
+
+ if cycle_id:
+ # Get all dates between the two dates
+ date_range = [
+ queryset.start_date + timedelta(days=x)
+ for x in range((queryset.end_date - queryset.start_date).days + 1)
+ ]
+
+ chart_data = {str(date): 0 for date in date_range}
+
+ completed_issues_distribution = (
+ Issue.objects.filter(
+ workspace__slug=slug,
+ project_id=project_id,
+ issue_cycle__cycle_id=cycle_id,
+ )
+ .annotate(date=TruncDate("completed_at"))
+ .values("date")
+ .annotate(total_completed=Count("id"))
+ .values("date", "total_completed")
+ .order_by("date")
)
- .annotate(date=TruncDate("completed_at"))
- .values("date")
- .annotate(total_completed=Count("id"))
- .values("date", "total_completed")
- .order_by("date")
- )
+
+ if module_id:
+ # Get all dates between the two dates
+ date_range = [
+ queryset.start_date + timedelta(days=x)
+ for x in range((queryset.target_date - queryset.start_date).days + 1)
+ ]
+
+ chart_data = {str(date): 0 for date in date_range}
+
+ completed_issues_distribution = (
+ Issue.objects.filter(
+ workspace__slug=slug,
+ project_id=project_id,
+ issue_module__module_id=module_id,
+ )
+ .annotate(date=TruncDate("completed_at"))
+ .values("date")
+ .annotate(total_completed=Count("id"))
+ .values("date", "total_completed")
+ .order_by("date")
+ )
+
for date in date_range:
cumulative_pending_issues = total_issues
From d7097330ef6aee7883c1f694e0fc47e86f9bf433 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Tue, 20 Jun 2023 10:27:45 +0530
Subject: [PATCH 24/59] chore: update issue search for cycle and modules
(#1314)
* chore: update issue search for cycle and modules
* dev: return state name, group and color in search
* dev: sub issue search
---
apiserver/plane/api/views/search.py | 25 ++++++++++++++++++++++---
1 file changed, 22 insertions(+), 3 deletions(-)
diff --git a/apiserver/plane/api/views/search.py b/apiserver/plane/api/views/search.py
index 078a9a6a5..51925dd7b 100644
--- a/apiserver/plane/api/views/search.py
+++ b/apiserver/plane/api/views/search.py
@@ -206,8 +206,12 @@ class IssueSearchEndpoint(BaseAPIView):
def get(self, request, slug, project_id):
try:
query = request.query_params.get("search", False)
- parent = request.query_params.get("parent", False)
- blocker_blocked_by = request.query_params.get("blocker_blocked_by", False)
+ parent = request.query_params.get("parent", "false")
+ blocker_blocked_by = request.query_params.get("blocker_blocked_by", "false")
+ cycle = request.query_params.get("cycle", "false")
+ module = request.query_params.get("module", "false")
+ sub_issue = request.query_params.get("sub_issue", "false")
+
issue_id = request.query_params.get("issue_id", False)
issues = Issue.issue_objects.filter(
@@ -235,6 +239,18 @@ class IssueSearchEndpoint(BaseAPIView):
~Q(blocked_issues__block=issue),
~Q(blocker_issues__blocked_by=issue),
)
+ if sub_issue == "true" and issue_id:
+ issue = Issue.issue_objects.get(pk=issue_id)
+ issues = issues.filter(~Q(pk=issue_id), parent__isnull=True)
+ if issue.parent:
+ issues = issues.filter(~Q(pk=issue.parent_id))
+
+ if cycle == "true":
+ issues = issues.exclude(issue_cycle__isnull=False)
+
+ if module == "true":
+ issues = issues.exclude(issue_module__isnull=False)
+
return Response(
issues.values(
@@ -244,6 +260,9 @@ class IssueSearchEndpoint(BaseAPIView):
"project__identifier",
"project_id",
"workspace__slug",
+ "state__name",
+ "state__group",
+ "state__color",
),
status=status.HTTP_200_OK,
)
@@ -252,7 +271,7 @@ class IssueSearchEndpoint(BaseAPIView):
{"error": "Issue Does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
- capture_exception(e)
+ print(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
From cf8c902473cf3a5f1fdffd7d0a4a605072186a5d Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Tue, 20 Jun 2023 16:32:02 +0530
Subject: [PATCH 25/59] chore: update cycle and module stats logic (#1323)
* refactor: cycles stats
* chore: show assignee avatar in stats
* chore: cycles and modules sidebar stats refactor
* fix: build errors
---
.../command-palette/change-issue-assignee.tsx | 2 +-
.../command-palette/change-issue-priority.tsx | 2 +-
.../command-palette/change-issue-state.tsx | 2 +-
.../core/board-view/single-issue.tsx | 3 -
.../core/sidebar/progress-chart.tsx | 52 ++---
.../core/sidebar/sidebar-progress-stats.tsx | 216 ++++++++----------
.../core/sidebar/single-progress-stats.tsx | 7 +-
.../cycles/active-cycle-details.tsx | 130 +++++------
.../components/cycles/active-cycle-stats.tsx | 150 +++++-------
apps/app/components/cycles/sidebar.tsx | 50 ++--
apps/app/components/modules/sidebar.tsx | 20 +-
apps/app/components/ui/avatar.tsx | 2 +-
.../projects/[projectId]/cycles/index.tsx | 20 +-
.../[projectId]/modules/[moduleId].tsx | 9 +-
apps/app/types/cycles.d.ts | 28 +++
apps/app/types/modules.d.ts | 5 +
16 files changed, 297 insertions(+), 401 deletions(-)
diff --git a/apps/app/components/command-palette/change-issue-assignee.tsx b/apps/app/components/command-palette/change-issue-assignee.tsx
index 56351335e..1021623db 100644
--- a/apps/app/components/command-palette/change-issue-assignee.tsx
+++ b/apps/app/components/command-palette/change-issue-assignee.tsx
@@ -80,7 +80,7 @@ export const ChangeIssueAssignee: React.FC = ({ setIsPaletteOpen, issue,
console.error(e);
});
},
- [workspaceSlug, issueId, projectId]
+ [workspaceSlug, issueId, projectId, user]
);
const handleIssueAssignees = (assignee: string) => {
diff --git a/apps/app/components/command-palette/change-issue-priority.tsx b/apps/app/components/command-palette/change-issue-priority.tsx
index 2db03268d..07ba210a6 100644
--- a/apps/app/components/command-palette/change-issue-priority.tsx
+++ b/apps/app/components/command-palette/change-issue-priority.tsx
@@ -51,7 +51,7 @@ export const ChangeIssuePriority: React.FC = ({ setIsPaletteOpen, issue,
console.error(e);
});
},
- [workspaceSlug, issueId, projectId]
+ [workspaceSlug, issueId, projectId, user]
);
const handleIssueState = (priority: string | null) => {
diff --git a/apps/app/components/command-palette/change-issue-state.tsx b/apps/app/components/command-palette/change-issue-state.tsx
index 0378df878..00c9745be 100644
--- a/apps/app/components/command-palette/change-issue-state.tsx
+++ b/apps/app/components/command-palette/change-issue-state.tsx
@@ -63,7 +63,7 @@ export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue, use
console.error(e);
});
},
- [workspaceSlug, issueId, projectId, mutateIssueDetails]
+ [workspaceSlug, issueId, projectId, mutateIssueDetails, user]
);
const handleIssueState = (stateId: string) => {
diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx
index 072a5113a..003be9e94 100644
--- a/apps/app/components/core/board-view/single-issue.tsx
+++ b/apps/app/components/core/board-view/single-issue.tsx
@@ -181,9 +181,6 @@ export const SingleBoardIssue: React.FC = ({
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
mutate(MODULE_DETAILS(moduleId as string));
} else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
- })
- .catch((error) => {
- console.log(error);
});
},
[
diff --git a/apps/app/components/core/sidebar/progress-chart.tsx b/apps/app/components/core/sidebar/progress-chart.tsx
index 93e11f762..e6349bfe5 100644
--- a/apps/app/components/core/sidebar/progress-chart.tsx
+++ b/apps/app/components/core/sidebar/progress-chart.tsx
@@ -3,16 +3,15 @@ import React from "react";
// ui
import { LineGraph } from "components/ui";
// helpers
-import { getDatesInRange, renderShortNumericDateFormat } from "helpers/date-time.helper";
+import { renderShortNumericDateFormat } from "helpers/date-time.helper";
//types
-import { IIssue } from "types";
+import { TCompletionChartDistribution } from "types";
type Props = {
- issues: IIssue[];
- start: string;
- end: string;
- width?: number;
- height?: number;
+ distribution: TCompletionChartDistribution;
+ startDate: string | Date;
+ endDate: string | Date;
+ totalIssues: number;
};
const styleById = {
@@ -41,32 +40,11 @@ const DashedLine = ({ series, lineGenerator, xScale, yScale }: any) =>
/>
));
-const ProgressChart: React.FC = ({ issues, start, end }) => {
- const startDate = new Date(start);
- const endDate = new Date(end);
-
- const getChartData = () => {
- const dateRangeArray = getDatesInRange(startDate, endDate);
- let count = 0;
-
- const dateWiseData = dateRangeArray.map((d) => {
- const current = d.toISOString().split("T")[0];
- const total = issues.length;
- const currentData = issues.filter(
- (i) => i.completed_at && i.completed_at.toString().split("T")[0] === current
- );
- count = currentData ? currentData.length + count : count;
-
- return {
- currentDate: renderShortNumericDateFormat(current),
- currentDateData: currentData,
- pending: new Date(current) < new Date() ? total - count : null,
- };
- });
- return dateWiseData;
- };
-
- const chartData = getChartData();
+const ProgressChart: React.FC = ({ distribution, startDate, endDate, totalIssues }) => {
+ const chartData = Object.keys(distribution).map((key) => ({
+ currentDate: renderShortNumericDateFormat(key),
+ pending: distribution[key],
+ }));
return (
@@ -74,7 +52,7 @@ const ProgressChart: React.FC
= ({ issues, start, end }) => {
animate
curve="monotoneX"
height="160px"
- width="360px"
+ width="100%"
enableGridY={false}
lineWidth={1}
margin={{ top: 30, right: 30, bottom: 30, left: 30 }}
@@ -97,7 +75,7 @@ const ProgressChart: React.FC = ({ issues, start, end }) => {
data: [
{
x: chartData[0].currentDate,
- y: issues.length,
+ y: totalIssues,
},
{
x: chartData[chartData.length - 1].currentDate,
@@ -113,10 +91,10 @@ const ProgressChart: React.FC = ({ issues, start, end }) => {
enablePoints={false}
enableArea
colors={(datum) => datum.color ?? "#3F76FF"}
- customYAxisTickValues={[0, issues.length]}
+ customYAxisTickValues={[0, totalIssues]}
gridXValues={chartData.map((item, index) => (index % 2 === 0 ? item.currentDate : ""))}
theme={{
- background: "rgb(var(--color-bg-sidebar))",
+ background: "transparent",
axis: {
domain: {
line: {
diff --git a/apps/app/components/core/sidebar/sidebar-progress-stats.tsx b/apps/app/components/core/sidebar/sidebar-progress-stats.tsx
index 140a49aab..b90f4e3ee 100644
--- a/apps/app/components/core/sidebar/sidebar-progress-stats.tsx
+++ b/apps/app/components/core/sidebar/sidebar-progress-stats.tsx
@@ -1,15 +1,7 @@
import React from "react";
-import Image from "next/image";
-import { useRouter } from "next/router";
-
-import useSWR from "swr";
-
// headless ui
import { Tab } from "@headlessui/react";
-// services
-import issuesServices from "services/issues.service";
-import projectService from "services/project.service";
// hooks
import useLocalStorage from "hooks/use-local-storage";
import useIssuesView from "hooks/use-issues-view";
@@ -17,61 +9,43 @@ import useIssuesView from "hooks/use-issues-view";
import { SingleProgressStats } from "components/core";
// ui
import { Avatar } from "components/ui";
-// icons
-import User from "public/user.png";
// types
-import { IIssue, IIssueLabels, IModule, UserAuth } from "types";
-// fetch-keys
-import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
+import {
+ IModule,
+ TAssigneesDistribution,
+ TCompletionChartDistribution,
+ TLabelsDistribution,
+} from "types";
+// constants
+import { STATE_GROUP_COLORS } from "constants/state";
// types
type Props = {
- groupedIssues: any;
- issues: IIssue[];
+ distribution: {
+ assignees: TAssigneesDistribution[];
+ completion_chart: TCompletionChartDistribution;
+ labels: TLabelsDistribution[];
+ };
+ groupedIssues: {
+ [key: string]: number;
+ };
+ totalIssues: number;
module?: IModule;
- userAuth?: UserAuth;
roundedTab?: boolean;
noBackground?: boolean;
};
-const stateGroupColours: {
- [key: string]: string;
-} = {
- backlog: "#3f76ff",
- unstarted: "#ff9e9e",
- started: "#d687ff",
- cancelled: "#ff5353",
- completed: "#096e8d",
-};
-
export const SidebarProgressStats: React.FC = ({
+ distribution,
groupedIssues,
- issues,
+ totalIssues,
module,
- userAuth,
roundedTab,
noBackground,
}) => {
- const router = useRouter();
- const { workspaceSlug, projectId } = router.query;
-
const { filters, setFilters } = useIssuesView();
const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees");
- const { data: issueLabels } = useSWR(
- workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
- workspaceSlug && projectId
- ? () => issuesServices.getIssueLabels(workspaceSlug as string, projectId as string)
- : null
- );
-
- const { data: members } = useSWR(
- workspaceSlug && projectId ? PROJECT_MEMBERS(workspaceSlug as string) : null,
- workspaceSlug && projectId
- ? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
- : null
- );
-
const currentValue = (tab: string | null) => {
switch (tab) {
case "Assignees":
@@ -85,6 +59,7 @@ export const SidebarProgressStats: React.FC = ({
return 0;
}
};
+
return (
= ({
-
- {members?.map((member, index) => {
- const totalArray = issues?.filter((i) => i?.assignees?.includes(member.member.id));
- const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
-
- if (totalArray.length > 0) {
- return (
-
-
- {member.member.first_name}
- >
- }
- completed={completeArray.length}
- total={totalArray.length}
- onClick={() => {
- if (filters?.assignees?.includes(member.member.id))
- setFilters({
- assignees: filters?.assignees?.filter((a) => a !== member.member.id),
- });
- else
- setFilters({ assignees: [...(filters?.assignees ?? []), member.member.id] });
- }}
- selected={filters?.assignees?.includes(member.member.id)}
- />
- );
- }
- })}
- {issues?.filter((i) => i?.assignees?.length === 0).length > 0 ? (
-
-
-
-
- No assignee
- >
- }
- completed={
- issues?.filter(
- (i) => i?.state_detail.group === "completed" && i.assignees?.length === 0
- ).length
- }
- total={issues?.filter((i) => i?.assignees?.length === 0).length}
- />
- ) : (
- ""
- )}
-
- {issueLabels?.map((label, index) => {
- const totalArray = issues?.filter((i) => i?.labels?.includes(label.id));
- const completeArray = totalArray?.filter((i) => i?.state_detail.group === "completed");
-
- if (totalArray.length > 0) {
+ {distribution.assignees.map((assignee, index) => {
+ if (assignee.assignee_id)
return (
-
- {label?.name}
+ {assignee.first_name}
}
- completed={completeArray.length}
- total={totalArray.length}
+ completed={assignee.completed_issues}
+ total={assignee.total_issues}
onClick={() => {
- if (filters.labels?.includes(label.id))
+ if (filters?.assignees?.includes(assignee.assignee_id ?? ""))
setFilters({
- labels: filters?.labels?.filter((l) => l !== label.id),
+ assignees: filters?.assignees?.filter((a) => a !== assignee.assignee_id),
+ });
+ else
+ setFilters({
+ assignees: [...(filters?.assignees ?? []), assignee.assignee_id ?? ""],
});
- else setFilters({ labels: [...(filters?.labels ?? []), label.id] });
}}
- selected={filters?.labels?.includes(label.id)}
+ selected={filters?.assignees?.includes(assignee.assignee_id ?? "")}
+ />
+ );
+ else
+ return (
+
+
+
+
+ No assignee
+
+ }
+ completed={assignee.completed_issues}
+ total={assignee.total_issues}
/>
);
- }
})}
-
+
+ {distribution.labels.map((label, index) => (
+
+
+ {label.label_name ?? "No labels"}
+
+ }
+ completed={label.completed_issues}
+ total={label.total_issues}
+ onClick={() => {
+ if (filters.labels?.includes(label.label_id ?? ""))
+ setFilters({
+ labels: filters?.labels?.filter((l) => l !== label.label_id),
+ });
+ else setFilters({ labels: [...(filters?.labels ?? []), label.label_id ?? ""] });
+ }}
+ selected={filters?.labels?.includes(label.label_id ?? "")}
+ />
+ ))}
+
+
{Object.keys(groupedIssues).map((group, index) => (
= ({
{group}
}
completed={groupedIssues[group]}
- total={issues.length}
+ total={totalIssues}
/>
))}
diff --git a/apps/app/components/core/sidebar/single-progress-stats.tsx b/apps/app/components/core/sidebar/single-progress-stats.tsx
index d8236de9b..7c7f65446 100644
--- a/apps/app/components/core/sidebar/single-progress-stats.tsx
+++ b/apps/app/components/core/sidebar/single-progress-stats.tsx
@@ -23,10 +23,10 @@ export const SingleProgressStats: React.FC = ({
} ${selected ? "bg-brand-surface-1" : ""}`}
onClick={onClick}
>
- {title}
+ {title}
-
+
@@ -36,8 +36,7 @@ export const SingleProgressStats: React.FC = ({
%
-
of
-
{total}
+
of {total}
);
diff --git a/apps/app/components/cycles/active-cycle-details.tsx b/apps/app/components/cycles/active-cycle-details.tsx
index 21f37e5a6..5a0c1cb58 100644
--- a/apps/app/components/cycles/active-cycle-details.tsx
+++ b/apps/app/components/cycles/active-cycle-details.tsx
@@ -10,7 +10,7 @@ import cyclesService from "services/cycles.service";
// hooks
import useToast from "hooks/use-toast";
// ui
-import { LinearProgressIndicator, Tooltip } from "components/ui";
+import { LinearProgressIndicator, Loader, Tooltip } from "components/ui";
import { AssigneesList } from "components/ui/avatar";
import { SingleProgressStats } from "components/core";
// components
@@ -43,10 +43,6 @@ import { ICycle, IIssue } from "types";
// fetch-keys
import { CURRENT_CYCLE_LIST, CYCLES_LIST, CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys";
-type TSingleStatProps = {
- cycle: ICycle;
-};
-
const stateGroups = [
{
key: "backlog_issues",
@@ -75,12 +71,43 @@ const stateGroups = [
},
];
-export const ActiveCycleDetails: React.FC = ({ cycle }) => {
+export const ActiveCycleDetails: React.FC = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
+ const { data: currentCycle } = useSWR(
+ workspaceSlug && projectId ? CURRENT_CYCLE_LIST(projectId as string) : null,
+ workspaceSlug && projectId
+ ? () =>
+ cyclesService.getCyclesWithParams(workspaceSlug as string, projectId as string, "current")
+ : null
+ );
+ const cycle = currentCycle ? currentCycle[0] : null;
+
+ const { data: issues } = useSWR(
+ workspaceSlug && projectId && cycle?.id
+ ? CYCLE_ISSUES_WITH_PARAMS(cycle?.id, { priority: "urgent,high" })
+ : null,
+ workspaceSlug && projectId && cycle?.id
+ ? () =>
+ cyclesService.getCycleIssuesWithParams(
+ workspaceSlug as string,
+ projectId as string,
+ cycle.id,
+ { priority: "urgent,high" }
+ )
+ : null
+ ) as { data: IIssue[] | undefined };
+
+ if (!cycle)
+ return (
+
+
No active cycle is present.
+
+ );
+
const endDate = new Date(cycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? "");
@@ -164,21 +191,6 @@ export const ActiveCycleDetails: React.FC = ({ cycle }) => {
});
};
- const { data: issues } = useSWR(
- workspaceSlug && projectId && cycle.id
- ? CYCLE_ISSUES_WITH_PARAMS(cycle.id, { priority: "high" })
- : null,
- workspaceSlug && projectId && cycle.id
- ? () =>
- cyclesService.getCycleIssuesWithParams(
- workspaceSlug as string,
- projectId as string,
- cycle.id,
- { priority: "high" }
- )
- : null
- ) as { data: IIssue[] };
-
const progressIndicatorData = stateGroups.map((group, index) => ({
id: index,
name: group.title,
@@ -193,7 +205,7 @@ export const ActiveCycleDetails: React.FC = ({ cycle }) => {
-
High Priority Issues
-
-
- {issues
- ?.filter((issue) => issue.priority === "urgent" || issue.priority === "high")
- .map((issue) => (
+
High Priority Issues
+
+ {issues ? (
+ issues.map((issue) => (
= ({ cycle }) => {
{getPriorityIcon(issue.priority, "text-sm")}
@@ -455,7 +459,7 @@ export const ActiveCycleDetails: React.FC
= ({ cycle }) => {
Array.isArray(issue.assignees) ? (
@@ -466,7 +470,14 @@ export const ActiveCycleDetails: React.FC
= ({ cycle }) => {
- ))}
+ ))
+ ) : (
+
+
+
+
+
+ )}
@@ -478,33 +489,17 @@ export const ActiveCycleDetails: React.FC
= ({ cycle }) => {
width:
issues &&
`${
- (issues?.filter(
- (issue) =>
- issue?.state_detail?.group === "completed" &&
- (issue?.priority === "urgent" || issue?.priority === "high")
- )?.length /
- issues?.filter(
- (issue) => issue?.priority === "urgent" || issue?.priority === "high"
- )?.length) *
+ (issues.filter((issue) => issue?.state_detail?.group === "completed")
+ ?.length /
+ issues.length) *
100 ?? 0
}%`,
}}
/>
- {
- issues?.filter(
- (issue) =>
- issue?.state_detail?.group === "completed" &&
- (issue?.priority === "urgent" || issue?.priority === "high")
- )?.length
- }{" "}
- of{" "}
- {
- issues?.filter(
- (issue) => issue?.priority === "urgent" || issue?.priority === "high"
- )?.length
- }
+ {issues?.filter((issue) => issue?.state_detail?.group === "completed")?.length} of{" "}
+ {issues?.length}
@@ -512,11 +507,11 @@ export const ActiveCycleDetails: React.FC
= ({ cycle }) => {
@@ -532,11 +527,10 @@ export const ActiveCycleDetails: React.FC
= ({ cycle }) => {
diff --git a/apps/app/components/cycles/active-cycle-stats.tsx b/apps/app/components/cycles/active-cycle-stats.tsx
index bc04a4bd9..a01293d43 100644
--- a/apps/app/components/cycles/active-cycle-stats.tsx
+++ b/apps/app/components/cycles/active-cycle-stats.tsx
@@ -1,14 +1,7 @@
import React from "react";
-import { useRouter } from "next/router";
-
-import useSWR from "swr";
-
// headless ui
import { Tab } from "@headlessui/react";
-// services
-import issuesServices from "services/issues.service";
-import projectService from "services/project.service";
// hooks
import useLocalStorage from "hooks/use-local-storage";
// components
@@ -16,34 +9,15 @@ import { SingleProgressStats } from "components/core";
// ui
import { Avatar } from "components/ui";
// types
-import { IIssue, IIssueLabels } from "types";
-// fetch-keys
-import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
+import { ICycle } from "types";
// types
type Props = {
- issues: IIssue[];
+ cycle: ICycle;
};
-export const ActiveCycleProgressStats: React.FC
= ({ issues }) => {
- const router = useRouter();
- const { workspaceSlug, projectId } = router.query;
-
+export const ActiveCycleProgressStats: React.FC = ({ cycle }) => {
const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees");
- const { data: issueLabels } = useSWR(
- workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
- workspaceSlug && projectId
- ? () => issuesServices.getIssueLabels(workspaceSlug as string, projectId as string)
- : null
- );
-
- const { data: members } = useSWR(
- workspaceSlug && projectId ? PROJECT_MEMBERS(workspaceSlug as string) : null,
- workspaceSlug && projectId
- ? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
- : null
- );
-
const currentValue = (tab: string | null) => {
switch (tab) {
case "Assignees":
@@ -55,6 +29,7 @@ export const ActiveCycleProgressStats: React.FC = ({ issues }) => {
return 0;
}
};
+
return (
= ({ issues }) => {
as="div"
className="flex flex-col w-full mt-2 gap-1 overflow-y-scroll items-center text-brand-secondary"
>
- {members?.map((member, index) => {
- const totalArray = issues?.filter((i) => i?.assignees?.includes(member.member.id));
- const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
-
- if (totalArray.length > 0) {
+ {cycle.distribution.assignees.map((assignee, index) => {
+ if (assignee.assignee_id)
return (
-
- {member.member.first_name}
+
+ {assignee.first_name}
}
- completed={completeArray.length}
- total={totalArray.length}
+ completed={assignee.completed_issues}
+ total={assignee.total_issues}
+ />
+ );
+ else
+ return (
+
+
+
+
+ No assignee
+
+ }
+ completed={assignee.completed_issues}
+ total={assignee.total_issues}
/>
);
- }
})}
- {issues?.filter((i) => i?.assignees?.length === 0).length > 0 ? (
-
-
-
-
- No assignee
-
- }
- completed={
- issues?.filter(
- (i) => i?.state_detail.group === "completed" && i.assignees?.length === 0
- ).length
- }
- total={issues?.filter((i) => i?.assignees?.length === 0).length}
- />
- ) : (
- ""
- )}
- {issueLabels?.map((label, index) => {
- const totalArray = issues?.filter((i) => i?.labels?.includes(label.id));
- const completeArray = totalArray?.filter((i) => i?.state_detail.group === "completed");
-
- if (totalArray.length > 0) {
- return (
-
-
- {label?.name}
-
- }
- completed={completeArray.length}
- total={totalArray.length}
- />
- );
- }
- })}
+ {cycle.distribution.labels.map((label, index) => (
+
+
+ {label.label_name ?? "No labels"}
+
+ }
+ completed={label.completed_issues}
+ total={label.total_issues}
+ />
+ ))}
diff --git a/apps/app/components/cycles/sidebar.tsx b/apps/app/components/cycles/sidebar.tsx
index 2cb5b9fcb..d61bd1943 100644
--- a/apps/app/components/cycles/sidebar.tsx
+++ b/apps/app/components/cycles/sidebar.tsx
@@ -2,11 +2,22 @@ import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
-import useSWR, { mutate } from "swr";
+import { mutate } from "swr";
// react-hook-form
import { useForm } from "react-hook-form";
+// headless ui
import { Disclosure, Popover, Transition } from "@headlessui/react";
+// services
+import cyclesService from "services/cycles.service";
+// hooks
+import useToast from "hooks/use-toast";
+// components
+import { SidebarProgressStats } from "components/core";
+import ProgressChart from "components/core/sidebar/progress-chart";
+import { DeleteCycleModal } from "components/cycles";
+// ui
+import { CustomMenu, CustomRangeDatePicker, Loader, ProgressBar } from "components/ui";
// icons
import {
CalendarDaysIcon,
@@ -18,17 +29,6 @@ import {
DocumentIcon,
LinkIcon,
} from "@heroicons/react/24/outline";
-// ui
-import { CustomMenu, CustomRangeDatePicker, Loader, ProgressBar } from "components/ui";
-// hooks
-import useToast from "hooks/use-toast";
-// services
-import cyclesService from "services/cycles.service";
-// components
-import { SidebarProgressStats } from "components/core";
-import ProgressChart from "components/core/sidebar/progress-chart";
-import { DeleteCycleModal } from "components/cycles";
-// icons
import { ExclamationIcon } from "components/icons";
// helpers
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper";
@@ -38,9 +38,9 @@ import {
renderShortDate,
} from "helpers/date-time.helper";
// types
-import { ICurrentUserResponse, ICycle, IIssue } from "types";
+import { ICurrentUserResponse, ICycle } from "types";
// fetch-keys
-import { CYCLE_DETAILS, CYCLE_ISSUES } from "constants/fetch-keys";
+import { CYCLE_DETAILS } from "constants/fetch-keys";
type Props = {
cycle: ICycle | undefined;
@@ -69,18 +69,6 @@ export const CycleDetailsSidebar: React.FC = ({
end_date: new Date().toString(),
};
- const { data: issues } = useSWR(
- workspaceSlug && projectId && cycleId ? CYCLE_ISSUES(cycleId as string) : null,
- workspaceSlug && projectId && cycleId
- ? () =>
- cyclesService.getCycleIssues(
- workspaceSlug as string,
- projectId as string,
- cycleId as string
- )
- : null
- );
-
const { setValue, reset, watch } = useForm({
defaultValues,
});
@@ -553,9 +541,10 @@ export const CycleDetailsSidebar: React.FC = ({
@@ -604,7 +593,7 @@ export const CycleDetailsSidebar: React.FC = ({
{cycle.total_issues > 0 ? (
= ({
completed: cycle.completed_issues,
cancelled: cycle.cancelled_issues,
}}
+ totalIssues={cycle.total_issues}
/>
) : (
diff --git a/apps/app/components/modules/sidebar.tsx b/apps/app/components/modules/sidebar.tsx
index f453e4c68..de8714968 100644
--- a/apps/app/components/modules/sidebar.tsx
+++ b/apps/app/components/modules/sidebar.tsx
@@ -52,20 +52,13 @@ const defaultValues: Partial = {
};
type Props = {
- issues: IIssue[];
module?: IModule;
isOpen: boolean;
moduleIssues?: IIssue[];
user: ICurrentUserResponse | undefined;
};
-export const ModuleDetailsSidebar: React.FC = ({
- issues,
- module,
- isOpen,
- moduleIssues,
- user,
-}) => {
+export const ModuleDetailsSidebar: React.FC = ({ module, isOpen, moduleIssues, user }) => {
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
const [moduleLinkModal, setModuleLinkModal] = useState(false);
@@ -464,9 +457,10 @@ export const ModuleDetailsSidebar: React.FC = ({
@@ -517,7 +511,7 @@ export const ModuleDetailsSidebar: React.FC = ({
<>
= ({
completed: module.completed_issues,
cancelled: module.cancelled_issues,
}}
- userAuth={memberRole}
+ totalIssues={module.total_issues}
module={module}
/>
diff --git a/apps/app/components/ui/avatar.tsx b/apps/app/components/ui/avatar.tsx
index 06534c121..c91541aaa 100644
--- a/apps/app/components/ui/avatar.tsx
+++ b/apps/app/components/ui/avatar.tsx
@@ -13,7 +13,7 @@ import { IUser, IUserLite } from "types";
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
type AvatarProps = {
- user?: Partial | Partial | IUser | IUserLite | undefined | null;
+ user?: Partial | Partial | null;
index?: number;
height?: string;
width?: string;
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx
index 2bd2aa1b9..502578927 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx
@@ -32,7 +32,7 @@ import { ListBulletIcon, PlusIcon, Squares2X2Icon } from "@heroicons/react/24/ou
import { SelectCycleType } from "types";
import type { NextPage } from "next";
// fetch-keys
-import { CURRENT_CYCLE_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
+import { PROJECT_DETAILS } from "constants/fetch-keys";
const tabsList = ["All", "Active", "Upcoming", "Completed", "Drafts"];
@@ -72,14 +72,6 @@ const ProjectCycles: NextPage = () => {
: null
);
- const { data: currentCycle } = useSWR(
- workspaceSlug && projectId ? CURRENT_CYCLE_LIST(projectId as string) : null,
- workspaceSlug && projectId
- ? () =>
- cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, "current")
- : null
- );
-
useEffect(() => {
if (createUpdateCycleModal) return;
const timer = setTimeout(() => {
@@ -201,15 +193,7 @@ const ProjectCycles: NextPage = () => {
{cyclesView !== "gantt_chart" && (
- {currentCycle?.[0] ? (
-
- ) : (
-
-
- No active cycle is present.
-
-
- )}
+
)}
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx
index 657f48fe2..d63af5865 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx
@@ -5,13 +5,7 @@ import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// icons
-import {
- ArrowLeftIcon,
- ListBulletIcon,
- PlusIcon,
- RectangleGroupIcon,
- RectangleStackIcon,
-} from "@heroicons/react/24/outline";
+import { ArrowLeftIcon, RectangleGroupIcon } from "@heroicons/react/24/outline";
// services
import modulesService from "services/modules.service";
import issuesService from "services/issues.service";
@@ -191,7 +185,6 @@ const SingleModule: React.FC = () => {
Date: Wed, 21 Jun 2023 17:10:52 +0530
Subject: [PATCH 26/59] fix: inbox mutation fixes (#1324)
* chore: inbox status update mutation
* fix: inbox issue activity mutation
* refactor: code structure
* chore: snoozed status message
* chore: disable older dates for snoozing
* chore: extend snooze time
* chore: hide copy link from inbox
---
.../components/inbox/delete-issue-modal.tsx | 3 +-
.../components/inbox/inbox-action-headers.tsx | 24 +++++++-
.../app/components/inbox/inbox-issue-card.tsx | 8 ++-
.../components/inbox/inbox-main-content.tsx | 24 ++++++--
.../app/components/inbox/select-duplicate.tsx | 2 +-
apps/app/components/issues/sidebar.tsx | 16 +++---
.../projects/[projectId]/inbox/[inboxId].tsx | 56 +++++++++++--------
apps/app/styles/react-datepicker.css | 10 +++-
8 files changed, 101 insertions(+), 42 deletions(-)
diff --git a/apps/app/components/inbox/delete-issue-modal.tsx b/apps/app/components/inbox/delete-issue-modal.tsx
index 46ba1ebdd..c6f5320a2 100644
--- a/apps/app/components/inbox/delete-issue-modal.tsx
+++ b/apps/app/components/inbox/delete-issue-modal.tsx
@@ -130,7 +130,8 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data })
{data?.project_detail?.identifier}-{data?.sequence_id}
- {""}? This action cannot be undone.
+ {""}? The issue will only be deleted from the inbox and this action cannot be
+ undone.
diff --git a/apps/app/components/inbox/inbox-action-headers.tsx b/apps/app/components/inbox/inbox-action-headers.tsx
index 8be550d98..5ceaa8f2c 100644
--- a/apps/app/components/inbox/inbox-action-headers.tsx
+++ b/apps/app/components/inbox/inbox-action-headers.tsx
@@ -76,6 +76,11 @@ export const InboxActionHeader: React.FC
= (props) => {
const issueStatus = issue?.issue_inbox[0].status;
const isAllowed = memberRole.isMember || memberRole.isOwner;
+ const today = new Date();
+ const tomorrow = new Date(today);
+
+ tomorrow.setDate(today.getDate() + 1);
+
return (
@@ -142,12 +147,20 @@ export const InboxActionHeader: React.FC
= (props) => {
{isAllowed && (
-
+
-
+
@@ -165,6 +178,7 @@ export const InboxActionHeader: React.FC = (props) => {
setDate(val);
}}
dateFormat="dd-MM-yyyy"
+ minDate={tomorrow}
inline
/>
= (props) => {
)}
+
+ )}
+ {isAllowed && (
+
= (props) => {
{issue.issue_inbox[0].snoozed_till && (
-
+
Snoozed till {renderShortNumericDateFormat(issue.issue_inbox[0].snoozed_till)}
diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx
index 1bf330e5e..83948495c 100644
--- a/apps/app/components/inbox/inbox-main-content.tsx
+++ b/apps/app/components/inbox/inbox-main-content.tsx
@@ -36,7 +36,7 @@ import { renderShortNumericDateFormat } from "helpers/date-time.helper";
// types
import type { IInboxIssue, IIssue } from "types";
// fetch-keys
-import { INBOX_ISSUES, INBOX_ISSUE_DETAILS } from "constants/fetch-keys";
+import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
const defaultValues = {
name: "",
@@ -129,6 +129,7 @@ export const InboxMainContent: React.FC = () => {
.then(() => {
mutateIssueDetails();
mutate(INBOX_ISSUES(inboxId.toString(), params));
+ mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
});
},
[
@@ -157,7 +158,9 @@ export const InboxMainContent: React.FC = () => {
: issueStatus === -1
? "text-red-500 border-red-500 bg-red-500/10"
: issueStatus === 0
- ? "text-blue-500 border-blue-500 bg-blue-500/10"
+ ? new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date()
+ ? "text-red-500 border-red-500 bg-red-500/10"
+ : "text-blue-500 border-blue-500 bg-blue-500/10"
: issueStatus === 1
? "text-green-500 border-green-500 bg-green-500/10"
: issueStatus === 2
@@ -178,10 +181,19 @@ export const InboxMainContent: React.FC = () => {
) : issueStatus === 0 ? (
<>
-
- This issue has been snoozed till{" "}
- {renderShortNumericDateFormat(issueDetails.issue_inbox[0].snoozed_till ?? "")}.
-
+ {new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date() ? (
+
+ This issue was snoozed till{" "}
+ {renderShortNumericDateFormat(issueDetails.issue_inbox[0].snoozed_till ?? "")}
+ .
+
+ ) : (
+
+ This issue has been snoozed till{" "}
+ {renderShortNumericDateFormat(issueDetails.issue_inbox[0].snoozed_till ?? "")}
+ .
+
+ )}
>
) : issueStatus === 1 ? (
<>
diff --git a/apps/app/components/inbox/select-duplicate.tsx b/apps/app/components/inbox/select-duplicate.tsx
index fdf7034df..74409a57f 100644
--- a/apps/app/components/inbox/select-duplicate.tsx
+++ b/apps/app/components/inbox/select-duplicate.tsx
@@ -76,7 +76,7 @@ export const SelectDuplicateInboxIssueModal: React.FC = (props) => {
return (
setQuery("")} appear>
-
+
= ({
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
-
-
-
+ {(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
+
+
+
+ )}
{!isNotAllowed && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && (
{
const { user } = useUserAuth();
const { issues: inboxIssues, mutate: mutateInboxIssues } = useInboxView();
+ const { setToastAlert } = useToast();
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
@@ -103,6 +105,28 @@ const ProjectInbox: NextPage = () => {
const markInboxStatus = async (data: TInboxStatus) => {
if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId) return;
+ mutate(
+ INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string),
+ (prevData) => {
+ if (!prevData) return prevData;
+
+ return {
+ ...prevData,
+ issue_inbox: [{ ...prevData.issue_inbox[0], ...data }],
+ };
+ },
+ false
+ );
+ mutateInboxIssues(
+ (prevData) =>
+ (prevData ?? []).map((i) =>
+ i.bridge_id === inboxIssueId
+ ? { ...i, issue_inbox: [{ ...i.issue_inbox[0], ...data }] }
+ : i
+ ),
+ false
+ );
+
await inboxServices
.markInboxStatus(
workspaceSlug.toString(),
@@ -112,28 +136,16 @@ const ProjectInbox: NextPage = () => {
data,
user
)
- .then(() => {
- mutate(
- INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string),
- (prevData) => {
- if (!prevData) return prevData;
-
- return {
- ...prevData,
- issue_inbox: [{ ...prevData.issue_inbox[0], ...data }],
- };
- },
- false
- );
- mutateInboxIssues(
- (prevData) =>
- (prevData ?? []).map((i) =>
- i.bridge_id === inboxIssueId
- ? { ...i, issue_inbox: [{ ...i.issue_inbox[0], ...data }] }
- : i
- ),
- false
- );
+ .catch(() =>
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Something went wrong while updating inbox status. Please try again.",
+ })
+ )
+ .finally(() => {
+ mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string));
+ mutateInboxIssues();
});
};
diff --git a/apps/app/styles/react-datepicker.css b/apps/app/styles/react-datepicker.css
index 59ce4d9d9..918f4ed66 100644
--- a/apps/app/styles/react-datepicker.css
+++ b/apps/app/styles/react-datepicker.css
@@ -104,11 +104,19 @@
color: rgba(var(--color-text-base)) !important;
}
-.react-datepicker__day--selected {
+.react-datepicker__day--selected,
+.react-datepicker__day--selected:hover {
background-color: #216ba5 !important;
color: white !important;
}
+.react-datepicker__day--disabled,
+.react-datepicker__day--disabled:hover {
+ background: transparent !important;
+ color: rgba(var(--color-text-secondary)) !important;
+ cursor: default;
+}
+
.react-datepicker__day--today {
font-weight: 800;
}
From a0ae569a681c4602b8f28b0f444823074330c53a Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Wed, 21 Jun 2023 17:32:56 +0530
Subject: [PATCH 27/59] chore: show inbox applied filters list (#1334)
---
.../app/components/inbox/filters-dropdown.tsx | 14 +-
apps/app/components/inbox/filters-list.tsx | 126 ++++++++++++++++++
apps/app/components/inbox/index.ts | 1 +
.../components/inbox/issues-list-sidebar.tsx | 9 +-
apps/app/constants/inbox.ts | 34 ++++-
apps/app/contexts/inbox-view-context.tsx | 36 ++++-
apps/app/contexts/issue-view.context.tsx | 2 +-
apps/app/hooks/use-inbox-view.tsx | 5 +-
.../projects/[projectId]/inbox/[inboxId].tsx | 2 +-
9 files changed, 206 insertions(+), 23 deletions(-)
create mode 100644 apps/app/components/inbox/filters-list.tsx
diff --git a/apps/app/components/inbox/filters-dropdown.tsx b/apps/app/components/inbox/filters-dropdown.tsx
index 938e4fc01..b567baed0 100644
--- a/apps/app/components/inbox/filters-dropdown.tsx
+++ b/apps/app/components/inbox/filters-dropdown.tsx
@@ -6,7 +6,7 @@ import { getPriorityIcon } from "components/icons";
import { IInboxFilterOptions } from "types";
// constants
import { PRIORITIES } from "constants/project";
-import { STATUS } from "constants/inbox";
+import { INBOX_STATUS } from "constants/inbox";
type Props = {
filters: Partial;
@@ -45,16 +45,16 @@ export const FiltersDropdown: React.FC = ({ filters, onSelect, direction,
{
id: "inbox_status",
label: "Status",
- value: Object.values(STATUS),
+ value: INBOX_STATUS.map((status) => status.value),
children: [
- ...Object.keys(STATUS).map((status) => ({
- id: status,
- label: status,
+ ...INBOX_STATUS.map((status) => ({
+ id: status.key,
+ label: status.label,
value: {
key: "inbox_status",
- value: STATUS[status],
+ value: status.value,
},
- selected: filters?.inbox_status?.includes(STATUS[status]),
+ selected: filters?.inbox_status?.includes(status.value),
})),
],
},
diff --git a/apps/app/components/inbox/filters-list.tsx b/apps/app/components/inbox/filters-list.tsx
new file mode 100644
index 000000000..aa8213b2f
--- /dev/null
+++ b/apps/app/components/inbox/filters-list.tsx
@@ -0,0 +1,126 @@
+// hooks
+import useInboxView from "hooks/use-inbox-view";
+// icons
+import { XMarkIcon } from "@heroicons/react/24/outline";
+import { getPriorityIcon } from "components/icons";
+// helpers
+import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
+// constants
+import { INBOX_STATUS } from "constants/inbox";
+
+export const InboxFiltersList = () => {
+ const { filters, setFilters, clearAllFilters, filtersLength } = useInboxView();
+
+ if (filtersLength <= 0) return <>>;
+
+ return (
+
+ {Object.keys(filters).map((key) => {
+ const filterKey = key as keyof typeof filters;
+
+ if (filters[filterKey] !== null)
+ return (
+
+
+ {replaceUnderscoreIfSnakeCase(key)}:
+
+ {filters[filterKey] === null || (filters[filterKey]?.length ?? 0) <= 0 ? (
+
None
+ ) : (
+
+ {filterKey === "priority" ? (
+
+ {filters.priority?.map((priority) => (
+
+ {getPriorityIcon(priority)}
+
+ setFilters({
+ priority: filters.priority?.filter((p) => p !== priority),
+ })
+ }
+ >
+
+
+
+ ))}
+
+ setFilters({
+ priority: null,
+ })
+ }
+ >
+
+
+
+ ) : filterKey === "inbox_status" ? (
+
+ {filters.inbox_status?.map((status) => (
+
+ {INBOX_STATUS.find((s) => s.value === status)?.label}
+
+ setFilters({
+ inbox_status: filters.inbox_status?.filter((p) => p !== status),
+ })
+ }
+ >
+
+
+
+ ))}
+
+ setFilters({
+ priority: null,
+ })
+ }
+ >
+
+
+
+ ) : (
+ (filters[filterKey] as any)?.join(", ")
+ )}
+
+ )}
+
+ );
+ })}
+
+ Clear all
+
+
+
+ );
+};
diff --git a/apps/app/components/inbox/index.ts b/apps/app/components/inbox/index.ts
index 3f66790bc..7cdd8ee9d 100644
--- a/apps/app/components/inbox/index.ts
+++ b/apps/app/components/inbox/index.ts
@@ -1,6 +1,7 @@
export * from "./decline-issue-modal";
export * from "./delete-issue-modal";
export * from "./filters-dropdown";
+export * from "./filters-list";
export * from "./inbox-action-headers";
export * from "./inbox-issue-card";
export * from "./inbox-main-content";
diff --git a/apps/app/components/inbox/issues-list-sidebar.tsx b/apps/app/components/inbox/issues-list-sidebar.tsx
index 02181c02a..9f5c85db1 100644
--- a/apps/app/components/inbox/issues-list-sidebar.tsx
+++ b/apps/app/components/inbox/issues-list-sidebar.tsx
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
// hooks
import useInboxView from "hooks/use-inbox-view";
// components
-import { InboxIssueCard } from "components/inbox";
+import { InboxIssueCard, InboxFiltersList } from "components/inbox";
// ui
import { Loader } from "components/ui";
@@ -14,10 +14,11 @@ export const IssuesListSidebar = () => {
const { issues: inboxIssues } = useInboxView();
return (
- <>
+
+
{inboxIssues ? (
inboxIssues.length > 0 ? (
-
+
{inboxIssues.map((issue) => (
{
)}
- >
+
);
};
diff --git a/apps/app/constants/inbox.ts b/apps/app/constants/inbox.ts
index b5731fb89..9bbe8230a 100644
--- a/apps/app/constants/inbox.ts
+++ b/apps/app/constants/inbox.ts
@@ -1,9 +1,29 @@
-export const STATUS: { [key: string]: number } = {
- Pending: -2,
- Declined: -1,
- Snoozed: 0,
- Accepted: 1,
- Duplicate: 2,
-};
+export const INBOX_STATUS = [
+ {
+ key: "pending",
+ label: "Pending",
+ value: -2,
+ },
+ {
+ key: "declined",
+ label: "Declined",
+ value: -1,
+ },
+ {
+ key: "snoozed",
+ label: "Snoozed",
+ value: 0,
+ },
+ {
+ key: "accepted",
+ label: "Accepted",
+ value: 1,
+ },
+ {
+ key: "duplicate",
+ label: "Duplicate",
+ value: 2,
+ },
+];
export const INBOX_ISSUE_SOURCE = "in-app";
diff --git a/apps/app/contexts/inbox-view-context.tsx b/apps/app/contexts/inbox-view-context.tsx
index ff3bebc72..f6201fbb9 100644
--- a/apps/app/contexts/inbox-view-context.tsx
+++ b/apps/app/contexts/inbox-view-context.tsx
@@ -26,6 +26,7 @@ type ReducerActionType = {
type ContextType = InboxViewProps & {
setFilters: (filters: Partial
) => void;
+ clearAllFilters: () => void;
};
type StateType = {
@@ -53,7 +54,7 @@ export const reducer: ReducerFunctionType = (state, action) => {
...state,
filters: {
...state.filters,
- ...payload,
+ ...payload?.filters,
},
};
@@ -140,6 +141,38 @@ export const InboxViewContextProvider: React.FC<{ children: React.ReactNode }> =
[workspaceSlug, projectId, inboxId, mutateInboxDetails, state]
);
+ const clearAllFilters = useCallback(() => {
+ dispatch({
+ type: "SET_FILTERS",
+ payload: {
+ filters: { ...initialState.filters },
+ },
+ });
+
+ if (!workspaceSlug || !projectId || !inboxId) return;
+
+ const newViewProps = {
+ ...state,
+ filters: { ...initialState.filters },
+ };
+
+ mutateInboxDetails((prevData) => {
+ if (!prevData) return prevData;
+
+ return {
+ ...prevData,
+ view_props: newViewProps,
+ };
+ }, false);
+
+ saveDataToServer(
+ workspaceSlug.toString(),
+ projectId.toString(),
+ inboxId.toString(),
+ newViewProps
+ );
+ }, [inboxId, mutateInboxDetails, projectId, state, workspaceSlug]);
+
useEffect(() => {
dispatch({
type: "REHYDRATE_THEME",
@@ -154,6 +187,7 @@ export const InboxViewContextProvider: React.FC<{ children: React.ReactNode }> =
value={{
filters: state.filters,
setFilters,
+ clearAllFilters,
}}
>
diff --git a/apps/app/contexts/issue-view.context.tsx b/apps/app/contexts/issue-view.context.tsx
index 6053b8b15..d2a4496c9 100644
--- a/apps/app/contexts/issue-view.context.tsx
+++ b/apps/app/contexts/issue-view.context.tsx
@@ -168,7 +168,7 @@ export const reducer: ReducerFunctionType = (state, action) => {
...state,
filters: {
...state.filters,
- ...payload,
+ ...payload?.filters,
},
};
diff --git a/apps/app/hooks/use-inbox-view.tsx b/apps/app/hooks/use-inbox-view.tsx
index 4ce6f4859..a5dedc380 100644
--- a/apps/app/hooks/use-inbox-view.tsx
+++ b/apps/app/hooks/use-inbox-view.tsx
@@ -14,7 +14,7 @@ import { IInboxQueryParams } from "types";
import { INBOX_ISSUES } from "constants/fetch-keys";
const useInboxView = () => {
- const { filters, setFilters } = useContext(inboxViewContext);
+ const { filters, setFilters, clearAllFilters } = useContext(inboxViewContext);
const router = useRouter();
const { workspaceSlug, projectId, inboxId } = router.query;
@@ -50,10 +50,11 @@ const useInboxView = () => {
return {
filters,
setFilters,
+ clearAllFilters,
+ filtersLength,
params,
issues: inboxIssues,
mutate: mutateInboxIssues,
- filtersLength,
} as const;
};
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx
index c47e79d06..c08925c0f 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx
@@ -222,7 +222,7 @@ const ProjectInbox: NextPage = () => {
}}
onDelete={() => setDeleteIssueModal(true)}
/>
-
+
{inboxIssueId ? (
From 2cef6e67d47af9b6adb4f41b0fbd24083b0b8600 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Wed, 21 Jun 2023 17:56:08 +0530
Subject: [PATCH 28/59] chore: decline issue mutation (#1354)
---
.../components/inbox/decline-issue-modal.tsx | 79 ++-----------------
.../projects/[projectId]/inbox/[inboxId].tsx | 5 ++
2 files changed, 10 insertions(+), 74 deletions(-)
diff --git a/apps/app/components/inbox/decline-issue-modal.tsx b/apps/app/components/inbox/decline-issue-modal.tsx
index 64fe1682a..941841659 100644
--- a/apps/app/components/inbox/decline-issue-modal.tsx
+++ b/apps/app/components/inbox/decline-issue-modal.tsx
@@ -1,102 +1,33 @@
-import React, { useEffect, useState } from "react";
-
-import { useRouter } from "next/router";
-
-import { mutate } from "swr";
+import React, { useState } from "react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
-// services
-import inboxServices from "services/inbox.service";
-// hooks
-import useToast from "hooks/use-toast";
-import useInboxView from "hooks/use-inbox-view";
-import useUser from "hooks/use-user";
// icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ui
import { SecondaryButton, DangerButton } from "components/ui";
// types
-import type { IInboxIssue, ICurrentUserResponse, IInboxIssueDetail } from "types";
-// fetch-keys
-import { INBOX_ISSUES, INBOX_ISSUE_DETAILS } from "constants/fetch-keys";
+import type { IInboxIssue } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
data: IInboxIssue | undefined;
+ onSubmit: () => Promise
;
};
-export const DeclineIssueModal: React.FC = ({ isOpen, handleClose, data }) => {
+export const DeclineIssueModal: React.FC = ({ isOpen, handleClose, data, onSubmit }) => {
const [isDeclining, setIsDeclining] = useState(false);
- const router = useRouter();
- const { workspaceSlug, projectId, inboxId } = router.query;
-
- const { user } = useUser();
- const { setToastAlert } = useToast();
- const { params } = useInboxView();
-
const onClose = () => {
setIsDeclining(false);
handleClose();
};
const handleDecline = () => {
- if (!workspaceSlug || !projectId || !inboxId || !data) return;
-
setIsDeclining(true);
- inboxServices
- .markInboxStatus(
- workspaceSlug.toString(),
- projectId.toString(),
- inboxId.toString(),
- data.bridge_id,
- {
- status: -1,
- },
- user
- )
- .then(() => {
- mutate(
- INBOX_ISSUE_DETAILS(inboxId.toString(), data.bridge_id),
- (prevData) => {
- if (!prevData) return prevData;
-
- return {
- ...prevData,
- issue_inbox: [{ ...prevData.issue_inbox[0], status: -1 }],
- };
- },
- false
- );
- mutate(
- INBOX_ISSUES(inboxId.toString(), params),
- (prevData) =>
- prevData?.map((i) =>
- i.bridge_id === data.bridge_id
- ? { ...i, issue_inbox: [{ ...i.issue_inbox[0], status: -1 }] }
- : i
- ),
- false
- );
-
- setToastAlert({
- type: "success",
- title: "Success!",
- message: "Issue declined successfully.",
- });
- onClose();
- })
- .catch(() =>
- setToastAlert({
- type: "error",
- title: "Error!",
- message: "Issue could not be declined. Please try again.",
- })
- )
- .finally(() => setIsDeclining(false));
+ onSubmit().finally(() => setIsDeclining(false));
};
return (
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx
index c08925c0f..140e704ed 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx
@@ -194,6 +194,11 @@ const ProjectInbox: NextPage = () => {
isOpen={declineIssueModal}
handleClose={() => setDeclineIssueModal(false)}
data={inboxIssues?.find((i) => i.bridge_id === inboxIssueId)}
+ onSubmit={async () => {
+ await markInboxStatus({
+ status: -1,
+ }).finally(() => setDeclineIssueModal(false));
+ }}
/>
Date: Thu, 22 Jun 2023 16:23:39 +0530
Subject: [PATCH 29/59] fix: clear inbox status filter button (#1355)
---
apps/app/components/inbox/filters-list.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/app/components/inbox/filters-list.tsx b/apps/app/components/inbox/filters-list.tsx
index aa8213b2f..264b925a2 100644
--- a/apps/app/components/inbox/filters-list.tsx
+++ b/apps/app/components/inbox/filters-list.tsx
@@ -98,7 +98,7 @@ export const InboxFiltersList = () => {
type="button"
onClick={() =>
setFilters({
- priority: null,
+ inbox_status: null,
})
}
>
From 537cd2f5dd49416645195cb0fa61ab9fab975856 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Thu, 22 Jun 2023 19:47:56 +0530
Subject: [PATCH 30/59] chore: link and attachment count in sub issues (#1352)
---
apiserver/plane/api/views/issue.py | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py
index cd9f65e48..35583fea2 100644
--- a/apiserver/plane/api/views/issue.py
+++ b/apiserver/plane/api/views/issue.py
@@ -590,6 +590,20 @@ class SubIssuesEndpoint(BaseAPIView):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
+ .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")
+ )
)
state_distribution = (
From bfac39f1bc94a7450868386ee666adfe6b071886 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Thu, 22 Jun 2023 19:48:04 +0530
Subject: [PATCH 31/59] chore: inbox issue permissions (#1341)
* chore: inbox issue permissions
* dev: update delete endpoint
---
apiserver/plane/api/views/inbox.py | 109 ++++++++++++++++++++---------
1 file changed, 75 insertions(+), 34 deletions(-)
diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py
index 738c6ef9f..0e4c1603e 100644
--- a/apiserver/plane/api/views/inbox.py
+++ b/apiserver/plane/api/views/inbox.py
@@ -22,7 +22,7 @@ from plane.db.models import (
State,
IssueLink,
IssueAttachment,
- IssueActivity,
+ ProjectMember,
)
from plane.api.serializers import (
IssueSerializer,
@@ -246,13 +246,28 @@ class InboxIssueViewSet(BaseViewSet):
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
+ # Get the project member
+ project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
+ # Only project members admins and created_by users can access this endpoint
+ if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
+ return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
+ # Get issue data
issue_data = request.data.pop("issue", False)
if bool(issue_data):
issue = Issue.objects.get(
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
)
+ # Only allow guests and viewers to edit name and description
+ if project_member <= 10:
+ # viewers and guests since only viewers and guests
+ issue_data = {
+ "name": issue_data.get("name", issue.name),
+ "description_html": issue_data.get("description_html", issue.description_html),
+ "description": issue_data.get("description", issue.description)
+ }
+
issue_serializer = IssueCreateSerializer(
issue, data=issue_data, partial=True
)
@@ -279,46 +294,50 @@ class InboxIssueViewSet(BaseViewSet):
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
- serializer = InboxIssueSerializer(
- inbox_issue, data=request.data, partial=True
- )
+ # Only project admins and members can edit inbox issue attributes
+ if project_member.role > 10:
+ serializer = InboxIssueSerializer(
+ inbox_issue, data=request.data, partial=True
+ )
- if serializer.is_valid():
- serializer.save()
- # Update the issue state if the issue is rejected or marked as duplicate
- if serializer.data["status"] in [-1, 2]:
- issue = Issue.objects.get(
- pk=inbox_issue.issue_id,
- workspace__slug=slug,
- project_id=project_id,
- )
- state = State.objects.filter(
- group="cancelled", workspace__slug=slug, project_id=project_id
- ).first()
- if state is not None:
- issue.state = state
- issue.save()
-
- # Update the issue state if it is accepted
- if serializer.data["status"] in [1]:
- issue = Issue.objects.get(
- pk=inbox_issue.issue_id,
- workspace__slug=slug,
- project_id=project_id,
- )
-
- # Update the issue state only if it is in triage state
- if issue.state.name == "Triage":
- # Move to default state
+ if serializer.is_valid():
+ serializer.save()
+ # Update the issue state if the issue is rejected or marked as duplicate
+ if serializer.data["status"] in [-1, 2]:
+ issue = Issue.objects.get(
+ pk=inbox_issue.issue_id,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
state = State.objects.filter(
- workspace__slug=slug, project_id=project_id, default=True
+ group="cancelled", workspace__slug=slug, project_id=project_id
).first()
if state is not None:
issue.state = state
issue.save()
- return Response(serializer.data, status=status.HTTP_200_OK)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ # Update the issue state if it is accepted
+ if serializer.data["status"] in [1]:
+ issue = Issue.objects.get(
+ pk=inbox_issue.issue_id,
+ workspace__slug=slug,
+ project_id=project_id,
+ )
+
+ # Update the issue state only if it is in triage state
+ if issue.state.name == "Triage":
+ # Move to default state
+ state = State.objects.filter(
+ workspace__slug=slug, project_id=project_id, default=True
+ ).first()
+ if state is not None:
+ issue.state = state
+ issue.save()
+
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ else:
+ return Response(InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK)
except InboxIssue.DoesNotExist:
return Response(
{"error": "Inbox Issue does not exist"},
@@ -347,3 +366,25 @@ class InboxIssueViewSet(BaseViewSet):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+
+ def destroy(self, request, slug, project_id, inbox_id, pk):
+ try:
+ inbox_issue = InboxIssue.objects.get(
+ pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
+ )
+ # Get the project member
+ project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
+
+ if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
+ return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
+
+ inbox_issue.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
+ except InboxIssue.DoesNotExist:
+ return Response({"error": "Inbox Issue does not exists"}, status=status.HTTP_400_BAD_REQUEST)
+ 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
From 33cfbbf1530a763244123c99f550b9410a7d9449 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Thu, 22 Jun 2023 19:48:17 +0530
Subject: [PATCH 32/59] chore: enable print logs for background workers when in
DEBUG mode (#1357)
---
apiserver/plane/bgtasks/analytic_plot_export.py | 4 +++-
apiserver/plane/bgtasks/email_verification_task.py | 3 +++
apiserver/plane/bgtasks/forgot_password_task.py | 3 +++
apiserver/plane/bgtasks/importer_task.py | 3 +++
apiserver/plane/bgtasks/issue_activites_task.py | 3 +++
apiserver/plane/bgtasks/magic_link_code_task.py | 3 +++
apiserver/plane/bgtasks/project_invitation_task.py | 3 +++
apiserver/plane/bgtasks/user_welcome_task.py | 3 +++
apiserver/plane/bgtasks/workspace_invitation_task.py | 3 +++
9 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/apiserver/plane/bgtasks/analytic_plot_export.py b/apiserver/plane/bgtasks/analytic_plot_export.py
index 37362416f..27b625445 100644
--- a/apiserver/plane/bgtasks/analytic_plot_export.py
+++ b/apiserver/plane/bgtasks/analytic_plot_export.py
@@ -169,6 +169,8 @@ def analytic_export_task(email, data, slug):
msg.send(fail_silently=False)
except Exception as e:
- print(e)
+ # Print logs if in DEBUG mode
+ if settings.DEBUG:
+ print(e)
capture_exception(e)
return
diff --git a/apiserver/plane/bgtasks/email_verification_task.py b/apiserver/plane/bgtasks/email_verification_task.py
index 89551044b..93b15c425 100644
--- a/apiserver/plane/bgtasks/email_verification_task.py
+++ b/apiserver/plane/bgtasks/email_verification_task.py
@@ -39,5 +39,8 @@ def email_verification(first_name, email, token, current_site):
msg.send()
return
except Exception as e:
+ # Print logs if in DEBUG mode
+ if settings.DEBUG:
+ print(e)
capture_exception(e)
return
diff --git a/apiserver/plane/bgtasks/forgot_password_task.py b/apiserver/plane/bgtasks/forgot_password_task.py
index 687e4f976..93283dfd5 100644
--- a/apiserver/plane/bgtasks/forgot_password_task.py
+++ b/apiserver/plane/bgtasks/forgot_password_task.py
@@ -37,5 +37,8 @@ def forgot_password(first_name, email, uidb64, token, current_site):
msg.send()
return
except Exception as e:
+ # Print logs if in DEBUG mode
+ if settings.DEBUG:
+ print(e)
capture_exception(e)
return
diff --git a/apiserver/plane/bgtasks/importer_task.py b/apiserver/plane/bgtasks/importer_task.py
index 85ac1c89b..757ef601b 100644
--- a/apiserver/plane/bgtasks/importer_task.py
+++ b/apiserver/plane/bgtasks/importer_task.py
@@ -175,5 +175,8 @@ def service_importer(service, importer_id):
importer = Importer.objects.get(pk=importer_id)
importer.status = "failed"
importer.save()
+ # Print logs if in DEBUG mode
+ if settings.DEBUG:
+ print(e)
capture_exception(e)
return
diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py
index b1096e30b..5865a5982 100644
--- a/apiserver/plane/bgtasks/issue_activites_task.py
+++ b/apiserver/plane/bgtasks/issue_activites_task.py
@@ -1006,5 +1006,8 @@ def issue_activity(
)
return
except Exception as e:
+ # Print logs if in DEBUG mode
+ if settings.DEBUG:
+ print(e)
capture_exception(e)
return
diff --git a/apiserver/plane/bgtasks/magic_link_code_task.py b/apiserver/plane/bgtasks/magic_link_code_task.py
index 29851c435..91cc461bb 100644
--- a/apiserver/plane/bgtasks/magic_link_code_task.py
+++ b/apiserver/plane/bgtasks/magic_link_code_task.py
@@ -31,4 +31,7 @@ def magic_link(email, key, token, current_site):
return
except Exception as e:
capture_exception(e)
+ # Print logs if in DEBUG mode
+ if settings.DEBUG:
+ print(e)
return
diff --git a/apiserver/plane/bgtasks/project_invitation_task.py b/apiserver/plane/bgtasks/project_invitation_task.py
index 7f1125f80..8b8ef6e48 100644
--- a/apiserver/plane/bgtasks/project_invitation_task.py
+++ b/apiserver/plane/bgtasks/project_invitation_task.py
@@ -50,5 +50,8 @@ def project_invitation(email, project_id, token, current_site):
except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist) as e:
return
except Exception as e:
+ # Print logs if in DEBUG mode
+ if settings.DEBUG:
+ print(e)
capture_exception(e)
return
diff --git a/apiserver/plane/bgtasks/user_welcome_task.py b/apiserver/plane/bgtasks/user_welcome_task.py
index bea2ee33d..33f4b5686 100644
--- a/apiserver/plane/bgtasks/user_welcome_task.py
+++ b/apiserver/plane/bgtasks/user_welcome_task.py
@@ -29,5 +29,8 @@ def send_welcome_slack(user_id, created, message):
print(f"Got an error: {e.response['error']}")
return
except Exception as e:
+ # Print logs if in DEBUG mode
+ if settings.DEBUG:
+ print(e)
capture_exception(e)
return
diff --git a/apiserver/plane/bgtasks/workspace_invitation_task.py b/apiserver/plane/bgtasks/workspace_invitation_task.py
index 7b2bada0a..d84a0b414 100644
--- a/apiserver/plane/bgtasks/workspace_invitation_task.py
+++ b/apiserver/plane/bgtasks/workspace_invitation_task.py
@@ -66,5 +66,8 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
except (Workspace.DoesNotExist, WorkspaceMemberInvite.DoesNotExist) as e:
return
except Exception as e:
+ # Print logs if in DEBUG mode
+ if settings.DEBUG:
+ print(e)
capture_exception(e)
return
From d1e834eb6f79104123cca9ae578479bca78d4cef Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Thu, 22 Jun 2023 19:48:28 +0530
Subject: [PATCH 33/59] chore: updated user onboarded response (#1365)
---
apiserver/plane/api/views/people.py | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/apiserver/plane/api/views/people.py b/apiserver/plane/api/views/people.py
index 9dad5380b..8e19fea1a 100644
--- a/apiserver/plane/api/views/people.py
+++ b/apiserver/plane/api/views/people.py
@@ -98,20 +98,6 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
user = User.objects.get(pk=request.user.id)
user.is_onboarded = request.data.get("is_onboarded", False)
user.save()
-
- if user.last_workspace_id is not None:
- user_role = WorkspaceMember.objects.filter(
- workspace_id=user.last_workspace_id, member=request.user.id
- ).first()
- return Response(
- {
- "message": "Updated successfully",
- "role": user_role.company_role
- if user_role is not None
- else None,
- },
- status=status.HTTP_200_OK,
- )
return Response(
{"message": "Updated successfully"}, status=status.HTTP_200_OK
)
From e7bb580289f9271fbe2afeac656127622bf1f7b6 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Thu, 22 Jun 2023 19:48:40 +0530
Subject: [PATCH 34/59] chore: set sentry dsn from environment variable (#1366)
* chore: set sentry dsn from env variable for backend and worker
* dev: sentry dsn for docker compose hub file
---
.env.example | 4 +++-
docker-compose-hub.yml | 6 ++++--
docker-compose.yml | 2 ++
3 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/.env.example b/.env.example
index 166fbb9b7..fc1aef49d 100644
--- a/.env.example
+++ b/.env.example
@@ -21,10 +21,12 @@ NEXT_PUBLIC_TRACK_EVENTS=0
NEXT_PUBLIC_SLACK_CLIENT_ID=""
# Backend
-
# Debug value for api server use it as 0 for production use
DEBUG=0
+# Error logs
+SENTRY_DSN=""
+
# Database Settings
PGUSER="plane"
PGPASSWORD="plane"
diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml
index 8087bd427..63f196300 100644
--- a/docker-compose-hub.yml
+++ b/docker-compose-hub.yml
@@ -28,6 +28,8 @@ services:
env_file:
- .env
environment:
+ DEBUG: ${DEBUG}
+ SENTRY_DSN: ${SENTRY_DSN}
DJANGO_SETTINGS_MODULE: plane.settings.production
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
REDIS_URL: redis://plane-redis:6379/
@@ -52,7 +54,6 @@ services:
DEFAULT_EMAIL: ${DEFAULT_EMAIL}
DEFAULT_PASSWORD: ${DEFAULT_PASSWORD}
USE_MINIO: ${USE_MINIO}
- DEBUG: ${DEBUG}
depends_on:
- plane-db
- plane-redis
@@ -65,6 +66,8 @@ services:
env_file:
- .env
environment:
+ DEBUG: ${DEBUG}
+ SENTRY_DSN: ${SENTRY_DSN}
DJANGO_SETTINGS_MODULE: plane.settings.production
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
REDIS_URL: redis://plane-redis:6379/
@@ -89,7 +92,6 @@ services:
DEFAULT_EMAIL: ${DEFAULT_EMAIL}
DEFAULT_PASSWORD: ${DEFAULT_PASSWORD}
USE_MINIO: ${USE_MINIO}
- DEBUG: ${DEBUG}
depends_on:
- plane-api
- plane-db
diff --git a/docker-compose.yml b/docker-compose.yml
index bacfe5cb2..640bb723e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -38,6 +38,7 @@ services:
- .env
environment:
DEBUG: ${DEBUG}
+ SENTRY_DSN: ${SENTRY_DSN}
DJANGO_SETTINGS_MODULE: plane.settings.production
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
REDIS_URL: redis://plane-redis:6379/
@@ -80,6 +81,7 @@ services:
- .env
environment:
DEBUG: ${DEBUG}
+ SENTRY_DSN: ${SENTRY_DSN}
DJANGO_SETTINGS_MODULE: plane.settings.production
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
REDIS_URL: redis://plane-redis:6379/
From f8391507415facc23f3ed7afc12b1957caba13b6 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Fri, 23 Jun 2023 11:08:53 +0530
Subject: [PATCH 35/59] fix: create new issue when grouped by label (#1308)
---
.../components/core/board-view/all-boards.tsx | 3 +-
.../core/board-view/board-header.tsx | 2 +-
.../core/board-view/single-board.tsx | 72 ++++++++++---------
.../core/existing-issues-list-modal.tsx | 4 +-
apps/app/components/core/issues-view.tsx | 10 ++-
apps/app/components/issues/select/label.tsx | 7 +-
6 files changed, 55 insertions(+), 43 deletions(-)
diff --git a/apps/app/components/core/board-view/all-boards.tsx b/apps/app/components/core/board-view/all-boards.tsx
index 3e67e86b5..711fb7336 100644
--- a/apps/app/components/core/board-view/all-boards.tsx
+++ b/apps/app/components/core/board-view/all-boards.tsx
@@ -2,11 +2,12 @@
import useProjectIssuesView from "hooks/use-issues-view";
// components
import { SingleBoard } from "components/core/board-view/single-board";
+// icons
+import { getStateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types";
-import { getStateGroupIcon } from "components/icons";
type Props = {
type: "issue" | "cycle" | "module";
diff --git a/apps/app/components/core/board-view/board-header.tsx b/apps/app/components/core/board-view/board-header.tsx
index 6fa8f68f3..a5df7a426 100644
--- a/apps/app/components/core/board-view/board-header.tsx
+++ b/apps/app/components/core/board-view/board-header.tsx
@@ -166,7 +166,7 @@ export const BoardHeader: React.FC = ({
)}
- {!isCompleted && (
+ {!isCompleted && selectedGroup !== "created_by" && (
= ({
{provided.placeholder}
-
- {type === "issue" ? (
-
-
- Add Issue
-
- ) : (
- !isCompleted && (
-
-
- Add Issue
-
- }
- position="left"
- noBorder
+ {selectedGroup !== "created_by" && (
+
+ {type === "issue" ? (
+
-
- Create new
-
- {openIssuesListModal && (
-
- Add an existing issue
+
+ Add Issue
+
+ ) : (
+ !isCompleted && (
+
+
+ Add Issue
+
+ }
+ position="left"
+ noBorder
+ >
+
+ Create new
- )}
-
- )
- )}
-
+ {openIssuesListModal && (
+
+ Add an existing issue
+
+ )}
+
+ )
+ )}
+
+ )}
)}
diff --git a/apps/app/components/core/existing-issues-list-modal.tsx b/apps/app/components/core/existing-issues-list-modal.tsx
index 8ef6132ae..fe6f1e1b4 100644
--- a/apps/app/components/core/existing-issues-list-modal.tsx
+++ b/apps/app/components/core/existing-issues-list-modal.tsx
@@ -72,9 +72,9 @@ export const ExistingIssuesListModal: React.FC
= ({
const onSubmit: SubmitHandler = async (data) => {
if (!data.issues || data.issues.length === 0) {
setToastAlert({
- title: "Error",
type: "error",
- message: "Please select atleast one issue",
+ title: "Error!",
+ message: "Please select at least one issue.",
});
return;
diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx
index 2e9e14ae3..b90439b9d 100644
--- a/apps/app/components/core/issues-view.tsx
+++ b/apps/app/components/core/issues-view.tsx
@@ -283,9 +283,17 @@ export const IssuesView: React.FC = ({
const addIssueToState = useCallback(
(groupTitle: string) => {
setCreateIssueModal(true);
+
+ let preloadedValue: string | string[] = groupTitle;
+
+ if (selectedGroup === "labels") {
+ if (groupTitle === "None") preloadedValue = [];
+ else preloadedValue = [groupTitle];
+ }
+
if (selectedGroup)
setPreloadedData({
- [selectedGroup]: groupTitle,
+ [selectedGroup]: preloadedValue,
actionType: "createIssue",
});
else setPreloadedData({ actionType: "createIssue" });
diff --git a/apps/app/components/issues/select/label.tsx b/apps/app/components/issues/select/label.tsx
index a7ada1133..e99eecc16 100644
--- a/apps/app/components/issues/select/label.tsx
+++ b/apps/app/components/issues/select/label.tsx
@@ -6,6 +6,10 @@ import useSWR from "swr";
// headless ui
import { Combobox, Transition } from "@headlessui/react";
+// services
+import issuesServices from "services/issues.service";
+// ui
+import { IssueLabelsList } from "components/ui";
// icons
import {
CheckIcon,
@@ -14,13 +18,10 @@ import {
RectangleGroupIcon,
TagIcon,
} from "@heroicons/react/24/outline";
-// services
-import issuesServices from "services/issues.service";
// types
import type { IIssueLabels } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
-import { IssueLabelsList } from "components/ui";
type Props = {
setIsOpen: React.Dispatch>;
From 9c85704be3c687ae02340ab688e1583d8c2e022f Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Fri, 23 Jun 2023 11:09:03 +0530
Subject: [PATCH 36/59] style: profile settings, activity, preference page
padding (#1335)
---
apps/app/pages/[workspaceSlug]/me/profile/activity.tsx | 6 +++---
apps/app/pages/[workspaceSlug]/me/profile/index.tsx | 6 +++---
apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx | 6 +++---
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx b/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx
index db9b1e55b..3fe99df92 100644
--- a/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx
+++ b/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx
@@ -4,6 +4,7 @@ import useSWR from "swr";
import userService from "services/user.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
+import SettingsNavbar from "layouts/settings-navbar";
// components
import { Feeds } from "components/core";
// ui
@@ -11,7 +12,6 @@ import { Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// fetch-keys
import { USER_ACTIVITY } from "constants/fetch-keys";
-import SettingsNavbar from "layouts/settings-navbar";
const ProfileActivity = () => {
const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity());
@@ -24,8 +24,8 @@ const ProfileActivity = () => {
}
>
-
-
+
+
Profile Settings
diff --git a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx
index ce589c41d..247ef7e03 100644
--- a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx
+++ b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx
@@ -10,6 +10,7 @@ import useUserAuth from "hooks/use-user-auth";
import useToast from "hooks/use-toast";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
+import SettingsNavbar from "layouts/settings-navbar";
// components
import { ImageUploadModal } from "components/core";
// ui
@@ -22,7 +23,6 @@ import type { NextPage } from "next";
import type { IUser } from "types";
// constants
import { USER_ROLES } from "constants/workspace";
-import SettingsNavbar from "layouts/settings-navbar";
const defaultValues: Partial = {
avatar: "",
@@ -136,8 +136,8 @@ const Profile: NextPage = () => {
userImage
/>
{myProfile ? (
-
-
+
+
Profile Settings
diff --git a/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx b/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx
index f2fe98f80..abaaefcd8 100644
--- a/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx
+++ b/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx
@@ -25,7 +25,7 @@ const ProfilePreferences = () => {
if (myProfile?.theme.palette) setPreLoadedData(myProfile.theme);
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
}
- }, [myProfile, theme]);
+ }, [myProfile, theme, customThemeSelectorOptions]);
return (
{
}
>
{myProfile ? (
-
-
+
+
Profile Settings
From d3c56c1765eead777e977d66768e0c076267ef08 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Fri, 23 Jun 2023 11:09:34 +0530
Subject: [PATCH 37/59] fix: cycle stats empty state (#1338)
* chore: active cycle percentage fix
* fix: progress chart x-axis values
---
.../core/sidebar/progress-chart.tsx | 25 ++-
.../cycles/active-cycle-details.tsx | 173 +++++++++---------
.../components/cycles/active-cycle-stats.tsx | 153 ++++++++--------
.../components/cycles/single-cycle-list.tsx | 18 +-
apps/app/helpers/date-time.helper.ts | 7 +-
5 files changed, 211 insertions(+), 165 deletions(-)
diff --git a/apps/app/components/core/sidebar/progress-chart.tsx b/apps/app/components/core/sidebar/progress-chart.tsx
index e6349bfe5..47af406cc 100644
--- a/apps/app/components/core/sidebar/progress-chart.tsx
+++ b/apps/app/components/core/sidebar/progress-chart.tsx
@@ -3,7 +3,7 @@ import React from "react";
// ui
import { LineGraph } from "components/ui";
// helpers
-import { renderShortNumericDateFormat } from "helpers/date-time.helper";
+import { getDatesInRange, renderShortNumericDateFormat } from "helpers/date-time.helper";
//types
import { TCompletionChartDistribution } from "types";
@@ -46,6 +46,27 @@ const ProgressChart: React.FC = ({ distribution, startDate, endDate, tota
pending: distribution[key],
}));
+ const generateXAxisTickValues = () => {
+ const dates = getDatesInRange(startDate, endDate);
+
+ const maxDates = 4;
+ const totalDates = dates.length;
+
+ if (totalDates <= maxDates) return dates;
+ else {
+ const interval = Math.ceil(totalDates / maxDates);
+ const limitedDates = [];
+
+ for (let i = 0; i < totalDates; i += interval)
+ limitedDates.push(renderShortNumericDateFormat(dates[i]));
+
+ if (!limitedDates.includes(renderShortNumericDateFormat(dates[totalDates - 1])))
+ limitedDates.push(renderShortNumericDateFormat(dates[totalDates - 1]));
+
+ return limitedDates;
+ }
+ };
+
return (
= ({ distribution, startDate, endDate, tota
]}
layers={["grid", "markers", "areas", DashedLine, "slices", "points", "axes", "legends"]}
axisBottom={{
- tickValues: chartData.map((item, index) => (index % 2 === 0 ? item.currentDate : "")),
+ tickValues: generateXAxisTickValues(),
}}
enablePoints={false}
enableArea
diff --git a/apps/app/components/cycles/active-cycle-details.tsx b/apps/app/components/cycles/active-cycle-details.tsx
index 5a0c1cb58..b2f27d037 100644
--- a/apps/app/components/cycles/active-cycle-details.tsx
+++ b/apps/app/components/cycles/active-cycle-details.tsx
@@ -395,82 +395,87 @@ export const ActiveCycleDetails: React.FC = () => {
High Priority Issues
{issues ? (
- issues.map((issue) => (
-
-
-
+ issues.length > 0 ? (
+ issues.map((issue) => (
+
+
+
+
+
+ {issue.project_detail?.identifier}-{issue.sequence_id}
+
+
+
-
- {issue.project_detail?.identifier}-{issue.sequence_id}
+
+ {truncateText(issue.name, 30)}
-
-
- {truncateText(issue.name, 30)}
-
-
-
-
-
-
- {getPriorityIcon(issue.priority, "text-sm")}
-
- {issue.label_details.length > 0 ? (
-
- {issue.label_details.map((label) => (
-
-
- {label.name}
-
- ))}
+
+
+ {getPriorityIcon(issue.priority, "text-sm")}
- ) : (
- ""
- )}
-
- {issue.assignees &&
- issue.assignees.length > 0 &&
- Array.isArray(issue.assignees) ? (
-
-
+ {issue.label_details.length > 0 ? (
+
+ {issue.label_details.map((label) => (
+
+
+ {label.name}
+
+ ))}
) : (
""
)}
+
+ {issue.assignees &&
+ issue.assignees.length > 0 &&
+ Array.isArray(issue.assignees) ? (
+
+ ) : (
+ ""
+ )}
+
+ ))
+ ) : (
+
+ No issues present in the cycle.
- ))
+ )
) : (
@@ -481,27 +486,29 @@ export const ActiveCycleDetails: React.FC = () => {
-
-
-
issue?.state_detail?.group === "completed")
- ?.length /
- issues.length) *
- 100 ?? 0
- }%`,
- }}
- />
+ {issues && issues.length > 0 && (
+
+
+
issue?.state_detail?.group === "completed")
+ ?.length /
+ issues.length) *
+ 100 ?? 0
+ }%`,
+ }}
+ />
+
+
+ {issues?.filter((issue) => issue?.state_detail?.group === "completed")?.length} of{" "}
+ {issues?.length}
+
-
- {issues?.filter((issue) => issue?.state_detail?.group === "completed")?.length} of{" "}
- {issues?.length}
-
-
+ )}
diff --git a/apps/app/components/cycles/active-cycle-stats.tsx b/apps/app/components/cycles/active-cycle-stats.tsx
index a01293d43..30b69ffaf 100644
--- a/apps/app/components/cycles/active-cycle-stats.tsx
+++ b/apps/app/components/cycles/active-cycle-stats.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { Fragment } from "react";
// headless ui
import { Tab } from "@headlessui/react";
@@ -32,6 +32,7 @@ export const ActiveCycleProgressStats: React.FC
= ({ cycle }) => {
return (
{
switch (i) {
@@ -68,81 +69,87 @@ export const ActiveCycleProgressStats: React.FC = ({ cycle }) => {
Labels
-
-
- {cycle.distribution.assignees.map((assignee, index) => {
- if (assignee.assignee_id)
- return (
-
-
- {assignee.first_name}
-
- }
- completed={assignee.completed_issues}
- total={assignee.total_issues}
- />
- );
- else
- return (
-
-
-
0 ? (
+
+
+ {cycle.distribution.assignees.map((assignee, index) => {
+ if (assignee.assignee_id)
+ return (
+
+
+ {assignee.first_name}
- No assignee
-
- }
- completed={assignee.completed_issues}
- total={assignee.total_issues}
- />
- );
- })}
-
-
- {cycle.distribution.labels.map((label, index) => (
-
-
- {label.label_name ?? "No labels"}
-
- }
- completed={label.completed_issues}
- total={label.total_issues}
- />
- ))}
-
-
+ );
+ else
+ return (
+
+
+
+
+ No assignee
+
+ }
+ completed={assignee.completed_issues}
+ total={assignee.total_issues}
+ />
+ );
+ })}
+
+
+ {cycle.distribution.labels.map((label, index) => (
+
+
+ {label.label_name ?? "No labels"}
+
+ }
+ completed={label.completed_issues}
+ total={label.total_issues}
+ />
+ ))}
+
+
+ ) : (
+
+ No issues present in the cycle.
+
+ )}
);
};
diff --git a/apps/app/components/cycles/single-cycle-list.tsx b/apps/app/components/cycles/single-cycle-list.tsx
index f8a1fbf28..d957b5ab7 100644
--- a/apps/app/components/cycles/single-cycle-list.tsx
+++ b/apps/app/components/cycles/single-cycle-list.tsx
@@ -282,12 +282,18 @@ export const SingleCycleList: React.FC
= ({
>
{cycleStatus === "current" ? (
-
-
- {Math.floor((cycle.completed_issues / cycle.total_issues) * 100)} %
-
+ {cycle.total_issues > 0 ? (
+ <>
+
+
+ {Math.floor((cycle.completed_issues / cycle.total_issues) * 100)} %
+
+ >
+ ) : (
+ No issues present
+ )}
) : cycleStatus === "upcoming" ? (
diff --git a/apps/app/helpers/date-time.helper.ts b/apps/app/helpers/date-time.helper.ts
index 6333e7558..d462474a4 100644
--- a/apps/app/helpers/date-time.helper.ts
+++ b/apps/app/helpers/date-time.helper.ts
@@ -25,13 +25,18 @@ export const findHowManyDaysLeft = (date: string | Date) => {
return Math.ceil(timeDiff / (1000 * 3600 * 24));
};
-export const getDatesInRange = (startDate: Date, endDate: Date) => {
+export const getDatesInRange = (startDate: string | Date, endDate: string | Date) => {
+ startDate = new Date(startDate);
+ endDate = new Date(endDate);
+
const date = new Date(startDate.getTime());
const dates = [];
+
while (date <= endDate) {
dates.push(new Date(date));
date.setDate(date.getDate() + 1);
}
+
return dates;
};
From 8982452500ac046a16f83ec7a559720485b56f3d Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Fri, 23 Jun 2023 11:09:47 +0530
Subject: [PATCH 38/59] fix: issue title breaking in issue card (#1339)
---
apps/app/components/core/board-view/single-issue.tsx | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx
index 003be9e94..3571efa41 100644
--- a/apps/app/components/core/board-view/single-issue.tsx
+++ b/apps/app/components/core/board-view/single-issue.tsx
@@ -338,11 +338,8 @@ export const SingleBoardIssue: React.FC = ({
{issue.project_detail.identifier}-{issue.sequence_id}
)}
-
- {truncateText(issue.name, 100)}
+
+ {truncateText(issue.name, 120)}
From 048a01dbf35e1a8c4a22fba2c9aa35a24a0ce5df Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Fri, 23 Jun 2023 11:09:57 +0530
Subject: [PATCH 39/59] fix: description not loading while editing an issue
(#1349)
---
apps/app/components/core/issues-view.tsx | 1 -
apps/app/components/issues/form.tsx | 2 +-
apps/app/components/issues/modal.tsx | 2 +-
3 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx
index b90439b9d..bcc8e99ce 100644
--- a/apps/app/components/core/issues-view.tsx
+++ b/apps/app/components/core/issues-view.tsx
@@ -451,7 +451,6 @@ export const IssuesView: React.FC = ({
/>
setEditIssueModal(false)}
data={issueToEdit}
/>
diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx
index e59e9c2cb..5db688283 100644
--- a/apps/app/components/issues/form.tsx
+++ b/apps/app/components/issues/form.tsx
@@ -148,7 +148,7 @@ export const IssueForm: FC = ({
setValue,
setFocus,
} = useForm({
- defaultValues,
+ defaultValues: initialData ?? defaultValues,
reValidateMode: "onChange",
});
diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx
index 5ef39e13a..5142c6a32 100644
--- a/apps/app/components/issues/modal.tsx
+++ b/apps/app/components/issues/modal.tsx
@@ -328,7 +328,7 @@ export const CreateUpdateIssueModal: React.FC = ({
Date: Fri, 23 Jun 2023 11:10:09 +0530
Subject: [PATCH 40/59] fix: bulk delete issues mutation (#1351)
---
.../core/bulk-delete-issues-modal.tsx | 134 ++++++++++++------
1 file changed, 90 insertions(+), 44 deletions(-)
diff --git a/apps/app/components/core/bulk-delete-issues-modal.tsx b/apps/app/components/core/bulk-delete-issues-modal.tsx
index 603efe8e3..a2c6310e9 100644
--- a/apps/app/components/core/bulk-delete-issues-modal.tsx
+++ b/apps/app/components/core/bulk-delete-issues-modal.tsx
@@ -12,6 +12,8 @@ import { Combobox, Dialog, Transition } from "@headlessui/react";
import issuesServices from "services/issues.service";
// hooks
import useToast from "hooks/use-toast";
+import useIssuesView from "hooks/use-issues-view";
+import useCalendarIssuesView from "hooks/use-calendar-issues-view";
// ui
import { DangerButton, SecondaryButton } from "components/ui";
// icons
@@ -20,7 +22,15 @@ import { LayerDiagonalIcon } from "components/icons";
// types
import { ICurrentUserResponse, IIssue } from "types";
// fetch keys
-import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
+import {
+ CYCLE_DETAILS,
+ CYCLE_ISSUES_WITH_PARAMS,
+ MODULE_DETAILS,
+ MODULE_ISSUES_WITH_PARAMS,
+ PROJECT_ISSUES_LIST,
+ PROJECT_ISSUES_LIST_WITH_PARAMS,
+ VIEW_ISSUES,
+} from "constants/fetch-keys";
type FormInput = {
delete_issue_ids: string[];
@@ -36,7 +46,7 @@ export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen, user
const [query, setQuery] = useState("");
const router = useRouter();
- const { workspaceSlug, projectId } = router.query;
+ const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { data: issues } = useSWR(
workspaceSlug && projectId
@@ -48,6 +58,9 @@ export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen, user
);
const { setToastAlert } = useToast();
+ const { issueView, params } = useIssuesView();
+ const { params: calendarParams } = useCalendarIssuesView();
+ const { order_by, group_by, ...viewGanttParams } = params;
const {
handleSubmit,
@@ -61,6 +74,81 @@ export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen, user
},
});
+ const handleClose = () => {
+ setIsOpen(false);
+ setQuery("");
+ reset();
+ };
+
+ const handleDelete: SubmitHandler = async (data) => {
+ if (!workspaceSlug || !projectId) return;
+
+ if (!data.delete_issue_ids || data.delete_issue_ids.length === 0) {
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Please select at least one issue.",
+ });
+ return;
+ }
+
+ if (!Array.isArray(data.delete_issue_ids)) data.delete_issue_ids = [data.delete_issue_ids];
+
+ const calendarFetchKey = cycleId
+ ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
+ : moduleId
+ ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
+ : viewId
+ ? VIEW_ISSUES(viewId.toString(), calendarParams)
+ : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", calendarParams);
+
+ const ganttFetchKey = cycleId
+ ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
+ : moduleId
+ ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString())
+ : viewId
+ ? VIEW_ISSUES(viewId.toString(), viewGanttParams)
+ : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "");
+
+ await issuesServices
+ .bulkDeleteIssues(
+ workspaceSlug as string,
+ projectId as string,
+ {
+ issue_ids: data.delete_issue_ids,
+ },
+ user
+ )
+ .then(() => {
+ setToastAlert({
+ type: "success",
+ title: "Success!",
+ message: "Issues deleted successfully!",
+ });
+
+ if (issueView === "calendar") mutate(calendarFetchKey);
+ else if (issueView === "gantt_chart") mutate(ganttFetchKey);
+ else {
+ if (cycleId) {
+ mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params));
+ mutate(CYCLE_DETAILS(cycleId.toString()));
+ } else if (moduleId) {
+ mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
+ mutate(MODULE_DETAILS(moduleId as string));
+ } else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params));
+ }
+
+ handleClose();
+ })
+ .catch(() =>
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Something went wrong. Please try again.",
+ })
+ );
+ };
+
const filteredIssues: IIssue[] =
query === ""
? issues ?? []
@@ -72,48 +160,6 @@ export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen, user
.includes(query.toLowerCase())
) ?? [];
- const handleClose = () => {
- setIsOpen(false);
- setQuery("");
- reset();
- };
-
- const handleDelete: SubmitHandler = async (data) => {
- if (!data.delete_issue_ids || data.delete_issue_ids.length === 0) {
- setToastAlert({
- title: "Error",
- type: "error",
- message: "Please select atleast one issue",
- });
- return;
- }
-
- if (!Array.isArray(data.delete_issue_ids)) data.delete_issue_ids = [data.delete_issue_ids];
-
- if (workspaceSlug && projectId) {
- await issuesServices
- .bulkDeleteIssues(
- workspaceSlug as string,
- projectId as string,
- {
- issue_ids: data.delete_issue_ids,
- },
- user
- )
- .then((res) => {
- setToastAlert({
- title: "Success",
- type: "success",
- message: res.message,
- });
- handleClose();
- })
- .catch((e) => {
- console.log(e);
- });
- }
- };
-
return (
setQuery("")} appear>
From 62392be5a3aa018454f10e7786bceee7de25a5c0 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Fri, 23 Jun 2023 11:10:19 +0530
Subject: [PATCH 41/59] chore: info icon for activity graph (#1353)
---
.../components/workspace/activity-graph.tsx | 153 +++++++++++++++---
.../app/components/workspace/issues-stats.tsx | 14 +-
2 files changed, 142 insertions(+), 25 deletions(-)
diff --git a/apps/app/components/workspace/activity-graph.tsx b/apps/app/components/workspace/activity-graph.tsx
index ec8e1dfd4..1f9db203d 100644
--- a/apps/app/components/workspace/activity-graph.tsx
+++ b/apps/app/components/workspace/activity-graph.tsx
@@ -1,34 +1,141 @@
+import { useEffect, useRef, useState } from "react";
+
// ui
-import { CalendarGraph } from "components/ui";
+import { Tooltip } from "components/ui";
// helpers
-import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
+import { renderDateFormat, renderShortNumericDateFormat } from "helpers/date-time.helper";
// types
import { IUserActivity } from "types";
+// constants
+import { DAYS, MONTHS } from "constants/project";
type Props = {
activities: IUserActivity[] | undefined;
};
-export const ActivityGraph: React.FC = ({ activities }) => (
- ({
- day: activity.created_date,
- value: activity.activity_count,
- })) ?? []
+export const ActivityGraph: React.FC = ({ activities }) => {
+ const ref = useRef(null);
+
+ const [width, setWidth] = useState(0);
+
+ const today = new Date();
+ const lastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1);
+ const twoMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 2, 1);
+ const threeMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 3, 1);
+ const fourMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 4, 1);
+ const fiveMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 5, 1);
+
+ const recentMonths = [
+ fiveMonthsAgo,
+ fourMonthsAgo,
+ threeMonthsAgo,
+ twoMonthsAgo,
+ lastMonth,
+ today,
+ ];
+
+ const getDatesOfMonth = (dateOfMonth: Date) => {
+ const month = dateOfMonth.getMonth();
+ const year = dateOfMonth.getFullYear();
+
+ const dates = [];
+ const date = new Date(year, month, 1);
+
+ while (date.getMonth() === month && date < new Date()) {
+ dates.push(renderDateFormat(new Date(date)));
+ date.setDate(date.getDate() + 1);
}
- from={activities?.length ? activities[0].created_date : new Date()}
- to={activities?.length ? activities[activities.length - 1].created_date : new Date()}
- height="200px"
- margin={{ bottom: 0, left: 10, right: 10, top: 0 }}
- tooltip={(datum) => (
-
-
{renderShortDateWithYearFormat(datum.day)}: {" "}
- {datum.value}
+
+ return dates;
+ };
+
+ const recentDates = [
+ ...getDatesOfMonth(recentMonths[0]),
+ ...getDatesOfMonth(recentMonths[1]),
+ ...getDatesOfMonth(recentMonths[2]),
+ ...getDatesOfMonth(recentMonths[3]),
+ ...getDatesOfMonth(recentMonths[4]),
+ ...getDatesOfMonth(recentMonths[5]),
+ ];
+
+ const activitiesIntensity = (activityCount: number) => {
+ if (activityCount <= 3) return "opacity-20";
+ else if (activityCount > 3 && activityCount <= 6) return "opacity-40";
+ else if (activityCount > 6 && activityCount <= 9) return "opacity-80";
+ else return "";
+ };
+
+ const addPaddingTiles = () => {
+ const firstDateDay = new Date(recentDates[0]).getDay();
+
+ for (let i = 0; i < firstDateDay; i++) recentDates.unshift("");
+ };
+ addPaddingTiles();
+
+ useEffect(() => {
+ if (!ref.current) return;
+
+ setWidth(ref.current.offsetWidth);
+ }, [ref]);
+
+ return (
+
+
+
+ {DAYS.map((day, index) => (
+
+ {index % 2 === 0 && day.substring(0, 3)}
+
+ ))}
+
+
+
+ {recentMonths.map((month, index) => (
+
+ {MONTHS[month.getMonth()].substring(0, 3)}
+
+ ))}
+
+
+ {recentDates.map((date, index) => {
+ const isActive = activities?.find((a) => a.created_date === date);
+
+ return (
+
+
+
+ );
+ })}
+
+
+ Less
+
+
+
+
+
+ More
+
+
- )}
- theme={{
- background: "rgb(var(--color-bg-base))",
- }}
- />
-);
+
+ );
+};
diff --git a/apps/app/components/workspace/issues-stats.tsx b/apps/app/components/workspace/issues-stats.tsx
index bc7f0364f..8e108a676 100644
--- a/apps/app/components/workspace/issues-stats.tsx
+++ b/apps/app/components/workspace/issues-stats.tsx
@@ -1,7 +1,9 @@
// components
import { ActivityGraph } from "components/workspace";
// ui
-import { Loader } from "components/ui";
+import { Loader, Tooltip } from "components/ui";
+// icons
+import { InformationCircleIcon } from "@heroicons/react/24/outline";
// types
import { IUserWorkspaceDashboard } from "types";
@@ -66,7 +68,15 @@ export const IssuesStats: React.FC
= ({ data }) => (
-
Activity Graph
+
+ Activity Graph
+
+
+
+
From 41b7544cfce1fa3ceb7bf24d2579490a2cf0ea36 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Fri, 23 Jun 2023 13:18:38 +0530
Subject: [PATCH 42/59] feat: search endpoint (#1317)
* feat: search endpoint for parent issue selection
* feat: blocker and blocked by search endpoint
* refactor: blocker and blocked by components and types
* refactor: blocker and blocked by components, feeat: cycle and module new search endpoints
* chore: sub-issues param change
* style: show selected issues list
---
.../command-palette/command-pallette.tsx | 2 +-
.../core/existing-issues-list-modal.tsx | 308 +++++++++-------
.../issues/parent-issues-list-modal.tsx | 278 +++++++--------
apps/app/components/issues/select/parent.tsx | 1 -
.../issues/sidebar-select/blocked.tsx | 330 +++++------------
.../issues/sidebar-select/blocker.tsx | 331 +++++-------------
.../issues/sidebar-select/parent.tsx | 7 +-
apps/app/components/issues/sidebar.tsx | 15 +-
.../app/components/issues/sub-issues-list.tsx | 29 +-
.../projects/[projectId]/cycles/[cycleId].tsx | 54 ++-
.../projects/[projectId]/issues/[issueId].tsx | 16 +-
.../[projectId]/modules/[moduleId].tsx | 37 +-
apps/app/services/project.service.ts | 16 +
apps/app/types/issues.d.ts | 18 +-
apps/app/types/projects.d.ts | 22 ++
15 files changed, 584 insertions(+), 880 deletions(-)
diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx
index ffbe67ca5..cb2330b24 100644
--- a/apps/app/components/command-palette/command-pallette.tsx
+++ b/apps/app/components/command-palette/command-pallette.tsx
@@ -81,7 +81,7 @@ export const CommandPalette: React.FC = () => {
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false);
- const [searchTerm, setSearchTerm] = React.useState
("");
+ const [searchTerm, setSearchTerm] = useState("");
const [results, setResults] = useState({
results: {
workspace: [],
diff --git a/apps/app/components/core/existing-issues-list-modal.tsx b/apps/app/components/core/existing-issues-list-modal.tsx
index fe6f1e1b4..e9a004a5e 100644
--- a/apps/app/components/core/existing-issues-list-modal.tsx
+++ b/apps/app/components/core/existing-issues-list-modal.tsx
@@ -1,23 +1,24 @@
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
-// react-hook-form
-import { Controller, SubmitHandler, useForm } from "react-hook-form";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
+// services
+import projectService from "services/project.service";
// hooks
import useToast from "hooks/use-toast";
import useIssuesView from "hooks/use-issues-view";
+import useDebounce from "hooks/use-debounce";
// ui
-import { PrimaryButton, SecondaryButton } from "components/ui";
+import { Loader, PrimaryButton, SecondaryButton } from "components/ui";
// icons
-import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
+import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { LayerDiagonalIcon } from "components/icons";
// types
-import { IIssue } from "types";
+import { ISearchIssueResponse, TProjectIssuesSearchParams } from "types";
// fetch-keys
import {
CYCLE_DETAILS,
@@ -26,27 +27,30 @@ import {
MODULE_ISSUES_WITH_PARAMS,
} from "constants/fetch-keys";
-type FormInput = {
- issues: string[];
-};
-
type Props = {
isOpen: boolean;
handleClose: () => void;
- issues: IIssue[];
- handleOnSubmit: any;
+ searchParams: Partial;
+ handleOnSubmit: (data: ISearchIssueResponse[]) => Promise;
};
export const ExistingIssuesListModal: React.FC = ({
isOpen,
handleClose: onClose,
- issues,
+ searchParams,
handleOnSubmit,
}) => {
- const [query, setQuery] = useState("");
+ const [searchTerm, setSearchTerm] = useState("");
+ const [issues, setIssues] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isSearching, setIsSearching] = useState(false);
+ const [selectedIssues, setSelectedIssues] = useState([]);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
const router = useRouter();
- const { cycleId, moduleId } = router.query;
+ const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { setToastAlert } = useToast();
@@ -54,23 +58,12 @@ export const ExistingIssuesListModal: React.FC = ({
const handleClose = () => {
onClose();
- setQuery("");
- reset();
+ setSearchTerm("");
+ setSelectedIssues([]);
};
- const {
- handleSubmit,
- reset,
- control,
- formState: { isSubmitting },
- } = useForm({
- defaultValues: {
- issues: [],
- },
- });
-
- const onSubmit: SubmitHandler = async (data) => {
- if (!data.issues || data.issues.length === 0) {
+ const onSubmit = async () => {
+ if (selectedIssues.length === 0) {
setToastAlert({
type: "error",
title: "Error!",
@@ -80,11 +73,15 @@ export const ExistingIssuesListModal: React.FC = ({
return;
}
- await handleOnSubmit(data);
+ setIsSubmitting(true);
+
+ await handleOnSubmit(selectedIssues).finally(() => setIsSubmitting(false));
+
if (cycleId) {
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
mutate(CYCLE_DETAILS(cycleId as string));
}
+
if (moduleId) {
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
mutate(MODULE_DETAILS(moduleId as string));
@@ -95,18 +92,45 @@ export const ExistingIssuesListModal: React.FC = ({
setToastAlert({
title: "Success",
type: "success",
- message: `Issue${data.issues.length > 1 ? "s" : ""} added successfully`,
+ message: `Issue${selectedIssues.length > 1 ? "s" : ""} added successfully`,
});
};
- const filteredIssues: IIssue[] =
- query === ""
- ? issues ?? []
- : issues.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? [];
+ useEffect(() => {
+ if (!workspaceSlug || !projectId) return;
+
+ setIsLoading(true);
+
+ if (debouncedSearchTerm) {
+ setIsSearching(true);
+
+ projectService
+ .projectIssuesSearch(workspaceSlug as string, projectId as string, {
+ search: debouncedSearchTerm,
+ ...searchParams,
+ })
+ .then((res) => {
+ setIssues(res);
+ })
+ .finally(() => {
+ setIsLoading(false);
+ setIsSearching(false);
+ });
+ } else {
+ setIssues([]);
+ setIsLoading(false);
+ setIsSearching(false);
+ }
+ }, [debouncedSearchTerm, workspaceSlug, projectId, searchParams]);
return (
<>
- setQuery("")} appear>
+ setSearchTerm("")}
+ appear
+ >
= ({
leaveTo="opacity-0 scale-95"
>
-
- (
-
-
-
- setQuery(e.target.value)}
- />
-
+ {
+ if (selectedIssues.some((i) => i.id === val.id))
+ setSelectedIssues((prevData) => prevData.filter((i) => i.id !== val.id));
+ else setSelectedIssues((prevData) => [...prevData, val]);
+ }}
+ >
+
+
+ setSearchTerm(e.target.value)}
+ />
+
-
- {filteredIssues.length > 0 ? (
-
- {query === "" && (
-
- Select issues to add
-
- )}
-
-
- ) : (
-
-
-
- No issues found. Create a new issue with{" "}
- C
- .
-
-
- )}
-
-
+
+ {selectedIssues.length > 0 ? (
+
+ {selectedIssues.map((issue) => (
+
+ {issue.project__identifier}-{issue.sequence_id}
+
+ setSelectedIssues((prevData) =>
+ prevData.filter((i) => i.id !== issue.id)
+ )
+ }
+ >
+
+
+
+ ))}
+
+ ) : (
+
+ No issues selected
+
)}
- />
- {filteredIssues.length > 0 && (
-
-
Cancel
-
- {isSubmitting ? "Adding..." : "Add selected issues"}
-
-
- )}
-
+
+
+
+ {debouncedSearchTerm !== "" && (
+
+ Search results for{" "}
+
+ {'"'}
+ {debouncedSearchTerm}
+ {'"'}
+ {" "}
+ in project:
+
+ )}
+
+ {!isLoading &&
+ issues.length === 0 &&
+ searchTerm !== "" &&
+ debouncedSearchTerm !== "" && (
+
+
+
+ No issues found. Create a new issue with{" "}
+
+ C
+
+ .
+
+
+ )}
+
+ {isLoading || isSearching ? (
+
+
+
+
+
+
+ ) : (
+
+ )}
+
+
+ {selectedIssues.length > 0 && (
+
+
Cancel
+
+ {isSubmitting ? "Adding..." : "Add selected issues"}
+
+
+ )}
diff --git a/apps/app/components/issues/parent-issues-list-modal.tsx b/apps/app/components/issues/parent-issues-list-modal.tsx
index 7510d5e75..b93c07d3c 100644
--- a/apps/app/components/issues/parent-issues-list-modal.tsx
+++ b/apps/app/components/issues/parent-issues-list-modal.tsx
@@ -1,23 +1,28 @@
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
+
+import { useRouter } from "next/router";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
-// icons
-import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
-// ui
-import { PrimaryButton, SecondaryButton } from "components/ui";
-// types
-import { IIssue } from "types";
+// services
+import projectService from "services/project.service";
+// hooks
+import useDebounce from "hooks/use-debounce";
+// components
import { LayerDiagonalIcon } from "components/icons";
+// ui
+import { Loader } from "components/ui";
+// icons
+import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
+// types
+import { ISearchIssueResponse } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
value?: any;
onChange: (...event: any[]) => void;
- issues: IIssue[];
- title?: string;
- multiple?: boolean;
+ issueId?: string;
customDisplay?: JSX.Element;
};
@@ -26,28 +31,60 @@ export const ParentIssuesListModal: React.FC = ({
handleClose: onClose,
value,
onChange,
- issues,
- title = "Issues",
- multiple = false,
+ issueId,
customDisplay,
}) => {
- const [query, setQuery] = useState("");
- const [values, setValues] = useState([]);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [issues, setIssues] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isSearching, setIsSearching] = useState(false);
+
+ const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
+
+ const router = useRouter();
+ const { workspaceSlug, projectId } = router.query;
const handleClose = () => {
onClose();
- setQuery("");
- setValues([]);
+ setSearchTerm("");
};
- const filteredIssues: IIssue[] =
- query === ""
- ? issues ?? []
- : issues?.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? [];
+ useEffect(() => {
+ if (!workspaceSlug || !projectId) return;
+
+ setIsLoading(true);
+
+ if (debouncedSearchTerm) {
+ setIsSearching(true);
+
+ projectService
+ .projectIssuesSearch(workspaceSlug as string, projectId as string, {
+ search: debouncedSearchTerm,
+ parent: true,
+ issue_id: issueId,
+ })
+ .then((res) => {
+ setIssues(res);
+ })
+ .finally(() => {
+ setIsLoading(false);
+ setIsSearching(false);
+ });
+ } else {
+ setIssues([]);
+ setIsLoading(false);
+ setIsSearching(false);
+ }
+ }, [debouncedSearchTerm, workspaceSlug, projectId, issueId]);
return (
<>
- setQuery("")} appear>
+ setSearchTerm("")}
+ appear
+ >
= ({
leaveTo="opacity-0 scale-95"
>
- {multiple ? (
- <>
- ({})} multiple>
-
-
- setQuery(e.target.value)}
- displayValue={() => ""}
- />
-
- {customDisplay && {customDisplay}
}
-
- {filteredIssues.length > 0 && (
-
- {query === "" && (
- {title}
- )}
-
-
- )}
-
+
+
+
+ setSearchTerm(e.target.value)}
+ displayValue={() => ""}
+ />
+
+ {customDisplay && {customDisplay}
}
+
+ {debouncedSearchTerm !== "" && (
+
+ Search results for{" "}
+
+ {'"'}
+ {debouncedSearchTerm}
+ {'"'}
+ {" "}
+ in project:
+
+ )}
- {query !== "" && filteredIssues.length === 0 && (
-
-
-
- We couldn{"'"}t find any issue with that term. Please try again.
-
-
- )}
-
-
-
Cancel
-
onChange(values)}>Add issues
-
- >
- ) : (
-
-
-
- setQuery(e.target.value)}
- displayValue={() => ""}
- />
-
- {customDisplay && {customDisplay}
}
-
- {filteredIssues.length > 0 ? (
-
- {query === "" && (
- {title}
- )}
-
- {filteredIssues.map((issue) => (
-
- `flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${
- active ? "bg-brand-surface-2 text-brand-base" : ""
- } ${selected ? "text-brand-base" : ""}`
- }
- onClick={handleClose}
- >
- <>
-
-
- {issue.project_detail?.identifier}-{issue.sequence_id}
- {" "}
- {issue.name}
- >
-
- ))}
-
-
- ) : (
+ {!isLoading &&
+ issues.length === 0 &&
+ searchTerm !== "" &&
+ debouncedSearchTerm !== "" && (
@@ -208,9 +152,45 @@ export const ParentIssuesListModal: React.FC = ({
)}
-
-
- )}
+
+ {isLoading || isSearching ? (
+
+
+
+
+
+
+ ) : (
+ 0 ? "p-2" : ""}`}>
+ {issues.map((issue) => (
+
+ `flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${
+ active ? "bg-brand-surface-2 text-brand-base" : ""
+ } ${selected ? "text-brand-base" : ""}`
+ }
+ onClick={handleClose}
+ >
+ <>
+
+
+ {issue.project__identifier}-{issue.sequence_id}
+ {" "}
+ {issue.name}
+ >
+
+ ))}
+
+ )}
+
+
diff --git a/apps/app/components/issues/select/parent.tsx b/apps/app/components/issues/select/parent.tsx
index c04e89b92..d73cd4e73 100644
--- a/apps/app/components/issues/select/parent.tsx
+++ b/apps/app/components/issues/select/parent.tsx
@@ -21,7 +21,6 @@ export const IssueParentSelect: React.FC = ({ control, isOpen, setIsOpen,
isOpen={isOpen}
handleClose={() => setIsOpen(false)}
onChange={onChange}
- issues={issues}
/>
)}
/>
diff --git a/apps/app/components/issues/sidebar-select/blocked.tsx b/apps/app/components/issues/sidebar-select/blocked.tsx
index c07f80817..de8985792 100644
--- a/apps/app/components/issues/sidebar-select/blocked.tsx
+++ b/apps/app/components/issues/sidebar-select/blocked.tsx
@@ -3,299 +3,135 @@ import React, { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
-import useSWR from "swr";
-
// react-hook-form
-import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form";
-// headless ui
-import { Combobox, Dialog, Transition } from "@headlessui/react";
+import { UseFormWatch } from "react-hook-form";
// hooks
import useToast from "hooks/use-toast";
-// services
-import issuesService from "services/issues.service";
-// ui
-import { PrimaryButton, SecondaryButton } from "components/ui";
+import useProjectDetails from "hooks/use-project-details";
+// components
+import { ExistingIssuesListModal } from "components/core";
// icons
-import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
-import { BlockedIcon, LayerDiagonalIcon } from "components/icons";
+import { XMarkIcon } from "@heroicons/react/24/outline";
+import { BlockedIcon } from "components/icons";
// types
-import { IIssue, UserAuth } from "types";
-// fetch-keys
-import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
-
-type FormInput = {
- blocked_issue_ids: string[];
-};
+import { BlockeIssue, IIssue, ISearchIssueResponse, UserAuth } from "types";
type Props = {
+ issueId?: string;
submitChanges: (formData: Partial) => void;
- issuesList: IIssue[];
watch: UseFormWatch;
userAuth: UserAuth;
};
export const SidebarBlockedSelect: React.FC = ({
+ issueId,
submitChanges,
- issuesList,
watch,
userAuth,
}) => {
- const [query, setQuery] = useState("");
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
const { setToastAlert } = useToast();
+ const { projectDetails } = useProjectDetails();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
- const { data: issues } = useSWR(
- workspaceSlug && projectId
- ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
- : null,
- workspaceSlug && projectId
- ? () => issuesService.getIssues(workspaceSlug as string, projectId as string)
- : null
- );
-
- const {
- handleSubmit,
- reset,
- watch: watchBlocked,
- setValue,
- } = useForm({
- defaultValues: {
- blocked_issue_ids: [],
- },
- });
-
const handleClose = () => {
setIsBlockedModalOpen(false);
- reset();
};
- const onSubmit: SubmitHandler = (data) => {
- if (!data.blocked_issue_ids || data.blocked_issue_ids.length === 0) {
+ const onSubmit = async (data: ISearchIssueResponse[]) => {
+ if (data.length === 0) {
setToastAlert({
title: "Error",
type: "error",
- message: "Please select atleast one issue",
+ message: "Please select at least one issue",
});
+
return;
}
- if (!Array.isArray(data.blocked_issue_ids)) data.blocked_issue_ids = [data.blocked_issue_ids];
+ const selectedIssues: BlockeIssue[] = data.map((i) => ({
+ blocked_issue_detail: {
+ id: i.id,
+ name: i.name,
+ sequence_id: i.sequence_id,
+ },
+ }));
- const newBlocked = [...watch("blocked_list"), ...data.blocked_issue_ids];
- submitChanges({ blocks_list: newBlocked });
+ const newBlocked = [...watch("blocked_issues"), ...selectedIssues];
+
+ submitChanges({
+ blocked_issues: newBlocked,
+ blocks_list: newBlocked.map((i) => i.blocked_issue_detail?.id ?? ""),
+ });
handleClose();
};
- const filteredIssues: IIssue[] =
- query === ""
- ? issuesList
- : issuesList.filter(
- (issue) =>
- issue.name.toLowerCase().includes(query.toLowerCase()) ||
- `${issue.project_detail.identifier}-${issue.sequence_id}`
- .toLowerCase()
- .includes(query.toLowerCase())
- );
-
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return (
-
-
-
-
- {watch("blocked_list") && watch("blocked_list").length > 0
- ? watch("blocked_list").map((issue) => (
-
- ))
- : null}
+ <>
+
setIsBlockedModalOpen(false)}
+ searchParams={{ blocker_blocked_by: true, issue_id: issueId }}
+ handleOnSubmit={onSubmit}
+ />
+
+
-
setQuery("")}
- appear
- >
-
-
-
-
+
+
+ {watch("blocked_issues") && watch("blocked_issues").length > 0
+ ? watch("blocked_issues").map((issue) => (
+
+
+
+
+ {`${projectDetails?.identifier}-${issue.blocked_issue_detail?.sequence_id}`}
+
+
+
{
+ const updatedBlocked = watch("blocked_issues").filter(
+ (i) => i.blocked_issue_detail?.id !== issue.blocked_issue_detail?.id
+ );
-
-
-
-
- {
- const selectedIssues = watchBlocked("blocked_issue_ids");
- if (selectedIssues.includes(val))
- setValue(
- "blocked_issue_ids",
- selectedIssues.filter((i) => i !== val)
- );
- else setValue("blocked_issue_ids", [...selectedIssues, val]);
+ submitChanges({
+ blocked_issues: updatedBlocked,
+ blocks_list: updatedBlocked.map((i) => i.blocked_issue_detail?.id ?? ""),
+ });
}}
>
-
-
- setQuery(e.target.value)}
- />
-
-
-
- {filteredIssues.length > 0 ? (
-
- {query === "" && (
-
- Select blocked issues
-
- )}
-
-
- ) : (
-
-
-
- No issues found. Create a new issue with{" "}
- C .
-
-
- )}
-
-
-
- {filteredIssues.length > 0 && (
-
-
Cancel
-
- Add selected issues
-
-
- )}
-
-
-
-
-
-
- setIsBlockedModalOpen(true)}
- disabled={isNotAllowed}
- >
- Select issues
-
+
+
+
+ ))
+ : null}
+
+
setIsBlockedModalOpen(true)}
+ disabled={isNotAllowed}
+ >
+ Select issues
+
+
-
+ >
);
};
diff --git a/apps/app/components/issues/sidebar-select/blocker.tsx b/apps/app/components/issues/sidebar-select/blocker.tsx
index aeede09bb..40f1eb10f 100644
--- a/apps/app/components/issues/sidebar-select/blocker.tsx
+++ b/apps/app/components/issues/sidebar-select/blocker.tsx
@@ -3,296 +3,137 @@ import React, { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
-import useSWR from "swr";
-
// react-hook-form
-import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form";
-// headless ui
-import { Combobox, Dialog, Transition } from "@headlessui/react";
+import { UseFormWatch } from "react-hook-form";
// hooks
import useToast from "hooks/use-toast";
-// services
-import issuesServices from "services/issues.service";
-// ui
-import { PrimaryButton, SecondaryButton } from "components/ui";
+import useProjectDetails from "hooks/use-project-details";
+// components
+import { ExistingIssuesListModal } from "components/core";
// icons
-import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
-import { BlockerIcon, LayerDiagonalIcon } from "components/icons";
+import { XMarkIcon } from "@heroicons/react/24/outline";
+import { BlockerIcon } from "components/icons";
// types
-import { IIssue, UserAuth } from "types";
-// fetch-keys
-import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
-
-type FormInput = {
- blocker_issue_ids: string[];
-};
+import { BlockeIssue, IIssue, ISearchIssueResponse, UserAuth } from "types";
type Props = {
+ issueId?: string;
submitChanges: (formData: Partial
) => void;
- issuesList: IIssue[];
watch: UseFormWatch;
userAuth: UserAuth;
};
export const SidebarBlockerSelect: React.FC = ({
+ issueId,
submitChanges,
- issuesList,
watch,
userAuth,
}) => {
- const [query, setQuery] = useState("");
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
const { setToastAlert } = useToast();
+ const { projectDetails } = useProjectDetails();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
- const { data: issues } = useSWR(
- workspaceSlug && projectId
- ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
- : null,
- workspaceSlug && projectId
- ? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
- : null
- );
-
- const {
- handleSubmit,
- reset,
- watch: watchBlocker,
- setValue,
- } = useForm({
- defaultValues: {
- blocker_issue_ids: [],
- },
- });
-
const handleClose = () => {
setIsBlockerModalOpen(false);
- reset();
};
- const onSubmit: SubmitHandler = (data) => {
- if (!data.blocker_issue_ids || data.blocker_issue_ids.length === 0) {
+ const onSubmit = async (data: ISearchIssueResponse[]) => {
+ if (data.length === 0) {
setToastAlert({
- title: "Error",
type: "error",
- message: "Please select atleast one issue",
+ title: "Error!",
+ message: "Please select at least one issue.",
});
+
return;
}
- if (!Array.isArray(data.blocker_issue_ids)) data.blocker_issue_ids = [data.blocker_issue_ids];
+ const selectedIssues: BlockeIssue[] = data.map((i) => ({
+ blocker_issue_detail: {
+ id: i.id,
+ name: i.name,
+ sequence_id: i.sequence_id,
+ },
+ }));
- const newBlockers = [...watch("blockers_list"), ...data.blocker_issue_ids];
- submitChanges({ blockers_list: newBlockers });
+ const newBlockers = [...watch("blocker_issues"), ...selectedIssues];
+
+ submitChanges({
+ blocker_issues: newBlockers,
+ blockers_list: newBlockers.map((i) => i.blocker_issue_detail?.id ?? ""),
+ });
handleClose();
};
- const filteredIssues: IIssue[] =
- query === ""
- ? issuesList
- : issuesList.filter(
- (issue) =>
- issue.name.toLowerCase().includes(query.toLowerCase()) ||
- `${issue.project_detail.identifier}-${issue.sequence_id}`
- .toLowerCase()
- .includes(query.toLowerCase())
- );
-
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return (
-
-
-
-
- {watch("blockers_list") && watch("blockers_list").length > 0
- ? watch("blockers_list").map((issue) => (
-
- ))
- : null}
+ <>
+
setIsBlockerModalOpen(false)}
+ searchParams={{ blocker_blocked_by: true, issue_id: issueId }}
+ handleOnSubmit={onSubmit}
+ />
+
+
-
setQuery("")}
- appear
- >
-
-
-
-
-
-
-
-
- {
- const selectedIssues = watchBlocker("blocker_issue_ids");
- if (selectedIssues.includes(val))
- setValue(
- "blocker_issue_ids",
- selectedIssues.filter((i) => i !== val)
- );
- else setValue("blocker_issue_ids", [...selectedIssues, val]);
- }}
+
+
+ {watch("blocker_issues") && watch("blocker_issues").length > 0
+ ? watch("blocker_issues").map((issue) => (
+
-
-
- setQuery(e.target.value)}
- />
-
-
-
- {filteredIssues.length > 0 ? (
-
- {query === "" && (
-
- Select blocker issues
-
- )}
-
-
- ) : (
-
-
-
- No issues found. Create a new issue with{" "}
- C .
-
-
- )}
-
-
+
+
+ {`${projectDetails?.identifier}-${issue.blocker_issue_detail?.sequence_id}`}
+
+
+
{
+ const updatedBlockers = watch("blocker_issues").filter(
+ (i) => i.blocker_issue_detail?.id !== issue.blocker_issue_detail?.id
+ );
- {filteredIssues.length > 0 && (
-
-
Cancel
-
- Add selected issues
-
-
- )}
-
-
-
-
-
-
setIsBlockerModalOpen(true)}
- disabled={isNotAllowed}
- >
- Select issues
-
+ submitChanges({
+ blocker_issues: updatedBlockers,
+ blockers_list: updatedBlockers.map(
+ (i) => i.blocker_issue_detail?.id ?? ""
+ ),
+ });
+ }}
+ >
+
+
+
+ ))
+ : null}
+
+ setIsBlockerModalOpen(true)}
+ disabled={isNotAllowed}
+ >
+ Select issues
+
+
-
+ >
);
};
diff --git a/apps/app/components/issues/sidebar-select/parent.tsx b/apps/app/components/issues/sidebar-select/parent.tsx
index 92a51269f..9d183d262 100644
--- a/apps/app/components/issues/sidebar-select/parent.tsx
+++ b/apps/app/components/issues/sidebar-select/parent.tsx
@@ -20,7 +20,6 @@ import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
type Props = {
control: Control
;
submitChanges: (formData: Partial) => void;
- issuesList: IIssue[];
customDisplay: JSX.Element;
watch: UseFormWatch;
userAuth: UserAuth;
@@ -29,7 +28,6 @@ type Props = {
export const SidebarParentSelect: React.FC = ({
control,
submitChanges,
- issuesList,
customDisplay,
watch,
userAuth,
@@ -37,7 +35,7 @@ export const SidebarParentSelect: React.FC = ({
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
const router = useRouter();
- const { workspaceSlug, projectId } = router.query;
+ const { workspaceSlug, projectId, issueId } = router.query;
const { data: issues } = useSWR(
workspaceSlug && projectId
@@ -68,8 +66,7 @@ export const SidebarParentSelect: React.FC = ({
submitChanges({ parent: val });
onChange(val);
}}
- issues={issuesList}
- title="Select Parent"
+ issueId={issueId as string}
value={value}
customDisplay={customDisplay}
/>
diff --git a/apps/app/components/issues/sidebar.tsx b/apps/app/components/issues/sidebar.tsx
index cdc815d3c..6f231871b 100644
--- a/apps/app/components/issues/sidebar.tsx
+++ b/apps/app/components/issues/sidebar.tsx
@@ -370,14 +370,6 @@ export const IssueDetailsSidebar: React.FC = ({
- i.id !== issueDetail?.id &&
- i.id !== issueDetail?.parent &&
- i.parent !== issueDetail?.id
- ) ?? []
- }
customDisplay={
issueDetail?.parent_detail ? (
= ({
className="flex items-center gap-2 rounded bg-brand-surface-2 px-3 py-2 text-xs"
onClick={() => submitChanges({ parent: null })}
>
+ Selected: {" "}
{issueDetail.parent_detail?.name}
) : (
-
+
No parent selected
)
@@ -400,16 +393,16 @@ export const IssueDetailsSidebar: React.FC
= ({
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && (
i.id !== issueDetail?.id) ?? []}
watch={watchIssue}
userAuth={memberRole}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && (
i.id !== issueDetail?.id) ?? []}
watch={watchIssue}
userAuth={memberRole}
/>
diff --git a/apps/app/components/issues/sub-issues-list.tsx b/apps/app/components/issues/sub-issues-list.tsx
index 76424767e..ac550348e 100644
--- a/apps/app/components/issues/sub-issues-list.tsx
+++ b/apps/app/components/issues/sub-issues-list.tsx
@@ -21,7 +21,7 @@ import { ChevronRightIcon, PlusIcon, XMarkIcon } from "@heroicons/react/24/outli
// helpers
import { orderArrayBy } from "helpers/array.helper";
// types
-import { ICurrentUserResponse, IIssue, ISubIssueResponse } from "types";
+import { ICurrentUserResponse, IIssue, ISearchIssueResponse, ISubIssueResponse } from "types";
// fetch-keys
import { PROJECT_ISSUES_LIST, SUB_ISSUES } from "constants/fetch-keys";
@@ -58,14 +58,16 @@ export const SubIssuesList: FC = ({ parentIssue, user }) => {
: null
);
- const addAsSubIssue = async (data: { issues: string[] }) => {
+ const addAsSubIssue = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId) return;
+ const payload = {
+ sub_issue_ids: data.map((i) => i.id),
+ };
+
await issuesService
- .addSubIssues(workspaceSlug as string, projectId as string, parentIssue?.id ?? "", {
- sub_issue_ids: data.issues,
- })
- .then((res) => {
+ .addSubIssues(workspaceSlug as string, projectId as string, parentIssue?.id ?? "", payload)
+ .then(() => {
mutate(
SUB_ISSUES(parentIssue?.id ?? ""),
(prevData) => {
@@ -74,10 +76,12 @@ export const SubIssuesList: FC = ({ parentIssue, user }) => {
const stateDistribution = { ...prevData.state_distribution };
- data.issues.forEach((issueId: string) => {
+ payload.sub_issue_ids.forEach((issueId: string) => {
const issue = issues?.find((i) => i.id === issueId);
+
if (issue) {
newSubIssues.push(issue);
+
const issueGroup = issue.state_detail.group;
stateDistribution[issueGroup] = stateDistribution[issueGroup] + 1;
}
@@ -96,7 +100,7 @@ export const SubIssuesList: FC = ({ parentIssue, user }) => {
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
(prevData) =>
(prevData ?? []).map((p) => {
- if (data.issues.includes(p.id))
+ if (payload.sub_issue_ids.includes(p.id))
return {
...p,
parent: parentIssue.id,
@@ -188,14 +192,7 @@ export const SubIssuesList: FC = ({ parentIssue, user }) => {
setSubIssuesListModal(false)}
- issues={
- issues?.filter(
- (i) =>
- (i.parent === "" || i.parent === null) &&
- i.id !== parentIssue?.id &&
- i.id !== parentIssue?.parent
- ) ?? []
- }
+ searchParams={{ sub_issue: true, issue_id: parentIssue?.id }}
handleOnSubmit={addAsSubIssue}
/>
{subIssuesResponse &&
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx
index af9f073a8..78af8e9e1 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx
@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { useRouter } from "next/router";
-import useSWR, { mutate } from "swr";
+import useSWR from "swr";
// icons
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import { CyclesIcon } from "components/icons";
@@ -16,7 +16,6 @@ import { CycleDetailsSidebar } from "components/cycles";
// services
import issuesService from "services/issues.service";
import cycleServices from "services/cycles.service";
-import projectService from "services/project.service";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
@@ -28,14 +27,10 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// helpers
import { truncateText } from "helpers/string.helper";
import { getDateRangeStatus } from "helpers/date-time.helper";
+// types
+import { ISearchIssueResponse } from "types";
// fetch-keys
-import {
- CYCLE_ISSUES,
- CYCLES_LIST,
- PROJECT_DETAILS,
- CYCLE_DETAILS,
- PROJECT_ISSUES_LIST,
-} from "constants/fetch-keys";
+import { CYCLES_LIST, CYCLE_DETAILS } from "constants/fetch-keys";
const SingleCycle: React.FC = () => {
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
@@ -49,13 +44,6 @@ const SingleCycle: React.FC = () => {
const { setToastAlert } = useToast();
- const { data: activeProject } = useSWR(
- workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
- workspaceSlug && projectId
- ? () => projectService.getProject(workspaceSlug as string, projectId as string)
- : null
- );
-
const { data: cycles } = useSWR(
workspaceSlug && projectId ? CYCLES_LIST(projectId as string) : null,
workspaceSlug && projectId
@@ -75,15 +63,6 @@ const SingleCycle: React.FC = () => {
: null
);
- const { data: issues } = useSWR(
- workspaceSlug && projectId
- ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
- : null,
- workspaceSlug && projectId
- ? () => issuesService.getIssues(workspaceSlug as string, projectId as string)
- : null
- );
-
const cycleStatus =
cycleDetails?.start_date && cycleDetails?.end_date
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
@@ -93,14 +72,21 @@ const SingleCycle: React.FC = () => {
setCycleIssuesListModal(true);
};
- const handleAddIssuesToCycle = async (data: { issues: string[] }) => {
+ const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId) return;
+ const payload = {
+ issues: data.map((i) => i.id),
+ };
+
await issuesService
- .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, data, user)
- .then(() => {
- mutate(CYCLE_ISSUES(cycleId as string));
- })
+ .addIssueToCycle(
+ workspaceSlug as string,
+ projectId as string,
+ cycleId as string,
+ payload,
+ user
+ )
.catch(() => {
setToastAlert({
type: "error",
@@ -115,15 +101,15 @@ const SingleCycle: React.FC = () => {
setCycleIssuesListModal(false)}
- issues={issues?.filter((i) => !i.cycle_id) ?? []}
+ searchParams={{ cycle: true }}
handleOnSubmit={handleAddIssuesToCycle}
/>
}
@@ -142,7 +128,7 @@ const SingleCycle: React.FC = () => {
{truncateText(cycle.name, 40)}
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx
index 516a2471b..93bfecfe0 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx
@@ -31,8 +31,6 @@ const defaultValues = {
state: "",
assignees_list: [],
priority: "low",
- blockers_list: [],
- blocked_list: [],
target_date: new Date().toString(),
issue_cycle: null,
issue_module: null,
@@ -65,6 +63,7 @@ const IssueDetailsPage: NextPage = () => {
ISSUE_DETAILS(issueId as string),
(prevData) => {
if (!prevData) return prevData;
+
return {
...prevData,
...formData,
@@ -73,10 +72,13 @@ const IssueDetailsPage: NextPage = () => {
false
);
- const payload = { ...formData };
+ const payload: Partial = {
+ ...formData,
+ };
+
await issuesService
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user)
- .then((res) => {
+ .then(() => {
mutateIssueDetails();
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
})
@@ -93,12 +95,6 @@ const IssueDetailsPage: NextPage = () => {
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
reset({
...issueDetails,
- blockers_list:
- issueDetails.blockers_list ??
- issueDetails.blocker_issues?.map((issue) => issue.blocker_issue_detail?.id),
- blocked_list:
- issueDetails.blocks_list ??
- issueDetails.blocked_issues?.map((issue) => issue.blocked_issue_detail?.id),
assignees_list:
issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id),
labels_list: issueDetails.labels_list ?? issueDetails.labels,
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx
index d63af5865..51b6b7a5b 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx
@@ -2,13 +2,12 @@ import React, { useState } from "react";
import { useRouter } from "next/router";
-import useSWR, { mutate } from "swr";
+import useSWR from "swr";
// icons
import { ArrowLeftIcon, RectangleGroupIcon } from "@heroicons/react/24/outline";
// services
import modulesService from "services/modules.service";
-import issuesService from "services/issues.service";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
@@ -21,20 +20,14 @@ import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "component
import { ModuleDetailsSidebar } from "components/modules";
import { AnalyticsProjectModal } from "components/analytics";
// ui
-import { CustomMenu, EmptySpace, EmptySpaceItem, SecondaryButton, Spinner } from "components/ui";
+import { CustomMenu, SecondaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// helpers
import { truncateText } from "helpers/string.helper";
// types
-import { IModule } from "types";
-
+import { ISearchIssueResponse } from "types";
// fetch-keys
-import {
- MODULE_DETAILS,
- MODULE_ISSUES,
- MODULE_LIST,
- PROJECT_ISSUES_LIST,
-} from "constants/fetch-keys";
+import { MODULE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys";
const SingleModule: React.FC = () => {
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
@@ -48,15 +41,6 @@ const SingleModule: React.FC = () => {
const { setToastAlert } = useToast();
- const { data: issues } = useSWR(
- workspaceSlug && projectId
- ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
- : null,
- workspaceSlug && projectId
- ? () => issuesService.getIssues(workspaceSlug as string, projectId as string)
- : null
- );
-
const { data: modules } = useSWR(
workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null,
workspaceSlug && projectId
@@ -76,7 +60,7 @@ const SingleModule: React.FC = () => {
: null
);
- const { data: moduleDetails } = useSWR(
+ const { data: moduleDetails } = useSWR(
moduleId ? MODULE_DETAILS(moduleId as string) : null,
workspaceSlug && projectId
? () =>
@@ -88,18 +72,21 @@ const SingleModule: React.FC = () => {
: null
);
- const handleAddIssuesToModule = async (data: { issues: string[] }) => {
+ const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId) return;
+ const payload = {
+ issues: data.map((i) => i.id),
+ };
+
await modulesService
.addIssuesToModule(
workspaceSlug as string,
projectId as string,
moduleId as string,
- data,
+ payload,
user
)
- .then(() => mutate(MODULE_ISSUES(moduleId as string)))
.catch(() =>
setToastAlert({
type: "error",
@@ -118,7 +105,7 @@ const SingleModule: React.FC = () => {
setModuleIssuesListModal(false)}
- issues={issues?.filter((i) => !i.module_id) ?? []}
+ searchParams={{ module: true }}
handleOnSubmit={handleAddIssuesToModule}
/>
{
+ return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/search-issues/`, {
+ params,
+ })
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
}
export default new ProjectServices();
diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts
index 0ac0207be..a8881924b 100644
--- a/apps/app/types/issues.d.ts
+++ b/apps/app/types/issues.d.ts
@@ -69,12 +69,8 @@ export interface IIssue {
assignees_list: string[];
attachment_count: number;
attachments: any[];
- blocked_by_issue_details: any[];
- blocked_issue_details: any[];
blocked_issues: BlockeIssue[];
- blocked_list: string[];
blocker_issues: BlockeIssue[];
- blockers: any[];
blockers_list: string[];
blocks_list: string[];
bridge_id?: string | null;
@@ -141,26 +137,14 @@ export interface ISubIssueResponse {
}
export interface BlockeIssue {
- id: string;
blocked_issue_detail?: BlockeIssueDetail;
- created_at: Date;
- updated_at: Date;
- created_by: string;
- updated_by: string;
- project: string;
- workspace: string;
- block: string;
- blocked_by: string;
blocker_issue_detail?: BlockeIssueDetail;
}
export interface BlockeIssueDetail {
id: string;
name: string;
- description: string;
- priority: null;
- start_date: null;
- target_date: null;
+ sequence_id: number;
}
export interface IIssueComment {
diff --git a/apps/app/types/projects.d.ts b/apps/app/types/projects.d.ts
index 51cc4ba9a..c9972bc62 100644
--- a/apps/app/types/projects.d.ts
+++ b/apps/app/types/projects.d.ts
@@ -124,3 +124,25 @@ export interface GithubRepositoriesResponse {
repositories: IGithubRepository[];
total_count: number;
}
+
+export type TProjectIssuesSearchParams = {
+ search: string;
+ parent?: boolean;
+ blocker_blocked_by?: boolean;
+ cycle?: boolean;
+ module?: boolean;
+ sub_issue?: boolean;
+ issue_id?: string;
+};
+
+export interface ISearchIssueResponse {
+ id: string;
+ name: string;
+ project_id: string;
+ project__identifier: string;
+ sequence_id: number;
+ state__color: string;
+ state__group: string;
+ state__name: string;
+ workspace__slug: string;
+}
From 71b2884b570c260118ab8a8f16656d011f819bf8 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Fri, 23 Jun 2023 13:19:26 +0530
Subject: [PATCH 43/59] chore: route to issue after creating it (#1359)
* chore: navigate to newly created inbox issue
* refactor: inbox
* fix: hide ai modal after issue creation
* chore: hide action buttons after acting upon them
* chore: add icon to inbox status
* chore: update inbox status colors
---
.../app/components/inbox/filters-dropdown.tsx | 126 +++---
.../components/inbox/inbox-action-headers.tsx | 415 ++++++++++--------
.../app/components/inbox/inbox-issue-card.tsx | 179 ++++----
.../components/inbox/inbox-main-content.tsx | 114 +++--
apps/app/components/issues/form.tsx | 2 +
apps/app/components/issues/modal.tsx | 4 +
apps/app/constants/inbox.ts | 15 +
apps/app/pages/[workspaceSlug]/index.tsx | 2 +-
.../projects/[projectId]/inbox/[inboxId].tsx | 222 +---------
apps/app/services/inbox.service.ts | 2 +-
10 files changed, 517 insertions(+), 564 deletions(-)
diff --git a/apps/app/components/inbox/filters-dropdown.tsx b/apps/app/components/inbox/filters-dropdown.tsx
index b567baed0..1b6af608c 100644
--- a/apps/app/components/inbox/filters-dropdown.tsx
+++ b/apps/app/components/inbox/filters-dropdown.tsx
@@ -1,63 +1,81 @@
+// hooks
+import useInboxView from "hooks/use-inbox-view";
// ui
import { MultiLevelDropdown } from "components/ui";
// icons
import { getPriorityIcon } from "components/icons";
-// types
-import { IInboxFilterOptions } from "types";
// constants
import { PRIORITIES } from "constants/project";
import { INBOX_STATUS } from "constants/inbox";
-type Props = {
- filters: Partial;
- onSelect: (option: any) => void;
- direction?: "left" | "right";
- height?: "sm" | "md" | "rg" | "lg";
-};
+export const FiltersDropdown: React.FC = () => {
+ const { filters, setFilters, filtersLength } = useInboxView();
-export const FiltersDropdown: React.FC = ({ filters, onSelect, direction, height }) => (
- ({
- id: priority ?? "none",
- label: (
-
- {getPriorityIcon(priority)} {priority ?? "None"}
-
- ),
- value: {
- key: "priority",
- value: priority,
- },
- selected: filters?.priority?.includes(priority ?? "none"),
- })),
- ],
- },
- {
- id: "inbox_status",
- label: "Status",
- value: INBOX_STATUS.map((status) => status.value),
- children: [
- ...INBOX_STATUS.map((status) => ({
- id: status.key,
- label: status.label,
- value: {
- key: "inbox_status",
- value: status.value,
- },
- selected: filters?.inbox_status?.includes(status.value),
- })),
- ],
- },
- ]}
- />
-);
+ return (
+
+
{
+ const key = option.key as keyof typeof filters;
+
+ const valueExists = (filters[key] as any[])?.includes(option.value);
+
+ if (valueExists) {
+ setFilters({
+ [option.key]: ((filters[key] ?? []) as any[])?.filter((val) => val !== option.value),
+ });
+ } else {
+ setFilters({
+ [option.key]: [...((filters[key] ?? []) as any[]), option.value],
+ });
+ }
+ }}
+ direction="right"
+ height="rg"
+ options={[
+ {
+ id: "priority",
+ label: "Priority",
+ value: PRIORITIES,
+ children: [
+ ...PRIORITIES.map((priority) => ({
+ id: priority ?? "none",
+ label: (
+
+ {getPriorityIcon(priority)} {priority ?? "None"}
+
+ ),
+ value: {
+ key: "priority",
+ value: priority,
+ },
+ selected: filters?.priority?.includes(priority ?? "none"),
+ })),
+ ],
+ },
+ {
+ id: "inbox_status",
+ label: "Status",
+ value: INBOX_STATUS.map((status) => status.value),
+ children: [
+ ...INBOX_STATUS.map((status) => ({
+ id: status.key,
+ label: status.label,
+ value: {
+ key: "inbox_status",
+ value: status.value,
+ },
+ selected: filters?.inbox_status?.includes(status.value),
+ })),
+ ],
+ },
+ ]}
+ />
+ {filtersLength > 0 && (
+
+ {filtersLength}
+
+ )}
+
+ );
+};
diff --git a/apps/app/components/inbox/inbox-action-headers.tsx b/apps/app/components/inbox/inbox-action-headers.tsx
index 5ceaa8f2c..5702d560b 100644
--- a/apps/app/components/inbox/inbox-action-headers.tsx
+++ b/apps/app/components/inbox/inbox-action-headers.tsx
@@ -2,17 +2,27 @@ import { useEffect, useState } from "react";
import { useRouter } from "next/router";
+import { mutate } from "swr";
+
// react-datepicker
import DatePicker from "react-datepicker";
// headless ui
import { Popover } from "@headlessui/react";
// contexts
import { useProjectMyMembership } from "contexts/project-member.context";
+// services
+import inboxServices from "services/inbox.service";
// hooks
import useInboxView from "hooks/use-inbox-view";
import useUserAuth from "hooks/use-user-auth";
+import useToast from "hooks/use-toast";
// components
-import { FiltersDropdown } from "components/inbox";
+import {
+ DeclineIssueModal,
+ DeleteIssueModal,
+ FiltersDropdown,
+ SelectDuplicateInboxIssueModal,
+} from "components/inbox";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
// icons
@@ -26,47 +36,84 @@ import {
TrashIcon,
} from "@heroicons/react/24/outline";
// types
-import type { IInboxIssue } from "types";
-
-type Props = {
- issueCount: number;
- currentIssueIndex: number;
- issue?: IInboxIssue;
- onAccept: () => Promise;
- onDecline: () => void;
- onMarkAsDuplicate: () => void;
- onSnooze: (date: Date | string) => void;
- onDelete: () => void;
-};
-
-export const InboxActionHeader: React.FC = (props) => {
- const {
- issueCount,
- currentIssueIndex,
- onAccept,
- onDecline,
- onMarkAsDuplicate,
- onSnooze,
- onDelete,
- issue,
- } = props;
+import type { IInboxIssueDetail, TInboxStatus } from "types";
+// fetch-keys
+import { INBOX_ISSUE_DETAILS } from "constants/fetch-keys";
+export const InboxActionHeader = () => {
const [isAccepting, setIsAccepting] = useState(false);
const [date, setDate] = useState(new Date());
+ const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false);
+ const [declineIssueModal, setDeclineIssueModal] = useState(false);
+ const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const router = useRouter();
- const { inboxIssueId } = router.query;
+ const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
- const { memberRole } = useProjectMyMembership();
- const { filters, setFilters, filtersLength } = useInboxView();
const { user } = useUserAuth();
+ const { memberRole } = useProjectMyMembership();
+ const { issues: inboxIssues, mutate: mutateInboxIssues } = useInboxView();
+ const { setToastAlert } = useToast();
+
+ const markInboxStatus = async (data: TInboxStatus) => {
+ if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId) return;
+
+ mutate(
+ INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string),
+ (prevData) => {
+ if (!prevData) return prevData;
+
+ return {
+ ...prevData,
+ issue_inbox: [{ ...prevData.issue_inbox[0], ...data }],
+ };
+ },
+ false
+ );
+ mutateInboxIssues(
+ (prevData) =>
+ (prevData ?? []).map((i) =>
+ i.bridge_id === inboxIssueId
+ ? { ...i, issue_inbox: [{ ...i.issue_inbox[0], ...data }] }
+ : i
+ ),
+ false
+ );
+
+ await inboxServices
+ .markInboxStatus(
+ workspaceSlug.toString(),
+ projectId.toString(),
+ inboxId.toString(),
+ inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId)?.bridge_id!,
+ data,
+ user
+ )
+ .catch(() =>
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message: "Something went wrong while updating inbox status. Please try again.",
+ })
+ )
+ .finally(() => {
+ mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string));
+ mutateInboxIssues();
+ });
+ };
const handleAcceptIssue = () => {
setIsAccepting(true);
- onAccept().finally(() => setIsAccepting(false));
+ markInboxStatus({
+ status: 1,
+ }).finally(() => setIsAccepting(false));
};
+ const issue = inboxIssues?.find((issue) => issue.bridge_id === inboxIssueId);
+ const currentIssueIndex =
+ inboxIssues?.findIndex((issue) => issue.bridge_id === inboxIssueId) ?? 0;
+
useEffect(() => {
if (!issue?.issue_inbox[0].snoozed_till) return;
@@ -82,163 +129,165 @@ export const InboxActionHeader: React.FC = (props) => {
tomorrow.setDate(today.getDate() + 1);
return (
-
-
-
-
-
Inbox
-
-
-
{
- const key = option.key as keyof typeof filters;
-
- const valueExists = (filters[key] as any[])?.includes(option.value);
-
- if (valueExists) {
- setFilters({
- [option.key]: ((filters[key] ?? []) as any[])?.filter(
- (val) => val !== option.value
- ),
- });
- } else {
- setFilters({
- [option.key]: [...((filters[key] ?? []) as any[]), option.value],
- });
- }
- }}
- direction="right"
- height="rg"
- />
- {filtersLength > 0 && (
-
- {filtersLength}
-
- )}
-
-
- {inboxIssueId && (
-
-
-
{
- const e = new KeyboardEvent("keydown", { key: "ArrowUp" });
- document.dispatchEvent(e);
- }}
- >
-
-
-
{
- const e = new KeyboardEvent("keydown", { key: "ArrowDown" });
- document.dispatchEvent(e);
- }}
- >
-
-
-
- {currentIssueIndex + 1}/{issueCount}
-
+ <>
+
setSelectDuplicateIssue(false)}
+ value={
+ inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId)?.issue_inbox[0]
+ .duplicate_to
+ }
+ onSubmit={(dupIssueId: string) => {
+ markInboxStatus({
+ status: 2,
+ duplicate_to: dupIssueId,
+ }).finally(() => setSelectDuplicateIssue(false));
+ }}
+ />
+ setDeclineIssueModal(false)}
+ data={inboxIssues?.find((i) => i.bridge_id === inboxIssueId)}
+ onSubmit={async () => {
+ await markInboxStatus({
+ status: -1,
+ }).finally(() => setDeclineIssueModal(false));
+ }}
+ />
+ setDeleteIssueModal(false)}
+ data={inboxIssues?.find((i) => i.bridge_id === inboxIssueId)}
+ />
+
+
+
+
+
Inbox
-
+ {inboxIssueId && (
+
+
+
{
+ const e = new KeyboardEvent("keydown", { key: "ArrowUp" });
+ document.dispatchEvent(e);
+ }}
>
-
-
+
+
{
+ const e = new KeyboardEvent("keydown", { key: "ArrowDown" });
+ document.dispatchEvent(e);
+ }}
+ >
+
+
+
+ {currentIssueIndex + 1}/{inboxIssues?.length ?? 0}
+
+
+
+ {isAllowed && (issueStatus === 0 || issueStatus === -2) && (
+
+
+
+
+
+ Snooze
+
+
+
+ {({ close }) => (
+
+
{
+ if (!val) return;
+ setDate(val);
+ }}
+ dateFormat="dd-MM-yyyy"
+ minDate={tomorrow}
+ inline
+ />
+ {
+ close();
+ markInboxStatus({
+ status: 0,
+ snoozed_till: new Date(date),
+ });
+ }}
+ >
+ Snooze
+
+
+ )}
+
+
+
+ )}
+ {isAllowed && issueStatus === -2 && (
+
+
setSelectDuplicateIssue(true)}
>
-
-
- Snooze
-
-
-
- {({ close }) => (
-
-
{
- if (!val) return;
- setDate(val);
- }}
- dateFormat="dd-MM-yyyy"
- minDate={tomorrow}
- inline
- />
- {
- close();
- onSnooze(date);
- }}
- >
- Snooze
-
-
- )}
-
-
-
- )}
- {isAllowed && (
-
-
-
- Mark as duplicate
-
-
-
- {isAccepting ? "Accepting..." : "Accept"}
-
-
-
- Decline
-
-
- )}
- {(isAllowed || user?.id === issue?.created_by) && (
-
-
-
- Delete
-
-
- )}
+
+
Mark as duplicate
+
+
+ )}
+ {isAllowed && (issueStatus === 0 || issueStatus === -2) && (
+
+
+
+ {isAccepting ? "Accepting..." : "Accept"}
+
+
+ )}
+ {isAllowed && issueStatus === -2 && (
+
+ setDeclineIssueModal(true)}
+ >
+
+ Decline
+
+
+ )}
+ {(isAllowed || user?.id === issue?.created_by) && (
+
+ setDeleteIssueModal(true)}
+ >
+
+ Delete
+
+
+ )}
+
-
- )}
-
+ )}
+
+ >
);
};
diff --git a/apps/app/components/inbox/inbox-issue-card.tsx b/apps/app/components/inbox/inbox-issue-card.tsx
index 814f221a5..072647ae3 100644
--- a/apps/app/components/inbox/inbox-issue-card.tsx
+++ b/apps/app/components/inbox/inbox-issue-card.tsx
@@ -4,13 +4,21 @@ import Link from "next/link";
// ui
import { Tooltip } from "components/ui";
// icons
-import { getPriorityIcon, getStateGroupIcon } from "components/icons";
-import { CalendarDaysIcon, ClockIcon } from "@heroicons/react/24/outline";
+import { getPriorityIcon } from "components/icons";
+import {
+ CalendarDaysIcon,
+ CheckCircleIcon,
+ ClockIcon,
+ DocumentDuplicateIcon,
+ ExclamationTriangleIcon,
+ XCircleIcon,
+} from "@heroicons/react/24/outline";
// helpers
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
-import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import type { IInboxIssue } from "types";
+// constants
+import { INBOX_STATUS } from "constants/inbox";
type Props = {
issue: IInboxIssue;
@@ -30,93 +38,88 @@ export const InboxIssueCard: React.FC
= (props) => {
href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${issue.bridge_id}`}
>
-
-
-
-
- {issue.project_detail?.identifier}-{issue.sequence_id}
-
-
{issue.name}
-
-
-
-
- {getStateGroupIcon(
- issue.state_detail?.group ?? "backlog",
- "14",
- "14",
- issue.state_detail?.color
- )}
- {issue.state_detail?.name ?? "Triage"}
-
-
-
-
- {getPriorityIcon(
- issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
- "text-sm"
- )}
-
-
-
-
-
- {renderShortNumericDateFormat(issue.created_at ?? "")}
-
-
- {issue.issue_inbox[0].snoozed_till && (
-
-
-
- Snoozed till {renderShortNumericDateFormat(issue.issue_inbox[0].snoozed_till)}
-
-
- )}
-
+
+
+ {issue.project_detail?.identifier}-{issue.sequence_id}
+
+
{issue.name}
-
+
+
+
+ {getPriorityIcon(
+ issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
+ "text-sm"
+ )}
+
+
+
+
+
+ {renderShortNumericDateFormat(issue.created_at ?? "")}
+
+
+
+
s.value === issueStatus)?.textColor
+ }`}
+ >
+ {issueStatus === -2 ? (
+ <>
+
+ Pending
+ >
+ ) : issueStatus === -1 ? (
+ <>
+
+ Declined
+ >
+ ) : issueStatus === 0 ? (
+ <>
+
+
+ {new Date(issue.issue_inbox[0].snoozed_till ?? "") < new Date()
+ ? "Snoozed date passed"
+ : "Snoozed"}
+
+ >
+ ) : issueStatus === 1 ? (
+ <>
+
+ Accepted
+ >
+ ) : (
+ <>
+
+ Duplicate
+ >
+ )}
+
+
);
diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx
index 83948495c..d03368944 100644
--- a/apps/app/components/inbox/inbox-main-content.tsx
+++ b/apps/app/components/inbox/inbox-main-content.tsx
@@ -1,6 +1,6 @@
import { useCallback, useEffect } from "react";
-import { useRouter } from "next/router";
+import Router, { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
@@ -29,6 +29,7 @@ import {
ClockIcon,
DocumentDuplicateIcon,
ExclamationTriangleIcon,
+ InboxIcon,
XCircleIcon,
} from "@heroicons/react/24/outline";
// helpers
@@ -37,6 +38,8 @@ import { renderShortNumericDateFormat } from "helpers/date-time.helper";
import type { IInboxIssue, IIssue } from "types";
// fetch-keys
import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
+// constants
+import { INBOX_STATUS } from "constants/inbox";
const defaultValues = {
name: "",
@@ -55,7 +58,7 @@ export const InboxMainContent: React.FC = () => {
const { user } = useUserAuth();
const { memberRole } = useProjectMyMembership();
- const { params } = useInboxView();
+ const { params, issues: inboxIssues } = useInboxView();
const { reset, control, watch } = useForm({
defaultValues,
@@ -76,17 +79,6 @@ export const InboxMainContent: React.FC = () => {
: null
);
- useEffect(() => {
- if (!issueDetails || !inboxIssueId) return;
-
- reset({
- ...issueDetails,
- assignees_list:
- issueDetails.assignees_list ?? (issueDetails.assignee_details ?? []).map((user) => user.id),
- labels_list: issueDetails.labels_list ?? issueDetails.labels,
- });
- }, [issueDetails, reset, inboxIssueId]);
-
const submitChanges = useCallback(
async (formData: Partial) => {
if (!workspaceSlug || !projectId || !inboxIssueId || !inboxId || !issueDetails) return;
@@ -144,7 +136,86 @@ export const InboxMainContent: React.FC = () => {
]
);
+ const onKeyDown = useCallback(
+ (e: KeyboardEvent) => {
+ if (!inboxIssues || !inboxIssueId) return;
+
+ const currentIssueIndex = inboxIssues.findIndex((issue) => issue.bridge_id === inboxIssueId);
+
+ switch (e.key) {
+ case "ArrowUp":
+ Router.push({
+ pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
+ query: {
+ inboxIssueId:
+ currentIssueIndex === 0
+ ? inboxIssues[inboxIssues.length - 1].bridge_id
+ : inboxIssues[currentIssueIndex - 1].bridge_id,
+ },
+ });
+ break;
+ case "ArrowDown":
+ Router.push({
+ pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
+ query: {
+ inboxIssueId:
+ currentIssueIndex === inboxIssues.length - 1
+ ? inboxIssues[0].bridge_id
+ : inboxIssues[currentIssueIndex + 1].bridge_id,
+ },
+ });
+ break;
+ default:
+ break;
+ }
+ },
+ [workspaceSlug, projectId, inboxIssueId, inboxId, inboxIssues]
+ );
+
+ useEffect(() => {
+ document.addEventListener("keydown", onKeyDown);
+
+ return () => {
+ document.removeEventListener("keydown", onKeyDown);
+ };
+ }, [onKeyDown]);
+
+ useEffect(() => {
+ if (!issueDetails || !inboxIssueId) return;
+
+ reset({
+ ...issueDetails,
+ assignees_list:
+ issueDetails.assignees_list ?? (issueDetails.assignee_details ?? []).map((user) => user.id),
+ labels_list: issueDetails.labels_list ?? issueDetails.labels,
+ });
+ }, [issueDetails, reset, inboxIssueId]);
+
const issueStatus = issueDetails?.issue_inbox[0].status;
+ const inboxStatusDetails = INBOX_STATUS.find((s) => s.value === issueStatus);
+
+ if (!inboxIssueId)
+ return (
+
+
+
+
+ {inboxIssues && inboxIssues.length > 0 ? (
+
+ {inboxIssues?.length} issues found. Select an issue from the sidebar to view its
+ details.
+
+ ) : (
+
+ No issues found. Use{" "}
+ C shortcut to
+ create a new issue
+
+ )}
+
+
+
+ );
return (
<>
@@ -153,19 +224,10 @@ export const InboxMainContent: React.FC = () => {
{issueStatus === -2 ? (
@@ -266,6 +328,4 @@ export const InboxMainContent: React.FC = () => {
)}
>
);
-
- return null;
};
diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx
index 5db688283..cc5cae3f1 100644
--- a/apps/app/components/issues/form.tsx
+++ b/apps/app/components/issues/form.tsx
@@ -163,6 +163,8 @@ export const IssueForm: FC
= ({
const handleCreateUpdateIssue = async (formData: Partial) => {
await handleFormSubmit(formData);
+ setGptAssistantModal(false);
+
reset({
...defaultValues,
project: projectId,
diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx
index 5142c6a32..22a275fee 100644
--- a/apps/app/components/issues/modal.tsx
+++ b/apps/app/components/issues/modal.tsx
@@ -188,6 +188,10 @@ export const CreateUpdateIssueModal: React.FC = ({
message: "Issue created successfully.",
});
+ router.push(
+ `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${res.issue_inbox[0].id}`
+ );
+
mutate(INBOX_ISSUES(inboxId.toString(), inboxParams));
})
.catch(() => {
diff --git a/apps/app/constants/inbox.ts b/apps/app/constants/inbox.ts
index 9bbe8230a..3d7e8a054 100644
--- a/apps/app/constants/inbox.ts
+++ b/apps/app/constants/inbox.ts
@@ -3,26 +3,41 @@ export const INBOX_STATUS = [
key: "pending",
label: "Pending",
value: -2,
+ textColor: "text-yellow-500",
+ bgColor: "bg-yellow-500/10",
+ borderColor: "border-yellow-500",
},
{
key: "declined",
label: "Declined",
value: -1,
+ textColor: "text-red-500",
+ bgColor: "bg-red-500/10",
+ borderColor: "border-red-500",
},
{
key: "snoozed",
label: "Snoozed",
value: 0,
+ textColor: "text-brand-secondary",
+ bgColor: "bg-gray-500/10",
+ borderColor: "border-gray-500",
},
{
key: "accepted",
label: "Accepted",
value: 1,
+ textColor: "text-green-500",
+ bgColor: "bg-green-500/10",
+ borderColor: "border-green-500",
},
{
key: "duplicate",
label: "Duplicate",
value: 2,
+ textColor: "text-brand-secondary",
+ bgColor: "bg-gray-500/10",
+ borderColor: "border-gray-500",
},
];
diff --git a/apps/app/pages/[workspaceSlug]/index.tsx b/apps/app/pages/[workspaceSlug]/index.tsx
index 233fd9b8a..3c75890f6 100644
--- a/apps/app/pages/[workspaceSlug]/index.tsx
+++ b/apps/app/pages/[workspaceSlug]/index.tsx
@@ -49,7 +49,7 @@ const WorkspacePage: NextPage = () => {
)}
-
+
Plane is open source, support us by starring us on GitHub.
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx
index 140e704ed..dc301e0d8 100644
--- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx
+++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx
@@ -1,29 +1,13 @@
-import { useState, useEffect, useCallback } from "react";
+import { useRouter } from "next/router";
-import Router, { useRouter } from "next/router";
-
-import useSWR, { mutate } from "swr";
-
-// services
-import inboxServices from "services/inbox.service";
-import projectService from "services/project.service";
// hooks
-import useInboxView from "hooks/use-inbox-view";
-import useUserAuth from "hooks/use-user-auth";
-import useToast from "hooks/use-toast";
+import useProjectDetails from "hooks/use-project-details";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// contexts
import { InboxViewContextProvider } from "contexts/inbox-view-context";
// components
-import {
- InboxActionHeader,
- InboxMainContent,
- SelectDuplicateInboxIssueModal,
- DeclineIssueModal,
- DeleteIssueModal,
- IssuesListSidebar,
-} from "components/inbox";
+import { InboxActionHeader, InboxMainContent, IssuesListSidebar } from "components/inbox";
// helper
import { truncateText } from "helpers/string.helper";
// ui
@@ -31,123 +15,14 @@ import { PrimaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
-import { InboxIcon } from "components/icons";
// types
-import { IInboxIssueDetail, TInboxStatus } from "types";
import type { NextPage } from "next";
-// fetch-keys
-import { INBOX_ISSUE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys";
const ProjectInbox: NextPage = () => {
- const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false);
- const [declineIssueModal, setDeclineIssueModal] = useState(false);
- const [deleteIssueModal, setDeleteIssueModal] = useState(false);
-
const router = useRouter();
- const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
+ const { workspaceSlug } = router.query;
- const { user } = useUserAuth();
- const { issues: inboxIssues, mutate: mutateInboxIssues } = useInboxView();
- const { setToastAlert } = useToast();
-
- const { data: projectDetails } = useSWR(
- workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
- workspaceSlug && projectId
- ? () => projectService.getProject(workspaceSlug as string, projectId as string)
- : null
- );
-
- const onKeyDown = useCallback(
- (e: KeyboardEvent) => {
- if (!inboxIssues || !inboxIssueId) return;
-
- const currentIssueIndex = inboxIssues.findIndex((issue) => issue.bridge_id === inboxIssueId);
-
- switch (e.key) {
- case "ArrowUp":
- Router.push({
- pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
- query: {
- inboxIssueId:
- currentIssueIndex === 0
- ? inboxIssues[inboxIssues.length - 1].bridge_id
- : inboxIssues[currentIssueIndex - 1].bridge_id,
- },
- });
- break;
- case "ArrowDown":
- Router.push({
- pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
- query: {
- inboxIssueId:
- currentIssueIndex === inboxIssues.length - 1
- ? inboxIssues[0].bridge_id
- : inboxIssues[currentIssueIndex + 1].bridge_id,
- },
- });
-
- break;
- default:
- break;
- }
- },
- [workspaceSlug, projectId, inboxIssueId, inboxId, inboxIssues]
- );
-
- useEffect(() => {
- document.addEventListener("keydown", onKeyDown);
-
- return () => {
- document.removeEventListener("keydown", onKeyDown);
- };
- }, [onKeyDown]);
-
- const markInboxStatus = async (data: TInboxStatus) => {
- if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId) return;
-
- mutate
(
- INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string),
- (prevData) => {
- if (!prevData) return prevData;
-
- return {
- ...prevData,
- issue_inbox: [{ ...prevData.issue_inbox[0], ...data }],
- };
- },
- false
- );
- mutateInboxIssues(
- (prevData) =>
- (prevData ?? []).map((i) =>
- i.bridge_id === inboxIssueId
- ? { ...i, issue_inbox: [{ ...i.issue_inbox[0], ...data }] }
- : i
- ),
- false
- );
-
- await inboxServices
- .markInboxStatus(
- workspaceSlug.toString(),
- projectId.toString(),
- inboxId.toString(),
- inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId)?.bridge_id!,
- data,
- user
- )
- .catch(() =>
- setToastAlert({
- type: "error",
- title: "Error!",
- message: "Something went wrong while updating inbox status. Please try again.",
- })
- )
- .finally(() => {
- mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string));
- mutateInboxIssues();
- });
- };
+ const { projectDetails } = useProjectDetails();
return (
@@ -175,88 +50,15 @@ const ProjectInbox: NextPage = () => {
}
>
- <>
-
setSelectDuplicateIssue(false)}
- value={
- inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId)
- ?.issue_inbox[0].duplicate_to
- }
- onSubmit={(dupIssueId: string) => {
- markInboxStatus({
- status: 2,
- duplicate_to: dupIssueId,
- }).finally(() => setSelectDuplicateIssue(false));
- }}
- />
- setDeclineIssueModal(false)}
- data={inboxIssues?.find((i) => i.bridge_id === inboxIssueId)}
- onSubmit={async () => {
- await markInboxStatus({
- status: -1,
- }).finally(() => setDeclineIssueModal(false));
- }}
- />
- setDeleteIssueModal(false)}
- data={inboxIssues?.find((i) => i.bridge_id === inboxIssueId)}
- />
-
-
issue.bridge_id === inboxIssueId)}
- currentIssueIndex={
- inboxIssues?.findIndex((issue) => issue.bridge_id === inboxIssueId) ?? 0
- }
- issueCount={inboxIssues?.length ?? 0}
- onAccept={() =>
- markInboxStatus({
- status: 1,
- })
- }
- onDecline={() => setDeclineIssueModal(true)}
- onMarkAsDuplicate={() => setSelectDuplicateIssue(true)}
- onSnooze={(date) => {
- markInboxStatus({
- status: 0,
- snoozed_till: new Date(date),
- });
- }}
- onDelete={() => setDeleteIssueModal(true)}
- />
-
-
-
- {inboxIssueId ? (
-
- ) : (
-
-
-
-
- {inboxIssues && inboxIssues.length > 0 ? (
-
- {inboxIssues?.length} issues found. Select an issue from the sidebar to
- view its details.
-
- ) : (
-
- No issues found. Use{" "}
- C {" "}
- shortcut to create a new issue
-
- )}
-
-
-
- )}
-
+
);
diff --git a/apps/app/services/inbox.service.ts b/apps/app/services/inbox.service.ts
index 9ee20923c..61949c877 100644
--- a/apps/app/services/inbox.service.ts
+++ b/apps/app/services/inbox.service.ts
@@ -162,7 +162,7 @@ class InboxServices extends APIService {
inboxId: string,
data: any,
user: ICurrentUserResponse | undefined
- ): Promise
{
+ ): Promise {
return this.post(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`,
data
From 37303e6cb86a71e5ac7b94ed28ae7268f6967b29 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Fri, 23 Jun 2023 13:30:52 +0530
Subject: [PATCH 44/59] refactor: inbox issues (#1370)
* refactor: inbox issue endpoints
* dev: update inbox issues endpooint
---
apiserver/plane/api/views/inbox.py | 25 ++++++++-----------------
1 file changed, 8 insertions(+), 17 deletions(-)
diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py
index 0e4c1603e..f794db058 100644
--- a/apiserver/plane/api/views/inbox.py
+++ b/apiserver/plane/api/views/inbox.py
@@ -68,13 +68,12 @@ class InboxViewSet(BaseViewSet):
inbox = Inbox.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
-
+ # Handle default inbox delete
if inbox.is_default:
return Response(
{"error": "You cannot delete the default inbox"},
status=status.HTTP_400_BAD_REQUEST,
)
-
inbox.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
@@ -120,23 +119,17 @@ class InboxIssueViewSet(BaseViewSet):
workspace__slug=slug,
project_id=project_id,
)
+ .filter(**filters)
+ .order_by(order_by)
+ .annotate(bridge_id=F("issue_inbox__id"))
+ .select_related("workspace", "project", "state", "parent")
+ .prefetch_related("assignees", "labels")
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
- .annotate(bridge_id=F("issue_inbox__id"))
- .filter(project_id=project_id)
- .filter(workspace__slug=slug)
- .select_related("project")
- .select_related("workspace")
- .select_related("state")
- .select_related("parent")
- .prefetch_related("assignees")
- .prefetch_related("labels")
- .order_by(order_by)
- .filter(**filters)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -180,7 +173,8 @@ class InboxIssueViewSet(BaseViewSet):
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
)
- if not request.data.get("issue", {}).get("priority", "low") in [
+ # Check for valid priority
+ if not request.data.get("issue", {}).get("priority", None) in [
"low",
"medium",
"high",
@@ -213,7 +207,6 @@ class InboxIssueViewSet(BaseViewSet):
)
# Create an Issue Activity
- # Track the issue
issue_activity.delay(
type="issue.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
@@ -231,9 +224,7 @@ class InboxIssueViewSet(BaseViewSet):
)
serializer = IssueStateInboxSerializer(issue)
-
return Response(serializer.data, status=status.HTTP_200_OK)
-
except Exception as e:
capture_exception(e)
return Response(
From 0cb856b92fef7f5a5db68008b2a53c432e33b95f Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Fri, 23 Jun 2023 13:33:01 +0530
Subject: [PATCH 45/59] chore: inbox issue ordering (#1367)
---
apiserver/plane/api/views/inbox.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py
index f794db058..24fff54ae 100644
--- a/apiserver/plane/api/views/inbox.py
+++ b/apiserver/plane/api/views/inbox.py
@@ -111,7 +111,6 @@ class InboxIssueViewSet(BaseViewSet):
def list(self, request, slug, project_id, inbox_id):
try:
- order_by = request.GET.get("order_by", "created_at")
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.objects.filter(
@@ -120,10 +119,10 @@ class InboxIssueViewSet(BaseViewSet):
project_id=project_id,
)
.filter(**filters)
- .order_by(order_by)
.annotate(bridge_id=F("issue_inbox__id"))
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
+ .order_by("issue_inbox__snoozed_till", "issue_inbox__status")
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
From e08fc59114f9b634ca59a588cb653794cd969208 Mon Sep 17 00:00:00 2001
From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Date: Fri, 23 Jun 2023 17:20:05 +0530
Subject: [PATCH 46/59] feat: spreadsheet view (#1369)
* feat: spreadsheet view
* fix: fix scroll and overflow issues, feat: updated issue properties component, style: ui improvements
* feat: sub-issue toggle and sub-issue hook added, chore: code refactor
* fix: only render parent issue
* feat: sub issue fetching hook updated and nested sub issue added, chore: code refactor
* style: title sticky to left on scroll and column styling
* fix: tooltip , filter and view z-index fix
* feat: spreadsheet view column sorting, fix: sticky scroll issue fix
* feat: updated issue view filter for spreadsheet view
* style: spreadsheet view column
* feat: double click to edit title
* fix: estimate sorting fix
* style: spreadsheet view columns
* fix: spreadsheet view mutation, feat: edit , copy and delete option added
* fix: edit sub issue fix
---
apps/app/components/core/index.ts | 1 +
.../components/core/issues-view-filter.tsx | 24 +-
apps/app/components/core/issues-view.tsx | 16 +-
.../components/core/spreadsheet-view/index.ts | 4 +
.../core/spreadsheet-view/single-issue.tsx | 266 ++++++++++++++++++
.../spreadsheet-view/spreadsheet-columns.tsx | 241 ++++++++++++++++
.../spreadsheet-view/spreadsheet-issues.tsx | 90 ++++++
.../spreadsheet-view/spreadsheet-view.tsx | 94 +++++++
.../components/issues/delete-issue-modal.tsx | 16 ++
apps/app/components/issues/modal.tsx | 12 +
.../issues/view-select/assignee.tsx | 66 +++--
.../issues/view-select/due-date.tsx | 3 +
.../issues/view-select/estimate.tsx | 20 +-
.../issues/view-select/priority.tsx | 21 +-
.../components/issues/view-select/state.tsx | 28 +-
apps/app/components/ui/custom-menu.tsx | 8 +-
apps/app/components/ui/datepicker.tsx | 6 +-
.../components/ui/multi-level-dropdown.tsx | 2 +-
apps/app/components/ui/tooltip.tsx | 2 +-
apps/app/constants/fetch-keys.ts | 5 +-
apps/app/constants/spreadsheet.ts | 60 ++++
.../app/hooks/use-spreadsheet-issues-view.tsx | 125 ++++++++
apps/app/hooks/use-sub-issue.tsx | 34 +++
apps/app/types/issues.d.ts | 18 +-
24 files changed, 1093 insertions(+), 69 deletions(-)
create mode 100644 apps/app/components/core/spreadsheet-view/index.ts
create mode 100644 apps/app/components/core/spreadsheet-view/single-issue.tsx
create mode 100644 apps/app/components/core/spreadsheet-view/spreadsheet-columns.tsx
create mode 100644 apps/app/components/core/spreadsheet-view/spreadsheet-issues.tsx
create mode 100644 apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx
create mode 100644 apps/app/constants/spreadsheet.ts
create mode 100644 apps/app/hooks/use-spreadsheet-issues-view.tsx
create mode 100644 apps/app/hooks/use-sub-issue.tsx
diff --git a/apps/app/components/core/index.ts b/apps/app/components/core/index.ts
index e3e187d60..c50ce7251 100644
--- a/apps/app/components/core/index.ts
+++ b/apps/app/components/core/index.ts
@@ -2,6 +2,7 @@ export * from "./board-view";
export * from "./calendar-view";
export * from "./gantt-chart-view";
export * from "./list-view";
+export * from "./spreadsheet-view";
export * from "./sidebar";
export * from "./bulk-delete-issues-modal";
export * from "./existing-issues-list-modal";
diff --git a/apps/app/components/core/issues-view-filter.tsx b/apps/app/components/core/issues-view-filter.tsx
index 6856e8f8b..679f6adc3 100644
--- a/apps/app/components/core/issues-view-filter.tsx
+++ b/apps/app/components/core/issues-view-filter.tsx
@@ -10,7 +10,7 @@ import { Popover, Transition } from "@headlessui/react";
// components
import { SelectFilters } from "components/views";
// ui
-import { CustomMenu, ToggleSwitch } from "components/ui";
+import { CustomMenu, Icon, ToggleSwitch } from "components/ui";
// icons
import {
ChevronDownIcon,
@@ -83,6 +83,15 @@ export const IssuesFilterView: React.FC = () => {
>
+ setIssueView("spreadsheet")}
+ >
+
+
{
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
-
+
- {issueView !== "calendar" && (
+ {issueView !== "calendar" && issueView !== "spreadsheet" && (
<>
Group by
@@ -221,7 +230,7 @@ export const IssuesFilterView: React.FC = () => {
- {issueView !== "calendar" && (
+ {issueView !== "calendar" && issueView !== "spreadsheet" && (
<>
Show empty states
@@ -252,6 +261,13 @@ export const IssuesFilterView: React.FC = () => {
{Object.keys(properties).map((key) => {
if (key === "estimate" && !isEstimateActive) return null;
+ if (
+ (issueView === "spreadsheet" && key === "sub_issue_count") ||
+ key === "attachment_count" ||
+ key === "link"
+ )
+ return null;
+
return (
= ({
user={user}
userAuth={memberRole}
/>
+ ) : issueView === "spreadsheet" ? (
+
) : (
issueView === "gantt_chart" &&
)}
diff --git a/apps/app/components/core/spreadsheet-view/index.ts b/apps/app/components/core/spreadsheet-view/index.ts
new file mode 100644
index 000000000..7729d5e93
--- /dev/null
+++ b/apps/app/components/core/spreadsheet-view/index.ts
@@ -0,0 +1,4 @@
+export * from "./spreadsheet-view";
+export * from "./single-issue";
+export * from "./spreadsheet-columns";
+export * from "./spreadsheet-issues";
diff --git a/apps/app/components/core/spreadsheet-view/single-issue.tsx b/apps/app/components/core/spreadsheet-view/single-issue.tsx
new file mode 100644
index 000000000..d3086e254
--- /dev/null
+++ b/apps/app/components/core/spreadsheet-view/single-issue.tsx
@@ -0,0 +1,266 @@
+import React, { useCallback } from "react";
+
+import Link from "next/link";
+import { useRouter } from "next/router";
+
+import { mutate } from "swr";
+
+// components
+import {
+ ViewAssigneeSelect,
+ ViewDueDateSelect,
+ ViewEstimateSelect,
+ ViewPrioritySelect,
+ ViewStateSelect,
+} from "components/issues";
+// icons
+import { CustomMenu, Icon } from "components/ui";
+import { LinkIcon, PencilIcon, TrashIcon, XMarkIcon } from "@heroicons/react/24/outline";
+// hooks
+import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
+import useToast from "hooks/use-toast";
+// services
+import issuesService from "services/issues.service";
+// constant
+import {
+ CYCLE_ISSUES_WITH_PARAMS,
+ MODULE_ISSUES_WITH_PARAMS,
+ PROJECT_ISSUES_LIST_WITH_PARAMS,
+ VIEW_ISSUES,
+} from "constants/fetch-keys";
+// types
+import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
+// helper
+import { copyTextToClipboard } from "helpers/string.helper";
+
+type Props = {
+ issue: IIssue;
+ expanded: boolean;
+ handleToggleExpand: (issueId: string) => void;
+ properties: Properties;
+ handleEditIssue: () => void;
+ handleDeleteIssue: (issue: IIssue) => void;
+ gridTemplateColumns: string;
+ user: ICurrentUserResponse | undefined;
+ userAuth: UserAuth;
+ nestingLevel: number;
+};
+
+export const SingleSpreadsheetIssue: React.FC = ({
+ issue,
+ expanded,
+ handleToggleExpand,
+ properties,
+ handleEditIssue,
+ handleDeleteIssue,
+ gridTemplateColumns,
+ user,
+ userAuth,
+ nestingLevel,
+}) => {
+ const router = useRouter();
+
+ const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
+
+ const { params } = useSpreadsheetIssuesView();
+
+ const { setToastAlert } = useToast();
+
+ const partialUpdateIssue = useCallback(
+ (formData: Partial, issueId: string) => {
+ if (!workspaceSlug || !projectId) return;
+
+ const fetchKey = cycleId
+ ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
+ : moduleId
+ ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
+ : viewId
+ ? VIEW_ISSUES(viewId.toString(), params)
+ : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
+
+ mutate(
+ fetchKey,
+ (prevData) =>
+ (prevData ?? []).map((p) => {
+ if (p.id === issueId) {
+ return {
+ ...p,
+ ...formData,
+ };
+ }
+ return p;
+ }),
+ false
+ );
+
+ issuesService
+ .patchIssue(workspaceSlug as string, projectId as string, issueId as string, formData, user)
+ .then(() => {
+ mutate(fetchKey);
+ })
+ .catch((error) => {
+ console.log(error);
+ });
+ },
+ [workspaceSlug, projectId, cycleId, moduleId, viewId, params, user]
+ );
+
+ const handleCopyText = () => {
+ const originURL =
+ typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
+ copyTextToClipboard(
+ `${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
+ ).then(() => {
+ setToastAlert({
+ type: "success",
+ title: "Link Copied!",
+ message: "Issue link copied to clipboard.",
+ });
+ });
+ };
+
+ const paddingLeft = `${nestingLevel * 68}px`;
+
+ const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
+
+ return (
+
+
+
+ {properties.key && (
+ <>
+
+ {issue.project_detail?.identifier}-{issue.sequence_id}
+
+ >
+ )}
+
+
+ {issue.sub_issues_count > 0 && (
+ handleToggleExpand(issue.id)}
+ >
+
+
+ )}
+
+
+
+
+ {issue.name}
+
+
+
+ {properties.state && (
+
+
+
+ )}
+ {properties.priority && (
+
+
+
+ )}
+ {properties.assignee && (
+
+
+
+ )}
+ {properties.labels ? (
+ issue.label_details.length > 0 ? (
+
+ {issue.label_details.slice(0, 4).map((label, index) => (
+
+
+
+ ))}
+ {issue.label_details.length > 4 ?
+{issue.label_details.length - 4} : null}
+
+ ) : (
+
+ No Labels
+
+ )
+ ) : (
+ ""
+ )}
+ {properties.due_date && (
+
+
+
+ )}
+ {properties.estimate && (
+
+
+
+ )}
+
+ {!isNotAllowed && (
+
+
+
+
+ handleDeleteIssue(issue)}>
+
+
+ Delete issue
+
+
+
+
+
+ Copy issue link
+
+
+
+ )}
+
+
+ );
+};
diff --git a/apps/app/components/core/spreadsheet-view/spreadsheet-columns.tsx b/apps/app/components/core/spreadsheet-view/spreadsheet-columns.tsx
new file mode 100644
index 000000000..9f615f165
--- /dev/null
+++ b/apps/app/components/core/spreadsheet-view/spreadsheet-columns.tsx
@@ -0,0 +1,241 @@
+import React from "react";
+// hooks
+import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
+import useLocalStorage from "hooks/use-local-storage";
+// component
+import { CustomMenu, Icon } from "components/ui";
+// icon
+import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
+// types
+import { TIssueOrderByOptions } from "types";
+
+type Props = {
+ columnData: any;
+ gridTemplateColumns: string;
+};
+
+export const SpreadsheetColumns: React.FC = ({ columnData, gridTemplateColumns }) => {
+ const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
+ "spreadsheetViewSorting",
+ ""
+ );
+
+ const { orderBy, setOrderBy } = useSpreadsheetIssuesView();
+
+ const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
+ setOrderBy(order);
+ setSelectedMenuItem(`${order}_${itemKey}`);
+ };
+ return (
+
+ {columnData.map((col: any) => {
+ if (col.isActive) {
+ return (
+
+ {col.propertyName === "title" || col.propertyName === "priority" ? (
+
+ {col.icon ? (
+
+ ) : col.propertyName === "priority" ? (
+
+ signal_cellular_alt
+
+ ) : (
+ ""
+ )}
+
+ {col.colName}
+
+ ) : (
+
+ {col.icon ? (
+
+ ) : col.propertyName === "priority" ? (
+
+ signal_cellular_alt
+
+ ) : (
+ ""
+ )}
+
+ {col.colName}
+
+
+ }
+ menuItemsWhiteBg
+ width="xl"
+ >
+
{
+ handleOrderBy(col.ascendingOrder, col.propertyName);
+ }}
+ >
+
+
+ {col.propertyName === "assignee" || col.propertyName === "labels" ? (
+ <>
+ A-Z
+ Ascending
+ >
+ ) : col.propertyName === "due_date" ? (
+ <>
+ 1-9
+ Ascending
+ >
+ ) : col.propertyName === "estimate" ? (
+ <>
+ 0
+
+ 10
+ >
+ ) : (
+ <>
+ First
+
+ Last
+ >
+ )}
+
+
+
+
+
+
{
+ handleOrderBy(col.descendingOrder, col.propertyName);
+ }}
+ >
+
+
+ {col.propertyName === "assignee" || col.propertyName === "labels" ? (
+ <>
+ Z-A
+ Descending
+ >
+ ) : col.propertyName === "due_date" ? (
+ <>
+ 9-1
+ Descending
+ >
+ ) : col.propertyName === "estimate" ? (
+ <>
+ 10
+
+ 0
+ >
+ ) : (
+ <>
+ Last
+
+ First
+ >
+ )}
+
+
+
+
+
+
{
+ handleOrderBy("-created_at", col.propertyName);
+ }}
+ >
+
+
+
+ )}
+
+ );
+ }
+ })}
+
+ );
+};
diff --git a/apps/app/components/core/spreadsheet-view/spreadsheet-issues.tsx b/apps/app/components/core/spreadsheet-view/spreadsheet-issues.tsx
new file mode 100644
index 000000000..8652b3a7e
--- /dev/null
+++ b/apps/app/components/core/spreadsheet-view/spreadsheet-issues.tsx
@@ -0,0 +1,90 @@
+import React, { useState } from "react";
+
+// components
+import { SingleSpreadsheetIssue } from "components/core";
+// hooks
+import useSubIssue from "hooks/use-sub-issue";
+// types
+import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
+
+type Props = {
+ key: string;
+ issue: IIssue;
+ expandedIssues: string[];
+ setExpandedIssues: React.Dispatch
>;
+ properties: Properties;
+ handleEditIssue: (issue: IIssue) => void;
+ handleDeleteIssue: (issue: IIssue) => void;
+ gridTemplateColumns: string;
+ user: ICurrentUserResponse | undefined;
+ userAuth: UserAuth;
+ nestingLevel?: number;
+};
+
+export const SpreadsheetIssues: React.FC = ({
+ key,
+ issue,
+ expandedIssues,
+ setExpandedIssues,
+ gridTemplateColumns,
+ properties,
+ handleEditIssue,
+ handleDeleteIssue,
+ user,
+ userAuth,
+ nestingLevel = 0,
+}) => {
+ const handleToggleExpand = (issueId: string) => {
+ setExpandedIssues((prevState) => {
+ const newArray = [...prevState];
+ const index = newArray.indexOf(issueId);
+ if (index > -1) {
+ newArray.splice(index, 1);
+ } else {
+ newArray.push(issueId);
+ }
+ return newArray;
+ });
+ };
+
+ const isExpanded = expandedIssues.indexOf(issue.id) > -1;
+
+ const { subIssues, isLoading } = useSubIssue(issue.id, isExpanded);
+
+ return (
+
+ handleEditIssue(issue)}
+ handleDeleteIssue={handleDeleteIssue}
+ user={user}
+ userAuth={userAuth}
+ nestingLevel={nestingLevel}
+ />
+
+ {isExpanded &&
+ !isLoading &&
+ subIssues &&
+ subIssues.length > 0 &&
+ subIssues.map((subIssue: IIssue, subIndex: number) => (
+ handleEditIssue(subIssue)}
+ handleDeleteIssue={handleDeleteIssue}
+ user={user}
+ userAuth={userAuth}
+ nestingLevel={nestingLevel + 1}
+ />
+ ))}
+
+ );
+};
diff --git a/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx b/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx
new file mode 100644
index 000000000..6f36b2dbb
--- /dev/null
+++ b/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx
@@ -0,0 +1,94 @@
+import React, { useState } from "react";
+
+// next
+import { useRouter } from "next/router";
+
+// components
+import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
+import { Icon, Spinner } from "components/ui";
+// hooks
+import useIssuesProperties from "hooks/use-issue-properties";
+import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
+// types
+import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
+// constants
+import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
+// icon
+import { PlusIcon } from "@heroicons/react/24/outline";
+
+type Props = {
+ handleEditIssue: (issue: IIssue) => void;
+ handleDeleteIssue: (issue: IIssue) => void;
+ user: ICurrentUserResponse | undefined;
+ userAuth: UserAuth;
+};
+
+export const SpreadsheetView: React.FC = ({
+ handleEditIssue,
+ handleDeleteIssue,
+ user,
+ userAuth,
+}) => {
+ const [expandedIssues, setExpandedIssues] = useState([]);
+
+ const router = useRouter();
+ const { workspaceSlug, projectId } = router.query;
+
+ const { spreadsheetIssues } = useSpreadsheetIssuesView();
+
+ const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
+
+ const columnData = SPREADSHEET_COLUMN.map((column) => ({
+ ...column,
+ isActive: properties
+ ? column.propertyName === "labels"
+ ? properties[column.propertyName as keyof Properties]
+ : column.propertyName === "title"
+ ? true
+ : properties[column.propertyName as keyof Properties]
+ : false,
+ }));
+
+ const gridTemplateColumns = columnData
+ .filter((column) => column.isActive)
+ .map((column) => column.colSize)
+ .join(" ");
+
+ return (
+
+
+
+
+ {spreadsheetIssues ? (
+
+ {spreadsheetIssues.map((issue: IIssue, index) => (
+
+ ))}
+
{
+ const e = new KeyboardEvent("keydown", { key: "c" });
+ document.dispatchEvent(e);
+ }}
+ >
+
+ Add Issue
+
+
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/apps/app/components/issues/delete-issue-modal.tsx b/apps/app/components/issues/delete-issue-modal.tsx
index d0ef4b6e9..ffdebb314 100644
--- a/apps/app/components/issues/delete-issue-modal.tsx
+++ b/apps/app/components/issues/delete-issue-modal.tsx
@@ -12,6 +12,7 @@ import issueServices from "services/issues.service";
import useIssuesView from "hooks/use-issues-view";
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
import useToast from "hooks/use-toast";
+import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
// icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ui
@@ -41,6 +42,7 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, u
const { issueView, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
+ const { params: spreadsheetParams } = useSpreadsheetIssuesView();
const { setToastAlert } = useToast();
@@ -74,6 +76,20 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, u
(prevData) => (prevData ?? []).filter((p) => p.id !== data.id),
false
);
+ } else if (issueView === "spreadsheet") {
+ const spreadsheetFetchKey = cycleId
+ ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams)
+ : moduleId
+ ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams)
+ : viewId
+ ? VIEW_ISSUES(viewId.toString(), spreadsheetParams)
+ : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", spreadsheetParams);
+
+ mutate(
+ spreadsheetFetchKey,
+ (prevData) => (prevData ?? []).filter((p) => p.id !== data.id),
+ false
+ );
} else {
if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
else if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx
index 22a275fee..d88ad674d 100644
--- a/apps/app/components/issues/modal.tsx
+++ b/apps/app/components/issues/modal.tsx
@@ -17,6 +17,7 @@ import useIssuesView from "hooks/use-issues-view";
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
import useToast from "hooks/use-toast";
import useInboxView from "hooks/use-inbox-view";
+import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
// components
import { IssueForm } from "components/issues";
// types
@@ -79,6 +80,7 @@ export const CreateUpdateIssueModal: React.FC = ({
const { params: calendarParams } = useCalendarIssuesView();
const { order_by, group_by, ...viewGanttParams } = params;
const { params: inboxParams } = useInboxView();
+ const { params: spreadsheetParams } = useSpreadsheetIssuesView();
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string };
@@ -211,6 +213,14 @@ export const CreateUpdateIssueModal: React.FC = ({
? VIEW_ISSUES(viewId.toString(), calendarParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", calendarParams);
+ const spreadsheetFetchKey = cycleId
+ ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams)
+ : moduleId
+ ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams)
+ : viewId
+ ? VIEW_ISSUES(viewId.toString(), spreadsheetParams)
+ : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", spreadsheetParams);
+
const ganttFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
: moduleId
@@ -234,6 +244,7 @@ export const CreateUpdateIssueModal: React.FC = ({
if (issueView === "calendar") mutate(calendarFetchKey);
if (issueView === "gantt_chart") mutate(ganttFetchKey);
+ if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
setToastAlert({
type: "success",
@@ -264,6 +275,7 @@ export const CreateUpdateIssueModal: React.FC = ({
mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
} else {
if (issueView === "calendar") mutate(calendarFetchKey);
+ if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
}
diff --git a/apps/app/components/issues/view-select/assignee.tsx b/apps/app/components/issues/view-select/assignee.tsx
index 27d4901f6..1dbfbabba 100644
--- a/apps/app/components/issues/view-select/assignee.tsx
+++ b/apps/app/components/issues/view-select/assignee.tsx
@@ -22,6 +22,7 @@ type Props = {
position?: "left" | "right";
selfPositioned?: boolean;
tooltipPosition?: "left" | "right";
+ customButton?: boolean;
user: ICurrentUserResponse | undefined;
isNotAllowed: boolean;
};
@@ -34,6 +35,7 @@ export const ViewAssigneeSelect: React.FC = ({
tooltipPosition = "right",
user,
isNotAllowed,
+ customButton = false,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@@ -65,6 +67,38 @@ export const ViewAssigneeSelect: React.FC = ({
),
}));
+ const assigneeLabel = (
+ 0
+ ? issue.assignee_details
+ .map((assignee) =>
+ assignee?.first_name !== "" ? assignee?.first_name : assignee?.email
+ )
+ .join(", ")
+ : "No Assignee"
+ }
+ >
+
+ {issue.assignees && issue.assignees.length > 0 && Array.isArray(issue.assignees) ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ );
+
return (
= ({
);
}}
options={options}
- label={
- 0
- ? issue.assignee_details
- .map((assignee) =>
- assignee?.first_name !== "" ? assignee?.first_name : assignee?.email
- )
- .join(", ")
- : "No Assignee"
- }
- >
-
- {issue.assignees && issue.assignees.length > 0 && Array.isArray(issue.assignees) ? (
-
- ) : (
-
-
-
- )}
-
-
- }
+ {...(customButton ? { customButton: assigneeLabel } : { label: assigneeLabel })}
multiple
noChevron
position={position}
diff --git a/apps/app/components/issues/view-select/due-date.tsx b/apps/app/components/issues/view-select/due-date.tsx
index bea5ff045..f74b62689 100644
--- a/apps/app/components/issues/view-select/due-date.tsx
+++ b/apps/app/components/issues/view-select/due-date.tsx
@@ -12,6 +12,7 @@ import { ICurrentUserResponse, IIssue } from "types";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial, issueId: string) => void;
+ noBorder?: boolean;
user: ICurrentUserResponse | undefined;
isNotAllowed: boolean;
};
@@ -19,6 +20,7 @@ type Props = {
export const ViewDueDateSelect: React.FC = ({
issue,
partialUpdateIssue,
+ noBorder = false,
user,
isNotAllowed,
}) => {
@@ -62,6 +64,7 @@ export const ViewDueDateSelect: React.FC = ({
);
}}
className={issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"}
+ noBorder={noBorder}
disabled={isNotAllowed}
/>
diff --git a/apps/app/components/issues/view-select/estimate.tsx b/apps/app/components/issues/view-select/estimate.tsx
index 914a5286e..02a3e0710 100644
--- a/apps/app/components/issues/view-select/estimate.tsx
+++ b/apps/app/components/issues/view-select/estimate.tsx
@@ -18,6 +18,7 @@ type Props = {
partialUpdateIssue: (formData: Partial
, issueId: string) => void;
position?: "left" | "right";
selfPositioned?: boolean;
+ customButton?: boolean;
user: ICurrentUserResponse | undefined;
isNotAllowed: boolean;
};
@@ -27,6 +28,7 @@ export const ViewEstimateSelect: React.FC = ({
partialUpdateIssue,
position = "left",
selfPositioned = false,
+ customButton = false,
user,
isNotAllowed,
}) => {
@@ -37,6 +39,15 @@ export const ViewEstimateSelect: React.FC = ({
const estimateValue = estimatePoints?.find((e) => e.key === issue.estimate_point)?.value;
+ const estimateLabels = (
+
+
+
+ {estimateValue ?? "None"}
+
+
+ );
+
if (!isEstimateActive) return null;
return (
@@ -57,14 +68,7 @@ export const ViewEstimateSelect: React.FC = ({
user
);
}}
- label={
-
-
-
- {estimateValue ?? "Estimate"}
-
-
- }
+ {...(customButton ? { customButton: estimateLabels } : { label: estimateLabels })}
maxHeight="md"
noChevron
disabled={isNotAllowed}
diff --git a/apps/app/components/issues/view-select/priority.tsx b/apps/app/components/issues/view-select/priority.tsx
index a0c5cd47c..499546931 100644
--- a/apps/app/components/issues/view-select/priority.tsx
+++ b/apps/app/components/issues/view-select/priority.tsx
@@ -12,12 +12,15 @@ import { ICurrentUserResponse, IIssue } from "types";
import { PRIORITIES } from "constants/project";
// services
import trackEventServices from "services/track-event.service";
+// helper
+import { capitalizeFirstLetter } from "helpers/string.helper";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial, issueId: string) => void;
position?: "left" | "right";
selfPositioned?: boolean;
+ noBorder?: boolean;
user: ICurrentUserResponse | undefined;
isNotAllowed: boolean;
};
@@ -27,6 +30,7 @@ export const ViewPrioritySelect: React.FC = ({
partialUpdateIssue,
position = "left",
selfPositioned = false,
+ noBorder = false,
user,
isNotAllowed,
}) => {
@@ -55,10 +59,12 @@ export const ViewPrioritySelect: React.FC = ({
customButton={
= ({
: issue.priority === "low"
? "border-green-500/20 bg-green-500/20 text-green-500"
: "border-brand-base"
- }`}
+ } items-center`}
>
-
+
{getPriorityIcon(
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
"text-sm"
)}
+ {noBorder
+ ? issue.priority && issue.priority !== ""
+ ? capitalizeFirstLetter(issue.priority) ?? ""
+ : "None"
+ : ""}
diff --git a/apps/app/components/issues/view-select/state.tsx b/apps/app/components/issues/view-select/state.tsx
index 2b904eb1e..c097c7326 100644
--- a/apps/app/components/issues/view-select/state.tsx
+++ b/apps/app/components/issues/view-select/state.tsx
@@ -22,6 +22,7 @@ type Props = {
partialUpdateIssue: (formData: Partial, issueId: string) => void;
position?: "left" | "right";
selfPositioned?: boolean;
+ customButton?: boolean;
user: ICurrentUserResponse | undefined;
isNotAllowed: boolean;
};
@@ -31,6 +32,7 @@ export const ViewStateSelect: React.FC = ({
partialUpdateIssue,
position = "left",
selfPositioned = false,
+ customButton = false,
user,
isNotAllowed,
}) => {
@@ -58,6 +60,19 @@ export const ViewStateSelect: React.FC = ({
const selectedOption = states?.find((s) => s.id === issue.state);
+ const stateLabel = (
+
+
+ {selectedOption &&
+ getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)}
+ {selectedOption?.name ?? "State"}
+
+
+ );
+
return (
= ({
}
}}
options={options}
- label={
-
-
- {selectedOption &&
- getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)}
- {selectedOption?.name ?? "State"}
-
-
- }
+ {...(customButton ? { customButton: stateLabel } : { label: stateLabel })}
position={position}
disabled={isNotAllowed}
noChevron
diff --git a/apps/app/components/ui/custom-menu.tsx b/apps/app/components/ui/custom-menu.tsx
index dac7927b1..006802b79 100644
--- a/apps/app/components/ui/custom-menu.tsx
+++ b/apps/app/components/ui/custom-menu.tsx
@@ -20,6 +20,7 @@ type Props = {
position?: "left" | "right";
verticalPosition?: "top" | "bottom";
customButton?: JSX.Element;
+ menuItemsWhiteBg?: boolean;
};
type MenuItemProps = {
@@ -44,6 +45,7 @@ const CustomMenu = ({
position = "right",
verticalPosition = "bottom",
customButton,
+ menuItemsWhiteBg = false,
}: Props) => (
{({ open }) => (
@@ -105,7 +107,7 @@ const CustomMenu = ({
leaveTo="transform opacity-0 scale-95"
>
{children}
diff --git a/apps/app/components/ui/datepicker.tsx b/apps/app/components/ui/datepicker.tsx
index 999b46ce4..80ce7aa91 100644
--- a/apps/app/components/ui/datepicker.tsx
+++ b/apps/app/components/ui/datepicker.tsx
@@ -11,6 +11,7 @@ type Props = {
placeholder?: string;
displayShortForm?: boolean;
error?: boolean;
+ noBorder?: boolean;
className?: string;
isClearable?: boolean;
disabled?: boolean;
@@ -23,6 +24,7 @@ export const CustomDatePicker: React.FC = ({
placeholder = "Select date",
displayShortForm = false,
error = false,
+ noBorder = false,
className = "",
isClearable = true,
disabled = false,
@@ -44,7 +46,9 @@ export const CustomDatePicker: React.FC = ({
: ""
} ${error ? "border-red-500 bg-red-100" : ""} ${
disabled ? "cursor-not-allowed" : "cursor-pointer"
- } w-full rounded-md border border-brand-base bg-transparent caret-transparent ${className}`}
+ } ${
+ noBorder ? "" : "border border-brand-base"
+ } w-full rounded-md bg-transparent caret-transparent ${className}`}
dateFormat="dd-MM-yyyy"
isClearable={isClearable}
disabled={disabled}
diff --git a/apps/app/components/ui/multi-level-dropdown.tsx b/apps/app/components/ui/multi-level-dropdown.tsx
index 0033e8e02..0f25d06b3 100644
--- a/apps/app/components/ui/multi-level-dropdown.tsx
+++ b/apps/app/components/ui/multi-level-dropdown.tsx
@@ -35,7 +35,7 @@ export const MultiLevelDropdown: React.FC = ({
const [openChildFor, setOpenChildFor] = useState(null);
return (
-
+
{({ open }) => (
<>
diff --git a/apps/app/components/ui/tooltip.tsx b/apps/app/components/ui/tooltip.tsx
index 11504facd..86ca39e54 100644
--- a/apps/app/components/ui/tooltip.tsx
+++ b/apps/app/components/ui/tooltip.tsx
@@ -42,7 +42,7 @@ export const Tooltip: React.FC
= ({
disabled={disabled}
content={
diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts
index 5625b710d..7e77e6dc2 100644
--- a/apps/app/constants/fetch-keys.ts
+++ b/apps/app/constants/fetch-keys.ts
@@ -1,7 +1,7 @@
import { IAnalyticsParams, IJiraMetadata } from "types";
const paramsToKey = (params: any) => {
- const { state, priority, assignees, created_by, labels, target_date } = params;
+ const { state, priority, assignees, created_by, labels, target_date, sub_issue } = params;
let stateKey = state ? state.split(",") : [];
let priorityKey = priority ? priority.split(",") : [];
@@ -12,6 +12,7 @@ const paramsToKey = (params: any) => {
const type = params.type ? params.type.toUpperCase() : "NULL";
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
const orderBy = params.order_by ? params.order_by.toUpperCase() : "NULL";
+ const subIssue = sub_issue ? sub_issue.toUpperCase() : "NULL";
// sorting each keys in ascending order
stateKey = stateKey.sort().join("_");
@@ -20,7 +21,7 @@ const paramsToKey = (params: any) => {
createdByKey = createdByKey.sort().join("_");
labelsKey = labelsKey.sort().join("_");
- return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}`;
+ return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}_${subIssue}`;
};
const inboxParamsToKey = (params: any) => {
diff --git a/apps/app/constants/spreadsheet.ts b/apps/app/constants/spreadsheet.ts
new file mode 100644
index 000000000..5ef60f40d
--- /dev/null
+++ b/apps/app/constants/spreadsheet.ts
@@ -0,0 +1,60 @@
+import {
+ CalendarDaysIcon,
+ PlayIcon,
+ Squares2X2Icon,
+ TagIcon,
+ UserGroupIcon,
+} from "@heroicons/react/24/outline";
+
+export const SPREADSHEET_COLUMN = [
+ {
+ propertyName: "title",
+ colName: "Title",
+ colSize: "440px",
+ },
+ {
+ propertyName: "state",
+ colName: "State",
+ colSize: "128px",
+ icon: Squares2X2Icon,
+ ascendingOrder: "state__name",
+ descendingOrder: "-state__name",
+ },
+ {
+ propertyName: "priority",
+ colName: "Priority",
+ colSize: "128px",
+ },
+ {
+ propertyName: "assignee",
+ colName: "Assignees",
+ colSize: "128px",
+ icon: UserGroupIcon,
+ ascendingOrder: "assignees__name",
+ descendingOrder: "-assignees__name",
+ },
+ {
+ propertyName: "labels",
+ colName: "Labels",
+ colSize: "128px",
+ icon: TagIcon,
+ ascendingOrder: "labels__name",
+ descendingOrder: "-labels__name",
+ },
+ {
+ propertyName: "due_date",
+ colName: "Due Date",
+ colSize: "128px",
+ icon: CalendarDaysIcon,
+ ascendingOrder: "target_date",
+ descendingOrder: "-target_date",
+ },
+ {
+ propertyName: "estimate",
+ colName: "Estimate",
+ colSize: "128px",
+ icon: PlayIcon,
+ ascendingOrder: "estimate_point",
+ descendingOrder: "-estimate_point",
+ },
+];
diff --git a/apps/app/hooks/use-spreadsheet-issues-view.tsx b/apps/app/hooks/use-spreadsheet-issues-view.tsx
new file mode 100644
index 000000000..6e7b66bec
--- /dev/null
+++ b/apps/app/hooks/use-spreadsheet-issues-view.tsx
@@ -0,0 +1,125 @@
+import { useContext } from "react";
+
+import { useRouter } from "next/router";
+
+import useSWR from "swr";
+
+// contexts
+import { issueViewContext } from "contexts/issue-view.context";
+// services
+import issuesService from "services/issues.service";
+import cyclesService from "services/cycles.service";
+import modulesService from "services/modules.service";
+// types
+import { IIssue } from "types";
+// fetch-keys
+import {
+ CYCLE_ISSUES_WITH_PARAMS,
+ MODULE_ISSUES_WITH_PARAMS,
+ PROJECT_ISSUES_LIST_WITH_PARAMS,
+ VIEW_ISSUES,
+} from "constants/fetch-keys";
+
+const useSpreadsheetIssuesView = () => {
+ const {
+ issueView,
+ orderBy,
+ setOrderBy,
+ filters,
+ setFilters,
+ resetFilterToDefault,
+ setNewFilterDefaultView,
+ setIssueView,
+ } = useContext(issueViewContext);
+
+ const router = useRouter();
+ const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
+
+ const params: any = {
+ order_by: orderBy,
+ assignees: filters?.assignees ? filters?.assignees.join(",") : undefined,
+ state: filters?.state ? filters?.state.join(",") : undefined,
+ priority: filters?.priority ? filters?.priority.join(",") : undefined,
+ type: filters?.type ? filters?.type : undefined,
+ labels: filters?.labels ? filters?.labels.join(",") : undefined,
+ issue__assignees__id: filters?.issue__assignees__id
+ ? filters?.issue__assignees__id.join(",")
+ : undefined,
+ issue__labels__id: filters?.issue__labels__id
+ ? filters?.issue__labels__id.join(",")
+ : undefined,
+ created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
+ sub_issue: "false",
+ };
+
+ const { data: projectSpreadsheetIssues } = useSWR(
+ workspaceSlug && projectId
+ ? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params)
+ : null,
+ workspaceSlug && projectId
+ ? () =>
+ issuesService.getIssuesWithParams(workspaceSlug.toString(), projectId.toString(), params)
+ : null
+ );
+
+ const { data: cycleSpreadsheetIssues } = useSWR(
+ workspaceSlug && projectId && cycleId
+ ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
+ : null,
+ workspaceSlug && projectId && cycleId
+ ? () =>
+ cyclesService.getCycleIssuesWithParams(
+ workspaceSlug.toString(),
+ projectId.toString(),
+ cycleId.toString(),
+ params
+ )
+ : null
+ );
+
+ const { data: moduleSpreadsheetIssues } = useSWR(
+ workspaceSlug && projectId && moduleId
+ ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
+ : null,
+ workspaceSlug && projectId && moduleId
+ ? () =>
+ modulesService.getModuleIssuesWithParams(
+ workspaceSlug.toString(),
+ projectId.toString(),
+ moduleId.toString(),
+ params
+ )
+ : null
+ );
+
+ const { data: viewSpreadsheetIssues } = useSWR(
+ workspaceSlug && projectId && viewId && params ? VIEW_ISSUES(viewId.toString(), params) : null,
+ workspaceSlug && projectId && viewId && params
+ ? () =>
+ issuesService.getIssuesWithParams(workspaceSlug.toString(), projectId.toString(), params)
+ : null
+ );
+
+ const spreadsheetIssues = cycleId
+ ? (cycleSpreadsheetIssues as IIssue[])
+ : moduleId
+ ? (moduleSpreadsheetIssues as IIssue[])
+ : viewId
+ ? (viewSpreadsheetIssues as IIssue[])
+ : (projectSpreadsheetIssues as IIssue[]);
+
+ return {
+ issueView,
+ spreadsheetIssues: spreadsheetIssues ?? [],
+ orderBy,
+ setOrderBy,
+ filters,
+ setFilters,
+ params,
+ resetFilterToDefault,
+ setNewFilterDefaultView,
+ setIssueView,
+ } as const;
+};
+
+export default useSpreadsheetIssuesView;
diff --git a/apps/app/hooks/use-sub-issue.tsx b/apps/app/hooks/use-sub-issue.tsx
new file mode 100644
index 000000000..8eb30fd0b
--- /dev/null
+++ b/apps/app/hooks/use-sub-issue.tsx
@@ -0,0 +1,34 @@
+import { useEffect, useState } from "react";
+
+import { useRouter } from "next/router";
+
+import useSWR from "swr";
+
+// services
+import issuesService from "services/issues.service";
+// types
+import { ISubIssueResponse } from "types";
+// fetch-keys
+import { SUB_ISSUES } from "constants/fetch-keys";
+
+const useSubIssue = (issueId: string, isExpanded: boolean) => {
+ const router = useRouter();
+ const { workspaceSlug, projectId } = router.query;
+
+ const shouldFetch = workspaceSlug && projectId && issueId && isExpanded;
+
+ const { data: subIssuesResponse, isLoading } = useSWR
(
+ shouldFetch ? SUB_ISSUES(issueId as string) : null,
+ shouldFetch
+ ? () =>
+ issuesService.subIssues(workspaceSlug as string, projectId as string, issueId as string)
+ : null
+ );
+
+ return {
+ subIssues: subIssuesResponse?.sub_issues ?? [],
+ isLoading,
+ };
+};
+
+export default useSubIssue;
diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts
index a8881924b..a33a04ffc 100644
--- a/apps/app/types/issues.d.ts
+++ b/apps/app/types/issues.d.ts
@@ -247,11 +247,25 @@ export interface IIssueFilterOptions {
created_by: string[] | null;
}
-export type TIssueViewOptions = "list" | "kanban" | "calendar" | "gantt_chart";
+export type TIssueViewOptions = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart";
export type TIssueGroupByOptions = "state" | "priority" | "labels" | "created_by" | null;
-export type TIssueOrderByOptions = "-created_at" | "-updated_at" | "priority" | "sort_order";
+export type TIssueOrderByOptions =
+ | "-created_at"
+ | "-updated_at"
+ | "priority"
+ | "sort_order"
+ | "state__name"
+ | "-state__name"
+ | "assignees__name"
+ | "-assignees__name"
+ | "labels__name"
+ | "-labels__name"
+ | "target_date"
+ | "-target_date"
+ | "estimate__point"
+ | "-estimate__point";
export interface IIssueViewOptions {
group_by: TIssueGroupByOptions;
From 8e094aa89597a7d6e11c0b986247aa7048072fa7 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Fri, 23 Jun 2023 17:28:06 +0530
Subject: [PATCH 47/59] chore: triage state filtering (#1372)
---
apiserver/plane/api/views/analytic.py | 4 +++-
apiserver/plane/api/views/importer.py | 8 +++++---
apiserver/plane/api/views/issue.py | 2 +-
apiserver/plane/api/views/state.py | 6 ++++--
apiserver/plane/db/models/issue.py | 6 ++++--
5 files changed, 17 insertions(+), 9 deletions(-)
diff --git a/apiserver/plane/api/views/analytic.py b/apiserver/plane/api/views/analytic.py
index a096c2700..e537af84a 100644
--- a/apiserver/plane/api/views/analytic.py
+++ b/apiserver/plane/api/views/analytic.py
@@ -3,6 +3,7 @@ from django.db.models import (
Count,
Sum,
F,
+ Q
)
from django.db.models.functions import ExtractMonth
@@ -59,10 +60,11 @@ class AnalyticsEndpoint(BaseAPIView):
colors = (
State.objects.filter(
+ ~Q(name="Triage"),
workspace__slug=slug, project_id__in=filters.get("project__in")
).values(key, "color")
if filters.get("project__in", False)
- else State.objects.filter(workspace__slug=slug).values(key, "color")
+ else State.objects.filter(~Q(name="Triage"), workspace__slug=slug).values(key, "color")
)
if x_axis in ["labels__name"] or segment in ["labels__name"]:
diff --git a/apiserver/plane/api/views/importer.py b/apiserver/plane/api/views/importer.py
index e045a2ec1..28d490740 100644
--- a/apiserver/plane/api/views/importer.py
+++ b/apiserver/plane/api/views/importer.py
@@ -7,7 +7,7 @@ from rest_framework.response import Response
from sentry_sdk import capture_exception
# Django imports
-from django.db.models import Max
+from django.db.models import Max, Q
# Module imports
from plane.api.views import BaseAPIView
@@ -309,11 +309,13 @@ class BulkImportIssuesEndpoint(BaseAPIView):
# Get the default state
default_state = State.objects.filter(
- project_id=project_id, default=True
+ ~Q(name="Triage"), project_id=project_id, default=True
).first()
# if there is no default state assign any random state
if default_state is None:
- default_state = State.objects.filter(project_id=project_id).first()
+ default_state = State.objects.filter(
+ ~Q(name="Triage"), sproject_id=project_id
+ ).first()
# Get the maximum sequence_id
last_id = IssueSequence.objects.filter(project_id=project_id).aggregate(
diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py
index 35583fea2..fa7d9d6ec 100644
--- a/apiserver/plane/api/views/issue.py
+++ b/apiserver/plane/api/views/issue.py
@@ -607,7 +607,7 @@ class SubIssuesEndpoint(BaseAPIView):
)
state_distribution = (
- State.objects.filter(workspace__slug=slug, project_id=project_id)
+ State.objects.filter(~Q(name="Triage"), workspace__slug=slug, project_id=project_id)
.annotate(
state_count=Count(
"state_issue",
diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py
index 29cba7a74..4fe0c8260 100644
--- a/apiserver/plane/api/views/state.py
+++ b/apiserver/plane/api/views/state.py
@@ -3,13 +3,13 @@ from itertools import groupby
# Django imports
from django.db import IntegrityError
+from django.db.models import Q
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception
-
# Module imports
from . import BaseViewSet, BaseAPIView
from plane.api.serializers import StateSerializer
@@ -34,6 +34,7 @@ class StateViewSet(BaseViewSet):
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(project__project_projectmember__member=self.request.user)
+ .filter(~Q(name="Triage"))
.select_related("project")
.select_related("workspace")
.distinct()
@@ -80,7 +81,8 @@ class StateViewSet(BaseViewSet):
def destroy(self, request, slug, project_id, pk):
try:
state = State.objects.get(
- pk=pk, project_id=project_id, workspace__slug=slug
+ ~Q(name="Triage"),
+ pk=pk, project_id=project_id, workspace__slug=slug,
)
if state.default:
diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py
index 7efe86d46..1ecad6424 100644
--- a/apiserver/plane/db/models/issue.py
+++ b/apiserver/plane/db/models/issue.py
@@ -98,11 +98,13 @@ class Issue(ProjectBaseModel):
from plane.db.models import State
default_state = State.objects.filter(
- project=self.project, default=True
+ ~models.Q(name="Triage"), project=self.project, default=True
).first()
# if there is no default state assign any random state
if default_state is None:
- random_state = State.objects.filter(project=self.project).first()
+ random_state = State.objects.filter(
+ ~models.Q(name="Triage"), project=self.project
+ ).first()
self.state = random_state
if random_state.group == "started":
self.start_date = timezone.now().date()
From fef83d31536049f0b7d48b2361b0e343119b56f1 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Fri, 23 Jun 2023 17:28:13 +0530
Subject: [PATCH 48/59] fix: inbox issue update (#1373)
---
apiserver/plane/api/views/inbox.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py
index 24fff54ae..ada76c9b3 100644
--- a/apiserver/plane/api/views/inbox.py
+++ b/apiserver/plane/api/views/inbox.py
@@ -250,7 +250,7 @@ class InboxIssueViewSet(BaseViewSet):
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
)
# Only allow guests and viewers to edit name and description
- if project_member <= 10:
+ if project_member.role <= 10:
# viewers and guests since only viewers and guests
issue_data = {
"name": issue_data.get("name", issue.name),
From df41daf71b332bf03f9ac1ab993be45878dc75f1 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Fri, 23 Jun 2023 17:28:21 +0530
Subject: [PATCH 49/59] fix: auth error messages (#1376)
---
apiserver/plane/api/views/authentication.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apiserver/plane/api/views/authentication.py b/apiserver/plane/api/views/authentication.py
index 385ec7568..068fae5a9 100644
--- a/apiserver/plane/api/views/authentication.py
+++ b/apiserver/plane/api/views/authentication.py
@@ -72,7 +72,7 @@ class SignUpEndpoint(BaseAPIView):
# Check if the user already exists
if User.objects.filter(email=email).exists():
return Response(
- {"error": "User already exist please sign in"},
+ {"error": "User with this email already exists"},
status=status.HTTP_400_BAD_REQUEST,
)
From ff87137e404a02fc650c674ada5843ca2cc48cbe Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Fri, 23 Jun 2023 19:29:17 +0530
Subject: [PATCH 50/59] fix: onboarding tracker (#1360)
---
apps/app/services/user.service.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/apps/app/services/user.service.ts b/apps/app/services/user.service.ts
index c6b8494a4..3fe8852a0 100644
--- a/apps/app/services/user.service.ts
+++ b/apps/app/services/user.service.ts
@@ -58,7 +58,6 @@ class UserService extends APIService {
if (trackEvent)
trackEventServices.trackUserOnboardingCompleteEvent(
{
- ...response.data,
user_role: userRole ?? "None",
},
user
From ca799a2b02587ce576776bf591e76f87aa84a95e Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Fri, 23 Jun 2023 19:29:36 +0530
Subject: [PATCH 51/59] fix: update task for ai requests (#1368)
---
apps/app/components/issues/form.tsx | 2 +-
apps/app/components/pages/create-update-block-inline.tsx | 2 +-
apps/app/components/pages/single-page-block.tsx | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx
index cc5cae3f1..e7be252b4 100644
--- a/apps/app/components/issues/form.tsx
+++ b/apps/app/components/issues/form.tsx
@@ -200,7 +200,7 @@ export const IssueForm: FC = ({
projectId as string,
{
prompt: issueName,
- task: "Generate a proper description for this issue in context of a project management software.",
+ task: "Generate a proper description for this issue.",
},
user
)
diff --git a/apps/app/components/pages/create-update-block-inline.tsx b/apps/app/components/pages/create-update-block-inline.tsx
index 91c1108e4..0b3fcb17c 100644
--- a/apps/app/components/pages/create-update-block-inline.tsx
+++ b/apps/app/components/pages/create-update-block-inline.tsx
@@ -195,7 +195,7 @@ export const CreateUpdateBlockInline: React.FC = ({
projectId as string,
{
prompt: watch("name"),
- task: "Generate a proper description for this issue in context of a project management software.",
+ task: "Generate a proper description for this issue.",
},
user
)
diff --git a/apps/app/components/pages/single-page-block.tsx b/apps/app/components/pages/single-page-block.tsx
index 3efbd33eb..7255abbc8 100644
--- a/apps/app/components/pages/single-page-block.tsx
+++ b/apps/app/components/pages/single-page-block.tsx
@@ -194,7 +194,7 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, index,
projectId as string,
{
prompt: block.name,
- task: "Generate a proper description for this issue in context of a project management software.",
+ task: "Generate a proper description for this issue.",
},
user
)
From 428d0dbac9fad925640312490df793084a04e247 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Fri, 23 Jun 2023 19:30:11 +0530
Subject: [PATCH 52/59] fix: words breaking abruptly (#1371)
---
.../app/components/analytics/custom-analytics/sidebar.tsx | 8 ++++----
apps/app/components/analytics/project-modal.tsx | 2 +-
.../components/analytics/scope-and-demand/leaderboard.tsx | 2 +-
apps/app/components/breadcrumbs/index.tsx | 2 +-
apps/app/components/core/board-view/single-issue.tsx | 4 ++--
apps/app/components/core/sidebar/links-list.tsx | 2 +-
apps/app/components/cycles/active-cycle-details.tsx | 2 +-
apps/app/components/cycles/delete-cycle-modal.tsx | 2 +-
apps/app/components/cycles/single-cycle-card.tsx | 4 ++--
apps/app/components/cycles/single-cycle-list.tsx | 8 ++++++--
apps/app/components/estimates/delete-estimate-modal.tsx | 4 ++--
apps/app/components/inbox/decline-issue-modal.tsx | 2 +-
apps/app/components/inbox/delete-issue-modal.tsx | 2 +-
apps/app/components/integration/delete-import-modal.tsx | 2 +-
apps/app/components/issues/delete-issue-modal.tsx | 2 +-
apps/app/components/issues/sub-issues-list.tsx | 2 +-
apps/app/components/modules/delete-module-modal.tsx | 2 +-
apps/app/components/modules/single-module-card.tsx | 2 +-
apps/app/components/pages/delete-page-modal.tsx | 2 +-
apps/app/components/pages/single-page-block.tsx | 2 +-
apps/app/components/project/delete-project-modal.tsx | 8 ++++----
apps/app/components/project/single-project-card.tsx | 2 +-
apps/app/components/ui/multi-level-dropdown.tsx | 2 +-
apps/app/components/views/delete-view-modal.tsx | 2 +-
apps/app/components/workspace/delete-workspace-modal.tsx | 8 ++++----
25 files changed, 42 insertions(+), 38 deletions(-)
diff --git a/apps/app/components/analytics/custom-analytics/sidebar.tsx b/apps/app/components/analytics/custom-analytics/sidebar.tsx
index 5f4700f29..b533df519 100644
--- a/apps/app/components/analytics/custom-analytics/sidebar.tsx
+++ b/apps/app/components/analytics/custom-analytics/sidebar.tsx
@@ -237,7 +237,7 @@ export const AnalyticsSidebar: React.FC = ({
{project?.name.charAt(0)}
)}
-
+
{project.name}
({project.identifier})
@@ -276,7 +276,7 @@ export const AnalyticsSidebar: React.FC = ({
{projectId ? (
cycleId && cycleDetails ? (
-
Analytics for {cycleDetails.name}
+
Analytics for {cycleDetails.name}
Lead
@@ -304,7 +304,7 @@ export const AnalyticsSidebar: React.FC
= ({
) : moduleId && moduleDetails ? (
-
Analytics for {moduleDetails.name}
+
Analytics for {moduleDetails.name}
Lead
@@ -352,7 +352,7 @@ export const AnalyticsSidebar: React.FC
= ({
{projectDetails?.name.charAt(0)}
)}
- {projectDetails?.name}
+ {projectDetails?.name}
diff --git a/apps/app/components/analytics/project-modal.tsx b/apps/app/components/analytics/project-modal.tsx
index da308582f..5fdb6682d 100644
--- a/apps/app/components/analytics/project-modal.tsx
+++ b/apps/app/components/analytics/project-modal.tsx
@@ -160,7 +160,7 @@ export const AnalyticsProjectModal: React.FC
= ({ isOpen, onClose }) => {
}`}
>
-
+
Analytics for{" "}
{cycleId ? cycleDetails?.name : moduleId ? moduleDetails?.name : projectDetails?.name}
diff --git a/apps/app/components/analytics/scope-and-demand/leaderboard.tsx b/apps/app/components/analytics/scope-and-demand/leaderboard.tsx
index 855f9eff4..72b892eeb 100644
--- a/apps/app/components/analytics/scope-and-demand/leaderboard.tsx
+++ b/apps/app/components/analytics/scope-and-demand/leaderboard.tsx
@@ -33,7 +33,7 @@ export const AnalyticsLeaderboard: React.FC = ({ users, title }) => (
{user.firstName !== "" ? user.firstName[0] : "?"}
)}
-
+
{user.firstName !== "" ? `${user.firstName} ${user.lastName}` : "No assignee"}
diff --git a/apps/app/components/breadcrumbs/index.tsx b/apps/app/components/breadcrumbs/index.tsx
index 240faefa2..6e2c85785 100644
--- a/apps/app/components/breadcrumbs/index.tsx
+++ b/apps/app/components/breadcrumbs/index.tsx
@@ -52,7 +52,7 @@ const BreadcrumbItem: React.FC
= ({ title, link, icon }) =>
{icon}
- {title}
+ {title}
)}
diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx
index 3571efa41..5c0cc5102 100644
--- a/apps/app/components/core/board-view/single-issue.tsx
+++ b/apps/app/components/core/board-view/single-issue.tsx
@@ -338,8 +338,8 @@ export const SingleBoardIssue: React.FC = ({
{issue.project_detail.identifier}-{issue.sequence_id}
)}
-
- {truncateText(issue.name, 120)}
+
+ {issue.name}
diff --git a/apps/app/components/core/sidebar/links-list.tsx b/apps/app/components/core/sidebar/links-list.tsx
index 951332f3f..590ebc758 100644
--- a/apps/app/components/core/sidebar/links-list.tsx
+++ b/apps/app/components/core/sidebar/links-list.tsx
@@ -53,7 +53,7 @@ export const LinksList: React.FC = ({ links, handleDeleteLink, userAuth }
-
{link.title}
+
{link.title}
Added {timeAgo(link.created_at)}
diff --git a/apps/app/components/cycles/active-cycle-details.tsx b/apps/app/components/cycles/active-cycle-details.tsx
index b2f27d037..b15dbd3ca 100644
--- a/apps/app/components/cycles/active-cycle-details.tsx
+++ b/apps/app/components/cycles/active-cycle-details.tsx
@@ -226,7 +226,7 @@ export const ActiveCycleDetails: React.FC = () => {
/>
-
+
{truncateText(cycle.name, 70)}
diff --git a/apps/app/components/cycles/delete-cycle-modal.tsx b/apps/app/components/cycles/delete-cycle-modal.tsx
index 3f2de2913..d60e3ddce 100644
--- a/apps/app/components/cycles/delete-cycle-modal.tsx
+++ b/apps/app/components/cycles/delete-cycle-modal.tsx
@@ -143,7 +143,7 @@ export const DeleteCycleModal: React.FC = ({
Are you sure you want to delete cycle-{" "}
-
+
{data?.name}
? All of the data related to the cycle will be permanently removed. This
diff --git a/apps/app/components/cycles/single-cycle-card.tsx b/apps/app/components/cycles/single-cycle-card.tsx
index c6a6365b0..c00429a43 100644
--- a/apps/app/components/cycles/single-cycle-card.tsx
+++ b/apps/app/components/cycles/single-cycle-card.tsx
@@ -150,8 +150,8 @@ export const SingleCycleCard: React.FC = ({
}`}
/>
-
-
+
+
{truncateText(cycle.name, 15)}
diff --git a/apps/app/components/cycles/single-cycle-list.tsx b/apps/app/components/cycles/single-cycle-list.tsx
index d957b5ab7..fa725b83a 100644
--- a/apps/app/components/cycles/single-cycle-list.tsx
+++ b/apps/app/components/cycles/single-cycle-list.tsx
@@ -173,8 +173,12 @@ export const SingleCycleList: React.FC = ({
}`}
/>
-
-
+
+
{truncateText(cycle.name, 70)}
diff --git a/apps/app/components/estimates/delete-estimate-modal.tsx b/apps/app/components/estimates/delete-estimate-modal.tsx
index c456ceab6..5a4f9ccfa 100644
--- a/apps/app/components/estimates/delete-estimate-modal.tsx
+++ b/apps/app/components/estimates/delete-estimate-modal.tsx
@@ -74,9 +74,9 @@ export const DeleteEstimateModal: React.FC = ({
-
+
Are you sure you want to delete estimate-{" "}
- {data.name}
+ {data.name}
{""}? All of the data related to the estiamte will be permanently removed.
This action cannot be undone.
diff --git a/apps/app/components/inbox/decline-issue-modal.tsx b/apps/app/components/inbox/decline-issue-modal.tsx
index 941841659..11f1db5de 100644
--- a/apps/app/components/inbox/decline-issue-modal.tsx
+++ b/apps/app/components/inbox/decline-issue-modal.tsx
@@ -72,7 +72,7 @@ export const DeclineIssueModal: React.FC = ({ isOpen, handleClose, data,
Are you sure you want to decline issue{" "}
-
+
{data?.project_detail?.identifier}-{data?.sequence_id}
{""}? This action cannot be undone.
diff --git a/apps/app/components/inbox/delete-issue-modal.tsx b/apps/app/components/inbox/delete-issue-modal.tsx
index c6f5320a2..f188ff1aa 100644
--- a/apps/app/components/inbox/delete-issue-modal.tsx
+++ b/apps/app/components/inbox/delete-issue-modal.tsx
@@ -127,7 +127,7 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data })
Are you sure you want to delete issue{" "}
-
+
{data?.project_detail?.identifier}-{data?.sequence_id}
{""}? The issue will only be deleted from the inbox and this action cannot be
diff --git a/apps/app/components/integration/delete-import-modal.tsx b/apps/app/components/integration/delete-import-modal.tsx
index cd0b12a2a..57af3fbfc 100644
--- a/apps/app/components/integration/delete-import-modal.tsx
+++ b/apps/app/components/integration/delete-import-modal.tsx
@@ -104,7 +104,7 @@ export const DeleteImportModal: React.FC = ({ isOpen, handleClose, data,
Are you sure you want to delete import from{" "}
-
+
{data?.service}
? All of the data related to the import will be permanently removed. This
diff --git a/apps/app/components/issues/delete-issue-modal.tsx b/apps/app/components/issues/delete-issue-modal.tsx
index ffdebb314..000b21387 100644
--- a/apps/app/components/issues/delete-issue-modal.tsx
+++ b/apps/app/components/issues/delete-issue-modal.tsx
@@ -151,7 +151,7 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, u
Are you sure you want to delete issue{" "}
-
+
{data?.project_detail.identifier}-{data?.sequence_id}
{""}? All of the data related to the issue will be permanently removed. This
diff --git a/apps/app/components/issues/sub-issues-list.tsx b/apps/app/components/issues/sub-issues-list.tsx
index ac550348e..3558c8d74 100644
--- a/apps/app/components/issues/sub-issues-list.tsx
+++ b/apps/app/components/issues/sub-issues-list.tsx
@@ -282,7 +282,7 @@ export const SubIssuesList: FC = ({ parentIssue, user }) => {
{issue.project_detail.identifier}-{issue.sequence_id}
- {issue.name}
+ {issue.name}
{!isNotAllowed && (
diff --git a/apps/app/components/modules/delete-module-modal.tsx b/apps/app/components/modules/delete-module-modal.tsx
index f2a9ec7ee..deece2ea5 100644
--- a/apps/app/components/modules/delete-module-modal.tsx
+++ b/apps/app/components/modules/delete-module-modal.tsx
@@ -111,7 +111,7 @@ export const DeleteModuleModal: React.FC = ({ isOpen, setIsOpen, data, us
Are you sure you want to delete module-{" "}
-
+
{data?.name}
? All of the data related to the module will be permanently removed. This
diff --git a/apps/app/components/modules/single-module-card.tsx b/apps/app/components/modules/single-module-card.tsx
index 21eb25bbb..ac92bdcdd 100644
--- a/apps/app/components/modules/single-module-card.tsx
+++ b/apps/app/components/modules/single-module-card.tsx
@@ -138,7 +138,7 @@ export const SingleModuleCard: React.FC = ({ module, handleEditModule, us
-
+
{truncateText(module.name, 75)}
diff --git a/apps/app/components/pages/delete-page-modal.tsx b/apps/app/components/pages/delete-page-modal.tsx
index 6277870d1..eaa7c2189 100644
--- a/apps/app/components/pages/delete-page-modal.tsx
+++ b/apps/app/components/pages/delete-page-modal.tsx
@@ -136,7 +136,7 @@ export const DeletePageModal: React.FC = ({
Are you sure you want to delete Page-{" "}
-
+
{data?.name}
? All of the data related to the page will be permanently removed. This
diff --git a/apps/app/components/pages/single-page-block.tsx b/apps/app/components/pages/single-page-block.tsx
index 7255abbc8..898f4aba5 100644
--- a/apps/app/components/pages/single-page-block.tsx
+++ b/apps/app/components/pages/single-page-block.tsx
@@ -417,7 +417,7 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, index,
setCreateBlockForm(true)}
>
diff --git a/apps/app/components/project/delete-project-modal.tsx b/apps/app/components/project/delete-project-modal.tsx
index 5a4be1706..eabe85f2d 100644
--- a/apps/app/components/project/delete-project-modal.tsx
+++ b/apps/app/components/project/delete-project-modal.tsx
@@ -128,13 +128,13 @@ export const DeleteProjectModal: React.FC
= ({
Are you sure you want to delete project{" "}
- {selectedProject?.name} ? All
- of the data related to the project will be permanently removed. This action
- cannot be undone
+ {selectedProject?.name} ?
+ All of the data related to the project will be permanently removed. This
+ action cannot be undone
-
+
Enter the project name{" "}
{selectedProject?.name} {" "}
to continue:
diff --git a/apps/app/components/project/single-project-card.tsx b/apps/app/components/project/single-project-card.tsx
index 66ef6aa2a..04e56652d 100644
--- a/apps/app/components/project/single-project-card.tsx
+++ b/apps/app/components/project/single-project-card.tsx
@@ -195,7 +195,7 @@ export const SingleProjectCard: React.FC = ({
) : null}
-
+
{truncateText(project.description ?? "", 100)}
diff --git a/apps/app/components/ui/multi-level-dropdown.tsx b/apps/app/components/ui/multi-level-dropdown.tsx
index 0f25d06b3..e93fba887 100644
--- a/apps/app/components/ui/multi-level-dropdown.tsx
+++ b/apps/app/components/ui/multi-level-dropdown.tsx
@@ -127,7 +127,7 @@ export const MultiLevelDropdown: React.FC = ({
}}
className={`${
child.selected ? "bg-brand-surface-2" : ""
- } flex w-full items-center whitespace-nowrap break-all rounded px-1 py-1.5 text-left capitalize text-brand-secondary hover:bg-brand-surface-2`}
+ } flex w-full items-center whitespace-nowrap break-words rounded px-1 py-1.5 text-left capitalize text-brand-secondary hover:bg-brand-surface-2`}
>
{child.label}
diff --git a/apps/app/components/views/delete-view-modal.tsx b/apps/app/components/views/delete-view-modal.tsx
index c57c29dc3..fa5e6781c 100644
--- a/apps/app/components/views/delete-view-modal.tsx
+++ b/apps/app/components/views/delete-view-modal.tsx
@@ -115,7 +115,7 @@ export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, user
Are you sure you want to delete view-{" "}
-
+
{data?.name}
? All of the data related to the view will be permanently removed. This
diff --git a/apps/app/components/workspace/delete-workspace-modal.tsx b/apps/app/components/workspace/delete-workspace-modal.tsx
index 344d700b0..b9f3e60f4 100644
--- a/apps/app/components/workspace/delete-workspace-modal.tsx
+++ b/apps/app/components/workspace/delete-workspace-modal.tsx
@@ -120,14 +120,14 @@ export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose, u
Are you sure you want to delete workspace{" "}
- {data?.name} ? All of the data
- related to the workspace will be permanently removed. This action cannot be
- undone.
+ {data?.name} ? All of the
+ data related to the workspace will be permanently removed. This action cannot
+ be undone.
-
+
Enter the workspace name{" "}
{selectedWorkspace?.name} {" "}
to continue:
From b87e2fff4cb72a1601b270026c80a7aaacbc512e Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Fri, 23 Jun 2023 19:30:32 +0530
Subject: [PATCH 53/59] chore: show error messages from request (#1375)
---
apps/app/pages/index.tsx | 34 +++++++++------------
apps/app/pages/reset-password.tsx | 6 ++--
apps/app/pages/sign-up.tsx | 23 ++++++--------
apps/app/services/authentication.service.ts | 3 +-
4 files changed, 30 insertions(+), 36 deletions(-)
diff --git a/apps/app/pages/index.tsx b/apps/app/pages/index.tsx
index ab2633241..abe317415 100644
--- a/apps/app/pages/index.tsx
+++ b/apps/app/pages/index.tsx
@@ -46,14 +46,12 @@ const HomePage: NextPage = () => {
} else {
throw Error("Cant find credentials");
}
- } catch (error: any) {
- console.log(error);
+ } catch (err: any) {
setToastAlert({
title: "Error signing in!",
type: "error",
message:
- error?.error ||
- "Something went wrong. Please try again later or contact the support team.",
+ err?.error || "Something went wrong. Please try again later or contact the support team.",
});
}
};
@@ -71,13 +69,12 @@ const HomePage: NextPage = () => {
} else {
throw Error("Cant find credentials");
}
- } catch (error: any) {
+ } catch (err: any) {
setToastAlert({
title: "Error signing in!",
type: "error",
message:
- error?.error ||
- "Something went wrong. Please try again later or contact the support team.",
+ err?.error || "Something went wrong. Please try again later or contact the support team.",
});
}
};
@@ -88,22 +85,23 @@ const HomePage: NextPage = () => {
.then((response) => {
try {
if (response) mutateUser();
- } catch (error: any) {
- console.log(error);
+ } catch (err: any) {
setToastAlert({
- title: "Error signing in!",
type: "error",
+ title: "Error!",
message:
- error?.error ||
+ err?.error ||
"Something went wrong. Please try again later or contact the support team.",
});
}
})
- .catch(() =>
+ .catch((err) =>
setToastAlert({
- title: "Oops!",
type: "error",
- message: "Enter the correct email address and password to sign in",
+ title: "Error!",
+ message:
+ err?.error ||
+ "Something went wrong. Please try again later or contact the support team.",
})
);
};
@@ -111,14 +109,12 @@ const HomePage: NextPage = () => {
const handleEmailCodeSignIn = async (response: any) => {
try {
if (response) mutateUser();
- } catch (error: any) {
- console.log(error);
+ } catch (err: any) {
setToastAlert({
- title: "Error signing in!",
type: "error",
+ title: "Error!",
message:
- error?.error ||
- "Something went wrong. Please try again later or contact the support team.",
+ err?.error || "Something went wrong. Please try again later or contact the support team.",
});
}
};
diff --git a/apps/app/pages/reset-password.tsx b/apps/app/pages/reset-password.tsx
index 86ab556b1..82ee32114 100644
--- a/apps/app/pages/reset-password.tsx
+++ b/apps/app/pages/reset-password.tsx
@@ -63,11 +63,13 @@ const ResetPasswordPage: NextPage = () => {
});
router.push("/");
})
- .catch(() =>
+ .catch((err) =>
setToastAlert({
type: "error",
title: "Error!",
- message: "Something went wrong. Please try again.",
+ message:
+ err?.error ||
+ "Something went wrong. Please try again later or contact the support team.",
})
);
};
diff --git a/apps/app/pages/sign-up.tsx b/apps/app/pages/sign-up.tsx
index eeffb53bd..2c86d96ff 100644
--- a/apps/app/pages/sign-up.tsx
+++ b/apps/app/pages/sign-up.tsx
@@ -47,20 +47,15 @@ const SignUp: NextPage = () => {
if (response) await mutateUser();
router.push("/");
})
- .catch((err) => {
- if (err.status === 400)
- setToastAlert({
- type: "error",
- title: "Error!",
- message: "An user already exists with this Email ID.",
- });
- else
- setToastAlert({
- type: "error",
- title: "Error!",
- message: "Something went wrong. Please try again later or contact the support team.",
- });
- });
+ .catch((err) =>
+ setToastAlert({
+ type: "error",
+ title: "Error!",
+ message:
+ err?.error ||
+ "Something went wrong. Please try again later or contact the support team.",
+ })
+ );
};
return (
diff --git a/apps/app/services/authentication.service.ts b/apps/app/services/authentication.service.ts
index f0d19da24..86f55e329 100644
--- a/apps/app/services/authentication.service.ts
+++ b/apps/app/services/authentication.service.ts
@@ -28,7 +28,7 @@ class AuthService extends APIService {
return response?.data;
})
.catch((error) => {
- throw error?.response;
+ throw error?.response?.data;
});
}
@@ -51,6 +51,7 @@ class AuthService extends APIService {
throw error?.response?.data;
});
}
+
async magicSignIn(data: any) {
const response = await this.post("/api/magic-sign-in/", data, { headers: {} });
if (response?.status === 200) {
From ca7d3309d31e44336d68e57fcd572e8699373074 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Fri, 23 Jun 2023 19:30:53 +0530
Subject: [PATCH 54/59] chore: accept issue confirmation modal (#1377)
* chore: accept issue confirmation modal
* chore: add inbox option to the command menu
* fix: status colors not loading
* chore: show state name on the inbox issue sidebar
---
.../command-palette/command-pallette.tsx | 54 ++++++++---
.../components/inbox/accept-issue-modal.tsx | 92 +++++++++++++++++++
.../components/inbox/inbox-action-headers.tsx | 26 +++---
.../components/inbox/inbox-main-content.tsx | 4 +-
apps/app/components/inbox/index.ts | 1 +
.../components/inbox/issues-list-sidebar.tsx | 5 +-
.../issues/sidebar-select/state.tsx | 29 ++++--
.../components/ui/multi-level-dropdown.tsx | 2 +-
apps/app/styles/command-pallette.css | 4 +-
9 files changed, 177 insertions(+), 40 deletions(-)
create mode 100644 apps/app/components/inbox/accept-issue-modal.tsx
diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx
index cb2330b24..32634f18c 100644
--- a/apps/app/components/command-palette/command-pallette.tsx
+++ b/apps/app/components/command-palette/command-pallette.tsx
@@ -9,6 +9,7 @@ import {
ChatBubbleOvalLeftEllipsisIcon,
DocumentTextIcon,
FolderPlusIcon,
+ InboxIcon,
LinkIcon,
MagnifyingGlassIcon,
RocketLaunchIcon,
@@ -34,6 +35,7 @@ import { Dialog, Transition } from "@headlessui/react";
// cmdk
import { Command } from "cmdk";
// hooks
+import useProjectDetails from "hooks/use-project-details";
import useTheme from "hooks/use-theme";
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
@@ -64,10 +66,11 @@ import {
// services
import issuesService from "services/issues.service";
import workspaceService from "services/workspace.service";
+import inboxService from "services/inbox.service";
// types
import { IIssue, IWorkspaceSearchResults } from "types";
// fetch keys
-import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
+import { INBOX_LIST, ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
export const CommandPalette: React.FC = () => {
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
@@ -105,6 +108,8 @@ export const CommandPalette: React.FC = () => {
const { workspaceSlug, projectId, issueId, inboxId } = router.query;
const { user } = useUser();
+ const { projectDetails } = useProjectDetails();
+
const { setToastAlert } = useToast();
const { toggleCollapsed } = useTheme();
@@ -116,6 +121,13 @@ export const CommandPalette: React.FC = () => {
: null
);
+ const { data: inboxList } = useSWR(
+ workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null,
+ workspaceSlug && projectId
+ ? () => inboxService.getInboxes(workspaceSlug as string, projectId as string)
+ : null
+ );
+
const updateIssue = useCallback(
async (formData: Partial) => {
if (!workspaceSlug || !projectId || !issueId) return;
@@ -321,9 +333,9 @@ export const CommandPalette: React.FC = () => {
setDeleteIssueModal(true);
};
- const goToSettings = (path: string = "") => {
+ const redirect = (path: string) => {
setIsPaletteOpen(false);
- router.push(`/${workspaceSlug}/settings/${path}`);
+ router.push(path);
};
return (
@@ -396,7 +408,7 @@ export const CommandPalette: React.FC = () => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
-
+
@@ -409,14 +421,14 @@ export const CommandPalette: React.FC = () => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
-
+
{
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
return 0;
}}
onKeyDown={(e) => {
- // when seach is empty and page is undefined
+ // when search is empty and page is undefined
// when user tries to close the modal with esc
if (e.key === "Escape" && !page && !searchTerm) {
setIsPaletteOpen(false);
@@ -698,6 +710,24 @@ export const CommandPalette: React.FC = () => {
D
+
+ {projectDetails && projectDetails.inbox_view && (
+
+
+ redirect(
+ `/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`
+ )
+ }
+ className="focus:outline-none"
+ >
+
+
+ Open inbox
+
+
+
+ )}
>
)}
@@ -814,7 +844,7 @@ export const CommandPalette: React.FC = () => {
{page === "settings" && workspaceSlug && (
<>
goToSettings()}
+ onSelect={() => redirect(`/${workspaceSlug}/settings`)}
className="focus:outline-none"
>
@@ -823,7 +853,7 @@ export const CommandPalette: React.FC = () => {
goToSettings("members")}
+ onSelect={() => redirect(`/${workspaceSlug}/settings/members`)}
className="focus:outline-none"
>
@@ -832,7 +862,7 @@ export const CommandPalette: React.FC = () => {
goToSettings("billing")}
+ onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)}
className="focus:outline-none"
>
@@ -841,7 +871,7 @@ export const CommandPalette: React.FC = () => {
goToSettings("integrations")}
+ onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)}
className="focus:outline-none"
>
@@ -850,12 +880,12 @@ export const CommandPalette: React.FC = () => {
goToSettings("import-export")}
+ onSelect={() => redirect(`/${workspaceSlug}/settings/import-export`)}
className="focus:outline-none"
>
- Import/ Export
+ Import/Export
>
diff --git a/apps/app/components/inbox/accept-issue-modal.tsx b/apps/app/components/inbox/accept-issue-modal.tsx
new file mode 100644
index 000000000..6427c562c
--- /dev/null
+++ b/apps/app/components/inbox/accept-issue-modal.tsx
@@ -0,0 +1,92 @@
+import React, { useState } from "react";
+
+// headless ui
+import { Dialog, Transition } from "@headlessui/react";
+// icons
+import { CheckCircleIcon } from "@heroicons/react/24/outline";
+// ui
+import { SecondaryButton, PrimaryButton } from "components/ui";
+// types
+import type { IInboxIssue } from "types";
+
+type Props = {
+ isOpen: boolean;
+ handleClose: () => void;
+ data: IInboxIssue | undefined;
+ onSubmit: () => Promise;
+};
+
+export const AcceptIssueModal: React.FC = ({ isOpen, handleClose, data, onSubmit }) => {
+ const [isAccepting, setIsAccepting] = useState(false);
+
+ const onClose = () => {
+ setIsAccepting(false);
+ handleClose();
+ };
+
+ const handleAccept = () => {
+ setIsAccepting(true);
+
+ onSubmit().finally(() => setIsAccepting(false));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Accept Issue
+
+
+
+
+ Are you sure you want to accept issue{" "}
+
+ {data?.project_detail?.identifier}-{data?.sequence_id}
+
+ {""}? Once accepted, this issue will be added to the project issues list.
+
+
+
+
Cancel
+
+ {isAccepting ? "Accepting..." : "Accept Issue"}
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/app/components/inbox/inbox-action-headers.tsx b/apps/app/components/inbox/inbox-action-headers.tsx
index 5702d560b..ac144ba0b 100644
--- a/apps/app/components/inbox/inbox-action-headers.tsx
+++ b/apps/app/components/inbox/inbox-action-headers.tsx
@@ -18,6 +18,7 @@ import useUserAuth from "hooks/use-user-auth";
import useToast from "hooks/use-toast";
// components
import {
+ AcceptIssueModal,
DeclineIssueModal,
DeleteIssueModal,
FiltersDropdown,
@@ -41,9 +42,9 @@ import type { IInboxIssueDetail, TInboxStatus } from "types";
import { INBOX_ISSUE_DETAILS } from "constants/fetch-keys";
export const InboxActionHeader = () => {
- const [isAccepting, setIsAccepting] = useState(false);
const [date, setDate] = useState(new Date());
const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false);
+ const [acceptIssueModal, setAcceptIssueModal] = useState(false);
const [declineIssueModal, setDeclineIssueModal] = useState(false);
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
@@ -102,14 +103,6 @@ export const InboxActionHeader = () => {
});
};
- const handleAcceptIssue = () => {
- setIsAccepting(true);
-
- markInboxStatus({
- status: 1,
- }).finally(() => setIsAccepting(false));
- };
-
const issue = inboxIssues?.find((issue) => issue.bridge_id === inboxIssueId);
const currentIssueIndex =
inboxIssues?.findIndex((issue) => issue.bridge_id === inboxIssueId) ?? 0;
@@ -144,6 +137,16 @@ export const InboxActionHeader = () => {
}).finally(() => setSelectDuplicateIssue(false));
}}
/>
+ setAcceptIssueModal(false)}
+ data={inboxIssues?.find((i) => i.bridge_id === inboxIssueId)}
+ onSubmit={async () => {
+ await markInboxStatus({
+ status: 1,
+ }).finally(() => setAcceptIssueModal(false));
+ }}
+ />
setDeclineIssueModal(false)}
@@ -252,11 +255,10 @@ export const InboxActionHeader = () => {
setAcceptIssueModal(true)}
>
- {isAccepting ? "Accepting..." : "Accept"}
+ Accept
)}
diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx
index d03368944..a75c00317 100644
--- a/apps/app/components/inbox/inbox-main-content.tsx
+++ b/apps/app/components/inbox/inbox-main-content.tsx
@@ -227,7 +227,9 @@ export const InboxMainContent: React.FC = () => {
issueStatus === 0 &&
new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date()
? "text-red-500 border-red-500 bg-red-500/10"
- : `${inboxStatusDetails?.textColor} ${inboxStatusDetails?.bgColor} ${inboxStatusDetails?.borderColor}`
+ : inboxStatusDetails
+ ? `${inboxStatusDetails.textColor} ${inboxStatusDetails.bgColor} ${inboxStatusDetails.borderColor}`
+ : ""
}`}
>
{issueStatus === -2 ? (
diff --git a/apps/app/components/inbox/index.ts b/apps/app/components/inbox/index.ts
index 7cdd8ee9d..38cea0348 100644
--- a/apps/app/components/inbox/index.ts
+++ b/apps/app/components/inbox/index.ts
@@ -1,3 +1,4 @@
+export * from "./accept-issue-modal";
export * from "./decline-issue-modal";
export * from "./delete-issue-modal";
export * from "./filters-dropdown";
diff --git a/apps/app/components/inbox/issues-list-sidebar.tsx b/apps/app/components/inbox/issues-list-sidebar.tsx
index 9f5c85db1..6126be117 100644
--- a/apps/app/components/inbox/issues-list-sidebar.tsx
+++ b/apps/app/components/inbox/issues-list-sidebar.tsx
@@ -11,7 +11,7 @@ export const IssuesListSidebar = () => {
const router = useRouter();
const { inboxIssueId } = router.query;
- const { issues: inboxIssues } = useInboxView();
+ const { issues: inboxIssues, filtersLength } = useInboxView();
return (
@@ -29,7 +29,8 @@ export const IssuesListSidebar = () => {
) : (
- No issues found for the selected filters. Try changing the filters.
+ {filtersLength > 0 &&
+ "No issues found for the selected filters. Try changing the filters."}
)
) : (
diff --git a/apps/app/components/issues/sidebar-select/state.tsx b/apps/app/components/issues/sidebar-select/state.tsx
index 8abb362db..02d1dd5cb 100644
--- a/apps/app/components/issues/sidebar-select/state.tsx
+++ b/apps/app/components/issues/sidebar-select/state.tsx
@@ -27,7 +27,7 @@ type Props = {
export const SidebarStateSelect: React.FC = ({ value, onChange, userAuth }) => {
const router = useRouter();
- const { workspaceSlug, projectId } = router.query;
+ const { workspaceSlug, projectId, inboxIssueId } = router.query;
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
@@ -50,15 +50,24 @@ export const SidebarStateSelect: React.FC = ({ value, onChange, userAuth
- {getStateGroupIcon(
- selectedState?.group ?? "backlog",
- "16",
- "16",
- selectedState?.color ?? ""
- )}
- {addSpaceIfCamelCase(selectedState?.name ?? "")}
-
+ selectedState ? (
+
+ {getStateGroupIcon(
+ selectedState?.group ?? "backlog",
+ "16",
+ "16",
+ selectedState?.color ?? ""
+ )}
+ {addSpaceIfCamelCase(selectedState?.name ?? "")}
+
+ ) : inboxIssueId ? (
+
+ {getStateGroupIcon("backlog", "16", "16", "#ff7700")}
+ Triage
+
+ ) : (
+ "None"
+ )
}
value={value}
onChange={onChange}
diff --git a/apps/app/components/ui/multi-level-dropdown.tsx b/apps/app/components/ui/multi-level-dropdown.tsx
index e93fba887..b0997972f 100644
--- a/apps/app/components/ui/multi-level-dropdown.tsx
+++ b/apps/app/components/ui/multi-level-dropdown.tsx
@@ -35,7 +35,7 @@ export const MultiLevelDropdown: React.FC = ({
const [openChildFor, setOpenChildFor] = useState(null);
return (
-
+
{({ open }) => (
<>
diff --git a/apps/app/styles/command-pallette.css b/apps/app/styles/command-pallette.css
index 5362f308a..a421eeba9 100644
--- a/apps/app/styles/command-pallette.css
+++ b/apps/app/styles/command-pallette.css
@@ -31,9 +31,9 @@
}
[cmdk-item]:hover {
- background-color: rgba(var(--color-bg-base));
+ background-color: rgba(var(--color-bg-surface-2));
}
[cmdk-item][aria-selected="true"] {
- background-color: rgba(var(--color-bg-base));
+ background-color: rgba(var(--color-bg-surface-2));
}
From 160cc014a731a36626e97b29059cf759120610e2 Mon Sep 17 00:00:00 2001
From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Date: Fri, 23 Jun 2023 21:33:24 +0530
Subject: [PATCH 55/59] feat: spreadsheet view improvements (#1379)
* feat: quick menu for spreadsheet view added ,style: spreadsheet view column updated ,fix: z-index issue
* feat: sorting indicator, style: spreadsheet column
---
.../core/spreadsheet-view/single-issue.tsx | 23 ++-
.../spreadsheet-view/spreadsheet-columns.tsx | 180 +++++++++++-------
.../spreadsheet-view/spreadsheet-view.tsx | 2 +-
apps/app/constants/spreadsheet.ts | 2 +
4 files changed, 125 insertions(+), 82 deletions(-)
diff --git a/apps/app/components/core/spreadsheet-view/single-issue.tsx b/apps/app/components/core/spreadsheet-view/single-issue.tsx
index d3086e254..f763e706c 100644
--- a/apps/app/components/core/spreadsheet-view/single-issue.tsx
+++ b/apps/app/components/core/spreadsheet-view/single-issue.tsx
@@ -128,15 +128,15 @@ export const SingleSpreadsheetIssue: React.FC
= ({
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-brand-surface-2 border-b border-brand-base w-full min-w-max"
style={{ gridTemplateColumns }}
>
-
+
- {properties.key && (
- <>
-
+
+ {properties.key && (
+
{issue.project_detail?.identifier}-{issue.sequence_id}
-
- >
- )}
+
+ )}
+
{issue.sub_issues_count > 0 && (
@@ -237,9 +237,14 @@ export const SingleSpreadsheetIssue: React.FC
= ({
/>
)}
-
+
{!isNotAllowed && (
-
+
diff --git a/apps/app/components/core/spreadsheet-view/spreadsheet-columns.tsx b/apps/app/components/core/spreadsheet-view/spreadsheet-columns.tsx
index 9f615f165..85d05a288 100644
--- a/apps/app/components/core/spreadsheet-view/spreadsheet-columns.tsx
+++ b/apps/app/components/core/spreadsheet-view/spreadsheet-columns.tsx
@@ -19,13 +19,17 @@ export const SpreadsheetColumns: React.FC
= ({ columnData, gridTemplateCo
"spreadsheetViewSorting",
""
);
+ const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } =
+ useLocalStorage("spreadsheetViewActiveSortingProperty", "");
const { orderBy, setOrderBy } = useSpreadsheetIssuesView();
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
setOrderBy(order);
setSelectedMenuItem(`${order}_${itemKey}`);
+ setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
};
+
return (
= ({ columnData, gridTemplateCo
if (col.isActive) {
return (
- {col.propertyName === "title" || col.propertyName === "priority" ? (
+ {col.propertyName === "title" ? (
- {col.icon ? (
-
- ) : col.propertyName === "priority" ? (
-
- signal_cellular_alt
-
- ) : (
- ""
- )}
-
{col.colName}
) : (
+ {activeSortingProperty === col.propertyName && (
+
+
+
+ )}
+
{col.icon ? (
= ({ columnData, gridTemplateCo
: "text-brand-secondary hover:text-brand-base"
}`}
>
-
+
{col.propertyName === "assignee" || col.propertyName === "labels" ? (
<>
-
A-Z
-
Ascending
+
+
+
+
+
A
+
+
Z
>
) : col.propertyName === "due_date" ? (
<>
-
1-9
-
Ascending
- >
- ) : col.propertyName === "estimate" ? (
- <>
-
0
+
+
+
+
+
New
-
10
+
Old
>
) : (
<>
+
+
+
+
First
Last
@@ -146,7 +162,7 @@ export const SpreadsheetColumns: React.FC
= ({ columnData, gridTemplateCo
= ({ columnData, gridTemplateCo
: "text-brand-secondary hover:text-brand-base"
}`}
>
-
+
{col.propertyName === "assignee" || col.propertyName === "labels" ? (
<>
-
Z-A
-
Descending
+
+
+
+
+
Z
+
+
A
>
) : col.propertyName === "due_date" ? (
<>
-
9-1
-
Descending
- >
- ) : col.propertyName === "estimate" ? (
- <>
-
10
+
+
+
+
+
Old
-
0
+
New
>
) : (
<>
+
+
+
+
Last
First
@@ -198,38 +240,32 @@ export const SpreadsheetColumns: React.FC
= ({ columnData, gridTemplateCo
/>
-
{
- handleOrderBy("-created_at", col.propertyName);
- }}
- >
-
-
+ key={col.property}
+ onClick={() => {
+ handleOrderBy("-created_at", col.propertyName);
+ }}
+ >
+
+
+
+
+
+
+ Clear sorting
+
+
+
+ )}
)}
diff --git a/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx b/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx
index 6f36b2dbb..c47ddd805 100644
--- a/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx
+++ b/apps/app/components/core/spreadsheet-view/spreadsheet-view.tsx
@@ -56,7 +56,7 @@ export const SpreadsheetView: React.FC = ({
return (
-
+
{spreadsheetIssues ? (
diff --git a/apps/app/constants/spreadsheet.ts b/apps/app/constants/spreadsheet.ts
index 5ef60f40d..8e4bdb21d 100644
--- a/apps/app/constants/spreadsheet.ts
+++ b/apps/app/constants/spreadsheet.ts
@@ -24,6 +24,8 @@ export const SPREADSHEET_COLUMN = [
propertyName: "priority",
colName: "Priority",
colSize: "128px",
+ ascendingOrder: "priority",
+ descendingOrder: "-priority",
},
{
propertyName: "assignee",
From ccbe773ce1d7a76e2f515b85b2f59aeaa8c82499 Mon Sep 17 00:00:00 2001
From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Date: Fri, 23 Jun 2023 22:18:03 +0530
Subject: [PATCH 56/59] fix: state and priority ordering (#1378)
---
apiserver/plane/api/views/issue.py | 40 +++++++++++++++++++++++++++---
1 file changed, 36 insertions(+), 4 deletions(-)
diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py
index fa7d9d6ec..dba7a7a2f 100644
--- a/apiserver/plane/api/views/issue.py
+++ b/apiserver/plane/api/views/issue.py
@@ -152,8 +152,9 @@ class IssueViewSet(BaseViewSet):
filters = issue_filters(request.query_params, "GET")
show_sub_issues = request.GET.get("show_sub_issues", "true")
- # Custom ordering for priority
+ # 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")
@@ -178,7 +179,13 @@ class IssueViewSet(BaseViewSet):
)
)
- if order_by_param == "priority":
+ # 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(
*[
@@ -188,6 +195,29 @@ class IssueViewSet(BaseViewSet):
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")
else:
issue_queryset = issue_queryset.order_by(order_by_param)
@@ -209,7 +239,7 @@ class IssueViewSet(BaseViewSet):
return Response(issues, status=status.HTTP_200_OK)
except Exception as e:
- capture_exception(e)
+ print(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
@@ -607,7 +637,9 @@ class SubIssuesEndpoint(BaseAPIView):
)
state_distribution = (
- State.objects.filter(~Q(name="Triage"), workspace__slug=slug, project_id=project_id)
+ State.objects.filter(
+ ~Q(name="Triage"), workspace__slug=slug, project_id=project_id
+ )
.annotate(
state_count=Count(
"state_issue",
From ddaa8df1c5542bee9e3866a54491b4f783a35a03 Mon Sep 17 00:00:00 2001
From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Date: Sat, 24 Jun 2023 00:35:00 +0530
Subject: [PATCH 57/59] fix: progress chart to show ideal line only when data
is present (#1384)
---
.../core/sidebar/progress-chart.tsx | 23 +++++++++++--------
1 file changed, 13 insertions(+), 10 deletions(-)
diff --git a/apps/app/components/core/sidebar/progress-chart.tsx b/apps/app/components/core/sidebar/progress-chart.tsx
index 47af406cc..8c7dba11e 100644
--- a/apps/app/components/core/sidebar/progress-chart.tsx
+++ b/apps/app/components/core/sidebar/progress-chart.tsx
@@ -93,16 +93,19 @@ const ProgressChart: React.FC
= ({ distribution, startDate, endDate, tota
id: "ideal",
color: "#a9bbd0",
fill: "transparent",
- data: [
- {
- x: chartData[0].currentDate,
- y: totalIssues,
- },
- {
- x: chartData[chartData.length - 1].currentDate,
- y: 0,
- },
- ],
+ data:
+ chartData.length > 0
+ ? [
+ {
+ x: chartData[0].currentDate,
+ y: totalIssues,
+ },
+ {
+ x: chartData[chartData.length - 1].currentDate,
+ y: 0,
+ },
+ ]
+ : [],
},
]}
layers={["grid", "markers", "areas", DashedLine, "slices", "points", "axes", "legends"]}
From 5ada1e539784299ed6d14e3476e8e06164ed8a0d Mon Sep 17 00:00:00 2001
From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Date: Sat, 24 Jun 2023 00:35:43 +0530
Subject: [PATCH 58/59] fix: spreadsheet view bug fixes (#1383)
* fix: due date sorting fix
* fix: update and delete sub-issue fix
---
.../core/spreadsheet-view/single-issue.tsx | 4 +--
.../spreadsheet-view/spreadsheet-issues.tsx | 4 +--
.../components/issues/delete-issue-modal.tsx | 28 +++++++++++++++----
apps/app/components/issues/modal.tsx | 1 +
apps/app/constants/spreadsheet.ts | 4 +--
5 files changed, 29 insertions(+), 12 deletions(-)
diff --git a/apps/app/components/core/spreadsheet-view/single-issue.tsx b/apps/app/components/core/spreadsheet-view/single-issue.tsx
index f763e706c..bae89d8bb 100644
--- a/apps/app/components/core/spreadsheet-view/single-issue.tsx
+++ b/apps/app/components/core/spreadsheet-view/single-issue.tsx
@@ -38,7 +38,7 @@ type Props = {
expanded: boolean;
handleToggleExpand: (issueId: string) => void;
properties: Properties;
- handleEditIssue: () => void;
+ handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
gridTemplateColumns: string;
user: ICurrentUserResponse | undefined;
@@ -245,7 +245,7 @@ export const SingleSpreadsheetIssue: React.FC = ({
>
{!isNotAllowed && (
-
+ handleEditIssue(issue)}>
Edit issue
diff --git a/apps/app/components/core/spreadsheet-view/spreadsheet-issues.tsx b/apps/app/components/core/spreadsheet-view/spreadsheet-issues.tsx
index 8652b3a7e..0edfbceb1 100644
--- a/apps/app/components/core/spreadsheet-view/spreadsheet-issues.tsx
+++ b/apps/app/components/core/spreadsheet-view/spreadsheet-issues.tsx
@@ -59,7 +59,7 @@ export const SpreadsheetIssues: React.FC
= ({
handleToggleExpand={handleToggleExpand}
gridTemplateColumns={gridTemplateColumns}
properties={properties}
- handleEditIssue={() => handleEditIssue(issue)}
+ handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
user={user}
userAuth={userAuth}
@@ -78,7 +78,7 @@ export const SpreadsheetIssues: React.FC = ({
setExpandedIssues={setExpandedIssues}
gridTemplateColumns={gridTemplateColumns}
properties={properties}
- handleEditIssue={() => handleEditIssue(subIssue)}
+ handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
user={user}
userAuth={userAuth}
diff --git a/apps/app/components/issues/delete-issue-modal.tsx b/apps/app/components/issues/delete-issue-modal.tsx
index 000b21387..6ac9282ad 100644
--- a/apps/app/components/issues/delete-issue-modal.tsx
+++ b/apps/app/components/issues/delete-issue-modal.tsx
@@ -18,12 +18,13 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ui
import { SecondaryButton, DangerButton } from "components/ui";
// types
-import type { IIssue, ICurrentUserResponse } from "types";
+import type { IIssue, ICurrentUserResponse, ISubIssueResponse } from "types";
// fetch-keys
import {
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
+ SUB_ISSUES,
VIEW_ISSUES,
} from "constants/fetch-keys";
@@ -84,12 +85,27 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, u
: viewId
? VIEW_ISSUES(viewId.toString(), spreadsheetParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", spreadsheetParams);
+ if (data.parent) {
+ mutate(
+ SUB_ISSUES(data.parent.toString()),
+ (prevData) => {
+ if (!prevData) return prevData;
+ const updatedArray = (prevData.sub_issues ?? []).filter((i) => i.id !== data.id);
- mutate(
- spreadsheetFetchKey,
- (prevData) => (prevData ?? []).filter((p) => p.id !== data.id),
- false
- );
+ return {
+ ...prevData,
+ sub_issues: updatedArray,
+ };
+ },
+ false
+ );
+ } else {
+ mutate(
+ spreadsheetFetchKey,
+ (prevData) => (prevData ?? []).filter((p) => p.id !== data.id),
+ false
+ );
+ }
} else {
if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
else if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx
index d88ad674d..dd5262453 100644
--- a/apps/app/components/issues/modal.tsx
+++ b/apps/app/components/issues/modal.tsx
@@ -276,6 +276,7 @@ export const CreateUpdateIssueModal: React.FC = ({
} else {
if (issueView === "calendar") mutate(calendarFetchKey);
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
+ if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString()));
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
}
diff --git a/apps/app/constants/spreadsheet.ts b/apps/app/constants/spreadsheet.ts
index 8e4bdb21d..d58ebc861 100644
--- a/apps/app/constants/spreadsheet.ts
+++ b/apps/app/constants/spreadsheet.ts
@@ -48,8 +48,8 @@ export const SPREADSHEET_COLUMN = [
colName: "Due Date",
colSize: "128px",
icon: CalendarDaysIcon,
- ascendingOrder: "target_date",
- descendingOrder: "-target_date",
+ ascendingOrder: "-target_date",
+ descendingOrder: "target_date",
},
{
propertyName: "estimate",
From 56a35e74bbf6782f6275baeb3fdd716a8d5e78c5 Mon Sep 17 00:00:00 2001
From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Date: Sat, 24 Jun 2023 01:25:18 +0530
Subject: [PATCH 59/59] fix: spreadsheet bug fixes (#1385)
* fix: sub issue accordion fix
* chore: assignees sort order updated
---
apps/app/components/issues/delete-issue-modal.tsx | 1 +
apps/app/constants/spreadsheet.ts | 4 ++--
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/apps/app/components/issues/delete-issue-modal.tsx b/apps/app/components/issues/delete-issue-modal.tsx
index 6ac9282ad..c4fb3b00a 100644
--- a/apps/app/components/issues/delete-issue-modal.tsx
+++ b/apps/app/components/issues/delete-issue-modal.tsx
@@ -99,6 +99,7 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, u
},
false
);
+ mutate(spreadsheetFetchKey);
} else {
mutate(
spreadsheetFetchKey,
diff --git a/apps/app/constants/spreadsheet.ts b/apps/app/constants/spreadsheet.ts
index d58ebc861..b55cbdb23 100644
--- a/apps/app/constants/spreadsheet.ts
+++ b/apps/app/constants/spreadsheet.ts
@@ -32,8 +32,8 @@ export const SPREADSHEET_COLUMN = [
colName: "Assignees",
colSize: "128px",
icon: UserGroupIcon,
- ascendingOrder: "assignees__name",
- descendingOrder: "-assignees__name",
+ ascendingOrder: "assignees__first_name",
+ descendingOrder: "-assignees__first_name",
},
{
propertyName: "labels",