From aaffe37fbe2e5129cdf9b0e0c613c79d51d0b8cb Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Mon, 22 May 2023 22:09:38 +0530 Subject: [PATCH 01/65] fix: Showing status for accounts not created in the workspace members (#1111) --- apps/app/pages/[workspaceSlug]/settings/members.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/app/pages/[workspaceSlug]/settings/members.tsx b/apps/app/pages/[workspaceSlug]/settings/members.tsx index 4e6e4f579..fd80de11b 100644 --- a/apps/app/pages/[workspaceSlug]/settings/members.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/members.tsx @@ -66,6 +66,7 @@ const MembersSettings: NextPage = () => { role: item.role, status: true, member: true, + accountCreated: true, })) || []), ...(workspaceInvitations?.map((item) => ({ id: item.id, @@ -77,6 +78,7 @@ const MembersSettings: NextPage = () => { role: item.role, status: item.accepted, member: false, + accountCreated: item?.accepted ? false : true, })) || []), ]; @@ -200,6 +202,11 @@ const MembersSettings: NextPage = () => {

Pending

)} + {member?.status && !member?.accountCreated && ( +
+

Account not created

+
+ )} Date: Mon, 22 May 2023 22:09:52 +0530 Subject: [PATCH 02/65] dev: added tooltip to title and added info for issues (#1112) * update: tooltip in the blocks * dev: added tooltip to title and added info for issues --- .../cycles/cycles-list-gantt-chart.tsx | 10 +++-- apps/app/components/cycles/gantt-chart.tsx | 38 +++++++++++++++--- .../components/gantt-chart/blocks/index.tsx | 5 ++- apps/app/components/issues/gantt-chart.tsx | 40 +++++++++++++++---- apps/app/components/modules/gantt-chart.tsx | 38 +++++++++++++++--- .../modules/modules-list-gantt-chart.tsx | 10 +++-- apps/app/components/views/gantt-chart.tsx | 38 +++++++++++++++--- 7 files changed, 146 insertions(+), 33 deletions(-) diff --git a/apps/app/components/cycles/cycles-list-gantt-chart.tsx b/apps/app/components/cycles/cycles-list-gantt-chart.tsx index 9c8e922d2..056872c33 100644 --- a/apps/app/components/cycles/cycles-list-gantt-chart.tsx +++ b/apps/app/components/cycles/cycles-list-gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // types import { ICycle } from "types"; @@ -31,9 +33,11 @@ export const CyclesListGanttChartView: FC = ({ cycles }) => {
-
- {data?.name} -
+ +
+ {data?.name} +
+
); diff --git a/apps/app/components/cycles/gantt-chart.tsx b/apps/app/components/cycles/gantt-chart.tsx index 44abc392b..42ecad448 100644 --- a/apps/app/components/cycles/gantt-chart.tsx +++ b/apps/app/components/cycles/gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // hooks import useGanttChartCycleIssues from "hooks/gantt-chart/cycle-issues-view"; @@ -38,9 +40,23 @@ export const CycleIssuesGanttChartView: FC = ({}) => { className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: data?.state_detail?.color || "#858e96" }} /> -
- {data?.name} -
+ +
+ {data?.name} +
+
+ {data.infoToggle && ( + +
+ + info + +
+
+ )} ); @@ -59,10 +75,20 @@ export const CycleIssuesGanttChartView: FC = ({}) => { const blockFormat = (blocks: any) => blocks && blocks.length > 0 ? blocks.map((_block: any) => { - if (_block?.start_date && _block.target_date) console.log("_block", _block); + let startDate = new Date(_block.created_at); + let targetDate = new Date(_block.updated_at); + let infoToggle = true; + + if (_block?.start_date && _block.target_date) { + startDate = _block?.start_date; + targetDate = _block.target_date; + infoToggle = false; + } + return { - start_date: new Date(_block.created_at), - target_date: new Date(_block.updated_at), + start_date: new Date(startDate), + target_date: new Date(targetDate), + infoToggle: infoToggle, data: _block, }; }) diff --git a/apps/app/components/gantt-chart/blocks/index.tsx b/apps/app/components/gantt-chart/blocks/index.tsx index f2d44b294..d5eadf2a0 100644 --- a/apps/app/components/gantt-chart/blocks/index.tsx +++ b/apps/app/components/gantt-chart/blocks/index.tsx @@ -49,7 +49,10 @@ export const GanttChartBlocks: FC<{ width: `${block?.position?.width}px`, }} > - {blockRender({ ...block?.data })} + {blockRender({ + ...block?.data, + infoToggle: block?.infoToggle ? true : false, + })}
diff --git a/apps/app/components/issues/gantt-chart.tsx b/apps/app/components/issues/gantt-chart.tsx index 1330c2438..571583707 100644 --- a/apps/app/components/issues/gantt-chart.tsx +++ b/apps/app/components/issues/gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // hooks import useGanttChartIssues from "hooks/gantt-chart/issue-view"; @@ -37,9 +39,23 @@ export const IssueGanttChartView: FC = ({}) => { className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: data?.state_detail?.color || "#858e96" }} /> -
- {data?.name} -
+ +
+ {data?.name} +
+
+ {data.infoToggle && ( + +
+ + info + +
+
+ )} ); @@ -51,17 +67,25 @@ export const IssueGanttChartView: FC = ({}) => { start_date: data?.start_date, target_date: data?.target_date, }; - - console.log("payload", payload); }; const blockFormat = (blocks: any) => blocks && blocks.length > 0 ? blocks.map((_block: any) => { - if (_block?.start_date && _block.target_date) console.log("_block", _block); + let startDate = new Date(_block.created_at); + let targetDate = new Date(_block.updated_at); + let infoToggle = true; + + if (_block?.start_date && _block.target_date) { + startDate = _block?.start_date; + targetDate = _block.target_date; + infoToggle = false; + } + return { - start_date: new Date(_block.created_at), - target_date: new Date(_block.updated_at), + start_date: new Date(startDate), + target_date: new Date(targetDate), + infoToggle: infoToggle, data: _block, }; }) diff --git a/apps/app/components/modules/gantt-chart.tsx b/apps/app/components/modules/gantt-chart.tsx index fa92964c1..e24e1dd9a 100644 --- a/apps/app/components/modules/gantt-chart.tsx +++ b/apps/app/components/modules/gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // hooks import useGanttChartModuleIssues from "hooks/gantt-chart/module-issues-view"; @@ -38,9 +40,23 @@ export const ModuleIssuesGanttChartView: FC = ({}) => { className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: data?.state_detail?.color || "#858e96" }} /> -
- {data?.name} -
+ +
+ {data?.name} +
+
+ {data.infoToggle && ( + +
+ + info + +
+
+ )} ); @@ -59,10 +75,20 @@ export const ModuleIssuesGanttChartView: FC = ({}) => { const blockFormat = (blocks: any) => blocks && blocks.length > 0 ? blocks.map((_block: any) => { - if (_block?.start_date && _block.target_date) console.log("_block", _block); + let startDate = new Date(_block.created_at); + let targetDate = new Date(_block.updated_at); + let infoToggle = true; + + if (_block?.start_date && _block.target_date) { + startDate = _block?.start_date; + targetDate = _block.target_date; + infoToggle = false; + } + return { - start_date: new Date(_block.created_at), - target_date: new Date(_block.updated_at), + start_date: new Date(startDate), + target_date: new Date(targetDate), + infoToggle: infoToggle, data: _block, }; }) diff --git a/apps/app/components/modules/modules-list-gantt-chart.tsx b/apps/app/components/modules/modules-list-gantt-chart.tsx index cb1c7bc07..0f109ba6a 100644 --- a/apps/app/components/modules/modules-list-gantt-chart.tsx +++ b/apps/app/components/modules/modules-list-gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // types import { IModule } from "types"; // constants @@ -38,9 +40,11 @@ export const ModulesListGanttChartView: FC = ({ modules }) => { className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === data.status)?.color }} /> -
- {data?.name} -
+ +
+ {data?.name} +
+
); diff --git a/apps/app/components/views/gantt-chart.tsx b/apps/app/components/views/gantt-chart.tsx index dc81f70fa..a445331b2 100644 --- a/apps/app/components/views/gantt-chart.tsx +++ b/apps/app/components/views/gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // hooks import useGanttChartViewIssues from "hooks/gantt-chart/view-issues-view"; @@ -38,9 +40,23 @@ export const ViewIssuesGanttChartView: FC = ({}) => { className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: data?.state_detail?.color || "#858e96" }} /> -
- {data?.name} -
+ +
+ {data?.name} +
+
+ {data.infoToggle && ( + +
+ + info + +
+
+ )} ); @@ -59,10 +75,20 @@ export const ViewIssuesGanttChartView: FC = ({}) => { const blockFormat = (blocks: any) => blocks && blocks.length > 0 ? blocks.map((_block: any) => { - if (_block?.start_date && _block.target_date) console.log("_block", _block); + let startDate = new Date(_block.created_at); + let targetDate = new Date(_block.updated_at); + let infoToggle = true; + + if (_block?.start_date && _block.target_date) { + startDate = _block?.start_date; + targetDate = _block.target_date; + infoToggle = false; + } + return { - start_date: new Date(_block.created_at), - target_date: new Date(_block.updated_at), + start_date: new Date(startDate), + target_date: new Date(targetDate), + infoToggle: infoToggle, data: _block, }; }) From f095594be91a26fbe5ce59ce45d2eb2fe68eee33 Mon Sep 17 00:00:00 2001 From: gurusainath Date: Mon, 22 May 2023 22:38:22 +0530 Subject: [PATCH 03/65] update: Handled empty state in adding members to project. --- .../project/send-project-invitation-modal.tsx | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/apps/app/components/project/send-project-invitation-modal.tsx b/apps/app/components/project/send-project-invitation-modal.tsx index e18e7480e..cfc8cb99c 100644 --- a/apps/app/components/project/send-project-invitation-modal.tsx +++ b/apps/app/components/project/send-project-invitation-modal.tsx @@ -162,14 +162,22 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member input width="w-full" > - {uninvitedPeople?.map((person) => ( - - {person.member.email} - - ))} + {uninvitedPeople && uninvitedPeople.length > 0 ? ( + <> + {uninvitedPeople?.map((person) => ( + + {person.member.email} + + ))} + + ) : ( +
+ Invite members to workspace before adding them to a project. +
+ )} )} /> From 0bd6e53b44d50fb7370ba47b2aad0e10182844e5 Mon Sep 17 00:00:00 2001 From: Vihar Kurama Date: Wed, 24 May 2023 21:42:40 +0530 Subject: [PATCH 04/65] docs: updated Readme with new features and screenshots (#1132) * update readme images with new features * remove line-break on readme * update plane analytics image --- README.md | 66 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 827a2b146..5b3618363 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,18 @@ Discord

-
+

- + Plane Screens + + + Plane Screens @@ -89,41 +96,62 @@ docker-compose -f docker-compose-hub.yml up ## 📸 Screenshots

- + Plane Views + +

+

+ + Plane Issue Details -

-

- +

+

+ Plane Cycles and Modules -

-

- +

+

+ Plane Quick Lists -

-

- +

+

+ Plane Command K +

+

+ + Plane Command Menu + +

+

+ ## 📚Documentation From 0fb4a87454243b6c2141630f0eee7c9f8b615809 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Thu, 25 May 2023 10:24:20 +0530 Subject: [PATCH 05/65] fix: docker image uploads (#1108) * dev: basic initial setup for images * Update docker-compose.yml * dev: minio setup * dev: docker minio setup * dev: update the asset view * dev: setup minio with default configuration * dev: update minio setup for creating buckets * dev: update the permission sets * dev: get variables from shell for create bucket * dev: update image uploading setup for docker * dev: environment variables update * dev: web url for images * dev: update image configuration * dev: env update for email port --------- Co-authored-by: Narayana --- .env.example | 27 +++++++++++++-- apiserver/plane/api/views/asset.py | 12 +++++-- apiserver/plane/api/views/issue.py | 7 ++-- apiserver/plane/db/models/asset.py | 9 +++-- apiserver/plane/db/models/issue.py | 8 +++-- apiserver/plane/settings/local.py | 4 ++- apiserver/plane/settings/production.py | 46 ++++++++++++-------------- apiserver/plane/settings/staging.py | 4 +++ apiserver/plane/urls.py | 5 ++- apiserver/requirements/production.txt | 2 +- apps/app/next.config.js | 1 + docker-compose-hub.yml | 24 ++++++++++++-- docker-compose.yml | 27 +++++++++++++-- nginx/nginx.conf | 3 ++ setup.sh | 12 +++++-- 15 files changed, 142 insertions(+), 49 deletions(-) diff --git a/.env.example b/.env.example index 118a94883..727ea0806 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,41 @@ -# Replace with your instance Public IP +# Frontend +# Extra image domains that need to be added for Next Image NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS= +# Google Client ID for Google OAuth NEXT_PUBLIC_GOOGLE_CLIENTID="" -NEXT_PUBLIC_GITHUB_APP_NAME="" +# Github ID for Github OAuth NEXT_PUBLIC_GITHUB_ID="" +# Github App Name for GitHub Integration +NEXT_PUBLIC_GITHUB_APP_NAME="" +# Sentry DSN for error monitoring NEXT_PUBLIC_SENTRY_DSN="" +# Enable/Disable OAUTH - default 0 for selfhosted instance NEXT_PUBLIC_ENABLE_OAUTH=0 +# Enable/Disable sentry NEXT_PUBLIC_ENABLE_SENTRY=0 +# Enable/Disable session recording NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0 +# Enable/Disable event tracking NEXT_PUBLIC_TRACK_EVENTS=0 +# Slack for Slack Integration NEXT_PUBLIC_SLACK_CLIENT_ID="" + +# Backend +# Email Settings EMAIL_HOST="" EMAIL_HOST_USER="" EMAIL_HOST_PASSWORD="" +EMAIL_PORT=587 + +# AWS Settings AWS_REGION="" AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" AWS_S3_BUCKET_NAME="" +AWS_S3_ENDPOINT_URL="" + +# GPT settings OPENAI_API_KEY="" -GPT_ENGINE="" \ No newline at end of file +GPT_ENGINE="" + +# Auto generated and Required \ No newline at end of file diff --git a/apiserver/plane/api/views/asset.py b/apiserver/plane/api/views/asset.py index 98c9f9caf..705735e51 100644 --- a/apiserver/plane/api/views/asset.py +++ b/apiserver/plane/api/views/asset.py @@ -3,7 +3,7 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.parsers import MultiPartParser, FormParser from sentry_sdk import capture_exception - +from django.conf import settings # Module imports from .base import BaseAPIView from plane.db.models import FileAsset @@ -34,7 +34,10 @@ class FileAssetEndpoint(BaseAPIView): ) serializer.save(workspace_id=request.user.last_workspace_id) - return Response(serializer.data, status=status.HTTP_201_CREATED) + response_data = serializer.data + if settings.DOCKERIZED and "minio:9000" in response_data["asset"]: + response_data["asset"] = response_data["asset"].replace("minio:9000", settings.WEB_URL) + return Response(response_data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except Exception as e: capture_exception(e) @@ -82,7 +85,10 @@ class UserAssetsEndpoint(BaseAPIView): serializer = FileAssetSerializer(data=request.data) if serializer.is_valid(): serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) + response_data = serializer.data + if settings.DOCKERIZED and "minio:9000" in response_data["asset"]: + response_data["asset"] = response_data["asset"].replace("minio:9000", settings.WEB_URL) + return Response(response_data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except Exception as e: capture_exception(e) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 987677bb2..4f519ce69 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -9,7 +9,7 @@ from django.core.serializers.json import DjangoJSONEncoder from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page from django.db.models.functions import Coalesce - +from django.conf import settings # Third Party imports from rest_framework.response import Response from rest_framework import status @@ -788,6 +788,9 @@ class IssueAttachmentEndpoint(BaseAPIView): serializer = IssueAttachmentSerializer(data=request.data) if serializer.is_valid(): serializer.save(project_id=project_id, issue_id=issue_id) + response_data = serializer.data + if settings.DOCKERIZED and "minio:9000" in response_data["asset"]: + response_data["asset"] = response_data["asset"].replace("minio:9000", settings.WEB_URL) issue_activity.delay( type="attachment.activity.created", requested_data=None, @@ -799,7 +802,7 @@ class IssueAttachmentEndpoint(BaseAPIView): cls=DjangoJSONEncoder, ), ) - return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(response_data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except Exception as e: capture_exception(e) diff --git a/apiserver/plane/db/models/asset.py b/apiserver/plane/db/models/asset.py index acbb9428f..e37f2c0b0 100644 --- a/apiserver/plane/db/models/asset.py +++ b/apiserver/plane/db/models/asset.py @@ -4,6 +4,7 @@ from uuid import uuid4 # Django import from django.db import models from django.core.exceptions import ValidationError +from django.conf import settings # Module import from . import BaseModel @@ -16,9 +17,11 @@ def get_upload_path(instance, filename): def file_size(value): - limit = 5 * 1024 * 1024 - if value.size > limit: - raise ValidationError("File too large. Size should not exceed 5 MB.") + # File limit check is only for cloud hosted + if not settings.DOCKERIZED: + limit = 5 * 1024 * 1024 + if value.size > limit: + raise ValidationError("File too large. Size should not exceed 5 MB.") class FileAsset(BaseModel): diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 6e264566d..f58d4ac13 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -210,9 +210,11 @@ def get_upload_path(instance, filename): def file_size(value): - limit = 5 * 1024 * 1024 - if value.size > limit: - raise ValidationError("File too large. Size should not exceed 5 MB.") + # File limit check is only for cloud hosted + if not settings.DOCKERIZED: + limit = 5 * 1024 * 1024 + if value.size > limit: + raise ValidationError("File too large. Size should not exceed 5 MB.") class IssueAttachment(ProjectBaseModel): diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index e03a0b822..87e04e1ed 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -25,7 +25,9 @@ DATABASES = { } } -DOCKERIZED = os.environ.get("DOCKERIZED", False) +DOCKERIZED = int(os.environ.get( + "DOCKERIZED", 0 +)) == 1 if DOCKERIZED: DATABASES["default"] = dj_database_url.config() diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index e58736472..4d7da6ce3 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -29,9 +29,10 @@ DATABASES = { DATABASES["default"] = dj_database_url.config() SITE_ID = 1 -DOCKERIZED = os.environ.get( - "DOCKERIZED", False -) # Set the variable true if running in docker-compose environment +# Set the variable true if running in docker environment +DOCKERIZED = int(os.environ.get( + "DOCKERIZED", 0 +)) == 1 # Enable Connection Pooling (if desired) # DATABASES['default']['ENGINE'] = 'django_postgrespool' @@ -69,7 +70,7 @@ CORS_ALLOW_CREDENTIALS = True # Simplified static file serving. STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" -if os.environ.get("SENTRY_DSN", False): +if bool(os.environ.get("SENTRY_DSN", False)): sentry_sdk.init( dsn=os.environ.get("SENTRY_DSN", ""), integrations=[DjangoIntegration(), RedisIntegration()], @@ -80,12 +81,21 @@ if os.environ.get("SENTRY_DSN", False): environment="production", ) -if ( - os.environ.get("AWS_REGION", False) - and os.environ.get("AWS_ACCESS_KEY_ID", False) - and os.environ.get("AWS_SECRET_ACCESS_KEY", False) - and os.environ.get("AWS_S3_BUCKET_NAME", False) -): +if DOCKERIZED: + DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' + # The AWS access key to use. + AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key") + # The AWS secret access key to use. + AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key") + # The name of the bucket to store files in. + AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads") + # The full URL to the S3 endpoint. Leave blank to use the default region URL. + AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "http://minio:9000") + # Default permissions + AWS_DEFAULT_ACL = "public-read" + AWS_QUERYSTRING_AUTH = False + AWS_S3_FILE_OVERWRITE = False +else: # The AWS region to connect to. AWS_REGION = os.environ.get("AWS_REGION", "") @@ -99,7 +109,7 @@ if ( # AWS_SESSION_TOKEN = "" # The name of the bucket to store files in. - AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "") + AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME") # How to construct S3 URLs ("auto", "path", "virtual"). AWS_S3_ADDRESSING_STYLE = "auto" @@ -166,14 +176,8 @@ if ( # extra characters appended. AWS_S3_FILE_OVERWRITE = False - # AWS Settings End - DEFAULT_FILE_STORAGE = "django_s3_storage.storage.S3Storage" - -else: - MEDIA_URL = "/uploads/" - MEDIA_ROOT = os.path.join(BASE_DIR, "uploads") - +# AWS Settings End # Enable Connection Pooling (if desired) # DATABASES['default']['ENGINE'] = 'django_postgrespool' @@ -218,12 +222,6 @@ else: } } -RQ_QUEUES = { - "default": { - "USE_REDIS_CACHE": "default", - } -} - WEB_URL = os.environ.get("WEB_URL") diff --git a/apiserver/plane/settings/staging.py b/apiserver/plane/settings/staging.py index d4d0e5e12..d1d8e1749 100644 --- a/apiserver/plane/settings/staging.py +++ b/apiserver/plane/settings/staging.py @@ -49,6 +49,10 @@ CORS_ALLOW_ALL_ORIGINS = True # Simplified static file serving. STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" +# Make true if running in a docker environment +DOCKERIZED = int(os.environ.get( + "DOCKERIZED", 0 +)) == 1 sentry_sdk.init( dsn=os.environ.get("SENTRY_DSN"), diff --git a/apiserver/plane/urls.py b/apiserver/plane/urls.py index 3dfde38bd..a2244ffe0 100644 --- a/apiserver/plane/urls.py +++ b/apiserver/plane/urls.py @@ -7,7 +7,7 @@ from django.urls import path from django.views.generic import TemplateView from django.conf import settings -from django.conf.urls import include, url +from django.conf.urls import include, url, static # from django.conf.urls.static import static @@ -17,9 +17,8 @@ urlpatterns = [ path("api/", include("plane.api.urls")), path("", include("plane.web.urls")), ] -# + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) -# + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +urlpatterns = urlpatterns + static.static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.DEBUG: import debug_toolbar diff --git a/apiserver/requirements/production.txt b/apiserver/requirements/production.txt index 2547ce255..c37e98ffd 100644 --- a/apiserver/requirements/production.txt +++ b/apiserver/requirements/production.txt @@ -4,7 +4,7 @@ dj-database-url==1.2.0 gunicorn==20.1.0 whitenoise==6.3.0 django-storages==1.13.2 -boto==2.49.0 +boto3==1.26.136 django-anymail==9.0 twilio==7.16.2 django-debug-toolbar==3.8.1 diff --git a/apps/app/next.config.js b/apps/app/next.config.js index 876694142..646504a54 100644 --- a/apps/app/next.config.js +++ b/apps/app/next.config.js @@ -16,6 +16,7 @@ const nextConfig = { "planefs.s3.amazonaws.com", "images.unsplash.com", "avatars.githubusercontent.com", + "localhost", ...extraImageDomains, ], }, diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml index 435e47b29..c3df4684d 100644 --- a/docker-compose-hub.yml +++ b/docker-compose-hub.yml @@ -68,7 +68,7 @@ services: AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} - WEB_URL: localhost/ + WEB_URL: ${WEB_URL} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} DISABLE_COLLECTSTATIC: 1 DOCKERIZED: 1 @@ -104,13 +104,33 @@ services: AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} - WEB_URL: localhost/ + WEB_URL: ${WEB_URL} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} DISABLE_COLLECTSTATIC: 1 DOCKERIZED: 1 OPENAI_API_KEY: ${OPENAI_API_KEY} GPT_ENGINE: ${GPT_ENGINE} SECRET_KEY: ${SECRET_KEY} + minio: + image: minio/minio + ports: + - 9000:9000 + 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: + - minio + entrypoint: > + /bin/sh -c " + /usr/bin/mc config host add plane-minio http://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; + " volumes: pgdata: redisdata: diff --git a/docker-compose.yml b/docker-compose.yml index e4086acb2..68f06a3c8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: build: context: ./nginx dockerfile: Dockerfile + restart: always ports: - 80:80 depends_on: @@ -72,7 +73,8 @@ services: AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} - WEB_URL: localhost/ + AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL} + WEB_URL: ${WEB_URL} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} DISABLE_COLLECTSTATIC: 1 DOCKERIZED: 1 @@ -110,13 +112,34 @@ services: AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} - WEB_URL: localhost/ + AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL} + WEB_URL: ${WEB_URL} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} DISABLE_COLLECTSTATIC: 1 DOCKERIZED: 1 OPENAI_API_KEY: ${OPENAI_API_KEY} GPT_ENGINE: ${GPT_ENGINE} SECRET_KEY: ${SECRET_KEY} + minio: + image: minio/minio + ports: + - 9000:9000 + 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: + - minio + entrypoint: > + /bin/sh -c " + /usr/bin/mc config host add plane-minio http://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; + " volumes: pgdata: redisdata: diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 4ea689700..8f5fc2910 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -16,5 +16,8 @@ server { proxy_pass http://planebackend:8000/api/; } + location /uploads/ { + proxy_pass http://minio:9000/uploads/; + } } } \ No newline at end of file diff --git a/setup.sh b/setup.sh index e7f9a52dd..0b4ee279f 100755 --- a/setup.sh +++ b/setup.sh @@ -1,7 +1,15 @@ #!/bin/bash cp ./.env.example ./.env -echo -e "\nNEXT_PUBLIC_API_BASE_URL=http://$1" >> ./.env +# Export for tr error in mac export LC_ALL=C export LC_CTYPE=C -echo -e "\nSECRET_KEY=\"$(tr -dc 'a-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c50)\"" >> ./.env + +# Generate the NEXT_PUBLIC_API_BASE_URL with given IP +echo -e "\nNEXT_PUBLIC_API_BASE_URL=http://$1" >> ./.env + +# Generate the SECRET_KEY that will be used by django +echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c50)\"" >> ./.env + +# WEB_URL for email redirection and image saving +echo -e "WEB_URL=$1" >> ./.env \ No newline at end of file From e526a01295dbb5c0418c090bae3d56131b1ee917 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 25 May 2023 07:19:37 +0200 Subject: [PATCH 06/65] Clean up docker compose file for selfhosting (#1022) * Clean up docker compose file Removed nginx container (might want to put it back?) Changed spacing to tabs for better readability Changed the order, first the important stuff (plane) and later the database/redis All containers should be in the same format (first container_name, then image, then restart, etc.). Removed links because deprecated since compose version 2, all containers are in one docker network Removed build from plane-api Removed ports from redis and postgresql Removed PGDATA directory because that's the default one Renamed redis and db to plane-redis and plane-db * Fixed spacing (again) * Fix spacing (attempt 3) * Pasting error - should be good now * New compose download instructions --------- Co-authored-by: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> --- README.md | 14 +-- docker-compose-hub.yml | 256 ++++++++++++++++++++--------------------- 2 files changed, 130 insertions(+), 140 deletions(-) diff --git a/README.md b/README.md index 5b3618363..879be2c56 100644 --- a/README.md +++ b/README.md @@ -45,16 +45,12 @@ The easiest way to get started with Plane is by creating a [Plane Cloud](https:/ ### Docker Compose Setup -- Clone the Repository +- Download the docker-compose.yml and setup.sh file ```bash -git clone https://github.com/makeplane/plane -``` - -- Change Directory - -```bash -cd plane +curl https://raw.githubusercontent.com/makeplane/plane/develop/docker-compose-hub.yml --output docker-compose.yml +curl https://raw.githubusercontent.com/makeplane/plane/develop/setup.sh --output setup.sh +chmod +x setup.sh ``` - Run setup.sh @@ -76,7 +72,7 @@ set +a - Run Docker compose up ```bash -docker-compose -f docker-compose-hub.yml up +docker compose up -d ``` You can use the default email and password for your first login `captain@plane.so` and `password123`. diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml index c3df4684d..53a7ee0f2 100644 --- a/docker-compose-hub.yml +++ b/docker-compose-hub.yml @@ -1,136 +1,130 @@ version: "3.8" services: - nginx: - container_name: nginx - build: - context: ./nginx - dockerfile: Dockerfile - ports: - - 80:80 - depends_on: - - plane-web - - plane-api - db: - image: postgres:15.2-alpine - container_name: db - restart: always - volumes: - - pgdata:/var/lib/postgresql/data - environment: - POSTGRES_USER: plane - POSTGRES_DB: plane - POSTGRES_PASSWORD: xyzzyspoon - PGDATA: /var/lib/postgresql/data - command: postgres -c 'max_connections=1000' - ports: - - 5432:5432 - redis: - image: redis:6.2.7-alpine - container_name: redis - restart: always - ports: - - 6379:6379 - volumes: - - redisdata:/data - plane-web: - container_name: planefrontend - image: makeplane/plane-frontend:0.6 - restart: always - command: [ "/usr/local/bin/start.sh" ] - environment: - NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} - NEXT_PUBLIC_GOOGLE_CLIENTID: 0 - NEXT_PUBLIC_GITHUB_APP_NAME: 0 - NEXT_PUBLIC_GITHUB_ID: 0 - NEXT_PUBLIC_SENTRY_DSN: 0 - NEXT_PUBLIC_ENABLE_OAUTH: 0 - NEXT_PUBLIC_ENABLE_SENTRY: 0 - ports: - - 3000:3000 - plane-api: - container_name: planebackend - image: makeplane/plane-backend:0.6 - build: - context: ./apiserver - dockerfile: Dockerfile.api - restart: always - ports: - - 8000:8000 - environment: - DJANGO_SETTINGS_MODULE: plane.settings.production - DATABASE_URL: postgres://plane:xyzzyspoon@db:5432/plane - REDIS_URL: redis://redis:6379/ - EMAIL_HOST: ${EMAIL_HOST} - EMAIL_HOST_USER: ${EMAIL_HOST_USER} - EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} - AWS_REGION: ${AWS_REGION} - AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} - AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} - WEB_URL: ${WEB_URL} - GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} - DISABLE_COLLECTSTATIC: 1 - DOCKERIZED: 1 - OPENAI_API_KEY: ${OPENAI_API_KEY} - GPT_ENGINE: ${GPT_ENGINE} - SECRET_KEY: ${SECRET_KEY} - depends_on: - - db - - redis - command: ./bin/takeoff - links: - - db:db - - redis:redis - plane-worker: - container_name: planerqworker - image: makeplane/plane-worker:0.6 - depends_on: - - redis - - db - - plane-api - command: ./bin/worker - links: - - redis:redis - - db:db - environment: - DJANGO_SETTINGS_MODULE: plane.settings.production - DATABASE_URL: postgres://plane:xyzzyspoon@db:5432/plane - REDIS_URL: redis://redis:6379/ - EMAIL_HOST: ${EMAIL_HOST} - EMAIL_HOST_USER: ${EMAIL_HOST_USER} - EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} - AWS_REGION: ${AWS_REGION} - AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} - AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} - WEB_URL: ${WEB_URL} - GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} - DISABLE_COLLECTSTATIC: 1 - DOCKERIZED: 1 - OPENAI_API_KEY: ${OPENAI_API_KEY} - GPT_ENGINE: ${GPT_ENGINE} - SECRET_KEY: ${SECRET_KEY} - minio: - image: minio/minio - ports: - - 9000:9000 - environment: - MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} - MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} - command: server /export --console-address ":9090" + plane-web: + container_name: planefrontend + image: makeplane/plane-frontend:0.6 + restart: always + command: /usr/local/bin/start.sh + environment: + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + NEXT_PUBLIC_GOOGLE_CLIENTID: 0 + NEXT_PUBLIC_GITHUB_APP_NAME: 0 + NEXT_PUBLIC_GITHUB_ID: 0 + NEXT_PUBLIC_SENTRY_DSN: 0 + NEXT_PUBLIC_ENABLE_OAUTH: 0 + NEXT_PUBLIC_ENABLE_SENTRY: 0 + ports: + - 3000:3000 + + plane-api: + container_name: planebackend + image: makeplane/plane-backend:0.6 + restart: always + command: ./bin/takeoff + environment: + DJANGO_SETTINGS_MODULE: plane.settings.production + DATABASE_URL: postgres://plane:xyzzyspoon@plane-db:5432/plane + REDIS_URL: redis://plane-redis:6379/ + EMAIL_HOST: ${EMAIL_HOST} + EMAIL_HOST_USER: ${EMAIL_HOST_USER} + EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + AWS_REGION: ${AWS_REGION} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} + WEB_URL: localhost/ + GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} + DISABLE_COLLECTSTATIC: 1 + DOCKERIZED: 1 + OPENAI_API_KEY: ${OPENAI_API_KEY} + GPT_ENGINE: ${GPT_ENGINE} + SECRET_KEY: ${SECRET_KEY} + depends_on: + - plane-db + - plane-redis + ports: + - 8000:8000 + + plane-worker: + container_name: planerqworker + image: makeplane/plane-worker:0.6 + restart: always + command: ./bin/worker + environment: + DJANGO_SETTINGS_MODULE: plane.settings.production + DATABASE_URL: postgres://plane:xyzzyspoon@plane-db:5432/plane + REDIS_URL: redis://plane-redis:6379/ + EMAIL_HOST: ${EMAIL_HOST} + EMAIL_HOST_USER: ${EMAIL_HOST_USER} + EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + AWS_REGION: ${AWS_REGION} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} + WEB_URL: localhost/ + GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} + DISABLE_COLLECTSTATIC: 1 + DOCKERIZED: 1 + OPENAI_API_KEY: ${OPENAI_API_KEY} + GPT_ENGINE: ${GPT_ENGINE} + SECRET_KEY: ${SECRET_KEY} + depends_on: + - plane-api + - plane-db + - plane-redis + + plane-db: + container_name: plane-db + image: postgres:15.2-alpine + restart: always + command: postgres -c 'max_connections=1000' + environment: + POSTGRES_USER: plane + POSTGRES_DB: plane + POSTGRES_PASSWORD: xyzzyspoon + volumes: + - pgdata:/var/lib/postgresql/data + + plane-redis: + container_name: plane-redis + image: redis:6.2.7-alpine + restart: always + volumes: + - redisdata:/data + + plane-minio: + container_name: plane-minio + image: minio/minio + 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: + - minio + entrypoint: > + /bin/sh -c " + /usr/bin/mc config host add plane-minio http://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; + " + + nginx: + container_name: nginx + build: + context: ./nginx + dockerfile: Dockerfile + ports: + - 80:80 + depends_on: + - plane-web + - plane-api + - createbuckets: - image: minio/mc - depends_on: - - minio - entrypoint: > - /bin/sh -c " - /usr/bin/mc config host add plane-minio http://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; - " volumes: - pgdata: - redisdata: + pgdata: + redisdata: From def391cb76686abfdf777bfd34653d9782aade4b Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 25 May 2023 12:19:37 +0530 Subject: [PATCH 07/65] feat: storing my issue view display properties (#1124) --- apps/app/hooks/use-my-issues-filter.tsx | 175 ++++++++---------- .../pages/[workspaceSlug]/me/my-issues.tsx | 7 +- apps/app/services/workspace.service.ts | 8 + apps/app/types/workspace.d.ts | 14 ++ 4 files changed, 98 insertions(+), 106 deletions(-) diff --git a/apps/app/hooks/use-my-issues-filter.tsx b/apps/app/hooks/use-my-issues-filter.tsx index bd97427f5..8c5d3eaa5 100644 --- a/apps/app/hooks/use-my-issues-filter.tsx +++ b/apps/app/hooks/use-my-issues-filter.tsx @@ -1,26 +1,18 @@ -import { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import useSWR from "swr"; +import { useState, useEffect, useCallback } from "react"; +import useSWR, { mutate } from "swr"; // services -import stateService from "services/state.service"; -import userService from "services/user.service"; +import workspaceService from "services/workspace.service"; // hooks import useUser from "hooks/use-user"; -// helpers -import { groupBy } from "helpers/array.helper"; -import { getStatesList } from "helpers/state.helper"; // types -import { Properties, NestedKeyOf, IIssue } from "types"; -// fetch-keys -import { STATES_LIST } from "constants/fetch-keys"; -// constants -import { PRIORITIES } from "constants/project"; +import { IWorkspaceMember, Properties } from "types"; +import { WORKSPACE_MEMBERS_ME } from "constants/fetch-keys"; const initialValues: Properties = { assignee: true, due_date: false, key: true, - labels: true, + labels: false, priority: false, state: true, sub_issue_count: false, @@ -29,99 +21,80 @@ const initialValues: Properties = { estimate: false, }; -// TODO: Refactor this logic -const useMyIssuesProperties = (issues?: IIssue[]) => { +const useMyIssuesProperties = (workspaceSlug?: string) => { const [properties, setProperties] = useState(initialValues); - const [groupByProperty, setGroupByProperty] = useState | null>(null); - - // FIXME: where this hook is used we may not have project id in the url - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; const { user } = useUser(); - const { data: stateGroups } = useSWR( - workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => stateService.getStates(workspaceSlug as string, projectId as string) - : null - ); - const states = getStatesList(stateGroups ?? {}); - - useEffect(() => { - if (!user) return; - setProperties({ ...initialValues, ...user.my_issues_prop?.properties }); - setGroupByProperty(user.my_issues_prop?.groupBy ?? null); - }, [user]); - - const groupedByIssues: { - [key: string]: IIssue[]; - } = { - ...(groupByProperty === "state_detail.name" - ? Object.fromEntries( - states - ?.sort((a, b) => a.sequence - b.sequence) - ?.map((state) => [ - state.name, - issues?.filter((issue) => issue.state === state.name) ?? [], - ]) ?? [] - ) - : groupByProperty === "priority" - ? Object.fromEntries( - PRIORITIES.map((priority) => [ - priority, - issues?.filter((issue) => issue.priority === priority) ?? [], - ]) - ) - : {}), - ...groupBy(issues ?? [], groupByProperty ?? ""), - }; - - const setMyIssueProperty = (key: keyof Properties) => { - if (!user) return; - userService.updateUser({ my_issues_prop: { properties, groupBy: groupByProperty } }); - setProperties((prevData) => ({ - ...prevData, - [key]: !prevData[key], - })); - localStorage.setItem( - "my_issues_prop", - JSON.stringify({ - properties: { - ...properties, - [key]: !properties[key], - }, - groupBy: groupByProperty, - }) - ); - }; - - const setMyIssueGroupByProperty = (groupByProperty: NestedKeyOf | null) => { - if (!user) return; - userService.updateUser({ my_issues_prop: { properties, groupBy: groupByProperty } }); - setGroupByProperty(groupByProperty); - localStorage.setItem( - "my_issues_prop", - JSON.stringify({ properties, groupBy: groupByProperty }) - ); - }; - - useEffect(() => { - const viewProps = localStorage.getItem("my_issues_prop"); - if (viewProps) { - const { properties, groupBy } = JSON.parse(viewProps); - setProperties(properties); - setGroupByProperty(groupBy); + const { data: myWorkspace } = useSWR( + workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug as string) : null, + workspaceSlug ? () => workspaceService.workspaceMemberMe(workspaceSlug as string) : null, + { + shouldRetryOnError: false, } - }, []); + ); - return { - filteredIssues: groupedByIssues, - groupByProperty, - properties, - setMyIssueProperty, - setMyIssueGroupByProperty, - } as const; + useEffect(() => { + if (!myWorkspace || !workspaceSlug || !user) return; + + setProperties({ ...initialValues, ...myWorkspace.view_props }); + + if (!myWorkspace.view_props) { + workspaceService.updateWorkspaceView(workspaceSlug, { + view_props: { ...initialValues }, + }); + } + }, [myWorkspace, workspaceSlug, user]); + + const updateIssueProperties = useCallback( + (key: keyof Properties) => { + if (!workspaceSlug || !user) return; + + setProperties((prev) => ({ ...prev, [key]: !prev[key] })); + + if (myWorkspace) { + mutate( + WORKSPACE_MEMBERS_ME(workspaceSlug.toString()), + (prevData) => { + if (!prevData) return; + return { + ...prevData, + view_props: { ...prevData?.view_props, [key]: !prevData.view_props?.[key] }, + }; + }, + false + ); + if (myWorkspace.view_props) { + workspaceService.updateWorkspaceView(workspaceSlug, { + view_props: { + ...myWorkspace.view_props, + [key]: !myWorkspace.view_props[key], + }, + }); + } else { + workspaceService.updateWorkspaceView(workspaceSlug, { + view_props: { ...initialValues }, + }); + } + } + }, + [workspaceSlug, myWorkspace, user] + ); + + const newProperties: Properties = { + assignee: properties.assignee, + due_date: properties.due_date, + key: properties.key, + labels: properties.labels, + priority: properties.priority, + state: properties.state, + sub_issue_count: properties.sub_issue_count, + attachment_count: properties.attachment_count, + link: properties.link, + estimate: properties.estimate, + }; + + return [newProperties, updateIssueProperties] as const; }; export default useMyIssuesProperties; diff --git a/apps/app/pages/[workspaceSlug]/me/my-issues.tsx b/apps/app/pages/[workspaceSlug]/me/my-issues.tsx index d742a7aa5..c66fcee55 100644 --- a/apps/app/pages/[workspaceSlug]/me/my-issues.tsx +++ b/apps/app/pages/[workspaceSlug]/me/my-issues.tsx @@ -14,7 +14,7 @@ import useIssues from "hooks/use-issues"; import { Spinner, EmptySpace, EmptySpaceItem, PrimaryButton } from "components/ui"; import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs"; // hooks -import useIssuesProperties from "hooks/use-issue-properties"; +import useMyIssuesProperties from "hooks/use-my-issues-filter"; // types import { IIssue, Properties } from "types"; // components @@ -31,10 +31,7 @@ const MyIssuesPage: NextPage = () => { // fetching user issues const { myIssues } = useIssues(workspaceSlug as string); - const [properties, setProperties] = useIssuesProperties( - workspaceSlug ? (workspaceSlug as string) : undefined, - undefined - ); + const [properties, setProperties] = useMyIssuesProperties(workspaceSlug as string); return ( { + return this.post(`/api/workspaces/${workspaceSlug}/workspace-views/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async updateWorkspaceMember( workspaceSlug: string, memberId: string, diff --git a/apps/app/types/workspace.d.ts b/apps/app/types/workspace.d.ts index fbb54d759..a88a0f15b 100644 --- a/apps/app/types/workspace.d.ts +++ b/apps/app/types/workspace.d.ts @@ -33,6 +33,19 @@ export interface IWorkspaceMemberInvitation { workspace: IWorkspace; } +export type Properties = { + assignee: boolean; + due_date: boolean; + labels: boolean; + key: boolean; + priority: boolean; + state: boolean; + sub_issue_count: boolean; + link: boolean; + attachment_count: boolean; + estimate: boolean; +}; + export interface IWorkspaceMember { readonly id: string; user: IUserLite; @@ -40,6 +53,7 @@ export interface IWorkspaceMember { member: IUserLite; role: 5 | 10 | 15 | 20; company_role: string | null; + view_props: Properties; created_at: Date; updated_at: Date; created_by: string; From 74329a49ccef7b929c18d3962e74c61bcc9dcbd3 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 25 May 2023 12:19:48 +0530 Subject: [PATCH 08/65] fix: send code button behavior on enter key press (#1121) --- .../components/account/email-code-form.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/app/components/account/email-code-form.tsx b/apps/app/components/account/email-code-form.tsx index 48288f77e..b4a3ef31e 100644 --- a/apps/app/components/account/email-code-form.tsx +++ b/apps/app/components/account/email-code-form.tsx @@ -88,6 +88,25 @@ export const EmailCodeForm = ({ onSuccess }: any) => { setErrorResendingCode(false); }, [emailOld]); + useEffect(() => { + const submitForm = (e: KeyboardEvent) => { + if (!codeSent && e.key === "Enter") { + e.preventDefault(); + handleSubmit(onSubmit)().then(() => { + setResendCodeTimer(30); + }); + } + }; + + if (!codeSent) { + window.addEventListener("keydown", submitForm); + } + + return () => { + window.removeEventListener("keydown", submitForm); + }; + }, [handleSubmit, codeSent]); + return ( <>
From a16514ed11fca57055e1c4c497e0dc3e938b7606 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Thu, 25 May 2023 12:24:03 +0530 Subject: [PATCH 09/65] fix: auto generated secret key to only generate hexadecimal characters (#1133) --- setup.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index 0b4ee279f..f4ca1ea6d 100755 --- a/setup.sh +++ b/setup.sh @@ -5,6 +5,7 @@ cp ./.env.example ./.env export LC_ALL=C export LC_CTYPE=C + # Generate the NEXT_PUBLIC_API_BASE_URL with given IP echo -e "\nNEXT_PUBLIC_API_BASE_URL=http://$1" >> ./.env @@ -12,4 +13,4 @@ echo -e "\nNEXT_PUBLIC_API_BASE_URL=http://$1" >> ./.env echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c50)\"" >> ./.env # WEB_URL for email redirection and image saving -echo -e "WEB_URL=$1" >> ./.env \ No newline at end of file +echo -e "WEB_URL=$1" >> ./.env From e608b58e70f2b3a8962071edf6470fc3a22a53f0 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Thu, 25 May 2023 12:24:39 +0530 Subject: [PATCH 10/65] refactor: cycle views endpoint (#1128) --- apiserver/plane/api/urls.py | 24 -- apiserver/plane/api/views/__init__.py | 4 - apiserver/plane/api/views/cycle.py | 434 ++++---------------------- 3 files changed, 69 insertions(+), 393 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 93af9d762..73d3382a2 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -96,12 +96,8 @@ from plane.api.views import ( CycleViewSet, CycleIssueViewSet, CycleDateCheckEndpoint, - CurrentUpcomingCyclesEndpoint, - CompletedCyclesEndpoint, CycleFavoriteViewSet, - DraftCyclesEndpoint, TransferCycleIssueEndpoint, - InCompleteCyclesEndpoint, ## End Cycles # Modules ModuleViewSet, @@ -664,21 +660,6 @@ urlpatterns = [ CycleDateCheckEndpoint.as_view(), name="project-cycle", ), - path( - "workspaces//projects//cycles/current-upcoming-cycles/", - CurrentUpcomingCyclesEndpoint.as_view(), - name="project-cycle-upcoming", - ), - path( - "workspaces//projects//cycles/completed-cycles/", - CompletedCyclesEndpoint.as_view(), - name="project-cycle-completed", - ), - path( - "workspaces//projects//cycles/draft-cycles/", - DraftCyclesEndpoint.as_view(), - name="project-cycle-draft", - ), path( "workspaces//projects//user-favorite-cycles/", CycleFavoriteViewSet.as_view( @@ -703,11 +684,6 @@ urlpatterns = [ TransferCycleIssueEndpoint.as_view(), name="transfer-issues", ), - path( - "workspaces//projects//incomplete-cycles/", - InCompleteCyclesEndpoint.as_view(), - name="transfer-issues", - ), ## End Cycles # Issue path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 65554f529..6b4cb52a7 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -49,12 +49,8 @@ from .cycle import ( CycleViewSet, CycleIssueViewSet, CycleDateCheckEndpoint, - CurrentUpcomingCyclesEndpoint, - CompletedCyclesEndpoint, CycleFavoriteViewSet, - DraftCyclesEndpoint, TransferCycleIssueEndpoint, - InCompleteCyclesEndpoint, ) from .asset import FileAssetEndpoint, UserAssetsEndpoint from .issue import ( diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index b12b49b2f..63c832e71 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -152,6 +152,75 @@ class CycleViewSet(BaseViewSet): .distinct() ) + def list(self, request, slug, project_id): + 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, + ) + + # All Cycles + if cycle_view == "all": + 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(), + ) + return Response( + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK + ) + + # Upcoming Cycles + if cycle_view == "upcoming": + queryset = queryset.filter(start_date__gt=timezone.now()) + return Response( + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK + ) + + # Completed Cycles + if cycle_view == "completed": + queryset = queryset.filter(end_date__lt=timezone.now()) + return Response( + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK + ) + + # Draft Cycles + if cycle_view == "draft": + queryset = queryset.filter( + end_date=None, + start_date=None, + ) + return Response( + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK + ) + + # Incomplete Cycles + if cycle_view == "incomplete": + queryset = queryset.filter( + Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True), + ) + return Response( + CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK + ) + + 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, + ) + def create(self, request, slug, project_id): try: if ( @@ -478,352 +547,6 @@ class CycleDateCheckEndpoint(BaseAPIView): ) -class CurrentUpcomingCyclesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - try: - subquery = CycleFavorite.objects.filter( - user=self.request.user, - cycle_id=OuterRef("pk"), - project_id=project_id, - workspace__slug=slug, - ) - current_cycle = ( - Cycle.objects.filter( - workspace__slug=slug, - project_id=project_id, - start_date__lte=timezone.now(), - end_date__gte=timezone.now(), - ) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .annotate(is_favorite=Exists(subquery)) - .annotate(total_issues=Count("issue_cycle")) - .annotate( - completed_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="cancelled"), - ) - ) - .annotate( - started_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="unstarted"), - ) - ) - .annotate( - backlog_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="backlog"), - ) - ) - .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point")) - .annotate( - completed_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - started_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .prefetch_related( - Prefetch( - "issue_cycle__issue__assignees", - queryset=User.objects.only( - "avatar", "first_name", "id" - ).distinct(), - ) - ) - .order_by("name", "-is_favorite") - ) - - upcoming_cycle = ( - Cycle.objects.filter( - workspace__slug=slug, - project_id=project_id, - start_date__gt=timezone.now(), - ) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .annotate(is_favorite=Exists(subquery)) - .annotate(total_issues=Count("issue_cycle")) - .annotate( - completed_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="cancelled"), - ) - ) - .annotate( - started_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="unstarted"), - ) - ) - .annotate( - backlog_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="backlog"), - ) - ) - .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point")) - .annotate( - completed_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - started_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .prefetch_related( - Prefetch( - "issue_cycle__issue__assignees", - queryset=User.objects.only( - "avatar", "first_name", "id" - ).distinct(), - ) - ) - .order_by("name", "-is_favorite") - ) - - return Response( - { - "current_cycle": CycleSerializer(current_cycle, many=True).data, - "upcoming_cycle": CycleSerializer(upcoming_cycle, many=True).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 CompletedCyclesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - try: - subquery = CycleFavorite.objects.filter( - user=self.request.user, - cycle_id=OuterRef("pk"), - project_id=project_id, - workspace__slug=slug, - ) - completed_cycles = ( - Cycle.objects.filter( - workspace__slug=slug, - project_id=project_id, - end_date__lt=timezone.now(), - ) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .annotate(is_favorite=Exists(subquery)) - .annotate(total_issues=Count("issue_cycle")) - .annotate( - completed_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="cancelled"), - ) - ) - .annotate( - started_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="unstarted"), - ) - ) - .annotate( - backlog_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="backlog"), - ) - ) - .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point")) - .annotate( - completed_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - started_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .prefetch_related( - Prefetch( - "issue_cycle__issue__assignees", - queryset=User.objects.only( - "avatar", "first_name", "id" - ).distinct(), - ) - ) - .order_by("name", "-is_favorite") - ) - - return Response( - { - "completed_cycles": CycleSerializer( - completed_cycles, many=True - ).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 DraftCyclesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - try: - subquery = CycleFavorite.objects.filter( - user=self.request.user, - cycle_id=OuterRef("pk"), - project_id=project_id, - workspace__slug=slug, - ) - draft_cycles = ( - Cycle.objects.filter( - workspace__slug=slug, - project_id=project_id, - end_date=None, - start_date=None, - ) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .annotate(is_favorite=Exists(subquery)) - .annotate(total_issues=Count("issue_cycle")) - .annotate( - completed_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - cancelled_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="cancelled"), - ) - ) - .annotate( - started_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="unstarted"), - ) - ) - .annotate( - backlog_issues=Count( - "issue_cycle__issue__state__group", - filter=Q(issue_cycle__issue__state__group="backlog"), - ) - ) - .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point")) - .annotate( - completed_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="completed"), - ) - ) - .annotate( - started_estimates=Sum( - "issue_cycle__issue__estimate_point", - filter=Q(issue_cycle__issue__state__group="started"), - ) - ) - .prefetch_related( - Prefetch( - "issue_cycle__issue__assignees", - queryset=User.objects.only( - "avatar", "first_name", "id" - ).distinct(), - ) - ) - .order_by("name", "-is_favorite") - ) - - return Response( - {"draft_cycles": CycleSerializer(draft_cycles, many=True).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 CycleFavoriteViewSet(BaseViewSet): permission_classes = [ ProjectEntityPermission, @@ -948,22 +671,3 @@ class TransferCycleIssueEndpoint(BaseAPIView): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) - - -class InCompleteCyclesEndpoint(BaseAPIView): - def get(self, request, slug, project_id): - try: - cycles = Cycle.objects.filter( - Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True), - workspace__slug=slug, - project_id=project_id, - ).select_related("owned_by") - - serializer = CycleSerializer(cycles, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) From af2d7d6f75316b4b4e2b8c1f29121cd5514bb32f Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Thu, 25 May 2023 12:25:15 +0530 Subject: [PATCH 11/65] fix: project member delete when deleting user from workspace (#1123) * fix: project member delete when deleting user from workspace * fix: workspace and project member delete --- apiserver/plane/api/views/project.py | 48 ++++++++++++++++++++------ apiserver/plane/api/views/workspace.py | 45 ++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 10 deletions(-) diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index f6c4ed87d..cdbdea12a 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -37,18 +37,19 @@ from plane.db.models import ( State, TeamMember, ProjectFavorite, + ProjectIdentifier, + Module, + Cycle, + CycleFavorite, + ModuleFavorite, + PageFavorite, + IssueViewFavorite, + Page, + IssueAssignee, + ModuleMember ) -from plane.db.models import ( - Project, - ProjectMember, - Workspace, - ProjectMemberInvite, - User, - ProjectIdentifier, - Cycle, - Module, -) + from plane.bgtasks.project_invitation_task import project_invitation @@ -443,6 +444,33 @@ class ProjectMemberViewSet(BaseViewSet): capture_exception(e) return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST) + def destroy(self, request, slug, project_id, pk): + try: + project_member = ProjectMember.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + # Remove all favorites + ProjectFavorite.objects.filter(workspace__slug=slug, project_id=project_id, user=project_member.member).delete() + CycleFavorite.objects.filter(workspace__slug=slug, project_id=project_id, user=project_member.member).delete() + ModuleFavorite.objects.filter(workspace__slug=slug, project_id=project_id, user=project_member.member).delete() + PageFavorite.objects.filter(workspace__slug=slug, project_id=project_id, user=project_member.member).delete() + IssueViewFavorite.objects.filter(workspace__slug=slug, project_id=project_id, user=project_member.member).delete() + # Also remove issue from issue assigned + IssueAssignee.objects.filter( + workspace__slug=slug, project_id=project_id, assignee=project_member.member + ).delete() + + # Remove if module member + ModuleMember.objects.filter(workspace__slug=slug, project_id=project_id, member=project_member.member).delete() + # Delete owned Pages + Page.objects.filter(workspace__slug=slug, project_id=project_id, owned_by=project_member.member).delete() + project_member.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except ProjectMember.DoesNotExist: + return Response({"error": "Project Member does not exist"}, status=status.HTTP_400) + except Exception as e: + capture_exception(e) + return Response({"error": "Something went wrong please try again later"}) class AddMemberToProjectEndpoint(BaseAPIView): diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index dcb8941a1..3635efcea 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -50,6 +50,14 @@ from plane.db.models import ( IssueActivity, Issue, WorkspaceTheme, + IssueAssignee, + ProjectFavorite, + CycleFavorite, + ModuleMember, + ModuleFavorite, + PageFavorite, + Page, + IssueViewFavorite, ) from plane.api.permissions import WorkSpaceBasePermission, WorkSpaceAdminPermission from plane.bgtasks.workspace_invitation_task import workspace_invitation @@ -460,6 +468,43 @@ class WorkSpaceMemberViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) + def destroy(self, request, slug, pk): + try: + workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, pk=pk) + # Delete the user also from all the projects + ProjectMember.objects.filter( + workspace__slug=slug, member=workspace_member.member + ).delete() + # Remove all favorites + ProjectFavorite.objects.filter(workspace__slug=slug, user=workspace_member.member).delete() + CycleFavorite.objects.filter(workspace__slug=slug, user=workspace_member.member).delete() + ModuleFavorite.objects.filter(workspace__slug=slug, user=workspace_member.member).delete() + PageFavorite.objects.filter(workspace__slug=slug, user=workspace_member.member).delete() + IssueViewFavorite.objects.filter(workspace__slug=slug, user=workspace_member.member).delete() + # Also remove issue from issue assigned + IssueAssignee.objects.filter( + workspace__slug=slug, assignee=workspace_member.member + ).delete() + + # Remove if module member + ModuleMember.objects.filter(workspace__slug=slug, member=workspace_member.member).delete() + # Delete owned Pages + Page.objects.filter(workspace__slug=slug, owned_by=workspace_member.member).delete() + + workspace_member.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except WorkspaceMember.DoesNotExist: + return Response( + {"error": "Workspace Member 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, + ) + class TeamMemberViewSet(BaseViewSet): serializer_class = TeamSerializer From 5beb50fa76895cf2a94f8139f130e29f101e440a Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Thu, 25 May 2023 12:27:04 +0530 Subject: [PATCH 12/65] fix: role updation (#1110) --- apiserver/plane/api/views/project.py | 30 +++++++++++++++----------- apiserver/plane/api/views/workspace.py | 6 +++++- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index cdbdea12a..9b2b9ce5e 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -104,13 +104,15 @@ class ProjectViewSet(BaseViewSet): .values("count") ) .annotate( - total_cycles=Cycle.objects.filter(project_id=OuterRef("id")) + total_cycles=Cycle.objects.filter( + project_id=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") ) .annotate( - total_modules=Module.objects.filter(project_id=OuterRef("id")) + total_modules=Module.objects.filter( + project_id=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -134,12 +136,12 @@ class ProjectViewSet(BaseViewSet): if serializer.is_valid(): serializer.save() - ## Add the user as Administrator to the project + # Add the user as Administrator to the project ProjectMember.objects.create( project_id=serializer.data["id"], member=request.user, role=20 ) - ## Default states + # Default states states = [ { "name": "Backlog", @@ -320,7 +322,8 @@ class InviteProjectEndpoint(BaseAPIView): ) return Response( - ProjectMemberSerializer(project_member).data, status=status.HTTP_200_OK + ProjectMemberSerializer( + project_member).data, status=status.HTTP_200_OK ) except ValidationError: @@ -374,7 +377,7 @@ class UserProjectInvitationsViewset(BaseViewSet): ] ) - ## Delete joined project invites + # Delete joined project invites project_invitations.delete() return Response(status=status.HTTP_200_OK) @@ -412,14 +415,16 @@ class ProjectMemberViewSet(BaseViewSet): def partial_update(self, request, slug, project_id, pk): try: - project_member = ProjectMember.objects.get(pk=pk, workspace__slug=slug, project_id=project_id) + project_member = ProjectMember.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id) if request.user.id == project_member.member_id: return Response( {"error": "You cannot update your own role"}, status=status.HTTP_400_BAD_REQUEST, ) - - if request.data.get("role", 10) > project_member.role: + # Check while updating user roles + requested_project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, member=request.user) + if "role" in request.data and request.data.get("role", project_member.role) > requested_project_member.role: return Response( { "error": "You cannot update a role that is higher than your own role" @@ -472,7 +477,6 @@ class ProjectMemberViewSet(BaseViewSet): capture_exception(e) return Response({"error": "Something went wrong please try again later"}) - class AddMemberToProjectEndpoint(BaseAPIView): permission_classes = [ ProjectBasePermission, @@ -665,7 +669,8 @@ class ProjectIdentifierEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) - ProjectIdentifier.objects.filter(name=name, workspace__slug=slug).delete() + ProjectIdentifier.objects.filter( + name=name, workspace__slug=slug).delete() return Response( status=status.HTTP_204_NO_CONTENT, @@ -741,7 +746,8 @@ class ProjectUserViewsEndpoint(BaseAPIView): view_props = project_member.view_props default_props = project_member.default_props - project_member.view_props = request.data.get("view_props", view_props) + project_member.view_props = request.data.get( + "view_props", view_props) project_member.default_props = request.data.get( "default_props", default_props ) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 3635efcea..47dc1cd47 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -440,7 +440,11 @@ class WorkSpaceMemberViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) - if request.data.get("role", 10) > workspace_member.role: + # Get the requested user role + requested_workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user) + # Check if role is being updated + # One cannot update role higher than his own role + if "role" in request.data and request.data.get("role", workspace_member.role) > requested_workspace_member.role: return Response( { "error": "You cannot update a role that is higher than your own role" From 7e5b26ea82472fb1f626219ae5fb6f034e655293 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Thu, 25 May 2023 12:38:15 +0530 Subject: [PATCH 13/65] fix: project ordering messing up in projects page (#1122) --- apps/app/helpers/array.helper.ts | 4 +++- apps/app/hooks/use-projects.tsx | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/app/helpers/array.helper.ts b/apps/app/helpers/array.helper.ts index 3cd326be7..2432f88ad 100644 --- a/apps/app/helpers/array.helper.ts +++ b/apps/app/helpers/array.helper.ts @@ -8,10 +8,12 @@ export const groupBy = (array: any[], key: string) => { }; export const orderArrayBy = ( - array: any[], + orgArray: any[], key: string, ordering: "ascending" | "descending" = "ascending" ) => { + const array = [...orgArray]; + if (!array || !Array.isArray(array) || array.length === 0) return []; if (key[0] === "-") { diff --git a/apps/app/hooks/use-projects.tsx b/apps/app/hooks/use-projects.tsx index 247fc0db6..a8a2d455b 100644 --- a/apps/app/hooks/use-projects.tsx +++ b/apps/app/hooks/use-projects.tsx @@ -19,9 +19,9 @@ const useProjects = () => { workspaceSlug ? () => projectService.getProjects(workspaceSlug as string) : null ); - const recentProjects = projects + const recentProjects = [...(projects ?? [])] ?.sort((a, b) => Date.parse(`${a.updated_at}`) - Date.parse(`${b.updated_at}`)) - .filter((_item, index) => index < 3); + ?.slice(0, 3); return { projects: orderArrayBy(projects ?? [], "is_favorite", "descending") || [], From 11b28048bfe6221cc9ceaf37789ebcb444002570 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Thu, 25 May 2023 12:38:53 +0530 Subject: [PATCH 14/65] chore: analytics tracker events (#1119) * chore: added export analytics event tracker * chore: add analytics view events * chore: workspace analytics view track event added --- .../analytics/custom-analytics/sidebar.tsx | 70 +++++++++++++++++-- .../components/analytics/project-modal.tsx | 48 ++++++++++++- apps/app/pages/[workspaceSlug]/analytics.tsx | 26 ++++++- apps/app/services/api.service.ts | 1 - apps/app/services/track-event.service.ts | 28 ++++++++ 5 files changed, 166 insertions(+), 7 deletions(-) diff --git a/apps/app/components/analytics/custom-analytics/sidebar.tsx b/apps/app/components/analytics/custom-analytics/sidebar.tsx index e9a9d5d7f..7910c2cc8 100644 --- a/apps/app/components/analytics/custom-analytics/sidebar.tsx +++ b/apps/app/components/analytics/custom-analytics/sidebar.tsx @@ -7,6 +7,7 @@ import analyticsService from "services/analytics.service"; import projectService from "services/project.service"; import cyclesService from "services/cycles.service"; import modulesService from "services/modules.service"; +import trackEventServices from "services/track-event.service"; // hooks import useProjects from "hooks/use-projects"; import useToast from "hooks/use-toast"; @@ -23,7 +24,13 @@ import { ContrastIcon, LayerDiagonalIcon } from "components/icons"; // helpers import { renderShortDate } from "helpers/date-time.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IProject } from "types"; +import { + IAnalyticsParams, + IAnalyticsResponse, + IExportAnalyticsFormData, + IProject, + IWorkspace, +} from "types"; // fetch-keys import { ANALYTICS, CYCLE_DETAILS, MODULE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys"; // constants @@ -82,6 +89,59 @@ export const AnalyticsSidebar: React.FC = ({ : null ); + const trackExportAnalytics = () => { + const eventPayload: any = { + workspaceSlug: workspaceSlug?.toString(), + params: { + x_axis: params.x_axis, + y_axis: params.y_axis, + group: params.segment, + project: params.project, + }, + }; + + if (projectDetails) { + const workspaceDetails = projectDetails.workspace as IWorkspace; + + eventPayload.workspaceId = workspaceDetails.id; + eventPayload.workspaceName = workspaceDetails.name; + eventPayload.projectId = projectDetails.id; + eventPayload.projectIdentifier = projectDetails.identifier; + eventPayload.projectName = projectDetails.name; + } + + if (cycleDetails || moduleDetails) { + const details = cycleDetails || moduleDetails; + + eventPayload.workspaceId = details?.workspace_detail?.id; + eventPayload.workspaceName = details?.workspace_detail?.name; + eventPayload.projectId = details?.project_detail.id; + eventPayload.projectIdentifier = details?.project_detail.identifier; + eventPayload.projectName = details?.project_detail.name; + } + + if (cycleDetails) { + eventPayload.cycleId = cycleDetails.id; + eventPayload.cycleName = cycleDetails.name; + } + + if (moduleDetails) { + eventPayload.moduleId = moduleDetails.id; + eventPayload.moduleName = moduleDetails.name; + } + + trackEventServices.trackAnalyticsEvent( + eventPayload, + cycleId + ? "CYCLE_ANALYTICS_EXPORT" + : moduleId + ? "MODULE_ANALYTICS_EXPORT" + : projectId + ? "PROJECT_ANALYTICS_EXPORT" + : "WORKSPACE_ANALYTICS_EXPORT" + ); + }; + const exportAnalytics = () => { if (!workspaceSlug) return; @@ -95,13 +155,15 @@ export const AnalyticsSidebar: React.FC = ({ analyticsService .exportAnalytics(workspaceSlug.toString(), data) - .then((res) => + .then((res) => { setToastAlert({ type: "success", title: "Success!", message: res.message, - }) - ) + }); + + trackExportAnalytics(); + }) .catch(() => setToastAlert({ type: "error", diff --git a/apps/app/components/analytics/project-modal.tsx b/apps/app/components/analytics/project-modal.tsx index c2072642c..728ca4ed5 100644 --- a/apps/app/components/analytics/project-modal.tsx +++ b/apps/app/components/analytics/project-modal.tsx @@ -13,6 +13,7 @@ import analyticsService from "services/analytics.service"; import projectService from "services/project.service"; import cyclesService from "services/cycles.service"; import modulesService from "services/modules.service"; +import trackEventServices from "services/track-event.service"; // components import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; // icons @@ -22,7 +23,7 @@ import { XMarkIcon, } from "@heroicons/react/24/outline"; // types -import { IAnalyticsParams } from "types"; +import { IAnalyticsParams, IWorkspace } from "types"; // fetch-keys import { ANALYTICS, CYCLE_DETAILS, MODULE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys"; @@ -95,6 +96,50 @@ export const AnalyticsProjectModal: React.FC = ({ isOpen, onClose }) => { : null ); + const trackAnalyticsEvent = (tab: string) => { + const eventPayload: any = { + workspaceSlug: workspaceSlug?.toString(), + }; + + if (projectDetails) { + const workspaceDetails = projectDetails.workspace as IWorkspace; + + eventPayload.workspaceId = workspaceDetails.id; + eventPayload.workspaceName = workspaceDetails.name; + eventPayload.projectId = projectDetails.id; + eventPayload.projectIdentifier = projectDetails.identifier; + eventPayload.projectName = projectDetails.name; + } + + if (cycleDetails || moduleDetails) { + const details = cycleDetails || moduleDetails; + + eventPayload.workspaceId = details?.workspace_detail?.id; + eventPayload.workspaceName = details?.workspace_detail?.name; + eventPayload.projectId = details?.project_detail.id; + eventPayload.projectIdentifier = details?.project_detail.identifier; + eventPayload.projectName = details?.project_detail.name; + } + + if (cycleDetails) { + eventPayload.cycleId = cycleDetails.id; + eventPayload.cycleName = cycleDetails.name; + } + + if (moduleDetails) { + eventPayload.moduleId = moduleDetails.id; + eventPayload.moduleName = moduleDetails.name; + } + + const eventType = + tab === "Scope and Demand" ? "SCOPE_AND_DEMAND_ANALYTICS" : "CUSTOM_ANALYTICS"; + + trackEventServices.trackAnalyticsEvent( + eventPayload, + cycleId ? `CYCLE_${eventType}` : moduleId ? `MODULE_${eventType}` : `PROJECT_${eventType}` + ); + }; + const handleClose = () => { onClose(); }; @@ -146,6 +191,7 @@ export const AnalyticsProjectModal: React.FC = ({ isOpen, onClose }) => { selected ? "bg-brand-surface-2" : "" }` } + onClick={() => trackAnalyticsEvent(tab)} > {tab} diff --git a/apps/app/pages/[workspaceSlug]/analytics.tsx b/apps/app/pages/[workspaceSlug]/analytics.tsx index b32399448..ce161fbc4 100644 --- a/apps/app/pages/[workspaceSlug]/analytics.tsx +++ b/apps/app/pages/[workspaceSlug]/analytics.tsx @@ -1,4 +1,4 @@ -import React, { Fragment } from "react"; +import React, { Fragment, useEffect } from "react"; import { useRouter } from "next/router"; @@ -10,6 +10,7 @@ import { useForm } from "react-hook-form"; import { Tab } from "@headlessui/react"; // services import analyticsService from "services/analytics.service"; +import trackEventServices from "services/track-event.service"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; // components @@ -48,6 +49,28 @@ const Analytics = () => { workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null ); + const trackAnalyticsEvent = (tab: string) => { + const eventPayload = { + workspaceSlug: workspaceSlug?.toString(), + }; + + const eventType = + tab === "Scope and Demand" + ? "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" + : "WORKSPACE_CUSTOM_ANALYTICS"; + + trackEventServices.trackAnalyticsEvent(eventPayload, eventType); + }; + + useEffect(() => { + if (!workspaceSlug) return; + + trackEventServices.trackAnalyticsEvent( + { workspaceSlug: workspaceSlug?.toString() }, + "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" + ); + }, [workspaceSlug]); + return ( { selected ? "bg-brand-surface-2" : "" }` } + onClick={() => trackAnalyticsEvent(tab)} > {tab} diff --git a/apps/app/services/api.service.ts b/apps/app/services/api.service.ts index 958205005..5434742ba 100644 --- a/apps/app/services/api.service.ts +++ b/apps/app/services/api.service.ts @@ -9,7 +9,6 @@ axios.interceptors.response.use( if (unAuthorizedStatus.includes(status)) { Cookies.remove("refreshToken", { path: "/" }); Cookies.remove("accessToken", { path: "/" }); - console.log("window.location.href", window.location.pathname); if (window.location.pathname != "/signin") window.location.href = "/signin"; } return Promise.reject(error); diff --git a/apps/app/services/track-event.service.ts b/apps/app/services/track-event.service.ts index 54de7119b..29ea0084a 100644 --- a/apps/app/services/track-event.service.ts +++ b/apps/app/services/track-event.service.ts @@ -84,6 +84,21 @@ type ImporterEventType = | "GITHUB_IMPORTER_DELETE" | "JIRA_IMPORTER_CREATE" | "JIRA_IMPORTER_DELETE"; + +type AnalyticsEventType = + | "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" + | "WORKSPACE_CUSTOM_ANALYTICS" + | "WORKSPACE_ANALYTICS_EXPORT" + | "PROJECT_SCOPE_AND_DEMAND_ANALYTICS" + | "PROJECT_CUSTOM_ANALYTICS" + | "PROJECT_ANALYTICS_EXPORT" + | "CYCLE_SCOPE_AND_DEMAND_ANALYTICS" + | "CYCLE_CUSTOM_ANALYTICS" + | "CYCLE_ANALYTICS_EXPORT" + | "MODULE_SCOPE_AND_DEMAND_ANALYTICS" + | "MODULE_CUSTOM_ANALYTICS" + | "MODULE_ANALYTICS_EXPORT"; + class TrackEventServices extends APIService { constructor() { super("/"); @@ -615,6 +630,19 @@ class TrackEventServices extends APIService { }, }); } + + async trackAnalyticsEvent(data: any, eventName: AnalyticsEventType): Promise { + const payload = { ...data }; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: payload, + }, + }); + } } const trackEventServices = new TrackEventServices(); From 16604dd31bdde172b231d5c56de59acdeae791b9 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Thu, 25 May 2023 14:13:54 +0530 Subject: [PATCH 15/65] refactor: page views endpoint (#1130) --- apiserver/plane/api/urls.py | 24 --- apiserver/plane/api/views/__init__.py | 4 - apiserver/plane/api/views/page.py | 296 +++++--------------------- 3 files changed, 50 insertions(+), 274 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 73d3382a2..9decefca8 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -111,10 +111,6 @@ from plane.api.views import ( PageBlockViewSet, PageFavoriteViewSet, CreateIssueFromPageBlockEndpoint, - RecentPagesEndpoint, - FavoritePagesEndpoint, - MyPagesEndpoint, - CreatedbyOtherPagesEndpoint, ## End Pages # Api Tokens ApiTokenEndpoint, @@ -1053,26 +1049,6 @@ urlpatterns = [ CreateIssueFromPageBlockEndpoint.as_view(), name="page-block-issues", ), - path( - "workspaces//projects//pages/recent-pages/", - RecentPagesEndpoint.as_view(), - name="recent-pages", - ), - path( - "workspaces//projects//pages/favorite-pages/", - FavoritePagesEndpoint.as_view(), - name="recent-pages", - ), - path( - "workspaces//projects//pages/my-pages/", - MyPagesEndpoint.as_view(), - name="user-pages", - ), - path( - "workspaces//projects//pages/created-by-other-pages/", - CreatedbyOtherPagesEndpoint.as_view(), - name="created-by-other-pages", - ), ## End Pages # API Tokens path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"), diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 6b4cb52a7..4177b1371 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -118,10 +118,6 @@ from .page import ( PageBlockViewSet, PageFavoriteViewSet, CreateIssueFromPageBlockEndpoint, - RecentPagesEndpoint, - FavoritePagesEndpoint, - MyPagesEndpoint, - CreatedbyOtherPagesEndpoint, ) from .search import GlobalSearchEndpoint, IssueSearchEndpoint diff --git a/apiserver/plane/api/views/page.py b/apiserver/plane/api/views/page.py index 88ce318cf..edca47ffe 100644 --- a/apiserver/plane/api/views/page.py +++ b/apiserver/plane/api/views/page.py @@ -125,7 +125,57 @@ class PageViewSet(BaseViewSet): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + def list(self, request, slug, project_id): + try: + queryset = self.get_queryset() + page_view = request.GET.get("page_view", False) + if not page_view: + return Response({"error": "Page View parameter is required"}, status=status.HTTP_400_BAD_REQUEST) + + # All Pages + if page_view == "all": + return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) + + # Recent pages + if page_view == "recent": + current_time = date.today() + day_before = current_time - timedelta(days=1) + todays_pages = queryset.filter(updated_at__date=date.today()) + yesterdays_pages = queryset.filter(updated_at__date=day_before) + earlier_this_week = queryset.filter( updated_at__date__range=( + (timezone.now() - timedelta(days=7)), + (timezone.now() - timedelta(days=2)), + )) + return Response( + { + "today": PageSerializer(todays_pages, many=True).data, + "yesterday": PageSerializer(yesterdays_pages, many=True).data, + "earlier_this_week": PageSerializer(earlier_this_week, many=True).data, + }, + status=status.HTTP_200_OK, + ) + + # Favorite Pages + if page_view == "favorite": + queryset = queryset.filter(is_favorite=True) + return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) + + # My pages + if page_view == "created_by_me": + queryset = queryset.filter(owned_by=request.user) + return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) + + # Created by other Pages + if page_view == "created_by_other": + queryset = queryset.filter(~Q(owned_by=request.user), access=0) + return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) + + 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) class PageBlockViewSet(BaseViewSet): serializer_class = PageBlockSerializer @@ -269,249 +319,3 @@ class CreateIssueFromPageBlockEndpoint(BaseAPIView): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) - - -class RecentPagesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - try: - subquery = PageFavorite.objects.filter( - user=request.user, - page_id=OuterRef("pk"), - project_id=project_id, - workspace__slug=slug, - ) - current_time = date.today() - day_before = current_time - timedelta(days=1) - - todays_pages = ( - Page.objects.filter( - updated_at__date=date.today(), - workspace__slug=slug, - project_id=project_id, - ) - .filter(project__project_projectmember__member=request.user) - .annotate(is_favorite=Exists(subquery)) - .filter(Q(owned_by=self.request.user) | Q(access=0)) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .prefetch_related("labels") - .prefetch_related( - Prefetch( - "blocks", - queryset=PageBlock.objects.select_related( - "page", "issue", "workspace", "project" - ), - ) - ) - .order_by("-is_favorite", "-updated_at") - ) - - yesterdays_pages = ( - Page.objects.filter( - updated_at__date=day_before, - workspace__slug=slug, - project_id=project_id, - ) - .filter(project__project_projectmember__member=request.user) - .annotate(is_favorite=Exists(subquery)) - .filter(Q(owned_by=self.request.user) | Q(access=0)) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .prefetch_related("labels") - .prefetch_related( - Prefetch( - "blocks", - queryset=PageBlock.objects.select_related( - "page", "issue", "workspace", "project" - ), - ) - ) - .order_by("-is_favorite", "-updated_at") - ) - - earlier_this_week = ( - Page.objects.filter( - updated_at__date__range=( - (timezone.now() - timedelta(days=7)), - (timezone.now() - timedelta(days=2)), - ), - workspace__slug=slug, - project_id=project_id, - ) - .annotate(is_favorite=Exists(subquery)) - .filter(Q(owned_by=self.request.user) | Q(access=0)) - .filter(project__project_projectmember__member=request.user) - .annotate(is_favorite=Exists(subquery)) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .prefetch_related("labels") - .prefetch_related( - Prefetch( - "blocks", - queryset=PageBlock.objects.select_related( - "page", "issue", "workspace", "project" - ), - ) - ) - .order_by("-is_favorite", "-updated_at") - ) - todays_pages_serializer = PageSerializer(todays_pages, many=True) - yesterday_pages_serializer = PageSerializer(yesterdays_pages, many=True) - earlier_this_week_serializer = PageSerializer(earlier_this_week, many=True) - return Response( - { - "today": todays_pages_serializer.data, - "yesterday": yesterday_pages_serializer.data, - "earlier_this_week": earlier_this_week_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, - ) - - -class FavoritePagesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - try: - subquery = PageFavorite.objects.filter( - user=request.user, - page_id=OuterRef("pk"), - project_id=project_id, - workspace__slug=slug, - ) - pages = ( - Page.objects.filter( - workspace__slug=slug, - project_id=project_id, - ) - .annotate(is_favorite=Exists(subquery)) - .filter(Q(owned_by=self.request.user) | Q(access=0)) - .filter(project__project_projectmember__member=request.user) - .filter(is_favorite=True) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .prefetch_related("labels") - .prefetch_related( - Prefetch( - "blocks", - queryset=PageBlock.objects.select_related( - "page", "issue", "workspace", "project" - ), - ) - ) - .order_by("name", "-is_favorite") - ) - - serializer = PageSerializer(pages, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - -class MyPagesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - try: - subquery = PageFavorite.objects.filter( - user=request.user, - page_id=OuterRef("pk"), - project_id=project_id, - workspace__slug=slug, - ) - pages = ( - Page.objects.filter( - workspace__slug=slug, project_id=project_id, owned_by=request.user - ) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .prefetch_related("labels") - .annotate(is_favorite=Exists(subquery)) - .filter(Q(owned_by=self.request.user) | Q(access=0)) - .filter(project__project_projectmember__member=request.user) - .prefetch_related( - Prefetch( - "blocks", - queryset=PageBlock.objects.select_related( - "page", "issue", "workspace", "project" - ), - ) - ) - .order_by("-is_favorite", "name") - ) - serializer = PageSerializer(pages, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - -class CreatedbyOtherPagesEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def get(self, request, slug, project_id): - try: - subquery = PageFavorite.objects.filter( - user=request.user, - page_id=OuterRef("pk"), - project_id=project_id, - workspace__slug=slug, - ) - pages = ( - Page.objects.filter( - ~Q(owned_by=request.user), - workspace__slug=slug, - project_id=project_id, - access=0, - ) - .select_related("project") - .select_related("workspace") - .select_related("owned_by") - .prefetch_related("labels") - .annotate(is_favorite=Exists(subquery)) - .prefetch_related( - Prefetch( - "blocks", - queryset=PageBlock.objects.select_related( - "page", "issue", "workspace", "project" - ), - ) - ) - .order_by("-is_favorite", "name") - ) - serializer = PageSerializer(pages, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) From b6321438cec779a2993830258f297e1b4264278f Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 26 May 2023 11:09:59 +0530 Subject: [PATCH 16/65] chore: docker setup (#1136) * chore: update docker environment variables and compose file for better readability * dev: update single dockerfile * dev: update WEB_URL configuration * dev: move database settings to environment variable * chore: remove port configuration from default compose file * dev: update example env to add EMAIL_FROM and default values for AWS --- .env.example | 15 +- Dockerfile | 13 +- apiserver/plane/api/views/asset.py | 8 +- apiserver/plane/api/views/issue.py | 4 +- .../plane/bgtasks/email_verification_task.py | 2 +- .../plane/bgtasks/forgot_password_task.py | 2 +- .../plane/bgtasks/magic_link_code_task.py | 2 +- .../plane/bgtasks/project_invitation_task.py | 2 +- .../bgtasks/workspace_invitation_task.py | 2 +- apiserver/plane/settings/local.py | 2 +- apiserver/plane/settings/production.py | 4 +- docker-compose-hub.yml | 56 ++-- docker-compose.yml | 283 +++++++++--------- nginx/nginx.conf | 2 +- setup.sh | 2 +- 15 files changed, 204 insertions(+), 195 deletions(-) diff --git a/.env.example b/.env.example index 727ea0806..8e65a6900 100644 --- a/.env.example +++ b/.env.example @@ -26,16 +26,23 @@ EMAIL_HOST="" EMAIL_HOST_USER="" EMAIL_HOST_PASSWORD="" EMAIL_PORT=587 +EMAIL_FROM="Team Plane " # AWS Settings AWS_REGION="" -AWS_ACCESS_KEY_ID="" -AWS_SECRET_ACCESS_KEY="" -AWS_S3_BUCKET_NAME="" +AWS_ACCESS_KEY_ID="access-key" +AWS_SECRET_ACCESS_KEY="secret-key" +AWS_S3_BUCKET_NAME="uploads" AWS_S3_ENDPOINT_URL="" # GPT settings OPENAI_API_KEY="" GPT_ENGINE="" -# Auto generated and Required \ No newline at end of file +# Github +GITHUB_CLIENT_SECRET="" # For fetching release notes + +# Settings related to Docker +DOCKERIZED=1 + +# Auto generated and Required that will be generated from setup.sh \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index cb7ef6887..0610aab1c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,6 @@ RUN turbo prune --scope=app --docker # Add lockfile and package.json's of isolated subworkspace FROM node:18-alpine AS installer - RUN apk add --no-cache libc6-compat RUN apk update WORKDIR /app @@ -44,6 +43,8 @@ FROM python:3.11.1-alpine3.17 AS backend ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 ENV PIP_DISABLE_PIP_VERSION_CHECK=1 +ENV DJANGO_SETTINGS_MODULE plane.settings.production +ENV DOCKERIZED 1 WORKDIR /code @@ -88,11 +89,6 @@ RUN chmod +x ./bin/takeoff ./bin/worker RUN chmod -R 777 /code # Expose container port and run entry point script -EXPOSE 8000 -EXPOSE 3000 -EXPOSE 80 - - WORKDIR /app @@ -126,9 +122,6 @@ COPY start.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/replace-env-vars.sh RUN chmod +x /usr/local/bin/start.sh +EXPOSE 80 CMD ["supervisord","-c","/code/supervisor.conf"] - - - - diff --git a/apiserver/plane/api/views/asset.py b/apiserver/plane/api/views/asset.py index 705735e51..0102867d7 100644 --- a/apiserver/plane/api/views/asset.py +++ b/apiserver/plane/api/views/asset.py @@ -35,8 +35,8 @@ class FileAssetEndpoint(BaseAPIView): serializer.save(workspace_id=request.user.last_workspace_id) response_data = serializer.data - if settings.DOCKERIZED and "minio:9000" in response_data["asset"]: - response_data["asset"] = response_data["asset"].replace("minio:9000", settings.WEB_URL) + if settings.DOCKERIZED and settings.AWS_S3_ENDPOINT_URL in response_data["asset"]: + response_data["asset"] = response_data["asset"].replace(settings.AWS_S3_ENDPOINT_URL, settings.WEB_URL) return Response(response_data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except Exception as e: @@ -86,8 +86,8 @@ class UserAssetsEndpoint(BaseAPIView): if serializer.is_valid(): serializer.save() response_data = serializer.data - if settings.DOCKERIZED and "minio:9000" in response_data["asset"]: - response_data["asset"] = response_data["asset"].replace("minio:9000", settings.WEB_URL) + if settings.DOCKERIZED and settings.AWS_S3_ENDPOINT_URL in response_data["asset"]: + response_data["asset"] = response_data["asset"].replace(settings.AWS_S3_ENDPOINT_URL, settings.WEB_URL) return Response(response_data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except Exception as e: diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 4f519ce69..2a2b3e3a9 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -789,8 +789,8 @@ class IssueAttachmentEndpoint(BaseAPIView): if serializer.is_valid(): serializer.save(project_id=project_id, issue_id=issue_id) response_data = serializer.data - if settings.DOCKERIZED and "minio:9000" in response_data["asset"]: - response_data["asset"] = response_data["asset"].replace("minio:9000", settings.WEB_URL) + if settings.DOCKERIZED and settings.AWS_S3_ENDPOINT_URL in response_data["asset"]: + response_data["asset"] = response_data["asset"].replace(settings.AWS_S3_ENDPOINT_URL, settings.WEB_URL) issue_activity.delay( type="attachment.activity.created", requested_data=None, diff --git a/apiserver/plane/bgtasks/email_verification_task.py b/apiserver/plane/bgtasks/email_verification_task.py index 1da3a7510..89551044b 100644 --- a/apiserver/plane/bgtasks/email_verification_task.py +++ b/apiserver/plane/bgtasks/email_verification_task.py @@ -19,7 +19,7 @@ def email_verification(first_name, email, token, current_site): try: realtivelink = "/request-email-verification/" + "?token=" + str(token) - abs_url = "http://" + current_site + realtivelink + abs_url = current_site + realtivelink from_email_string = settings.EMAIL_FROM diff --git a/apiserver/plane/bgtasks/forgot_password_task.py b/apiserver/plane/bgtasks/forgot_password_task.py index f13f1b89a..d3f00e666 100644 --- a/apiserver/plane/bgtasks/forgot_password_task.py +++ b/apiserver/plane/bgtasks/forgot_password_task.py @@ -17,7 +17,7 @@ def forgot_password(first_name, email, uidb64, token, current_site): try: realtivelink = f"/email-verify/?uidb64={uidb64}&token={token}/" - abs_url = "http://" + current_site + realtivelink + abs_url = current_site + realtivelink from_email_string = settings.EMAIL_FROM diff --git a/apiserver/plane/bgtasks/magic_link_code_task.py b/apiserver/plane/bgtasks/magic_link_code_task.py index 00a4e6807..29851c435 100644 --- a/apiserver/plane/bgtasks/magic_link_code_task.py +++ b/apiserver/plane/bgtasks/magic_link_code_task.py @@ -13,7 +13,7 @@ from sentry_sdk import capture_exception def magic_link(email, key, token, current_site): try: realtivelink = f"/magic-sign-in/?password={token}&key={key}" - abs_url = "http://" + current_site + realtivelink + abs_url = current_site + realtivelink from_email_string = settings.EMAIL_FROM diff --git a/apiserver/plane/bgtasks/project_invitation_task.py b/apiserver/plane/bgtasks/project_invitation_task.py index 2015ffe5e..7f1125f80 100644 --- a/apiserver/plane/bgtasks/project_invitation_task.py +++ b/apiserver/plane/bgtasks/project_invitation_task.py @@ -21,7 +21,7 @@ def project_invitation(email, project_id, token, current_site): ) relativelink = f"/project-member-invitation/{project_member_invite.id}" - abs_url = "http://" + current_site + relativelink + abs_url = current_site + relativelink from_email_string = settings.EMAIL_FROM diff --git a/apiserver/plane/bgtasks/workspace_invitation_task.py b/apiserver/plane/bgtasks/workspace_invitation_task.py index 0ce32eee0..3386a59d5 100644 --- a/apiserver/plane/bgtasks/workspace_invitation_task.py +++ b/apiserver/plane/bgtasks/workspace_invitation_task.py @@ -25,7 +25,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor): realtivelink = ( f"/workspace-member-invitation/{workspace_member_invite.id}?email={email}" ) - abs_url = "http://" + current_site + realtivelink + abs_url = current_site + realtivelink from_email_string = settings.EMAIL_FROM diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index 87e04e1ed..70a4be49a 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -70,7 +70,7 @@ MEDIA_ROOT = os.path.join(BASE_DIR, "uploads") if DOCKERIZED: REDIS_URL = os.environ.get("REDIS_URL") -WEB_URL = os.environ.get("WEB_URL", "localhost:3000") +WEB_URL = os.environ.get("WEB_URL", "http://localhost:3000") PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False) diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index 4d7da6ce3..81a3c2082 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -90,7 +90,7 @@ if DOCKERIZED: # The name of the bucket to store files in. AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads") # The full URL to the S3 endpoint. Leave blank to use the default region URL. - AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "http://minio:9000") + AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "http://plane-minio:9000") # Default permissions AWS_DEFAULT_ACL = "public-read" AWS_QUERYSTRING_AUTH = False @@ -223,7 +223,7 @@ else: } -WEB_URL = os.environ.get("WEB_URL") +WEB_URL = os.environ.get("WEB_URL", "https://app.plane.so") PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml index 53a7ee0f2..465f9d3f3 100644 --- a/docker-compose-hub.yml +++ b/docker-compose-hub.yml @@ -3,7 +3,7 @@ version: "3.8" services: plane-web: container_name: planefrontend - image: makeplane/plane-frontend:0.6 + image: makeplane/plane-frontend:latest restart: always command: /usr/local/bin/start.sh environment: @@ -19,12 +19,12 @@ services: plane-api: container_name: planebackend - image: makeplane/plane-backend:0.6 + image: makeplane/plane-backend:latest restart: always command: ./bin/takeoff environment: DJANGO_SETTINGS_MODULE: plane.settings.production - DATABASE_URL: postgres://plane:xyzzyspoon@plane-db:5432/plane + DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@plane-db:5432/plane REDIS_URL: redis://plane-redis:6379/ EMAIL_HOST: ${EMAIL_HOST} EMAIL_HOST_USER: ${EMAIL_HOST_USER} @@ -33,7 +33,7 @@ services: AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} - WEB_URL: localhost/ + WEB_URL: ${WEB_URL} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} DISABLE_COLLECTSTATIC: 1 DOCKERIZED: 1 @@ -48,12 +48,12 @@ services: plane-worker: container_name: planerqworker - image: makeplane/plane-worker:0.6 + image: makeplane/plane-worker:latest restart: always command: ./bin/worker environment: DJANGO_SETTINGS_MODULE: plane.settings.production - DATABASE_URL: postgres://plane:xyzzyspoon@plane-db:5432/plane + DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@plane-db:5432/plane REDIS_URL: redis://plane-redis:6379/ EMAIL_HOST: ${EMAIL_HOST} EMAIL_HOST_USER: ${EMAIL_HOST_USER} @@ -62,7 +62,7 @@ services: AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} - WEB_URL: localhost/ + WEB_URL: ${WEB_URL} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} DISABLE_COLLECTSTATIC: 1 DOCKERIZED: 1 @@ -80,9 +80,9 @@ services: restart: always command: postgres -c 'max_connections=1000' environment: - POSTGRES_USER: plane + POSTGRES_USER: ${PGUSER} POSTGRES_DB: plane - POSTGRES_PASSWORD: xyzzyspoon + POSTGRES_PASSWORD: ${PGPASSWORD} volumes: - pgdata:/var/lib/postgresql/data @@ -94,25 +94,30 @@ services: - redisdata:/data plane-minio: - container_name: plane-minio - image: minio/minio - environment: - MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} - MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} - command: server /export --console-address ":9090" + container_name: plane-minio + image: minio/minio + volumes: + - uploads:/export + environment: + MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} + MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} + command: server /export --console-address ":9090" + ports: + - 9000:9000 createbuckets: - image: minio/mc - depends_on: - - minio - entrypoint: > - /bin/sh -c " - /usr/bin/mc config host add plane-minio http://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; - " + 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; + /usr/bin/mc mb plane-minio/\$AWS_S3_BUCKET_NAME; + /usr/bin/mc anonymous set download plane-minio/\$AWS_S3_BUCKET_NAME; + exit 0; + " +# Comment this if you already have a reverse proxy running nginx: container_name: nginx build: @@ -128,3 +133,4 @@ services: volumes: pgdata: redisdata: + uploads: diff --git a/docker-compose.yml b/docker-compose.yml index 68f06a3c8..44252ad6a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,145 +1,148 @@ version: "3.8" services: - nginx: - container_name: nginx - build: - context: ./nginx - dockerfile: Dockerfile - restart: always - ports: - - 80:80 - depends_on: - - plane-web - - plane-api - db: - image: postgres:15.2-alpine - container_name: db - restart: always - volumes: - - pgdata:/var/lib/postgresql/data - environment: - POSTGRES_USER: plane - POSTGRES_DB: plane - POSTGRES_PASSWORD: xyzzyspoon - PGDATA: /var/lib/postgresql/data - command: postgres -c 'max_connections=1000' - ports: - - 5432:5432 - redis: - image: redis:6.2.7-alpine - container_name: redis - restart: always - ports: - - 6379:6379 - volumes: - - redisdata:/data - plane-web: - container_name: planefrontend - build: - context: . - dockerfile: ./apps/app/Dockerfile.web - args: - NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 - command: [ "/usr/local/bin/start.sh" ] - ports: - - 3000:3000 - environment: - NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} - NEXT_PUBLIC_GOOGLE_CLIENTID: "0" - NEXT_PUBLIC_GITHUB_APP_NAME: "0" - NEXT_PUBLIC_GITHUB_ID: "0" - NEXT_PUBLIC_SENTRY_DSN: "0" - NEXT_PUBLIC_ENABLE_OAUTH: "0" - NEXT_PUBLIC_ENABLE_SENTRY: "0" - NEXT_PUBLIC_ENABLE_SESSION_RECORDER: "0" - NEXT_PUBLIC_TRACK_EVENTS: "0" - plane-api: - container_name: planebackend - build: - context: ./apiserver - dockerfile: Dockerfile.api - restart: always - ports: - - 8000:8000 - environment: - DJANGO_SETTINGS_MODULE: plane.settings.production - DATABASE_URL: postgres://plane:xyzzyspoon@db:5432/plane - REDIS_URL: redis://redis:6379/ - EMAIL_HOST: ${EMAIL_HOST} - EMAIL_HOST_USER: ${EMAIL_HOST_USER} - EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} - AWS_REGION: ${AWS_REGION} - AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} - AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} - AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL} - WEB_URL: ${WEB_URL} - GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} - DISABLE_COLLECTSTATIC: 1 - DOCKERIZED: 1 - OPENAI_API_KEY: ${OPENAI_API_KEY} - GPT_ENGINE: ${GPT_ENGINE} - SECRET_KEY: ${SECRET_KEY} - depends_on: - - db - - redis - command: ./bin/takeoff - links: - - db:db - - redis:redis - plane-worker: - container_name: planebgworker - build: - context: ./apiserver - dockerfile: Dockerfile.api - depends_on: - - redis - - db - - plane-api - command: ./bin/worker - links: - - redis:redis - - db:db - environment: - DJANGO_SETTINGS_MODULE: plane.settings.production - DATABASE_URL: postgres://plane:xyzzyspoon@db:5432/plane - REDIS_URL: redis://redis:6379/ - EMAIL_HOST: ${EMAIL_HOST} - EMAIL_HOST_USER: ${EMAIL_HOST_USER} - EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} - AWS_REGION: ${AWS_REGION} - AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} - AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} - AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL} - WEB_URL: ${WEB_URL} - GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} - DISABLE_COLLECTSTATIC: 1 - DOCKERIZED: 1 - OPENAI_API_KEY: ${OPENAI_API_KEY} - GPT_ENGINE: ${GPT_ENGINE} - SECRET_KEY: ${SECRET_KEY} - minio: - image: minio/minio - ports: - - 9000:9000 - environment: - MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} - MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} - command: server /export --console-address ":9090" + plane-web: + container_name: planefrontend + build: + context: . + dockerfile: ./apps/app/Dockerfile.web + args: + NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 + restart: always + command: [ "/usr/local/bin/start.sh" ] + environment: + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + NEXT_PUBLIC_GOOGLE_CLIENTID: "0" + NEXT_PUBLIC_GITHUB_APP_NAME: "0" + NEXT_PUBLIC_GITHUB_ID: "0" + NEXT_PUBLIC_SENTRY_DSN: "0" + NEXT_PUBLIC_ENABLE_OAUTH: "0" + NEXT_PUBLIC_ENABLE_SENTRY: "0" + NEXT_PUBLIC_ENABLE_SESSION_RECORDER: "0" + NEXT_PUBLIC_TRACK_EVENTS: "0" + + plane-api: + container_name: planebackend + build: + context: ./apiserver + dockerfile: Dockerfile.api + restart: always + command: ./bin/takeoff + environment: + DJANGO_SETTINGS_MODULE: plane.settings.production + DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@plane-db:5432/plane + REDIS_URL: redis://redis:6379/ + EMAIL_HOST: ${EMAIL_HOST} + EMAIL_HOST_USER: ${EMAIL_HOST_USER} + EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + AWS_REGION: ${AWS_REGION} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} + AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL} + WEB_URL: ${WEB_URL} + GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} + DISABLE_COLLECTSTATIC: 1 + DOCKERIZED: 1 + OPENAI_API_KEY: ${OPENAI_API_KEY} + GPT_ENGINE: ${GPT_ENGINE} + SECRET_KEY: ${SECRET_KEY} + depends_on: + - plane-db + - plane-redis + + plane-worker: + container_name: planebgworker + build: + context: ./apiserver + dockerfile: Dockerfile.api + restart: always + command: ./bin/worker + environment: + DJANGO_SETTINGS_MODULE: plane.settings.production + DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@plane-db:5432/plane + REDIS_URL: redis://redis:6379/ + EMAIL_HOST: ${EMAIL_HOST} + EMAIL_HOST_USER: ${EMAIL_HOST_USER} + EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + AWS_REGION: ${AWS_REGION} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} + AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL} + WEB_URL: ${WEB_URL} + GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} + DISABLE_COLLECTSTATIC: 1 + DOCKERIZED: 1 + OPENAI_API_KEY: ${OPENAI_API_KEY} + GPT_ENGINE: ${GPT_ENGINE} + SECRET_KEY: ${SECRET_KEY} + depends_on: + - plane-api + - plane-db + - plane-redis + + plane-db: + container_name: plane-db + image: postgres:15.2-alpine + restart: always + command: postgres -c 'max_connections=1000' + volumes: + - pgdata:/var/lib/postgresql/data + environment: + POSTGRES_USER: ${PGUSER} + POSTGRES_DB: plane + POSTGRES_PASSWORD: ${PGPASSWORD} + PGDATA: /var/lib/postgresql/data + ports: + - 5432:5432 + + plane-redis: + container_name: redis + image: redis:6.2.7-alpine + restart: always + volumes: + - redisdata:/data + ports: + - 6379:6379 + + plane-minio: + container_name: plane-minio + image: minio/minio + restart: always + command: server /export --console-address ":9090" + volumes: + - uploads:/export + environment: + MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} + MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} + + 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; + " + depends_on: + - plane-minio + + nginx: + container_name: nginx + build: + context: ./nginx + dockerfile: Dockerfile + restart: always + ports: + - 80:80 + depends_on: + - plane-web + - plane-api + - createbuckets: - image: minio/mc - depends_on: - - minio - entrypoint: > - /bin/sh -c " - /usr/bin/mc config host add plane-minio http://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; - " volumes: - pgdata: - redisdata: + pgdata: + redisdata: + uploads: diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 8f5fc2910..b57dc97f2 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -17,7 +17,7 @@ server { } location /uploads/ { - proxy_pass http://minio:9000/uploads/; + proxy_pass http://plane-minio:9000/uploads/; } } } \ No newline at end of file diff --git a/setup.sh b/setup.sh index f4ca1ea6d..7fc5847e0 100755 --- a/setup.sh +++ b/setup.sh @@ -7,7 +7,7 @@ export LC_CTYPE=C # Generate the NEXT_PUBLIC_API_BASE_URL with given IP -echo -e "\nNEXT_PUBLIC_API_BASE_URL=http://$1" >> ./.env +echo -e "\nNEXT_PUBLIC_API_BASE_URL=$1" >> ./.env # Generate the SECRET_KEY that will be used by django echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c50)\"" >> ./.env From f80b3f1eb13aaf5dc0302f722937f817253d680f Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 26 May 2023 13:51:09 +0530 Subject: [PATCH 17/65] fix: issue ordering for priority and updated_by parameters (#1142) --- apiserver/plane/api/serializers/issue.py | 4 +++ apiserver/plane/api/views/issue.py | 42 +++++++++++++++++++++--- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index a39128088..d3c17d057 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -1,3 +1,6 @@ +# Django imports +from django.utils import timezone + # Third Party imports from rest_framework import serializers @@ -251,6 +254,7 @@ class IssueCreateSerializer(BaseSerializer): batch_size=10, ) + instance.updated_at = timezone.now() return super().update(instance, validated_data) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 2a2b3e3a9..abdfe6aae 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -4,12 +4,24 @@ import random from itertools import chain # Django imports -from django.db.models import Prefetch, OuterRef, Func, F, Q, Count +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Q, + Count, + Case, + Value, + CharField, + When, +) from django.core.serializers.json import DjangoJSONEncoder from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page from django.db.models.functions import Coalesce from django.conf import settings + # Third Party imports from rest_framework.response import Response from rest_framework import status @@ -144,9 +156,13 @@ class IssueViewSet(BaseViewSet): filters = issue_filters(request.query_params, "GET") show_sub_issues = request.GET.get("show_sub_issues", "true") + # Custom ordering for priority + priority_order = ["urgent", "high", "medium", "low", None] + + order_by_param = request.GET.get("order_by", "-created_at") + issue_queryset = ( self.get_queryset() - .order_by(request.GET.get("order_by", "created_at")) .filter(**filters) .annotate(cycle_id=F("issue_cycle__id")) .annotate(module_id=F("issue_module__id")) @@ -166,6 +182,19 @@ class IssueViewSet(BaseViewSet): ) ) + if order_by_param == "priority": + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + else: + issue_queryset = issue_queryset.order_by(order_by_param) + issue_queryset = ( issue_queryset if show_sub_issues == "true" @@ -789,8 +818,13 @@ class IssueAttachmentEndpoint(BaseAPIView): if serializer.is_valid(): serializer.save(project_id=project_id, issue_id=issue_id) response_data = serializer.data - if settings.DOCKERIZED and settings.AWS_S3_ENDPOINT_URL in response_data["asset"]: - response_data["asset"] = response_data["asset"].replace(settings.AWS_S3_ENDPOINT_URL, settings.WEB_URL) + if ( + settings.DOCKERIZED + and settings.AWS_S3_ENDPOINT_URL in response_data["asset"] + ): + response_data["asset"] = response_data["asset"].replace( + settings.AWS_S3_ENDPOINT_URL, settings.WEB_URL + ) issue_activity.delay( type="attachment.activity.created", requested_data=None, From cd821a934d2bb4d0a79a0736608d3d9fb70c511d Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 26 May 2023 14:04:15 +0530 Subject: [PATCH 18/65] chore: update single click deployments (#1141) * chore: update single click deployments * dev: update environment variables --- .env.example | 6 ++++++ app.json | 26 +++++++++++++------------- deploy/heroku/Dockerfile | 4 ++++ docker-compose-hub.yml | 6 +++--- docker-compose.yml | 10 +++------- heroku.yml | 3 +++ 6 files changed, 32 insertions(+), 23 deletions(-) create mode 100644 deploy/heroku/Dockerfile create mode 100644 heroku.yml diff --git a/.env.example b/.env.example index 8e65a6900..7a7318455 100644 --- a/.env.example +++ b/.env.example @@ -45,4 +45,10 @@ GITHUB_CLIENT_SECRET="" # For fetching release notes # Settings related to Docker DOCKERIZED=1 +# Database Settings +PGUSER="plane" +PGPASSWORD="plane" +PGHOST="plane-db" +PGDATABASE="plane" + # Auto generated and Required that will be generated from setup.sh \ No newline at end of file diff --git a/app.json b/app.json index 7f6b27427..bc5789078 100644 --- a/app.json +++ b/app.json @@ -37,6 +37,14 @@ "description": "Email host to send emails from", "value": "" }, + "EMAIL_FROM": { + "description": "Email Sender", + "value": "" + }, + "EMAIL_PORT": { + "description": "The default Email PORT to use", + "value": "587" + }, "AWS_REGION": { "description": "AWS Region to use for S3", "value": "false" @@ -49,30 +57,22 @@ "description": "AWS Secret Access Key to use for S3", "value": "" }, - "SENTRY_DSN": { - "description": "", - "value": "" - }, "AWS_S3_BUCKET_NAME": { "description": "AWS Bucket Name to use for S3", "value": "" }, + "SENTRY_DSN": { + "description": "", + "value": "" + }, "WEB_URL": { - "description": "Web URL for Plane", + "description": "Web URL for Plane this will be used for redirections in the emails", "value": "" }, "GITHUB_CLIENT_SECRET": { "description": "Github Client Secret", "value": "" }, - "NEXT_PUBLIC_GITHUB_ID": { - "description": "Next Public Github ID", - "value": "" - }, - "NEXT_PUBLIC_GOOGLE_CLIENTID": { - "description": "Next Public Google Client ID", - "value": "" - }, "NEXT_PUBLIC_API_BASE_URL": { "description": "Next Public API Base URL", "value": "" diff --git a/deploy/heroku/Dockerfile b/deploy/heroku/Dockerfile new file mode 100644 index 000000000..edae32788 --- /dev/null +++ b/deploy/heroku/Dockerfile @@ -0,0 +1,4 @@ +# Deploy the Plane image +FROM makeplane/plane + +LABEL maintainer="engineering@plane.so" \ No newline at end of file diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml index 465f9d3f3..b3d8f1cd3 100644 --- a/docker-compose-hub.yml +++ b/docker-compose-hub.yml @@ -24,7 +24,7 @@ services: command: ./bin/takeoff environment: DJANGO_SETTINGS_MODULE: plane.settings.production - DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@plane-db:5432/plane + DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE} REDIS_URL: redis://plane-redis:6379/ EMAIL_HOST: ${EMAIL_HOST} EMAIL_HOST_USER: ${EMAIL_HOST_USER} @@ -53,7 +53,7 @@ services: command: ./bin/worker environment: DJANGO_SETTINGS_MODULE: plane.settings.production - DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@plane-db:5432/plane + DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE} REDIS_URL: redis://plane-redis:6379/ EMAIL_HOST: ${EMAIL_HOST} EMAIL_HOST_USER: ${EMAIL_HOST_USER} @@ -81,7 +81,7 @@ services: command: postgres -c 'max_connections=1000' environment: POSTGRES_USER: ${PGUSER} - POSTGRES_DB: plane + POSTGRES_DB: ${PGDATABASE} POSTGRES_PASSWORD: ${PGPASSWORD} volumes: - pgdata:/var/lib/postgresql/data diff --git a/docker-compose.yml b/docker-compose.yml index 44252ad6a..85002ec5e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,7 +30,7 @@ services: command: ./bin/takeoff environment: DJANGO_SETTINGS_MODULE: plane.settings.production - DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@plane-db:5432/plane + DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE} REDIS_URL: redis://redis:6379/ EMAIL_HOST: ${EMAIL_HOST} EMAIL_HOST_USER: ${EMAIL_HOST_USER} @@ -60,7 +60,7 @@ services: command: ./bin/worker environment: DJANGO_SETTINGS_MODULE: plane.settings.production - DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@plane-db:5432/plane + DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE} REDIS_URL: redis://redis:6379/ EMAIL_HOST: ${EMAIL_HOST} EMAIL_HOST_USER: ${EMAIL_HOST_USER} @@ -91,11 +91,9 @@ services: - pgdata:/var/lib/postgresql/data environment: POSTGRES_USER: ${PGUSER} - POSTGRES_DB: plane + POSTGRES_DB: ${PGDATABASE} POSTGRES_PASSWORD: ${PGPASSWORD} PGDATA: /var/lib/postgresql/data - ports: - - 5432:5432 plane-redis: container_name: redis @@ -103,8 +101,6 @@ services: restart: always volumes: - redisdata:/data - ports: - - 6379:6379 plane-minio: container_name: plane-minio diff --git a/heroku.yml b/heroku.yml new file mode 100644 index 000000000..e9d2ac033 --- /dev/null +++ b/heroku.yml @@ -0,0 +1,3 @@ +build: + docker: + web: deploy/heroku/Dockerfile \ No newline at end of file From 4ce0ac6ea1b7088ba4a1f84fcb0807f555aaeef1 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 26 May 2023 15:38:44 +0530 Subject: [PATCH 19/65] chore: pages endpoint updated (#1137) --- .../pages/pages-list/all-pages-list.tsx | 5 +- .../pages/pages-list/favorite-pages-list.tsx | 5 +- .../pages/pages-list/my-pages-list.tsx | 5 +- .../pages/pages-list/other-pages-list.tsx | 5 +- apps/app/services/pages.service.ts | 54 +++++++------------ 5 files changed, 35 insertions(+), 39 deletions(-) diff --git a/apps/app/components/pages/pages-list/all-pages-list.tsx b/apps/app/components/pages/pages-list/all-pages-list.tsx index ec364eba6..8bae35530 100644 --- a/apps/app/components/pages/pages-list/all-pages-list.tsx +++ b/apps/app/components/pages/pages-list/all-pages-list.tsx @@ -18,7 +18,10 @@ export const AllPagesList: React.FC = ({ viewType }) => { const { data: pages } = useSWR( workspaceSlug && projectId ? ALL_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => pagesService.getAllPages(workspaceSlug as string, projectId as string) + ? () => + pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, { + page_view: "all", + }) : null ); diff --git a/apps/app/components/pages/pages-list/favorite-pages-list.tsx b/apps/app/components/pages/pages-list/favorite-pages-list.tsx index bf0ab8133..b846fbb7e 100644 --- a/apps/app/components/pages/pages-list/favorite-pages-list.tsx +++ b/apps/app/components/pages/pages-list/favorite-pages-list.tsx @@ -18,7 +18,10 @@ export const FavoritePagesList: React.FC = ({ viewType }) => { const { data: pages } = useSWR( workspaceSlug && projectId ? FAVORITE_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => pagesService.getFavoritePages(workspaceSlug as string, projectId as string) + ? () => + pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, { + page_view: "favorite", + }) : null ); diff --git a/apps/app/components/pages/pages-list/my-pages-list.tsx b/apps/app/components/pages/pages-list/my-pages-list.tsx index 41704e52e..832d2a350 100644 --- a/apps/app/components/pages/pages-list/my-pages-list.tsx +++ b/apps/app/components/pages/pages-list/my-pages-list.tsx @@ -18,7 +18,10 @@ export const MyPagesList: React.FC = ({ viewType }) => { const { data: pages } = useSWR( workspaceSlug && projectId ? MY_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => pagesService.getMyPages(workspaceSlug as string, projectId as string) + ? () => + pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, { + page_view: "created_by_me", + }) : null ); diff --git a/apps/app/components/pages/pages-list/other-pages-list.tsx b/apps/app/components/pages/pages-list/other-pages-list.tsx index 7cf21a3e2..b8ab1bdee 100644 --- a/apps/app/components/pages/pages-list/other-pages-list.tsx +++ b/apps/app/components/pages/pages-list/other-pages-list.tsx @@ -18,7 +18,10 @@ export const OtherPagesList: React.FC = ({ viewType }) => { const { data: pages } = useSWR( workspaceSlug && projectId ? OTHER_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => pagesService.getOtherPages(workspaceSlug as string, projectId as string) + ? () => + pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, { + page_view: "created_by_other", + }) : null ); diff --git a/apps/app/services/pages.service.ts b/apps/app/services/pages.service.ts index 89fba2624..72776c29e 100644 --- a/apps/app/services/pages.service.ts +++ b/apps/app/services/pages.service.ts @@ -83,42 +83,26 @@ class PageServices extends APIService { }); } + async getPagesWithParams( + workspaceSlug: string, + projectId: string, + queries: any + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, { + params: queries, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async getRecentPages(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/recent-pages/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getAllPages(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getFavoritePages(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/favorite-pages/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getMyPages(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/my-pages/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getOtherPages(workspaceSlug: string, projectId: string): Promise { - return this.get( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/created-by-other-pages/` - ) + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, { + params: { + page_view: "recent", + }, + }) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; From 394f0bf555f0bc7f50bb0d510affea5fef31ff8c Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 26 May 2023 15:38:56 +0530 Subject: [PATCH 20/65] chore: cycles endpoint updated and code refactor (#1135) * chore: cycles endpoint updated and code refactor * chore: incomplete cycle endpoint updated * chore: code refactor --- .../cycles/active-cycle-details.tsx | 122 ++++----------- .../components/cycles/completed-cycles.tsx | 11 +- apps/app/components/cycles/cycles-view.tsx | 29 ++-- .../components/cycles/delete-cycle-modal.tsx | 84 +++-------- apps/app/components/cycles/modal.tsx | 21 +-- apps/app/components/cycles/select.tsx | 5 +- .../components/cycles/single-cycle-card.tsx | 140 ++++++------------ .../components/cycles/single-cycle-list.tsx | 140 ++++++------------ .../cycles/transfer-issues-modal.tsx | 5 +- .../issues/sidebar-select/cycle.tsx | 5 +- apps/app/constants/fetch-keys.ts | 7 +- .../projects/[projectId]/cycles/[cycleId].tsx | 5 +- .../projects/[projectId]/cycles/index.tsx | 39 +++-- apps/app/services/cycles.service.ts | 62 ++------ apps/app/types/cycles.d.ts | 13 -- 15 files changed, 226 insertions(+), 462 deletions(-) diff --git a/apps/app/components/cycles/active-cycle-details.tsx b/apps/app/components/cycles/active-cycle-details.tsx index 65c2e8b1b..7e8c8f7f7 100644 --- a/apps/app/components/cycles/active-cycle-details.tsx +++ b/apps/app/components/cycles/active-cycle-details.tsx @@ -40,20 +40,14 @@ import { } from "helpers/date-time.helper"; import { truncateText } from "helpers/string.helper"; // types -import { - CompletedCyclesResponse, - CurrentAndUpcomingCyclesResponse, - DraftCyclesResponse, - ICycle, - IIssue, -} from "types"; +import { ICycle, IIssue } from "types"; // fetch-keys import { CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_AND_UPCOMING_LIST, - CYCLE_DETAILS, + CYCLE_CURRENT_LIST, CYCLE_DRAFT_LIST, CYCLE_ISSUES, + CYCLE_LIST, } from "constants/fetch-keys"; type TSingleStatProps = { @@ -111,51 +105,18 @@ export const ActiveCycleDetails: React.FC = ({ cycle, isComple const handleAddToFavorites = () => { if (!workspaceSlug || !projectId || !cycle) return; - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - } + mutate( + CYCLE_CURRENT_LIST(projectId as string), + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? true : c.is_favorite, + })), + false + ); + mutate( - CYCLE_DETAILS(projectId as string), + CYCLE_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, @@ -180,51 +141,18 @@ export const ActiveCycleDetails: React.FC = ({ cycle, isComple const handleRemoveFromFavorites = () => { if (!workspaceSlug || !projectId || !cycle) return; - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - } + mutate( + CYCLE_CURRENT_LIST(projectId as string), + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? false : c.is_favorite, + })), + false + ); + mutate( - CYCLE_DETAILS(projectId as string), + CYCLE_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, diff --git a/apps/app/components/cycles/completed-cycles.tsx b/apps/app/components/cycles/completed-cycles.tsx index 36ef691f5..f561f32d4 100644 --- a/apps/app/components/cycles/completed-cycles.tsx +++ b/apps/app/components/cycles/completed-cycles.tsx @@ -38,7 +38,10 @@ export const CompletedCycles: React.FC = ({ const { data: completedCycles } = useSWR( workspaceSlug && projectId ? CYCLE_COMPLETE_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cyclesService.getCompletedCycles(workspaceSlug as string, projectId as string) + ? () => + cyclesService.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "completed", + }) : null ); @@ -64,7 +67,7 @@ export const CompletedCycles: React.FC = ({ data={selectedCycleForDelete} /> {completedCycles ? ( - completedCycles.completed_cycles.length > 0 ? ( + completedCycles.length > 0 ? (
= ({
{cycleView === "list" && (
- {completedCycles.completed_cycles.map((cycle) => ( + {completedCycles.map((cycle) => (
= ({ )} {cycleView === "board" && (
- {completedCycles.completed_cycles.map((cycle) => ( + {completedCycles.map((cycle) => ( >; setCreateUpdateCycleModal: React.Dispatch>; cyclesCompleteList: ICycle[] | undefined; - currentAndUpcomingCycles: CurrentAndUpcomingCyclesResponse | undefined; - draftCycles: DraftCyclesResponse | undefined; + currentCycle: ICycle[] | undefined; + upcomingCycles: ICycle[] | undefined; + draftCycles: ICycle[] | undefined; }; export const CyclesView: React.FC = ({ setSelectedCycle, setCreateUpdateCycleModal, cyclesCompleteList, - currentAndUpcomingCycles, + currentCycle, + upcomingCycles, draftCycles, }) => { const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycleTab", "All"); @@ -182,8 +179,8 @@ export const CyclesView: React.FC = ({ {cyclesView !== "gantt_chart" && ( - {currentAndUpcomingCycles?.current_cycle?.[0] ? ( - + {currentCycle?.[0] ? ( + ) : ( = ({ {cyclesView === "list" && ( = ({ )} {cyclesView === "board" && ( )} {cyclesView === "gantt_chart" && ( - + )} @@ -226,7 +223,7 @@ export const CyclesView: React.FC = ({ {cyclesView === "list" && ( = ({ )} {cyclesView === "board" && ( >; @@ -28,10 +23,10 @@ type TConfirmCycleDeletionProps = { // fetch-keys import { CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_AND_UPCOMING_LIST, - CYCLE_DETAILS, + CYCLE_CURRENT_LIST, CYCLE_DRAFT_LIST, CYCLE_LIST, + CYCLE_UPCOMING_LIST, } from "constants/fetch-keys"; import { getDateRangeStatus } from "helpers/date-time.helper"; @@ -60,63 +55,28 @@ export const DeleteCycleModal: React.FC = ({ await cycleService .deleteCycle(workspaceSlug as string, data.project, data.id) .then(() => { - switch (getDateRangeStatus(data.start_date, data.end_date)) { - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => { - if (!prevData) return; + const cycleType = getDateRangeStatus(data.start_date, data.end_date); + const fetchKey = + cycleType === "current" + ? CYCLE_CURRENT_LIST(projectId as string) + : cycleType === "upcoming" + ? CYCLE_UPCOMING_LIST(projectId as string) + : cycleType === "completed" + ? CYCLE_COMPLETE_LIST(projectId as string) + : CYCLE_DRAFT_LIST(projectId as string); - return { - completed_cycles: prevData.completed_cycles?.filter( - (cycle) => cycle.id !== data?.id - ), - }; - }, - false - ); - break; - case "current": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => { - if (!prevData) return; - return { - current_cycle: prevData.current_cycle?.filter((c) => c.id !== data?.id), - upcoming_cycle: prevData.upcoming_cycle, - }; - }, - false - ); - break; - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => { - if (!prevData) return; + mutate( + fetchKey, + (prevData) => { + if (!prevData) return; + + return prevData.filter((cycle) => cycle.id !== data?.id); + }, + false + ); - return { - current_cycle: prevData.current_cycle, - upcoming_cycle: prevData.upcoming_cycle?.filter((c) => c.id !== data?.id), - }; - }, - false - ); - break; - default: - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => { - if (!prevData) return; - return { - draft_cycles: prevData.draft_cycles?.filter((cycle) => cycle.id !== data?.id), - }; - }, - false - ); - } mutate( - CYCLE_DETAILS(projectId as string), + CYCLE_LIST(projectId as string), (prevData: any) => { if (!prevData) return; return prevData.filter((cycle: any) => cycle.id !== data?.id); diff --git a/apps/app/components/cycles/modal.tsx b/apps/app/components/cycles/modal.tsx index b3baacd65..8a0c6b8ad 100644 --- a/apps/app/components/cycles/modal.tsx +++ b/apps/app/components/cycles/modal.tsx @@ -19,10 +19,11 @@ import type { ICycle } from "types"; // fetch keys import { CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_AND_UPCOMING_LIST, - CYCLE_DETAILS, + CYCLE_CURRENT_LIST, CYCLE_DRAFT_LIST, CYCLE_INCOMPLETE_LIST, + CYCLE_LIST, + CYCLE_UPCOMING_LIST, } from "constants/fetch-keys"; type CycleModalProps = { @@ -50,16 +51,16 @@ export const CreateUpdateCycleModal: React.FC = ({ mutate(CYCLE_COMPLETE_LIST(projectId as string)); break; case "current": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(CYCLE_CURRENT_LIST(projectId as string)); break; case "upcoming": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(CYCLE_UPCOMING_LIST(projectId as string)); break; default: mutate(CYCLE_DRAFT_LIST(projectId as string)); } mutate(CYCLE_INCOMPLETE_LIST(projectId as string)); - mutate(CYCLE_DETAILS(projectId as string)); + mutate(CYCLE_LIST(projectId as string)); handleClose(); setToastAlert({ @@ -86,15 +87,15 @@ export const CreateUpdateCycleModal: React.FC = ({ mutate(CYCLE_COMPLETE_LIST(projectId as string)); break; case "current": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(CYCLE_CURRENT_LIST(projectId as string)); break; case "upcoming": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(CYCLE_UPCOMING_LIST(projectId as string)); break; default: mutate(CYCLE_DRAFT_LIST(projectId as string)); } - mutate(CYCLE_DETAILS(projectId as string)); + mutate(CYCLE_LIST(projectId as string)); if ( getDateRangeStatus(data?.start_date, data?.end_date) != getDateRangeStatus(res.start_date, res.end_date) @@ -104,10 +105,10 @@ export const CreateUpdateCycleModal: React.FC = ({ mutate(CYCLE_COMPLETE_LIST(projectId as string)); break; case "current": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(CYCLE_CURRENT_LIST(projectId as string)); break; case "upcoming": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(CYCLE_UPCOMING_LIST(projectId as string)); break; default: mutate(CYCLE_DRAFT_LIST(projectId as string)); diff --git a/apps/app/components/cycles/select.tsx b/apps/app/components/cycles/select.tsx index 971c60ddd..854749ec5 100644 --- a/apps/app/components/cycles/select.tsx +++ b/apps/app/components/cycles/select.tsx @@ -38,7 +38,10 @@ export const CycleSelect: React.FC = ({ const { data: cycles } = useSWR( workspaceSlug && projectId ? CYCLE_LIST(projectId) : null, workspaceSlug && projectId - ? () => cycleServices.getCycles(workspaceSlug as string, projectId) + ? () => + cycleServices.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "all", + }) : null ); diff --git a/apps/app/components/cycles/single-cycle-card.tsx b/apps/app/components/cycles/single-cycle-card.tsx index a5f6e8110..b6349b2ad 100644 --- a/apps/app/components/cycles/single-cycle-card.tsx +++ b/apps/app/components/cycles/single-cycle-card.tsx @@ -41,18 +41,14 @@ import { } from "helpers/date-time.helper"; import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // types -import { - CompletedCyclesResponse, - CurrentAndUpcomingCyclesResponse, - DraftCyclesResponse, - ICycle, -} from "types"; +import { ICycle } from "types"; // fetch-keys import { CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_AND_UPCOMING_LIST, - CYCLE_DETAILS, + CYCLE_CURRENT_LIST, CYCLE_DRAFT_LIST, + CYCLE_LIST, + CYCLE_UPCOMING_LIST, } from "constants/fetch-keys"; type TSingleStatProps = { @@ -108,51 +104,27 @@ export const SingleCycleCard: React.FC = ({ const handleAddToFavorites = () => { if (!workspaceSlug || !projectId || !cycle) return; - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - } + const fetchKey = + cycleStatus === "current" + ? CYCLE_CURRENT_LIST(projectId as string) + : cycleStatus === "upcoming" + ? CYCLE_UPCOMING_LIST(projectId as string) + : cycleStatus === "completed" + ? CYCLE_COMPLETE_LIST(projectId as string) + : CYCLE_DRAFT_LIST(projectId as string); + + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? true : c.is_favorite, + })), + false + ); + mutate( - CYCLE_DETAILS(projectId as string), + CYCLE_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, @@ -177,51 +149,27 @@ export const SingleCycleCard: React.FC = ({ const handleRemoveFromFavorites = () => { if (!workspaceSlug || !projectId || !cycle) return; - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - } + const fetchKey = + cycleStatus === "current" + ? CYCLE_CURRENT_LIST(projectId as string) + : cycleStatus === "upcoming" + ? CYCLE_UPCOMING_LIST(projectId as string) + : cycleStatus === "completed" + ? CYCLE_COMPLETE_LIST(projectId as string) + : CYCLE_DRAFT_LIST(projectId as string); + + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? false : c.is_favorite, + })), + false + ); + mutate( - CYCLE_DETAILS(projectId as string), + CYCLE_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, diff --git a/apps/app/components/cycles/single-cycle-list.tsx b/apps/app/components/cycles/single-cycle-list.tsx index a8976292e..aa2bf0975 100644 --- a/apps/app/components/cycles/single-cycle-list.tsx +++ b/apps/app/components/cycles/single-cycle-list.tsx @@ -32,18 +32,14 @@ import { } from "helpers/date-time.helper"; import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // types -import { - CompletedCyclesResponse, - CurrentAndUpcomingCyclesResponse, - DraftCyclesResponse, - ICycle, -} from "types"; +import { ICycle } from "types"; // fetch-keys import { CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_AND_UPCOMING_LIST, - CYCLE_DETAILS, + CYCLE_CURRENT_LIST, CYCLE_DRAFT_LIST, + CYCLE_LIST, + CYCLE_UPCOMING_LIST, } from "constants/fetch-keys"; import { type } from "os"; @@ -142,51 +138,27 @@ export const SingleCycleList: React.FC = ({ const handleAddToFavorites = () => { if (!workspaceSlug || !projectId || !cycle) return; - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - } + const fetchKey = + cycleStatus === "current" + ? CYCLE_CURRENT_LIST(projectId as string) + : cycleStatus === "upcoming" + ? CYCLE_UPCOMING_LIST(projectId as string) + : cycleStatus === "completed" + ? CYCLE_COMPLETE_LIST(projectId as string) + : CYCLE_DRAFT_LIST(projectId as string); + + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? true : c.is_favorite, + })), + false + ); + mutate( - CYCLE_DETAILS(projectId as string), + CYCLE_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, @@ -211,51 +183,27 @@ export const SingleCycleList: React.FC = ({ const handleRemoveFromFavorites = () => { if (!workspaceSlug || !projectId || !cycle) return; - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - } + const fetchKey = + cycleStatus === "current" + ? CYCLE_CURRENT_LIST(projectId as string) + : cycleStatus === "upcoming" + ? CYCLE_UPCOMING_LIST(projectId as string) + : cycleStatus === "completed" + ? CYCLE_COMPLETE_LIST(projectId as string) + : CYCLE_DRAFT_LIST(projectId as string); + + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? false : c.is_favorite, + })), + false + ); + mutate( - CYCLE_DETAILS(projectId as string), + CYCLE_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, diff --git a/apps/app/components/cycles/transfer-issues-modal.tsx b/apps/app/components/cycles/transfer-issues-modal.tsx index c857e154e..8d44c14d0 100644 --- a/apps/app/components/cycles/transfer-issues-modal.tsx +++ b/apps/app/components/cycles/transfer-issues-modal.tsx @@ -59,7 +59,10 @@ export const TransferIssuesModal: React.FC = ({ isOpen, handleClose }) => const { data: incompleteCycles } = useSWR( workspaceSlug && projectId ? CYCLE_INCOMPLETE_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cyclesService.getIncompleteCycles(workspaceSlug as string, projectId as string) + ? () => + cyclesService.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "incomplete", + }) : null ); diff --git a/apps/app/components/issues/sidebar-select/cycle.tsx b/apps/app/components/issues/sidebar-select/cycle.tsx index 3396dde27..d9e7772a3 100644 --- a/apps/app/components/issues/sidebar-select/cycle.tsx +++ b/apps/app/components/issues/sidebar-select/cycle.tsx @@ -35,7 +35,10 @@ export const SidebarCycleSelect: React.FC = ({ const { data: incompleteCycles } = useSWR( workspaceSlug && projectId ? CYCLE_INCOMPLETE_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cyclesService.getIncompleteCycles(workspaceSlug as string, projectId as string) + ? () => + cyclesService.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "incomplete", + }) : null ); diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 044dad38b..5b286bcc1 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -83,8 +83,11 @@ export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => { return `CYCLE_ISSUES_WITH_PARAMS_${cycleId.toUpperCase()}_${paramsKey.toUpperCase()}`; }; export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAILS_${cycleId.toUpperCase()}`; -export const CYCLE_CURRENT_AND_UPCOMING_LIST = (projectId: string) => - `CYCLE_CURRENT_AND_UPCOMING_LIST_${projectId.toUpperCase()}`; + +export const CYCLE_CURRENT_LIST = (projectId: string) => + `CYCLE_CURRENT_LIST${projectId.toUpperCase()}`; +export const CYCLE_UPCOMING_LIST = (projectId: string) => + `CYCLE_UPCOMING_LIST${projectId.toUpperCase()}`; export const CYCLE_DRAFT_LIST = (projectId: string) => `CYCLE_DRAFT_LIST_${projectId.toUpperCase()}`; export const CYCLE_COMPLETE_LIST = (projectId: string) => diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 4425014ac..3e5fafe6f 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -56,7 +56,10 @@ const SingleCycle: React.FC = () => { const { data: cycles } = useSWR( workspaceSlug && projectId ? CYCLE_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cycleServices.getCycles(workspaceSlug as string, projectId as string) + ? () => + cycleServices.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "all", + }) : null ); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 377d20fae..fd6df9832 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -21,10 +21,11 @@ import { SelectCycleType } from "types"; import type { NextPage } from "next"; // fetch-keys import { - CYCLE_CURRENT_AND_UPCOMING_LIST, CYCLE_DRAFT_LIST, PROJECT_DETAILS, - CYCLE_DETAILS, + CYCLE_UPCOMING_LIST, + CYCLE_CURRENT_LIST, + CYCLE_LIST, } from "constants/fetch-keys"; const ProjectCycles: NextPage = () => { @@ -44,21 +45,40 @@ const ProjectCycles: NextPage = () => { const { data: draftCycles } = useSWR( workspaceSlug && projectId ? CYCLE_DRAFT_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cycleService.getDraftCycles(workspaceSlug as string, projectId as string) + ? () => + cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "draft", + }) : null ); - const { data: currentAndUpcomingCycles } = useSWR( - workspaceSlug && projectId ? CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string) : null, + const { data: currentCycle } = useSWR( + workspaceSlug && projectId ? CYCLE_CURRENT_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cycleService.getCurrentAndUpcomingCycles(workspaceSlug as string, projectId as string) + ? () => + cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "current", + }) + : null + ); + + const { data: upcomingCycles } = useSWR( + workspaceSlug && projectId ? CYCLE_UPCOMING_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => + cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "upcoming", + }) : null ); const { data: cyclesCompleteList } = useSWR( - workspaceSlug && projectId ? CYCLE_DETAILS(projectId as string) : null, + workspaceSlug && projectId ? CYCLE_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cycleService.getCycles(workspaceSlug as string, projectId as string) + ? () => + cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "all", + }) : null ); @@ -121,7 +141,8 @@ const ProjectCycles: NextPage = () => { setSelectedCycle={setSelectedCycle} setCreateUpdateCycleModal={setCreateUpdateCycleModal} cyclesCompleteList={cyclesCompleteList} - currentAndUpcomingCycles={currentAndUpcomingCycles} + currentCycle={currentCycle} + upcomingCycles={upcomingCycles} draftCycles={draftCycles} />
diff --git a/apps/app/services/cycles.service.ts b/apps/app/services/cycles.service.ts index 0e942fed0..4d443a110 100644 --- a/apps/app/services/cycles.service.ts +++ b/apps/app/services/cycles.service.ts @@ -3,15 +3,7 @@ import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; // types -import type { - CycleIssueResponse, - CompletedCyclesResponse, - CurrentAndUpcomingCyclesResponse, - DraftCyclesResponse, - ICycle, - IIssue, - IIssueViewOptions, -} from "types"; +import type { ICycle, IIssue, IIssueViewOptions } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -34,16 +26,14 @@ class ProjectCycleServices extends APIService { }); } - async getCycles(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getIncompleteCycles(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/incomplete-cycles/`) + async getCyclesWithParams( + workspaceSlug: string, + projectId: string, + queries: any + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, { + params: queries, + }) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -159,40 +149,6 @@ class ProjectCycleServices extends APIService { }); } - async getCurrentAndUpcomingCycles( - workspaceSlug: string, - projectId: string - ): Promise { - return this.get( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/current-upcoming-cycles/` - ) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getDraftCycles(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/draft-cycles/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getCompletedCycles( - workspaceSlug: string, - projectId: string - ): Promise { - return this.get( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/completed-cycles/` - ) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - async addCycleToFavorites( workspaceSlug: string, projectId: string, diff --git a/apps/app/types/cycles.d.ts b/apps/app/types/cycles.d.ts index ee4b2cef6..a8da352ed 100644 --- a/apps/app/types/cycles.d.ts +++ b/apps/app/types/cycles.d.ts @@ -38,19 +38,6 @@ export interface ICycle { workspace_detail: IWorkspaceLite; } -export interface CurrentAndUpcomingCyclesResponse { - current_cycle: ICycle[]; - upcoming_cycle: ICycle[]; -} - -export interface DraftCyclesResponse { - draft_cycles: ICycle[]; -} - -export interface CompletedCyclesResponse { - completed_cycles: ICycle[]; -} - export interface CycleIssueResponse { id: string; issue_detail: IIssue; From 8252b1ccde8262723efbd0550b2f6a8acdac03e4 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Sun, 28 May 2023 09:56:15 +0530 Subject: [PATCH 21/65] chore: configuration for nginx port (#1144) --- .env.example | 3 +++ docker-compose-hub.yml | 2 +- docker-compose.yml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 7a7318455..a5ba26b3f 100644 --- a/.env.example +++ b/.env.example @@ -51,4 +51,7 @@ PGPASSWORD="plane" PGHOST="plane-db" PGDATABASE="plane" +# Nginx Configuration +NGINX_PORT=80 + # Auto generated and Required that will be generated from setup.sh \ No newline at end of file diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml index b3d8f1cd3..770409a43 100644 --- a/docker-compose-hub.yml +++ b/docker-compose-hub.yml @@ -124,7 +124,7 @@ services: context: ./nginx dockerfile: Dockerfile ports: - - 80:80 + - ${NGINX_PORT}:80 depends_on: - plane-web - plane-api diff --git a/docker-compose.yml b/docker-compose.yml index 85002ec5e..e1349e996 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -132,7 +132,7 @@ services: dockerfile: Dockerfile restart: always ports: - - 80:80 + - ${NGINX_PORT}:80 depends_on: - plane-web - plane-api From 1afb3ba4d207f3db3c0fac30aa8e14ed97c1625e Mon Sep 17 00:00:00 2001 From: Eli <12579154+erijohnt@users.noreply.github.com> Date: Sat, 27 May 2023 23:34:54 -0500 Subject: [PATCH 22/65] chore: speedup replace-env-vars.sh (#1146) --- replace-env-vars.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/replace-env-vars.sh b/replace-env-vars.sh index fe7acc698..6518fcc9e 100644 --- a/replace-env-vars.sh +++ b/replace-env-vars.sh @@ -11,7 +11,4 @@ fi # Only peform action if $FROM and $TO are different. echo "Replacing all statically built instances of $FROM with this string $TO ." -find apps/app/.next -type f | -while read file; do - sed -i "s|$FROM|$TO|g" "$file" -done \ No newline at end of file +grep -R -la "${FROM}" apps/app/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}" From d8c367c51e243bfdaee9abb7e7f55cd07b67385d Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Sun, 28 May 2023 14:47:25 +0530 Subject: [PATCH 23/65] docs: update self hosting setup script (#1145) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 879be2c56..31cf8a54d 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ chmod +x setup.sh - Run setup.sh ```bash -./setup.sh localhost +./setup.sh http://localhost ``` > If running in a cloud env replace localhost with public facing IP address of the VM From 030558c0264c48f52202bfa867270c59c1620118 Mon Sep 17 00:00:00 2001 From: Marks <132156916+markskram@users.noreply.github.com> Date: Sun, 28 May 2023 07:12:01 -0500 Subject: [PATCH 24/65] Update text during the onboarding process (#1149) * Update text during the onboarding process A few of the phrases were worded in ways that took a bit longer to process. These changes should make onboarding more comfortable. * Update text --- apps/app/constants/workspace.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/app/constants/workspace.ts b/apps/app/constants/workspace.ts index 24f886af9..8ab10c09d 100644 --- a/apps/app/constants/workspace.ts +++ b/apps/app/constants/workspace.ts @@ -43,27 +43,27 @@ export const ONBOARDING_CARDS = { step: "2/5", title: "Plan with Issues", description: - "The issue is the building block of the Plane. Most concepts in Plane are either associated with issues and their properties.", + "Issues are the building blocks of Plane. Most concepts in Plane are associated with issues or their properties.", }, cycle: { imgURL: Cycle, step: "3/5", title: "Move with Cycles", description: - "Cycles help you and your team to progress faster, similar to the sprints commonly used in agile development.", + "Cycles help you and your team progress faster, similar to sprints commonly used in agile development.", }, module: { imgURL: Module, step: "4/5", title: "Break into Modules ", description: - "Modules break your big think into Projects or Features, to help you organize better.", + "Modules break your big thoughts into Projects or Features, to help you organize better.", }, commandMenu: { imgURL: CommandMenu, step: "5 /5", title: "Command Menu", - description: "With Command Menu, you can create, update and navigate across the platform.", + description: "With Command Menu, you can create, update, and navigate across the platform.", }, }; From bc8430220bde484fef2025ac0874fe22a9a03e72 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Sun, 28 May 2023 18:11:36 +0530 Subject: [PATCH 25/65] chore: email settings for docker setup (#1148) --- docker-compose-hub.yml | 4 ++++ docker-compose.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml index 770409a43..97ef38260 100644 --- a/docker-compose-hub.yml +++ b/docker-compose-hub.yml @@ -29,6 +29,8 @@ services: EMAIL_HOST: ${EMAIL_HOST} EMAIL_HOST_USER: ${EMAIL_HOST_USER} EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + EMAIL_PORT: ${EMAIL_PORT} + EMAIL_FROM: ${EMAIL_FROM} AWS_REGION: ${AWS_REGION} AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} @@ -58,6 +60,8 @@ services: EMAIL_HOST: ${EMAIL_HOST} EMAIL_HOST_USER: ${EMAIL_HOST_USER} EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + EMAIL_PORT: ${EMAIL_PORT} + EMAIL_FROM: ${EMAIL_FROM} AWS_REGION: ${AWS_REGION} AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} diff --git a/docker-compose.yml b/docker-compose.yml index e1349e996..fe9e5f15d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,8 @@ services: EMAIL_HOST: ${EMAIL_HOST} EMAIL_HOST_USER: ${EMAIL_HOST_USER} EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + EMAIL_PORT: ${EMAIL_PORT} + EMAIL_FROM: ${EMAIL_FROM} AWS_REGION: ${AWS_REGION} AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} @@ -65,6 +67,8 @@ services: EMAIL_HOST: ${EMAIL_HOST} EMAIL_HOST_USER: ${EMAIL_HOST_USER} EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + EMAIL_PORT: ${EMAIL_PORT} + EMAIL_FROM: ${EMAIL_FROM} AWS_REGION: ${AWS_REGION} AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} From c9dee593ebcdd7e6b884e6c5a3b5fa5852660dce Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Sun, 28 May 2023 18:11:59 +0530 Subject: [PATCH 26/65] chore: total members in user workspace invites (#1143) --- apiserver/plane/api/serializers/workspace.py | 1 + apiserver/plane/api/views/workspace.py | 1 + 2 files changed, 2 insertions(+) diff --git a/apiserver/plane/api/serializers/workspace.py b/apiserver/plane/api/serializers/workspace.py index 4f4d13f76..4562726ae 100644 --- a/apiserver/plane/api/serializers/workspace.py +++ b/apiserver/plane/api/serializers/workspace.py @@ -44,6 +44,7 @@ class WorkSpaceMemberSerializer(BaseSerializer): class WorkSpaceMemberInviteSerializer(BaseSerializer): workspace = WorkSpaceSerializer(read_only=True) + total_members = serializers.IntegerField(read_only=True) class Meta: model = WorkspaceMemberInvite diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 47dc1cd47..64154500f 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -375,6 +375,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet): .get_queryset() .filter(email=self.request.user.email) .select_related("workspace", "workspace__owner") + .annotate(total_members=Count("workspace__workspace_member")) ) def create(self, request): From 053ebc031d0da02f74da1104e00f6a31bd5f413a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 29 May 2023 02:14:40 +0530 Subject: [PATCH 27/65] fix: cycles layout --- .../cycles/active-cycle-details.tsx | 8 +- .../components/cycles/all-cycles-board.tsx | 82 ---- .../app/components/cycles/all-cycles-list.tsx | 86 ---- .../components/cycles/completed-cycles.tsx | 129 ------ .../cycles/cycles-list/all-cycles-list.tsx | 31 ++ .../cycles-list/completed-cycles-list.tsx | 31 ++ .../cycles/cycles-list/draft-cycles-list.tsx | 31 ++ .../components/cycles/cycles-list/index.ts | 4 + .../cycles-list/upcoming-cycles-list.tsx | 31 ++ apps/app/components/cycles/cycles-view.tsx | 429 +++++++++--------- .../components/cycles/delete-cycle-modal.tsx | 2 +- apps/app/components/cycles/empty-cycle.tsx | 78 ---- apps/app/components/cycles/index.ts | 11 +- apps/app/components/cycles/modal.tsx | 2 +- .../components/cycles/single-cycle-card.tsx | 115 +---- .../components/cycles/single-cycle-list.tsx | 110 +---- .../projects/[projectId]/cycles/index.tsx | 234 +++++++--- 17 files changed, 516 insertions(+), 898 deletions(-) delete mode 100644 apps/app/components/cycles/all-cycles-board.tsx delete mode 100644 apps/app/components/cycles/all-cycles-list.tsx delete mode 100644 apps/app/components/cycles/completed-cycles.tsx create mode 100644 apps/app/components/cycles/cycles-list/all-cycles-list.tsx create mode 100644 apps/app/components/cycles/cycles-list/completed-cycles-list.tsx create mode 100644 apps/app/components/cycles/cycles-list/draft-cycles-list.tsx create mode 100644 apps/app/components/cycles/cycles-list/index.ts create mode 100644 apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx delete mode 100644 apps/app/components/cycles/empty-cycle.tsx diff --git a/apps/app/components/cycles/active-cycle-details.tsx b/apps/app/components/cycles/active-cycle-details.tsx index 7e8c8f7f7..e0016dd0b 100644 --- a/apps/app/components/cycles/active-cycle-details.tsx +++ b/apps/app/components/cycles/active-cycle-details.tsx @@ -42,13 +42,7 @@ import { truncateText } from "helpers/string.helper"; // types import { ICycle, IIssue } from "types"; // fetch-keys -import { - CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_LIST, - CYCLE_DRAFT_LIST, - CYCLE_ISSUES, - CYCLE_LIST, -} from "constants/fetch-keys"; +import { CYCLE_CURRENT_LIST, CYCLE_ISSUES, CYCLE_LIST } from "constants/fetch-keys"; type TSingleStatProps = { cycle: ICycle; diff --git a/apps/app/components/cycles/all-cycles-board.tsx b/apps/app/components/cycles/all-cycles-board.tsx deleted file mode 100644 index 259251fe6..000000000 --- a/apps/app/components/cycles/all-cycles-board.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useState } from "react"; - -// components -import { DeleteCycleModal, SingleCycleCard } from "components/cycles"; -import { EmptyState, Loader } from "components/ui"; -// image -import emptyCycle from "public/empty-state/empty-cycle.svg"; -// icon -import { XMarkIcon } from "@heroicons/react/24/outline"; -// types -import { ICycle, SelectCycleType } from "types"; - -type TCycleStatsViewProps = { - cycles: ICycle[] | undefined; - setCreateUpdateCycleModal: React.Dispatch>; - setSelectedCycle: React.Dispatch>; - type: "current" | "upcoming" | "draft"; -}; - -export const AllCyclesBoard: React.FC = ({ - cycles, - setCreateUpdateCycleModal, - setSelectedCycle, - type, -}) => { - const [cycleDeleteModal, setCycleDeleteModal] = useState(false); - const [selectedCycleForDelete, setSelectedCycleForDelete] = useState(); - - const handleDeleteCycle = (cycle: ICycle) => { - setSelectedCycleForDelete({ ...cycle, actionType: "delete" }); - setCycleDeleteModal(true); - }; - - const handleEditCycle = (cycle: ICycle) => { - setSelectedCycle({ ...cycle, actionType: "edit" }); - setCreateUpdateCycleModal(true); - }; - - return ( - <> - - {cycles ? ( - cycles.length > 0 ? ( -
- {cycles.map((cycle) => ( - handleDeleteCycle(cycle)} - handleEditCycle={() => handleEditCycle(cycle)} - /> - ))} -
- ) : type === "current" ? ( -
-

No cycle is present.

-
- ) : ( - - ) - ) : ( - - - - )} - - ); -}; diff --git a/apps/app/components/cycles/all-cycles-list.tsx b/apps/app/components/cycles/all-cycles-list.tsx deleted file mode 100644 index 502e13e8e..000000000 --- a/apps/app/components/cycles/all-cycles-list.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useState } from "react"; - -// components -import { DeleteCycleModal, SingleCycleList } from "components/cycles"; -import { EmptyState, Loader } from "components/ui"; -// image -import emptyCycle from "public/empty-state/empty-cycle.svg"; -// icon -import { XMarkIcon } from "@heroicons/react/24/outline"; -// types -import { ICycle, SelectCycleType } from "types"; - -type TCycleStatsViewProps = { - cycles: ICycle[] | undefined; - setCreateUpdateCycleModal: React.Dispatch>; - setSelectedCycle: React.Dispatch>; - type: "current" | "upcoming" | "draft"; -}; - -export const AllCyclesList: React.FC = ({ - cycles, - setCreateUpdateCycleModal, - setSelectedCycle, - type, -}) => { - const [cycleDeleteModal, setCycleDeleteModal] = useState(false); - const [selectedCycleForDelete, setSelectedCycleForDelete] = useState(); - - const handleDeleteCycle = (cycle: ICycle) => { - setSelectedCycleForDelete({ ...cycle, actionType: "delete" }); - setCycleDeleteModal(true); - }; - - const handleEditCycle = (cycle: ICycle) => { - setSelectedCycle({ ...cycle, actionType: "edit" }); - setCreateUpdateCycleModal(true); - }; - - return ( - <> - - {cycles ? ( - cycles.length > 0 ? ( -
- {cycles.map((cycle) => ( -
-
- handleDeleteCycle(cycle)} - handleEditCycle={() => handleEditCycle(cycle)} - /> -
-
- ))} -
- ) : type === "current" ? ( -
-

No cycle is present.

-
- ) : ( - - ) - ) : ( - - - - )} - - ); -}; diff --git a/apps/app/components/cycles/completed-cycles.tsx b/apps/app/components/cycles/completed-cycles.tsx deleted file mode 100644 index f561f32d4..000000000 --- a/apps/app/components/cycles/completed-cycles.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { useState } from "react"; - -import { useRouter } from "next/router"; - -import useSWR from "swr"; - -// services -import cyclesService from "services/cycles.service"; -// components -import { DeleteCycleModal, SingleCycleCard, SingleCycleList } from "components/cycles"; -// icons -import { ExclamationIcon } from "components/icons"; -// types -import { ICycle, SelectCycleType } from "types"; -// fetch-keys -import { CYCLE_COMPLETE_LIST } from "constants/fetch-keys"; -import { EmptyState, Loader } from "components/ui"; -// image -import emptyCycle from "public/empty-state/empty-cycle.svg"; - -export interface CompletedCyclesListProps { - cycleView: string; - setCreateUpdateCycleModal: React.Dispatch>; - setSelectedCycle: React.Dispatch>; -} - -export const CompletedCycles: React.FC = ({ - cycleView, - setCreateUpdateCycleModal, - setSelectedCycle, -}) => { - const [cycleDeleteModal, setCycleDeleteModal] = useState(false); - const [selectedCycleForDelete, setSelectedCycleForDelete] = useState(); - - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { data: completedCycles } = useSWR( - workspaceSlug && projectId ? CYCLE_COMPLETE_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => - cyclesService.getCyclesWithParams(workspaceSlug as string, projectId as string, { - cycle_view: "completed", - }) - : null - ); - - const handleDeleteCycle = (cycle: ICycle) => { - setSelectedCycleForDelete({ ...cycle, actionType: "delete" }); - setCycleDeleteModal(true); - }; - - const handleEditCycle = (cycle: ICycle) => { - setSelectedCycle({ ...cycle, actionType: "edit" }); - setCreateUpdateCycleModal(true); - }; - - return ( - <> - - {completedCycles ? ( - completedCycles.length > 0 ? ( -
-
- - Completed cycles are not editable. -
- {cycleView === "list" && ( -
- {completedCycles.map((cycle) => ( -
-
- handleDeleteCycle(cycle)} - handleEditCycle={() => handleEditCycle(cycle)} - isCompleted - /> -
-
- ))} -
- )} - {cycleView === "board" && ( -
- {completedCycles.map((cycle) => ( - handleDeleteCycle(cycle)} - handleEditCycle={() => handleEditCycle(cycle)} - isCompleted - /> - ))} -
- )} -
- ) : ( - - ) - ) : ( - - - - - - )} - - ); -}; diff --git a/apps/app/components/cycles/cycles-list/all-cycles-list.tsx b/apps/app/components/cycles/cycles-list/all-cycles-list.tsx new file mode 100644 index 000000000..1e696c7b5 --- /dev/null +++ b/apps/app/components/cycles/cycles-list/all-cycles-list.tsx @@ -0,0 +1,31 @@ +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import cyclesService from "services/cycles.service"; +// components +import { CyclesView } from "components/cycles"; +// fetch-keys +import { CYCLE_LIST } from "constants/fetch-keys"; + +type Props = { + viewType: string | null; +}; + +export const AllCyclesList: React.FC = ({ viewType }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: allCyclesList } = useSWR( + workspaceSlug && projectId ? CYCLE_LIST(projectId.toString()) : null, + workspaceSlug && projectId + ? () => + cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), { + cycle_view: "all", + }) + : null + ); + + return ; +}; diff --git a/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx b/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx new file mode 100644 index 000000000..a2e48c619 --- /dev/null +++ b/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx @@ -0,0 +1,31 @@ +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import cyclesService from "services/cycles.service"; +// components +import { CyclesView } from "components/cycles"; +// fetch-keys +import { CYCLE_COMPLETE_LIST } from "constants/fetch-keys"; + +type Props = { + viewType: string | null; +}; + +export const CompletedCyclesList: React.FC = ({ viewType }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: completedCyclesList } = useSWR( + workspaceSlug && projectId ? CYCLE_COMPLETE_LIST(projectId.toString()) : null, + workspaceSlug && projectId + ? () => + cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), { + cycle_view: "completed", + }) + : null + ); + + return ; +}; diff --git a/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx b/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx new file mode 100644 index 000000000..746c4b20a --- /dev/null +++ b/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx @@ -0,0 +1,31 @@ +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import cyclesService from "services/cycles.service"; +// components +import { CyclesView } from "components/cycles"; +// fetch-keys +import { CYCLE_DRAFT_LIST } from "constants/fetch-keys"; + +type Props = { + viewType: string | null; +}; + +export const DraftCyclesList: React.FC = ({ viewType }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: draftCyclesList } = useSWR( + workspaceSlug && projectId ? CYCLE_DRAFT_LIST(projectId.toString()) : null, + workspaceSlug && projectId + ? () => + cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), { + cycle_view: "draft", + }) + : null + ); + + return ; +}; diff --git a/apps/app/components/cycles/cycles-list/index.ts b/apps/app/components/cycles/cycles-list/index.ts new file mode 100644 index 000000000..b4b4fdfb5 --- /dev/null +++ b/apps/app/components/cycles/cycles-list/index.ts @@ -0,0 +1,4 @@ +export * from "./all-cycles-list"; +export * from "./completed-cycles-list"; +export * from "./draft-cycles-list"; +export * from "./upcoming-cycles-list"; diff --git a/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx b/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx new file mode 100644 index 000000000..00d06705e --- /dev/null +++ b/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx @@ -0,0 +1,31 @@ +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import cyclesService from "services/cycles.service"; +// components +import { CyclesView } from "components/cycles"; +// fetch-keys +import { CYCLE_UPCOMING_LIST } from "constants/fetch-keys"; + +type Props = { + viewType: string | null; +}; + +export const UpcomingCyclesList: React.FC = ({ viewType }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: upcomingCyclesList } = useSWR( + workspaceSlug && projectId ? CYCLE_UPCOMING_LIST(projectId.toString()) : null, + workspaceSlug && projectId + ? () => + cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), { + cycle_view: "upcoming", + }) + : null + ); + + return ; +}; diff --git a/apps/app/components/cycles/cycles-view.tsx b/apps/app/components/cycles/cycles-view.tsx index 0f1c917ce..5a3305ddb 100644 --- a/apps/app/components/cycles/cycles-view.tsx +++ b/apps/app/components/cycles/cycles-view.tsx @@ -1,246 +1,229 @@ -import React, { useEffect } from "react"; -import dynamic from "next/dynamic"; -// headless ui -import { Tab } from "@headlessui/react"; +import React, { useState } from "react"; + +import { useRouter } from "next/router"; + +import { mutate } from "swr"; + +// services +import cyclesService from "services/cycles.service"; // hooks -import useLocalStorage from "hooks/use-local-storage"; +import useToast from "hooks/use-toast"; // components import { - ActiveCycleDetails, - CompletedCyclesListProps, - AllCyclesBoard, - AllCyclesList, + CreateUpdateCycleModal, CyclesListGanttChartView, + DeleteCycleModal, + SingleCycleCard, + SingleCycleList, } from "components/cycles"; // ui import { EmptyState, Loader } from "components/ui"; -// icons -import { ChartBarIcon, ListBulletIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; +// images import emptyCycle from "public/empty-state/empty-cycle.svg"; +// helpers +import { getDateRangeStatus } from "helpers/date-time.helper"; // types -import { SelectCycleType, ICycle } from "types"; +import { ICycle } from "types"; +// fetch-keys +import { + CYCLE_COMPLETE_LIST, + CYCLE_CURRENT_LIST, + CYCLE_DRAFT_LIST, + CYCLE_LIST, + CYCLE_UPCOMING_LIST, +} from "constants/fetch-keys"; type Props = { - setSelectedCycle: React.Dispatch>; - setCreateUpdateCycleModal: React.Dispatch>; - cyclesCompleteList: ICycle[] | undefined; - currentCycle: ICycle[] | undefined; - upcomingCycles: ICycle[] | undefined; - draftCycles: ICycle[] | undefined; + cycles: ICycle[] | undefined; + viewType: string | null; }; -export const CyclesView: React.FC = ({ - setSelectedCycle, - setCreateUpdateCycleModal, - cyclesCompleteList, - currentCycle, - upcomingCycles, - draftCycles, -}) => { - const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycleTab", "All"); - const { storedValue: cyclesView, setValue: setCyclesView } = useLocalStorage("cycleView", "list"); +export const CyclesView: React.FC = ({ cycles, viewType }) => { + const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false); + const [selectedCycleToUpdate, setSelectedCycleToUpdate] = useState(null); - const currentTabValue = (tab: string | null) => { - switch (tab) { - case "All": - return 0; - case "Active": - return 1; - case "Upcoming": - return 2; - case "Completed": - return 3; - case "Drafts": - return 4; - default: - return 0; - } + const [deleteCycleModal, setDeleteCycleModal] = useState(false); + const [selectedCycleToDelete, setSelectedCycleToDelete] = useState(null); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { setToastAlert } = useToast(); + + const handleEditCycle = (cycle: ICycle) => { + setSelectedCycleToUpdate(cycle); + setCreateUpdateCycleModal(true); }; - const CompletedCycles = dynamic( - () => import("components/cycles").then((a) => a.CompletedCycles), - { - ssr: false, - loading: () => ( - - - - ), - } - ); + const handleDeleteCycle = (cycle: ICycle) => { + setSelectedCycleToDelete(cycle); + setDeleteCycleModal(true); + }; + + const handleAddToFavorites = (cycle: ICycle) => { + if (!workspaceSlug || !projectId) return; + + const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); + + const fetchKey = + cycleStatus === "current" + ? CYCLE_CURRENT_LIST(projectId as string) + : cycleStatus === "upcoming" + ? CYCLE_UPCOMING_LIST(projectId as string) + : cycleStatus === "completed" + ? CYCLE_COMPLETE_LIST(projectId as string) + : CYCLE_DRAFT_LIST(projectId as string); + + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? true : c.is_favorite, + })), + false + ); + + mutate( + CYCLE_LIST(projectId as string), + (prevData: any) => + (prevData ?? []).map((c: any) => ({ + ...c, + is_favorite: c.id === cycle.id ? true : c.is_favorite, + })), + false + ); + + cyclesService + .addCycleToFavorites(workspaceSlug as string, projectId as string, { + cycle: cycle.id, + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the cycle to favorites. Please try again.", + }); + }); + }; + + const handleRemoveFromFavorites = (cycle: ICycle) => { + if (!workspaceSlug || !projectId) return; + + const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); + + const fetchKey = + cycleStatus === "current" + ? CYCLE_CURRENT_LIST(projectId as string) + : cycleStatus === "upcoming" + ? CYCLE_UPCOMING_LIST(projectId as string) + : cycleStatus === "completed" + ? CYCLE_COMPLETE_LIST(projectId as string) + : CYCLE_DRAFT_LIST(projectId as string); + + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? false : c.is_favorite, + })), + false + ); + + mutate( + CYCLE_LIST(projectId as string), + (prevData: any) => + (prevData ?? []).map((c: any) => ({ + ...c, + is_favorite: c.id === cycle.id ? false : c.is_favorite, + })), + false + ); + + cyclesService + .removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't remove the cycle from favorites. Please try again.", + }); + }); + }; return ( <> -
-

Cycles

-
- - - -
-
- { - switch (i) { - case 0: - return setCycleTab("All"); - case 1: - return setCycleTab("Active"); - case 2: - return setCycleTab("Upcoming"); - case 3: - return setCycleTab("Completed"); - case 4: - return setCycleTab("Drafts"); - default: - return setCycleTab("All"); - } - }} - > -
- - {["All", "Active", "Upcoming", "Completed", "Drafts"].map((tab, index) => { - if ( - cyclesView === "gantt_chart" && - (tab === "Active" || tab === "Drafts" || tab === "Completed") - ) - return null; - - return ( - - `rounded-3xl border px-6 py-1 outline-none ${ - selected - ? "border-brand-accent bg-brand-accent text-white font-medium" - : "border-brand-base bg-brand-base hover:bg-brand-surface-2" - }` - } - > - {tab} - - ); - })} - -
- - - {cyclesView === "list" && ( - - )} - {cyclesView === "board" && ( - - )} - {cyclesView === "gantt_chart" && ( - - )} - - {cyclesView !== "gantt_chart" && ( - - {currentCycle?.[0] ? ( - - ) : ( - setCreateUpdateCycleModal(false)} + data={selectedCycleToUpdate} + /> + + {cycles ? ( + cycles.length > 0 ? ( + viewType === "list" ? ( +
+ {cycles.map((cycle) => ( +
+
+ handleDeleteCycle(cycle)} + handleEditCycle={() => handleEditCycle(cycle)} + handleAddToFavorites={() => handleAddToFavorites(cycle)} + handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)} + /> +
+
+ ))} +
+ ) : viewType === "board" ? ( +
+ {cycles.map((cycle) => ( + handleDeleteCycle(cycle)} + handleEditCycle={() => handleEditCycle(cycle)} + handleAddToFavorites={() => handleAddToFavorites(cycle)} + handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)} /> - )} - - )} - - {cyclesView === "list" && ( - - )} - {cyclesView === "board" && ( - - )} - {cyclesView === "gantt_chart" && ( - - )} - - - - - {cyclesView !== "gantt_chart" && ( - - {cyclesView === "list" && ( - - )} - {cyclesView === "board" && ( - - )} - - )} - - + ))} +
+ ) : ( + + ) + ) : ( + + ) + ) : viewType === "list" ? ( + + + + + + ) : viewType === "board" ? ( + + + + + + ) : ( + + + + )} ); }; diff --git a/apps/app/components/cycles/delete-cycle-modal.tsx b/apps/app/components/cycles/delete-cycle-modal.tsx index 894e1b4eb..dfae65cae 100644 --- a/apps/app/components/cycles/delete-cycle-modal.tsx +++ b/apps/app/components/cycles/delete-cycle-modal.tsx @@ -18,7 +18,7 @@ import type { ICycle } from "types"; type TConfirmCycleDeletionProps = { isOpen: boolean; setIsOpen: React.Dispatch>; - data?: ICycle; + data?: ICycle | null; }; // fetch-keys import { diff --git a/apps/app/components/cycles/empty-cycle.tsx b/apps/app/components/cycles/empty-cycle.tsx deleted file mode 100644 index af2f12a11..000000000 --- a/apps/app/components/cycles/empty-cycle.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from "react"; -import { LinearProgressIndicator } from "components/ui"; - -export const EmptyCycle = () => { - const emptyCycleData = [ - { - id: 1, - name: "backlog", - value: 20, - color: "#DEE2E6", - }, - { - id: 2, - name: "unstarted", - value: 14, - color: "#26B5CE", - }, - { - id: 3, - name: "started", - value: 27, - color: "#F7AE59", - }, - { - id: 4, - name: "cancelled", - value: 15, - color: "#D687FF", - }, - { - id: 5, - name: "completed", - value: 14, - color: "#09A953", - }, - ]; - return ( -
-
-
-
- Cycle Name -
- - -
-
- -
- -
-
- -
-
- Cycle Name -
- - -
-
- -
- -
-
-
- -
-

Create New Cycle

-

- Sprint more effectively with Cycles by confining your project
to a fixed amount of - time. Create new cycle now. -

-
-
- ); -}; diff --git a/apps/app/components/cycles/index.ts b/apps/app/components/cycles/index.ts index 3151529c5..40355d574 100644 --- a/apps/app/components/cycles/index.ts +++ b/apps/app/components/cycles/index.ts @@ -1,18 +1,15 @@ +export * from "./cycles-list"; export * from "./active-cycle-details"; -export * from "./cycles-view"; -export * from "./completed-cycles"; +export * from "./active-cycle-stats"; export * from "./cycles-list-gantt-chart"; -export * from "./all-cycles-board"; -export * from "./all-cycles-list"; +export * from "./cycles-view"; export * from "./delete-cycle-modal"; export * from "./form"; export * from "./gantt-chart"; export * from "./modal"; export * from "./select"; export * from "./sidebar"; -export * from "./single-cycle-list"; export * from "./single-cycle-card"; -export * from "./empty-cycle"; +export * from "./single-cycle-list"; export * from "./transfer-issues-modal"; export * from "./transfer-issues"; -export * from "./active-cycle-stats"; diff --git a/apps/app/components/cycles/modal.tsx b/apps/app/components/cycles/modal.tsx index 8a0c6b8ad..715b40e2c 100644 --- a/apps/app/components/cycles/modal.tsx +++ b/apps/app/components/cycles/modal.tsx @@ -29,7 +29,7 @@ import { type CycleModalProps = { isOpen: boolean; handleClose: () => void; - data?: ICycle; + data?: ICycle | null; }; export const CreateUpdateCycleModal: React.FC = ({ diff --git a/apps/app/components/cycles/single-cycle-card.tsx b/apps/app/components/cycles/single-cycle-card.tsx index b6349b2ad..3cc2eaa38 100644 --- a/apps/app/components/cycles/single-cycle-card.tsx +++ b/apps/app/components/cycles/single-cycle-card.tsx @@ -4,20 +4,17 @@ import Link from "next/link"; import Image from "next/image"; import { useRouter } from "next/router"; -import { mutate } from "swr"; - -// services -import cyclesService from "services/cycles.service"; +// headless ui +import { Disclosure, Transition } from "@headlessui/react"; // hooks import useToast from "hooks/use-toast"; +// components +import { SingleProgressStats } from "components/core"; // ui import { CustomMenu, LinearProgressIndicator, Tooltip } from "components/ui"; -import { Disclosure, Transition } from "@headlessui/react"; -import { AssigneesList, Avatar } from "components/ui/avatar"; -import { SingleProgressStats } from "components/core"; - +import { AssigneesList } from "components/ui/avatar"; // icons -import { CalendarDaysIcon, ExclamationCircleIcon } from "@heroicons/react/20/solid"; +import { CalendarDaysIcon } from "@heroicons/react/20/solid"; import { TargetIcon, ContrastIcon, @@ -42,19 +39,13 @@ import { import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // types import { ICycle } from "types"; -// fetch-keys -import { - CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_LIST, - CYCLE_DRAFT_LIST, - CYCLE_LIST, - CYCLE_UPCOMING_LIST, -} from "constants/fetch-keys"; type TSingleStatProps = { cycle: ICycle; handleEditCycle: () => void; handleDeleteCycle: () => void; + handleAddToFavorites: () => void; + handleRemoveFromFavorites: () => void; isCompleted?: boolean; }; @@ -90,6 +81,8 @@ export const SingleCycleCard: React.FC = ({ cycle, handleEditCycle, handleDeleteCycle, + handleAddToFavorites, + handleRemoveFromFavorites, isCompleted = false, }) => { const router = useRouter(); @@ -101,94 +94,6 @@ export const SingleCycleCard: React.FC = ({ const endDate = new Date(cycle.end_date ?? ""); const startDate = new Date(cycle.start_date ?? ""); - const handleAddToFavorites = () => { - if (!workspaceSlug || !projectId || !cycle) return; - - const fetchKey = - cycleStatus === "current" - ? CYCLE_CURRENT_LIST(projectId as string) - : cycleStatus === "upcoming" - ? CYCLE_UPCOMING_LIST(projectId as string) - : cycleStatus === "completed" - ? CYCLE_COMPLETE_LIST(projectId as string) - : CYCLE_DRAFT_LIST(projectId as string); - - mutate( - fetchKey, - (prevData) => - (prevData ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - false - ); - - mutate( - CYCLE_LIST(projectId as string), - (prevData: any) => - (prevData ?? []).map((c: any) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - false - ); - - cyclesService - .addCycleToFavorites(workspaceSlug as string, projectId as string, { - cycle: cycle.id, - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); - }); - }; - - const handleRemoveFromFavorites = () => { - if (!workspaceSlug || !projectId || !cycle) return; - - const fetchKey = - cycleStatus === "current" - ? CYCLE_CURRENT_LIST(projectId as string) - : cycleStatus === "upcoming" - ? CYCLE_UPCOMING_LIST(projectId as string) - : cycleStatus === "completed" - ? CYCLE_COMPLETE_LIST(projectId as string) - : CYCLE_DRAFT_LIST(projectId as string); - - mutate( - fetchKey, - (prevData) => - (prevData ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - false - ); - - mutate( - CYCLE_LIST(projectId as string), - (prevData: any) => - (prevData ?? []).map((c: any) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - false - ); - - cyclesService - .removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the cycle from favorites. Please try again.", - }); - }); - }; - const handleCopyText = () => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; diff --git a/apps/app/components/cycles/single-cycle-list.tsx b/apps/app/components/cycles/single-cycle-list.tsx index aa2bf0975..cadabba7f 100644 --- a/apps/app/components/cycles/single-cycle-list.tsx +++ b/apps/app/components/cycles/single-cycle-list.tsx @@ -4,17 +4,12 @@ import Link from "next/link"; import Image from "next/image"; import { useRouter } from "next/router"; -import { mutate } from "swr"; - -// services -import cyclesService from "services/cycles.service"; // hooks import useToast from "hooks/use-toast"; // ui import { CustomMenu, LinearProgressIndicator, Tooltip } from "components/ui"; - // icons -import { CalendarDaysIcon, ExclamationCircleIcon } from "@heroicons/react/20/solid"; +import { CalendarDaysIcon } from "@heroicons/react/20/solid"; import { TargetIcon, ContrastIcon, @@ -33,20 +28,13 @@ import { import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // types import { ICycle } from "types"; -// fetch-keys -import { - CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_LIST, - CYCLE_DRAFT_LIST, - CYCLE_LIST, - CYCLE_UPCOMING_LIST, -} from "constants/fetch-keys"; -import { type } from "os"; type TSingleStatProps = { cycle: ICycle; handleEditCycle: () => void; handleDeleteCycle: () => void; + handleAddToFavorites: () => void; + handleRemoveFromFavorites: () => void; isCompleted?: boolean; }; @@ -124,6 +112,8 @@ export const SingleCycleList: React.FC = ({ cycle, handleEditCycle, handleDeleteCycle, + handleAddToFavorites, + handleRemoveFromFavorites, isCompleted = false, }) => { const router = useRouter(); @@ -135,94 +125,6 @@ export const SingleCycleList: React.FC = ({ const endDate = new Date(cycle.end_date ?? ""); const startDate = new Date(cycle.start_date ?? ""); - const handleAddToFavorites = () => { - if (!workspaceSlug || !projectId || !cycle) return; - - const fetchKey = - cycleStatus === "current" - ? CYCLE_CURRENT_LIST(projectId as string) - : cycleStatus === "upcoming" - ? CYCLE_UPCOMING_LIST(projectId as string) - : cycleStatus === "completed" - ? CYCLE_COMPLETE_LIST(projectId as string) - : CYCLE_DRAFT_LIST(projectId as string); - - mutate( - fetchKey, - (prevData) => - (prevData ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - false - ); - - mutate( - CYCLE_LIST(projectId as string), - (prevData: any) => - (prevData ?? []).map((c: any) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - false - ); - - cyclesService - .addCycleToFavorites(workspaceSlug as string, projectId as string, { - cycle: cycle.id, - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", - }); - }); - }; - - const handleRemoveFromFavorites = () => { - if (!workspaceSlug || !projectId || !cycle) return; - - const fetchKey = - cycleStatus === "current" - ? CYCLE_CURRENT_LIST(projectId as string) - : cycleStatus === "upcoming" - ? CYCLE_UPCOMING_LIST(projectId as string) - : cycleStatus === "completed" - ? CYCLE_COMPLETE_LIST(projectId as string) - : CYCLE_DRAFT_LIST(projectId as string); - - mutate( - fetchKey, - (prevData) => - (prevData ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - false - ); - - mutate( - CYCLE_LIST(projectId as string), - (prevData: any) => - (prevData ?? []).map((c: any) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - false - ); - - cyclesService - .removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the cycle from favorites. Please try again.", - }); - }); - }; - const handleCopyText = () => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; @@ -250,7 +152,7 @@ export const SingleCycleList: React.FC = ({ return (
-
+
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index fd6df9832..b9b02d49c 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -3,92 +3,46 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; + +// headless ui +import { Tab } from "@headlessui/react"; // hooks +import useLocalStorage from "hooks/use-local-storage"; // services import cycleService from "services/cycles.service"; import projectService from "services/project.service"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // components -import { CreateUpdateCycleModal, CyclesView } from "components/cycles"; +import { + ActiveCycleDetails, + AllCyclesList, + CompletedCyclesList, + CreateUpdateCycleModal, + DraftCyclesList, + UpcomingCyclesList, +} from "components/cycles"; // ui -import { PrimaryButton } from "components/ui"; +import { EmptyState, PrimaryButton } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons -import { PlusIcon } from "@heroicons/react/24/outline"; +import { ListBulletIcon, PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; +// images +import emptyCycle from "public/empty-state/empty-cycle.svg"; // types import { SelectCycleType } from "types"; import type { NextPage } from "next"; // fetch-keys -import { - CYCLE_DRAFT_LIST, - PROJECT_DETAILS, - CYCLE_UPCOMING_LIST, - CYCLE_CURRENT_LIST, - CYCLE_LIST, -} from "constants/fetch-keys"; +import { PROJECT_DETAILS, CYCLE_CURRENT_LIST } from "constants/fetch-keys"; + +const tabsList = ["All", "Active", "Upcoming", "Completed", "Drafts"]; const ProjectCycles: NextPage = () => { const [selectedCycle, setSelectedCycle] = useState(); const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false); - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - 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: draftCycles } = useSWR( - workspaceSlug && projectId ? CYCLE_DRAFT_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => - cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, { - cycle_view: "draft", - }) - : null - ); - - const { data: currentCycle } = useSWR( - workspaceSlug && projectId ? CYCLE_CURRENT_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => - cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, { - cycle_view: "current", - }) - : null - ); - - const { data: upcomingCycles } = useSWR( - workspaceSlug && projectId ? CYCLE_UPCOMING_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => - cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, { - cycle_view: "upcoming", - }) - : null - ); - - const { data: cyclesCompleteList } = useSWR( - workspaceSlug && projectId ? CYCLE_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => - cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, { - cycle_view: "all", - }) - : null - ); - - useEffect(() => { - if (createUpdateCycleModal) return; - const timer = setTimeout(() => { - setSelectedCycle(undefined); - clearTimeout(timer); - }, 500); - }, [createUpdateCycleModal]); + const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycleTab", "All"); + const { storedValue: cyclesView, setValue: setCyclesView } = useLocalStorage("cycleView", "list"); const currentTabValue = (tab: string | null) => { switch (tab) { @@ -107,6 +61,34 @@ const ProjectCycles: NextPage = () => { } }; + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + 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: currentCycle } = useSWR( + workspaceSlug && projectId ? CYCLE_CURRENT_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => + cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "current", + }) + : null + ); + + useEffect(() => { + if (createUpdateCycleModal) return; + const timer = setTimeout(() => { + setSelectedCycle(undefined); + clearTimeout(timer); + }, 500); + }, [createUpdateCycleModal]); + return ( { data={selectedCycle} />
- +
+

Cycles

+
+ + + +
+
+ { + switch (i) { + case 0: + return setCycleTab("All"); + case 1: + return setCycleTab("Active"); + case 2: + return setCycleTab("Upcoming"); + case 3: + return setCycleTab("Completed"); + case 4: + return setCycleTab("Drafts"); + default: + return setCycleTab("All"); + } + }} + > + + {tabsList.map((tab, index) => { + if (cyclesView === "gantt_chart" && (tab === "Active" || tab === "Drafts")) + return null; + + return ( + + `rounded-3xl border px-6 py-1 outline-none ${ + selected + ? "border-brand-accent bg-brand-accent text-white font-medium" + : "border-brand-base bg-brand-base hover:bg-brand-surface-2" + }` + } + > + {tab} + + ); + })} + + + + + + {cyclesView !== "gantt_chart" && ( + + {currentCycle?.[0] ? ( + + ) : ( + + )} + + )} + + + + + + + {cyclesView !== "gantt_chart" && ( + + + + )} + +
); From 1c98f2dca97667bd91eff46605b4efa21e6d4066 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 29 May 2023 02:38:16 +0530 Subject: [PATCH 28/65] chore: update cycle fetch keys names --- .../cycles/active-cycle-details.tsx | 10 ++-- .../cycles/cycles-list/all-cycles-list.tsx | 4 +- .../cycles-list/completed-cycles-list.tsx | 4 +- .../cycles/cycles-list/draft-cycles-list.tsx | 4 +- .../cycles-list/upcoming-cycles-list.tsx | 4 +- apps/app/components/cycles/cycles-view.tsx | 30 +++++------ .../components/cycles/delete-cycle-modal.tsx | 20 +++---- apps/app/components/cycles/form.tsx | 3 +- apps/app/components/cycles/modal.tsx | 52 ++++++++++--------- apps/app/components/cycles/select.tsx | 4 +- .../cycles/transfer-issues-modal.tsx | 8 +-- .../issues/sidebar-select/cycle.tsx | 4 +- apps/app/constants/fetch-keys.ts | 24 ++++----- .../projects/[projectId]/cycles/[cycleId].tsx | 4 +- .../projects/[projectId]/cycles/index.tsx | 19 +++---- 15 files changed, 97 insertions(+), 97 deletions(-) diff --git a/apps/app/components/cycles/active-cycle-details.tsx b/apps/app/components/cycles/active-cycle-details.tsx index e0016dd0b..8dfe36a53 100644 --- a/apps/app/components/cycles/active-cycle-details.tsx +++ b/apps/app/components/cycles/active-cycle-details.tsx @@ -42,7 +42,7 @@ import { truncateText } from "helpers/string.helper"; // types import { ICycle, IIssue } from "types"; // fetch-keys -import { CYCLE_CURRENT_LIST, CYCLE_ISSUES, CYCLE_LIST } from "constants/fetch-keys"; +import { CURRENT_CYCLE_LIST, CYCLES_LIST, CYCLE_ISSUES } from "constants/fetch-keys"; type TSingleStatProps = { cycle: ICycle; @@ -100,7 +100,7 @@ export const ActiveCycleDetails: React.FC = ({ cycle, isComple if (!workspaceSlug || !projectId || !cycle) return; mutate( - CYCLE_CURRENT_LIST(projectId as string), + CURRENT_CYCLE_LIST(projectId as string), (prevData) => (prevData ?? []).map((c) => ({ ...c, @@ -110,7 +110,7 @@ export const ActiveCycleDetails: React.FC = ({ cycle, isComple ); mutate( - CYCLE_LIST(projectId as string), + CYCLES_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, @@ -136,7 +136,7 @@ export const ActiveCycleDetails: React.FC = ({ cycle, isComple if (!workspaceSlug || !projectId || !cycle) return; mutate( - CYCLE_CURRENT_LIST(projectId as string), + CURRENT_CYCLE_LIST(projectId as string), (prevData) => (prevData ?? []).map((c) => ({ ...c, @@ -146,7 +146,7 @@ export const ActiveCycleDetails: React.FC = ({ cycle, isComple ); mutate( - CYCLE_LIST(projectId as string), + CYCLES_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, diff --git a/apps/app/components/cycles/cycles-list/all-cycles-list.tsx b/apps/app/components/cycles/cycles-list/all-cycles-list.tsx index 1e696c7b5..11dc08b3e 100644 --- a/apps/app/components/cycles/cycles-list/all-cycles-list.tsx +++ b/apps/app/components/cycles/cycles-list/all-cycles-list.tsx @@ -7,7 +7,7 @@ import cyclesService from "services/cycles.service"; // components import { CyclesView } from "components/cycles"; // fetch-keys -import { CYCLE_LIST } from "constants/fetch-keys"; +import { CYCLES_LIST } from "constants/fetch-keys"; type Props = { viewType: string | null; @@ -18,7 +18,7 @@ export const AllCyclesList: React.FC = ({ viewType }) => { const { workspaceSlug, projectId } = router.query; const { data: allCyclesList } = useSWR( - workspaceSlug && projectId ? CYCLE_LIST(projectId.toString()) : null, + workspaceSlug && projectId ? CYCLES_LIST(projectId.toString()) : null, workspaceSlug && projectId ? () => cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), { diff --git a/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx b/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx index a2e48c619..bcbb11d4a 100644 --- a/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx +++ b/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx @@ -7,7 +7,7 @@ import cyclesService from "services/cycles.service"; // components import { CyclesView } from "components/cycles"; // fetch-keys -import { CYCLE_COMPLETE_LIST } from "constants/fetch-keys"; +import { COMPLETED_CYCLES_LIST } from "constants/fetch-keys"; type Props = { viewType: string | null; @@ -18,7 +18,7 @@ export const CompletedCyclesList: React.FC = ({ viewType }) => { const { workspaceSlug, projectId } = router.query; const { data: completedCyclesList } = useSWR( - workspaceSlug && projectId ? CYCLE_COMPLETE_LIST(projectId.toString()) : null, + workspaceSlug && projectId ? COMPLETED_CYCLES_LIST(projectId.toString()) : null, workspaceSlug && projectId ? () => cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), { diff --git a/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx b/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx index 746c4b20a..e64e49dfd 100644 --- a/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx +++ b/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx @@ -7,7 +7,7 @@ import cyclesService from "services/cycles.service"; // components import { CyclesView } from "components/cycles"; // fetch-keys -import { CYCLE_DRAFT_LIST } from "constants/fetch-keys"; +import { DRAFT_CYCLES_LIST } from "constants/fetch-keys"; type Props = { viewType: string | null; @@ -18,7 +18,7 @@ export const DraftCyclesList: React.FC = ({ viewType }) => { const { workspaceSlug, projectId } = router.query; const { data: draftCyclesList } = useSWR( - workspaceSlug && projectId ? CYCLE_DRAFT_LIST(projectId.toString()) : null, + workspaceSlug && projectId ? DRAFT_CYCLES_LIST(projectId.toString()) : null, workspaceSlug && projectId ? () => cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), { diff --git a/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx b/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx index 00d06705e..44023e618 100644 --- a/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx +++ b/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx @@ -7,7 +7,7 @@ import cyclesService from "services/cycles.service"; // components import { CyclesView } from "components/cycles"; // fetch-keys -import { CYCLE_UPCOMING_LIST } from "constants/fetch-keys"; +import { UPCOMING_CYCLES_LIST } from "constants/fetch-keys"; type Props = { viewType: string | null; @@ -18,7 +18,7 @@ export const UpcomingCyclesList: React.FC = ({ viewType }) => { const { workspaceSlug, projectId } = router.query; const { data: upcomingCyclesList } = useSWR( - workspaceSlug && projectId ? CYCLE_UPCOMING_LIST(projectId.toString()) : null, + workspaceSlug && projectId ? UPCOMING_CYCLES_LIST(projectId.toString()) : null, workspaceSlug && projectId ? () => cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), { diff --git a/apps/app/components/cycles/cycles-view.tsx b/apps/app/components/cycles/cycles-view.tsx index 5a3305ddb..fc720c251 100644 --- a/apps/app/components/cycles/cycles-view.tsx +++ b/apps/app/components/cycles/cycles-view.tsx @@ -26,11 +26,11 @@ import { getDateRangeStatus } from "helpers/date-time.helper"; import { ICycle } from "types"; // fetch-keys import { - CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_LIST, - CYCLE_DRAFT_LIST, - CYCLE_LIST, - CYCLE_UPCOMING_LIST, + COMPLETED_CYCLES_LIST, + CURRENT_CYCLE_LIST, + CYCLES_LIST, + DRAFT_CYCLES_LIST, + UPCOMING_CYCLES_LIST, } from "constants/fetch-keys"; type Props = { @@ -67,12 +67,12 @@ export const CyclesView: React.FC = ({ cycles, viewType }) => { const fetchKey = cycleStatus === "current" - ? CYCLE_CURRENT_LIST(projectId as string) + ? CURRENT_CYCLE_LIST(projectId as string) : cycleStatus === "upcoming" - ? CYCLE_UPCOMING_LIST(projectId as string) + ? UPCOMING_CYCLES_LIST(projectId as string) : cycleStatus === "completed" - ? CYCLE_COMPLETE_LIST(projectId as string) - : CYCLE_DRAFT_LIST(projectId as string); + ? COMPLETED_CYCLES_LIST(projectId as string) + : DRAFT_CYCLES_LIST(projectId as string); mutate( fetchKey, @@ -85,7 +85,7 @@ export const CyclesView: React.FC = ({ cycles, viewType }) => { ); mutate( - CYCLE_LIST(projectId as string), + CYCLES_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, @@ -114,12 +114,12 @@ export const CyclesView: React.FC = ({ cycles, viewType }) => { const fetchKey = cycleStatus === "current" - ? CYCLE_CURRENT_LIST(projectId as string) + ? CURRENT_CYCLE_LIST(projectId as string) : cycleStatus === "upcoming" - ? CYCLE_UPCOMING_LIST(projectId as string) + ? UPCOMING_CYCLES_LIST(projectId as string) : cycleStatus === "completed" - ? CYCLE_COMPLETE_LIST(projectId as string) - : CYCLE_DRAFT_LIST(projectId as string); + ? COMPLETED_CYCLES_LIST(projectId as string) + : DRAFT_CYCLES_LIST(projectId as string); mutate( fetchKey, @@ -132,7 +132,7 @@ export const CyclesView: React.FC = ({ cycles, viewType }) => { ); mutate( - CYCLE_LIST(projectId as string), + CYCLES_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, diff --git a/apps/app/components/cycles/delete-cycle-modal.tsx b/apps/app/components/cycles/delete-cycle-modal.tsx index dfae65cae..187c9694c 100644 --- a/apps/app/components/cycles/delete-cycle-modal.tsx +++ b/apps/app/components/cycles/delete-cycle-modal.tsx @@ -22,11 +22,11 @@ type TConfirmCycleDeletionProps = { }; // fetch-keys import { - CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_LIST, - CYCLE_DRAFT_LIST, - CYCLE_LIST, - CYCLE_UPCOMING_LIST, + COMPLETED_CYCLES_LIST, + CURRENT_CYCLE_LIST, + CYCLES_LIST, + DRAFT_CYCLES_LIST, + UPCOMING_CYCLES_LIST, } from "constants/fetch-keys"; import { getDateRangeStatus } from "helpers/date-time.helper"; @@ -58,12 +58,12 @@ export const DeleteCycleModal: React.FC = ({ const cycleType = getDateRangeStatus(data.start_date, data.end_date); const fetchKey = cycleType === "current" - ? CYCLE_CURRENT_LIST(projectId as string) + ? CURRENT_CYCLE_LIST(projectId as string) : cycleType === "upcoming" - ? CYCLE_UPCOMING_LIST(projectId as string) + ? UPCOMING_CYCLES_LIST(projectId as string) : cycleType === "completed" - ? CYCLE_COMPLETE_LIST(projectId as string) - : CYCLE_DRAFT_LIST(projectId as string); + ? COMPLETED_CYCLES_LIST(projectId as string) + : DRAFT_CYCLES_LIST(projectId as string); mutate( fetchKey, @@ -76,7 +76,7 @@ export const DeleteCycleModal: React.FC = ({ ); mutate( - CYCLE_LIST(projectId as string), + CYCLES_LIST(projectId as string), (prevData: any) => { if (!prevData) return; return prevData.filter((cycle: any) => cycle.id !== data?.id); diff --git a/apps/app/components/cycles/form.tsx b/apps/app/components/cycles/form.tsx index a0bd781ce..5c0b2e080 100644 --- a/apps/app/components/cycles/form.tsx +++ b/apps/app/components/cycles/form.tsx @@ -12,7 +12,7 @@ type Props = { handleFormSubmit: (values: Partial) => Promise; handleClose: () => void; status: boolean; - data?: ICycle; + data?: ICycle | null; }; const defaultValues: Partial = { @@ -28,7 +28,6 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat formState: { errors, isSubmitting }, handleSubmit, control, - watch, reset, } = useForm({ defaultValues, diff --git a/apps/app/components/cycles/modal.tsx b/apps/app/components/cycles/modal.tsx index 715b40e2c..e27b5ac32 100644 --- a/apps/app/components/cycles/modal.tsx +++ b/apps/app/components/cycles/modal.tsx @@ -18,12 +18,12 @@ import { getDateRangeStatus, isDateGreaterThanToday } from "helpers/date-time.he import type { ICycle } from "types"; // fetch keys import { - CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_LIST, - CYCLE_DRAFT_LIST, - CYCLE_INCOMPLETE_LIST, - CYCLE_LIST, - CYCLE_UPCOMING_LIST, + COMPLETED_CYCLES_LIST, + CURRENT_CYCLE_LIST, + CYCLES_LIST, + DRAFT_CYCLES_LIST, + INCOMPLETE_CYCLES_LIST, + UPCOMING_CYCLES_LIST, } from "constants/fetch-keys"; type CycleModalProps = { @@ -43,24 +43,26 @@ export const CreateUpdateCycleModal: React.FC = ({ const { setToastAlert } = useToast(); const createCycle = async (payload: Partial) => { + if (!workspaceSlug || !projectId) return; + await cycleService - .createCycle(workspaceSlug as string, projectId as string, payload) + .createCycle(workspaceSlug.toString(), projectId.toString(), payload) .then((res) => { switch (getDateRangeStatus(res.start_date, res.end_date)) { case "completed": - mutate(CYCLE_COMPLETE_LIST(projectId as string)); + mutate(COMPLETED_CYCLES_LIST(projectId.toString())); break; case "current": - mutate(CYCLE_CURRENT_LIST(projectId as string)); + mutate(CURRENT_CYCLE_LIST(projectId.toString())); break; case "upcoming": - mutate(CYCLE_UPCOMING_LIST(projectId as string)); + mutate(UPCOMING_CYCLES_LIST(projectId.toString())); break; default: - mutate(CYCLE_DRAFT_LIST(projectId as string)); + mutate(DRAFT_CYCLES_LIST(projectId.toString())); } - mutate(CYCLE_INCOMPLETE_LIST(projectId as string)); - mutate(CYCLE_LIST(projectId as string)); + mutate(INCOMPLETE_CYCLES_LIST(projectId.toString())); + mutate(CYCLES_LIST(projectId.toString())); handleClose(); setToastAlert({ @@ -69,7 +71,7 @@ export const CreateUpdateCycleModal: React.FC = ({ message: "Cycle created successfully.", }); }) - .catch((err) => { + .catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -79,39 +81,41 @@ export const CreateUpdateCycleModal: React.FC = ({ }; const updateCycle = async (cycleId: string, payload: Partial) => { + if (!workspaceSlug || !projectId) return; + await cycleService - .updateCycle(workspaceSlug as string, projectId as string, cycleId, payload) + .updateCycle(workspaceSlug.toString(), projectId.toString(), cycleId, payload) .then((res) => { switch (getDateRangeStatus(data?.start_date, data?.end_date)) { case "completed": - mutate(CYCLE_COMPLETE_LIST(projectId as string)); + mutate(COMPLETED_CYCLES_LIST(projectId.toString())); break; case "current": - mutate(CYCLE_CURRENT_LIST(projectId as string)); + mutate(CURRENT_CYCLE_LIST(projectId.toString())); break; case "upcoming": - mutate(CYCLE_UPCOMING_LIST(projectId as string)); + mutate(UPCOMING_CYCLES_LIST(projectId.toString())); break; default: - mutate(CYCLE_DRAFT_LIST(projectId as string)); + mutate(DRAFT_CYCLES_LIST(projectId.toString())); } - mutate(CYCLE_LIST(projectId as string)); + mutate(CYCLES_LIST(projectId.toString())); if ( getDateRangeStatus(data?.start_date, data?.end_date) != getDateRangeStatus(res.start_date, res.end_date) ) { switch (getDateRangeStatus(res.start_date, res.end_date)) { case "completed": - mutate(CYCLE_COMPLETE_LIST(projectId as string)); + mutate(COMPLETED_CYCLES_LIST(projectId.toString())); break; case "current": - mutate(CYCLE_CURRENT_LIST(projectId as string)); + mutate(CURRENT_CYCLE_LIST(projectId.toString())); break; case "upcoming": - mutate(CYCLE_UPCOMING_LIST(projectId as string)); + mutate(UPCOMING_CYCLES_LIST(projectId.toString())); break; default: - mutate(CYCLE_DRAFT_LIST(projectId as string)); + mutate(DRAFT_CYCLES_LIST(projectId.toString())); } } diff --git a/apps/app/components/cycles/select.tsx b/apps/app/components/cycles/select.tsx index 854749ec5..5a6f00ada 100644 --- a/apps/app/components/cycles/select.tsx +++ b/apps/app/components/cycles/select.tsx @@ -14,7 +14,7 @@ import cycleServices from "services/cycles.service"; // components import { CreateUpdateCycleModal } from "components/cycles"; // fetch-keys -import { CYCLE_LIST } from "constants/fetch-keys"; +import { CYCLES_LIST } from "constants/fetch-keys"; export type IssueCycleSelectProps = { projectId: string; @@ -36,7 +36,7 @@ export const CycleSelect: React.FC = ({ const { workspaceSlug } = router.query; const { data: cycles } = useSWR( - workspaceSlug && projectId ? CYCLE_LIST(projectId) : null, + workspaceSlug && projectId ? CYCLES_LIST(projectId) : null, workspaceSlug && projectId ? () => cycleServices.getCyclesWithParams(workspaceSlug as string, projectId as string, { diff --git a/apps/app/components/cycles/transfer-issues-modal.tsx b/apps/app/components/cycles/transfer-issues-modal.tsx index 8d44c14d0..30db2f320 100644 --- a/apps/app/components/cycles/transfer-issues-modal.tsx +++ b/apps/app/components/cycles/transfer-issues-modal.tsx @@ -10,16 +10,16 @@ import { Dialog, Transition } from "@headlessui/react"; import cyclesService from "services/cycles.service"; // hooks import useToast from "hooks/use-toast"; +import useIssuesView from "hooks/use-issues-view"; //icons import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline"; -import { ContrastIcon, CyclesIcon, ExclamationIcon, TransferIcon } from "components/icons"; +import { ContrastIcon, ExclamationIcon, TransferIcon } from "components/icons"; // fetch-key -import { CYCLE_INCOMPLETE_LIST, CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; +import { CYCLE_ISSUES_WITH_PARAMS, INCOMPLETE_CYCLES_LIST } from "constants/fetch-keys"; // types import { ICycle } from "types"; //helper import { getDateRangeStatus } from "helpers/date-time.helper"; -import useIssuesView from "hooks/use-issues-view"; type Props = { isOpen: boolean; @@ -57,7 +57,7 @@ export const TransferIssuesModal: React.FC = ({ isOpen, handleClose }) => }; const { data: incompleteCycles } = useSWR( - workspaceSlug && projectId ? CYCLE_INCOMPLETE_LIST(projectId as string) : null, + workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null, workspaceSlug && projectId ? () => cyclesService.getCyclesWithParams(workspaceSlug as string, projectId as string, { diff --git a/apps/app/components/issues/sidebar-select/cycle.tsx b/apps/app/components/issues/sidebar-select/cycle.tsx index d9e7772a3..ad112ab52 100644 --- a/apps/app/components/issues/sidebar-select/cycle.tsx +++ b/apps/app/components/issues/sidebar-select/cycle.tsx @@ -16,7 +16,7 @@ import { ContrastIcon } from "components/icons"; // types import { ICycle, IIssue, UserAuth } from "types"; // fetch-keys -import { CYCLE_ISSUES, CYCLE_INCOMPLETE_LIST, ISSUE_DETAILS } from "constants/fetch-keys"; +import { CYCLE_ISSUES, INCOMPLETE_CYCLES_LIST, ISSUE_DETAILS } from "constants/fetch-keys"; type Props = { issueDetail: IIssue | undefined; @@ -33,7 +33,7 @@ export const SidebarCycleSelect: React.FC = ({ const { workspaceSlug, projectId, issueId } = router.query; const { data: incompleteCycles } = useSWR( - workspaceSlug && projectId ? CYCLE_INCOMPLETE_LIST(projectId as string) : null, + workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null, workspaceSlug && projectId ? () => cyclesService.getCyclesWithParams(workspaceSlug as string, projectId as string, { diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 5b286bcc1..75b187b2b 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -71,9 +71,18 @@ export const PROJECT_ISSUE_LABELS = (projectId: string) => export const PROJECT_GITHUB_REPOSITORY = (projectId: string) => `PROJECT_GITHUB_REPOSITORY_${projectId.toUpperCase()}`; -export const CYCLE_LIST = (projectId: string) => `CYCLE_LIST_${projectId.toUpperCase()}`; -export const CYCLE_INCOMPLETE_LIST = (projectId: string) => - `CYCLE_INCOMPLETE_LIST_${projectId.toUpperCase()}`; +// cycles +export const CYCLES_LIST = (projectId: string) => `CYCLE_LIST_${projectId.toUpperCase()}`; +export const INCOMPLETE_CYCLES_LIST = (projectId: string) => + `INCOMPLETE_CYCLES_LIST_${projectId.toUpperCase()}`; +export const CURRENT_CYCLE_LIST = (projectId: string) => + `CURRENT_CYCLE_LIST_${projectId.toUpperCase()}`; +export const UPCOMING_CYCLES_LIST = (projectId: string) => + `UPCOMING_CYCLES_LIST_${projectId.toUpperCase()}`; +export const DRAFT_CYCLES_LIST = (projectId: string) => + `DRAFT_CYCLES_LIST_${projectId.toUpperCase()}`; +export const COMPLETED_CYCLES_LIST = (projectId: string) => + `COMPLETED_CYCLES_LIST_${projectId.toUpperCase()}`; export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId.toUpperCase()}`; export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => { if (!params) return `CYCLE_ISSUES_WITH_PARAMS_${cycleId.toUpperCase()}`; @@ -84,15 +93,6 @@ export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => { }; export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAILS_${cycleId.toUpperCase()}`; -export const CYCLE_CURRENT_LIST = (projectId: string) => - `CYCLE_CURRENT_LIST${projectId.toUpperCase()}`; -export const CYCLE_UPCOMING_LIST = (projectId: string) => - `CYCLE_UPCOMING_LIST${projectId.toUpperCase()}`; -export const CYCLE_DRAFT_LIST = (projectId: string) => - `CYCLE_DRAFT_LIST_${projectId.toUpperCase()}`; -export const CYCLE_COMPLETE_LIST = (projectId: string) => - `CYCLE_COMPLETE_LIST_${projectId.toUpperCase()}`; - export const STATES_LIST = (projectId: string) => `STATES_LIST_${projectId.toUpperCase()}`; export const STATE_DETAILS = "STATE_DETAILS"; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 3e5fafe6f..08c04a736 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -30,7 +30,7 @@ import { getDateRangeStatus } from "helpers/date-time.helper"; // fetch-keys import { CYCLE_ISSUES, - CYCLE_LIST, + CYCLES_LIST, PROJECT_DETAILS, CYCLE_DETAILS, PROJECT_ISSUES_LIST, @@ -54,7 +54,7 @@ const SingleCycle: React.FC = () => { ); const { data: cycles } = useSWR( - workspaceSlug && projectId ? CYCLE_LIST(projectId as string) : null, + workspaceSlug && projectId ? CYCLES_LIST(projectId as string) : null, workspaceSlug && projectId ? () => cycleServices.getCyclesWithParams(workspaceSlug as string, projectId as string, { diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index b9b02d49c..4573cd53c 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -23,17 +23,15 @@ import { UpcomingCyclesList, } from "components/cycles"; // ui -import { EmptyState, PrimaryButton } from "components/ui"; +import { PrimaryButton } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons import { ListBulletIcon, PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; -// images -import emptyCycle from "public/empty-state/empty-cycle.svg"; // types import { SelectCycleType } from "types"; import type { NextPage } from "next"; // fetch-keys -import { PROJECT_DETAILS, CYCLE_CURRENT_LIST } from "constants/fetch-keys"; +import { CURRENT_CYCLE_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; const tabsList = ["All", "Active", "Upcoming", "Completed", "Drafts"]; @@ -72,7 +70,7 @@ const ProjectCycles: NextPage = () => { ); const { data: currentCycle } = useSWR( - workspaceSlug && projectId ? CYCLE_CURRENT_LIST(projectId as string) : null, + workspaceSlug && projectId ? CURRENT_CYCLE_LIST(projectId as string) : null, workspaceSlug && projectId ? () => cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, { @@ -207,12 +205,11 @@ const ProjectCycles: NextPage = () => { {currentCycle?.[0] ? ( ) : ( - +
+

+ No active cycle is present. +

+
)} )} From ffc6077e9ba878f0e5d4697cb7bbece06534fab0 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 29 May 2023 12:11:16 +0530 Subject: [PATCH 29/65] chore: improve docker setup (#1150) --- .env.example | 21 ++++++++++++++------- apiserver/plane/api/views/asset.py | 4 ++-- apiserver/plane/api/views/issue.py | 2 +- apiserver/plane/db/models/asset.py | 7 ++----- apiserver/plane/db/models/issue.py | 6 ++---- apiserver/plane/settings/local.py | 7 +++++-- apiserver/plane/settings/production.py | 8 ++++++-- apiserver/plane/settings/staging.py | 3 ++- docker-compose-hub.yml | 8 ++++++++ docker-compose.yml | 10 +++++++++- setup.sh | 2 +- 11 files changed, 52 insertions(+), 26 deletions(-) diff --git a/.env.example b/.env.example index a5ba26b3f..35c5f83d3 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,13 @@ NEXT_PUBLIC_TRACK_EVENTS=0 NEXT_PUBLIC_SLACK_CLIENT_ID="" # Backend + +# Database Settings +PGUSER="plane" +PGPASSWORD="plane" +PGHOST="plane-db" +PGDATABASE="plane" + # Email Settings EMAIL_HOST="" EMAIL_HOST_USER="" @@ -32,8 +39,10 @@ EMAIL_FROM="Team Plane " AWS_REGION="" AWS_ACCESS_KEY_ID="access-key" AWS_SECRET_ACCESS_KEY="secret-key" +# Changing this requires change in the nginx.conf for uploads if using minio setup AWS_S3_BUCKET_NAME="uploads" -AWS_S3_ENDPOINT_URL="" +# Maximum file upload limit +FILE_SIZE_LIMIT=5242880 # GPT settings OPENAI_API_KEY="" @@ -45,13 +54,11 @@ GITHUB_CLIENT_SECRET="" # For fetching release notes # Settings related to Docker DOCKERIZED=1 -# Database Settings -PGUSER="plane" -PGPASSWORD="plane" -PGHOST="plane-db" -PGDATABASE="plane" - # Nginx Configuration NGINX_PORT=80 +# Default Creds +DEFAULT_EMAIL="captain@plane.so" +DEFAULT_PASSWORD="password123" + # Auto generated and Required that will be generated from setup.sh \ No newline at end of file diff --git a/apiserver/plane/api/views/asset.py b/apiserver/plane/api/views/asset.py index 0102867d7..0f0513405 100644 --- a/apiserver/plane/api/views/asset.py +++ b/apiserver/plane/api/views/asset.py @@ -35,7 +35,7 @@ class FileAssetEndpoint(BaseAPIView): serializer.save(workspace_id=request.user.last_workspace_id) response_data = serializer.data - if settings.DOCKERIZED and settings.AWS_S3_ENDPOINT_URL in response_data["asset"]: + if settings.DOCKERIZED and settings.USE_MINIO: response_data["asset"] = response_data["asset"].replace(settings.AWS_S3_ENDPOINT_URL, settings.WEB_URL) return Response(response_data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -86,7 +86,7 @@ class UserAssetsEndpoint(BaseAPIView): if serializer.is_valid(): serializer.save() response_data = serializer.data - if settings.DOCKERIZED and settings.AWS_S3_ENDPOINT_URL in response_data["asset"]: + if settings.DOCKERIZED and settings.USE_MINIO: response_data["asset"] = response_data["asset"].replace(settings.AWS_S3_ENDPOINT_URL, settings.WEB_URL) return Response(response_data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index abdfe6aae..74d0d466c 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -820,7 +820,7 @@ class IssueAttachmentEndpoint(BaseAPIView): response_data = serializer.data if ( settings.DOCKERIZED - and settings.AWS_S3_ENDPOINT_URL in response_data["asset"] + and settings.USE_MINIO ): response_data["asset"] = response_data["asset"].replace( settings.AWS_S3_ENDPOINT_URL, settings.WEB_URL diff --git a/apiserver/plane/db/models/asset.py b/apiserver/plane/db/models/asset.py index e37f2c0b0..01ef1d9d8 100644 --- a/apiserver/plane/db/models/asset.py +++ b/apiserver/plane/db/models/asset.py @@ -17,11 +17,8 @@ def get_upload_path(instance, filename): def file_size(value): - # File limit check is only for cloud hosted - if not settings.DOCKERIZED: - limit = 5 * 1024 * 1024 - if value.size > limit: - raise ValidationError("File too large. Size should not exceed 5 MB.") + if value.size > settings.FILE_SIZE_LIMIT: + raise ValidationError("File too large. Size should not exceed 5 MB.") class FileAsset(BaseModel): diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index f58d4ac13..e25695c42 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -211,10 +211,8 @@ def get_upload_path(instance, filename): def file_size(value): # File limit check is only for cloud hosted - if not settings.DOCKERIZED: - limit = 5 * 1024 * 1024 - if value.size > limit: - raise ValidationError("File too large. Size should not exceed 5 MB.") + if value.size > settings.FILE_SIZE_LIMIT: + raise ValidationError("File too large. Size should not exceed 5 MB.") class IssueAttachment(ProjectBaseModel): diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index 70a4be49a..324d2edbe 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -29,6 +29,10 @@ DOCKERIZED = int(os.environ.get( "DOCKERIZED", 0 )) == 1 +USE_MINIO = int(os.environ.get("USE_MINIO"), 0) == 1 + +FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) + if DOCKERIZED: DATABASES["default"] = dj_database_url.config() @@ -86,5 +90,4 @@ LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False) 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) \ No newline at end of file +GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False) diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index 81a3c2082..a1d450137 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -34,6 +34,10 @@ DOCKERIZED = int(os.environ.get( "DOCKERIZED", 0 )) == 1 +USE_MINIO = int(os.environ.get("USE_MINIO"), 0) == 1 + +FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) + # Enable Connection Pooling (if desired) # DATABASES['default']['ENGINE'] = 'django_postgrespool' @@ -81,7 +85,7 @@ if bool(os.environ.get("SENTRY_DSN", False)): environment="production", ) -if DOCKERIZED: +if DOCKERIZED and USE_MINIO: DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' # The AWS access key to use. AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key") @@ -90,7 +94,7 @@ if DOCKERIZED: # The name of the bucket to store files in. AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads") # The full URL to the S3 endpoint. Leave blank to use the default region URL. - AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "http://plane-minio:9000") + AWS_S3_ENDPOINT_URL = "http://plane-minio:9000" # Default permissions AWS_DEFAULT_ACL = "public-read" AWS_QUERYSTRING_AUTH = False diff --git a/apiserver/plane/settings/staging.py b/apiserver/plane/settings/staging.py index d1d8e1749..c5db5d300 100644 --- a/apiserver/plane/settings/staging.py +++ b/apiserver/plane/settings/staging.py @@ -53,6 +53,8 @@ STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" DOCKERIZED = int(os.environ.get( "DOCKERIZED", 0 )) == 1 +FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) +USE_MINIO = int(os.environ.get("USE_MINIO"), 0) == 1 sentry_sdk.init( dsn=os.environ.get("SENTRY_DSN"), @@ -169,7 +171,6 @@ CSRF_COOKIE_SECURE = True REDIS_URL = os.environ.get("REDIS_URL") -DOCKERIZED = os.environ.get("DOCKERIZED", False) CACHES = { "default": { diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml index 97ef38260..1bdd41d07 100644 --- a/docker-compose-hub.yml +++ b/docker-compose-hub.yml @@ -35,6 +35,7 @@ services: AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} + FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT} WEB_URL: ${WEB_URL} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} DISABLE_COLLECTSTATIC: 1 @@ -42,6 +43,9 @@ services: OPENAI_API_KEY: ${OPENAI_API_KEY} GPT_ENGINE: ${GPT_ENGINE} SECRET_KEY: ${SECRET_KEY} + DEFAULT_EMAIL: ${DEFAULT_EMAIL} + DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} + USE_MINIO: 1 depends_on: - plane-db - plane-redis @@ -66,6 +70,7 @@ services: AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} + FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT} WEB_URL: ${WEB_URL} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} DISABLE_COLLECTSTATIC: 1 @@ -73,6 +78,9 @@ services: OPENAI_API_KEY: ${OPENAI_API_KEY} GPT_ENGINE: ${GPT_ENGINE} SECRET_KEY: ${SECRET_KEY} + DEFAULT_EMAIL: ${DEFAULT_EMAIL} + DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} + USE_MINIO: 1 depends_on: - plane-api - plane-db diff --git a/docker-compose.yml b/docker-compose.yml index fe9e5f15d..19f95f875 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,6 +42,7 @@ services: AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL} + FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT} WEB_URL: ${WEB_URL} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} DISABLE_COLLECTSTATIC: 1 @@ -49,6 +50,9 @@ services: OPENAI_API_KEY: ${OPENAI_API_KEY} GPT_ENGINE: ${GPT_ENGINE} SECRET_KEY: ${SECRET_KEY} + DEFAULT_EMAIL: ${DEFAULT_EMAIL} + DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} + USE_MINIO: 1 depends_on: - plane-db - plane-redis @@ -74,6 +78,7 @@ services: AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME} AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL} + FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT} WEB_URL: ${WEB_URL} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} DISABLE_COLLECTSTATIC: 1 @@ -81,6 +86,9 @@ services: OPENAI_API_KEY: ${OPENAI_API_KEY} GPT_ENGINE: ${GPT_ENGINE} SECRET_KEY: ${SECRET_KEY} + DEFAULT_EMAIL: ${DEFAULT_EMAIL} + DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} + USE_MINIO: 1 depends_on: - plane-api - plane-db @@ -136,7 +144,7 @@ services: dockerfile: Dockerfile restart: always ports: - - ${NGINX_PORT}:80 + - ${NGINX_PORT}:80 depends_on: - plane-web - plane-api diff --git a/setup.sh b/setup.sh index 7fc5847e0..8c1f81a48 100755 --- a/setup.sh +++ b/setup.sh @@ -10,7 +10,7 @@ export LC_CTYPE=C echo -e "\nNEXT_PUBLIC_API_BASE_URL=$1" >> ./.env # Generate the SECRET_KEY that will be used by django -echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c50)\"" >> ./.env +echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./.env # WEB_URL for email redirection and image saving echo -e "WEB_URL=$1" >> ./.env From 022960d7e33412aed67bad72898db50fea193dd8 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 29 May 2023 15:16:49 +0530 Subject: [PATCH 30/65] fix: pages layout and alignment issues (#1152) --- .../pages/pages-list/all-pages-list.tsx | 6 +- .../pages/pages-list/favorite-pages-list.tsx | 6 +- .../pages/pages-list/my-pages-list.tsx | 6 +- .../pages/pages-list/other-pages-list.tsx | 6 +- .../pages/pages-list/recent-pages-list.tsx | 45 ++- apps/app/components/pages/pages-view.tsx | 110 +++---- .../projects/[projectId]/pages/index.tsx | 268 ++++++------------ 7 files changed, 161 insertions(+), 286 deletions(-) diff --git a/apps/app/components/pages/pages-list/all-pages-list.tsx b/apps/app/components/pages/pages-list/all-pages-list.tsx index 8bae35530..67f6135ed 100644 --- a/apps/app/components/pages/pages-list/all-pages-list.tsx +++ b/apps/app/components/pages/pages-list/all-pages-list.tsx @@ -25,9 +25,5 @@ export const AllPagesList: React.FC = ({ viewType }) => { : null ); - return ( -
- -
- ); + return ; }; diff --git a/apps/app/components/pages/pages-list/favorite-pages-list.tsx b/apps/app/components/pages/pages-list/favorite-pages-list.tsx index b846fbb7e..89ba6d293 100644 --- a/apps/app/components/pages/pages-list/favorite-pages-list.tsx +++ b/apps/app/components/pages/pages-list/favorite-pages-list.tsx @@ -25,9 +25,5 @@ export const FavoritePagesList: React.FC = ({ viewType }) => { : null ); - return ( -
- -
- ); + return ; }; diff --git a/apps/app/components/pages/pages-list/my-pages-list.tsx b/apps/app/components/pages/pages-list/my-pages-list.tsx index 832d2a350..d821eb655 100644 --- a/apps/app/components/pages/pages-list/my-pages-list.tsx +++ b/apps/app/components/pages/pages-list/my-pages-list.tsx @@ -25,9 +25,5 @@ export const MyPagesList: React.FC = ({ viewType }) => { : null ); - return ( -
- -
- ); + return ; }; diff --git a/apps/app/components/pages/pages-list/other-pages-list.tsx b/apps/app/components/pages/pages-list/other-pages-list.tsx index b8ab1bdee..dc476408e 100644 --- a/apps/app/components/pages/pages-list/other-pages-list.tsx +++ b/apps/app/components/pages/pages-list/other-pages-list.tsx @@ -25,9 +25,5 @@ export const OtherPagesList: React.FC = ({ viewType }) => { : null ); - return ( -
- -
- ); + return ; }; diff --git a/apps/app/components/pages/pages-list/recent-pages-list.tsx b/apps/app/components/pages/pages-list/recent-pages-list.tsx index 7b162f796..44225aee5 100644 --- a/apps/app/components/pages/pages-list/recent-pages-list.tsx +++ b/apps/app/components/pages/pages-list/recent-pages-list.tsx @@ -37,37 +37,28 @@ export const RecentPagesList: React.FC = ({ viewType }) => { <> {pages ? ( Object.keys(pages).length > 0 && !isEmpty ? ( -
- {Object.keys(pages).map((key) => { - if (pages[key].length === 0) return null; + Object.keys(pages).map((key) => { + if (pages[key].length === 0) return null; - return ( - -
-

- {replaceUnderscoreIfSnakeCase(key)} -

- -
-
- ); - })} -
+ return ( +
+

+ {replaceUnderscoreIfSnakeCase(key)} +

+ +
+ ); + }) ) : ( -
- -
+ ) ) : ( - + diff --git a/apps/app/components/pages/pages-view.tsx b/apps/app/components/pages/pages-view.tsx index 58aa21c07..a1c0d083f 100644 --- a/apps/app/components/pages/pages-view.tsx +++ b/apps/app/components/pages/pages-view.tsx @@ -200,61 +200,63 @@ export const PagesView: React.FC = ({ pages, viewType }) => { data={selectedPageToDelete} /> {pages ? ( - pages.length > 0 ? ( - viewType === "list" ? ( -
    - {pages.map((page) => ( - handleEditPage(page)} - handleDeletePage={() => handleDeletePage(page)} - handleAddToFavorites={() => handleAddToFavorites(page)} - handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)} - partialUpdatePage={partialUpdatePage} - /> - ))} -
- ) : viewType === "detailed" ? ( -
- {pages.map((page) => ( - handleEditPage(page)} - handleDeletePage={() => handleDeletePage(page)} - handleAddToFavorites={() => handleAddToFavorites(page)} - handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)} - partialUpdatePage={partialUpdatePage} - /> - ))} -
+
+ {pages.length > 0 ? ( + viewType === "list" ? ( +
    + {pages.map((page) => ( + handleEditPage(page)} + handleDeletePage={() => handleDeletePage(page)} + handleAddToFavorites={() => handleAddToFavorites(page)} + handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)} + partialUpdatePage={partialUpdatePage} + /> + ))} +
+ ) : viewType === "detailed" ? ( +
+ {pages.map((page) => ( + handleEditPage(page)} + handleDeletePage={() => handleDeletePage(page)} + handleAddToFavorites={() => handleAddToFavorites(page)} + handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)} + partialUpdatePage={partialUpdatePage} + /> + ))} +
+ ) : ( +
+ {pages.map((page) => ( + handleEditPage(page)} + handleDeletePage={() => handleDeletePage(page)} + handleAddToFavorites={() => handleAddToFavorites(page)} + handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)} + partialUpdatePage={partialUpdatePage} + /> + ))} +
+ ) ) : ( -
- {pages.map((page) => ( - handleEditPage(page)} - handleDeletePage={() => handleDeletePage(page)} - handleAddToFavorites={() => handleAddToFavorites(page)} - handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)} - partialUpdatePage={partialUpdatePage} - /> - ))} -
- ) - ) : ( - - ) + + )} +
) : viewType === "list" ? ( diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index 874057e06..0d5d1252a 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -1,19 +1,15 @@ -import { useState } from "react"; +import { useState, Fragment } from "react"; import { useRouter } from "next/router"; import dynamic from "next/dynamic"; -import useSWR, { mutate } from "swr"; +import useSWR from "swr"; -// react-hook-form -import { useForm } from "react-hook-form"; // headless ui import { Tab } from "@headlessui/react"; // services import projectService from "services/project.service"; -import pagesService from "services/pages.service"; // hooks -import useToast from "hooks/use-toast"; import useLocalStorage from "hooks/use-local-storage"; // icons import { PlusIcon } from "components/icons"; @@ -22,20 +18,15 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // components import { RecentPagesList, CreateUpdatePageModal, TPagesListProps } from "components/pages"; // ui -import { Input, PrimaryButton } from "components/ui"; +import { PrimaryButton } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons import { ListBulletIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; // types -import { IPage, TPageViewProps } from "types"; +import { TPageViewProps } from "types"; import type { NextPage } from "next"; // fetch-keys -import { - ALL_PAGES_LIST, - MY_PAGES_LIST, - PROJECT_DETAILS, - RECENT_PAGES_LIST, -} from "constants/fetch-keys"; +import { PROJECT_DETAILS } from "constants/fetch-keys"; const AllPagesList = dynamic( () => import("components/pages").then((a) => a.AllPagesList), @@ -65,6 +56,8 @@ const OtherPagesList = dynamic( } ); +const tabsList = ["Recent", "All", "Favorites", "Created by me", "Created by others"]; + const ProjectPages: NextPage = () => { const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false); @@ -73,22 +66,8 @@ const ProjectPages: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { setToastAlert } = useToast(); - const { storedValue: pageTab, setValue: setPageTab } = useLocalStorage("pageTab", "Recent"); - const { - handleSubmit, - register, - watch, - reset, - formState: { isSubmitting }, - } = useForm>({ - defaultValues: { - name: "", - }, - }); - const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId @@ -96,58 +75,6 @@ const ProjectPages: NextPage = () => { : null ); - const createPage = async (formData: Partial) => { - if (!workspaceSlug || !projectId) return; - - if (formData.name === "") { - setToastAlert({ - type: "error", - title: "Error!", - message: "Page name is required", - }); - return; - } - - await pagesService - .createPage(workspaceSlug as string, projectId as string, formData) - .then((res) => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Page created successfully.", - }); - router.push(`/${workspaceSlug}/projects/${projectId}/pages/${res.id}`); - reset(); - - mutate(RECENT_PAGES_LIST(projectId as string)); - mutate( - MY_PAGES_LIST(projectId as string), - (prevData) => { - if (!prevData) return undefined; - - return [res, ...(prevData as IPage[])]; - }, - false - ); - mutate( - ALL_PAGES_LIST(projectId as string), - (prevData) => { - if (!prevData) return undefined; - - return [res, ...(prevData as IPage[])]; - }, - false - ); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Page could not be created. Please try again", - }); - }); - }; - const currentTabValue = (tab: string | null) => { switch (tab) { case "Recent": @@ -195,114 +122,85 @@ const ProjectPages: NextPage = () => { } > -
+

Pages

- {/* - - {watch("name") !== "" && ( - - {isSubmitting ? "Creating..." : "Create"} - - )} - */} -
- { - switch (i) { - case 0: - return setPageTab("Recent"); - case 1: - return setPageTab("All"); - case 2: - return setPageTab("Favorites"); - case 3: - return setPageTab("Created by me"); - case 4: - return setPageTab("Created by others"); + { + switch (i) { + case 0: + return setPageTab("Recent"); + case 1: + return setPageTab("All"); + case 2: + return setPageTab("Favorites"); + case 3: + return setPageTab("Created by me"); + case 4: + return setPageTab("Created by others"); - default: - return setPageTab("Recent"); - } - }} - > - -
- {["Recent", "All", "Favorites", "Created by me", "Created by others"].map( - (tab, index) => ( - - `rounded-full border px-5 py-1.5 text-sm outline-none ${ - selected - ? "border-brand-accent bg-brand-accent text-white" - : "border-brand-base bg-brand-base hover:bg-brand-surface-1" - }` - } - > - {tab} - - ) - )} -
-
- - - {/* */} -
-
- - - - - - - - - - - - - - - - - -
-
+ {tab} + + ))} +
+
+ + +
+ + + + + + + + + + + + + + + + + + +
From 26ba4d71c315bbaa6287168e844b8db0a0913b2a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 29 May 2023 15:38:35 +0530 Subject: [PATCH 31/65] fix: order by last updated, cycles empty state alignment (#1151) * fix: order by last updated * fix: cycles empty space alignment, chore: new meta tags * chore: update meta tags --- .../auth-screens/not-authorized-view.tsx | 7 +- .../auth-screens/workspace/not-a-member.tsx | 60 +++++------- .../app/components/issues/sub-issues-list.tsx | 15 +-- apps/app/constants/issue.ts | 2 +- apps/app/constants/seo-variables.ts | 12 +-- .../project-authorization-wrapper.tsx | 16 +--- .../workspace-authorization-wrapper.tsx | 94 ++++++++----------- apps/app/layouts/container.tsx | 69 -------------- apps/app/layouts/default-layout/index.tsx | 21 +---- apps/app/pages/404.tsx | 7 +- .../[workspaceSlug]/me/profile/activity.tsx | 3 - .../[workspaceSlug]/me/profile/index.tsx | 3 - .../me/profile/preferences.tsx | 3 - .../projects/[projectId]/cycles/index.tsx | 3 - .../projects/[projectId]/modules/index.tsx | 3 - .../projects/[projectId]/pages/[pageId].tsx | 3 - .../projects/[projectId]/pages/index.tsx | 3 - .../projects/[projectId]/views/index.tsx | 3 - .../pages/[workspaceSlug]/settings/index.tsx | 3 - apps/app/pages/_app.tsx | 28 +++++- apps/app/pages/{_error.js => _error.tsx} | 29 +----- apps/app/pages/error.tsx | 7 +- apps/app/pages/invitations.tsx | 8 +- apps/app/pages/magic-sign-in.tsx | 6 +- apps/app/pages/signin.tsx | 6 +- apps/app/types/issues.d.ts | 2 +- 26 files changed, 119 insertions(+), 297 deletions(-) delete mode 100644 apps/app/layouts/container.tsx rename apps/app/pages/{_error.js => _error.tsx} (70%) diff --git a/apps/app/components/auth-screens/not-authorized-view.tsx b/apps/app/components/auth-screens/not-authorized-view.tsx index dc1db8a21..0ebc85fb4 100644 --- a/apps/app/components/auth-screens/not-authorized-view.tsx +++ b/apps/app/components/auth-screens/not-authorized-view.tsx @@ -21,12 +21,7 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => { const { asPath: currentPath } = useRouter(); return ( - +
{ - const router = useRouter(); - - return ( - -
-
-
-

Not Authorized!

-

- You{"'"}re not a member of this workspace. Please contact the workspace admin to get - an invitation or check your pending invitations. -

-
-
+export const NotAWorkspaceMember = () => ( + +
+
+
+

Not Authorized!

+

+ You{"'"}re not a member of this workspace. Please contact the workspace admin to get an + invitation or check your pending invitations. +

+
+
- - ); -}; +
+
+); diff --git a/apps/app/components/issues/sub-issues-list.tsx b/apps/app/components/issues/sub-issues-list.tsx index 85f05a3c9..351bf47c5 100644 --- a/apps/app/components/issues/sub-issues-list.tsx +++ b/apps/app/components/issues/sub-issues-list.tsx @@ -165,15 +165,10 @@ export const SubIssuesList: FC = ({ parentIssue }) => { }); }; - const completedSubIssues = - subIssuesResponse && subIssuesResponse.state_distribution - ? (subIssuesResponse?.state_distribution.completed - ? subIssuesResponse?.state_distribution.completed - : 0) + - (subIssuesResponse?.state_distribution.cancelled - ? subIssuesResponse?.state_distribution.cancelled - : 0) - : 0; + const completedSubIssues = subIssuesResponse + ? subIssuesResponse?.state_distribution.completed + + subIssuesResponse?.state_distribution.cancelled + : 0; const totalSubIssues = subIssuesResponse && subIssuesResponse.sub_issues ? subIssuesResponse?.sub_issues.length : 0; @@ -278,7 +273,7 @@ export const SubIssuesList: FC = ({ parentIssue }) => { key={issue.id} href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`} > - +
= [ { name: "Manual", key: "sort_order" }, { name: "Last created", key: "-created_at" }, - { name: "Last updated", key: "updated_at" }, + { name: "Last updated", key: "-updated_at" }, { name: "Priority", key: "priority" }, ]; diff --git a/apps/app/constants/seo-variables.ts b/apps/app/constants/seo-variables.ts index 2b2d876d2..aafd5f7a3 100644 --- a/apps/app/constants/seo-variables.ts +++ b/apps/app/constants/seo-variables.ts @@ -1,8 +1,8 @@ -export const SITE_NAME = "Plane"; -export const SITE_TITLE = "Plane | Accelerate software development with peace."; +export const SITE_NAME = "Plane | Simple, extensible, open-source project management tool."; +export const SITE_TITLE = "Plane | Simple, extensible, open-source project management tool."; export const SITE_DESCRIPTION = - "Plane accelerated the software development by order of magnitude for agencies and product companies."; + "Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind."; export const SITE_KEYWORDS = - "software development, plan, ship, software, accelerate, code management, release management"; -export const SITE_URL = "http://localhost:3000/"; -export const TWITTER_USER_NAME = "caravel"; + "software development, plan, ship, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration"; +export const SITE_URL = "https://app.plane.so/"; +export const TWITTER_USER_NAME = "Plane | Simple, extensible, open-source project management tool."; diff --git a/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx b/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx index eec8a8f2e..e19ea6b70 100644 --- a/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx +++ b/apps/app/layouts/auth-layout/project-authorization-wrapper.tsx @@ -5,10 +5,7 @@ import { useRouter } from "next/router"; // contexts import { useProjectMyMembership, ProjectMemberProvider } from "contexts/project-member.context"; -// hooks -import useIssuesView from "hooks/use-issues-view"; // layouts -import Container from "layouts/container"; import AppHeader from "layouts/app-layout/app-header"; import AppSidebar from "layouts/app-layout/app-sidebar"; // components @@ -19,15 +16,7 @@ import { PrimaryButton, Spinner } from "components/ui"; // icons import { LayerDiagonalIcon } from "components/icons"; -type Meta = { - title?: string | null; - description?: string | null; - image?: string | null; - url?: string | null; -}; - type Props = { - meta?: Meta; children: React.ReactNode; noHeader?: boolean; bg?: "primary" | "secondary"; @@ -43,7 +32,6 @@ export const ProjectAuthorizationWrapper: React.FC = (props) => ( ); const ProjectAuthorizationWrapped: React.FC = ({ - meta, children, noHeader = false, bg = "primary", @@ -61,7 +49,7 @@ const ProjectAuthorizationWrapped: React.FC = ({ const settingsLayout = router.pathname.includes("/settings"); return ( - + <>
@@ -126,6 +114,6 @@ const ProjectAuthorizationWrapped: React.FC = ({ )}
-
+ ); }; diff --git a/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx b/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx index 4bc5156b7..e67424758 100644 --- a/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx +++ b/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx @@ -8,7 +8,6 @@ import useSWR from "swr"; // services import workspaceServices from "services/workspace.service"; // layouts -import Container from "layouts/container"; import AppSidebar from "layouts/app-layout/app-sidebar"; import AppHeader from "layouts/app-layout/app-header"; import { UserAuthorizationLayout } from "./user-authorization-wrapper"; @@ -21,15 +20,7 @@ import { LayerDiagonalIcon } from "components/icons"; // fetch-keys import { WORKSPACE_MEMBERS_ME } from "constants/fetch-keys"; -type Meta = { - title?: string | null; - description?: string | null; - image?: string | null; - url?: string | null; -}; - type Props = { - meta?: Meta; children: React.ReactNode; noHeader?: boolean; bg?: "primary" | "secondary"; @@ -39,7 +30,6 @@ type Props = { }; export const WorkspaceAuthorizationLayout: React.FC = ({ - meta, children, noHeader = false, bg = "primary", @@ -88,50 +78,48 @@ 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 && ( + + )} +
+
+ {children}
-
- )} -
- +
+
+ )} +
); }; diff --git a/apps/app/layouts/container.tsx b/apps/app/layouts/container.tsx deleted file mode 100644 index 5adf9430b..000000000 --- a/apps/app/layouts/container.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from "react"; -// next -import Head from "next/head"; -import { useRouter } from "next/router"; -// constants -import { - SITE_NAME, - SITE_DESCRIPTION, - SITE_URL, - TWITTER_USER_NAME, - SITE_KEYWORDS, - SITE_TITLE, -} from "constants/seo-variables"; - -type Meta = { - title?: string | null; - description?: string | null; - image?: string | null; - url?: string | null; -}; - -type Props = { - meta?: Meta; - children: React.ReactNode; - noPadding?: boolean; - bg?: "primary" | "secondary"; - noHeader?: boolean; - breadcrumbs?: JSX.Element; - left?: JSX.Element; - right?: JSX.Element; -}; - -const Container = ({ meta, children }: Props) => { - const router = useRouter(); - const image = meta?.image || "/site-image.png"; - const title = meta?.title || SITE_TITLE; - const url = meta?.url || `${SITE_URL}${router.asPath}`; - const description = meta?.description || SITE_DESCRIPTION; - - return ( - <> - - {title} - - - - - - - - - - - - - - {image && ( - - )} - - {children} - - ); -}; - -export default Container; diff --git a/apps/app/layouts/default-layout/index.tsx b/apps/app/layouts/default-layout/index.tsx index b615ab548..cce155906 100644 --- a/apps/app/layouts/default-layout/index.tsx +++ b/apps/app/layouts/default-layout/index.tsx @@ -1,15 +1,4 @@ -// layouts -import Container from "layouts/container"; - -type Meta = { - title?: string | null; - description?: string | null; - image?: string | null; - url?: string | null; -}; - type Props = { - meta?: Meta; children: React.ReactNode; noPadding?: boolean; bg?: "primary" | "secondary"; @@ -19,12 +8,10 @@ type Props = { right?: JSX.Element; }; -const DefaultLayout: React.FC = ({ meta, children }) => ( - -
- <>{children} -
-
+const DefaultLayout: React.FC = ({ children }) => ( +
+ <>{children} +
); export default DefaultLayout; diff --git a/apps/app/pages/404.tsx b/apps/app/pages/404.tsx index 2a136a164..a70bc2345 100644 --- a/apps/app/pages/404.tsx +++ b/apps/app/pages/404.tsx @@ -13,12 +13,7 @@ import Image404 from "public/404.svg"; import type { NextPage } from "next"; const PageNotFound: NextPage = () => ( - +
diff --git a/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx b/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx index 64e300ce0..db9b1e55b 100644 --- a/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx +++ b/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx @@ -18,9 +18,6 @@ const ProfileActivity = () => { return ( diff --git a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx index b6c3d36b9..0d1782563 100644 --- a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx +++ b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx @@ -123,9 +123,6 @@ const Profile: NextPage = () => { return ( diff --git a/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx b/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx index 5ea79014d..6b48d9aa3 100644 --- a/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx +++ b/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx @@ -29,9 +29,6 @@ const ProfilePreferences = () => { return ( diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 4573cd53c..0e56ad57e 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -89,9 +89,6 @@ const ProjectCycles: NextPage = () => { return ( diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx index 3fa24e332..7e2cd1565 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx @@ -66,9 +66,6 @@ const ProjectModules: NextPage = () => { return ( diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index d70559d3f..61b49a049 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -290,9 +290,6 @@ const SinglePage: NextPage = () => { return ( diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index 0d5d1252a..dba6cce54 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -100,9 +100,6 @@ const ProjectPages: NextPage = () => { handleClose={() => setCreateUpdatePageModal(false)} /> diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx index c796525f6..6d2798d87 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx @@ -60,9 +60,6 @@ const ProjectViews: NextPage = () => { return ( diff --git a/apps/app/pages/[workspaceSlug]/settings/index.tsx b/apps/app/pages/[workspaceSlug]/settings/index.tsx index 8ebf74aa3..0913b28ab 100644 --- a/apps/app/pages/[workspaceSlug]/settings/index.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/index.tsx @@ -146,9 +146,6 @@ const WorkspaceSettings: NextPage = () => { return ( diff --git a/apps/app/pages/_app.tsx b/apps/app/pages/_app.tsx index 0f50b3b8c..61788592a 100644 --- a/apps/app/pages/_app.tsx +++ b/apps/app/pages/_app.tsx @@ -10,8 +10,8 @@ import "styles/command-pallette.css"; import "styles/nprogress.css"; import "styles/react-datepicker.css"; -// router import Router from "next/router"; +import Head from "next/head"; // nprogress import NProgress from "nprogress"; @@ -24,6 +24,15 @@ import { ThemeContextProvider } from "contexts/theme.context"; import type { AppProps } from "next/app"; // constants import { THEMES } from "constants/themes"; +// constants +import { + SITE_NAME, + SITE_DESCRIPTION, + SITE_URL, + TWITTER_USER_NAME, + SITE_KEYWORDS, + SITE_TITLE, +} from "constants/seo-variables"; const CrispWithNoSSR = dynamic(() => import("constants/crisp"), { ssr: false }); @@ -39,7 +48,22 @@ function MyApp({ Component, pageProps }: AppProps) { - + {" "} + + {SITE_TITLE} + + + + + + + + + + + + + diff --git a/apps/app/pages/_error.js b/apps/app/pages/_error.tsx similarity index 70% rename from apps/app/pages/_error.js rename to apps/app/pages/_error.tsx index a615a24dc..0d1605484 100644 --- a/apps/app/pages/_error.js +++ b/apps/app/pages/_error.tsx @@ -1,21 +1,3 @@ -/** - * NOTE: This requires `@sentry/nextjs` version 7.3.0 or higher. - * - * NOTE: If using this with `next` version 12.2.0 or lower, uncomment the - * penultimate line in `CustomErrorComponent`. - * - * This page is loaded by Nextjs: - * - on the server, when data-fetching methods throw or reject - * - on the client, when `getInitialProps` throws or rejects - * - on the client, when a React lifecycle method throws or rejects, and it's - * caught by the built-in Nextjs error boundary - * - * See: - * - https://nextjs.org/docs/basic-features/data-fetching/overview - * - https://nextjs.org/docs/api-reference/data-fetching/get-initial-props - * - https://reactjs.org/docs/error-boundaries.html - */ - import * as Sentry from "@sentry/nextjs"; import { useRouter } from "next/router"; @@ -48,12 +30,7 @@ const CustomErrorComponent = () => { }; return ( - +
@@ -91,9 +68,7 @@ const CustomErrorComponent = () => { ); }; -CustomErrorComponent.getInitialProps = async (contextData) => { - // In case this is running in a serverless function, await this in order to give Sentry - // time to send the error before the lambda exits +CustomErrorComponent.getInitialProps = async (contextData: any) => { await Sentry.captureUnderscoreErrorException(contextData); const { res, err } = contextData; diff --git a/apps/app/pages/error.tsx b/apps/app/pages/error.tsx index 3909a1268..19663e68e 100644 --- a/apps/app/pages/error.tsx +++ b/apps/app/pages/error.tsx @@ -6,12 +6,7 @@ import DefaultLayout from "layouts/default-layout"; import type { NextPage } from "next"; const ErrorPage: NextPage = () => ( - +

Error!

diff --git a/apps/app/pages/invitations.tsx b/apps/app/pages/invitations.tsx index a288f87fb..21f6e25a7 100644 --- a/apps/app/pages/invitations.tsx +++ b/apps/app/pages/invitations.tsx @@ -80,13 +80,7 @@ const OnBoard: NextPage = () => { return ( - +
{user && (
diff --git a/apps/app/pages/magic-sign-in.tsx b/apps/app/pages/magic-sign-in.tsx index 521373685..f7308d1d2 100644 --- a/apps/app/pages/magic-sign-in.tsx +++ b/apps/app/pages/magic-sign-in.tsx @@ -48,11 +48,7 @@ const MagicSignIn: NextPage = () => { }, [password, key, mutateUser, router]); return ( - +
{isSigningIn ? (
diff --git a/apps/app/pages/signin.tsx b/apps/app/pages/signin.tsx index ee2469b86..26d6832f1 100644 --- a/apps/app/pages/signin.tsx +++ b/apps/app/pages/signin.tsx @@ -90,11 +90,7 @@ const SignInPage: NextPage = () => { ); return ( - + {isLoading ? (

Signing in. Please wait...

diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts index 5caabd131..e06d81169 100644 --- a/apps/app/types/issues.d.ts +++ b/apps/app/types/issues.d.ts @@ -267,7 +267,7 @@ export type TIssueViewOptions = "list" | "kanban" | "calendar" | "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"; export interface IIssueViewOptions { group_by: TIssueGroupByOptions; From c8caa925b19078ee3d5db414affd976a00b073cc Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 29 May 2023 18:35:16 +0530 Subject: [PATCH 32/65] chore: page and cycle refactor (#1159) * fix: release note modal fix * chore: cycle and page endpoint refactor --- .../components/cycles/cycles-list/all-cycles-list.tsx | 4 +--- .../cycles/cycles-list/completed-cycles-list.tsx | 8 +++++--- .../cycles/cycles-list/draft-cycles-list.tsx | 4 +--- .../cycles/cycles-list/upcoming-cycles-list.tsx | 8 +++++--- apps/app/components/cycles/select.tsx | 5 +---- apps/app/components/cycles/transfer-issues-modal.tsx | 8 +++++--- apps/app/components/issues/sidebar-select/cycle.tsx | 8 +++++--- .../app/components/pages/pages-list/all-pages-list.tsx | 5 +---- .../pages/pages-list/favorite-pages-list.tsx | 4 +--- apps/app/components/pages/pages-list/my-pages-list.tsx | 8 +++++--- .../components/pages/pages-list/other-pages-list.tsx | 8 +++++--- apps/app/pages/[workspaceSlug]/index.tsx | 10 ++++++---- .../projects/[projectId]/cycles/[cycleId].tsx | 5 +---- .../projects/[projectId]/cycles/index.tsx | 4 +--- apps/app/services/cycles.service.ts | 6 ++++-- apps/app/services/pages.service.ts | 6 ++++-- 16 files changed, 51 insertions(+), 50 deletions(-) diff --git a/apps/app/components/cycles/cycles-list/all-cycles-list.tsx b/apps/app/components/cycles/cycles-list/all-cycles-list.tsx index 11dc08b3e..7ebd92a50 100644 --- a/apps/app/components/cycles/cycles-list/all-cycles-list.tsx +++ b/apps/app/components/cycles/cycles-list/all-cycles-list.tsx @@ -21,9 +21,7 @@ export const AllCyclesList: React.FC = ({ viewType }) => { workspaceSlug && projectId ? CYCLES_LIST(projectId.toString()) : null, workspaceSlug && projectId ? () => - cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), { - cycle_view: "all", - }) + cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), "all") : null ); diff --git a/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx b/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx index bcbb11d4a..79a427d95 100644 --- a/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx +++ b/apps/app/components/cycles/cycles-list/completed-cycles-list.tsx @@ -21,9 +21,11 @@ export const CompletedCyclesList: React.FC = ({ viewType }) => { workspaceSlug && projectId ? COMPLETED_CYCLES_LIST(projectId.toString()) : null, workspaceSlug && projectId ? () => - cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), { - cycle_view: "completed", - }) + cyclesService.getCyclesWithParams( + workspaceSlug.toString(), + projectId.toString(), + "completed" + ) : null ); diff --git a/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx b/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx index e64e49dfd..fd2dccc93 100644 --- a/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx +++ b/apps/app/components/cycles/cycles-list/draft-cycles-list.tsx @@ -21,9 +21,7 @@ export const DraftCyclesList: React.FC = ({ viewType }) => { workspaceSlug && projectId ? DRAFT_CYCLES_LIST(projectId.toString()) : null, workspaceSlug && projectId ? () => - cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), { - cycle_view: "draft", - }) + cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), "draft") : null ); diff --git a/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx b/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx index 44023e618..140727cb8 100644 --- a/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx +++ b/apps/app/components/cycles/cycles-list/upcoming-cycles-list.tsx @@ -21,9 +21,11 @@ export const UpcomingCyclesList: React.FC = ({ viewType }) => { workspaceSlug && projectId ? UPCOMING_CYCLES_LIST(projectId.toString()) : null, workspaceSlug && projectId ? () => - cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), { - cycle_view: "upcoming", - }) + cyclesService.getCyclesWithParams( + workspaceSlug.toString(), + projectId.toString(), + "upcoming" + ) : null ); diff --git a/apps/app/components/cycles/select.tsx b/apps/app/components/cycles/select.tsx index 5a6f00ada..b5ec8a462 100644 --- a/apps/app/components/cycles/select.tsx +++ b/apps/app/components/cycles/select.tsx @@ -38,10 +38,7 @@ export const CycleSelect: React.FC = ({ const { data: cycles } = useSWR( workspaceSlug && projectId ? CYCLES_LIST(projectId) : null, workspaceSlug && projectId - ? () => - cycleServices.getCyclesWithParams(workspaceSlug as string, projectId as string, { - cycle_view: "all", - }) + ? () => cycleServices.getCyclesWithParams(workspaceSlug as string, projectId as string, "all") : null ); diff --git a/apps/app/components/cycles/transfer-issues-modal.tsx b/apps/app/components/cycles/transfer-issues-modal.tsx index 30db2f320..6eea69342 100644 --- a/apps/app/components/cycles/transfer-issues-modal.tsx +++ b/apps/app/components/cycles/transfer-issues-modal.tsx @@ -60,9 +60,11 @@ export const TransferIssuesModal: React.FC = ({ isOpen, handleClose }) => workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null, workspaceSlug && projectId ? () => - cyclesService.getCyclesWithParams(workspaceSlug as string, projectId as string, { - cycle_view: "incomplete", - }) + cyclesService.getCyclesWithParams( + workspaceSlug as string, + projectId as string, + "incomplete" + ) : null ); diff --git a/apps/app/components/issues/sidebar-select/cycle.tsx b/apps/app/components/issues/sidebar-select/cycle.tsx index ad112ab52..0d8beaab8 100644 --- a/apps/app/components/issues/sidebar-select/cycle.tsx +++ b/apps/app/components/issues/sidebar-select/cycle.tsx @@ -36,9 +36,11 @@ export const SidebarCycleSelect: React.FC = ({ workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null, workspaceSlug && projectId ? () => - cyclesService.getCyclesWithParams(workspaceSlug as string, projectId as string, { - cycle_view: "incomplete", - }) + cyclesService.getCyclesWithParams( + workspaceSlug as string, + projectId as string, + "incomplete" + ) : null ); diff --git a/apps/app/components/pages/pages-list/all-pages-list.tsx b/apps/app/components/pages/pages-list/all-pages-list.tsx index 67f6135ed..f25d00fd5 100644 --- a/apps/app/components/pages/pages-list/all-pages-list.tsx +++ b/apps/app/components/pages/pages-list/all-pages-list.tsx @@ -18,10 +18,7 @@ export const AllPagesList: React.FC = ({ viewType }) => { const { data: pages } = useSWR( workspaceSlug && projectId ? ALL_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => - pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, { - page_view: "all", - }) + ? () => pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, "all") : null ); diff --git a/apps/app/components/pages/pages-list/favorite-pages-list.tsx b/apps/app/components/pages/pages-list/favorite-pages-list.tsx index 89ba6d293..2faa4bf72 100644 --- a/apps/app/components/pages/pages-list/favorite-pages-list.tsx +++ b/apps/app/components/pages/pages-list/favorite-pages-list.tsx @@ -19,9 +19,7 @@ export const FavoritePagesList: React.FC = ({ viewType }) => { workspaceSlug && projectId ? FAVORITE_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId ? () => - pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, { - page_view: "favorite", - }) + pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, "favorite") : null ); diff --git a/apps/app/components/pages/pages-list/my-pages-list.tsx b/apps/app/components/pages/pages-list/my-pages-list.tsx index d821eb655..c225a0ac5 100644 --- a/apps/app/components/pages/pages-list/my-pages-list.tsx +++ b/apps/app/components/pages/pages-list/my-pages-list.tsx @@ -19,9 +19,11 @@ export const MyPagesList: React.FC = ({ viewType }) => { workspaceSlug && projectId ? MY_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId ? () => - pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, { - page_view: "created_by_me", - }) + pagesService.getPagesWithParams( + workspaceSlug as string, + projectId as string, + "created_by_me" + ) : null ); diff --git a/apps/app/components/pages/pages-list/other-pages-list.tsx b/apps/app/components/pages/pages-list/other-pages-list.tsx index dc476408e..64764533f 100644 --- a/apps/app/components/pages/pages-list/other-pages-list.tsx +++ b/apps/app/components/pages/pages-list/other-pages-list.tsx @@ -19,9 +19,11 @@ export const OtherPagesList: React.FC = ({ viewType }) => { workspaceSlug && projectId ? OTHER_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId ? () => - pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, { - page_view: "created_by_other", - }) + pagesService.getPagesWithParams( + workspaceSlug as string, + projectId as string, + "created_by_other" + ) : null ); diff --git a/apps/app/pages/[workspaceSlug]/index.tsx b/apps/app/pages/[workspaceSlug]/index.tsx index 6d8f3f9ed..233fd9b8a 100644 --- a/apps/app/pages/[workspaceSlug]/index.tsx +++ b/apps/app/pages/[workspaceSlug]/index.tsx @@ -41,10 +41,12 @@ const WorkspacePage: NextPage = () => { return ( - + {isProductUpdatesModalOpen && ( + + )}
diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 08c04a736..087747153 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -56,10 +56,7 @@ const SingleCycle: React.FC = () => { const { data: cycles } = useSWR( workspaceSlug && projectId ? CYCLES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => - cycleServices.getCyclesWithParams(workspaceSlug as string, projectId as string, { - cycle_view: "all", - }) + ? () => cycleServices.getCyclesWithParams(workspaceSlug as string, projectId as string, "all") : null ); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 0e56ad57e..bac59acc9 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -73,9 +73,7 @@ const ProjectCycles: NextPage = () => { workspaceSlug && projectId ? CURRENT_CYCLE_LIST(projectId as string) : null, workspaceSlug && projectId ? () => - cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, { - cycle_view: "current", - }) + cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, "current") : null ); diff --git a/apps/app/services/cycles.service.ts b/apps/app/services/cycles.service.ts index 4d443a110..d2cb4f5f3 100644 --- a/apps/app/services/cycles.service.ts +++ b/apps/app/services/cycles.service.ts @@ -29,10 +29,12 @@ class ProjectCycleServices extends APIService { async getCyclesWithParams( workspaceSlug: string, projectId: string, - queries: any + cycleType: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete" ): Promise { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, { - params: queries, + params: { + cycle_view: cycleType, + }, }) .then((response) => response?.data) .catch((error) => { diff --git a/apps/app/services/pages.service.ts b/apps/app/services/pages.service.ts index 72776c29e..5805ac538 100644 --- a/apps/app/services/pages.service.ts +++ b/apps/app/services/pages.service.ts @@ -86,10 +86,12 @@ class PageServices extends APIService { async getPagesWithParams( workspaceSlug: string, projectId: string, - queries: any + pageType: "all" | "favorite" | "created_by_me" | "created_by_other" ): Promise { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, { - params: queries, + params: { + page_view: pageType, + }, }) .then((response) => response?.data) .catch((error) => { From c949c4d2440341d1a1af1742c49bb0fc160a65cd Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 29 May 2023 18:35:43 +0530 Subject: [PATCH 33/65] fix: empty module mutation and issue view flicker fix (#1158) --- apps/app/components/issues/modal.tsx | 2 +- apps/app/contexts/issue-view.context.tsx | 13 +++++ .../[projectId]/modules/[moduleId].tsx | 50 ++++--------------- 3 files changed, 23 insertions(+), 42 deletions(-) diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx index de54e65b3..3be8e58ae 100644 --- a/apps/app/components/issues/modal.tsx +++ b/apps/app/components/issues/modal.tsx @@ -186,7 +186,7 @@ export const CreateUpdateIssueModal: React.FC = ({ mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); } else { if (issueView === "calendar") mutate(calendarFetchKey); - else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); + mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); } if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle); diff --git a/apps/app/contexts/issue-view.context.tsx b/apps/app/contexts/issue-view.context.tsx index ce24f5048..aa3ec586c 100644 --- a/apps/app/contexts/issue-view.context.tsx +++ b/apps/app/contexts/issue-view.context.tsx @@ -190,6 +190,19 @@ export const reducer: ReducerFunctionType = (state, action) => { }; const saveDataToServer = async (workspaceSlug: string, projectID: string, state: any) => { + mutate( + workspaceSlug && projectID ? USER_PROJECT_VIEW(projectID as string) : null, + (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + view_props: state, + }; + }, + false + ); + await projectService.setProjectView(workspaceSlug, projectID, { view_props: state, }); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index 41e566d90..f9df68e7e 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -172,47 +172,15 @@ const SingleModule: React.FC = () => { } > setAnalyticsModal(false)} /> - {moduleIssues ? ( - moduleIssues.length > 0 ? ( -
- -
- ) : ( -
- - { - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); - }} - /> - - -
- ) - ) : ( -
- -
- )} + +
+ +
+ Date: Mon, 29 May 2023 22:16:32 +0530 Subject: [PATCH 34/65] fix: default status set for module form (#1160) --- apps/app/components/modules/form.tsx | 2 +- apps/app/components/modules/modal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/app/components/modules/form.tsx b/apps/app/components/modules/form.tsx index d79748478..e94bc6d72 100644 --- a/apps/app/components/modules/form.tsx +++ b/apps/app/components/modules/form.tsx @@ -23,7 +23,7 @@ type Props = { const defaultValues: Partial = { name: "", description: "", - status: null, + status: "backlog", lead: null, members_list: [], }; diff --git a/apps/app/components/modules/modal.tsx b/apps/app/components/modules/modal.tsx index 02e23e138..69dcba34a 100644 --- a/apps/app/components/modules/modal.tsx +++ b/apps/app/components/modules/modal.tsx @@ -28,7 +28,7 @@ type Props = { const defaultValues: Partial = { name: "", description: "", - status: null, + status: "backlog", lead: null, members_list: [], }; From 44f8ba407dacaad895882715cd576a427dc0d625 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Tue, 30 May 2023 09:44:35 -0400 Subject: [PATCH 35/65] Authentication Workflow fixes. Redirection fixes (#832) * auth integration fixes * auth integration fixes * auth integration fixes * auth integration fixes * dev: update user api to return fallback workspace and improve the structure of the response * dev: fix the issue keyerror and move onboarding logic to serializer method field * dev: use-user-auth hook imlemented for route access validation and build issues resolved effected by user payload * fix: global theme color fix * style: new onboarding ui , fix: use-user-auth hook implemented * fix: command palette, project invite modal and issue detail page mutation type fix * fix: onboarding redirection fix * dev: build isuue resolved * fix: use user auth hook fix * fix: sign in toast alert fix, sign out redirection fix and user theme error fix * fix: user response fix * fix: unAuthorizedStatus logic updated --------- Co-authored-by: pablohashescobar Co-authored-by: gurusainath Co-authored-by: anmolsinghbhatia --- apiserver/plane/api/serializers/user.py | 4 + apiserver/plane/api/views/people.py | 51 +++-- .../components/account/email-code-form.tsx | 8 +- .../components/account/email-signin-form.tsx | 24 --- .../account/github-login-button.tsx | 2 +- apps/app/components/account/index.ts | 1 - .../auth-screens/not-authorized-view.tsx | 4 +- apps/app/components/breadcrumbs/index.tsx | 8 +- .../command-palette/change-issue-assignee.tsx | 15 +- .../command-palette/change-issue-priority.tsx | 14 +- .../command-palette/change-issue-state.tsx | 13 +- .../command-palette/command-pallette.tsx | 15 +- .../components/onboarding/invite-members.tsx | 3 +- .../components/onboarding/onboarding-card.tsx | 2 +- .../components/onboarding/user-details.tsx | 19 +- apps/app/components/onboarding/workspace.tsx | 167 +++++++++++----- .../project/send-project-invitation-modal.tsx | 7 +- apps/app/components/ui/icon.tsx | 12 ++ apps/app/components/ui/index.ts | 1 + .../workspace/create-workspace-form.tsx | 187 +++++++++--------- .../components/workspace/sidebar-dropdown.tsx | 26 +-- .../workspace/single-invitation.tsx | 34 ++-- apps/app/helpers/string.helper.ts | 9 + apps/app/hooks/use-user-auth.tsx | 107 ++++++++++ apps/app/hooks/use-user.tsx | 60 +++++- .../user-authorization-wrapper.tsx | 2 +- apps/app/lib/auth.ts | 2 +- apps/app/package.json | 2 +- .../[workspaceSlug]/me/profile/index.tsx | 4 +- .../me/profile/preferences.tsx | 4 +- .../projects/[projectId]/issues/[issueId].tsx | 13 +- apps/app/pages/_app.tsx | 33 +--- apps/app/pages/_error.tsx | 2 +- apps/app/pages/create-workspace.tsx | 40 +++- apps/app/pages/index.tsx | 152 +++++++++----- apps/app/pages/invitations.tsx | 158 ++++++++------- apps/app/pages/onboarding.tsx | 46 ++++- apps/app/pages/signin.tsx | 135 ------------- .../[invitationId].tsx | 4 +- apps/app/services/api.service.ts | 3 +- apps/app/styles/globals.css | 4 +- apps/app/types/users.d.ts | 5 +- yarn.lock | 12 +- 43 files changed, 821 insertions(+), 593 deletions(-) delete mode 100644 apps/app/components/account/email-signin-form.tsx create mode 100644 apps/app/components/ui/icon.tsx create mode 100644 apps/app/hooks/use-user-auth.tsx delete mode 100644 apps/app/pages/signin.tsx diff --git a/apiserver/plane/api/serializers/user.py b/apiserver/plane/api/serializers/user.py index 14a33d9c3..d8978479e 100644 --- a/apiserver/plane/api/serializers/user.py +++ b/apiserver/plane/api/serializers/user.py @@ -25,6 +25,10 @@ class UserSerializer(BaseSerializer): ] extra_kwargs = {"password": {"write_only": True}} + # If the user has already filled first name or last name then he is onboarded + def get_is_onboarded(self, obj): + return bool(obj.first_name) or bool(obj.last_name) + class UserLiteSerializer(BaseSerializer): class Meta: diff --git a/apiserver/plane/api/views/people.py b/apiserver/plane/api/views/people.py index 78ae5b2fc..fcf95ff64 100644 --- a/apiserver/plane/api/views/people.py +++ b/apiserver/plane/api/views/people.py @@ -31,36 +31,61 @@ class UserEndpoint(BaseViewSet): def retrieve(self, request): try: - workspace = Workspace.objects.get(pk=request.user.last_workspace_id) + workspace = Workspace.objects.get( + pk=request.user.last_workspace_id, workspace_member__member=request.user + ) workspace_invites = WorkspaceMemberInvite.objects.filter( email=request.user.email ).count() assigned_issues = Issue.objects.filter(assignees__in=[request.user]).count() + serialized_data = UserSerializer(request.user).data + serialized_data["workspace"] = { + "last_workspace_id": request.user.last_workspace_id, + "last_workspace_slug": workspace.slug, + "fallback_workspace_id": request.user.last_workspace_id, + "fallback_workspace_slug": workspace.slug, + "invites": workspace_invites, + } + serialized_data.setdefault("issues", {})["assigned_issues"] = assigned_issues + return Response( - { - "user": UserSerializer(request.user).data, - "slug": workspace.slug, - "workspace_invites": workspace_invites, - "assigned_issues": assigned_issues, - }, + serialized_data, status=status.HTTP_200_OK, ) except Workspace.DoesNotExist: + # This exception will be hit even when the `last_workspace_id` is None + workspace_invites = WorkspaceMemberInvite.objects.filter( email=request.user.email ).count() assigned_issues = Issue.objects.filter(assignees__in=[request.user]).count() + + fallback_workspace = Workspace.objects.filter( + workspace_member__member=request.user + ).order_by("created_at").first() + + serialized_data = UserSerializer(request.user).data + + serialized_data["workspace"] = { + "last_workspace_id": None, + "last_workspace_slug": None, + "fallback_workspace_id": fallback_workspace.id + if fallback_workspace is not None + else None, + "fallback_workspace_slug": fallback_workspace.slug + if fallback_workspace is not None + else None, + "invites": workspace_invites, + } + serialized_data.setdefault("issues", {})["assigned_issues"] = assigned_issues + return Response( - { - "user": UserSerializer(request.user).data, - "slug": None, - "workspace_invites": workspace_invites, - "assigned_issues": assigned_issues, - }, + serialized_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/apps/app/components/account/email-code-form.tsx b/apps/app/components/account/email-code-form.tsx index b4a3ef31e..dc1fc725e 100644 --- a/apps/app/components/account/email-code-form.tsx +++ b/apps/app/components/account/email-code-form.tsx @@ -66,8 +66,12 @@ export const EmailCodeForm = ({ onSuccess }: any) => { const handleSignin = async (formData: EmailCodeFormValues) => { await authenticationService .magicSignIn(formData) - .then((response) => { - onSuccess(response); + .then(() => { + setToastAlert({ + title: "Success", + type: "success", + message: "Successfully logged in!", + }); }) .catch((error) => { setToastAlert({ diff --git a/apps/app/components/account/email-signin-form.tsx b/apps/app/components/account/email-signin-form.tsx deleted file mode 100644 index e2f81d50c..000000000 --- a/apps/app/components/account/email-signin-form.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useState, FC } from "react"; -import { KeyIcon } from "@heroicons/react/24/outline"; -// components -import { EmailCodeForm, EmailPasswordForm } from "components/account"; - -export interface EmailSignInFormProps { - handleSuccess: () => void; -} - -export const EmailSignInForm: FC = (props) => { - const { handleSuccess } = props; - // states - const [useCode, setUseCode] = useState(true); - - return ( - <> - {useCode ? ( - - ) : ( - - )} - - ); -}; diff --git a/apps/app/components/account/github-login-button.tsx b/apps/app/components/account/github-login-button.tsx index 16b9743d5..764680d30 100644 --- a/apps/app/components/account/github-login-button.tsx +++ b/apps/app/components/account/github-login-button.tsx @@ -29,7 +29,7 @@ export const GithubLoginButton: FC = (props) => { useEffect(() => { const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - setLoginCallBackURL(`${origin}/signin` as any); + setLoginCallBackURL(`${origin}/` as any); }, []); return ( diff --git a/apps/app/components/account/index.ts b/apps/app/components/account/index.ts index b78e2e921..d0e5e4dfa 100644 --- a/apps/app/components/account/index.ts +++ b/apps/app/components/account/index.ts @@ -2,4 +2,3 @@ export * from "./google-login"; export * from "./email-code-form"; export * from "./email-password-form"; export * from "./github-login-button"; -export * from "./email-signin-form"; diff --git a/apps/app/components/auth-screens/not-authorized-view.tsx b/apps/app/components/auth-screens/not-authorized-view.tsx index 0ebc85fb4..d76e56b61 100644 --- a/apps/app/components/auth-screens/not-authorized-view.tsx +++ b/apps/app/components/auth-screens/not-authorized-view.tsx @@ -39,7 +39,7 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => { {user ? (

You have signed in as {user.email}.
- + Sign in {" "} with different account that has access to this page. @@ -47,7 +47,7 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => { ) : (

You need to{" "} - + Sign in {" "} with an account that has access to this page. diff --git a/apps/app/components/breadcrumbs/index.tsx b/apps/app/components/breadcrumbs/index.tsx index 98944fe37..240faefa2 100644 --- a/apps/app/components/breadcrumbs/index.tsx +++ b/apps/app/components/breadcrumbs/index.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import Link from "next/link"; // icons import { ArrowLeftIcon } from "@heroicons/react/24/outline"; +import { Icon } from "components/ui"; type BreadcrumbsProps = { children: any; @@ -16,10 +17,13 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => {

{children}
diff --git a/apps/app/components/command-palette/change-issue-assignee.tsx b/apps/app/components/command-palette/change-issue-assignee.tsx index 09f597e2e..fd853ef0f 100644 --- a/apps/app/components/command-palette/change-issue-assignee.tsx +++ b/apps/app/components/command-palette/change-issue-assignee.tsx @@ -57,12 +57,15 @@ export const ChangeIssueAssignee: React.FC = ({ setIsPaletteOpen, issue } async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueId) return; - mutate( + mutate( ISSUE_DETAILS(issueId as string), - (prevData: IIssue) => ({ - ...prevData, - ...formData, - }), + async (prevData) => { + if (!prevData) return prevData; + return { + ...prevData, + ...formData, + }; + }, false ); @@ -80,7 +83,7 @@ export const ChangeIssueAssignee: React.FC = ({ setIsPaletteOpen, issue } ); const handleIssueAssignees = (assignee: string) => { - const updatedAssignees = issue.assignees ?? []; + const updatedAssignees = issue.assignees_list ?? []; if (updatedAssignees.includes(assignee)) { updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); diff --git a/apps/app/components/command-palette/change-issue-priority.tsx b/apps/app/components/command-palette/change-issue-priority.tsx index b6eca1df8..d0e6258c5 100644 --- a/apps/app/components/command-palette/change-issue-priority.tsx +++ b/apps/app/components/command-palette/change-issue-priority.tsx @@ -27,12 +27,16 @@ export const ChangeIssuePriority: React.FC = ({ setIsPaletteOpen, issue } async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueId) return; - mutate( + mutate( ISSUE_DETAILS(issueId as string), - (prevData: IIssue) => ({ - ...prevData, - ...formData, - }), + async (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + ...formData, + }; + }, false ); diff --git a/apps/app/components/command-palette/change-issue-state.tsx b/apps/app/components/command-palette/change-issue-state.tsx index 30ab68b63..8e3255b0a 100644 --- a/apps/app/components/command-palette/change-issue-state.tsx +++ b/apps/app/components/command-palette/change-issue-state.tsx @@ -39,12 +39,15 @@ export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue }) = async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueId) return; - mutate( + mutate( ISSUE_DETAILS(issueId as string), - (prevData: IIssue) => ({ - ...prevData, - ...formData, - }), + async (prevData) => { + if (!prevData) return prevData; + return { + ...prevData, + ...formData, + }; + }, false ); diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx index bed84f5ad..06f4f3c2b 100644 --- a/apps/app/components/command-palette/command-pallette.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -120,12 +120,17 @@ export const CommandPalette: React.FC = () => { async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueId) return; - mutate( + mutate( ISSUE_DETAILS(issueId as string), - (prevData: IIssue) => ({ - ...prevData, - ...formData, - }), + + (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + ...formData, + }; + }, false ); diff --git a/apps/app/components/onboarding/invite-members.tsx b/apps/app/components/onboarding/invite-members.tsx index 4fa58db2e..e1ebdf117 100644 --- a/apps/app/components/onboarding/invite-members.tsx +++ b/apps/app/components/onboarding/invite-members.tsx @@ -45,8 +45,9 @@ export const InviteMembers: React.FC = ({ setStep, workspace }) => { >
-

Invite your team to your workspace.

+

Invite co-workers to your team

+ Email
= ({ data, gradient = false }) => (
diff --git a/apps/app/components/onboarding/user-details.tsx b/apps/app/components/onboarding/user-details.tsx index e8704df59..f4f94117d 100644 --- a/apps/app/components/onboarding/user-details.tsx +++ b/apps/app/components/onboarding/user-details.tsx @@ -66,10 +66,17 @@ export const UserDetails: React.FC = ({ user, setStep, setUserRole }) => return (
-
+
-
-
+
+

User Details

+

+ Enter your details as a first step to open your Plane account. +

+
+ +
+
First name = ({ user, setStep, setUserRole }) => error={errors.first_name} />
-
+
Last name = ({ user, setStep, setUserRole }) => />
-
+ +
What is your role?
= ({ user, setStep, setUserRole }) =>
+
>; @@ -30,6 +31,7 @@ export const Workspace: React.FC = ({ setStep, setWorkspace }) => { slug: "", company_size: null, }); + const [currentTab, setCurrentTab] = useState("create"); const { data: invitations, mutate } = useSWR(USER_WORKSPACE_INVITATIONS, () => workspaceService.userWorkspaceInvitations() @@ -64,53 +66,72 @@ export const Workspace: React.FC = ({ setStep, setWorkspace }) => { }); }; + const currentTabValue = (tab: string | null) => { + switch (tab) { + case "join": + return 0; + case "create": + return 1; + default: + return 1; + } + }; + + console.log("invitations:", invitations); + return ( -
+
{ + switch (i) { + case 0: + return setCurrentTab("join"); + case 1: + return setCurrentTab("create"); + default: + return setCurrentTab("create"); + } + }} > - - - `rounded-3xl border px-4 py-2 outline-none ${ - selected - ? "border-brand-accent bg-brand-accent text-white" - : "border-brand-base bg-brand-surface-2 hover:bg-brand-surface-1" - }` - } - > - New Workspace - - - `rounded-3xl border px-5 py-2 outline-none ${ - selected - ? "border-brand-accent bg-brand-accent text-white" - : "border-brand-base bg-brand-surface-2 hover:bg-brand-surface-1" - }` - } - > - Invited Workspace - + +
+

Workspaces

+

+ Create or join the workspace to get started with Plane. +

+
+
+ + `rounded-3xl border px-4 py-2 outline-none ${ + selected + ? "border-brand-accent bg-brand-accent text-white font-medium" + : "border-brand-base bg-brand-base hover:bg-brand-surface-2" + }` + } + > + Invited Workspace + + + `rounded-3xl border px-4 py-2 outline-none ${ + selected + ? "border-brand-accent bg-brand-accent text-white font-medium" + : "border-brand-base bg-brand-base hover:bg-brand-surface-2" + }` + } + > + New Workspace + +
- - { - setWorkspace(res); - setStep(3); - }} - defaultValues={defaultValues} - setDefaultValues={setDefaultValues} - /> - -
-
+
+
{invitations && invitations.length > 0 ? ( invitations.map((invitation) => (
@@ -129,34 +150,62 @@ export const Workspace: React.FC = ({ setStep, setWorkspace }) => { alt={invitation.workspace.name} /> ) : ( - - {invitation.workspace.name.charAt(0)} + + {getFirstCharacters(invitation.workspace.name)} )}
-
{invitation.workspace.name}
+
+ {truncateText(invitation.workspace.name, 30)} +

Invited by {invitation.workspace.owner.first_name}

- { + + {/* { + handleInvitation( + invitation, + invitationsRespond.includes(invitation.id) ? "withdraw" : "accepted" + ); + }} + type="button" + className={`${ + invitationsRespond.includes(invitation.id) + ? "bg-brand-surface-2 text-brand-secondary" + : "bg-brand-accent text-white" + } text-sm px-4 py-2 border border-brand-base rounded-3xl`} + + // className="h-4 w-4 rounded border-brand-base text-brand-accent focus:ring-brand-accent" + /> */}
@@ -167,7 +216,7 @@ export const Workspace: React.FC = ({ setStep, setWorkspace }) => {
)}
-
+
= ({ setStep, setWorkspace }) => {
+ + { + setWorkspace(res); + setStep(3); + }} + defaultValues={defaultValues} + setDefaultValues={setDefaultValues} + /> +
diff --git a/apps/app/components/project/send-project-invitation-modal.tsx b/apps/app/components/project/send-project-invitation-modal.tsx index cfc8cb99c..d21cf07f3 100644 --- a/apps/app/components/project/send-project-invitation-modal.tsx +++ b/apps/app/components/project/send-project-invitation-modal.tsx @@ -73,9 +73,12 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member .inviteProject(workspaceSlug as string, projectId as string, formData) .then((response) => { setIsOpen(false); - mutate( + mutate( PROJECT_INVITATIONS, - (prevData: any[]) => [{ ...formData, ...response }, ...(prevData ?? [])], + (prevData) => { + if (!prevData) return prevData; + return [{ ...formData, ...response }, ...(prevData ?? [])]; + }, false ); setToastAlert({ diff --git a/apps/app/components/ui/icon.tsx b/apps/app/components/ui/icon.tsx new file mode 100644 index 000000000..aefc24dff --- /dev/null +++ b/apps/app/components/ui/icon.tsx @@ -0,0 +1,12 @@ +import React from "react"; + +type Props = { + iconName: string; + className?: string; +}; + +export const Icon: React.FC = ({ iconName, className = "" }) => ( + + {iconName} + +); diff --git a/apps/app/components/ui/index.ts b/apps/app/components/ui/index.ts index 476f0af3e..6eb273c4a 100644 --- a/apps/app/components/ui/index.ts +++ b/apps/app/components/ui/index.ts @@ -25,3 +25,4 @@ export * from "./markdown-to-component"; export * from "./product-updates-modal"; export * from "./integration-and-import-export-banner"; export * from "./range-datepicker"; +export * from "./icon"; diff --git a/apps/app/components/workspace/create-workspace-form.tsx b/apps/app/components/workspace/create-workspace-form.tsx index 0b2b7d984..b3415b2c8 100644 --- a/apps/app/components/workspace/create-workspace-form.tsx +++ b/apps/app/components/workspace/create-workspace-form.tsx @@ -99,110 +99,105 @@ export const CreateWorkspaceForm: React.FC = ({ ); return ( - -
-
-
-
- Workspace name + +
+
+
+ Workspace name + + setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-")) + } + validations={{ + required: "Workspace name is required", + validate: (value) => + /^[\w\s-]*$/.test(value) || + `Name can only contain (" "), ( - ), ( _ ) & Alphanumeric characters.`, + }} + placeholder="e.g. My Workspace" + className="placeholder:text-brand-secondary" + error={errors.name} + /> +
+
+ Workspace URL +
+ + {typeof window !== "undefined" && window.location.origin}/ + - setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-")) - } + name="slug" + register={register} + className="block w-full rounded-md bg-transparent py-2 !px-0 text-sm" validations={{ - required: "Workspace name is required", - validate: (value) => - /^[\w\s-]*$/.test(value) || - `Name can only contain (" "), ( - ), ( _ ) & Alphanumeric characters.`, + required: "Workspace URL is required", }} - placeholder="e.g. My Workspace" - className="placeholder:text-brand-secondary" - error={errors.name} + onChange={(e) => + /^[a-zA-Z0-9_-]+$/.test(e.target.value) + ? setInvalidSlug(false) + : setInvalidSlug(true) + } />
-
- Workspace URL -
- - {typeof window !== "undefined" && window.location.origin}/ - - - /^[a-zA-Z0-9_-]+$/.test(e.target.value) - ? setInvalidSlug(false) - : setInvalidSlug(true) - } - /> -
- {slugError && ( - Workspace URL is already taken! - )} - {invalidSlug && ( - {`URL can only contain ( - ), ( _ ) & Alphanumeric characters.`} - )} -
-
- -
- How large is your company? -
- ( - Select company size - ) - } - input - width="w-full" - > - {COMPANY_SIZE?.map((item) => ( - - {item.label} - - ))} - - )} - /> - {errors.company_size && ( - {errors.company_size.message} - )} -
-
- -
- - {isSubmitting ? "Creating..." : "Create Workspace"} - + {slugError && ( + Workspace URL is already taken! + )} + {invalidSlug && ( + {`URL can only contain ( - ), ( _ ) & Alphanumeric characters.`} + )}
+ +
+ How large is your company? +
+ ( + Select company size + ) + } + input + width="w-full" + > + {COMPANY_SIZE?.map((item) => ( + + {item.label} + + ))} + + )} + /> + {errors.company_size && ( + {errors.company_size.message} + )} +
+
+
+ +
+ + {isSubmitting ? "Creating..." : "Create Workspace"} +
); diff --git a/apps/app/components/workspace/sidebar-dropdown.tsx b/apps/app/components/workspace/sidebar-dropdown.tsx index e4d3db7c5..680b688ed 100644 --- a/apps/app/components/workspace/sidebar-dropdown.tsx +++ b/apps/app/components/workspace/sidebar-dropdown.tsx @@ -67,17 +67,19 @@ export const WorkspaceSidebarDropdown = () => { }; const handleSignOut = async () => { - router.push("/signin").then(() => { - mutateUser(); - }); - - await authenticationService.signOut().catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Failed to sign out. Please try again.", + await authenticationService + .signOut() + .then(() => { + mutateUser(undefined); + router.push("/"); }) - ); + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Failed to sign out. Please try again.", + }) + ); }; return ( @@ -137,8 +139,8 @@ export const WorkspaceSidebarDropdown = () => { border border-brand-base bg-brand-surface-2 shadow-lg focus:outline-none" >
-
{user?.email}
- Workspace +
{user?.email}
+ Workspace {workspaces ? (
{workspaces.length > 0 ? ( diff --git a/apps/app/components/workspace/single-invitation.tsx b/apps/app/components/workspace/single-invitation.tsx index e9092c11d..9dbdc62ce 100644 --- a/apps/app/components/workspace/single-invitation.tsx +++ b/apps/app/components/workspace/single-invitation.tsx @@ -1,4 +1,5 @@ // next +import { getFirstCharacters, truncateText } from "helpers/string.helper"; import Image from "next/image"; // react import { useState } from "react"; @@ -22,9 +23,7 @@ const SingleInvitation: React.FC = ({ <>
  • diff --git a/apps/app/helpers/string.helper.ts b/apps/app/helpers/string.helper.ts index a13f149fc..4dfc9855b 100644 --- a/apps/app/helpers/string.helper.ts +++ b/apps/app/helpers/string.helper.ts @@ -109,3 +109,12 @@ export const generateRandomColor = (string: string): string => { return randomColor; }; + +export const getFirstCharacters = (str: string) => { + const words = str.trim().split(" "); + if (words.length === 1) { + return words[0].charAt(0); + } else { + return words[0].charAt(0) + words[1].charAt(0); + } +}; diff --git a/apps/app/hooks/use-user-auth.tsx b/apps/app/hooks/use-user-auth.tsx new file mode 100644 index 000000000..9dba5ce61 --- /dev/null +++ b/apps/app/hooks/use-user-auth.tsx @@ -0,0 +1,107 @@ +import { useEffect, useState } from "react"; +// next imports +import { useRouter } from "next/router"; +// swr +import useSWR from "swr"; +// keys +import { CURRENT_USER } from "constants/fetch-keys"; +// services +import userService from "services/user.service"; +import workspaceService from "services/workspace.service"; +// types +import type { IWorkspace, ICurrentUserResponse } from "types"; + +const useUserAuth = (routeAuth: "sign-in" | "onboarding" | "admin" | null = "admin") => { + const router = useRouter(); + const [isRouteAccess, setIsRouteAccess] = useState(true); + + const { + data: user, + isLoading, + error, + mutate, + } = useSWR(CURRENT_USER, () => userService.currentUser()); + + useEffect(() => { + const handleWorkSpaceRedirection = async () => { + workspaceService.userWorkspaces().then(async (userWorkspaces) => { + const lastActiveWorkspace = userWorkspaces.find( + (workspace: IWorkspace) => workspace.id === user?.last_workspace_id + ); + if (lastActiveWorkspace) { + router.push(`/${lastActiveWorkspace.slug}`); + return; + } else if (userWorkspaces.length > 0) { + router.push(`/${userWorkspaces[0].slug}`); + return; + } else { + const invitations = await workspaceService.userWorkspaceInvitations(); + if (invitations.length > 0) { + router.push(`/invitations`); + return; + } else { + router.push(`/create-workspace`); + return; + } + } + }); + }; + + const handleUserRouteAuthentication = async () => { + console.log("user", user); + + if (user && user.is_active) { + if (routeAuth === "sign-in") { + if (user.is_onboarded) handleWorkSpaceRedirection(); + else { + router.push("/onboarding"); + return; + } + } else if (routeAuth === "onboarding") { + if (user.is_onboarded) handleWorkSpaceRedirection(); + else { + setIsRouteAccess(() => false); + return; + } + } else { + if (!user.is_onboarded) { + router.push("/onboarding"); + return; + } else { + setIsRouteAccess(() => false); + return; + } + } + } else { + // user is not active and we can redirect to no access page + // router.push("/no-access"); + // remove token + return; + } + }; + + if (!isLoading) { + setIsRouteAccess(() => true); + if (user) handleUserRouteAuthentication(); + else { + if (routeAuth === "sign-in") { + setIsRouteAccess(() => false); + return; + } else { + router.push("/"); + return; + } + } + } + }, [user, isLoading, routeAuth, router]); + + return { + isLoading: isRouteAccess, + user: error ? undefined : user, + mutateUser: mutate, + assignedIssuesLength: user?.assigned_issues ?? 0, + workspaceInvitesLength: user?.workspace_invites ?? 0, + }; +}; + +export default useUserAuth; diff --git a/apps/app/hooks/use-user.tsx b/apps/app/hooks/use-user.tsx index e59cb32e5..f5186a273 100644 --- a/apps/app/hooks/use-user.tsx +++ b/apps/app/hooks/use-user.tsx @@ -1,13 +1,55 @@ -import { useContext } from "react"; +import { useEffect } from "react"; +import { useRouter } from "next/router"; +import useSWR from "swr"; +// services +import userService from "services/user.service"; +// constants +import { CURRENT_USER } from "constants/fetch-keys"; +// types +import type { ICurrentUserResponse, IUser } from "types"; -// context -import { UserContext } from "contexts/user.context"; +export default function useUser({ redirectTo = "", redirectIfFound = false, options = {} } = {}) { + const router = useRouter(); + // API to fetch user information + const { data, isLoading, error, mutate } = useSWR( + CURRENT_USER, + () => userService.currentUser(), + options + ); -const useUser = () => { - // context - const contextData = useContext(UserContext); + const user = error ? undefined : data; + // console.log("useUser", user); - return { ...contextData }; -}; + useEffect(() => { + // if no redirect needed, just return (example: already on /dashboard) + // if user data not yet there (fetch in progress, logged in or not) then don't do anything yet + if (!redirectTo || !user) return; -export default useUser; + if ( + // If redirectTo is set, redirect if the user was not found. + (redirectTo && !redirectIfFound) || + // If redirectIfFound is also set, redirect if the user was found + (redirectIfFound && user) + ) { + router.push(redirectTo); + return; + // const nextLocation = router.asPath.split("?next=")[1]; + // if (nextLocation) { + // router.push(nextLocation as string); + // return; + // } else { + // router.push("/"); + // return; + // } + } + }, [user, redirectIfFound, redirectTo, router]); + + return { + user, + isUserLoading: isLoading, + mutateUser: mutate, + userError: error, + assignedIssuesLength: user?.assigned_issues ?? 0, + workspaceInvitesLength: user?.workspace_invites ?? 0, + }; +} diff --git a/apps/app/layouts/auth-layout/user-authorization-wrapper.tsx b/apps/app/layouts/auth-layout/user-authorization-wrapper.tsx index d11bb2ac1..f48403b0f 100644 --- a/apps/app/layouts/auth-layout/user-authorization-wrapper.tsx +++ b/apps/app/layouts/auth-layout/user-authorization-wrapper.tsx @@ -32,7 +32,7 @@ export const UserAuthorizationLayout: React.FC = ({ children }) => { if (error) { const redirectTo = router.asPath; - router.push(`/signin?next=${redirectTo}`); + router.push(`/?next=${redirectTo}`); return null; } diff --git a/apps/app/lib/auth.ts b/apps/app/lib/auth.ts index e607fd00c..47a52663d 100644 --- a/apps/app/lib/auth.ts +++ b/apps/app/lib/auth.ts @@ -104,7 +104,7 @@ export const homePageRedirect = async (cookie?: string) => { if (!user) return { redirect: { - destination: "/signin", + destination: "/", permanent: false, }, }; diff --git a/apps/app/package.json b/apps/app/package.json index af90cf99a..01287f068 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -48,7 +48,7 @@ "react-markdown": "^8.0.7", "recharts": "^2.3.2", "remirror": "^2.0.23", - "swr": "^1.3.0", + "swr": "^2.1.3", "tlds": "^1.238.0", "uuid": "^9.0.0" }, diff --git a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx index 0d1782563..c4ab6918f 100644 --- a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx +++ b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx @@ -8,7 +8,7 @@ import { Controller, useForm } from "react-hook-form"; import fileService from "services/file.service"; import userService from "services/user.service"; // hooks -import useUser from "hooks/use-user"; +import useUserAuth from "hooks/use-user-auth"; import useToast from "hooks/use-toast"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; @@ -50,7 +50,7 @@ const Profile: NextPage = () => { } = useForm({ defaultValues }); const { setToastAlert } = useToast(); - const { user: myProfile, mutateUser } = useUser(); + const { user: myProfile, mutateUser } = useUserAuth(); useEffect(() => { reset({ ...defaultValues, ...myProfile }); diff --git a/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx b/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx index 6b48d9aa3..f2fe98f80 100644 --- a/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx +++ b/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useTheme } from "next-themes"; // hooks -import useUser from "hooks/use-user"; +import useUserAuth from "hooks/use-user-auth"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import SettingsNavbar from "layouts/settings-navbar"; @@ -15,7 +15,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { ICustomTheme } from "types"; const ProfilePreferences = () => { - const { user: myProfile } = useUser(); + const { user: myProfile } = useUserAuth(); const { theme } = useTheme(); const [customThemeSelectorOptions, setCustomThemeSelectorOptions] = useState(false); const [preLoadedData, setPreLoadedData] = useState(null); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 15237617e..29eaee83c 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -78,12 +78,15 @@ const IssueDetailsPage: NextPage = () => { async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueId) return; - mutate( + mutate( ISSUE_DETAILS(issueId as string), - (prevData: IIssue) => ({ - ...prevData, - ...formData, - }), + (prevData) => { + if (!prevData) return prevData; + return { + ...prevData, + ...formData, + }; + }, false ); diff --git a/apps/app/pages/_app.tsx b/apps/app/pages/_app.tsx index 61788592a..f1d16cfd1 100644 --- a/apps/app/pages/_app.tsx +++ b/apps/app/pages/_app.tsx @@ -44,31 +44,14 @@ Router.events.on("routeChangeComplete", NProgress.done); function MyApp({ Component, pageProps }: AppProps) { return ( - - - - - {" "} - - {SITE_TITLE} - - - - - - - - - - - - - - - - - - + // + + + + + + + // ); } diff --git a/apps/app/pages/_error.tsx b/apps/app/pages/_error.tsx index 0d1605484..8482cbc93 100644 --- a/apps/app/pages/_error.tsx +++ b/apps/app/pages/_error.tsx @@ -26,7 +26,7 @@ const CustomErrorComponent = () => { message: "Failed to sign out. Please try again.", }) ) - .finally(() => router.push("/signin")); + .finally(() => router.push("/")); }; return ( diff --git a/apps/app/pages/create-workspace.tsx b/apps/app/pages/create-workspace.tsx index d445e873e..389cdd7ec 100644 --- a/apps/app/pages/create-workspace.tsx +++ b/apps/app/pages/create-workspace.tsx @@ -2,7 +2,10 @@ import React from "react"; import { useRouter } from "next/router"; import Image from "next/image"; - +// hooks +import useUser from "hooks/use-user"; +// components +import { OnboardingLogo } from "components/onboarding"; // layouts import DefaultLayout from "layouts/default-layout"; import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper"; @@ -21,19 +24,36 @@ const CreateWorkspace: NextPage = () => { company_size: null, }; + const { user } = useUser(); return ( -
    -
    -
    - Plane Logo +
    +
    +
    +
    - {}} - onSubmit={(res) => router.push(`/${res.slug}`)} - /> + +
    +
    +
    +

    Create Workspace

    +

    + Create or join the workspace to get started with Plane. +

    +
    +
    + {}} + onSubmit={(res) => router.push(`/${res.slug}`)} + /> +
    +
    + +
    + Logged in: + {user?.email}
    diff --git a/apps/app/pages/index.tsx b/apps/app/pages/index.tsx index bd21abfc4..b032fb203 100644 --- a/apps/app/pages/index.tsx +++ b/apps/app/pages/index.tsx @@ -1,66 +1,116 @@ -import { useEffect } from "react"; - -import Router from "next/router"; - +import React from "react"; +// next imports +import Image from "next/image"; +// next types +import type { NextPage } from "next"; +// layouts +import DefaultLayout from "layouts/default-layout"; +// hooks +import useUserAuth from "hooks/use-user-auth"; +import useToast from "hooks/use-toast"; // services -import userService from "services/user.service"; -import workspaceService from "services/workspace.service"; +import authenticationService from "services/authentication.service"; +// social auth buttons +import { + GoogleLoginButton, + GithubLoginButton, + EmailCodeForm, + EmailPasswordForm, +} from "components/account"; // ui import { Spinner } from "components/ui"; -// types -import type { NextPage } from "next"; +// icons +import Logo from "public/logo.png"; -const redirectUserTo = async () => { - const user = await userService - .currentUser() - .then((res) => res) - .catch(() => { - Router.push("/signin"); - return; - }); +const HomePage: NextPage = () => { + const { user, isLoading, mutateUser } = useUserAuth("sign-in"); - if (!user) { - Router.push("/signin"); - return; - } else if (!user.user.is_onboarded) { - Router.push("/onboarding"); - return; - } else { - const userWorkspaces = await workspaceService.userWorkspaces(); + const { setToastAlert } = useToast(); - const lastActiveWorkspace = userWorkspaces.find( - (workspace) => workspace.id === user.user.last_workspace_id - ); - - if (lastActiveWorkspace) { - Router.push(`/${lastActiveWorkspace.slug}`); - return; - } else if (userWorkspaces.length > 0) { - Router.push(`/${userWorkspaces[0].slug}`); - return; - } else { - const invitations = await workspaceService.userWorkspaceInvitations(); - if (invitations.length > 0) { - Router.push(`/invitations`); - return; + const handleGoogleSignIn = async ({ clientId, credential }: any) => { + try { + if (clientId && credential) { + mutateUser( + await authenticationService.socialAuth({ + medium: "google", + credential, + clientId, + }) + ); } else { - Router.push(`/create-workspace`); - return; + throw Error("Cant find credentials"); } + } catch (error) { + console.log(error); + setToastAlert({ + title: "Error signing in!", + type: "error", + message: "Something went wrong. Please try again later or contact the support team.", + }); } - } -}; + }; -const Home: NextPage = () => { - useEffect(() => { - redirectUserTo(); - }, []); + const handleGithubSignIn = async (credential: string) => { + try { + if (process.env.NEXT_PUBLIC_GITHUB_ID && credential) { + mutateUser( + await authenticationService.socialAuth({ + medium: "github", + credential, + clientId: process.env.NEXT_PUBLIC_GITHUB_ID, + }) + ); + } else { + throw Error("Cant find credentials"); + } + } catch (error) { + console.log(error); + setToastAlert({ + title: "Error signing in!", + type: "error", + message: "Something went wrong. Please try again later or contact the support team.", + }); + } + }; return ( -
    - -
    + + {isLoading ? ( +
    + +
    + ) : ( +
    +
    +
    +
    + Plane Web Logo +
    + Sign In to your Plane Account +
    +
    + +
    + {parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? ( + <> + +
    + + +
    + + ) : ( + <> + + + )} +
    +
    +
    +
    + )} +
    ); }; -export default Home; +export default HomePage; diff --git a/apps/app/pages/invitations.tsx b/apps/app/pages/invitations.tsx index 21f6e25a7..107af4051 100644 --- a/apps/app/pages/invitations.tsx +++ b/apps/app/pages/invitations.tsx @@ -15,6 +15,7 @@ import DefaultLayout from "layouts/default-layout"; import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper"; // components import SingleInvitation from "components/workspace/single-invitation"; +import { OnboardingLogo } from "components/onboarding"; // ui import { Spinner, EmptySpace, EmptySpaceItem, SecondaryButton, PrimaryButton } from "components/ui"; // icons @@ -81,86 +82,93 @@ const OnBoard: NextPage = () => { return ( -
    - {user && ( -
    -

    logged in as {user.email}

    +
    +
    +
    +
    - )} -
    - {invitations && workspaces ? ( - invitations.length > 0 ? ( -
    -

    Workspace Invitations

    -

    - Select invites that you want to accept. -

    -
      - {invitations.map((invitation) => ( - - ))} -
    -
    - - - Go Home - - - - Accept and Continue - +
    + {invitations && workspaces ? ( + invitations.length > 0 ? ( +
    +
    +

    + Workspace Invitations +

    +

    + Create or join the workspace to get started with Plane. +

    +
    + +
      + {invitations.map((invitation) => ( + + ))} +
    + +
    + + + Go Home + + + + Accept and Continue + +
    -
    - ) : workspaces && workspaces.length > 0 ? ( -
    -

    Your workspaces

    - {workspaces.map((workspace) => ( - - - + ) : ( + invitations.length === 0 && + workspaces.length === 0 && ( + + { + router.push("/create-workspace"); + }} + /> + + ) ) - ) - ) : ( -
    - -
    - )} + ) : ( +
    + +
    + )} +
    +
    +
    + Logged in: + {user?.email}
    diff --git a/apps/app/pages/onboarding.tsx b/apps/app/pages/onboarding.tsx index 3891dba50..9d24a7664 100644 --- a/apps/app/pages/onboarding.tsx +++ b/apps/app/pages/onboarding.tsx @@ -1,14 +1,15 @@ import { useState } from "react"; import Image from "next/image"; -import { useRouter } from "next/router"; +import Router, { useRouter } from "next/router"; import { mutate } from "swr"; // services import userService from "services/user.service"; +import workspaceService from "services/workspace.service"; // hooks -import useUser from "hooks/use-user"; +import useUserAuth from "hooks/use-user-auth"; // layouts import DefaultLayout from "layouts/default-layout"; import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper"; @@ -38,15 +39,17 @@ const Onboarding: NextPage = () => { const router = useRouter(); - const { user } = useUser(); + const { user } = useUserAuth("onboarding"); + + console.log("user", user); return ( -
    +
    {step <= 3 ? ( -
    -
    +
    +
    {step === 1 ? ( @@ -59,7 +62,7 @@ const Onboarding: NextPage = () => {
    ) : (
    -
    +
    {step === 4 ? ( ) : step === 5 ? ( @@ -80,7 +83,7 @@ const Onboarding: NextPage = () => { if (step === 8) { userService .updateUserOnBoard({ userRole }) - .then(() => { + .then(async () => { mutate( CURRENT_USER, (prevData) => { @@ -96,7 +99,28 @@ const Onboarding: NextPage = () => { }, false ); - router.push("/"); + const userWorkspaces = await workspaceService.userWorkspaces(); + + const lastActiveWorkspace = userWorkspaces.find( + (workspace) => workspace.id === user?.last_workspace_id + ); + + if (lastActiveWorkspace) { + Router.push(`/${lastActiveWorkspace.slug}`); + return; + } else if (userWorkspaces.length > 0) { + Router.push(`/${userWorkspaces[0].slug}`); + return; + } else { + const invitations = await workspaceService.userWorkspaceInvitations(); + if (invitations.length > 0) { + Router.push(`/invitations`); + return; + } else { + Router.push(`/create-workspace`); + return; + } + } }) .catch((err) => { console.log(err); @@ -110,6 +134,10 @@ const Onboarding: NextPage = () => {
    )} +
    + Logged in: + {user?.email} +
    diff --git a/apps/app/pages/signin.tsx b/apps/app/pages/signin.tsx deleted file mode 100644 index 26d6832f1..000000000 --- a/apps/app/pages/signin.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { useRouter } from "next/router"; -import Image from "next/image"; -// hooks -import useUser from "hooks/use-user"; -import useToast from "hooks/use-toast"; -// services -import authenticationService from "services/authentication.service"; -// layouts -import DefaultLayout from "layouts/default-layout"; -// social button -import { - GoogleLoginButton, - GithubLoginButton, - EmailSignInForm, - EmailPasswordForm, -} from "components/account"; -// ui -import { Spinner } from "components/ui"; -// icons -import Logo from "public/logo.png"; -// types -import type { NextPage } from "next"; - -const SignInPage: NextPage = () => { - // router - const router = useRouter(); - // user hook - const { mutateUser } = useUser(); - // states - const [isLoading, setLoading] = useState(false); - - const { setToastAlert } = useToast(); - - const onSignInSuccess = useCallback(async () => { - setLoading(true); - await mutateUser(); - const nextLocation = router.asPath.split("?next=")[1]; - if (nextLocation) await router.push(nextLocation as string); - else await router.push("/"); - }, [mutateUser, router]); - - const handleGoogleSignIn = ({ clientId, credential }: any) => { - if (clientId && credential) { - setLoading(true); - authenticationService - .socialAuth({ - medium: "google", - credential, - clientId, - }) - .then(async () => { - await onSignInSuccess(); - }) - .catch((err) => { - console.log(err); - setToastAlert({ - title: "Error signing in!", - type: "error", - message: "Something went wrong. Please try again later or contact the support team.", - }); - setLoading(false); - }); - } - }; - - const handleGithubSignIn = useCallback( - (credential: string) => { - setLoading(true); - authenticationService - .socialAuth({ - medium: "github", - credential, - clientId: process.env.NEXT_PUBLIC_GITHUB_ID, - }) - .then(async () => { - await onSignInSuccess(); - }) - .catch((err) => { - console.log(err); - setToastAlert({ - title: "Error signing in!", - type: "error", - message: "Something went wrong. Please try again later or contact the support team.", - }); - setLoading(false); - }); - }, - [onSignInSuccess, setToastAlert] - ); - - return ( - - {isLoading ? ( -
    -

    Signing in. Please wait...

    - -
    - ) : ( -
    -
    -
    -
    - Plane Web Logo -

    - Sign In to your Plane Account -

    -
    - -
    - {parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? ( - <> - - -
    - - - -
    - - ) : ( - <> - - - )} -
    -
    -
    -
    - )} -
    - ); -}; - -export default SignInPage; diff --git a/apps/app/pages/workspace-member-invitation/[invitationId].tsx b/apps/app/pages/workspace-member-invitation/[invitationId].tsx index 4eb45f529..cc4c5ec6d 100644 --- a/apps/app/pages/workspace-member-invitation/[invitationId].tsx +++ b/apps/app/pages/workspace-member-invitation/[invitationId].tsx @@ -49,7 +49,7 @@ const WorkspaceInvitation: NextPage = () => { if (email === user?.email) { router.push("/invitations"); } else { - router.push("/signin"); + router.push("/"); } }) .catch((err) => console.error(err)); @@ -108,7 +108,7 @@ const WorkspaceInvitation: NextPage = () => { Icon={UserIcon} title="Sign in to continue" action={() => { - router.push("/signin"); + router.push("/"); }} /> ) : ( diff --git a/apps/app/services/api.service.ts b/apps/app/services/api.service.ts index 5434742ba..296b693aa 100644 --- a/apps/app/services/api.service.ts +++ b/apps/app/services/api.service.ts @@ -9,7 +9,8 @@ axios.interceptors.response.use( if (unAuthorizedStatus.includes(status)) { Cookies.remove("refreshToken", { path: "/" }); Cookies.remove("accessToken", { path: "/" }); - if (window.location.pathname != "/signin") window.location.href = "/signin"; + if (window.location.pathname != "/") + window.location.href = "/?next_url=window.location.pathname"; } return Promise.reject(error); } diff --git a/apps/app/styles/globals.css b/apps/app/styles/globals.css index 2791b4c42..22d060c86 100644 --- a/apps/app/styles/globals.css +++ b/apps/app/styles/globals.css @@ -17,9 +17,9 @@ } :root { - --color-bg-base: 243, 244, 246; + --color-bg-base: 255, 255, 255; --color-bg-surface-1: 249, 250, 251; - --color-bg-surface-2: 255, 255, 255; + --color-bg-surface-2: 243, 244, 246; --color-border: 229, 231, 235; --color-bg-sidebar: 255, 255, 255; diff --git a/apps/app/types/users.d.ts b/apps/app/types/users.d.ts index eb7f5c742..eaa75b75a 100644 --- a/apps/app/types/users.d.ts +++ b/apps/app/types/users.d.ts @@ -41,10 +41,11 @@ export interface ICustomTheme { textSecondary: string; } -export interface ICurrentUserResponse { +export interface ICurrentUserResponse extends IUser { assigned_issues: number; - user: IUser; + // user: IUser; workspace_invites: number; + is_onboarded: boolean; } export interface IUserLite { diff --git a/yarn.lock b/yarn.lock index d8a63fe19..5fb2c1578 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8212,10 +8212,12 @@ svgmoji@^3.2.0: "@svgmoji/openmoji" "^3.2.0" "@svgmoji/twemoji" "^3.2.0" -swr@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8" - integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw== +swr@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/swr/-/swr-2.1.3.tgz#ae3608d4dc19f75121e0b51d1982735fbf02f52e" + integrity sha512-g3ApxIM4Fjbd6vvEAlW60hJlKcYxHb+wtehogTygrh6Jsw7wNagv9m4Oj5Gq6zvvZw0tcyhVGL9L0oISvl3sUw== + dependencies: + use-sync-external-store "^1.2.0" table@^6.0.9: version "6.8.1" @@ -8680,7 +8682,7 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@1.2.0: +use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== From 857879f5ede66f31a7553e49c891497d69044306 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 1 Jun 2023 14:40:27 +0530 Subject: [PATCH 36/65] fix : theme provider fix (#1164) --- apps/app/pages/_app.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/app/pages/_app.tsx b/apps/app/pages/_app.tsx index f1d16cfd1..2b0c2ab6b 100644 --- a/apps/app/pages/_app.tsx +++ b/apps/app/pages/_app.tsx @@ -45,12 +45,14 @@ Router.events.on("routeChangeComplete", NProgress.done); function MyApp({ Component, pageProps }: AppProps) { return ( // - - - - - - + + + + + + + + // ); } From c234f2a08706e83949d193d721fd3e4f05457bb7 Mon Sep 17 00:00:00 2001 From: Nathanael Demacon Date: Fri, 2 Jun 2023 08:57:15 +0200 Subject: [PATCH 37/65] docs: fix docker compose quick start (#1173) * docs: add missing .env.example fetch in docker compose quick start * docs: clone the repository instead of fetching particular files --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 31cf8a54d..cf0af7fe2 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,11 @@ The easiest way to get started with Plane is by creating a [Plane Cloud](https:/ ### Docker Compose Setup -- Download the docker-compose.yml and setup.sh file +- Clone the repository ```bash -curl https://raw.githubusercontent.com/makeplane/plane/develop/docker-compose-hub.yml --output docker-compose.yml -curl https://raw.githubusercontent.com/makeplane/plane/develop/setup.sh --output setup.sh +git clone https://github.com/makeplane/plane +cd plane chmod +x setup.sh ``` From 37442f482b8604577ef410cb505565a0b7c285fe Mon Sep 17 00:00:00 2001 From: tarunratan Date: Sun, 4 Jun 2023 09:49:13 +0530 Subject: [PATCH 38/65] Update README.md (#1189) change in readme of docker-compose --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cf0af7fe2..bab7d9ebf 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ set +a - Run Docker compose up ```bash -docker compose up -d +docker-compose up -d ``` You can use the default email and password for your first login `captain@plane.so` and `password123`. From 799cf230b79164e5c31cf8c1255f6a709b92fbd5 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Sun, 4 Jun 2023 15:51:06 +0530 Subject: [PATCH 39/65] Revert "Update README.md (#1189)" (#1193) This reverts commit 37442f482b8604577ef410cb505565a0b7c285fe. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bab7d9ebf..cf0af7fe2 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ set +a - Run Docker compose up ```bash -docker-compose up -d +docker compose up -d ``` You can use the default email and password for your first login `captain@plane.so` and `password123`. From bffc6a60e770a8e59e9a276dc19cf0917f3a1b65 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 5 Jun 2023 12:50:44 +0530 Subject: [PATCH 40/65] fix: workspace member role update (#1203) --- apiserver/plane/api/views/workspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 64154500f..e8e7160f4 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -445,7 +445,7 @@ class WorkSpaceMemberViewSet(BaseViewSet): requested_workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user) # Check if role is being updated # One cannot update role higher than his own role - if "role" in request.data and request.data.get("role", workspace_member.role) > requested_workspace_member.role: + if "role" in request.data and int(request.data.get("role", workspace_member.role)) > requested_workspace_member.role: return Response( { "error": "You cannot update a role that is higher than your own role" From 50060a0bf95ab8f536fc401d6bc6cd94b1c1bcf3 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 5 Jun 2023 12:51:12 +0530 Subject: [PATCH 41/65] chore: update docker uploads (#1202) --- apiserver/plane/api/views/asset.py | 10 ++-------- apiserver/plane/api/views/issue.py | 10 +--------- apiserver/plane/settings/production.py | 12 ++++++++---- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/apiserver/plane/api/views/asset.py b/apiserver/plane/api/views/asset.py index 0f0513405..26fc8e936 100644 --- a/apiserver/plane/api/views/asset.py +++ b/apiserver/plane/api/views/asset.py @@ -34,10 +34,7 @@ class FileAssetEndpoint(BaseAPIView): ) serializer.save(workspace_id=request.user.last_workspace_id) - response_data = serializer.data - if settings.DOCKERIZED and settings.USE_MINIO: - response_data["asset"] = response_data["asset"].replace(settings.AWS_S3_ENDPOINT_URL, settings.WEB_URL) - return Response(response_data, status=status.HTTP_201_CREATED) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except Exception as e: capture_exception(e) @@ -85,10 +82,7 @@ class UserAssetsEndpoint(BaseAPIView): serializer = FileAssetSerializer(data=request.data) if serializer.is_valid(): serializer.save() - response_data = serializer.data - if settings.DOCKERIZED and settings.USE_MINIO: - response_data["asset"] = response_data["asset"].replace(settings.AWS_S3_ENDPOINT_URL, settings.WEB_URL) - return Response(response_data, status=status.HTTP_201_CREATED) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except Exception as e: capture_exception(e) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 74d0d466c..e6c37374b 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -817,14 +817,6 @@ class IssueAttachmentEndpoint(BaseAPIView): serializer = IssueAttachmentSerializer(data=request.data) if serializer.is_valid(): serializer.save(project_id=project_id, issue_id=issue_id) - response_data = serializer.data - if ( - settings.DOCKERIZED - and settings.USE_MINIO - ): - response_data["asset"] = response_data["asset"].replace( - settings.AWS_S3_ENDPOINT_URL, settings.WEB_URL - ) issue_activity.delay( type="attachment.activity.created", requested_data=None, @@ -836,7 +828,7 @@ class IssueAttachmentEndpoint(BaseAPIView): cls=DjangoJSONEncoder, ), ) - return Response(response_data, status=status.HTTP_201_CREATED) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except Exception as e: capture_exception(e) diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index a1d450137..90909cf08 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -30,9 +30,7 @@ DATABASES["default"] = dj_database_url.config() SITE_ID = 1 # Set the variable true if running in docker environment -DOCKERIZED = int(os.environ.get( - "DOCKERIZED", 0 -)) == 1 +DOCKERIZED = int(os.environ.get("DOCKERIZED", 0)) == 1 USE_MINIO = int(os.environ.get("USE_MINIO"), 0) == 1 @@ -86,7 +84,8 @@ if bool(os.environ.get("SENTRY_DSN", False)): ) if DOCKERIZED and USE_MINIO: - DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' + INSTALLED_APPS += ("storages",) + DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" # The AWS access key to use. AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key") # The AWS secret access key to use. @@ -99,6 +98,11 @@ if DOCKERIZED and USE_MINIO: AWS_DEFAULT_ACL = "public-read" AWS_QUERYSTRING_AUTH = False AWS_S3_FILE_OVERWRITE = False + + # Custom Domain settings + parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost")) + AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}" + AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:" else: # The AWS region to connect to. AWS_REGION = os.environ.get("AWS_REGION", "") From 58d1d8f1327d1ce33c228f9bd001dd7a953586dd Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 5 Jun 2023 12:51:30 +0530 Subject: [PATCH 42/65] fix: issue search for blocking and blocked_by condition (#1182) * fix: issue search for blocking and blocked_by condition * fix: issue search endpoint blockers * fix: rectify the filter parameters --- apiserver/plane/api/views/search.py | 13 ++++++++++--- apiserver/plane/utils/issue_search.py | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apiserver/plane/api/views/search.py b/apiserver/plane/api/views/search.py index 823a1fcc8..88dddc43c 100644 --- a/apiserver/plane/api/views/search.py +++ b/apiserver/plane/api/views/search.py @@ -210,13 +210,15 @@ class IssueSearchEndpoint(BaseAPIView): blocker_blocked_by = request.query_params.get("blocker_blocked_by", False) issue_id = request.query_params.get("issue_id", False) - issues = search_issues(query) - issues = issues.filter( + issues = Issue.objects.filter( workspace__slug=slug, project_id=project_id, project__project_projectmember__member=self.request.user, ) + if query: + issues = search_issues(query, issues) + if parent == "true" and issue_id: issue = Issue.objects.get(pk=issue_id) issues = issues.filter( @@ -227,7 +229,12 @@ class IssueSearchEndpoint(BaseAPIView): ) ) if blocker_blocked_by == "true" and issue_id: - issues = issues.filter(blocker_issues=issue_id, blocked_issues=issue_id) + issue = Issue.objects.get(pk=issue_id) + issues = issues.filter( + ~Q(pk=issue_id), + ~Q(blocked_issues__block=issue), + ~Q(blocker_issues__blocked_by=issue), + ) return Response( issues.values( diff --git a/apiserver/plane/utils/issue_search.py b/apiserver/plane/utils/issue_search.py index 93b0df6da..40f85dde4 100644 --- a/apiserver/plane/utils/issue_search.py +++ b/apiserver/plane/utils/issue_search.py @@ -8,7 +8,7 @@ from django.db.models import Q from plane.db.models import Issue -def search_issues(query): +def search_issues(query, queryset): fields = ["name", "sequence_id"] q = Q() for field in fields: @@ -18,6 +18,6 @@ def search_issues(query): q |= Q(**{"sequence_id": sequence_id}) else: q |= Q(**{f"{field}__icontains": query}) - return Issue.objects.filter( + return queryset.filter( q, ).distinct() From e3da80755c0861301e6461593e24e82ac7f115ec Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 5 Jun 2023 12:52:25 +0530 Subject: [PATCH 43/65] fix: minio settings (#1172) --- apiserver/plane/settings/local.py | 2 +- apiserver/plane/settings/production.py | 2 +- apiserver/plane/settings/staging.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index 324d2edbe..20b257a27 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -29,7 +29,7 @@ DOCKERIZED = int(os.environ.get( "DOCKERIZED", 0 )) == 1 -USE_MINIO = int(os.environ.get("USE_MINIO"), 0) == 1 +USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index 90909cf08..f0151cbbe 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -32,7 +32,7 @@ SITE_ID = 1 # Set the variable true if running in docker environment DOCKERIZED = int(os.environ.get("DOCKERIZED", 0)) == 1 -USE_MINIO = int(os.environ.get("USE_MINIO"), 0) == 1 +USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) diff --git a/apiserver/plane/settings/staging.py b/apiserver/plane/settings/staging.py index c5db5d300..c6ffcaf22 100644 --- a/apiserver/plane/settings/staging.py +++ b/apiserver/plane/settings/staging.py @@ -54,7 +54,7 @@ DOCKERIZED = int(os.environ.get( "DOCKERIZED", 0 )) == 1 FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) -USE_MINIO = int(os.environ.get("USE_MINIO"), 0) == 1 +USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 sentry_sdk.init( dsn=os.environ.get("SENTRY_DSN"), From 2511a284eb95115524d0163ab27c2e2accb62627 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 5 Jun 2023 12:53:04 +0530 Subject: [PATCH 44/65] feat: plane proxy setup (#1181) * feat: plane proxy setup * dev: remove port mapping from web, api and minio containers --- .env.example | 2 ++ docker-compose-hub.yml | 18 +++++------------- docker-compose.yml | 14 +++++++------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/.env.example b/.env.example index 35c5f83d3..746a5b9d7 100644 --- a/.env.example +++ b/.env.example @@ -53,6 +53,8 @@ GITHUB_CLIENT_SECRET="" # For fetching release notes # Settings related to Docker DOCKERIZED=1 +# set to 1 If using the pre-configured minio setup +USE_MINIO=1 # Nginx Configuration NGINX_PORT=80 diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml index 1bdd41d07..9decf871e 100644 --- a/docker-compose-hub.yml +++ b/docker-compose-hub.yml @@ -14,8 +14,6 @@ services: NEXT_PUBLIC_SENTRY_DSN: 0 NEXT_PUBLIC_ENABLE_OAUTH: 0 NEXT_PUBLIC_ENABLE_SENTRY: 0 - ports: - - 3000:3000 plane-api: container_name: planebackend @@ -45,12 +43,10 @@ services: SECRET_KEY: ${SECRET_KEY} DEFAULT_EMAIL: ${DEFAULT_EMAIL} DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} - USE_MINIO: 1 + USE_MINIO: ${USE_MINIO} depends_on: - plane-db - plane-redis - ports: - - 8000:8000 plane-worker: container_name: planerqworker @@ -80,7 +76,7 @@ services: SECRET_KEY: ${SECRET_KEY} DEFAULT_EMAIL: ${DEFAULT_EMAIL} DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} - USE_MINIO: 1 + USE_MINIO: ${USE_MINIO} depends_on: - plane-api - plane-db @@ -114,8 +110,6 @@ services: MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} command: server /export --console-address ":9090" - ports: - - 9000:9000 createbuckets: image: minio/mc @@ -130,11 +124,9 @@ services: " # Comment this if you already have a reverse proxy running - nginx: - container_name: nginx - build: - context: ./nginx - dockerfile: Dockerfile + plane-proxy: + container_name: planeproxy + image: makeplane/plane-proxy:latest ports: - ${NGINX_PORT}:80 depends_on: diff --git a/docker-compose.yml b/docker-compose.yml index 19f95f875..1c5a80f15 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: environment: DJANGO_SETTINGS_MODULE: plane.settings.production DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE} - REDIS_URL: redis://redis:6379/ + REDIS_URL: redis://plane-redis:6379/ EMAIL_HOST: ${EMAIL_HOST} EMAIL_HOST_USER: ${EMAIL_HOST_USER} EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} @@ -52,7 +52,7 @@ services: SECRET_KEY: ${SECRET_KEY} DEFAULT_EMAIL: ${DEFAULT_EMAIL} DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} - USE_MINIO: 1 + USE_MINIO: ${USE_MINIO} depends_on: - plane-db - plane-redis @@ -67,7 +67,7 @@ services: environment: DJANGO_SETTINGS_MODULE: plane.settings.production DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE} - REDIS_URL: redis://redis:6379/ + REDIS_URL: redis://plane-redis:6379/ EMAIL_HOST: ${EMAIL_HOST} EMAIL_HOST_USER: ${EMAIL_HOST_USER} EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} @@ -88,7 +88,7 @@ services: SECRET_KEY: ${SECRET_KEY} DEFAULT_EMAIL: ${DEFAULT_EMAIL} DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} - USE_MINIO: 1 + USE_MINIO: ${USE_MINIO} depends_on: - plane-api - plane-db @@ -108,7 +108,7 @@ services: PGDATA: /var/lib/postgresql/data plane-redis: - container_name: redis + container_name: plane-redis image: redis:6.2.7-alpine restart: always volumes: @@ -137,8 +137,8 @@ services: depends_on: - plane-minio - nginx: - container_name: nginx + plane-proxy: + container_name: planeproxy build: context: ./nginx dockerfile: Dockerfile From 77e05a3599a604e1e2c854c0ee7ba30703349eba Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 5 Jun 2023 17:45:10 +0530 Subject: [PATCH 45/65] fix: project member role update (#1205) --- apiserver/plane/api/views/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 9b2b9ce5e..194638f93 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -424,7 +424,7 @@ class ProjectMemberViewSet(BaseViewSet): ) # Check while updating user roles requested_project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, member=request.user) - if "role" in request.data and request.data.get("role", project_member.role) > requested_project_member.role: + if "role" in request.data and int(request.data.get("role", project_member.role)) > requested_project_member.role: return Response( { "error": "You cannot update a role that is higher than your own role" From 7eae6b4c9e2613866a8534aa74a1075dae113eec Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Mon, 5 Jun 2023 17:48:29 +0530 Subject: [PATCH 46/65] fix: user public authentication workflow updates (#1207) * auth integration fixes * auth integration fixes * auth integration fixes * auth integration fixes * dev: update user api to return fallback workspace and improve the structure of the response * dev: fix the issue keyerror and move onboarding logic to serializer method field * dev: use-user-auth hook imlemented for route access validation and build issues resolved effected by user payload * fix: global theme color fix * style: new onboarding ui , fix: use-user-auth hook implemented * fix: command palette, project invite modal and issue detail page mutation type fix * fix: onboarding redirection fix * dev: build isuue resolved * fix: use user auth hook fix * fix: sign in toast alert fix, sign out redirection fix and user theme error fix * fix: user response fix * fix: unAuthorizedStatus logic updated * dev: Implemented SEO in app.tsx * dev: User public auth workflow updates. --------- Co-authored-by: sriram veeraghanta Co-authored-by: pablohashescobar Co-authored-by: anmolsinghbhatia --- .../account/email-password-form.tsx | 4 ++-- apps/app/hooks/use-user-auth.tsx | 12 ++++++----- apps/app/pages/_app.tsx | 21 ++++++++++++++++--- apps/app/pages/index.tsx | 17 ++++++++++++++- apps/app/pages/onboarding.tsx | 2 -- apps/app/services/api.service.ts | 2 +- 6 files changed, 44 insertions(+), 14 deletions(-) diff --git a/apps/app/components/account/email-password-form.tsx b/apps/app/components/account/email-password-form.tsx index 3527ae440..7d56d67a0 100644 --- a/apps/app/components/account/email-password-form.tsx +++ b/apps/app/components/account/email-password-form.tsx @@ -17,7 +17,7 @@ type EmailPasswordFormValues = { medium?: string; }; -export const EmailPasswordForm = ({ onSuccess }: any) => { +export const EmailPasswordForm = ({ handleSignIn }: any) => { const { setToastAlert } = useToast(); const { register, @@ -38,7 +38,7 @@ export const EmailPasswordForm = ({ onSuccess }: any) => { authenticationService .emailLogin(formData) .then((response) => { - onSuccess(response); + if (handleSignIn) handleSignIn(response); }) .catch((error) => { console.log(error); diff --git a/apps/app/hooks/use-user-auth.tsx b/apps/app/hooks/use-user-auth.tsx index 9dba5ce61..619a2fcdd 100644 --- a/apps/app/hooks/use-user-auth.tsx +++ b/apps/app/hooks/use-user-auth.tsx @@ -13,6 +13,8 @@ import type { IWorkspace, ICurrentUserResponse } from "types"; const useUserAuth = (routeAuth: "sign-in" | "onboarding" | "admin" | null = "admin") => { const router = useRouter(); + const { next_url } = router.query as { next_url: string }; + const [isRouteAccess, setIsRouteAccess] = useState(true); const { @@ -48,8 +50,6 @@ const useUserAuth = (routeAuth: "sign-in" | "onboarding" | "admin" | null = "adm }; const handleUserRouteAuthentication = async () => { - console.log("user", user); - if (user && user.is_active) { if (routeAuth === "sign-in") { if (user.is_onboarded) handleWorkSpaceRedirection(); @@ -82,8 +82,10 @@ const useUserAuth = (routeAuth: "sign-in" | "onboarding" | "admin" | null = "adm if (!isLoading) { setIsRouteAccess(() => true); - if (user) handleUserRouteAuthentication(); - else { + if (user) { + if (next_url) router.push(next_url); + else handleUserRouteAuthentication(); + } else { if (routeAuth === "sign-in") { setIsRouteAccess(() => false); return; @@ -93,7 +95,7 @@ const useUserAuth = (routeAuth: "sign-in" | "onboarding" | "admin" | null = "adm } } } - }, [user, isLoading, routeAuth, router]); + }, [user, isLoading, routeAuth, router, next_url]); return { isLoading: isRouteAccess, diff --git a/apps/app/pages/_app.tsx b/apps/app/pages/_app.tsx index 2b0c2ab6b..402736a36 100644 --- a/apps/app/pages/_app.tsx +++ b/apps/app/pages/_app.tsx @@ -1,4 +1,7 @@ +// next imports +import Head from "next/head"; import dynamic from "next/dynamic"; +import Router from "next/router"; // themes import { ThemeProvider } from "next-themes"; @@ -10,9 +13,6 @@ import "styles/command-pallette.css"; import "styles/nprogress.css"; import "styles/react-datepicker.css"; -import Router from "next/router"; -import Head from "next/head"; - // nprogress import NProgress from "nprogress"; @@ -49,6 +49,21 @@ function MyApp({ Component, pageProps }: AppProps) { + + {SITE_TITLE} + + + + + + + + + + + + + diff --git a/apps/app/pages/index.tsx b/apps/app/pages/index.tsx index b032fb203..9f3ef51ba 100644 --- a/apps/app/pages/index.tsx +++ b/apps/app/pages/index.tsx @@ -73,6 +73,21 @@ const HomePage: NextPage = () => { } }; + const handleEmailPasswordSignIn = async (response: any) => { + try { + if (response) { + mutateUser(); + } + } catch (error) { + console.log(error); + setToastAlert({ + title: "Error signing in!", + type: "error", + message: "Something went wrong. Please try again later or contact the support team.", + }); + } + }; + return ( {isLoading ? ( @@ -101,7 +116,7 @@ const HomePage: NextPage = () => { ) : ( <> - + )}
    diff --git a/apps/app/pages/onboarding.tsx b/apps/app/pages/onboarding.tsx index 9d24a7664..64b08e2f8 100644 --- a/apps/app/pages/onboarding.tsx +++ b/apps/app/pages/onboarding.tsx @@ -41,8 +41,6 @@ const Onboarding: NextPage = () => { const { user } = useUserAuth("onboarding"); - console.log("user", user); - return ( diff --git a/apps/app/services/api.service.ts b/apps/app/services/api.service.ts index 296b693aa..cf540e2b5 100644 --- a/apps/app/services/api.service.ts +++ b/apps/app/services/api.service.ts @@ -10,7 +10,7 @@ axios.interceptors.response.use( Cookies.remove("refreshToken", { path: "/" }); Cookies.remove("accessToken", { path: "/" }); if (window.location.pathname != "/") - window.location.href = "/?next_url=window.location.pathname"; + window.location.href = `/?next_url=${window.location.pathname}`; } return Promise.reject(error); } From 557e96c68ead458636876a0b462c935c02aecf5b Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 5 Jun 2023 21:26:04 +0530 Subject: [PATCH 47/65] fix: email tls when selfhosting (#1206) --- .env.example | 1 + docker-compose-hub.yml | 2 ++ docker-compose.yml | 2 ++ 3 files changed, 5 insertions(+) diff --git a/.env.example b/.env.example index 746a5b9d7..a39da7045 100644 --- a/.env.example +++ b/.env.example @@ -34,6 +34,7 @@ EMAIL_HOST_USER="" EMAIL_HOST_PASSWORD="" EMAIL_PORT=587 EMAIL_FROM="Team Plane " +EMAIL_USE_TLS="1" # AWS Settings AWS_REGION="" diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml index 9decf871e..14331532c 100644 --- a/docker-compose-hub.yml +++ b/docker-compose-hub.yml @@ -29,6 +29,7 @@ services: EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} EMAIL_PORT: ${EMAIL_PORT} EMAIL_FROM: ${EMAIL_FROM} + EMAIL_USE_TLS: ${EMAIL_USE_TLS} AWS_REGION: ${AWS_REGION} AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} @@ -62,6 +63,7 @@ services: EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} EMAIL_PORT: ${EMAIL_PORT} EMAIL_FROM: ${EMAIL_FROM} + EMAIL_USE_TLS: ${EMAIL_USE_TLS} AWS_REGION: ${AWS_REGION} AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} diff --git a/docker-compose.yml b/docker-compose.yml index 1c5a80f15..5079d5379 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,7 @@ services: EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} EMAIL_PORT: ${EMAIL_PORT} EMAIL_FROM: ${EMAIL_FROM} + EMAIL_USE_TLS: ${EMAIL_USE_TLS} AWS_REGION: ${AWS_REGION} AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} @@ -73,6 +74,7 @@ services: EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} EMAIL_PORT: ${EMAIL_PORT} EMAIL_FROM: ${EMAIL_FROM} + EMAIL_USE_TLS: ${EMAIL_USE_TLS} AWS_REGION: ${AWS_REGION} AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} From b6c0ddac507e92cae698018cac9b88c583700c0f Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 6 Jun 2023 08:21:57 +0530 Subject: [PATCH 48/65] chore: move minio endpoint url to environment configuration (#1210) --- .env.example | 1 + apiserver/plane/settings/production.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index a39da7045..42d98677b 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,7 @@ EMAIL_USE_TLS="1" AWS_REGION="" AWS_ACCESS_KEY_ID="access-key" AWS_SECRET_ACCESS_KEY="secret-key" +AWS_S3_ENDPOINT_URL="http://plane-minio:9000" # Changing this requires change in the nginx.conf for uploads if using minio setup AWS_S3_BUCKET_NAME="uploads" # Maximum file upload limit diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index f0151cbbe..7e7f4186f 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -93,7 +93,7 @@ if DOCKERIZED and USE_MINIO: # The name of the bucket to store files in. AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads") # The full URL to the S3 endpoint. Leave blank to use the default region URL. - AWS_S3_ENDPOINT_URL = "http://plane-minio:9000" + AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "http://plane-minio:9000") # Default permissions AWS_DEFAULT_ACL = "public-read" AWS_QUERYSTRING_AUTH = False From fae9d8cdc11730c496cbd1d62724f84ea671372d Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 6 Jun 2023 19:15:20 +0530 Subject: [PATCH 49/65] chore: reset password url (#1220) * chore: reset password url * dev: update password reset endpoint * dev: update reset password url --- apiserver/plane/api/urls.py | 2 +- apiserver/plane/bgtasks/forgot_password_task.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 9decefca8..bf5180ff8 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -170,7 +170,7 @@ urlpatterns = [ ), # Password Manipulation path( - "password-reset///", + "reset-password///", ResetPasswordEndpoint.as_view(), name="password-reset", ), diff --git a/apiserver/plane/bgtasks/forgot_password_task.py b/apiserver/plane/bgtasks/forgot_password_task.py index d3f00e666..485fef000 100644 --- a/apiserver/plane/bgtasks/forgot_password_task.py +++ b/apiserver/plane/bgtasks/forgot_password_task.py @@ -16,7 +16,7 @@ from plane.db.models import User def forgot_password(first_name, email, uidb64, token, current_site): try: - realtivelink = f"/email-verify/?uidb64={uidb64}&token={token}/" + realtivelink = f"/reset-password/?uidb64={uidb64}&token={token}" abs_url = current_site + realtivelink from_email_string = settings.EMAIL_FROM From 705371eaf332591f6391a7f741a9889c5e95c0cd Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 6 Jun 2023 19:15:42 +0530 Subject: [PATCH 50/65] fix: file upload size limit (#1218) --- docker-compose-hub.yml | 21 ++++++------ docker-compose.yml | 39 +++++++++++------------ nginx/Dockerfile | 10 ++++-- nginx/env.sh | 4 +++ nginx/{nginx.conf => nginx.conf.template} | 5 ++- 5 files changed, 46 insertions(+), 33 deletions(-) create mode 100644 nginx/env.sh rename nginx/{nginx.conf => nginx.conf.template} (81%) diff --git a/docker-compose-hub.yml b/docker-compose-hub.yml index 14331532c..f259f6391 100644 --- a/docker-compose-hub.yml +++ b/docker-compose-hub.yml @@ -109,8 +109,8 @@ services: volumes: - uploads:/export environment: - MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} - MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} + MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} + MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} command: server /export --console-address ":9090" createbuckets: @@ -127,13 +127,16 @@ services: # Comment this if you already have a reverse proxy running plane-proxy: - container_name: planeproxy - image: makeplane/plane-proxy:latest - ports: - - ${NGINX_PORT}:80 - depends_on: - - plane-web - - plane-api + container_name: planeproxy + image: makeplane/plane-proxy:latest + ports: + - ${NGINX_PORT}:80 + environment: + FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880} + BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} + depends_on: + - plane-web + - plane-api volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 5079d5379..45a74afb6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,10 +4,10 @@ services: plane-web: container_name: planefrontend build: - context: . - dockerfile: ./apps/app/Dockerfile.web - args: - NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 + context: . + dockerfile: ./apps/app/Dockerfile.web + args: + NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 restart: always command: [ "/usr/local/bin/start.sh" ] environment: @@ -24,8 +24,8 @@ services: plane-api: container_name: planebackend build: - context: ./apiserver - dockerfile: Dockerfile.api + context: ./apiserver + dockerfile: Dockerfile.api restart: always command: ./bin/takeoff environment: @@ -61,8 +61,8 @@ services: plane-worker: container_name: planebgworker build: - context: ./apiserver - dockerfile: Dockerfile.api + context: ./apiserver + dockerfile: Dockerfile.api restart: always command: ./bin/worker environment: @@ -88,8 +88,8 @@ services: OPENAI_API_KEY: ${OPENAI_API_KEY} GPT_ENGINE: ${GPT_ENGINE} SECRET_KEY: ${SECRET_KEY} - DEFAULT_EMAIL: ${DEFAULT_EMAIL} - DEFAULT_PASSWORD: ${DEFAULT_PASSWORD} + DEFAULT_EMAIL: ${DEFAULT_EMAIL:-captain@plane.so} + DEFAULT_PASSWORD: ${DEFAULT_PASSWORD:-password123} USE_MINIO: ${USE_MINIO} depends_on: - plane-api @@ -130,27 +130,24 @@ 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; " depends_on: - plane-minio plane-proxy: container_name: planeproxy build: - context: ./nginx - dockerfile: Dockerfile + context: ./nginx + dockerfile: Dockerfile restart: always ports: - ${NGINX_PORT}:80 + 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: pgdata: diff --git a/nginx/Dockerfile b/nginx/Dockerfile index 4948a0109..b1aef1a20 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -1,4 +1,10 @@ -FROM nginx:1.21-alpine +FROM nginx:1.25.0-alpine RUN rm /etc/nginx/conf.d/default.conf -COPY nginx.conf /etc/nginx/nginx.conf \ No newline at end of file +COPY nginx.conf.template /etc/nginx/nginx.conf.template + +COPY ./env.sh /docker-entrypoint.sh + +RUN chmod +x /docker-entrypoint.sh +# Update all environment variables +CMD ["/docker-entrypoint.sh"] diff --git a/nginx/env.sh b/nginx/env.sh new file mode 100644 index 000000000..59e4a46a0 --- /dev/null +++ b/nginx/env.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +envsubst < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf +exec nginx -g 'daemon off;' diff --git a/nginx/nginx.conf b/nginx/nginx.conf.template similarity index 81% rename from nginx/nginx.conf rename to nginx/nginx.conf.template index b57dc97f2..206c94b51 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf.template @@ -8,6 +8,9 @@ server { listen 80; root /www/data/; access_log /var/log/nginx/access.log; + + client_max_body_size ${FILE_SIZE_LIMIT}; + location / { proxy_pass http://planefrontend:3000/; } @@ -16,7 +19,7 @@ server { proxy_pass http://planebackend:8000/api/; } - location /uploads/ { + location /${BUCKET_NAME}/ { proxy_pass http://plane-minio:9000/uploads/; } } From c1273532812feca3e4f9611da2074d29584ad4e2 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 6 Jun 2023 19:15:56 +0530 Subject: [PATCH 51/65] chore: workspace invite created detail (#1209) * chore: workspace invite created detail * dev: select related workspace member invite list --- apiserver/plane/api/serializers/workspace.py | 1 + apiserver/plane/api/views/workspace.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/serializers/workspace.py b/apiserver/plane/api/serializers/workspace.py index 4562726ae..078a4bf08 100644 --- a/apiserver/plane/api/serializers/workspace.py +++ b/apiserver/plane/api/serializers/workspace.py @@ -45,6 +45,7 @@ class WorkSpaceMemberSerializer(BaseSerializer): class WorkSpaceMemberInviteSerializer(BaseSerializer): workspace = WorkSpaceSerializer(read_only=True) total_members = serializers.IntegerField(read_only=True) + created_by_detail = UserLiteSerializer(read_only=True, source="created_by") class Meta: model = WorkspaceMemberInvite diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index e8e7160f4..58249d9b6 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -361,7 +361,7 @@ class WorkspaceInvitationsViewset(BaseViewSet): super() .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "workspace__owner") + .select_related("workspace", "workspace__owner", "created_by") ) @@ -374,7 +374,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet): super() .get_queryset() .filter(email=self.request.user.email) - .select_related("workspace", "workspace__owner") + .select_related("workspace", "workspace__owner", "created_by") .annotate(total_members=Count("workspace__workspace_member")) ) From 6f2a38ad66f60f305d871d6b694fd52c79793d81 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 6 Jun 2023 21:36:00 +0530 Subject: [PATCH 52/65] fix: bug and auth fixes (#1224) * fix: sign in and invitation page fixes * fix: project and workspace services track event fix * fix: user onboarding complete track event fix * fix: issue track event fix * fix: partial property , issue comment and mark as done issue track event fix * fix: bulk delete , move to cycle or module and issue label track event fix * fix: state , cycle and module track event fix * fix: pages and block track event fix * fix: integration , estimate , importer , analytics and gpt track event fix * fix: view track event fix * fix: build fix * fix: build fix --- .../custom-analytics/custom-analytics.tsx | 5 +- .../analytics/custom-analytics/sidebar.tsx | 6 +- .../components/analytics/project-modal.tsx | 7 +- .../command-palette/change-issue-assignee.tsx | 7 +- .../command-palette/change-issue-priority.tsx | 7 +- .../command-palette/change-issue-state.tsx | 7 +- .../command-palette/command-pallette.tsx | 17 +- .../components/core/board-view/all-boards.tsx | 5 +- .../core/board-view/single-board.tsx | 5 +- .../core/board-view/single-issue.tsx | 11 +- .../core/bulk-delete-issues-modal.tsx | 16 +- .../core/calendar-view/calendar.tsx | 17 +- .../core/calendar-view/single-date.tsx | 5 +- .../core/calendar-view/single-issue.tsx | 11 +- .../components/core/gpt-assistant-modal.tsx | 25 ++- apps/app/components/core/issues-view.tsx | 43 ++++-- .../components/core/list-view/all-lists.tsx | 5 +- .../core/list-view/single-issue.tsx | 11 +- .../components/core/list-view/single-list.tsx | 12 +- apps/app/components/cycles/cycles-view.tsx | 4 + .../components/cycles/delete-cycle-modal.tsx | 6 +- apps/app/components/cycles/modal.tsx | 8 +- apps/app/components/cycles/select.tsx | 9 +- apps/app/components/cycles/sidebar.tsx | 13 +- .../create-update-estimate-modal.tsx | 15 +- .../components/estimates/single-estimate.tsx | 6 +- .../integration/delete-import-modal.tsx | 7 +- .../components/integration/github/root.tsx | 10 +- apps/app/components/integration/guide.tsx | 9 +- apps/app/components/integration/jira/root.tsx | 10 +- apps/app/components/issues/activity.tsx | 14 +- .../components/issues/comment/add-comment.tsx | 16 +- .../components/issues/delete-issue-modal.tsx | 7 +- apps/app/components/issues/form.tsx | 25 ++- apps/app/components/issues/modal.tsx | 29 +++- .../components/issues/my-issues-list-item.tsx | 9 +- apps/app/components/issues/sidebar.tsx | 30 +++- .../app/components/issues/sub-issues-list.tsx | 7 +- .../issues/view-select/assignee.tsx | 7 +- .../issues/view-select/due-date.tsx | 13 +- .../issues/view-select/estimate.tsx | 7 +- .../issues/view-select/priority.tsx | 7 +- .../components/issues/view-select/state.tsx | 26 ++-- .../components/labels/create-label-modal.tsx | 7 +- .../labels/create-update-label-inline.tsx | 9 +- .../components/labels/delete-label-modal.tsx | 7 +- .../components/labels/labels-list-modal.tsx | 17 +- .../components/labels/single-label-group.tsx | 16 +- .../modules/delete-module-modal.tsx | 7 +- apps/app/components/modules/modal.tsx | 9 +- apps/app/components/modules/sidebar.tsx | 14 +- .../components/modules/single-module-card.tsx | 6 +- .../components/onboarding/invite-members.tsx | 7 +- apps/app/components/onboarding/workspace.tsx | 13 +- apps/app/components/pages/create-block.tsx | 20 ++- .../pages/create-update-block-inline.tsx | 66 +++++--- .../pages/create-update-page-modal.tsx | 9 +- .../components/pages/delete-page-modal.tsx | 6 +- apps/app/components/pages/pages-view.tsx | 7 +- .../components/pages/single-page-block.tsx | 66 +++++--- .../project/create-project-modal.tsx | 7 +- .../project/delete-project-modal.tsx | 6 +- .../project/send-project-invitation-modal.tsx | 7 +- apps/app/components/project/sidebar-list.tsx | 11 +- .../components/states/create-state-modal.tsx | 7 +- .../states/create-update-state-inline.tsx | 24 ++- .../components/states/delete-state-modal.tsx | 7 +- apps/app/components/states/single-state.tsx | 52 +++++-- .../components/views/delete-view-modal.tsx | 7 +- apps/app/components/views/modal.tsx | 8 +- .../workspace/create-workspace-form.tsx | 6 +- .../workspace/delete-workspace-modal.tsx | 7 +- .../send-workspace-invitation-modal.tsx | 11 +- .../workspace/single-invitation.tsx | 5 +- apps/app/contexts/issue-view.context.tsx | 101 ++++++++---- apps/app/pages/[workspaceSlug]/analytics.tsx | 10 +- .../projects/[projectId]/cycles/[cycleId].tsx | 6 +- .../projects/[projectId]/cycles/index.tsx | 4 + .../projects/[projectId]/issues/[issueId].tsx | 12 +- .../[projectId]/modules/[moduleId].tsx | 12 +- .../projects/[projectId]/modules/index.tsx | 6 + .../projects/[projectId]/pages/[pageId].tsx | 12 +- .../projects/[projectId]/pages/index.tsx | 4 + .../projects/[projectId]/settings/control.tsx | 5 +- .../[projectId]/settings/estimates.tsx | 9 +- .../[projectId]/settings/features.tsx | 8 +- .../projects/[projectId]/settings/index.tsx | 6 +- .../projects/[projectId]/settings/labels.tsx | 7 + .../projects/[projectId]/settings/members.tsx | 1 + .../projects/[projectId]/settings/states.tsx | 7 + .../projects/[projectId]/views/index.tsx | 6 + .../pages/[workspaceSlug]/projects/index.tsx | 4 + .../pages/[workspaceSlug]/settings/index.tsx | 8 +- .../[workspaceSlug]/settings/members.tsx | 1 + apps/app/pages/api/track-event.ts | 18 +-- apps/app/pages/create-workspace.tsx | 1 + apps/app/pages/index.tsx | 8 +- apps/app/pages/invitations.tsx | 2 +- apps/app/pages/magic-sign-in.tsx | 8 +- apps/app/pages/onboarding.tsx | 6 +- .../[invitationId].tsx | 13 +- apps/app/services/ai.service.ts | 7 +- apps/app/services/cycles.service.ts | 30 ++-- apps/app/services/estimates.service.ts | 21 ++- .../services/integration/github.service.ts | 7 +- apps/app/services/integration/index.ts | 12 +- apps/app/services/integration/jira.service.ts | 10 +- apps/app/services/issues.service.ts | 90 ++++++++--- apps/app/services/modules.service.ts | 36 +++-- apps/app/services/pages.service.ts | 52 +++++-- apps/app/services/project.service.ts | 32 +++- apps/app/services/state.service.ts | 30 ++-- apps/app/services/track-event.service.ts | 145 +++++++++++++++--- apps/app/services/user.service.ts | 13 +- apps/app/services/views.service.ts | 29 +++- apps/app/services/workspace.service.ts | 46 ++++-- apps/app/types/workspace.d.ts | 1 + 117 files changed, 1319 insertions(+), 494 deletions(-) diff --git a/apps/app/components/analytics/custom-analytics/custom-analytics.tsx b/apps/app/components/analytics/custom-analytics/custom-analytics.tsx index f9be7d1dd..f46b7f2b2 100644 --- a/apps/app/components/analytics/custom-analytics/custom-analytics.tsx +++ b/apps/app/components/analytics/custom-analytics/custom-analytics.tsx @@ -18,7 +18,7 @@ import { Loader, PrimaryButton } from "components/ui"; // helpers import { convertResponseToBarGraphData } from "helpers/analytics.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse } from "types"; +import { IAnalyticsParams, IAnalyticsResponse, ICurrentUserResponse } from "types"; // fetch-keys import { ANALYTICS } from "constants/fetch-keys"; @@ -29,6 +29,7 @@ type Props = { control: Control; setValue: UseFormSetValue; fullScreen: boolean; + user: ICurrentUserResponse | undefined; }; export const CustomAnalytics: React.FC = ({ @@ -38,6 +39,7 @@ export const CustomAnalytics: React.FC = ({ control, setValue, fullScreen, + user, }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -124,6 +126,7 @@ export const CustomAnalytics: React.FC = ({ params={params} fullScreen={fullScreen} isProjectLevel={isProjectLevel} + user={user} />
    ); diff --git a/apps/app/components/analytics/custom-analytics/sidebar.tsx b/apps/app/components/analytics/custom-analytics/sidebar.tsx index 7910c2cc8..5f4700f29 100644 --- a/apps/app/components/analytics/custom-analytics/sidebar.tsx +++ b/apps/app/components/analytics/custom-analytics/sidebar.tsx @@ -27,6 +27,7 @@ import { renderShortDate } from "helpers/date-time.helper"; import { IAnalyticsParams, IAnalyticsResponse, + ICurrentUserResponse, IExportAnalyticsFormData, IProject, IWorkspace, @@ -41,6 +42,7 @@ type Props = { params: IAnalyticsParams; fullScreen: boolean; isProjectLevel: boolean; + user: ICurrentUserResponse | undefined; }; export const AnalyticsSidebar: React.FC = ({ @@ -48,6 +50,7 @@ export const AnalyticsSidebar: React.FC = ({ params, fullScreen, isProjectLevel = false, + user, }) => { const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; @@ -138,7 +141,8 @@ export const AnalyticsSidebar: React.FC = ({ ? "MODULE_ANALYTICS_EXPORT" : projectId ? "PROJECT_ANALYTICS_EXPORT" - : "WORKSPACE_ANALYTICS_EXPORT" + : "WORKSPACE_ANALYTICS_EXPORT", + user ); }; diff --git a/apps/app/components/analytics/project-modal.tsx b/apps/app/components/analytics/project-modal.tsx index 728ca4ed5..da308582f 100644 --- a/apps/app/components/analytics/project-modal.tsx +++ b/apps/app/components/analytics/project-modal.tsx @@ -26,6 +26,7 @@ import { import { IAnalyticsParams, IWorkspace } from "types"; // fetch-keys import { ANALYTICS, CYCLE_DETAILS, MODULE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys"; +import useUserAuth from "hooks/use-user-auth"; type Props = { isOpen: boolean; @@ -47,6 +48,8 @@ export const AnalyticsProjectModal: React.FC = ({ isOpen, onClose }) => { const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { user } = useUserAuth(); + const { control, watch, setValue } = useForm({ defaultValues }); const params: IAnalyticsParams = { @@ -136,7 +139,8 @@ export const AnalyticsProjectModal: React.FC = ({ isOpen, onClose }) => { trackEventServices.trackAnalyticsEvent( eventPayload, - cycleId ? `CYCLE_${eventType}` : moduleId ? `MODULE_${eventType}` : `PROJECT_${eventType}` + cycleId ? `CYCLE_${eventType}` : moduleId ? `MODULE_${eventType}` : `PROJECT_${eventType}`, + user ); }; @@ -210,6 +214,7 @@ export const AnalyticsProjectModal: React.FC = ({ isOpen, onClose }) => { control={control} setValue={setValue} fullScreen={fullScreen} + user={user} /> diff --git a/apps/app/components/command-palette/change-issue-assignee.tsx b/apps/app/components/command-palette/change-issue-assignee.tsx index fd853ef0f..56351335e 100644 --- a/apps/app/components/command-palette/change-issue-assignee.tsx +++ b/apps/app/components/command-palette/change-issue-assignee.tsx @@ -7,7 +7,7 @@ import { Command } from "cmdk"; // services import issuesService from "services/issues.service"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; // constants import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, PROJECT_MEMBERS } from "constants/fetch-keys"; // icons @@ -18,9 +18,10 @@ import { Avatar } from "components/ui"; type Props = { setIsPaletteOpen: Dispatch>; issue: IIssue; + user: ICurrentUserResponse | undefined; }; -export const ChangeIssueAssignee: React.FC = ({ setIsPaletteOpen, issue }) => { +export const ChangeIssueAssignee: React.FC = ({ setIsPaletteOpen, issue, user }) => { const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; @@ -71,7 +72,7 @@ export const ChangeIssueAssignee: React.FC = ({ setIsPaletteOpen, issue } const payload = { ...formData }; await issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) .then(() => { mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); }) diff --git a/apps/app/components/command-palette/change-issue-priority.tsx b/apps/app/components/command-palette/change-issue-priority.tsx index d0e6258c5..2db03268d 100644 --- a/apps/app/components/command-palette/change-issue-priority.tsx +++ b/apps/app/components/command-palette/change-issue-priority.tsx @@ -7,7 +7,7 @@ import { Command } from "cmdk"; // services import issuesService from "services/issues.service"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; // constants import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; import { PRIORITIES } from "constants/project"; @@ -17,9 +17,10 @@ import { CheckIcon, getPriorityIcon } from "components/icons"; type Props = { setIsPaletteOpen: Dispatch>; issue: IIssue; + user: ICurrentUserResponse; }; -export const ChangeIssuePriority: React.FC = ({ setIsPaletteOpen, issue }) => { +export const ChangeIssuePriority: React.FC = ({ setIsPaletteOpen, issue, user }) => { const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; @@ -42,7 +43,7 @@ export const ChangeIssuePriority: React.FC = ({ setIsPaletteOpen, issue } const payload = { ...formData }; await issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) .then(() => { mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); }) diff --git a/apps/app/components/command-palette/change-issue-state.tsx b/apps/app/components/command-palette/change-issue-state.tsx index 8e3255b0a..0378df878 100644 --- a/apps/app/components/command-palette/change-issue-state.tsx +++ b/apps/app/components/command-palette/change-issue-state.tsx @@ -12,7 +12,7 @@ import { getStatesList } from "helpers/state.helper"; import issuesService from "services/issues.service"; import stateService from "services/state.service"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; // fetch keys import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys"; // icons @@ -21,9 +21,10 @@ import { CheckIcon, getStateGroupIcon } from "components/icons"; type Props = { setIsPaletteOpen: Dispatch>; issue: IIssue; + user: ICurrentUserResponse | undefined; }; -export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue }) => { +export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue, user }) => { const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; @@ -53,7 +54,7 @@ export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue }) = const payload = { ...formData }; await issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) .then(() => { mutateIssueDetails(); mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx index 06f4f3c2b..ff889898e 100644 --- a/apps/app/components/command-palette/command-pallette.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -136,7 +136,7 @@ export const CommandPalette: React.FC = () => { const payload = { ...formData }; await issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) .then(() => { mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); mutate(ISSUE_DETAILS(issueId as string)); @@ -330,25 +330,33 @@ export const CommandPalette: React.FC = () => { <> {workspaceSlug && ( - + )} {projectId && ( <> setIsCreateCycleModalOpen(false)} + user={user} /> setIsCreateViewModalOpen(false)} isOpen={isCreateViewModalOpen} + user={user} /> setIsCreateUpdatePageModalOpen(false)} + user={user} /> )} @@ -357,6 +365,7 @@ export const CommandPalette: React.FC = () => { handleClose={() => setDeleteIssueModal(false)} isOpen={deleteIssueModal} data={issueDetails} + user={user} /> )} @@ -367,6 +376,7 @@ export const CommandPalette: React.FC = () => { { )} @@ -861,12 +872,14 @@ export const CommandPalette: React.FC = () => { )} {page === "change-issue-assignee" && issueDetails && ( )} {page === "change-interface-theme" && ( diff --git a/apps/app/components/core/board-view/all-boards.tsx b/apps/app/components/core/board-view/all-boards.tsx index 495cee0a5..3e67e86b5 100644 --- a/apps/app/components/core/board-view/all-boards.tsx +++ b/apps/app/components/core/board-view/all-boards.tsx @@ -5,7 +5,7 @@ import { SingleBoard } from "components/core/board-view/single-board"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; // types -import { IIssue, IState, UserAuth } from "types"; +import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types"; import { getStateGroupIcon } from "components/icons"; type Props = { @@ -19,6 +19,7 @@ type Props = { handleTrashBox: (isDragging: boolean) => void; removeIssue: ((bridgeId: string, issueId: string) => void) | null; isCompleted?: boolean; + user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; @@ -33,6 +34,7 @@ export const AllBoards: React.FC = ({ handleTrashBox, removeIssue, isCompleted = false, + user, userAuth, }) => { const { @@ -65,6 +67,7 @@ export const AllBoards: React.FC = ({ handleTrashBox={handleTrashBox} removeIssue={removeIssue} isCompleted={isCompleted} + user={user} userAuth={userAuth} /> ); diff --git a/apps/app/components/core/board-view/single-board.tsx b/apps/app/components/core/board-view/single-board.tsx index 16fe0e887..f7816cd5b 100644 --- a/apps/app/components/core/board-view/single-board.tsx +++ b/apps/app/components/core/board-view/single-board.tsx @@ -17,7 +17,7 @@ import { PlusIcon } from "@heroicons/react/24/outline"; // helpers import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // types -import { IIssue, IState, UserAuth } from "types"; +import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types"; type Props = { type?: "issue" | "cycle" | "module"; @@ -31,6 +31,7 @@ type Props = { handleTrashBox: (isDragging: boolean) => void; removeIssue: ((bridgeId: string, issueId: string) => void) | null; isCompleted?: boolean; + user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; @@ -46,6 +47,7 @@ export const SingleBoard: React.FC = ({ handleTrashBox, removeIssue, isCompleted = false, + user, userAuth, }) => { // collapse/expand @@ -129,6 +131,7 @@ export const SingleBoard: React.FC = ({ removeIssue(issue.bridge_id, issue.id); }} isCompleted={isCompleted} + user={user} userAuth={userAuth} /> )} diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx index 7f8a89aa2..e2d530153 100644 --- a/apps/app/components/core/board-view/single-issue.tsx +++ b/apps/app/components/core/board-view/single-issue.tsx @@ -44,7 +44,7 @@ import { LayerDiagonalIcon } from "components/icons"; import { handleIssuesMutation } from "constants/issue"; import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // types -import { IIssue, Properties, TIssueGroupByOptions, UserAuth } from "types"; +import { ICurrentUserResponse, IIssue, Properties, TIssueGroupByOptions, UserAuth } from "types"; // fetch-keys import { CYCLE_DETAILS, @@ -69,6 +69,7 @@ type Props = { handleDeleteIssue: (issue: IIssue) => void; handleTrashBox: (isDragging: boolean) => void; isCompleted?: boolean; + user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; @@ -87,6 +88,7 @@ export const SingleBoardIssue: React.FC = ({ handleDeleteIssue, handleTrashBox, isCompleted = false, + user, userAuth, }) => { // context menu @@ -170,7 +172,7 @@ export const SingleBoardIssue: React.FC = ({ } issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId, formData) + .patchIssue(workspaceSlug as string, projectId as string, issueId, formData, user) .then(() => { if (cycleId) { mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); @@ -342,6 +344,7 @@ export const SingleBoardIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} isNotAllowed={isNotAllowed} + user={user} selfPositioned /> )} @@ -350,6 +353,7 @@ export const SingleBoardIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} isNotAllowed={isNotAllowed} + user={user} selfPositioned /> )} @@ -357,6 +361,7 @@ export const SingleBoardIssue: React.FC = ({ )} @@ -384,6 +389,7 @@ export const SingleBoardIssue: React.FC = ({ partialUpdateIssue={partialUpdateIssue} isNotAllowed={isNotAllowed} tooltipPosition="left" + user={user} selfPositioned /> )} @@ -392,6 +398,7 @@ export const SingleBoardIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} isNotAllowed={isNotAllowed} + user={user} selfPositioned /> )} diff --git a/apps/app/components/core/bulk-delete-issues-modal.tsx b/apps/app/components/core/bulk-delete-issues-modal.tsx index 7fe2181f6..603efe8e3 100644 --- a/apps/app/components/core/bulk-delete-issues-modal.tsx +++ b/apps/app/components/core/bulk-delete-issues-modal.tsx @@ -18,7 +18,7 @@ import { DangerButton, SecondaryButton } from "components/ui"; import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { LayerDiagonalIcon } from "components/icons"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; // fetch keys import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; @@ -29,9 +29,10 @@ type FormInput = { type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; + user: ICurrentUserResponse | undefined; }; -export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen }) => { +export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen, user }) => { const [query, setQuery] = useState(""); const router = useRouter(); @@ -91,9 +92,14 @@ export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen }) => if (workspaceSlug && projectId) { await issuesServices - .bulkDeleteIssues(workspaceSlug as string, projectId as string, { - issue_ids: data.delete_issue_ids, - }) + .bulkDeleteIssues( + workspaceSlug as string, + projectId as string, + { + issue_ids: data.delete_issue_ids, + }, + user + ) .then((res) => { setToastAlert({ title: "Success", diff --git a/apps/app/components/core/calendar-view/calendar.tsx b/apps/app/components/core/calendar-view/calendar.tsx index ce453f898..fa29eb9f7 100644 --- a/apps/app/components/core/calendar-view/calendar.tsx +++ b/apps/app/components/core/calendar-view/calendar.tsx @@ -24,7 +24,7 @@ import { formatDate, } from "helpers/calendar.helper"; // types -import { ICalendarRange, IIssue, UserAuth } from "types"; +import { ICalendarRange, ICurrentUserResponse, IIssue, UserAuth } from "types"; // fetch-keys import { CYCLE_ISSUES_WITH_PARAMS, @@ -38,6 +38,7 @@ type Props = { handleDeleteIssue: (issue: IIssue) => void; addIssueToDate: (date: string) => void; isCompleted: boolean; + user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; @@ -46,6 +47,7 @@ export const CalendarView: React.FC = ({ handleDeleteIssue, addIssueToDate, isCompleted = false, + user, userAuth, }) => { const [showWeekEnds, setShowWeekEnds] = useState(false); @@ -134,9 +136,15 @@ export const CalendarView: React.FC = ({ ); issuesService - .patchIssue(workspaceSlug as string, projectId as string, draggableId, { - target_date: destination?.droppableId, - }) + .patchIssue( + workspaceSlug as string, + projectId as string, + draggableId, + { + target_date: destination?.droppableId, + }, + user + ) .then(() => mutate(fetchKey)); }; @@ -219,6 +227,7 @@ export const CalendarView: React.FC = ({ addIssueToDate={addIssueToDate} isMonthlyView={isMonthlyView} showWeekEnds={showWeekEnds} + user={user} isNotAllowed={isNotAllowed} /> ))} diff --git a/apps/app/components/core/calendar-view/single-date.tsx b/apps/app/components/core/calendar-view/single-date.tsx index fd552188c..2dbfe5dfd 100644 --- a/apps/app/components/core/calendar-view/single-date.tsx +++ b/apps/app/components/core/calendar-view/single-date.tsx @@ -10,7 +10,7 @@ import { PlusSmallIcon } from "@heroicons/react/24/outline"; // helper import { formatDate } from "helpers/calendar.helper"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; type Props = { handleEditIssue: (issue: IIssue) => void; @@ -23,6 +23,7 @@ type Props = { addIssueToDate: (date: string) => void; isMonthlyView: boolean; showWeekEnds: boolean; + user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -34,6 +35,7 @@ export const SingleCalendarDate: React.FC = ({ addIssueToDate, isMonthlyView, showWeekEnds, + user, isNotAllowed, }) => { const [showAllIssues, setShowAllIssues] = useState(false); @@ -72,6 +74,7 @@ export const SingleCalendarDate: React.FC = ({ issue={issue} handleEditIssue={handleEditIssue} handleDeleteIssue={handleDeleteIssue} + user={user} isNotAllowed={isNotAllowed} /> )} diff --git a/apps/app/components/core/calendar-view/single-issue.tsx b/apps/app/components/core/calendar-view/single-issue.tsx index 9102f7338..4fa9def3b 100644 --- a/apps/app/components/core/calendar-view/single-issue.tsx +++ b/apps/app/components/core/calendar-view/single-issue.tsx @@ -28,7 +28,7 @@ import { LayerDiagonalIcon } from "components/icons"; // helper import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // type -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; // fetch-keys import { CYCLE_ISSUES_WITH_PARAMS, @@ -44,6 +44,7 @@ type Props = { provided: DraggableProvided; snapshot: DraggableStateSnapshot; issue: IIssue; + user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -54,6 +55,7 @@ export const SingleCalendarIssue: React.FC = ({ provided, snapshot, issue, + user, isNotAllowed, }) => { const router = useRouter(); @@ -95,7 +97,7 @@ export const SingleCalendarIssue: React.FC = ({ ); issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, formData) + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, formData, user) .then(() => { mutate(fetchKey); }) @@ -183,6 +185,7 @@ export const SingleCalendarIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="left" + user={user} isNotAllowed={isNotAllowed} /> )} @@ -192,6 +195,7 @@ export const SingleCalendarIssue: React.FC = ({ partialUpdateIssue={partialUpdateIssue} position="left" isNotAllowed={isNotAllowed} + user={user} /> )} @@ -199,6 +203,7 @@ export const SingleCalendarIssue: React.FC = ({ )} @@ -227,6 +232,7 @@ export const SingleCalendarIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="left" + user={user} isNotAllowed={isNotAllowed} /> )} @@ -235,6 +241,7 @@ export const SingleCalendarIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="left" + user={user} isNotAllowed={isNotAllowed} /> )} diff --git a/apps/app/components/core/gpt-assistant-modal.tsx b/apps/app/components/core/gpt-assistant-modal.tsx index 90c558717..a62b3e413 100644 --- a/apps/app/components/core/gpt-assistant-modal.tsx +++ b/apps/app/components/core/gpt-assistant-modal.tsx @@ -10,6 +10,7 @@ import aiService from "services/ai.service"; import trackEventServices from "services/track-event.service"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // ui import { Input, PrimaryButton, SecondaryButton } from "components/ui"; @@ -60,6 +61,8 @@ export const GptAssistantModal: React.FC = ({ const router = useRouter(); const { workspaceSlug } = router.query; + const { user } = useUserAuth(); + const editorRef = useRef(null); const { setToastAlert } = useToast(); @@ -97,10 +100,15 @@ export const GptAssistantModal: React.FC = ({ } await aiService - .createGptTask(workspaceSlug as string, projectId as string, { - prompt: content && content !== "" ? content : htmlContent ?? "", - task: formData.task, - }) + .createGptTask( + workspaceSlug as string, + projectId as string, + { + prompt: content && content !== "" ? content : htmlContent ?? "", + task: formData.task, + }, + user + ) .then((res) => { setResponse(res.response_html); setFocus("task"); @@ -190,10 +198,15 @@ export const GptAssistantModal: React.FC = ({ if (block) trackEventServices.trackUseGPTResponseEvent( block, - "USE_GPT_RESPONSE_IN_PAGE_BLOCK" + "USE_GPT_RESPONSE_IN_PAGE_BLOCK", + user ); else if (issue) - trackEventServices.trackUseGPTResponseEvent(issue, "USE_GPT_RESPONSE_IN_ISSUE"); + trackEventServices.trackUseGPTResponseEvent( + issue, + "USE_GPT_RESPONSE_IN_ISSUE", + user + ); }} > Use this response diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx index cc0672843..bfa6b9f46 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/issues-view.tsx @@ -17,6 +17,7 @@ import { useProjectMyMembership } from "contexts/project-member.context"; // hooks import useToast from "hooks/use-toast"; import useIssuesView from "hooks/use-issues-view"; +import useUserAuth from "hooks/use-user-auth"; // components import { AllLists, AllBoards, FilterList, CalendarView, GanttChartView } from "components/core"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; @@ -89,6 +90,8 @@ export const IssuesView: React.FC = ({ const { memberRole } = useProjectMyMembership(); + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const { @@ -220,11 +223,17 @@ export const IssuesView: React.FC = ({ // patch request issuesService - .patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, { - priority: draggedItem.priority, - state: draggedItem.state, - sort_order: draggedItem.sort_order, - }) + .patchIssue( + workspaceSlug as string, + projectId as string, + draggedItem.id, + { + priority: draggedItem.priority, + state: draggedItem.state, + sort_order: draggedItem.sort_order, + }, + user + ) .then((response) => { const sourceStateBeforeDrag = states.find((state) => state.name === source.droppableId); @@ -232,14 +241,17 @@ export const IssuesView: React.FC = ({ sourceStateBeforeDrag?.group !== "completed" && response?.state_detail?.group === "completed" ) - trackEventServices.trackIssueMarkedAsDoneEvent({ - workspaceSlug, - workspaceId: draggedItem.workspace, - projectName: draggedItem.project_detail.name, - projectIdentifier: draggedItem.project_detail.identifier, - projectId, - issueId: draggedItem.id, - }); + trackEventServices.trackIssueMarkedAsDoneEvent( + { + workspaceSlug, + workspaceId: draggedItem.workspace, + projectName: draggedItem.project_detail.name, + projectIdentifier: draggedItem.project_detail.identifier, + projectId, + issueId: draggedItem.id, + }, + user + ); if (cycleId) { mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); @@ -419,6 +431,7 @@ export const IssuesView: React.FC = ({ isOpen={createViewModal !== null} handleClose={() => setCreateViewModal(null)} preLoadedData={createViewModal} + user={user} /> = ({ handleClose={() => setDeleteIssueModal(false)} isOpen={deleteIssueModal} data={issueToDelete} + user={user} /> setTransferIssuesModal(false)} @@ -508,6 +522,7 @@ export const IssuesView: React.FC = ({ : null } isCompleted={isCompleted} + user={user} userAuth={memberRole} /> ) : issueView === "kanban" ? ( @@ -528,6 +543,7 @@ export const IssuesView: React.FC = ({ : null } isCompleted={isCompleted} + user={user} userAuth={memberRole} /> ) : issueView === "calendar" ? ( @@ -536,6 +552,7 @@ export const IssuesView: React.FC = ({ handleDeleteIssue={handleDeleteIssue} addIssueToDate={addIssueToDate} isCompleted={isCompleted} + user={user} userAuth={memberRole} /> ) : ( diff --git a/apps/app/components/core/list-view/all-lists.tsx b/apps/app/components/core/list-view/all-lists.tsx index d8fa8b9ee..fd063728a 100644 --- a/apps/app/components/core/list-view/all-lists.tsx +++ b/apps/app/components/core/list-view/all-lists.tsx @@ -3,7 +3,7 @@ import useIssuesView from "hooks/use-issues-view"; // components import { SingleList } from "components/core/list-view/single-list"; // types -import { IIssue, IState, UserAuth } from "types"; +import { ICurrentUserResponse, IIssue, IState, UserAuth } from "types"; // types type Props = { @@ -16,6 +16,7 @@ type Props = { openIssuesListModal?: (() => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null; isCompleted?: boolean; + user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; @@ -29,6 +30,7 @@ export const AllLists: React.FC = ({ handleDeleteIssue, removeIssue, isCompleted = false, + user, userAuth, }) => { const { groupedByIssues, groupByProperty: selectedGroup, showEmptyGroups } = useIssuesView(); @@ -58,6 +60,7 @@ export const AllLists: React.FC = ({ openIssuesListModal={type !== "issue" ? openIssuesListModal : null} removeIssue={removeIssue} isCompleted={isCompleted} + user={user} userAuth={userAuth} /> ); diff --git a/apps/app/components/core/list-view/single-issue.tsx b/apps/app/components/core/list-view/single-issue.tsx index 6f6a5ab4f..ea4ebc811 100644 --- a/apps/app/components/core/list-view/single-issue.tsx +++ b/apps/app/components/core/list-view/single-issue.tsx @@ -36,7 +36,7 @@ import { LayerDiagonalIcon } from "components/icons"; import { copyTextToClipboard, truncateText } from "helpers/string.helper"; import { handleIssuesMutation } from "constants/issue"; // types -import { IIssue, Properties, UserAuth } from "types"; +import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types"; // fetch-keys import { CYCLE_DETAILS, @@ -57,6 +57,7 @@ type Props = { removeIssue?: (() => void) | null; handleDeleteIssue: (issue: IIssue) => void; isCompleted?: boolean; + user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; @@ -71,6 +72,7 @@ export const SingleListIssue: React.FC = ({ groupTitle, handleDeleteIssue, isCompleted = false, + user, userAuth, }) => { // context menu @@ -141,7 +143,7 @@ export const SingleListIssue: React.FC = ({ ); issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId, formData) + .patchIssue(workspaceSlug as string, projectId as string, issueId, formData, user) .then(() => { if (cycleId) { mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); @@ -241,6 +243,7 @@ export const SingleListIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="right" + user={user} isNotAllowed={isNotAllowed} /> )} @@ -249,6 +252,7 @@ export const SingleListIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="right" + user={user} isNotAllowed={isNotAllowed} /> )} @@ -256,6 +260,7 @@ export const SingleListIssue: React.FC = ({ )} @@ -284,6 +289,7 @@ export const SingleListIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="right" + user={user} isNotAllowed={isNotAllowed} /> )} @@ -292,6 +298,7 @@ export const SingleListIssue: React.FC = ({ issue={issue} partialUpdateIssue={partialUpdateIssue} position="right" + user={user} isNotAllowed={isNotAllowed} /> )} diff --git a/apps/app/components/core/list-view/single-list.tsx b/apps/app/components/core/list-view/single-list.tsx index dd5ffb110..a8c6e4a51 100644 --- a/apps/app/components/core/list-view/single-list.tsx +++ b/apps/app/components/core/list-view/single-list.tsx @@ -19,7 +19,14 @@ import { getPriorityIcon, getStateGroupIcon } from "components/icons"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; // types -import { IIssue, IIssueLabels, IState, TIssueGroupByOptions, UserAuth } from "types"; +import { + ICurrentUserResponse, + IIssue, + IIssueLabels, + IState, + TIssueGroupByOptions, + UserAuth, +} from "types"; // fetch-keys import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys"; @@ -39,6 +46,7 @@ type Props = { openIssuesListModal?: (() => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null; isCompleted?: boolean; + user: ICurrentUserResponse | undefined; userAuth: UserAuth; }; @@ -56,6 +64,7 @@ export const SingleList: React.FC = ({ openIssuesListModal, removeIssue, isCompleted = false, + user, userAuth, }) => { const router = useRouter(); @@ -208,6 +217,7 @@ export const SingleList: React.FC = ({ removeIssue(issue.bridge_id, issue.id); }} isCompleted={isCompleted} + user={user} userAuth={userAuth} /> )) diff --git a/apps/app/components/cycles/cycles-view.tsx b/apps/app/components/cycles/cycles-view.tsx index fc720c251..261e81018 100644 --- a/apps/app/components/cycles/cycles-view.tsx +++ b/apps/app/components/cycles/cycles-view.tsx @@ -8,6 +8,7 @@ import { mutate } from "swr"; import cyclesService from "services/cycles.service"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // components import { CreateUpdateCycleModal, @@ -48,6 +49,7 @@ export const CyclesView: React.FC = ({ cycles, viewType }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); const { setToastAlert } = useToast(); const handleEditCycle = (cycle: ICycle) => { @@ -158,11 +160,13 @@ export const CyclesView: React.FC = ({ cycles, viewType }) => { isOpen={createUpdateCycleModal} handleClose={() => setCreateUpdateCycleModal(false)} data={selectedCycleToUpdate} + user={user} /> {cycles ? ( cycles.length > 0 ? ( diff --git a/apps/app/components/cycles/delete-cycle-modal.tsx b/apps/app/components/cycles/delete-cycle-modal.tsx index 187c9694c..3f2de2913 100644 --- a/apps/app/components/cycles/delete-cycle-modal.tsx +++ b/apps/app/components/cycles/delete-cycle-modal.tsx @@ -14,11 +14,12 @@ import { DangerButton, SecondaryButton } from "components/ui"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types -import type { ICycle } from "types"; +import type { ICurrentUserResponse, ICycle } from "types"; type TConfirmCycleDeletionProps = { isOpen: boolean; setIsOpen: React.Dispatch>; data?: ICycle | null; + user: ICurrentUserResponse | undefined; }; // fetch-keys import { @@ -34,6 +35,7 @@ export const DeleteCycleModal: React.FC = ({ isOpen, setIsOpen, data, + user, }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); @@ -53,7 +55,7 @@ export const DeleteCycleModal: React.FC = ({ setIsDeleteLoading(true); await cycleService - .deleteCycle(workspaceSlug as string, data.project, data.id) + .deleteCycle(workspaceSlug as string, data.project, data.id, user) .then(() => { const cycleType = getDateRangeStatus(data.start_date, data.end_date); const fetchKey = diff --git a/apps/app/components/cycles/modal.tsx b/apps/app/components/cycles/modal.tsx index e27b5ac32..3c1c16506 100644 --- a/apps/app/components/cycles/modal.tsx +++ b/apps/app/components/cycles/modal.tsx @@ -15,7 +15,7 @@ import { CycleForm } from "components/cycles"; // helper import { getDateRangeStatus, isDateGreaterThanToday } from "helpers/date-time.helper"; // types -import type { ICycle } from "types"; +import type { ICurrentUserResponse, ICycle } from "types"; // fetch keys import { COMPLETED_CYCLES_LIST, @@ -30,12 +30,14 @@ type CycleModalProps = { isOpen: boolean; handleClose: () => void; data?: ICycle | null; + user: ICurrentUserResponse | undefined; }; export const CreateUpdateCycleModal: React.FC = ({ isOpen, handleClose, data, + user, }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -46,7 +48,7 @@ export const CreateUpdateCycleModal: React.FC = ({ if (!workspaceSlug || !projectId) return; await cycleService - .createCycle(workspaceSlug.toString(), projectId.toString(), payload) + .createCycle(workspaceSlug.toString(), projectId.toString(), payload, user) .then((res) => { switch (getDateRangeStatus(res.start_date, res.end_date)) { case "completed": @@ -84,7 +86,7 @@ export const CreateUpdateCycleModal: React.FC = ({ if (!workspaceSlug || !projectId) return; await cycleService - .updateCycle(workspaceSlug.toString(), projectId.toString(), cycleId, payload) + .updateCycle(workspaceSlug.toString(), projectId.toString(), cycleId, payload, user) .then((res) => { switch (getDateRangeStatus(data?.start_date, data?.end_date)) { case "completed": diff --git a/apps/app/components/cycles/select.tsx b/apps/app/components/cycles/select.tsx index b5ec8a462..2fdda69b5 100644 --- a/apps/app/components/cycles/select.tsx +++ b/apps/app/components/cycles/select.tsx @@ -4,6 +4,7 @@ import { useRouter } from "next/router"; import useSWR from "swr"; +import useUserAuth from "hooks/use-user-auth"; // headless ui import { Listbox, Transition } from "@headlessui/react"; // icons @@ -35,6 +36,8 @@ export const CycleSelect: React.FC = ({ const router = useRouter(); const { workspaceSlug } = router.query; + const { user } = useUserAuth(); + const { data: cycles } = useSWR( workspaceSlug && projectId ? CYCLES_LIST(projectId) : null, workspaceSlug && projectId @@ -54,7 +57,11 @@ export const CycleSelect: React.FC = ({ return ( <> - + {({ open }) => ( <> diff --git a/apps/app/components/cycles/sidebar.tsx b/apps/app/components/cycles/sidebar.tsx index 494383fc4..9a7d04f3e 100644 --- a/apps/app/components/cycles/sidebar.tsx +++ b/apps/app/components/cycles/sidebar.tsx @@ -39,7 +39,7 @@ import { renderShortDate, } from "helpers/date-time.helper"; // types -import { ICycle, IIssue } from "types"; +import { ICurrentUserResponse, ICycle, IIssue } from "types"; // fetch-keys import { CYCLE_DETAILS, CYCLE_ISSUES } from "constants/fetch-keys"; @@ -48,6 +48,7 @@ type Props = { isOpen: boolean; cycleStatus: string; isCompleted: boolean; + user: ICurrentUserResponse | undefined; }; export const CycleDetailsSidebar: React.FC = ({ @@ -55,6 +56,7 @@ export const CycleDetailsSidebar: React.FC = ({ isOpen, cycleStatus, isCompleted, + user, }) => { const [cycleDeleteModal, setCycleDeleteModal] = useState(false); @@ -94,7 +96,7 @@ export const CycleDetailsSidebar: React.FC = ({ ); cyclesService - .patchCycle(workspaceSlug as string, projectId as string, cycleId as string, data) + .patchCycle(workspaceSlug as string, projectId as string, cycleId as string, data, user) .then(() => mutate(CYCLE_DETAILS(cycleId as string))) .catch((e) => console.log(e)); }; @@ -294,7 +296,12 @@ export const CycleDetailsSidebar: React.FC = ({ return ( <> - +
    void; data?: IEstimate; + user: ICurrentUserResponse | undefined; }; type FormValues = { @@ -49,7 +50,7 @@ const defaultValues: Partial = { value6: "", }; -export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, isOpen }) => { +export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, isOpen, user }) => { const { register, formState: { isSubmitting }, @@ -73,7 +74,7 @@ export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, if (!workspaceSlug || !projectId) return; await estimatesService - .createEstimate(workspaceSlug as string, projectId as string, payload) + .createEstimate(workspaceSlug as string, projectId as string, payload, user) .then(() => { mutate(ESTIMATES_LIST(projectId as string)); onClose(); @@ -118,7 +119,13 @@ export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, ); await estimatesService - .patchEstimate(workspaceSlug as string, projectId as string, data?.id as string, payload) + .patchEstimate( + workspaceSlug as string, + projectId as string, + data?.id as string, + payload, + user + ) .then(() => { mutate(ESTIMATES_LIST(projectId.toString())); mutate(ESTIMATE_DETAILS(data.id)); diff --git a/apps/app/components/estimates/single-estimate.tsx b/apps/app/components/estimates/single-estimate.tsx index 43dcc45b7..5e331c139 100644 --- a/apps/app/components/estimates/single-estimate.tsx +++ b/apps/app/components/estimates/single-estimate.tsx @@ -16,15 +16,17 @@ import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; // helpers import { orderArrayBy } from "helpers/array.helper"; // types -import { IEstimate } from "types"; +import { ICurrentUserResponse, IEstimate } from "types"; type Props = { + user: ICurrentUserResponse | undefined; estimate: IEstimate; editEstimate: (estimate: IEstimate) => void; handleEstimateDelete: (estimateId: string) => void; }; export const SingleEstimate: React.FC = ({ + user, estimate, editEstimate, handleEstimateDelete, @@ -52,7 +54,7 @@ export const SingleEstimate: React.FC = ({ }, false); await projectService - .updateProject(workspaceSlug as string, projectId as string, payload) + .updateProject(workspaceSlug as string, projectId as string, payload, user) .catch(() => { setToastAlert({ type: "error", diff --git a/apps/app/components/integration/delete-import-modal.tsx b/apps/app/components/integration/delete-import-modal.tsx index 8c5ab3803..cd0b12a2a 100644 --- a/apps/app/components/integration/delete-import-modal.tsx +++ b/apps/app/components/integration/delete-import-modal.tsx @@ -15,7 +15,7 @@ import { DangerButton, Input, SecondaryButton } from "components/ui"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types -import { IImporterService } from "types"; +import { ICurrentUserResponse, IImporterService } from "types"; // fetch-keys import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys"; @@ -23,9 +23,10 @@ type Props = { isOpen: boolean; handleClose: () => void; data: IImporterService | null; + user: ICurrentUserResponse | undefined; }; -export const DeleteImportModal: React.FC = ({ isOpen, handleClose, data }) => { +export const DeleteImportModal: React.FC = ({ isOpen, handleClose, data, user }) => { const [deleteLoading, setDeleteLoading] = useState(false); const [confirmDeleteImport, setConfirmDeleteImport] = useState(false); @@ -45,7 +46,7 @@ export const DeleteImportModal: React.FC = ({ isOpen, handleClose, data } false ); - IntegrationService.deleteImporterService(workspaceSlug as string, data.service, data.id) + IntegrationService.deleteImporterService(workspaceSlug as string, data.service, data.id, user) .catch(() => setToastAlert({ type: "error", diff --git a/apps/app/components/integration/github/root.tsx b/apps/app/components/integration/github/root.tsx index b8bfb346b..96ce845c4 100644 --- a/apps/app/components/integration/github/root.tsx +++ b/apps/app/components/integration/github/root.tsx @@ -27,7 +27,7 @@ import { ArrowLeftIcon, ListBulletIcon } from "@heroicons/react/24/outline"; // images import GithubLogo from "public/services/github.png"; // types -import { IGithubRepoCollaborator, IGithubServiceImportFormData } from "types"; +import { ICurrentUserResponse, IGithubRepoCollaborator, IGithubServiceImportFormData } from "types"; // fetch-keys import { APP_INTEGRATIONS, @@ -89,7 +89,11 @@ const integrationWorkflowData = [ }, ]; -export const GithubImporterRoot = () => { +type Props = { + user: ICurrentUserResponse | undefined; +}; + +export const GithubImporterRoot: React.FC = ({ user }) => { const [currentStep, setCurrentStep] = useState({ state: "import-configure", }); @@ -157,7 +161,7 @@ export const GithubImporterRoot = () => { project_id: formData.project, }; - await GithubIntegrationService.createGithubServiceImport(workspaceSlug as string, payload) + await GithubIntegrationService.createGithubServiceImport(workspaceSlug as string, payload, user) .then(() => { router.push(`/${workspaceSlug}/settings/import-export`); mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)); diff --git a/apps/app/components/integration/guide.tsx b/apps/app/components/integration/guide.tsx index 06f13b752..2db8ef010 100644 --- a/apps/app/components/integration/guide.tsx +++ b/apps/app/components/integration/guide.tsx @@ -6,6 +6,8 @@ import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; +// hooks +import useUserAuth from "hooks/use-user-auth"; // services import IntegrationService from "services/integration"; // components @@ -35,6 +37,8 @@ const IntegrationGuide = () => { const router = useRouter(); const { workspaceSlug, provider } = router.query; + const { user } = useUserAuth(); + const { data: importerServices } = useSWR( workspaceSlug ? IMPORTER_SERVICES_LIST(workspaceSlug as string) : null, workspaceSlug ? () => IntegrationService.getImporterServicesList(workspaceSlug as string) : null @@ -51,6 +55,7 @@ const IntegrationGuide = () => { isOpen={deleteImportModal} handleClose={() => setDeleteImportModal(false)} data={importToDelete} + user={user} />
    {!provider && ( @@ -156,8 +161,8 @@ const IntegrationGuide = () => { )} - {provider && provider === "github" && } - {provider && provider === "jira" && } + {provider && provider === "github" && } + {provider && provider === "jira" && }
    ); diff --git a/apps/app/components/integration/jira/root.tsx b/apps/app/components/integration/jira/root.tsx index 6ec39a36a..b1098c9cb 100644 --- a/apps/app/components/integration/jira/root.tsx +++ b/apps/app/components/integration/jira/root.tsx @@ -35,7 +35,7 @@ import { import JiraLogo from "public/services/jira.png"; -import { IJiraImporterForm } from "types"; +import { ICurrentUserResponse, IJiraImporterForm } from "types"; const integrationWorkflowData: Array<{ title: string; @@ -64,7 +64,11 @@ const integrationWorkflowData: Array<{ }, ]; -export const JiraImporterRoot = () => { +type Props = { + user: ICurrentUserResponse | undefined; +}; + +export const JiraImporterRoot: React.FC = ({ user }) => { const [currentStep, setCurrentStep] = useState({ state: "import-configure", }); @@ -85,7 +89,7 @@ export const JiraImporterRoot = () => { if (!workspaceSlug) return; await jiraImporterService - .createJiraImporter(workspaceSlug.toString(), data) + .createJiraImporter(workspaceSlug.toString(), data, user) .then(() => { mutate(IMPORTER_SERVICES_LIST(workspaceSlug.toString())); router.push(`/${workspaceSlug}/settings/import-export`); diff --git a/apps/app/components/issues/activity.tsx b/apps/app/components/issues/activity.tsx index 7a3aed4a6..1ccafae56 100644 --- a/apps/app/components/issues/activity.tsx +++ b/apps/app/components/issues/activity.tsx @@ -27,7 +27,7 @@ import { Loader } from "components/ui"; import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper"; // types -import { IIssueComment, IIssueLabels } from "types"; +import { ICurrentUserResponse, IIssueComment, IIssueLabels } from "types"; import { PROJECT_ISSUES_ACTIVITY, PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; import useEstimateOption from "hooks/use-estimate-option"; @@ -110,7 +110,11 @@ const activityDetails: { }, }; -export const IssueActivitySection: React.FC = () => { +type Props = { + user: ICurrentUserResponse | undefined; +}; + +export const IssueActivitySection: React.FC = ({ user }) => { const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; @@ -143,7 +147,8 @@ export const IssueActivitySection: React.FC = () => { projectId as string, issueId as string, comment.id, - comment + comment, + user ) .then((res) => { mutateIssueActivities(); @@ -160,7 +165,8 @@ export const IssueActivitySection: React.FC = () => { workspaceSlug as string, projectId as string, issueId as string, - commentId + commentId, + user ) .then(() => mutateIssueActivities()); }; diff --git a/apps/app/components/issues/comment/add-comment.tsx b/apps/app/components/issues/comment/add-comment.tsx index b088be77a..7a5e62636 100644 --- a/apps/app/components/issues/comment/add-comment.tsx +++ b/apps/app/components/issues/comment/add-comment.tsx @@ -14,7 +14,7 @@ import useToast from "hooks/use-toast"; // ui import { Loader, SecondaryButton } from "components/ui"; // types -import type { IIssueComment } from "types"; +import type { ICurrentUserResponse, IIssueComment } from "types"; // fetch-keys import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; @@ -40,7 +40,11 @@ const defaultValues: Partial = { comment_html: "", }; -export const AddComment: React.FC = () => { +type Props = { + user: ICurrentUserResponse | undefined; +}; + +export const AddComment: React.FC = ({ user }) => { const { handleSubmit, control, @@ -67,7 +71,13 @@ export const AddComment: React.FC = () => { ) return; await issuesServices - .createIssueComment(workspaceSlug as string, projectId as string, issueId as string, formData) + .createIssueComment( + workspaceSlug as string, + projectId as string, + issueId as string, + formData, + user + ) .then(() => { mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); reset(defaultValues); diff --git a/apps/app/components/issues/delete-issue-modal.tsx b/apps/app/components/issues/delete-issue-modal.tsx index 5f4d0c2b2..525858b0c 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 { IIssue } from "types"; +import type { ICurrentUserResponse, IIssue } from "types"; // fetch-keys import { CYCLE_ISSUES_WITH_PARAMS, @@ -30,9 +30,10 @@ type Props = { isOpen: boolean; handleClose: () => void; data: IIssue | null; + user: ICurrentUserResponse | undefined; }; -export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data }) => { +export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); @@ -57,7 +58,7 @@ export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data }) if (!workspaceSlug || !projectId || !data) return; await issueServices - .deleteIssue(workspaceSlug as string, projectId as string, data.id) + .deleteIssue(workspaceSlug as string, projectId as string, data.id, user) .then(() => { if (issueView === "calendar") { const calendarFetchKey = cycleId diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx index f09e5aedd..894830bd7 100644 --- a/apps/app/components/issues/form.tsx +++ b/apps/app/components/issues/form.tsx @@ -39,7 +39,7 @@ import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline"; // helpers import { cosineSimilarity } from "helpers/string.helper"; // types -import type { IIssue } from "types"; +import type { ICurrentUserResponse, IIssue } from "types"; // rich-text-editor const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { ssr: false, @@ -91,6 +91,7 @@ export interface IssueFormProps { setCreateMore: React.Dispatch>; handleClose: () => void; status: boolean; + user: ICurrentUserResponse | undefined; } export const IssueForm: FC = ({ @@ -103,6 +104,7 @@ export const IssueForm: FC = ({ setCreateMore, handleClose, status, + user, }) => { // states const [mostSimilarIssue, setMostSimilarIssue] = useState(); @@ -177,10 +179,15 @@ export const IssueForm: FC = ({ setIAmFeelingLucky(true); aiService - .createGptTask(workspaceSlug as string, projectId as string, { - prompt: issueName, - task: "Generate a proper description for this issue in context of a project management software.", - }) + .createGptTask( + workspaceSlug as string, + projectId as string, + { + prompt: issueName, + task: "Generate a proper description for this issue in context of a project management software.", + }, + user + ) .then((res) => { if (res.response === "") setToastAlert({ @@ -227,12 +234,18 @@ export const IssueForm: FC = ({ isOpen={stateModal} handleClose={() => setStateModal(false)} projectId={projectId} + user={user} + /> + setCycleModal(false)} + user={user} /> - setCycleModal(false)} /> setLabelModal(false)} projectId={projectId} + user={user} /> )} diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx index 3be8e58ae..3210e350a 100644 --- a/apps/app/components/issues/modal.tsx +++ b/apps/app/components/issues/modal.tsx @@ -102,9 +102,15 @@ export const CreateUpdateIssueModal: React.FC = ({ if (!workspaceSlug || !projectId) return; await issuesService - .addIssueToCycle(workspaceSlug as string, activeProject ?? "", cycleId, { - issues: [issueId], - }) + .addIssueToCycle( + workspaceSlug as string, + activeProject ?? "", + cycleId, + { + issues: [issueId], + }, + user + ) .then(() => { if (cycleId) { mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId, params)); @@ -117,9 +123,15 @@ export const CreateUpdateIssueModal: React.FC = ({ if (!workspaceSlug || !projectId) return; await modulesService - .addIssuesToModule(workspaceSlug as string, activeProject ?? "", moduleId as string, { - issues: [issueId], - }) + .addIssuesToModule( + workspaceSlug as string, + activeProject ?? "", + moduleId as string, + { + issues: [issueId], + }, + user + ) .then(() => { if (moduleId) { mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)); @@ -148,7 +160,7 @@ export const CreateUpdateIssueModal: React.FC = ({ if (!workspaceSlug) return; await issuesService - .createIssues(workspaceSlug as string, activeProject ?? "", payload) + .createIssues(workspaceSlug as string, activeProject ?? "", payload, user) .then(async (res) => { mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle); @@ -180,7 +192,7 @@ export const CreateUpdateIssueModal: React.FC = ({ const updateIssue = async (payload: Partial) => { await issuesService - .updateIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload) + .updateIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user) .then((res) => { if (isUpdatingSingleIssue) { mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); @@ -261,6 +273,7 @@ export const CreateUpdateIssueModal: React.FC = ({ projectId={activeProject ?? ""} setActiveProject={setActiveProject} status={data ? true : false} + user={user} /> diff --git a/apps/app/components/issues/my-issues-list-item.tsx b/apps/app/components/issues/my-issues-list-item.tsx index 06cd6f87f..e026b2f89 100644 --- a/apps/app/components/issues/my-issues-list-item.tsx +++ b/apps/app/components/issues/my-issues-list-item.tsx @@ -7,6 +7,7 @@ import { mutate } from "swr"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // services import issuesService from "services/issues.service"; // components @@ -37,6 +38,9 @@ type Props = { export const MyIssuesListItem: React.FC = ({ issue, properties, projectId }) => { const router = useRouter(); const { workspaceSlug } = router.query; + + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const partialUpdateIssue = useCallback( @@ -55,7 +59,7 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId ); issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId, formData) + .patchIssue(workspaceSlug as string, projectId as string, issueId, formData, user) .then((res) => { mutate(USER_ISSUE(workspaceSlug as string)); }) @@ -110,6 +114,7 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId )} @@ -117,6 +122,7 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId )} @@ -124,6 +130,7 @@ export const MyIssuesListItem: React.FC = ({ issue, properties, projectId )} diff --git a/apps/app/components/issues/sidebar.tsx b/apps/app/components/issues/sidebar.tsx index 869dda5c1..5b15fd46f 100644 --- a/apps/app/components/issues/sidebar.tsx +++ b/apps/app/components/issues/sidebar.tsx @@ -12,6 +12,7 @@ import { TwitterPicker } from "react-color"; import { Popover, Listbox, Transition } from "@headlessui/react"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // services import issuesService from "services/issues.service"; import modulesService from "services/modules.service"; @@ -76,6 +77,8 @@ export const IssueDetailsSidebar: React.FC = ({ const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; + const { user } = useUserAuth(); + const { memberRole } = useProjectMyMembership(); const { setToastAlert } = useToast(); @@ -110,7 +113,7 @@ export const IssueDetailsSidebar: React.FC = ({ const handleNewLabel = (formData: any) => { if (!workspaceSlug || !projectId || isSubmitting) return; issuesService - .createIssueLabel(workspaceSlug as string, projectId as string, formData) + .createIssueLabel(workspaceSlug as string, projectId as string, formData, user) .then((res) => { reset(defaultValues); issueLabelMutate((prevData) => [...(prevData ?? []), res], false); @@ -124,9 +127,15 @@ export const IssueDetailsSidebar: React.FC = ({ if (!workspaceSlug || !projectId || !issueDetail) return; issuesService - .addIssueToCycle(workspaceSlug as string, projectId as string, cycleDetails.id, { - issues: [issueDetail.id], - }) + .addIssueToCycle( + workspaceSlug as string, + projectId as string, + cycleDetails.id, + { + issues: [issueDetail.id], + }, + user + ) .then((res) => { mutate(ISSUE_DETAILS(issueId as string)); }); @@ -139,9 +148,15 @@ export const IssueDetailsSidebar: React.FC = ({ if (!workspaceSlug || !projectId || !issueDetail) return; modulesService - .addIssuesToModule(workspaceSlug as string, projectId as string, moduleDetail.id, { - issues: [issueDetail.id], - }) + .addIssuesToModule( + workspaceSlug as string, + projectId as string, + moduleDetail.id, + { + issues: [issueDetail.id], + }, + user + ) .then((res) => { mutate(ISSUE_DETAILS(issueId as string)); }); @@ -228,6 +243,7 @@ export const IssueDetailsSidebar: React.FC = ({ handleClose={() => setDeleteIssueModal(false)} isOpen={deleteIssueModal} data={issueDetail ?? null} + user={user} />
    diff --git a/apps/app/components/issues/sub-issues-list.tsx b/apps/app/components/issues/sub-issues-list.tsx index 351bf47c5..76424767e 100644 --- a/apps/app/components/issues/sub-issues-list.tsx +++ b/apps/app/components/issues/sub-issues-list.tsx @@ -21,15 +21,16 @@ import { ChevronRightIcon, PlusIcon, XMarkIcon } from "@heroicons/react/24/outli // helpers import { orderArrayBy } from "helpers/array.helper"; // types -import { IIssue, ISubIssueResponse } from "types"; +import { ICurrentUserResponse, IIssue, ISubIssueResponse } from "types"; // fetch-keys import { PROJECT_ISSUES_LIST, SUB_ISSUES } from "constants/fetch-keys"; type Props = { parentIssue: IIssue; + user: ICurrentUserResponse | undefined; }; -export const SubIssuesList: FC = ({ parentIssue }) => { +export const SubIssuesList: FC = ({ parentIssue, user }) => { // states const [createIssueModal, setCreateIssueModal] = useState(false); const [subIssuesListModal, setSubIssuesListModal] = useState(false); @@ -134,7 +135,7 @@ export const SubIssuesList: FC = ({ parentIssue }) => { ); issuesService - .patchIssue(workspaceSlug.toString(), projectId.toString(), issueId, { parent: null }) + .patchIssue(workspaceSlug.toString(), projectId.toString(), issueId, { parent: null }, user) .then((res) => { mutate(SUB_ISSUES(parentIssue.id ?? "")); diff --git a/apps/app/components/issues/view-select/assignee.tsx b/apps/app/components/issues/view-select/assignee.tsx index 71e16a092..b15b17176 100644 --- a/apps/app/components/issues/view-select/assignee.tsx +++ b/apps/app/components/issues/view-select/assignee.tsx @@ -12,7 +12,7 @@ import { AssigneesList, Avatar, CustomSearchSelect, Tooltip } from "components/u // icons import { UserGroupIcon } from "@heroicons/react/24/outline"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; // fetch-keys import { PROJECT_MEMBERS } from "constants/fetch-keys"; @@ -22,6 +22,7 @@ type Props = { position?: "left" | "right"; selfPositioned?: boolean; tooltipPosition?: "left" | "right"; + user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -31,6 +32,7 @@ export const ViewAssigneeSelect: React.FC = ({ position = "left", selfPositioned = false, tooltipPosition = "right", + user, isNotAllowed, }) => { const router = useRouter(); @@ -83,7 +85,8 @@ export const ViewAssigneeSelect: React.FC = ({ projectName: issue.project_detail.name, issueId: issue.id, }, - "ISSUE_PROPERTY_UPDATE_ASSIGNEE" + "ISSUE_PROPERTY_UPDATE_ASSIGNEE", + user ); }} options={options} diff --git a/apps/app/components/issues/view-select/due-date.tsx b/apps/app/components/issues/view-select/due-date.tsx index 60fa8c718..bea5ff045 100644 --- a/apps/app/components/issues/view-select/due-date.tsx +++ b/apps/app/components/issues/view-select/due-date.tsx @@ -7,15 +7,21 @@ import { findHowManyDaysLeft } from "helpers/date-time.helper"; // services import trackEventServices from "services/track-event.service"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; type Props = { issue: IIssue; partialUpdateIssue: (formData: Partial, issueId: string) => void; + user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; -export const ViewDueDateSelect: React.FC = ({ issue, partialUpdateIssue, isNotAllowed }) => { +export const ViewDueDateSelect: React.FC = ({ + issue, + partialUpdateIssue, + user, + isNotAllowed, +}) => { const router = useRouter(); const { workspaceSlug } = router.query; @@ -51,7 +57,8 @@ export const ViewDueDateSelect: React.FC = ({ issue, partialUpdateIssue, projectName: issue.project_detail.name, issueId: issue.id, }, - "ISSUE_PROPERTY_UPDATE_DUE_DATE" + "ISSUE_PROPERTY_UPDATE_DUE_DATE", + user ); }} className={issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"} diff --git a/apps/app/components/issues/view-select/estimate.tsx b/apps/app/components/issues/view-select/estimate.tsx index 586f930bc..914a5286e 100644 --- a/apps/app/components/issues/view-select/estimate.tsx +++ b/apps/app/components/issues/view-select/estimate.tsx @@ -11,13 +11,14 @@ import { CustomSelect, Tooltip } from "components/ui"; // icons import { PlayIcon } from "@heroicons/react/24/outline"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; type Props = { issue: IIssue; partialUpdateIssue: (formData: Partial, issueId: string) => void; position?: "left" | "right"; selfPositioned?: boolean; + user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -26,6 +27,7 @@ export const ViewEstimateSelect: React.FC = ({ partialUpdateIssue, position = "left", selfPositioned = false, + user, isNotAllowed, }) => { const router = useRouter(); @@ -51,7 +53,8 @@ export const ViewEstimateSelect: React.FC = ({ projectName: issue.project_detail.name, issueId: issue.id, }, - "ISSUE_PROPERTY_UPDATE_ESTIMATE" + "ISSUE_PROPERTY_UPDATE_ESTIMATE", + user ); }} label={ diff --git a/apps/app/components/issues/view-select/priority.tsx b/apps/app/components/issues/view-select/priority.tsx index aa9a5306e..a0c5cd47c 100644 --- a/apps/app/components/issues/view-select/priority.tsx +++ b/apps/app/components/issues/view-select/priority.tsx @@ -7,7 +7,7 @@ import { CustomSelect, Tooltip } from "components/ui"; // icons import { getPriorityIcon } from "components/icons/priority-icon"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; // constants import { PRIORITIES } from "constants/project"; // services @@ -18,6 +18,7 @@ type Props = { partialUpdateIssue: (formData: Partial, issueId: string) => void; position?: "left" | "right"; selfPositioned?: boolean; + user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -26,6 +27,7 @@ export const ViewPrioritySelect: React.FC = ({ partialUpdateIssue, position = "left", selfPositioned = false, + user, isNotAllowed, }) => { const router = useRouter(); @@ -45,7 +47,8 @@ export const ViewPrioritySelect: React.FC = ({ projectName: issue.project_detail.name, issueId: issue.id, }, - "ISSUE_PROPERTY_UPDATE_PRIORITY" + "ISSUE_PROPERTY_UPDATE_PRIORITY", + user ); }} maxHeight="md" diff --git a/apps/app/components/issues/view-select/state.tsx b/apps/app/components/issues/view-select/state.tsx index 68a99ac72..2b904eb1e 100644 --- a/apps/app/components/issues/view-select/state.tsx +++ b/apps/app/components/issues/view-select/state.tsx @@ -13,7 +13,7 @@ import { getStateGroupIcon } from "components/icons"; import { addSpaceIfCamelCase } from "helpers/string.helper"; import { getStatesList } from "helpers/state.helper"; // types -import { IIssue } from "types"; +import { ICurrentUserResponse, IIssue } from "types"; // fetch-keys import { STATES_LIST } from "constants/fetch-keys"; @@ -22,6 +22,7 @@ type Props = { partialUpdateIssue: (formData: Partial, issueId: string) => void; position?: "left" | "right"; selfPositioned?: boolean; + user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; @@ -30,6 +31,7 @@ export const ViewStateSelect: React.FC = ({ partialUpdateIssue, position = "left", selfPositioned = false, + user, isNotAllowed, }) => { const router = useRouter(); @@ -77,21 +79,25 @@ export const ViewStateSelect: React.FC = ({ projectName: issue.project_detail.name, issueId: issue.id, }, - "ISSUE_PROPERTY_UPDATE_STATE" + "ISSUE_PROPERTY_UPDATE_STATE", + user ); const oldState = states.find((s) => s.id === issue.state); const newState = states.find((s) => s.id === data); if (oldState?.group !== "completed" && newState?.group !== "completed") { - trackEventServices.trackIssueMarkedAsDoneEvent({ - workspaceSlug: issue.workspace_detail.slug, - workspaceId: issue.workspace_detail.id, - projectId: issue.project_detail.id, - projectIdentifier: issue.project_detail.identifier, - projectName: issue.project_detail.name, - issueId: issue.id, - }); + trackEventServices.trackIssueMarkedAsDoneEvent( + { + workspaceSlug: issue.workspace_detail.slug, + workspaceId: issue.workspace_detail.id, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + user + ); } }} options={options} diff --git a/apps/app/components/labels/create-label-modal.tsx b/apps/app/components/labels/create-label-modal.tsx index 858f7f7b5..c7566dc77 100644 --- a/apps/app/components/labels/create-label-modal.tsx +++ b/apps/app/components/labels/create-label-modal.tsx @@ -17,7 +17,7 @@ import { Input, PrimaryButton, SecondaryButton } from "components/ui"; // icons import { ChevronDownIcon } from "@heroicons/react/24/outline"; // types -import type { IIssueLabels, IState } from "types"; +import type { ICurrentUserResponse, IIssueLabels, IState } from "types"; // constants import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; @@ -26,6 +26,7 @@ type Props = { isOpen: boolean; projectId: string; handleClose: () => void; + user: ICurrentUserResponse | undefined; }; const defaultValues: Partial = { @@ -33,7 +34,7 @@ const defaultValues: Partial = { color: "#858E96", }; -export const CreateLabelModal: React.FC = ({ isOpen, projectId, handleClose }) => { +export const CreateLabelModal: React.FC = ({ isOpen, projectId, handleClose, user }) => { const router = useRouter(); const { workspaceSlug } = router.query; @@ -57,7 +58,7 @@ export const CreateLabelModal: React.FC = ({ isOpen, projectId, handleClo if (!workspaceSlug) return; await issuesService - .createIssueLabel(workspaceSlug as string, projectId as string, formData) + .createIssueLabel(workspaceSlug as string, projectId as string, formData, user) .then((res) => { mutate( PROJECT_ISSUE_LABELS(projectId), diff --git a/apps/app/components/labels/create-update-label-inline.tsx b/apps/app/components/labels/create-update-label-inline.tsx index 7010d564e..7b1a04b91 100644 --- a/apps/app/components/labels/create-update-label-inline.tsx +++ b/apps/app/components/labels/create-update-label-inline.tsx @@ -6,6 +6,8 @@ import { mutate } from "swr"; // react-hook-form import { Controller, SubmitHandler, useForm } from "react-hook-form"; +// hooks +import useUserAuth from "hooks/use-user-auth"; // react-color import { TwitterPicker } from "react-color"; // headless ui @@ -42,6 +44,8 @@ export const CreateUpdateLabelInline = forwardRef(function CreateUpd const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { handleSubmit, control, @@ -58,7 +62,7 @@ export const CreateUpdateLabelInline = forwardRef(function CreateUpd if (!workspaceSlug || !projectId || isSubmitting) return; await issuesService - .createIssueLabel(workspaceSlug as string, projectId as string, formData) + .createIssueLabel(workspaceSlug as string, projectId as string, formData, user) .then((res) => { mutate( PROJECT_ISSUE_LABELS(projectId as string), @@ -78,7 +82,8 @@ export const CreateUpdateLabelInline = forwardRef(function CreateUpd workspaceSlug as string, projectId as string, labelToUpdate?.id ?? "", - formData + formData, + user ) .then(() => { reset(defaultValues); diff --git a/apps/app/components/labels/delete-label-modal.tsx b/apps/app/components/labels/delete-label-modal.tsx index 772c9d064..dd3df0fc4 100644 --- a/apps/app/components/labels/delete-label-modal.tsx +++ b/apps/app/components/labels/delete-label-modal.tsx @@ -15,7 +15,7 @@ import useToast from "hooks/use-toast"; // ui import { DangerButton, SecondaryButton } from "components/ui"; // types -import type { IIssueLabels } from "types"; +import type { ICurrentUserResponse, IIssueLabels } from "types"; // fetch-keys import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; @@ -23,9 +23,10 @@ type Props = { isOpen: boolean; onClose: () => void; data: IIssueLabels | null; + user: ICurrentUserResponse | undefined; }; -export const DeleteLabelModal: React.FC = ({ isOpen, onClose, data }) => { +export const DeleteLabelModal: React.FC = ({ isOpen, onClose, data, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); @@ -50,7 +51,7 @@ export const DeleteLabelModal: React.FC = ({ isOpen, onClose, data }) => ); await issuesService - .deleteIssueLabel(workspaceSlug.toString(), projectId.toString(), data.id) + .deleteIssueLabel(workspaceSlug.toString(), projectId.toString(), data.id, user) .then(() => handleClose()) .catch(() => { setIsDeleteLoading(false); diff --git a/apps/app/components/labels/labels-list-modal.tsx b/apps/app/components/labels/labels-list-modal.tsx index 8d3c04672..23f60ab7f 100644 --- a/apps/app/components/labels/labels-list-modal.tsx +++ b/apps/app/components/labels/labels-list-modal.tsx @@ -11,7 +11,7 @@ import { RectangleStackIcon, MagnifyingGlassIcon } from "@heroicons/react/24/out // services import issuesService from "services/issues.service"; // types -import { IIssueLabels } from "types"; +import { ICurrentUserResponse, IIssueLabels } from "types"; // constants import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; @@ -19,9 +19,10 @@ type Props = { isOpen: boolean; handleClose: () => void; parent: IIssueLabels | undefined; + user: ICurrentUserResponse | undefined; }; -export const LabelsListModal: React.FC = ({ isOpen, handleClose, parent }) => { +export const LabelsListModal: React.FC = ({ isOpen, handleClose, parent, user }) => { const [query, setQuery] = useState(""); const router = useRouter(); @@ -58,9 +59,15 @@ export const LabelsListModal: React.FC = ({ isOpen, handleClose, parent } ); await issuesService - .patchIssueLabel(workspaceSlug as string, projectId as string, label.id, { - parent: parent?.id ?? "", - }) + .patchIssueLabel( + workspaceSlug as string, + projectId as string, + label.id, + { + parent: parent?.id ?? "", + }, + user + ) .then(() => mutate()); }; diff --git a/apps/app/components/labels/single-label-group.tsx b/apps/app/components/labels/single-label-group.tsx index 00837a05c..f1a74f296 100644 --- a/apps/app/components/labels/single-label-group.tsx +++ b/apps/app/components/labels/single-label-group.tsx @@ -20,7 +20,7 @@ import { TrashIcon, } from "@heroicons/react/24/outline"; // types -import { IIssueLabels } from "types"; +import { ICurrentUserResponse, IIssueLabels } from "types"; // fetch-keys import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; @@ -30,6 +30,7 @@ type Props = { addLabelToGroup: (parentLabel: IIssueLabels) => void; editLabel: (label: IIssueLabels) => void; handleLabelDelete: () => void; + user: ICurrentUserResponse | undefined; }; export const SingleLabelGroup: React.FC = ({ @@ -38,6 +39,7 @@ export const SingleLabelGroup: React.FC = ({ addLabelToGroup, editLabel, handleLabelDelete, + user, }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -57,9 +59,15 @@ export const SingleLabelGroup: React.FC = ({ ); issuesService - .patchIssueLabel(workspaceSlug as string, projectId as string, label.id, { - parent: null, - }) + .patchIssueLabel( + workspaceSlug as string, + projectId as string, + label.id, + { + parent: null, + }, + user + ) .then(() => { mutate(PROJECT_ISSUE_LABELS(projectId as string)); }); diff --git a/apps/app/components/modules/delete-module-modal.tsx b/apps/app/components/modules/delete-module-modal.tsx index f8d803732..f2a9ec7ee 100644 --- a/apps/app/components/modules/delete-module-modal.tsx +++ b/apps/app/components/modules/delete-module-modal.tsx @@ -15,7 +15,7 @@ import { SecondaryButton, DangerButton } from "components/ui"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types -import type { IModule } from "types"; +import type { ICurrentUserResponse, IModule } from "types"; // fetch-keys import { MODULE_LIST } from "constants/fetch-keys"; @@ -23,9 +23,10 @@ type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; data?: IModule; + user: ICurrentUserResponse | undefined; }; -export const DeleteModuleModal: React.FC = ({ isOpen, setIsOpen, data }) => { +export const DeleteModuleModal: React.FC = ({ isOpen, setIsOpen, data, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); @@ -50,7 +51,7 @@ export const DeleteModuleModal: React.FC = ({ isOpen, setIsOpen, data }) ); await modulesService - .deleteModule(workspaceSlug as string, projectId as string, data.id) + .deleteModule(workspaceSlug as string, projectId as string, data.id, user) .then(() => { if (moduleId) router.push(`/${workspaceSlug}/projects/${data.project}/modules`); handleClose(); diff --git a/apps/app/components/modules/modal.tsx b/apps/app/components/modules/modal.tsx index 69dcba34a..c06be46d5 100644 --- a/apps/app/components/modules/modal.tsx +++ b/apps/app/components/modules/modal.tsx @@ -15,7 +15,7 @@ import modulesService from "services/modules.service"; // hooks import useToast from "hooks/use-toast"; // types -import type { IModule } from "types"; +import type { ICurrentUserResponse, IModule } from "types"; // fetch-keys import { MODULE_LIST } from "constants/fetch-keys"; @@ -23,6 +23,7 @@ type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; data?: IModule; + user: ICurrentUserResponse | undefined; }; const defaultValues: Partial = { @@ -33,7 +34,7 @@ const defaultValues: Partial = { members_list: [], }; -export const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, data }) => { +export const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, data, user }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -50,7 +51,7 @@ export const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, da const createModule = async (payload: Partial) => { await modulesService - .createModule(workspaceSlug as string, projectId as string, payload) + .createModule(workspaceSlug as string, projectId as string, payload, user) .then(() => { mutate(MODULE_LIST(projectId as string)); handleClose(); @@ -72,7 +73,7 @@ export const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, da const updateModule = async (payload: Partial) => { await modulesService - .updateModule(workspaceSlug as string, projectId as string, data?.id ?? "", payload) + .updateModule(workspaceSlug as string, projectId as string, data?.id ?? "", payload, user) .then((res) => { mutate( MODULE_LIST(projectId as string), diff --git a/apps/app/components/modules/sidebar.tsx b/apps/app/components/modules/sidebar.tsx index c1b4674b7..f453e4c68 100644 --- a/apps/app/components/modules/sidebar.tsx +++ b/apps/app/components/modules/sidebar.tsx @@ -37,7 +37,7 @@ import { LinkIcon } from "@heroicons/react/20/solid"; import { renderDateFormat, renderShortDate } from "helpers/date-time.helper"; import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper"; // types -import { IIssue, IModule, ModuleLink } from "types"; +import { ICurrentUserResponse, IIssue, IModule, ModuleLink } from "types"; // fetch-keys import { MODULE_DETAILS } from "constants/fetch-keys"; // constant @@ -56,9 +56,16 @@ type Props = { module?: IModule; isOpen: boolean; moduleIssues?: IIssue[]; + user: ICurrentUserResponse | undefined; }; -export const ModuleDetailsSidebar: React.FC = ({ issues, module, isOpen, moduleIssues }) => { +export const ModuleDetailsSidebar: React.FC = ({ + issues, + module, + isOpen, + moduleIssues, + user, +}) => { const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const [moduleLinkModal, setModuleLinkModal] = useState(false); @@ -86,7 +93,7 @@ export const ModuleDetailsSidebar: React.FC = ({ issues, module, isOpen, ); modulesService - .patchModule(workspaceSlug as string, projectId as string, moduleId as string, data) + .patchModule(workspaceSlug as string, projectId as string, moduleId as string, data, user) .then(() => mutate(MODULE_DETAILS(moduleId as string))) .catch((e) => console.log(e)); }; @@ -181,6 +188,7 @@ export const ModuleDetailsSidebar: React.FC = ({ issues, module, isOpen, isOpen={moduleDeleteModal} setIsOpen={setModuleDeleteModal} data={module} + user={user} />
    void; + user: ICurrentUserResponse | undefined; }; -export const SingleModuleCard: React.FC = ({ module, handleEditModule }) => { +export const SingleModuleCard: React.FC = ({ module, handleEditModule, user }) => { const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const router = useRouter(); @@ -128,6 +129,7 @@ export const SingleModuleCard: React.FC = ({ module, handleEditModule }) isOpen={moduleDeleteModal} setIsOpen={setModuleDeleteModal} data={module} + user={user} />
    diff --git a/apps/app/components/onboarding/invite-members.tsx b/apps/app/components/onboarding/invite-members.tsx index e1ebdf117..0114b2dcd 100644 --- a/apps/app/components/onboarding/invite-members.tsx +++ b/apps/app/components/onboarding/invite-members.tsx @@ -2,16 +2,17 @@ import { useForm } from "react-hook-form"; import useToast from "hooks/use-toast"; import workspaceService from "services/workspace.service"; -import { IUser } from "types"; +import { ICurrentUserResponse, IUser } from "types"; // ui components import { MultiInput, PrimaryButton, SecondaryButton } from "components/ui"; type Props = { setStep: React.Dispatch>; workspace: any; + user: ICurrentUserResponse | undefined; }; -export const InviteMembers: React.FC = ({ setStep, workspace }) => { +export const InviteMembers: React.FC = ({ setStep, workspace, user }) => { const { setToastAlert } = useToast(); const { @@ -23,7 +24,7 @@ export const InviteMembers: React.FC = ({ setStep, workspace }) => { const onSubmit = async (formData: IUser) => { await workspaceService - .inviteWorkspace(workspace.slug, formData) + .inviteWorkspace(workspace.slug, formData, user) .then(() => { setToastAlert({ type: "success", diff --git a/apps/app/components/onboarding/workspace.tsx b/apps/app/components/onboarding/workspace.tsx index 344fb74fe..137d58057 100644 --- a/apps/app/components/onboarding/workspace.tsx +++ b/apps/app/components/onboarding/workspace.tsx @@ -9,7 +9,7 @@ import { Tab } from "@headlessui/react"; // services import workspaceService from "services/workspace.service"; // types -import { IWorkspaceMemberInvitation } from "types"; +import { ICurrentUserResponse, IWorkspaceMemberInvitation } from "types"; // fetch-keys import { USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; // constants @@ -21,9 +21,10 @@ import { getFirstCharacters, truncateText } from "helpers/string.helper"; type Props = { setStep: React.Dispatch>; setWorkspace: React.Dispatch>; + user: ICurrentUserResponse | undefined; }; -export const Workspace: React.FC = ({ setStep, setWorkspace }) => { +export const Workspace: React.FC = ({ setStep, setWorkspace, user }) => { const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false); const [invitationsRespond, setInvitationsRespond] = useState([]); const [defaultValues, setDefaultValues] = useState({ @@ -98,7 +99,7 @@ export const Workspace: React.FC = ({ setStep, setWorkspace }) => { >
    -

    Workspaces

    +

    Workspace

    Create or join the workspace to get started with Plane.

    @@ -161,7 +162,10 @@ export const Workspace: React.FC = ({ setStep, setWorkspace }) => { {truncateText(invitation.workspace.name, 30)}

    - Invited by {invitation.workspace.owner.first_name} + Invited by{" "} + {invitation.created_by_detail + ? invitation.created_by_detail.first_name + : invitation.workspace.owner.first_name}

    @@ -237,6 +241,7 @@ export const Workspace: React.FC = ({ setStep, setWorkspace }) => { }} defaultValues={defaultValues} setDefaultValues={setDefaultValues} + user={user} /> diff --git a/apps/app/components/pages/create-block.tsx b/apps/app/components/pages/create-block.tsx index 59bb81898..d4098db93 100644 --- a/apps/app/components/pages/create-block.tsx +++ b/apps/app/components/pages/create-block.tsx @@ -16,7 +16,7 @@ import useToast from "hooks/use-toast"; // ui import { TextArea } from "components/ui"; // types -import { IPageBlock } from "types"; +import { ICurrentUserResponse, IPageBlock } from "types"; // fetch-keys import { PAGE_BLOCKS_LIST } from "constants/fetch-keys"; @@ -24,7 +24,11 @@ const defaultValues = { name: "", }; -export const CreateBlock = () => { +type Props = { + user: ICurrentUserResponse | undefined; +}; + +export const CreateBlock: React.FC = ({ user }) => { const [blockTitle, setBlockTitle] = useState(""); const router = useRouter(); @@ -49,9 +53,15 @@ export const CreateBlock = () => { if (!workspaceSlug || !projectId || !pageId) return; await pagesService - .createPageBlock(workspaceSlug as string, projectId as string, pageId as string, { - name: watch("name"), - }) + .createPageBlock( + workspaceSlug as string, + projectId as string, + pageId as string, + { + name: watch("name"), + }, + user + ) .then((res) => { mutate( PAGE_BLOCKS_LIST(pageId as string), diff --git a/apps/app/components/pages/create-update-block-inline.tsx b/apps/app/components/pages/create-update-block-inline.tsx index 528c83f9e..27e6bd419 100644 --- a/apps/app/components/pages/create-update-block-inline.tsx +++ b/apps/app/components/pages/create-update-block-inline.tsx @@ -20,7 +20,7 @@ import { GptAssistantModal } from "components/core"; // ui import { Loader, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; // types -import { IPageBlock } from "types"; +import { ICurrentUserResponse, IPageBlock } from "types"; // fetch-keys import { PAGE_BLOCKS_LIST } from "constants/fetch-keys"; @@ -30,6 +30,7 @@ type Props = { handleAiAssistance?: (response: string) => void; setIsSyncing?: React.Dispatch>; focus?: keyof IPageBlock; + user: ICurrentUserResponse | undefined; }; const defaultValues = { @@ -61,6 +62,7 @@ export const CreateUpdateBlockInline: React.FC = ({ handleAiAssistance, setIsSyncing, focus, + user, }) => { const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); const [gptAssistantModal, setGptAssistantModal] = useState(false); @@ -96,11 +98,17 @@ export const CreateUpdateBlockInline: React.FC = ({ if (!workspaceSlug || !projectId || !pageId) return; await pagesService - .createPageBlock(workspaceSlug as string, projectId as string, pageId as string, { - name: formData.name, - description: formData.description ?? "", - description_html: formData.description_html ?? "

    ", - }) + .createPageBlock( + workspaceSlug as string, + projectId as string, + pageId as string, + { + name: formData.name, + description: formData.description ?? "", + description_html: formData.description_html ?? "

    ", + }, + user + ) .then((res) => { mutate( PAGE_BLOCKS_LIST(pageId as string), @@ -139,21 +147,34 @@ export const CreateUpdateBlockInline: React.FC = ({ ); await pagesService - .patchPageBlock(workspaceSlug as string, projectId as string, pageId as string, data.id, { - name: formData.name, - description: formData.description, - description_html: formData.description_html, - }) + .patchPageBlock( + workspaceSlug as string, + projectId as string, + pageId as string, + data.id, + { + name: formData.name, + description: formData.description, + description_html: formData.description_html, + }, + user + ) .then((res) => { mutate(PAGE_BLOCKS_LIST(pageId as string)); editorRef.current?.setEditorValue(res.description_html); if (data.issue && data.sync) issuesService - .patchIssue(workspaceSlug as string, projectId as string, data.issue, { - name: res.name, - description: res.description, - description_html: res.description_html, - }) + .patchIssue( + workspaceSlug as string, + projectId as string, + data.issue, + { + name: res.name, + description: res.description, + description_html: res.description_html, + }, + user + ) .finally(() => { if (setIsSyncing) setIsSyncing(false); }); @@ -169,10 +190,15 @@ export const CreateUpdateBlockInline: React.FC = ({ setIAmFeelingLucky(true); aiService - .createGptTask(workspaceSlug as string, projectId as string, { - prompt: watch("name"), - task: "Generate a proper description for this issue in context of a project management software.", - }) + .createGptTask( + workspaceSlug as string, + projectId as string, + { + prompt: watch("name"), + task: "Generate a proper description for this issue in context of a project management software.", + }, + user + ) .then((res) => { if (res.response === "") setToastAlert({ diff --git a/apps/app/components/pages/create-update-page-modal.tsx b/apps/app/components/pages/create-update-page-modal.tsx index 8c26e0d00..57e25b5f7 100644 --- a/apps/app/components/pages/create-update-page-modal.tsx +++ b/apps/app/components/pages/create-update-page-modal.tsx @@ -13,7 +13,7 @@ import useToast from "hooks/use-toast"; // components import { PageForm } from "./page-form"; // types -import { IPage } from "types"; +import { ICurrentUserResponse, IPage } from "types"; // fetch-keys import { ALL_PAGES_LIST, @@ -26,9 +26,10 @@ type Props = { isOpen: boolean; handleClose: () => void; data?: IPage | null; + user: ICurrentUserResponse | undefined; }; -export const CreateUpdatePageModal: React.FC = ({ isOpen, handleClose, data }) => { +export const CreateUpdatePageModal: React.FC = ({ isOpen, handleClose, data, user }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -40,7 +41,7 @@ export const CreateUpdatePageModal: React.FC = ({ isOpen, handleClose, da const createPage = async (payload: IPage) => { await pagesService - .createPage(workspaceSlug as string, projectId as string, payload) + .createPage(workspaceSlug as string, projectId as string, payload, user) .then((res) => { mutate(RECENT_PAGES_LIST(projectId as string)); mutate( @@ -82,7 +83,7 @@ export const CreateUpdatePageModal: React.FC = ({ isOpen, handleClose, da const updatePage = async (payload: IPage) => { await pagesService - .patchPage(workspaceSlug as string, projectId as string, data?.id ?? "", payload) + .patchPage(workspaceSlug as string, projectId as string, data?.id ?? "", payload, user) .then((res) => { mutate(RECENT_PAGES_LIST(projectId as string)); mutate( diff --git a/apps/app/components/pages/delete-page-modal.tsx b/apps/app/components/pages/delete-page-modal.tsx index b2f202284..6277870d1 100644 --- a/apps/app/components/pages/delete-page-modal.tsx +++ b/apps/app/components/pages/delete-page-modal.tsx @@ -15,7 +15,7 @@ import { DangerButton, SecondaryButton } from "components/ui"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types -import type { IPage } from "types"; +import type { ICurrentUserResponse, IPage } from "types"; // fetch-keys import { ALL_PAGES_LIST, @@ -28,12 +28,14 @@ type TConfirmPageDeletionProps = { isOpen: boolean; setIsOpen: React.Dispatch>; data?: IPage | null; + user: ICurrentUserResponse | undefined; }; export const DeletePageModal: React.FC = ({ isOpen, setIsOpen, data, + user, }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); @@ -52,7 +54,7 @@ export const DeletePageModal: React.FC = ({ if (!data || !workspaceSlug || !projectId) return; await pagesService - .deletePage(workspaceSlug as string, data.project, data.id) + .deletePage(workspaceSlug as string, data.project, data.id, user) .then(() => { mutate(RECENT_PAGES_LIST(projectId as string)); mutate( diff --git a/apps/app/components/pages/pages-view.tsx b/apps/app/components/pages/pages-view.tsx index a1c0d083f..7d1eac724 100644 --- a/apps/app/components/pages/pages-view.tsx +++ b/apps/app/components/pages/pages-view.tsx @@ -8,6 +8,7 @@ import pagesService from "services/pages.service"; import projectService from "services/project.service"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // components import { CreateUpdatePageModal, @@ -44,6 +45,8 @@ export const PagesView: React.FC = ({ pages, viewType }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const { data: people } = useSWR( @@ -181,7 +184,7 @@ export const PagesView: React.FC = ({ pages, viewType }) => { ); pagesService - .patchPage(workspaceSlug.toString(), projectId.toString(), page.id, formData) + .patchPage(workspaceSlug.toString(), projectId.toString(), page.id, formData, user) .then(() => { mutate(RECENT_PAGES_LIST(projectId.toString())); }); @@ -193,11 +196,13 @@ export const PagesView: React.FC = ({ pages, viewType }) => { isOpen={createUpdatePageModal} handleClose={() => setCreateUpdatePageModal(false)} data={selectedPageToUpdate} + user={user} /> {pages ? (
    diff --git a/apps/app/components/pages/single-page-block.tsx b/apps/app/components/pages/single-page-block.tsx index 42f0e5523..3efbd33eb 100644 --- a/apps/app/components/pages/single-page-block.tsx +++ b/apps/app/components/pages/single-page-block.tsx @@ -35,7 +35,7 @@ import { // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types -import { IIssue, IPageBlock, IProject } from "types"; +import { ICurrentUserResponse, IIssue, IPageBlock, IProject } from "types"; // fetch-keys import { PAGE_BLOCKS_LIST } from "constants/fetch-keys"; @@ -43,9 +43,10 @@ type Props = { block: IPageBlock; projectDetails: IProject | undefined; index: number; + user: ICurrentUserResponse | undefined; }; -export const SinglePageBlock: React.FC = ({ block, projectDetails, index }) => { +export const SinglePageBlock: React.FC = ({ block, projectDetails, index, user }) => { const [isSyncing, setIsSyncing] = useState(false); const [createBlockForm, setCreateBlockForm] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); @@ -87,20 +88,33 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, index ); await pagesService - .patchPageBlock(workspaceSlug as string, projectId as string, pageId as string, block.id, { - name: formData.name, - description: formData.description, - description_html: formData.description_html, - }) + .patchPageBlock( + workspaceSlug as string, + projectId as string, + pageId as string, + block.id, + { + name: formData.name, + description: formData.description, + description_html: formData.description_html, + }, + user + ) .then((res) => { mutate(PAGE_BLOCKS_LIST(pageId as string)); if (block.issue && block.sync) issuesService - .patchIssue(workspaceSlug as string, projectId as string, block.issue, { - name: res.name, - description: res.description, - description_html: res.description_html, - }) + .patchIssue( + workspaceSlug as string, + projectId as string, + block.issue, + { + name: res.name, + description: res.description, + description_html: res.description_html, + }, + user + ) .finally(() => setIsSyncing(false)); }); }; @@ -113,7 +127,8 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, index workspaceSlug as string, projectId as string, pageId as string, - block.id + block.id, + user ) .then((res: IIssue) => { mutate( @@ -152,7 +167,13 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, index ); await pagesService - .deletePageBlock(workspaceSlug as string, projectId as string, pageId as string, block.id) + .deletePageBlock( + workspaceSlug as string, + projectId as string, + pageId as string, + block.id, + user + ) .catch(() => { setToastAlert({ type: "error", @@ -168,10 +189,15 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, index setIAmFeelingLucky(true); aiService - .createGptTask(workspaceSlug as string, projectId as string, { - prompt: block.name, - task: "Generate a proper description for this issue in context of a project management software.", - }) + .createGptTask( + workspaceSlug as string, + projectId as string, + { + prompt: block.name, + task: "Generate a proper description for this issue in context of a project management software.", + }, + user + ) .then((res) => { if (res.response === "") setToastAlert({ @@ -243,7 +269,8 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, index block.id, { sync: !block.sync, - } + }, + user ); }; @@ -281,6 +308,7 @@ export const SinglePageBlock: React.FC = ({ block, projectDetails, index data={block} setIsSyncing={setIsSyncing} focus="name" + user={user} />
    ) : ( diff --git a/apps/app/components/project/create-project-modal.tsx b/apps/app/components/project/create-project-modal.tsx index c731c12f0..ca60adde1 100644 --- a/apps/app/components/project/create-project-modal.tsx +++ b/apps/app/components/project/create-project-modal.tsx @@ -24,7 +24,7 @@ import EmojiIconPicker from "components/emoji-icon-picker"; // helpers import { getRandomEmoji } from "helpers/common.helper"; // types -import { IProject } from "types"; +import { ICurrentUserResponse, IProject } from "types"; // fetch-keys import { PROJECTS_LIST, WORKSPACE_MEMBERS_ME } from "constants/fetch-keys"; // constants @@ -33,6 +33,7 @@ import { NETWORK_CHOICES } from "constants/project"; type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; + user: ICurrentUserResponse | undefined; }; const defaultValues: Partial = { @@ -63,7 +64,7 @@ const IsGuestCondition: React.FC<{ }; export const CreateProjectModal: React.FC = (props) => { - const { isOpen, setIsOpen } = props; + const { isOpen, setIsOpen, user } = props; const [isChangeIdentifierRequired, setIsChangeIdentifierRequired] = useState(true); @@ -120,7 +121,7 @@ export const CreateProjectModal: React.FC = (props) => { else payload.emoji = formData.emoji_and_icon; await projectServices - .createProject(workspaceSlug as string, payload) + .createProject(workspaceSlug as string, payload, user) .then((res) => { mutate( PROJECTS_LIST(workspaceSlug as string), diff --git a/apps/app/components/project/delete-project-modal.tsx b/apps/app/components/project/delete-project-modal.tsx index 49d3f745b..5a4be1706 100644 --- a/apps/app/components/project/delete-project-modal.tsx +++ b/apps/app/components/project/delete-project-modal.tsx @@ -13,7 +13,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // ui import { DangerButton, Input, SecondaryButton } from "components/ui"; // types -import type { IProject, IWorkspace } from "types"; +import type { ICurrentUserResponse, IProject, IWorkspace } from "types"; // fetch-keys import { PROJECTS_LIST } from "constants/fetch-keys"; @@ -22,6 +22,7 @@ type TConfirmProjectDeletionProps = { onClose: () => void; onSuccess?: () => void; data: IProject | null; + user: ICurrentUserResponse | undefined; }; export const DeleteProjectModal: React.FC = ({ @@ -29,6 +30,7 @@ export const DeleteProjectModal: React.FC = ({ data, onClose, onSuccess, + user, }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [confirmProjectName, setConfirmProjectName] = useState(""); @@ -65,7 +67,7 @@ export const DeleteProjectModal: React.FC = ({ setIsDeleteLoading(true); if (!data || !workspaceSlug || !canDelete) return; await projectService - .deleteProject(workspaceSlug, data.id) + .deleteProject(workspaceSlug, data.id, user) .then(() => { handleClose(); mutate(PROJECTS_LIST(workspaceSlug), (prevData) => diff --git a/apps/app/components/project/send-project-invitation-modal.tsx b/apps/app/components/project/send-project-invitation-modal.tsx index d21cf07f3..e08b92e8c 100644 --- a/apps/app/components/project/send-project-invitation-modal.tsx +++ b/apps/app/components/project/send-project-invitation-modal.tsx @@ -15,7 +15,7 @@ import useToast from "hooks/use-toast"; import projectService from "services/project.service"; import workspaceService from "services/workspace.service"; // types -import { IProjectMemberInvitation } from "types"; +import { ICurrentUserResponse, IProjectMemberInvitation } from "types"; // fetch-keys import { PROJECT_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys"; // constants @@ -25,6 +25,7 @@ type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; members: any[]; + user: ICurrentUserResponse | undefined; }; type ProjectMember = IProjectMemberInvitation & { @@ -40,7 +41,7 @@ const defaultValues: Partial = { user_id: "", }; -const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, members }) => { +const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, members, user }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -70,7 +71,7 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member const onSubmit = async (formData: ProjectMember) => { if (!workspaceSlug || !projectId || isSubmitting) return; await projectService - .inviteProject(workspaceSlug as string, projectId as string, formData) + .inviteProject(workspaceSlug as string, projectId as string, formData, user) .then((response) => { setIsOpen(false); mutate( diff --git a/apps/app/components/project/sidebar-list.tsx b/apps/app/components/project/sidebar-list.tsx index 9d4b9f415..f9b6c5760 100644 --- a/apps/app/components/project/sidebar-list.tsx +++ b/apps/app/components/project/sidebar-list.tsx @@ -9,6 +9,7 @@ import { PlusIcon } from "@heroicons/react/24/outline"; // hooks import useToast from "hooks/use-toast"; import useTheme from "hooks/use-theme"; +import useUserAuth from "hooks/use-user-auth"; // services import projectService from "services/project.service"; // components @@ -29,6 +30,9 @@ export const ProjectSidebarList: FC = () => { // router const router = useRouter(); const { workspaceSlug } = router.query; + + const { user } = useUserAuth(); + // states const [isCreateProjectModal, setCreateProjectModal] = useState(false); // theme @@ -136,11 +140,16 @@ export const ProjectSidebarList: FC = () => { return ( <> - + setDeleteProjectModal(false)} data={projectToDelete} + user={user} />
    {favoriteProjects && favoriteProjects.length > 0 && ( diff --git a/apps/app/components/states/create-state-modal.tsx b/apps/app/components/states/create-state-modal.tsx index ed62bb9ac..dca001932 100644 --- a/apps/app/components/states/create-state-modal.tsx +++ b/apps/app/components/states/create-state-modal.tsx @@ -19,7 +19,7 @@ import { CustomSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "c // icons import { ChevronDownIcon } from "@heroicons/react/24/outline"; // types -import type { IState, IStateResponse } from "types"; +import type { ICurrentUserResponse, IState, IStateResponse } from "types"; // fetch keys import { STATES_LIST } from "constants/fetch-keys"; // constants @@ -30,6 +30,7 @@ type Props = { isOpen: boolean; projectId: string; handleClose: () => void; + user: ICurrentUserResponse | undefined; }; const defaultValues: Partial = { @@ -39,7 +40,7 @@ const defaultValues: Partial = { group: "backlog", }; -export const CreateStateModal: React.FC = ({ isOpen, projectId, handleClose }) => { +export const CreateStateModal: React.FC = ({ isOpen, projectId, handleClose, user }) => { const router = useRouter(); const { workspaceSlug } = router.query; @@ -69,7 +70,7 @@ export const CreateStateModal: React.FC = ({ isOpen, projectId, handleClo }; await stateService - .createState(workspaceSlug as string, projectId, payload) + .createState(workspaceSlug as string, projectId, payload, user) .then((res) => { mutate( STATES_LIST(projectId.toString()), diff --git a/apps/app/components/states/create-update-state-inline.tsx b/apps/app/components/states/create-update-state-inline.tsx index 42ab52945..8a9d81968 100644 --- a/apps/app/components/states/create-update-state-inline.tsx +++ b/apps/app/components/states/create-update-state-inline.tsx @@ -17,7 +17,7 @@ import useToast from "hooks/use-toast"; // ui import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui"; // types -import type { IState, IStateResponse } from "types"; +import type { ICurrentUserResponse, IState, IStateResponse } from "types"; // fetch-keys import { STATES_LIST } from "constants/fetch-keys"; // constants @@ -27,6 +27,7 @@ type Props = { data: IState | null; onClose: () => void; selectedGroup: StateGroup | null; + user: ICurrentUserResponse | undefined; }; export type StateGroup = "backlog" | "unstarted" | "started" | "completed" | "cancelled" | null; @@ -37,7 +38,12 @@ const defaultValues: Partial = { group: "backlog", }; -export const CreateUpdateStateInline: React.FC = ({ data, onClose, selectedGroup }) => { +export const CreateUpdateStateInline: React.FC = ({ + data, + onClose, + selectedGroup, + user, +}) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -83,7 +89,7 @@ export const CreateUpdateStateInline: React.FC = ({ data, onClose, select if (!data) { await stateService - .createState(workspaceSlug.toString(), projectId.toString(), { ...payload }) + .createState(workspaceSlug.toString(), projectId.toString(), { ...payload }, user) .then((res) => { mutate( STATES_LIST(projectId.toString()), @@ -121,9 +127,15 @@ export const CreateUpdateStateInline: React.FC = ({ data, onClose, select }); } else { await stateService - .updateState(workspaceSlug.toString(), projectId.toString(), data.id, { - ...payload, - }) + .updateState( + workspaceSlug.toString(), + projectId.toString(), + data.id, + { + ...payload, + }, + user + ) .then(() => { mutate(STATES_LIST(projectId.toString())); handleClose(); diff --git a/apps/app/components/states/delete-state-modal.tsx b/apps/app/components/states/delete-state-modal.tsx index 7fdce5d44..a92f235c4 100644 --- a/apps/app/components/states/delete-state-modal.tsx +++ b/apps/app/components/states/delete-state-modal.tsx @@ -15,7 +15,7 @@ import useToast from "hooks/use-toast"; // ui import { DangerButton, SecondaryButton } from "components/ui"; // types -import type { IState, IStateResponse } from "types"; +import type { ICurrentUserResponse, IState, IStateResponse } from "types"; // fetch-keys import { STATES_LIST } from "constants/fetch-keys"; @@ -23,9 +23,10 @@ type Props = { isOpen: boolean; onClose: () => void; data: IState | null; + user: ICurrentUserResponse | undefined; }; -export const DeleteStateModal: React.FC = ({ isOpen, onClose, data }) => { +export const DeleteStateModal: React.FC = ({ isOpen, onClose, data, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); @@ -44,7 +45,7 @@ export const DeleteStateModal: React.FC = ({ isOpen, onClose, data }) => setIsDeleteLoading(true); await stateServices - .deleteState(workspaceSlug as string, data.project, data.id) + .deleteState(workspaceSlug as string, data.project, data.id, user) .then(() => { mutate( STATES_LIST(data.project), diff --git a/apps/app/components/states/single-state.tsx b/apps/app/components/states/single-state.tsx index d9425c524..8bf4198fe 100644 --- a/apps/app/components/states/single-state.tsx +++ b/apps/app/components/states/single-state.tsx @@ -21,7 +21,7 @@ import { addSpaceIfCamelCase } from "helpers/string.helper"; import { groupBy, orderArrayBy } from "helpers/array.helper"; import { orderStateGroups } from "helpers/state.helper"; // types -import { IState } from "types"; +import { ICurrentUserResponse, IState } from "types"; // fetch-keys import { STATES_LIST } from "constants/fetch-keys"; @@ -31,6 +31,7 @@ type Props = { statesList: IState[]; handleEditState: () => void; handleDeleteState: () => void; + user: ICurrentUserResponse | undefined; }; export const SingleState: React.FC = ({ @@ -39,6 +40,7 @@ export const SingleState: React.FC = ({ statesList, handleEditState, handleDeleteState, + user, }) => { const [isSubmitting, setIsSubmitting] = useState(false); @@ -67,14 +69,26 @@ export const SingleState: React.FC = ({ if (currentDefaultState) stateService - .patchState(workspaceSlug as string, projectId as string, currentDefaultState?.id ?? "", { - default: false, - }) + .patchState( + workspaceSlug as string, + projectId as string, + currentDefaultState?.id ?? "", + { + default: false, + }, + user + ) .then(() => { stateService - .patchState(workspaceSlug as string, projectId as string, state.id, { - default: true, - }) + .patchState( + workspaceSlug as string, + projectId as string, + state.id, + { + default: true, + }, + user + ) .then(() => { mutate(STATES_LIST(projectId as string)); setIsSubmitting(false); @@ -85,9 +99,15 @@ export const SingleState: React.FC = ({ }); else stateService - .patchState(workspaceSlug as string, projectId as string, state.id, { - default: true, - }) + .patchState( + workspaceSlug as string, + projectId as string, + state.id, + { + default: true, + }, + user + ) .then(() => { mutate(STATES_LIST(projectId as string)); setIsSubmitting(false); @@ -121,9 +141,15 @@ export const SingleState: React.FC = ({ ); stateService - .patchState(workspaceSlug as string, projectId as string, state.id, { - sequence: newSequence, - }) + .patchState( + workspaceSlug as string, + projectId as string, + state.id, + { + sequence: newSequence, + }, + user + ) .then((res) => { console.log(res); mutate(STATES_LIST(projectId as string)); diff --git a/apps/app/components/views/delete-view-modal.tsx b/apps/app/components/views/delete-view-modal.tsx index 31cfc33e9..c57c29dc3 100644 --- a/apps/app/components/views/delete-view-modal.tsx +++ b/apps/app/components/views/delete-view-modal.tsx @@ -15,7 +15,7 @@ import { DangerButton, SecondaryButton } from "components/ui"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types -import type { IView } from "types"; +import type { ICurrentUserResponse, IView } from "types"; // fetch-keys import { VIEWS_LIST } from "constants/fetch-keys"; @@ -23,9 +23,10 @@ type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; data: IView | null; + user: ICurrentUserResponse | undefined; }; -export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen }) => { +export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); @@ -43,7 +44,7 @@ export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen }) => if (!workspaceSlug || !data || !projectId) return; await viewsService - .deleteView(workspaceSlug as string, projectId as string, data.id) + .deleteView(workspaceSlug as string, projectId as string, data.id, user) .then(() => { mutate(VIEWS_LIST(projectId as string), (views) => views?.filter((view) => view.id !== data.id) diff --git a/apps/app/components/views/modal.tsx b/apps/app/components/views/modal.tsx index a04c19b64..755251356 100644 --- a/apps/app/components/views/modal.tsx +++ b/apps/app/components/views/modal.tsx @@ -13,7 +13,7 @@ import useToast from "hooks/use-toast"; // components import { ViewForm } from "components/views"; // types -import { IView } from "types"; +import { ICurrentUserResponse, IView } from "types"; // fetch-keys import { VIEWS_LIST } from "constants/fetch-keys"; @@ -22,6 +22,7 @@ type Props = { handleClose: () => void; data?: IView | null; preLoadedData?: Partial | null; + user: ICurrentUserResponse | undefined; }; export const CreateUpdateViewModal: React.FC = ({ @@ -29,6 +30,7 @@ export const CreateUpdateViewModal: React.FC = ({ handleClose, data, preLoadedData, + user, }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -45,7 +47,7 @@ export const CreateUpdateViewModal: React.FC = ({ query_data: payload.query, }; await viewsService - .createView(workspaceSlug as string, projectId as string, payload) + .createView(workspaceSlug as string, projectId as string, payload, user) .then(() => { mutate(VIEWS_LIST(projectId as string)); handleClose(); @@ -71,7 +73,7 @@ export const CreateUpdateViewModal: React.FC = ({ query_data: payload.query, }; await viewsService - .updateView(workspaceSlug as string, projectId as string, data?.id ?? "", payloadData) + .updateView(workspaceSlug as string, projectId as string, data?.id ?? "", payloadData, user) .then((res) => { mutate( VIEWS_LIST(projectId as string), diff --git a/apps/app/components/workspace/create-workspace-form.tsx b/apps/app/components/workspace/create-workspace-form.tsx index b3415b2c8..e3da82c10 100644 --- a/apps/app/components/workspace/create-workspace-form.tsx +++ b/apps/app/components/workspace/create-workspace-form.tsx @@ -11,7 +11,7 @@ import useToast from "hooks/use-toast"; // ui import { CustomSelect, Input, PrimaryButton } from "components/ui"; // types -import { IWorkspace } from "types"; +import { ICurrentUserResponse, IWorkspace } from "types"; // fetch-keys import { USER_WORKSPACES } from "constants/fetch-keys"; // constants @@ -25,6 +25,7 @@ type Props = { company_size: number | null; }; setDefaultValues: Dispatch>; + user: ICurrentUserResponse | undefined; }; const restrictedUrls = [ @@ -44,6 +45,7 @@ export const CreateWorkspaceForm: React.FC = ({ onSubmit, defaultValues, setDefaultValues, + user, }) => { const [slugError, setSlugError] = useState(false); const [invalidSlug, setInvalidSlug] = useState(false); @@ -66,7 +68,7 @@ export const CreateWorkspaceForm: React.FC = ({ if (res.status === true && !restrictedUrls.includes(formData.slug)) { setSlugError(false); await workspaceService - .createWorkspace(formData) + .createWorkspace(formData, user) .then((res) => { setToastAlert({ type: "success", diff --git a/apps/app/components/workspace/delete-workspace-modal.tsx b/apps/app/components/workspace/delete-workspace-modal.tsx index ceacd11e7..344d700b0 100644 --- a/apps/app/components/workspace/delete-workspace-modal.tsx +++ b/apps/app/components/workspace/delete-workspace-modal.tsx @@ -15,7 +15,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // ui import { DangerButton, Input, SecondaryButton } from "components/ui"; // types -import type { IWorkspace } from "types"; +import type { ICurrentUserResponse, IWorkspace } from "types"; // fetch-keys import { USER_WORKSPACES } from "constants/fetch-keys"; @@ -23,9 +23,10 @@ type Props = { isOpen: boolean; data: IWorkspace | null; onClose: () => void; + user: ICurrentUserResponse | undefined; }; -export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose }) => { +export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [confirmWorkspaceName, setConfirmWorkspaceName] = useState(""); @@ -57,7 +58,7 @@ export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose }) setIsDeleteLoading(true); if (!data || !canDelete) return; await workspaceService - .deleteWorkspace(data.slug) + .deleteWorkspace(data.slug, user) .then(() => { handleClose(); router.push("/"); diff --git a/apps/app/components/workspace/send-workspace-invitation-modal.tsx b/apps/app/components/workspace/send-workspace-invitation-modal.tsx index f58269b89..52dc74149 100644 --- a/apps/app/components/workspace/send-workspace-invitation-modal.tsx +++ b/apps/app/components/workspace/send-workspace-invitation-modal.tsx @@ -10,7 +10,7 @@ import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ // hooks import useToast from "hooks/use-toast"; // types -import { IWorkspaceMemberInvitation } from "types"; +import { ICurrentUserResponse, IWorkspaceMemberInvitation } from "types"; // fetch keys import { WORKSPACE_INVITATIONS } from "constants/fetch-keys"; // constants @@ -21,6 +21,7 @@ type Props = { setIsOpen: React.Dispatch>; workspace_slug: string; members: any[]; + user: ICurrentUserResponse | undefined; }; const defaultValues: Partial = { @@ -33,6 +34,7 @@ const SendWorkspaceInvitationModal: React.FC = ({ setIsOpen, workspace_slug, members, + user, }) => { const { setToastAlert } = useToast(); @@ -54,7 +56,7 @@ const SendWorkspaceInvitationModal: React.FC = ({ const onSubmit = async (formData: IWorkspaceMemberInvitation) => { await workspaceService - .inviteWorkspace(workspace_slug, { emails: [formData] }) + .inviteWorkspace(workspace_slug, { emails: [formData] }, user) .then((res) => { setIsOpen(false); handleClose(); @@ -101,7 +103,10 @@ const SendWorkspaceInvitationModal: React.FC = ({
    - + Members

    diff --git a/apps/app/components/workspace/single-invitation.tsx b/apps/app/components/workspace/single-invitation.tsx index 9dbdc62ce..6d89c7675 100644 --- a/apps/app/components/workspace/single-invitation.tsx +++ b/apps/app/components/workspace/single-invitation.tsx @@ -46,7 +46,10 @@ const SingleInvitation: React.FC = ({

    {truncateText(invitation.workspace.name, 30)}

    - Invited by {invitation.workspace.owner.first_name} + Invited by{" "} + {invitation.created_by_detail + ? invitation.created_by_detail.first_name + : invitation.workspace.owner.first_name}

    diff --git a/apps/app/contexts/issue-view.context.tsx b/apps/app/contexts/issue-view.context.tsx index aa3ec586c..619027866 100644 --- a/apps/app/contexts/issue-view.context.tsx +++ b/apps/app/contexts/issue-view.context.tsx @@ -18,6 +18,7 @@ import { IProjectMember, TIssueGroupByOptions, TIssueOrderByOptions, + ICurrentUserResponse, } from "types"; // fetch-keys import { @@ -26,6 +27,7 @@ import { USER_PROJECT_VIEW, VIEW_DETAILS, } from "constants/fetch-keys"; +import useUserAuth from "hooks/use-user-auth"; export const issueViewContext = createContext({} as ContextType); @@ -212,33 +214,54 @@ const saveCycleFilters = async ( workspaceSlug: string, projectId: string, cycleId: string, - state: any + state: any, + user: ICurrentUserResponse | undefined ) => { - await cyclesService.patchCycle(workspaceSlug, projectId, cycleId, { - ...state, - }); + await cyclesService.patchCycle( + workspaceSlug, + projectId, + cycleId, + { + ...state, + }, + user + ); }; const saveModuleFilters = async ( workspaceSlug: string, projectId: string, moduleId: string, - state: any + state: any, + user: ICurrentUserResponse | undefined ) => { - await modulesService.patchModule(workspaceSlug, projectId, moduleId, { - ...state, - }); + await modulesService.patchModule( + workspaceSlug, + projectId, + moduleId, + { + ...state, + }, + user + ); }; const saveViewFilters = async ( workspaceSlug: string, projectId: string, viewId: string, - state: any + state: any, + user: ICurrentUserResponse | undefined ) => { - await viewsService.patchView(workspaceSlug, projectId, viewId, { - ...state, - }); + await viewsService.patchView( + workspaceSlug, + projectId, + viewId, + { + ...state, + }, + user + ); }; const setNewDefault = async (workspaceSlug: string, projectId: string, state: any) => { @@ -267,6 +290,8 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> = const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; + const { user } = useUserAuth(); + const { data: myViewProps, mutate: mutateMyViewProps } = useSWR( workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null, workspaceSlug && projectId @@ -505,14 +530,20 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> = }; }, false); - saveCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), { - view_props: { - filters: { - ...state.filters, - ...property, + saveCycleFilters( + workspaceSlug.toString(), + projectId.toString(), + cycleId.toString(), + { + view_props: { + filters: { + ...state.filters, + ...property, + }, }, }, - }); + user + ); } else if (moduleId) { mutateModuleDetails((prevData: any) => { if (!prevData) return prevData; @@ -528,14 +559,20 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> = }; }, false); - saveModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), { - view_props: { - filters: { - ...state.filters, - ...property, + saveModuleFilters( + workspaceSlug.toString(), + projectId.toString(), + moduleId.toString(), + { + view_props: { + filters: { + ...state.filters, + ...property, + }, }, }, - }); + user + ); } else if (viewId) { mutateViewDetails((prevData: any) => { if (!prevData) return prevData; @@ -548,12 +585,18 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> = }; }, false); if (saveToServer) - saveViewFilters(workspaceSlug as string, projectId as string, viewId as string, { - query_data: { - ...state.filters, - ...property, + saveViewFilters( + workspaceSlug as string, + projectId as string, + viewId as string, + { + query_data: { + ...state.filters, + ...property, + }, }, - }); + user + ); } else { mutateMyViewProps((prevData) => { if (!prevData) return prevData; diff --git a/apps/app/pages/[workspaceSlug]/analytics.tsx b/apps/app/pages/[workspaceSlug]/analytics.tsx index ce161fbc4..eb2816b56 100644 --- a/apps/app/pages/[workspaceSlug]/analytics.tsx +++ b/apps/app/pages/[workspaceSlug]/analytics.tsx @@ -6,6 +6,8 @@ import useSWR from "swr"; // react-hook-form import { useForm } from "react-hook-form"; +// hooks +import useUserAuth from "hooks/use-user-auth"; // headless ui import { Tab } from "@headlessui/react"; // services @@ -35,6 +37,8 @@ const Analytics = () => { const router = useRouter(); const { workspaceSlug } = router.query; + const { user } = useUserAuth(); + const { control, watch, setValue } = useForm({ defaultValues }); const params: IAnalyticsParams = { @@ -59,7 +63,7 @@ const Analytics = () => { ? "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" : "WORKSPACE_CUSTOM_ANALYTICS"; - trackEventServices.trackAnalyticsEvent(eventPayload, eventType); + trackEventServices.trackAnalyticsEvent(eventPayload, eventType, user); }; useEffect(() => { @@ -67,7 +71,8 @@ const Analytics = () => { trackEventServices.trackAnalyticsEvent( { workspaceSlug: workspaceSlug?.toString() }, - "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" + "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS", + user ); }, [workspaceSlug]); @@ -119,6 +124,7 @@ const Analytics = () => { params={params} control={control} setValue={setValue} + user={user} fullScreen /> diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 087747153..718a8bd3a 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -19,6 +19,7 @@ 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"; // components import { AnalyticsProjectModal } from "components/analytics"; // ui @@ -44,6 +45,8 @@ const SingleCycle: React.FC = () => { const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const { data: activeProject } = useSWR( @@ -94,7 +97,7 @@ const SingleCycle: React.FC = () => { if (!workspaceSlug || !projectId) return; await issuesService - .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, data) + .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, data, user) .then(() => { mutate(CYCLE_ISSUES(cycleId as string)); }) @@ -185,6 +188,7 @@ const SingleCycle: React.FC = () => { cycle={cycleDetails} isOpen={cycleSidebar} isCompleted={cycleStatus === "completed" ?? false} + user={user} /> diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index bac59acc9..97519f41a 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -8,6 +8,7 @@ import useSWR from "swr"; import { Tab } from "@headlessui/react"; // hooks import useLocalStorage from "hooks/use-local-storage"; +import useUserAuth from "hooks/use-user-auth"; // services import cycleService from "services/cycles.service"; import projectService from "services/project.service"; @@ -62,6 +63,8 @@ const ProjectCycles: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { data: activeProject } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId @@ -110,6 +113,7 @@ const ProjectCycles: NextPage = () => { isOpen={createUpdateCycleModal} handleClose={() => setCreateUpdateCycleModal(false)} data={selectedCycle} + user={user} />
    diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 29eaee83c..5f6615f55 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -7,6 +7,8 @@ import useSWR, { mutate } from "swr"; // react-hook-form import { useForm } from "react-hook-form"; +// hooks +import useUserAuth from "hooks/use-user-auth"; // services import issuesService from "services/issues.service"; // layouts @@ -50,6 +52,8 @@ const IssueDetailsPage: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; + const { user } = useUserAuth(); + const { data: issueDetails, mutate: mutateIssueDetails } = useSWR( workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, workspaceSlug && projectId && issueId @@ -92,7 +96,7 @@ const IssueDetailsPage: NextPage = () => { const payload = { ...formData }; await issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) .then((res) => { mutateIssueDetails(); mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); @@ -192,7 +196,7 @@ const IssueDetailsPage: NextPage = () => { ) : null}
    - +
    @@ -204,8 +208,8 @@ const IssueDetailsPage: NextPage = () => {

    Comments/Activity

    - - + +
    diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index f9df68e7e..657f48fe2 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -17,6 +17,7 @@ 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"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // contexts @@ -49,6 +50,8 @@ const SingleModule: React.FC = () => { const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const { data: issues } = useSWR( @@ -95,7 +98,13 @@ const SingleModule: React.FC = () => { if (!workspaceSlug || !projectId) return; await modulesService - .addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, data) + .addIssuesToModule( + workspaceSlug as string, + projectId as string, + moduleId as string, + data, + user + ) .then(() => mutate(MODULE_ISSUES(moduleId as string))) .catch(() => setToastAlert({ @@ -186,6 +195,7 @@ const SingleModule: React.FC = () => { module={moduleDetails} isOpen={moduleSidebar} moduleIssues={moduleIssues} + user={user} /> diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx index 7e2cd1565..be92a9f86 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx @@ -6,6 +6,8 @@ import useSWR from "swr"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; +// hooks +import useUserAuth from "hooks/use-user-auth"; // services import projectService from "services/project.service"; import modulesService from "services/modules.service"; @@ -37,6 +39,8 @@ const ProjectModules: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { data: activeProject } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId @@ -89,6 +93,7 @@ const ProjectModules: NextPage = () => { isOpen={createUpdateModule} setIsOpen={setCreateUpdateModule} data={selectedModule} + user={user} /> {modules ? ( modules.length > 0 ? ( @@ -126,6 +131,7 @@ const ProjectModules: NextPage = () => { key={module.id} module={module} handleEditModule={() => handleEditModule(module)} + user={user} /> ))}
    diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index 61b49a049..c157d7b96 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -116,7 +116,7 @@ const SinglePage: NextPage = () => { if (!formData.name || formData.name.length === 0 || formData.name === "") return; await pagesService - .patchPage(workspaceSlug as string, projectId as string, pageId as string, formData) + .patchPage(workspaceSlug as string, projectId as string, pageId as string, formData, user) .then(() => { mutate( PAGE_DETAILS(pageId as string), @@ -143,7 +143,7 @@ const SinglePage: NextPage = () => { ); await pagesService - .patchPage(workspaceSlug as string, projectId as string, pageId as string, formData) + .patchPage(workspaceSlug as string, projectId as string, pageId as string, formData, user) .then(() => { mutate(PAGE_DETAILS(pageId as string)); }); @@ -237,7 +237,8 @@ const SinglePage: NextPage = () => { result.draggableId, { sort_order: newSortOrder, - } + }, + user ); }; @@ -529,6 +530,7 @@ const SinglePage: NextPage = () => { block={block} projectDetails={projectDetails} index={index} + user={user} /> ))} {provided.placeholder} @@ -542,6 +544,7 @@ const SinglePage: NextPage = () => { setCreateBlockForm(false)} focus="name" + user={user} />
    )} @@ -550,6 +553,7 @@ const SinglePage: NextPage = () => { isOpen={labelModal} handleClose={() => setLabelModal(false)} projectId={projectId} + user={user} /> )} @@ -562,7 +566,7 @@ const SinglePage: NextPage = () => {
    - +
    ) : ( diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index dba6cce54..a51c8b44f 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -27,6 +27,7 @@ import { TPageViewProps } from "types"; import type { NextPage } from "next"; // fetch-keys import { PROJECT_DETAILS } from "constants/fetch-keys"; +import useUserAuth from "hooks/use-user-auth"; const AllPagesList = dynamic( () => import("components/pages").then((a) => a.AllPagesList), @@ -66,6 +67,8 @@ const ProjectPages: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { storedValue: pageTab, setValue: setPageTab } = useLocalStorage("pageTab", "Recent"); const { data: projectDetails } = useSWR( @@ -98,6 +101,7 @@ const ProjectPages: NextPage = () => { setCreateUpdatePageModal(false)} + user={user} /> { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId @@ -65,7 +68,7 @@ const ControlSettings: NextPage = () => { }; await projectService - .updateProject(workspaceSlug as string, projectId as string, payload) + .updateProject(workspaceSlug as string, projectId as string, payload, user) .then((res) => { mutate(PROJECT_DETAILS(projectId as string)); mutate(PROJECTS_LIST(workspaceSlug as string)); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx index 3f3c0331f..76e353319 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx @@ -16,6 +16,7 @@ import { CreateUpdateEstimateModal, SingleEstimate } from "components/estimates" import { SettingsHeader } from "components/project"; //hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // ui import { EmptyState, Loader, SecondaryButton } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; @@ -37,6 +38,8 @@ const EstimatesSettings: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const { projectDetails } = useProjectDetails(); @@ -63,7 +66,7 @@ const EstimatesSettings: NextPage = () => { ); estimatesService - .deleteEstimate(workspaceSlug as string, projectId as string, estimateId) + .deleteEstimate(workspaceSlug as string, projectId as string, estimateId, user) .catch(() => { setToastAlert({ type: "error", @@ -87,7 +90,7 @@ const EstimatesSettings: NextPage = () => { ); projectService - .updateProject(workspaceSlug as string, projectId as string, { estimate: null }) + .updateProject(workspaceSlug as string, projectId as string, { estimate: null }, user) .catch(() => setToastAlert({ type: "error", @@ -106,6 +109,7 @@ const EstimatesSettings: NextPage = () => { setEstimateFormOpen(false); setEstimateToUpdate(undefined); }} + user={user} /> { estimate={estimate} editEstimate={(estimate) => editEstimate(estimate)} handleEstimateDelete={(estimateId) => removeEstimate(estimateId)} + user={user} /> ))} diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx index 9f8d06b3a..6932ffbd7 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/features.tsx @@ -11,6 +11,7 @@ import trackEventServices, { MiscellaneousEventType } from "services/track-event import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // components import { SettingsHeader } from "components/project"; // ui @@ -75,6 +76,8 @@ const FeaturesSettings: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const { data: projectDetails } = useSWR( @@ -134,7 +137,7 @@ const FeaturesSettings: NextPage = () => { }); await projectService - .updateProject(workspaceSlug as string, projectId as string, formData) + .updateProject(workspaceSlug as string, projectId as string, formData, user) .then(() => { mutate( projectDetails.is_favorite @@ -194,7 +197,8 @@ const FeaturesSettings: NextPage = () => { }, !projectDetails?.[feature.property as keyof IProject] ? getEventType(feature.title, true) - : getEventType(feature.title, false) + : getEventType(feature.title, false), + user ); handleSubmit({ [feature.property]: !projectDetails?.[feature.property as keyof IProject], diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx index 5210c9ebe..14e20811a 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx @@ -17,6 +17,7 @@ import { ImagePickerPopover } from "components/core"; import EmojiIconPicker from "components/emoji-icon-picker"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // ui import { Input, @@ -45,6 +46,8 @@ const defaultValues: Partial = { const GeneralSettings: NextPage = () => { const [selectProject, setSelectedProject] = useState(null); + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const router = useRouter(); @@ -83,7 +86,7 @@ const GeneralSettings: NextPage = () => { if (!workspaceSlug || !projectDetails) return; await projectService - .updateProject(workspaceSlug as string, projectDetails.id, payload) + .updateProject(workspaceSlug as string, projectDetails.id, payload, user) .then((res) => { mutate( PROJECT_DETAILS(projectDetails.id), @@ -154,6 +157,7 @@ const GeneralSettings: NextPage = () => { onSuccess={() => { router.push(`/${workspaceSlug}/projects`); }} + user={user} /> diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx index eea76527a..6d48c5ee0 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx @@ -4,6 +4,8 @@ import { useRouter } from "next/router"; import useSWR from "swr"; +// hooks +import useUserAuth from "hooks/use-user-auth"; // services import projectService from "services/project.service"; import issuesService from "services/issues.service"; @@ -47,6 +49,8 @@ const LabelsSettings: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const scrollToRef = useRef(null); const { data: projectDetails } = useSWR( @@ -85,11 +89,13 @@ const LabelsSettings: NextPage = () => { isOpen={labelsListModal} handleClose={() => setLabelsListModal(false)} parent={parentLabel} + user={user} /> setSelectDeleteLabel(null)} + user={user} /> { }); }} handleLabelDelete={() => setSelectDeleteLabel(label)} + user={user} /> ); }) diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx index ce9110aad..942ed7037 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx @@ -135,6 +135,7 @@ const MembersSettings: NextPage = () => { isOpen={inviteModal} setIsOpen={setInviteModal} members={members} + user={user} /> { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { projectDetails } = useProjectDetails(); const { data: states } = useSWR( @@ -55,6 +58,7 @@ const StatesSettings: NextPage = () => { isOpen={!!selectDeleteState} data={statesList?.find((s) => s.id === selectDeleteState) ?? null} onClose={() => setSelectDeleteState(null)} + user={user} /> { }} data={null} selectedGroup={key as keyof StateGroup} + user={user} /> )} {orderedStateGroups[key].map((state, index) => @@ -111,6 +116,7 @@ const StatesSettings: NextPage = () => { statesList={statesList} handleEditState={() => setSelectedState(state.id)} handleDeleteState={() => setSelectDeleteState(state.id)} + user={user} /> ) : (
    { statesList?.find((state) => state.id === selectedState) ?? null } selectedGroup={key as keyof StateGroup} + user={user} />
    ) diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx index 6d2798d87..44c25cdf0 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx @@ -4,6 +4,8 @@ import { useRouter } from "next/router"; import useSWR from "swr"; +// hooks +import useUserAuth from "hooks/use-user-auth"; // services import viewsService from "services/views.service"; import projectService from "services/project.service"; @@ -34,6 +36,8 @@ const ProjectViews: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { user } = useUserAuth(); + const { data: activeProject } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId @@ -86,11 +90,13 @@ const ProjectViews: NextPage = () => { isOpen={createUpdateViewModal} handleClose={() => setCreateUpdateViewModal(false)} data={selectedViewToUpdate} + user={user} /> {views ? ( views.length > 0 ? ( diff --git a/apps/app/pages/[workspaceSlug]/projects/index.tsx b/apps/app/pages/[workspaceSlug]/projects/index.tsx index 6b29d41c5..fa34f02c0 100644 --- a/apps/app/pages/[workspaceSlug]/projects/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/index.tsx @@ -9,6 +9,7 @@ import projectService from "services/project.service"; // hooks import useProjects from "hooks/use-projects"; import useWorkspaces from "hooks/use-workspaces"; +import useUserAuth from "hooks/use-user-auth"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; // components @@ -30,6 +31,8 @@ const ProjectsPage: NextPage = () => { // router const router = useRouter(); const { workspaceSlug } = router.query; + + const { user } = useUserAuth(); // context data const { activeWorkspace } = useWorkspaces(); const { projects } = useProjects(); @@ -81,6 +84,7 @@ const ProjectsPage: NextPage = () => { isOpen={!!deleteProject} onClose={() => setDeleteProject(null)} data={projects?.find((item) => item.id === deleteProject) ?? null} + user={user} /> {projects ? (
    diff --git a/apps/app/pages/[workspaceSlug]/settings/index.tsx b/apps/app/pages/[workspaceSlug]/settings/index.tsx index 0913b28ab..0400ecaee 100644 --- a/apps/app/pages/[workspaceSlug]/settings/index.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/index.tsx @@ -12,6 +12,7 @@ import workspaceService from "services/workspace.service"; import fileService from "services/file.service"; // hooks import useToast from "hooks/use-toast"; +import useUserAuth from "hooks/use-user-auth"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import SettingsNavbar from "layouts/settings-navbar"; @@ -49,6 +50,8 @@ const WorkspaceSettings: NextPage = () => { const router = useRouter(); const { workspaceSlug } = router.query; + const { user } = useUserAuth(); + const { setToastAlert } = useToast(); const { data: activeWorkspace } = useSWR( @@ -82,7 +85,7 @@ const WorkspaceSettings: NextPage = () => { }; await workspaceService - .updateWorkspace(activeWorkspace.slug, payload) + .updateWorkspace(activeWorkspace.slug, payload, user) .then((res) => { mutate(USER_WORKSPACES, (prevData) => prevData?.map((workspace) => (workspace.id === res.id ? res : workspace)) @@ -114,7 +117,7 @@ const WorkspaceSettings: NextPage = () => { fileService.deleteFile(asset).then(() => { workspaceService - .updateWorkspace(activeWorkspace.slug, { logo: "" }) + .updateWorkspace(activeWorkspace.slug, { logo: "" }, user) .then((res) => { setToastAlert({ type: "success", @@ -169,6 +172,7 @@ const WorkspaceSettings: NextPage = () => { setIsOpen(false); }} data={activeWorkspace ?? null} + user={user} />
    diff --git a/apps/app/pages/[workspaceSlug]/settings/members.tsx b/apps/app/pages/[workspaceSlug]/settings/members.tsx index fd80de11b..10969dde5 100644 --- a/apps/app/pages/[workspaceSlug]/settings/members.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/members.tsx @@ -135,6 +135,7 @@ const MembersSettings: NextPage = () => { setIsOpen={setInviteModal} workspace_slug={workspaceSlug as string} members={members} + user={user} /> res.json()) - .then((data) => data.user) - .catch(() => res.status(401).json({ message: "Unauthorized" })); - if (!user) return res.status(401).json({ message: "Unauthorized" }); // TODO: cache user info diff --git a/apps/app/pages/create-workspace.tsx b/apps/app/pages/create-workspace.tsx index 389cdd7ec..82f0d2001 100644 --- a/apps/app/pages/create-workspace.tsx +++ b/apps/app/pages/create-workspace.tsx @@ -47,6 +47,7 @@ const CreateWorkspace: NextPage = () => { defaultValues={defaultValues} setDefaultValues={() => {}} onSubmit={(res) => router.push(`/${res.slug}`)} + user={user} />
    diff --git a/apps/app/pages/index.tsx b/apps/app/pages/index.tsx index 9f3ef51ba..d135cc704 100644 --- a/apps/app/pages/index.tsx +++ b/apps/app/pages/index.tsx @@ -95,21 +95,21 @@ const HomePage: NextPage = () => {
    ) : ( -
    +
    Plane Web Logo -
    +
    Sign In to your Plane Account
    -
    +
    {parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? ( <> -
    +
    diff --git a/apps/app/pages/invitations.tsx b/apps/app/pages/invitations.tsx index 107af4051..ed5545557 100644 --- a/apps/app/pages/invitations.tsx +++ b/apps/app/pages/invitations.tsx @@ -124,7 +124,7 @@ const OnBoard: NextPage = () => {
    ) : workspaces && workspaces.length > 0 ? ( -
    +

    Your workspaces

    {workspaces.map((workspace) => ( diff --git a/apps/app/pages/magic-sign-in.tsx b/apps/app/pages/magic-sign-in.tsx index f7308d1d2..0a2c2493f 100644 --- a/apps/app/pages/magic-sign-in.tsx +++ b/apps/app/pages/magic-sign-in.tsx @@ -49,18 +49,18 @@ const MagicSignIn: NextPage = () => { return ( -
    +
    {isSigningIn ? (

    Signing you in...

    -

    +

    Please wait while we are preparing your take off.

    ) : errorSigningIn ? (

    Error

    -

    +

    {errorSigningIn}. { ) : (

    Success

    -

    Redirecting you to the app...

    +

    Redirecting you to the app...

    )}
    diff --git a/apps/app/pages/onboarding.tsx b/apps/app/pages/onboarding.tsx index 64b08e2f8..db056cf5c 100644 --- a/apps/app/pages/onboarding.tsx +++ b/apps/app/pages/onboarding.tsx @@ -53,9 +53,9 @@ const Onboarding: NextPage = () => { {step === 1 ? ( ) : step === 2 ? ( - + ) : ( - + )}
    ) : ( @@ -80,7 +80,7 @@ const Onboarding: NextPage = () => { onClick={() => { if (step === 8) { userService - .updateUserOnBoard({ userRole }) + .updateUserOnBoard({ userRole }, user) .then(async () => { mutate( CURRENT_USER, diff --git a/apps/app/pages/workspace-member-invitation/[invitationId].tsx b/apps/app/pages/workspace-member-invitation/[invitationId].tsx index cc4c5ec6d..8fae9547b 100644 --- a/apps/app/pages/workspace-member-invitation/[invitationId].tsx +++ b/apps/app/pages/workspace-member-invitation/[invitationId].tsx @@ -41,10 +41,15 @@ const WorkspaceInvitation: NextPage = () => { const handleAccept = () => { if (!invitationDetail) return; workspaceService - .joinWorkspace(invitationDetail.workspace.slug, invitationDetail.id, { - accepted: true, - email: invitationDetail.email, - }) + .joinWorkspace( + invitationDetail.workspace.slug, + invitationDetail.id, + { + accepted: true, + email: invitationDetail.email, + }, + user + ) .then(() => { if (email === user?.email) { router.push("/invitations"); diff --git a/apps/app/services/ai.service.ts b/apps/app/services/ai.service.ts index 6d1e3b0f9..ecb1ada52 100644 --- a/apps/app/services/ai.service.ts +++ b/apps/app/services/ai.service.ts @@ -3,7 +3,7 @@ import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; // types -import { IGptResponse } from "types"; +import { ICurrentUserResponse, IGptResponse } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -18,11 +18,12 @@ class AiServices extends APIService { async createGptTask( workspaceSlug: string, projectId: string, - data: { prompt: string; task: string } + data: { prompt: string; task: string }, + user: ICurrentUserResponse | undefined ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data) .then((response) => { - if (trackEvent) trackEventServices.trackAskGptEvent(response?.data, "ASK_GPT"); + if (trackEvent) trackEventServices.trackAskGptEvent(response?.data, "ASK_GPT", user); return response?.data; }) .catch((error) => { diff --git a/apps/app/services/cycles.service.ts b/apps/app/services/cycles.service.ts index d2cb4f5f3..d305ee63e 100644 --- a/apps/app/services/cycles.service.ts +++ b/apps/app/services/cycles.service.ts @@ -3,7 +3,7 @@ import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; // types -import type { ICycle, IIssue, IIssueViewOptions } from "types"; +import type { ICurrentUserResponse, ICycle, IIssue, IIssueViewOptions } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -15,10 +15,15 @@ class ProjectCycleServices extends APIService { super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); } - async createCycle(workspaceSlug: string, projectId: string, data: any): Promise { + async createCycle( + workspaceSlug: string, + projectId: string, + data: any, + user: ICurrentUserResponse | undefined + ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data) .then((response) => { - if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_CREATE"); + if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_CREATE", user); return response?.data; }) .catch((error) => { @@ -88,14 +93,15 @@ class ProjectCycleServices extends APIService { workspaceSlug: string, projectId: string, cycleId: string, - data: any + data: any, + user: ICurrentUserResponse | undefined ): Promise { return this.put( `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`, data ) .then((response) => { - if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE"); + if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE", user); return response?.data; }) .catch((error) => { @@ -107,14 +113,15 @@ class ProjectCycleServices extends APIService { workspaceSlug: string, projectId: string, cycleId: string, - data: Partial + data: Partial, + user: ICurrentUserResponse | undefined ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`, data ) .then((response) => { - if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE"); + if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE", user); return response?.data; }) .catch((error) => { @@ -122,10 +129,15 @@ class ProjectCycleServices extends APIService { }); } - async deleteCycle(workspaceSlug: string, projectId: string, cycleId: string): Promise { + async deleteCycle( + workspaceSlug: string, + projectId: string, + cycleId: string, + user: ICurrentUserResponse | undefined + ): Promise { return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`) .then((response) => { - if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_DELETE"); + if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_DELETE", user); return response?.data; }) .catch((error) => { diff --git a/apps/app/services/estimates.service.ts b/apps/app/services/estimates.service.ts index d64f37f28..8b0fe25f4 100644 --- a/apps/app/services/estimates.service.ts +++ b/apps/app/services/estimates.service.ts @@ -1,7 +1,7 @@ // services import APIService from "services/api.service"; // types -import type { IEstimate, IEstimateFormData } from "types"; +import type { ICurrentUserResponse, IEstimate, IEstimateFormData } from "types"; import trackEventServices from "services/track-event.service"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -17,12 +17,13 @@ class ProjectEstimateServices extends APIService { async createEstimate( workspaceSlug: string, projectId: string, - data: IEstimateFormData + data: IEstimateFormData, + user: ICurrentUserResponse | undefined ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, data) .then((response) => { if (trackEvent) - trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_CREATE"); + trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_CREATE", user); return response?.data; }) .catch((error) => { @@ -34,7 +35,8 @@ class ProjectEstimateServices extends APIService { workspaceSlug: string, projectId: string, estimateId: string, - data: IEstimateFormData + data: IEstimateFormData, + user: ICurrentUserResponse | undefined ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`, @@ -42,7 +44,7 @@ class ProjectEstimateServices extends APIService { ) .then((response) => { if (trackEvent) - trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_UPDATE"); + trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_UPDATE", user); return response?.data; }) .catch((error) => { @@ -72,13 +74,18 @@ class ProjectEstimateServices extends APIService { }); } - async deleteEstimate(workspaceSlug: string, projectId: string, estimateId: string): Promise { + async deleteEstimate( + workspaceSlug: string, + projectId: string, + estimateId: string, + user: ICurrentUserResponse | undefined + ): Promise { return this.delete( `/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/` ) .then((response) => { if (trackEvent) - trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_DELETE"); + trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_DELETE", user); return response?.data; }) .catch((error) => { diff --git a/apps/app/services/integration/github.service.ts b/apps/app/services/integration/github.service.ts index 101e7ac67..494785f04 100644 --- a/apps/app/services/integration/github.service.ts +++ b/apps/app/services/integration/github.service.ts @@ -1,7 +1,7 @@ import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; -import { IGithubRepoInfo, IGithubServiceImportFormData } from "types"; +import { ICurrentUserResponse, IGithubRepoInfo, IGithubServiceImportFormData } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -39,7 +39,8 @@ class GithubIntegrationService extends APIService { async createGithubServiceImport( workspaceSlug: string, - data: IGithubServiceImportFormData + data: IGithubServiceImportFormData, + user: ICurrentUserResponse | undefined ): Promise { return this.post( `/api/workspaces/${workspaceSlug}/projects/importers/${integrationServiceType}/`, @@ -47,7 +48,7 @@ class GithubIntegrationService extends APIService { ) .then((response) => { if (trackEvent) - trackEventServices.trackImporterEvent(response?.data, "GITHUB_IMPORTER_CREATE"); + trackEventServices.trackImporterEvent(response?.data, "GITHUB_IMPORTER_CREATE", user); return response?.data; }) .catch((error) => { diff --git a/apps/app/services/integration/index.ts b/apps/app/services/integration/index.ts index 51ecd33c1..9cc146866 100644 --- a/apps/app/services/integration/index.ts +++ b/apps/app/services/integration/index.ts @@ -2,7 +2,12 @@ import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; // types -import { IAppIntegration, IImporterService, IWorkspaceIntegration } from "types"; +import { + IAppIntegration, + ICurrentUserResponse, + IImporterService, + IWorkspaceIntegration, +} from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -51,13 +56,14 @@ class IntegrationService extends APIService { async deleteImporterService( workspaceSlug: string, service: string, - importerId: string + importerId: string, + user: ICurrentUserResponse | undefined ): Promise { return this.delete(`/api/workspaces/${workspaceSlug}/importers/${service}/${importerId}/`) .then((response) => { const eventName = service === "github" ? "GITHUB_IMPORTER_DELETE" : "JIRA_IMPORTER_DELETE"; - if (trackEvent) trackEventServices.trackImporterEvent(response?.data, eventName); + if (trackEvent) trackEventServices.trackImporterEvent(response?.data, eventName, user); return response?.data; }) .catch((error) => { diff --git a/apps/app/services/integration/jira.service.ts b/apps/app/services/integration/jira.service.ts index 456530308..8f6a9fec9 100644 --- a/apps/app/services/integration/jira.service.ts +++ b/apps/app/services/integration/jira.service.ts @@ -2,7 +2,7 @@ import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; // types -import { IJiraMetadata, IJiraResponse, IJiraImporterForm } from "types"; +import { IJiraMetadata, IJiraResponse, IJiraImporterForm, ICurrentUserResponse } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -24,11 +24,15 @@ class JiraImportedService extends APIService { }); } - async createJiraImporter(workspaceSlug: string, data: IJiraImporterForm): Promise { + async createJiraImporter( + workspaceSlug: string, + data: IJiraImporterForm, + user: ICurrentUserResponse | undefined + ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/importers/jira/`, data) .then((response) => { if (trackEvent) - trackEventServices.trackImporterEvent(response?.data, "JIRA_IMPORTER_CREATE"); + trackEventServices.trackImporterEvent(response?.data, "JIRA_IMPORTER_CREATE", user); return response?.data; }) .catch((error) => { diff --git a/apps/app/services/issues.service.ts b/apps/app/services/issues.service.ts index c5a23b232..658272e7a 100644 --- a/apps/app/services/issues.service.ts +++ b/apps/app/services/issues.service.ts @@ -2,7 +2,14 @@ import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; // type -import type { IIssue, IIssueActivity, IIssueComment, IIssueLabels, IIssueViewOptions } from "types"; +import type { + ICurrentUserResponse, + IIssue, + IIssueActivity, + IIssueComment, + IIssueLabels, + IIssueViewOptions, +} from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -14,10 +21,15 @@ class ProjectIssuesServices extends APIService { super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); } - async createIssues(workspaceSlug: string, projectId: string, data: any): Promise { + async createIssues( + workspaceSlug: string, + projectId: string, + data: any, + user: ICurrentUserResponse | undefined + ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, data) .then((response) => { - if (trackEvent) trackEventServices.trackIssueEvent(response.data, "ISSUE_CREATE"); + if (trackEvent) trackEventServices.trackIssueEvent(response.data, "ISSUE_CREATE", user); return response?.data; }) .catch((error) => { @@ -93,7 +105,8 @@ class ProjectIssuesServices extends APIService { cycleId: string, data: { issues: string[]; - } + }, + user: ICurrentUserResponse | undefined ) { return this.post( `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`, @@ -111,7 +124,8 @@ class ProjectIssuesServices extends APIService { issueId: response?.data?.[0]?.issue_detail?.id, cycleId, }, - response.data.length > 1 ? "ISSUE_MOVED_TO_CYCLE_IN_BULK" : "ISSUE_MOVED_TO_CYCLE" + response.data.length > 1 ? "ISSUE_MOVED_TO_CYCLE_IN_BULK" : "ISSUE_MOVED_TO_CYCLE", + user ); return response?.data; }) @@ -167,7 +181,8 @@ class ProjectIssuesServices extends APIService { workspaceSlug: string, projectId: string, issueId: string, - data: any + data: any, + user: ICurrentUserResponse | undefined ): Promise { return this.post( `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/`, @@ -175,7 +190,7 @@ class ProjectIssuesServices extends APIService { ) .then((response) => { if (trackEvent) - trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_CREATE"); + trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_CREATE", user); return response?.data; }) .catch((error) => { @@ -188,7 +203,8 @@ class ProjectIssuesServices extends APIService { projectId: string, issueId: string, commentId: string, - data: IIssueComment + data: IIssueComment, + user: ICurrentUserResponse | undefined ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/`, @@ -196,7 +212,7 @@ class ProjectIssuesServices extends APIService { ) .then((response) => { if (trackEvent) - trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_UPDATE"); + trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_UPDATE", user); return response?.data; }) .catch((error) => { @@ -208,7 +224,8 @@ class ProjectIssuesServices extends APIService { workspaceSlug: string, projectId: string, issueId: string, - commentId: string + commentId: string, + user: ICurrentUserResponse | undefined ): Promise { return this.delete( `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/` @@ -220,7 +237,8 @@ class ProjectIssuesServices extends APIService { issueId, commentId, }, - "ISSUE_COMMENT_DELETE" + "ISSUE_COMMENT_DELETE", + user ); return response?.data; }) @@ -240,7 +258,8 @@ class ProjectIssuesServices extends APIService { async createIssueLabel( workspaceSlug: string, projectId: string, - data: any + data: any, + user: ICurrentUserResponse | undefined ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`, data) .then((response: { data: IIssueLabels; [key: string]: any }) => { @@ -256,7 +275,8 @@ class ProjectIssuesServices extends APIService { labelId: response?.data?.id, color: response?.data?.color, }, - "ISSUE_LABEL_CREATE" + "ISSUE_LABEL_CREATE", + user ); return response?.data; }) @@ -269,7 +289,8 @@ class ProjectIssuesServices extends APIService { workspaceSlug: string, projectId: string, labelId: string, - data: any + data: any, + user: ICurrentUserResponse | undefined ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/${labelId}/`, @@ -288,7 +309,8 @@ class ProjectIssuesServices extends APIService { labelId: response?.data?.id, color: response?.data?.color, }, - "ISSUE_LABEL_UPDATE" + "ISSUE_LABEL_UPDATE", + user ); return response?.data; }) @@ -297,7 +319,12 @@ class ProjectIssuesServices extends APIService { }); } - async deleteIssueLabel(workspaceSlug: string, projectId: string, labelId: string): Promise { + async deleteIssueLabel( + workspaceSlug: string, + projectId: string, + labelId: string, + user: ICurrentUserResponse | undefined + ): Promise { return this.delete( `/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/${labelId}/` ) @@ -308,7 +335,8 @@ class ProjectIssuesServices extends APIService { workspaceSlug, projectId, }, - "ISSUE_LABEL_DELETE" + "ISSUE_LABEL_DELETE", + user ); return response?.data; }) @@ -321,14 +349,15 @@ class ProjectIssuesServices extends APIService { workspaceSlug: string, projectId: string, issueId: string, - data: any + data: any, + user: ICurrentUserResponse | undefined ): Promise { return this.put( `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`, data ) .then((response) => { - if (trackEvent) trackEventServices.trackIssueEvent(response.data, "ISSUE_UPDATE"); + if (trackEvent) trackEventServices.trackIssueEvent(response.data, "ISSUE_UPDATE", user); return response?.data; }) .catch((error) => { @@ -340,14 +369,15 @@ class ProjectIssuesServices extends APIService { workspaceSlug: string, projectId: string, issueId: string, - data: Partial + data: Partial, + user: ICurrentUserResponse | undefined ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`, data ) .then((response) => { - if (trackEvent) trackEventServices.trackIssueEvent(response.data, "ISSUE_UPDATE"); + if (trackEvent) trackEventServices.trackIssueEvent(response.data, "ISSUE_UPDATE", user); return response?.data; }) .catch((error) => { @@ -355,10 +385,15 @@ class ProjectIssuesServices extends APIService { }); } - async deleteIssue(workspaceSlug: string, projectId: string, issuesId: string): Promise { + async deleteIssue( + workspaceSlug: string, + projectId: string, + issuesId: string, + user: ICurrentUserResponse | undefined + ): Promise { return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issuesId}/`) .then((response) => { - if (trackEvent) trackEventServices.trackIssueEvent({ issuesId }, "ISSUE_DELETE"); + if (trackEvent) trackEventServices.trackIssueEvent({ issuesId }, "ISSUE_DELETE", user); return response?.data; }) .catch((error) => { @@ -366,13 +401,18 @@ class ProjectIssuesServices extends APIService { }); } - async bulkDeleteIssues(workspaceSlug: string, projectId: string, data: any): Promise { + async bulkDeleteIssues( + workspaceSlug: string, + projectId: string, + data: any, + user: ICurrentUserResponse | undefined + ): Promise { return this.delete( `/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-delete-issues/`, data ) .then((response) => { - if (trackEvent) trackEventServices.trackIssueBulkDeleteEvent(data); + if (trackEvent) trackEventServices.trackIssueBulkDeleteEvent(data, user); return response?.data; }) .catch((error) => { diff --git a/apps/app/services/modules.service.ts b/apps/app/services/modules.service.ts index 89227982c..ec4165c9b 100644 --- a/apps/app/services/modules.service.ts +++ b/apps/app/services/modules.service.ts @@ -3,7 +3,7 @@ import APIService from "services/api.service"; import trackEventServices from "./track-event.service"; // types -import type { IIssueViewOptions, IModule, IIssue } from "types"; +import type { IIssueViewOptions, IModule, IIssue, ICurrentUserResponse } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -23,10 +23,15 @@ class ProjectIssuesServices extends APIService { }); } - async createModule(workspaceSlug: string, projectId: string, data: any): Promise { + async createModule( + workspaceSlug: string, + projectId: string, + data: any, + user: ICurrentUserResponse | undefined + ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`, data) .then((response) => { - if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_CREATE"); + if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_CREATE", user); return response?.data; }) .catch((error) => { @@ -38,14 +43,15 @@ class ProjectIssuesServices extends APIService { workspaceSlug: string, projectId: string, moduleId: string, - data: any + data: any, + user: ICurrentUserResponse | undefined ): Promise { return this.put( `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`, data ) .then((response) => { - if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE"); + if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE", user); return response?.data; }) .catch((error) => { @@ -69,14 +75,15 @@ class ProjectIssuesServices extends APIService { workspaceSlug: string, projectId: string, moduleId: string, - data: any + data: any, + user: ICurrentUserResponse | undefined ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`, data ) .then((response) => { - if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE"); + if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE", user); return response?.data; }) .catch((error) => { @@ -84,12 +91,17 @@ class ProjectIssuesServices extends APIService { }); } - async deleteModule(workspaceSlug: string, projectId: string, moduleId: string): Promise { + async deleteModule( + workspaceSlug: string, + projectId: string, + moduleId: string, + user: ICurrentUserResponse | undefined + ): Promise { return this.delete( `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/` ) .then((response) => { - if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_DELETE"); + if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_DELETE", user); return response?.data; }) .catch((error) => { @@ -136,7 +148,8 @@ class ProjectIssuesServices extends APIService { workspaceSlug: string, projectId: string, moduleId: string, - data: { issues: string[] } + data: { issues: string[] }, + user: ICurrentUserResponse | undefined ): Promise { return this.post( `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`, @@ -154,7 +167,8 @@ class ProjectIssuesServices extends APIService { issueId: response?.data?.[0]?.issue_detail?.id, moduleId, }, - response?.data?.length > 1 ? "ISSUE_MOVED_TO_MODULE_IN_BULK" : "ISSUE_MOVED_TO_MODULE" + response?.data?.length > 1 ? "ISSUE_MOVED_TO_MODULE_IN_BULK" : "ISSUE_MOVED_TO_MODULE", + user ); return response?.data; }) diff --git a/apps/app/services/pages.service.ts b/apps/app/services/pages.service.ts index 5805ac538..72b12012e 100644 --- a/apps/app/services/pages.service.ts +++ b/apps/app/services/pages.service.ts @@ -3,7 +3,7 @@ import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; // types -import { IPage, IPageBlock, RecentPagesResponse, IIssue } from "types"; +import { IPage, IPageBlock, RecentPagesResponse, IIssue, ICurrentUserResponse } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -15,10 +15,15 @@ class PageServices extends APIService { super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); } - async createPage(workspaceSlug: string, projectId: string, data: Partial): Promise { + async createPage( + workspaceSlug: string, + projectId: string, + data: Partial, + user: ICurrentUserResponse | undefined + ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, data) .then((response) => { - if (trackEvent) trackEventServices.trackPageEvent(response?.data, "PAGE_CREATE"); + if (trackEvent) trackEventServices.trackPageEvent(response?.data, "PAGE_CREATE", user); return response?.data; }) .catch((error) => { @@ -30,14 +35,15 @@ class PageServices extends APIService { workspaceSlug: string, projectId: string, pageId: string, - data: Partial + data: Partial, + user: ICurrentUserResponse | undefined ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`, data ) .then((response) => { - if (trackEvent) trackEventServices.trackPageEvent(response?.data, "PAGE_UPDATE"); + if (trackEvent) trackEventServices.trackPageEvent(response?.data, "PAGE_UPDATE", user); return response?.data; }) .catch((error) => { @@ -45,10 +51,15 @@ class PageServices extends APIService { }); } - async deletePage(workspaceSlug: string, projectId: string, pageId: string): Promise { + async deletePage( + workspaceSlug: string, + projectId: string, + pageId: string, + user: ICurrentUserResponse | undefined + ): Promise { return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`) .then((response) => { - if (trackEvent) trackEventServices.trackPageEvent(response?.data, "PAGE_DELETE"); + if (trackEvent) trackEventServices.trackPageEvent(response?.data, "PAGE_DELETE", user); return response?.data; }) .catch((error) => { @@ -123,14 +134,16 @@ class PageServices extends APIService { workspaceSlug: string, projectId: string, pageId: string, - data: Partial + data: Partial, + user: ICurrentUserResponse | undefined ): Promise { return this.post( `/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/`, data ) .then((response) => { - if (trackEvent) trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_CREATE"); + if (trackEvent) + trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_CREATE", user); return response?.data; }) .catch((error) => { @@ -158,14 +171,16 @@ class PageServices extends APIService { projectId: string, pageId: string, pageBlockId: string, - data: Partial + data: Partial, + user: ICurrentUserResponse | undefined ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${pageBlockId}/`, data ) .then((response) => { - if (trackEvent) trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_UPDATE"); + if (trackEvent) + trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_UPDATE", user); return response?.data; }) .catch((error) => { @@ -177,13 +192,15 @@ class PageServices extends APIService { workspaceSlug: string, projectId: string, pageId: string, - pageBlockId: string + pageBlockId: string, + user: ICurrentUserResponse | undefined ): Promise { return this.delete( `/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${pageBlockId}/` ) .then((response) => { - if (trackEvent) trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_DELETE"); + if (trackEvent) + trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_DELETE", user); return response?.data; }) .catch((error) => { @@ -209,14 +226,19 @@ class PageServices extends APIService { workspaceSlug: string, projectId: string, pageId: string, - blockId: string + blockId: string, + user: ICurrentUserResponse | undefined ): Promise { return this.post( `/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${blockId}/issues/` ) .then((response) => { if (trackEvent) - trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_CONVERTED_TO_ISSUE"); + trackEventServices.trackPageBlockEvent( + response?.data, + "PAGE_BLOCK_CONVERTED_TO_ISSUE", + user + ); return response?.data; }) .catch((error) => { diff --git a/apps/app/services/project.service.ts b/apps/app/services/project.service.ts index 1443fee69..2f04f622b 100644 --- a/apps/app/services/project.service.ts +++ b/apps/app/services/project.service.ts @@ -5,6 +5,7 @@ import trackEventServices from "services/track-event.service"; // types import type { GithubRepositoriesResponse, + ICurrentUserResponse, IFavoriteProject, IProject, IProjectMember, @@ -22,10 +23,14 @@ class ProjectServices extends APIService { super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); } - async createProject(workspaceSlug: string, data: Partial): Promise { + async createProject( + workspaceSlug: string, + data: Partial, + user: ICurrentUserResponse | undefined + ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/`, data) .then((response) => { - if (trackEvent) trackEventServices.trackProjectEvent(response.data, "CREATE_PROJECT"); + if (trackEvent) trackEventServices.trackProjectEvent(response.data, "CREATE_PROJECT", user); return response?.data; }) .catch((error) => { @@ -64,11 +69,12 @@ class ProjectServices extends APIService { async updateProject( workspaceSlug: string, projectId: string, - data: Partial + data: Partial, + user: ICurrentUserResponse | undefined ): Promise { return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`, data) .then((response) => { - if (trackEvent) trackEventServices.trackProjectEvent(response.data, "UPDATE_PROJECT"); + if (trackEvent) trackEventServices.trackProjectEvent(response.data, "UPDATE_PROJECT", user); return response?.data; }) .catch((error) => { @@ -76,10 +82,14 @@ class ProjectServices extends APIService { }); } - async deleteProject(workspaceSlug: string, projectId: string): Promise { + async deleteProject( + workspaceSlug: string, + projectId: string, + user: ICurrentUserResponse | undefined + ): Promise { return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`) .then((response) => { - if (trackEvent) trackEventServices.trackProjectEvent({ projectId }, "DELETE_PROJECT"); + if (trackEvent) trackEventServices.trackProjectEvent({ projectId }, "DELETE_PROJECT", user); return response?.data; }) .catch((error) => { @@ -87,7 +97,12 @@ class ProjectServices extends APIService { }); } - async inviteProject(workspaceSlug: string, projectId: string, data: any): Promise { + async inviteProject( + workspaceSlug: string, + projectId: string, + data: any, + user: ICurrentUserResponse | undefined + ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/add/`, data) .then((response) => { if (trackEvent) @@ -99,7 +114,8 @@ class ProjectServices extends APIService { projectName: response?.data?.project?.name, memberEmail: response?.data?.member?.email, }, - "PROJECT_MEMBER_INVITE" + "PROJECT_MEMBER_INVITE", + user ); return response?.data; }) diff --git a/apps/app/services/state.service.ts b/apps/app/services/state.service.ts index a9d0e4cdb..52481f8bb 100644 --- a/apps/app/services/state.service.ts +++ b/apps/app/services/state.service.ts @@ -8,17 +8,22 @@ const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; // types -import type { IState, IStateResponse } from "types"; +import type { ICurrentUserResponse, IState, IStateResponse } from "types"; class ProjectStateServices extends APIService { constructor() { super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); } - async createState(workspaceSlug: string, projectId: string, data: any): Promise { + async createState( + workspaceSlug: string, + projectId: string, + data: any, + user: ICurrentUserResponse | undefined + ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`, data) .then((response) => { - if (trackEvent) trackEventServices.trackStateEvent(response?.data, "STATE_CREATE"); + if (trackEvent) trackEventServices.trackStateEvent(response?.data, "STATE_CREATE", user); return response?.data; }) .catch((error) => { @@ -55,14 +60,15 @@ class ProjectStateServices extends APIService { workspaceSlug: string, projectId: string, stateId: string, - data: IState + data: IState, + user: ICurrentUserResponse | undefined ): Promise { return this.put( `/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`, data ) .then((response) => { - if (trackEvent) trackEventServices.trackStateEvent(response?.data, "STATE_UPDATE"); + if (trackEvent) trackEventServices.trackStateEvent(response?.data, "STATE_UPDATE", user); return response?.data; }) .catch((error) => { @@ -74,14 +80,15 @@ class ProjectStateServices extends APIService { workspaceSlug: string, projectId: string, stateId: string, - data: Partial + data: Partial, + user: ICurrentUserResponse | undefined ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`, data ) .then((response) => { - if (trackEvent) trackEventServices.trackStateEvent(response?.data, "STATE_UPDATE"); + if (trackEvent) trackEventServices.trackStateEvent(response?.data, "STATE_UPDATE", user); return response?.data; }) .catch((error) => { @@ -89,10 +96,15 @@ class ProjectStateServices extends APIService { }); } - async deleteState(workspaceSlug: string, projectId: string, stateId: string): Promise { + async deleteState( + workspaceSlug: string, + projectId: string, + stateId: string, + user: ICurrentUserResponse | undefined + ): Promise { return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`) .then((response) => { - if (trackEvent) trackEventServices.trackStateEvent(response?.data, "STATE_DELETE"); + if (trackEvent) trackEventServices.trackStateEvent(response?.data, "STATE_DELETE", user); return response?.data; }) .catch((error) => { diff --git a/apps/app/services/track-event.service.ts b/apps/app/services/track-event.service.ts index 29ea0084a..cfd7e155c 100644 --- a/apps/app/services/track-event.service.ts +++ b/apps/app/services/track-event.service.ts @@ -6,6 +6,7 @@ const trackEvent = // types import type { + ICurrentUserResponse, ICycle, IEstimate, IGptResponse, @@ -104,7 +105,11 @@ class TrackEventServices extends APIService { super("/"); } - async trackWorkspaceEvent(data: IWorkspace | any, eventName: WorkspaceEventType): Promise { + async trackWorkspaceEvent( + data: IWorkspace | any, + eventName: WorkspaceEventType, + user: ICurrentUserResponse | undefined + ): Promise { let payload: any; if ( eventName !== "DELETE_WORKSPACE" && @@ -127,13 +132,15 @@ class TrackEventServices extends APIService { extra: { ...payload, }, + user: user, }, }); } async trackProjectEvent( data: Partial | any, - eventName: ProjectEventType + eventName: ProjectEventType, + user: ICurrentUserResponse | undefined ): Promise { let payload: any; if (eventName !== "DELETE_PROJECT" && eventName !== "PROJECT_MEMBER_INVITE") @@ -154,11 +161,15 @@ class TrackEventServices extends APIService { extra: { ...payload, }, + user: user, }, }); } - async trackUserOnboardingCompleteEvent(data: any): Promise { + async trackUserOnboardingCompleteEvent( + data: any, + user: ICurrentUserResponse | undefined + ): Promise { return this.request({ url: "/api/track-event", method: "POST", @@ -167,11 +178,16 @@ class TrackEventServices extends APIService { extra: { ...data, }, + user: user, }, }); } - async trackIssueEvent(data: IIssue | any, eventName: IssueEventType): Promise { + async trackIssueEvent( + data: IIssue | any, + eventName: IssueEventType, + user: ICurrentUserResponse | undefined + ): Promise { let payload: any; if (eventName !== "ISSUE_DELETE") payload = { @@ -193,11 +209,15 @@ class TrackEventServices extends APIService { extra: { ...payload, }, + user: user, }, }); } - async trackIssueMarkedAsDoneEvent(data: any): Promise { + async trackIssueMarkedAsDoneEvent( + data: any, + user: ICurrentUserResponse | undefined + ): Promise { if (!trackEvent) return; return this.request({ url: "/api/track-event", @@ -207,6 +227,7 @@ class TrackEventServices extends APIService { extra: { ...data, }, + user: user, }, }); } @@ -218,7 +239,8 @@ class TrackEventServices extends APIService { | "ISSUE_PROPERTY_UPDATE_STATE" | "ISSUE_PROPERTY_UPDATE_ASSIGNEE" | "ISSUE_PROPERTY_UPDATE_DUE_DATE" - | "ISSUE_PROPERTY_UPDATE_ESTIMATE" + | "ISSUE_PROPERTY_UPDATE_ESTIMATE", + user: ICurrentUserResponse | undefined ): Promise { if (!trackEvent) return; return this.request({ @@ -229,13 +251,15 @@ class TrackEventServices extends APIService { extra: { ...data, }, + user: user, }, }); } async trackIssueCommentEvent( data: Partial | any, - eventName: IssueCommentEventType + eventName: IssueCommentEventType, + user: ICurrentUserResponse | undefined ): Promise { let payload: any; if (eventName !== "ISSUE_COMMENT_DELETE") @@ -257,6 +281,7 @@ class TrackEventServices extends APIService { extra: { ...payload, }, + user: user, }, }); } @@ -267,7 +292,8 @@ class TrackEventServices extends APIService { | "ISSUE_MOVED_TO_CYCLE" | "ISSUE_MOVED_TO_MODULE" | "ISSUE_MOVED_TO_CYCLE_IN_BULK" - | "ISSUE_MOVED_TO_MODULE_IN_BULK" + | "ISSUE_MOVED_TO_MODULE_IN_BULK", + user: ICurrentUserResponse | undefined ): Promise { return this.request({ url: "/api/track-event", @@ -277,11 +303,12 @@ class TrackEventServices extends APIService { extra: { ...data, }, + user: user, }, }); } - async trackIssueBulkDeleteEvent(data: any): Promise { + async trackIssueBulkDeleteEvent(data: any, user: ICurrentUserResponse | undefined): Promise { return this.request({ url: "/api/track-event", method: "POST", @@ -290,11 +317,16 @@ class TrackEventServices extends APIService { extra: { ...data, }, + user: user, }, }); } - async trackIssueLabelEvent(data: any, eventName: IssueLabelEventType): Promise { + async trackIssueLabelEvent( + data: any, + eventName: IssueLabelEventType, + user: ICurrentUserResponse | undefined + ): Promise { return this.request({ url: "/api/track-event", method: "POST", @@ -303,11 +335,16 @@ class TrackEventServices extends APIService { extra: { ...data, }, + user: user, }, }); } - async trackStateEvent(data: IState | any, eventName: StateEventType): Promise { + async trackStateEvent( + data: IState | any, + eventName: StateEventType, + user: ICurrentUserResponse | undefined + ): Promise { let payload: any; if (eventName !== "STATE_DELETE") payload = { @@ -329,11 +366,16 @@ class TrackEventServices extends APIService { extra: { ...payload, }, + user: user, }, }); } - async trackCycleEvent(data: ICycle | any, eventName: CycleEventType): Promise { + async trackCycleEvent( + data: ICycle | any, + eventName: CycleEventType, + user: ICurrentUserResponse | undefined + ): Promise { let payload: any; if (eventName !== "CYCLE_DELETE") payload = { @@ -355,11 +397,16 @@ class TrackEventServices extends APIService { extra: { ...payload, }, + user: user, }, }); } - async trackModuleEvent(data: IModule | any, eventName: ModuleEventType): Promise { + async trackModuleEvent( + data: IModule | any, + eventName: ModuleEventType, + user: ICurrentUserResponse | undefined + ): Promise { let payload: any; if (eventName !== "MODULE_DELETE") payload = { @@ -381,11 +428,16 @@ class TrackEventServices extends APIService { extra: { ...payload, }, + user: user, }, }); } - async trackPageEvent(data: Partial | any, eventName: PagesEventType): Promise { + async trackPageEvent( + data: Partial | any, + eventName: PagesEventType, + user: ICurrentUserResponse | undefined + ): Promise { let payload: any; if (eventName !== "PAGE_DELETE") payload = { @@ -407,13 +459,15 @@ class TrackEventServices extends APIService { extra: { ...payload, }, + user: user, }, }); } async trackPageBlockEvent( data: Partial | IIssue, - eventName: PageBlocksEventType + eventName: PageBlocksEventType, + user: ICurrentUserResponse | undefined ): Promise { let payload: any; if (eventName !== "PAGE_BLOCK_DELETE" && eventName !== "PAGE_BLOCK_CONVERTED_TO_ISSUE") @@ -447,11 +501,16 @@ class TrackEventServices extends APIService { extra: { ...payload, }, + user: user, }, }); } - async trackAskGptEvent(data: IGptResponse, eventName: GptEventType): Promise { + async trackAskGptEvent( + data: IGptResponse, + eventName: GptEventType, + user: ICurrentUserResponse | undefined + ): Promise { const payload = { workspaceId: data?.workspace_detail?.id, workspaceName: data?.workspace_detail?.name, @@ -469,11 +528,16 @@ class TrackEventServices extends APIService { extra: { ...payload, }, + user: user, }, }); } - async trackUseGPTResponseEvent(data: IIssue | IPageBlock, eventName: GptEventType): Promise { + async trackUseGPTResponseEvent( + data: IIssue | IPageBlock, + eventName: GptEventType, + user: ICurrentUserResponse | undefined + ): Promise { if (!trackEvent) return; let payload: any; @@ -509,11 +573,16 @@ class TrackEventServices extends APIService { extra: { ...payload, }, + user: user, }, }); } - async trackViewEvent(data: IView, eventName: ViewEventType): Promise { + async trackViewEvent( + data: IView, + eventName: ViewEventType, + user: ICurrentUserResponse | undefined + ): Promise { let payload: any; if (eventName === "VIEW_DELETE") payload = data; else @@ -533,11 +602,16 @@ class TrackEventServices extends APIService { extra: { ...payload, }, + user: user, }, }); } - async trackMiscellaneousEvent(data: any, eventName: MiscellaneousEventType): Promise { + async trackMiscellaneousEvent( + data: any, + eventName: MiscellaneousEventType, + user: ICurrentUserResponse | undefined + ): Promise { return this.request({ url: "/api/track-event", method: "POST", @@ -546,11 +620,16 @@ class TrackEventServices extends APIService { extra: { ...data, }, + user: user, }, }); } - async trackAppIntegrationEvent(data: any, eventName: IntegrationEventType): Promise { + async trackAppIntegrationEvent( + data: any, + eventName: IntegrationEventType, + user: ICurrentUserResponse | undefined + ): Promise { return this.request({ url: "/api/track-event", method: "POST", @@ -559,11 +638,16 @@ class TrackEventServices extends APIService { extra: { ...data, }, + user: user, }, }); } - async trackGitHubSyncEvent(data: any, eventName: GitHubSyncEventType): Promise { + async trackGitHubSyncEvent( + data: any, + eventName: GitHubSyncEventType, + user: ICurrentUserResponse | undefined + ): Promise { return this.request({ url: "/api/track-event", method: "POST", @@ -572,13 +656,15 @@ class TrackEventServices extends APIService { extra: { ...data, }, + user: user, }, }); } async trackIssueEstimateEvent( data: { estimate: IEstimate }, - eventName: IssueEstimateEventType + eventName: IssueEstimateEventType, + user: ICurrentUserResponse | undefined ): Promise { let payload: any; if (eventName === "ESTIMATE_DELETE") payload = data; @@ -601,11 +687,16 @@ class TrackEventServices extends APIService { extra: { ...payload, }, + user: user, }, }); } - async trackImporterEvent(data: any, eventName: ImporterEventType): Promise { + async trackImporterEvent( + data: any, + eventName: ImporterEventType, + user: ICurrentUserResponse | undefined + ): Promise { let payload: any; if (eventName === "GITHUB_IMPORTER_DELETE" || eventName === "JIRA_IMPORTER_DELETE") payload = data; @@ -627,11 +718,16 @@ class TrackEventServices extends APIService { extra: { ...payload, }, + user: user, }, }); } - async trackAnalyticsEvent(data: any, eventName: AnalyticsEventType): Promise { + async trackAnalyticsEvent( + data: any, + eventName: AnalyticsEventType, + user: ICurrentUserResponse | undefined + ): Promise { const payload = { ...data }; return this.request({ @@ -640,6 +736,7 @@ class TrackEventServices extends APIService { data: { eventName, extra: payload, + user: user, }, }); } diff --git a/apps/app/services/user.service.ts b/apps/app/services/user.service.ts index 5f0786b81..209d3cc4f 100644 --- a/apps/app/services/user.service.ts +++ b/apps/app/services/user.service.ts @@ -50,16 +50,19 @@ class UserService extends APIService { }); } - async updateUserOnBoard({ userRole }: any): Promise { + async updateUserOnBoard({ userRole }: any, user: ICurrentUserResponse | undefined): Promise { return this.patch("/api/users/me/onboard/", { is_onboarded: true, }) .then((response) => { if (trackEvent) - trackEventServices.trackUserOnboardingCompleteEvent({ - ...response.data, - user_role: userRole ?? "None", - }); + trackEventServices.trackUserOnboardingCompleteEvent( + { + ...response.data, + user_role: userRole ?? "None", + }, + user + ); return response?.data; }) .catch((error) => { diff --git a/apps/app/services/views.service.ts b/apps/app/services/views.service.ts index af2426a7c..e1d25925e 100644 --- a/apps/app/services/views.service.ts +++ b/apps/app/services/views.service.ts @@ -1,6 +1,7 @@ // services import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; +import { ICurrentUserResponse } from "types"; // types import { IView } from "types/views"; @@ -15,10 +16,15 @@ class ViewServices extends APIService { super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); } - async createView(workspaceSlug: string, projectId: string, data: IView): Promise { + async createView( + workspaceSlug: string, + projectId: string, + data: IView, + user: ICurrentUserResponse | undefined + ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`, data) .then((response) => { - if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_CREATE"); + if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_CREATE", user); return response?.data; }) .catch((error) => { @@ -30,11 +36,12 @@ class ViewServices extends APIService { workspaceSlug: string, projectId: string, viewId: string, - data: IView + data: IView, + user: ICurrentUserResponse | undefined ): Promise { return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data) .then((response) => { - if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE"); + if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE", user); return response?.data; }) .catch((error) => { @@ -46,14 +53,15 @@ class ViewServices extends APIService { workspaceSlug: string, projectId: string, viewId: string, - data: Partial + data: Partial, + user: ICurrentUserResponse | undefined ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data ) .then((response) => { - if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE"); + if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE", user); return response?.data; }) .catch((error) => { @@ -61,10 +69,15 @@ class ViewServices extends APIService { }); } - async deleteView(workspaceSlug: string, projectId: string, viewId: string): Promise { + async deleteView( + workspaceSlug: string, + projectId: string, + viewId: string, + user: ICurrentUserResponse | undefined + ): Promise { return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`) .then((response) => { - if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_DELETE"); + if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_DELETE", user); return response?.data; }) .catch((error) => { diff --git a/apps/app/services/workspace.service.ts b/apps/app/services/workspace.service.ts index 004240e5d..21ec7c1df 100644 --- a/apps/app/services/workspace.service.ts +++ b/apps/app/services/workspace.service.ts @@ -12,6 +12,7 @@ import { ILastActiveWorkspaceDetails, IWorkspaceSearchResults, IProductUpdateResponse, + ICurrentUserResponse, } from "types"; const trackEvent = @@ -38,10 +39,14 @@ class WorkspaceService extends APIService { }); } - async createWorkspace(data: Partial): Promise { + async createWorkspace( + data: Partial, + user: ICurrentUserResponse | undefined + ): Promise { return this.post("/api/workspaces/", data) .then((response) => { - if (trackEvent) trackEventServices.trackWorkspaceEvent(response.data, "CREATE_WORKSPACE"); + if (trackEvent) + trackEventServices.trackWorkspaceEvent(response.data, "CREATE_WORKSPACE", user); return response?.data; }) .catch((error) => { @@ -49,10 +54,15 @@ class WorkspaceService extends APIService { }); } - async updateWorkspace(workspaceSlug: string, data: Partial): Promise { + async updateWorkspace( + workspaceSlug: string, + data: Partial, + user: ICurrentUserResponse | undefined + ): Promise { return this.patch(`/api/workspaces/${workspaceSlug}/`, data) .then((response) => { - if (trackEvent) trackEventServices.trackWorkspaceEvent(response.data, "UPDATE_WORKSPACE"); + if (trackEvent) + trackEventServices.trackWorkspaceEvent(response.data, "UPDATE_WORKSPACE", user); return response?.data; }) .catch((error) => { @@ -60,11 +70,14 @@ class WorkspaceService extends APIService { }); } - async deleteWorkspace(workspaceSlug: string): Promise { + async deleteWorkspace( + workspaceSlug: string, + user: ICurrentUserResponse | undefined + ): Promise { return this.delete(`/api/workspaces/${workspaceSlug}/`) .then((response) => { if (trackEvent) - trackEventServices.trackWorkspaceEvent({ workspaceSlug }, "DELETE_WORKSPACE"); + trackEventServices.trackWorkspaceEvent({ workspaceSlug }, "DELETE_WORKSPACE", user); return response?.data; }) .catch((error) => { @@ -72,11 +85,15 @@ class WorkspaceService extends APIService { }); } - async inviteWorkspace(workspaceSlug: string, data: any): Promise { + async inviteWorkspace( + workspaceSlug: string, + data: any, + user: ICurrentUserResponse | undefined + ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/invite/`, data) .then((response) => { if (trackEvent) - trackEventServices.trackWorkspaceEvent(response.data, "WORKSPACE_USER_INVITE"); + trackEventServices.trackWorkspaceEvent(response.data, "WORKSPACE_USER_INVITE", user); return response?.data; }) .catch((error) => { @@ -84,7 +101,12 @@ class WorkspaceService extends APIService { }); } - async joinWorkspace(workspaceSlug: string, invitationId: string, data: any): Promise { + async joinWorkspace( + workspaceSlug: string, + invitationId: string, + data: any, + user: ICurrentUserResponse | undefined + ): Promise { return this.post( `/api/users/me/invitations/workspaces/${workspaceSlug}/${invitationId}/join/`, data, @@ -94,7 +116,11 @@ class WorkspaceService extends APIService { ) .then((response) => { if (trackEvent) - trackEventServices.trackWorkspaceEvent(response.data, "WORKSPACE_USER_INVITE_ACCEPT"); + trackEventServices.trackWorkspaceEvent( + response.data, + "WORKSPACE_USER_INVITE_ACCEPT", + user + ); return response?.data; }) .catch((error) => { diff --git a/apps/app/types/workspace.d.ts b/apps/app/types/workspace.d.ts index a88a0f15b..ee0458bfe 100644 --- a/apps/app/types/workspace.d.ts +++ b/apps/app/types/workspace.d.ts @@ -30,6 +30,7 @@ export interface IWorkspaceMemberInvitation { message: string; responded_at: Date; role: 5 | 10 | 15 | 20; + created_by_detail: IUser; workspace: IWorkspace; } From 1f3fdd5d0a9718e4d665b02f2664c4c84668d936 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 6 Jun 2023 21:36:13 +0530 Subject: [PATCH 53/65] feat: reset password page for self-hosted added (#1221) * feat: reset password page for self-hosted added * chore: change reset password workflow * dev: update email template for reset password * chore: updated restricted workspace slugs list --------- Co-authored-by: pablohashescobar --- .../emails/auth/forgot_password.html | 10 +- .../account/email-password-form.tsx | 116 ++++++++------- .../account/email-reset-password-form.tsx | 93 ++++++++++++ apps/app/components/account/index.ts | 3 +- .../workspace/create-workspace-form.tsx | 1 + apps/app/pages/reset-password.tsx | 132 ++++++++++++++++++ apps/app/services/user.service.ts | 23 +++ 7 files changed, 319 insertions(+), 59 deletions(-) create mode 100644 apps/app/components/account/email-reset-password-form.tsx create mode 100644 apps/app/pages/reset-password.tsx diff --git a/apiserver/templates/emails/auth/forgot_password.html b/apiserver/templates/emails/auth/forgot_password.html index 7c3ae585f..14a15e5b2 100644 --- a/apiserver/templates/emails/auth/forgot_password.html +++ b/apiserver/templates/emails/auth/forgot_password.html @@ -2,10 +2,10 @@

    Dear {{first_name}},

    - Welcome! Your account has been created. - Verify your email by clicking on the link below
    - {{forgot_password_url}} - successfully.

    + Here is the link to reset your password. +
    + Link: {{forgot_password_url}} +

    + Thank you

    - \ No newline at end of file diff --git a/apps/app/components/account/email-password-form.tsx b/apps/app/components/account/email-password-form.tsx index 7d56d67a0..920ec0829 100644 --- a/apps/app/components/account/email-password-form.tsx +++ b/apps/app/components/account/email-password-form.tsx @@ -1,6 +1,4 @@ -import React from "react"; - -import Link from "next/link"; +import React, { useState } from "react"; // react hook form import { useForm } from "react-hook-form"; @@ -8,6 +6,8 @@ import { useForm } from "react-hook-form"; import authenticationService from "services/authentication.service"; // hooks import useToast from "hooks/use-toast"; +// components +import { EmailResetPasswordForm } from "components/account"; // ui import { Input, SecondaryButton } from "components/ui"; // types @@ -18,7 +18,10 @@ type EmailPasswordFormValues = { }; export const EmailPasswordForm = ({ handleSignIn }: any) => { + const [isResettingPassword, setIsResettingPassword] = useState(false); + const { setToastAlert } = useToast(); + const { register, handleSubmit, @@ -58,59 +61,66 @@ export const EmailPasswordForm = ({ handleSignIn }: any) => { }); }); }; + return ( <> - -
    - - /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( - value - ) || "Email ID is not valid", - }} - error={errors.email} - placeholder="Enter your Email ID" - /> -
    -
    - -
    -
    -
    - - - Forgot your password? - - + {isResettingPassword ? ( + + ) : ( + +
    + + /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + value + ) || "Email ID is not valid", + }} + error={errors.email} + placeholder="Enter your Email ID" + />
    -
    -
    - - {isSubmitting ? "Signing in..." : "Sign In"} - -
    - +
    + +
    +
    +
    + +
    +
    +
    + + {isSubmitting ? "Signing in..." : "Sign In"} + +
    + + )} ); }; diff --git a/apps/app/components/account/email-reset-password-form.tsx b/apps/app/components/account/email-reset-password-form.tsx new file mode 100644 index 000000000..03ea69042 --- /dev/null +++ b/apps/app/components/account/email-reset-password-form.tsx @@ -0,0 +1,93 @@ +import React from "react"; + +// react hook form +import { useForm } from "react-hook-form"; +// services +import userService from "services/user.service"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { Input, PrimaryButton, SecondaryButton } from "components/ui"; +// types +type Props = { + setIsResettingPassword: React.Dispatch>; +}; + +export const EmailResetPasswordForm: React.FC = ({ setIsResettingPassword }) => { + const { setToastAlert } = useToast(); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { + email: "", + }, + mode: "onChange", + reValidateMode: "onChange", + }); + + const forgotPassword = async (formData: any) => { + const payload = { + email: formData.email, + }; + + await userService + .forgotPassword(payload) + .then(() => + setToastAlert({ + type: "success", + title: "Success!", + message: "Password reset link has been sent to your email address.", + }) + ) + .catch((err) => { + if (err.status === 400) + setToastAlert({ + type: "error", + title: "Error!", + message: "Please check the Email ID entered.", + }); + else + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong. Please try again.", + }); + }); + }; + + return ( +
    +
    + + /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + value + ) || "Email ID is not valid", + }} + error={errors.email} + placeholder="Enter registered Email ID" + /> +
    +
    + setIsResettingPassword(false)} + > + Go Back + + + {isSubmitting ? "Sending link..." : "Send reset link"} + +
    +
    + ); +}; diff --git a/apps/app/components/account/index.ts b/apps/app/components/account/index.ts index d0e5e4dfa..dee3e3251 100644 --- a/apps/app/components/account/index.ts +++ b/apps/app/components/account/index.ts @@ -1,4 +1,5 @@ -export * from "./google-login"; export * from "./email-code-form"; export * from "./email-password-form"; +export * from "./email-reset-password-form"; export * from "./github-login-button"; +export * from "./google-login"; diff --git a/apps/app/components/workspace/create-workspace-form.tsx b/apps/app/components/workspace/create-workspace-form.tsx index e3da82c10..213232d24 100644 --- a/apps/app/components/workspace/create-workspace-form.tsx +++ b/apps/app/components/workspace/create-workspace-form.tsx @@ -36,6 +36,7 @@ const restrictedUrls = [ "invitations", "magic-sign-in", "onboarding", + "reset-password", "signin", "workspace-member-invitation", "404", diff --git a/apps/app/pages/reset-password.tsx b/apps/app/pages/reset-password.tsx new file mode 100644 index 000000000..86ab556b1 --- /dev/null +++ b/apps/app/pages/reset-password.tsx @@ -0,0 +1,132 @@ +import React from "react"; + +import { useRouter } from "next/router"; +import Image from "next/image"; + +// react-hook-form +import { useForm } from "react-hook-form"; +// hooks +import useToast from "hooks/use-toast"; +// services +import userService from "services/user.service"; +// layouts +import DefaultLayout from "layouts/default-layout"; +// ui +import { Input, SecondaryButton } from "components/ui"; +// icons +import Logo from "public/logo.png"; +// types +import type { NextPage } from "next"; + +type FormData = { + password: string; + confirmPassword: string; +}; + +const ResetPasswordPage: NextPage = () => { + const router = useRouter(); + const { uidb64, token } = router.query; + + const { setToastAlert } = useToast(); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm(); + + const onSubmit = async (formData: FormData) => { + if (!uidb64 || !token) return; + + if (formData.password !== formData.confirmPassword) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Passwords do not match.", + }); + + return; + } + + const payload = { + new_password: formData.password, + confirm_password: formData.confirmPassword, + }; + + await userService + .resetPassword(uidb64.toString(), token.toString(), payload) + .then(() => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Password reset successfully. You can now login with your new password.", + }); + router.push("/"); + }) + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong. Please try again.", + }) + ); + }; + + return ( + +
    +
    +
    +
    + Plane Web Logo +

    + Reset your password +

    +
    +
    +
    +
    + +
    +
    + +
    +
    + + {isSubmitting ? "Resetting..." : "Reset"} + +
    +
    +
    +
    +
    +
    +
    + ); +}; + +export default ResetPasswordPage; diff --git a/apps/app/services/user.service.ts b/apps/app/services/user.service.ts index 209d3cc4f..c6b8494a4 100644 --- a/apps/app/services/user.service.ts +++ b/apps/app/services/user.service.ts @@ -92,6 +92,29 @@ class UserService extends APIService { throw error?.response?.data; }); } + + async forgotPassword(data: { email: string }): Promise { + return this.post(`/api/forgot-password/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async resetPassword( + uidb64: string, + token: string, + data: { + new_password: string; + confirm_password: string; + } + ): Promise { + return this.post(`/api/reset-password/${uidb64}/${token}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } export default new UserService(); From 684df96969f881c5367ab9f39d68fb3384d921fa Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 7 Jun 2023 01:56:21 +0530 Subject: [PATCH 54/65] chore: replace nextjs Image element (#1227) --- apps/app/.eslintrc.js | 3 + .../scope-and-demand/leaderboard.tsx | 10 +- apps/app/components/core/feeds.tsx | 8 +- .../components/core/image-picker-popover.tsx | 16 +-- .../cycles/active-cycle-details.tsx | 3 +- .../components/cycles/active-cycle-stats.tsx | 9 +- apps/app/components/cycles/sidebar.tsx | 3 +- .../components/cycles/single-cycle-card.tsx | 3 +- .../components/cycles/single-cycle-list.tsx | 2 +- .../integration/github/single-user-select.tsx | 7 +- apps/app/components/issues/activity.tsx | 5 +- .../issues/comment/comment-card.tsx | 5 +- apps/app/components/onboarding/workspace.tsx | 4 +- .../project/create-project-modal.tsx | 11 +- .../project/single-project-card.tsx | 7 +- apps/app/components/search-listbox/index.tsx | 11 +- apps/app/components/ui/avatar.tsx | 6 +- .../components/workspace/sidebar-dropdown.tsx | 23 ++-- .../workspace/single-invitation.tsx | 115 ++++++++---------- .../[workspaceSlug]/me/profile/index.tsx | 11 +- .../projects/[projectId]/settings/control.tsx | 17 +-- .../projects/[projectId]/settings/index.tsx | 7 +- .../projects/[projectId]/settings/members.tsx | 7 +- .../pages/[workspaceSlug]/settings/index.tsx | 8 +- .../[workspaceSlug]/settings/members.tsx | 6 +- 25 files changed, 121 insertions(+), 186 deletions(-) diff --git a/apps/app/.eslintrc.js b/apps/app/.eslintrc.js index c8df60750..38e6a5f4c 100644 --- a/apps/app/.eslintrc.js +++ b/apps/app/.eslintrc.js @@ -1,4 +1,7 @@ module.exports = { root: true, extends: ["custom"], + rules: { + "@next/next/no-img-element": "off", + }, }; diff --git a/apps/app/components/analytics/scope-and-demand/leaderboard.tsx b/apps/app/components/analytics/scope-and-demand/leaderboard.tsx index 9e28d0f3e..855f9eff4 100644 --- a/apps/app/components/analytics/scope-and-demand/leaderboard.tsx +++ b/apps/app/components/analytics/scope-and-demand/leaderboard.tsx @@ -1,5 +1,3 @@ -import Image from "next/image"; - type Props = { users: { avatar: string | null; @@ -23,12 +21,10 @@ export const AnalyticsLeaderboard: React.FC = ({ users, title }) => ( >
    {user && user.avatar && user.avatar !== "" ? ( -
    - + {user.email
    diff --git a/apps/app/components/core/feeds.tsx b/apps/app/components/core/feeds.tsx index bd0e4c6bf..cce482da5 100644 --- a/apps/app/components/core/feeds.tsx +++ b/apps/app/components/core/feeds.tsx @@ -1,5 +1,6 @@ import React from "react"; -import Image from "next/image"; + +import Link from "next/link"; // icons import { @@ -22,7 +23,6 @@ import { renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper" import { addSpaceIfCamelCase } from "helpers/string.helper"; // types import RemirrorRichTextEditor from "components/rich-text-editor"; -import Link from "next/link"; const activityDetails: { [key: string]: { @@ -206,7 +206,7 @@ export const Feeds: React.FC = ({ activities }) => (
    {activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? ( - {activity.actor_detail.first_name} = ({ activities }) => ( activityDetails[activity.field as keyof typeof activityDetails]?.icon ) : activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? ( - {activity.actor_detail.first_name} = ({ label, value, onChange }) onChange={(e) => setFormData({ ...formData, search: e.target.value })} placeholder="Search for images" /> - setSearchParams(formData.search)} - className="bg-indigo-600" - size="sm" - > + setSearchParams(formData.search)} size="sm"> Search
    @@ -123,12 +115,10 @@ export const ImagePickerPopover: React.FC = ({ label, value, onChange }) key={image.id} className="relative col-span-2 aspect-video md:col-span-1" > - {image.alt_description} { setIsOpen(false); onChange(image.urls.regular); diff --git a/apps/app/components/cycles/active-cycle-details.tsx b/apps/app/components/cycles/active-cycle-details.tsx index 8dfe36a53..0bb5a6bcb 100644 --- a/apps/app/components/cycles/active-cycle-details.tsx +++ b/apps/app/components/cycles/active-cycle-details.tsx @@ -1,7 +1,6 @@ import React from "react"; import Link from "next/link"; -import Image from "next/image"; import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; @@ -301,7 +300,7 @@ export const ActiveCycleDetails: React.FC = ({ cycle, isComple
    {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( - = ({ issues }) => { -
    - + = ({
    {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( - = ({
    Creator:
    {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( - = ({
    {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( - = ({ collaborator, index, users,
    - {`${collaborator.login}
    diff --git a/apps/app/components/issues/activity.tsx b/apps/app/components/issues/activity.tsx index 1ccafae56..3b9ecc853 100644 --- a/apps/app/components/issues/activity.tsx +++ b/apps/app/components/issues/activity.tsx @@ -1,6 +1,7 @@ import React from "react"; + import { useRouter } from "next/router"; -import Image from "next/image"; + import useSWR from "swr"; // icons @@ -346,7 +347,7 @@ export const IssueActivitySection: React.FC = ({ user }) => { ?.icon ) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? ( - {activityItem.actor_detail.first_name} = ({ comment, onSubmit, handleCommentD
    {comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? ( - {comment.actor_detail.first_name} ) : (
    = ({ setStep, setWorkspace, user }) => {
    {invitation.workspace.logo && invitation.workspace.logo !== "" ? ( - = (props) => { leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > -
    +
    {watch("cover_image") !== null && ( - cover image )} diff --git a/apps/app/components/project/single-project-card.tsx b/apps/app/components/project/single-project-card.tsx index c2844212d..66ef6aa2a 100644 --- a/apps/app/components/project/single-project-card.tsx +++ b/apps/app/components/project/single-project-card.tsx @@ -2,7 +2,6 @@ import React from "react"; import { useRouter } from "next/router"; import Link from "next/link"; -import Image from "next/image"; import { mutate } from "swr"; @@ -142,15 +141,13 @@ export const SingleProjectCard: React.FC = ({
    - {project.name}
    {!hasJoined ? ( diff --git a/apps/app/components/search-listbox/index.tsx b/apps/app/components/search-listbox/index.tsx index ef7c33634..cbbc6cf5b 100644 --- a/apps/app/components/search-listbox/index.tsx +++ b/apps/app/components/search-listbox/index.tsx @@ -1,6 +1,5 @@ import React, { useState } from "react"; -import Image from "next/image"; import { useRouter } from "next/router"; import useSWR from "swr"; @@ -62,12 +61,10 @@ const SearchListbox: React.FC = ({ if (user.member.avatar && user.member.avatar !== "") { return (
    - avatar
    ); @@ -151,7 +148,9 @@ const SearchListbox: React.FC = ({ )) ) : ( -

    No {title.toLowerCase()} found

    +

    + No {title.toLowerCase()} found +

    ) ) : (

    Loading...

    diff --git a/apps/app/components/ui/avatar.tsx b/apps/app/components/ui/avatar.tsx index 7d2d9a575..06534c121 100644 --- a/apps/app/components/ui/avatar.tsx +++ b/apps/app/components/ui/avatar.tsx @@ -44,11 +44,9 @@ export const Avatar: React.FC = ({ width: width, }} > - {user.first_name}
    diff --git a/apps/app/components/workspace/sidebar-dropdown.tsx b/apps/app/components/workspace/sidebar-dropdown.tsx index 680b688ed..6e5774434 100644 --- a/apps/app/components/workspace/sidebar-dropdown.tsx +++ b/apps/app/components/workspace/sidebar-dropdown.tsx @@ -1,9 +1,10 @@ import { Fragment } from "react"; -import { Menu, Transition } from "@headlessui/react"; + import { useRouter } from "next/router"; -import Image from "next/image"; import Link from "next/link"; -import { CheckIcon, PlusIcon } from "@heroicons/react/24/outline"; + +// headless ui +import { Menu, Transition } from "@headlessui/react"; // hooks import useUser from "hooks/use-user"; import useTheme from "hooks/use-theme"; @@ -14,7 +15,9 @@ import userService from "services/user.service"; import authenticationService from "services/authentication.service"; // components import { Avatar, Loader } from "components/ui"; -// helper +// icons +import { CheckIcon, PlusIcon } from "@heroicons/react/24/outline"; +// helpers import { truncateText } from "helpers/string.helper"; // types import { IWorkspace } from "types"; @@ -94,12 +97,10 @@ export const WorkspaceSidebarDropdown = () => { >
    {activeWorkspace?.logo && activeWorkspace.logo !== "" ? ( - Workspace Logo ) : ( activeWorkspace?.name?.charAt(0) ?? "..." @@ -155,12 +156,10 @@ export const WorkspaceSidebarDropdown = () => {
    {workspace?.logo && workspace.logo !== "" ? ( - Workspace Logo ) : ( workspace?.name?.charAt(0) ?? "..." diff --git a/apps/app/components/workspace/single-invitation.tsx b/apps/app/components/workspace/single-invitation.tsx index 6d89c7675..73f6d4655 100644 --- a/apps/app/components/workspace/single-invitation.tsx +++ b/apps/app/components/workspace/single-invitation.tsx @@ -1,8 +1,5 @@ -// next +// helpers import { getFirstCharacters, truncateText } from "helpers/string.helper"; -import Image from "next/image"; -// react -import { useState } from "react"; // types import { IWorkspaceMemberInvitation } from "types"; @@ -16,65 +13,57 @@ const SingleInvitation: React.FC = ({ invitation, invitationsRespond, handleInvitation, -}) => { - const [isChecked, setIsChecked] = useState(invitationsRespond.includes(invitation.id)); - - return ( - <> -
  • -
  • + -
  • - - ); -}; + )} +
    +
    +
    +
    {truncateText(invitation.workspace.name, 30)}
    +

    + Invited by{" "} + {invitation.created_by_detail + ? invitation.created_by_detail.first_name + : invitation.workspace.owner.first_name} +

    +
    +
    + +
    + + +); export default SingleInvitation; diff --git a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx index c4ab6918f..8c1f9cd1e 100644 --- a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx +++ b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx @@ -1,7 +1,5 @@ import React, { useEffect, useState } from "react"; -import Image from "next/image"; - // react-hook-form import { Controller, useForm } from "react-hook-form"; // services @@ -168,14 +166,11 @@ const Profile: NextPage = () => {
    ) : (
    - {myProfile.first_name} setIsImageUploadModalOpen(true)} - priority + alt={myProfile.first_name} />
    )} diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/control.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/control.tsx index 5dc398e66..c6e02d726 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/control.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/control.tsx @@ -1,7 +1,6 @@ import React, { useEffect } from "react"; import { useRouter } from "next/router"; -import Image from "next/image"; import useSWR, { mutate } from "swr"; @@ -138,12 +137,10 @@ const ControlSettings: NextPage = () => {
    {person.member.avatar && person.member.avatar !== "" ? (
    - avatar
    ) : ( @@ -201,12 +198,10 @@ const ControlSettings: NextPage = () => {
    {person.member.avatar && person.member.avatar !== "" ? (
    - avatar
    ) : ( diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx index 14e20811a..92b55cebe 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from "react"; -import Image from "next/image"; import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; @@ -256,12 +255,10 @@ const GeneralSettings: NextPage = () => { {watch("cover_image") ? (
    - {projectDetails?.name
    {
    {member.avatar && member.avatar !== "" ? ( - {member.first_name} ) : member.first_name !== "" ? ( member.first_name.charAt(0) diff --git a/apps/app/pages/[workspaceSlug]/settings/index.tsx b/apps/app/pages/[workspaceSlug]/settings/index.tsx index 0400ecaee..21865ef02 100644 --- a/apps/app/pages/[workspaceSlug]/settings/index.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/index.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from "react"; -import Image from "next/image"; import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; @@ -190,13 +189,10 @@ const WorkspaceSettings: NextPage = () => {